mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-14 01:20:41 +00:00
feat(selling): add Proforma Invoice doctype and server API
- Submittable, non-accounting Proforma Invoice + Proforma Invoice Item child doctype (in_create; posts no GL/stock, stores item + qty only) - Server API: pending-qty aggregation per Sales Order line (issued proformas only), make_proforma_invoice (sole creation path, gated on the settings toggle), PDF rendered from an in-memory qty-adjusted copy of the Sales Order and attached, send_proforma_email - Non-blocking proforma_qty write-back to the Sales Order on submit/cancel
This commit is contained in:
234
erpnext/selling/doctype/proforma_invoice/proforma_invoice.json
Normal file
234
erpnext/selling/doctype/proforma_invoice/proforma_invoice.json
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "naming_series:",
|
||||||
|
"creation": "2026-07-16 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"naming_series",
|
||||||
|
"sales_order",
|
||||||
|
"customer",
|
||||||
|
"customer_name",
|
||||||
|
"column_break_header",
|
||||||
|
"proforma_date",
|
||||||
|
"company",
|
||||||
|
"currency",
|
||||||
|
"items_section",
|
||||||
|
"items",
|
||||||
|
"total_qty",
|
||||||
|
"grand_total",
|
||||||
|
"print_section",
|
||||||
|
"print_format",
|
||||||
|
"letter_head",
|
||||||
|
"proforma_pdf",
|
||||||
|
"status_section",
|
||||||
|
"status",
|
||||||
|
"sent_on",
|
||||||
|
"emailed_to",
|
||||||
|
"amended_from"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Series",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "PRO-.YYYY.-",
|
||||||
|
"print_hide": 1,
|
||||||
|
"reqd": 1,
|
||||||
|
"set_only_once": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sales_order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Sales Order",
|
||||||
|
"options": "Sales Order",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_order.customer",
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Customer",
|
||||||
|
"options": "Customer",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "customer.customer_name",
|
||||||
|
"fieldname": "customer_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_global_search": 1,
|
||||||
|
"label": "Customer Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_header",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Today",
|
||||||
|
"fieldname": "proforma_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Date",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_order.company",
|
||||||
|
"fieldname": "company",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Company",
|
||||||
|
"options": "Company",
|
||||||
|
"read_only": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "sales_order.currency",
|
||||||
|
"fieldname": "currency",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Currency",
|
||||||
|
"options": "Currency",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "items_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Items"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "items",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Items",
|
||||||
|
"options": "Proforma Invoice Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "total_qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Total Quantity",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "grand_total",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Grand Total",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "print_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Print Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "print_format",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Print Format",
|
||||||
|
"options": "Print Format",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "letter_head",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Letter Head",
|
||||||
|
"options": "Letter Head",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "proforma_pdf",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"label": "Proforma PDF",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status_section",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Draft",
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Draft\nIssued\nCancelled",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "sent_on",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Sent On",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "emailed_to",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "Emailed To",
|
||||||
|
"no_copy": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "amended_from",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"ignore_user_permissions": 1,
|
||||||
|
"label": "Amended From",
|
||||||
|
"no_copy": 1,
|
||||||
|
"options": "Proforma Invoice",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"in_create": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"is_submittable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-07-16 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Selling",
|
||||||
|
"name": "Proforma Invoice",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales User",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"amend": 1,
|
||||||
|
"cancel": 1,
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Sales Manager",
|
||||||
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "customer_name"
|
||||||
|
}
|
||||||
217
erpnext/selling/doctype/proforma_invoice/proforma_invoice.py
Normal file
217
erpnext/selling/doctype/proforma_invoice/proforma_invoice.py
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import flt, now
|
||||||
|
from frappe.utils.file_manager import save_file
|
||||||
|
|
||||||
|
|
||||||
|
class ProformaInvoice(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
from erpnext.selling.doctype.proforma_invoice_item.proforma_invoice_item import (
|
||||||
|
ProformaInvoiceItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
amended_from: DF.Link | None
|
||||||
|
company: DF.Link
|
||||||
|
currency: DF.Link | None
|
||||||
|
customer: DF.Link | None
|
||||||
|
customer_name: DF.Data | None
|
||||||
|
emailed_to: DF.SmallText | None
|
||||||
|
grand_total: DF.Currency
|
||||||
|
items: DF.Table[ProformaInvoiceItem]
|
||||||
|
letter_head: DF.Link | None
|
||||||
|
naming_series: DF.Literal["PRO-.YYYY.-"]
|
||||||
|
print_format: DF.Link | None
|
||||||
|
proforma_date: DF.Date
|
||||||
|
proforma_pdf: DF.Attach | None
|
||||||
|
sales_order: DF.Link
|
||||||
|
sent_on: DF.Datetime | None
|
||||||
|
status: DF.Literal["Draft", "Issued", "Cancelled"]
|
||||||
|
total_qty: DF.Float
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
validate_feature_enabled()
|
||||||
|
self.set_total_qty()
|
||||||
|
self.warn_on_over_proforma_qty()
|
||||||
|
|
||||||
|
def before_submit(self) -> None:
|
||||||
|
self.status = "Issued"
|
||||||
|
|
||||||
|
def on_submit(self) -> None:
|
||||||
|
self.update_proforma_qty_in_sales_order()
|
||||||
|
self.generate_and_attach_pdf()
|
||||||
|
|
||||||
|
def on_cancel(self) -> None:
|
||||||
|
self.status = "Cancelled"
|
||||||
|
self.update_proforma_qty_in_sales_order()
|
||||||
|
|
||||||
|
def set_total_qty(self) -> None:
|
||||||
|
self.total_qty = sum(flt(item.qty) for item in self.items)
|
||||||
|
|
||||||
|
def warn_on_over_proforma_qty(self) -> None:
|
||||||
|
"""Soft-warn (never block) if a line exceeds its pending proforma qty."""
|
||||||
|
pending = {row["so_detail"]: row["pending_qty"] for row in get_pending_proforma_qty(self.sales_order)}
|
||||||
|
for item in self.items:
|
||||||
|
if flt(item.qty) > flt(pending.get(item.so_detail)) + 0.0001:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Qty {0} for {1} exceeds the pending proforma qty {2}.").format(
|
||||||
|
flt(item.qty), item.item_code, flt(pending.get(item.so_detail))
|
||||||
|
),
|
||||||
|
indicator="orange",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_proforma_qty_in_sales_order(self) -> None:
|
||||||
|
"""Refresh the non-blocking, cosmetic proforma_qty counter on each SO item."""
|
||||||
|
qty_map = get_proformed_qty_map(self.sales_order)
|
||||||
|
for name in frappe.get_all("Sales Order Item", filters={"parent": self.sales_order}, pluck="name"):
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Sales Order Item", name, "proforma_qty", flt(qty_map.get(name)), update_modified=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_and_attach_pdf(self) -> None:
|
||||||
|
if self.proforma_pdf:
|
||||||
|
return
|
||||||
|
printed = self.render_pdf()
|
||||||
|
file = save_file(printed["fname"], printed["fcontent"], self.doctype, self.name, is_private=1)
|
||||||
|
self.db_set("proforma_pdf", file.file_url)
|
||||||
|
|
||||||
|
def render_pdf(self) -> dict:
|
||||||
|
"""Render the proforma PDF from an in-memory, qty-adjusted copy of the Sales Order.
|
||||||
|
|
||||||
|
The Sales Order copy is never saved; it exists only to reuse the standard tax/total
|
||||||
|
calculation and print format so the proforma shows accurate gross for the partial qty.
|
||||||
|
"""
|
||||||
|
sales_order = frappe.get_doc("Sales Order", self.sales_order)
|
||||||
|
qty_by_detail = {item.so_detail: item.qty for item in self.items}
|
||||||
|
sales_order.items = [item for item in sales_order.items if item.name in qty_by_detail]
|
||||||
|
for item in sales_order.items:
|
||||||
|
item.qty = qty_by_detail[item.name]
|
||||||
|
sales_order.run_method("calculate_taxes_and_totals")
|
||||||
|
sales_order.proforma_no = self.name
|
||||||
|
sales_order.proforma_date = self.proforma_date
|
||||||
|
self.db_set("grand_total", sales_order.grand_total)
|
||||||
|
return frappe.attach_print(
|
||||||
|
"Sales Order",
|
||||||
|
sales_order.name,
|
||||||
|
doc=sales_order,
|
||||||
|
file_name=self.name,
|
||||||
|
print_format=self.print_format,
|
||||||
|
letterhead=self.letter_head,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_pending_proforma_qty(sales_order: str) -> list[dict]:
|
||||||
|
"""Per-SO-line pending proforma qty = ordered qty minus already issued proforma qty."""
|
||||||
|
sales_order_doc = frappe.get_doc("Sales Order", sales_order)
|
||||||
|
proformed = get_proformed_qty_map(sales_order)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"item_name": item.item_name,
|
||||||
|
"uom": item.uom,
|
||||||
|
"so_detail": item.name,
|
||||||
|
"so_qty": flt(item.qty),
|
||||||
|
"pending_qty": flt(item.qty) - flt(proformed.get(item.name)),
|
||||||
|
}
|
||||||
|
for item in sales_order_doc.items
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_proformed_qty_map(sales_order: str) -> dict[str, float]:
|
||||||
|
"""Sum of issued (docstatus = 1) proforma qty per Sales Order Item row."""
|
||||||
|
proformas = frappe.get_all(
|
||||||
|
"Proforma Invoice", filters={"sales_order": sales_order, "docstatus": 1}, pluck="name"
|
||||||
|
)
|
||||||
|
if not proformas:
|
||||||
|
return {}
|
||||||
|
item = frappe.qb.DocType("Proforma Invoice Item")
|
||||||
|
rows = (
|
||||||
|
frappe.qb.from_(item)
|
||||||
|
.select(item.so_detail, Sum(item.qty).as_("qty"))
|
||||||
|
.where(item.parent.isin(proformas))
|
||||||
|
.groupby(item.so_detail)
|
||||||
|
).run(as_dict=True)
|
||||||
|
return {row.so_detail: flt(row.qty) for row in rows}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def make_proforma_invoice(
|
||||||
|
sales_order: str,
|
||||||
|
items: str,
|
||||||
|
naming_series: str | None = None,
|
||||||
|
print_format: str | None = None,
|
||||||
|
letter_head: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""The sole creation path for a Proforma Invoice (the doctype is `in_create`)."""
|
||||||
|
validate_feature_enabled()
|
||||||
|
selected = frappe.parse_json(items)
|
||||||
|
sales_order_doc = frappe.get_doc("Sales Order", sales_order)
|
||||||
|
so_items = {item.name: item for item in sales_order_doc.items}
|
||||||
|
|
||||||
|
proforma = frappe.new_doc("Proforma Invoice")
|
||||||
|
proforma.sales_order = sales_order
|
||||||
|
if naming_series:
|
||||||
|
proforma.naming_series = naming_series
|
||||||
|
proforma.print_format = print_format or frappe.db.get_single_value(
|
||||||
|
"Selling Settings", "default_proforma_print_format"
|
||||||
|
)
|
||||||
|
proforma.letter_head = letter_head
|
||||||
|
|
||||||
|
for row in selected:
|
||||||
|
qty = flt(row.get("qty"))
|
||||||
|
so_item = so_items.get(row.get("so_detail"))
|
||||||
|
if qty <= 0 or not so_item:
|
||||||
|
continue
|
||||||
|
proforma.append(
|
||||||
|
"items",
|
||||||
|
{
|
||||||
|
"item_code": so_item.item_code,
|
||||||
|
"item_name": so_item.item_name,
|
||||||
|
"uom": so_item.uom,
|
||||||
|
"qty": qty,
|
||||||
|
"so_detail": so_item.name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not proforma.items:
|
||||||
|
frappe.throw(_("Please enter a quantity for at least one item."))
|
||||||
|
|
||||||
|
proforma.insert()
|
||||||
|
proforma.submit()
|
||||||
|
return proforma.name
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def send_proforma_email(proforma_name: str, recipients: str) -> None:
|
||||||
|
proforma = frappe.get_doc("Proforma Invoice", proforma_name)
|
||||||
|
if not proforma.proforma_pdf:
|
||||||
|
frappe.throw(_("This Proforma Invoice has no PDF to send."))
|
||||||
|
|
||||||
|
file_name = frappe.db.get_value("File", {"file_url": proforma.proforma_pdf}, "name")
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[email.strip() for email in recipients.split(",") if email.strip()],
|
||||||
|
subject=_("Proforma Invoice {0}").format(proforma.name),
|
||||||
|
message=_("Please find attached the proforma invoice {0}.").format(proforma.name),
|
||||||
|
attachments=[{"fid": file_name}],
|
||||||
|
)
|
||||||
|
proforma.db_set("sent_on", now())
|
||||||
|
proforma.db_set("emailed_to", recipients)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_feature_enabled() -> None:
|
||||||
|
if not frappe.db.get_single_value("Selling Settings", "enable_proforma_invoice"):
|
||||||
|
frappe.throw(_("Proforma Invoice is not enabled in Selling Settings."))
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2026-07-16 00:00:00.000000",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"item_code",
|
||||||
|
"item_name",
|
||||||
|
"column_break_qty",
|
||||||
|
"qty",
|
||||||
|
"uom",
|
||||||
|
"so_detail"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"columns": 4,
|
||||||
|
"fieldname": "item_code",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Code",
|
||||||
|
"options": "Item",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fetch_from": "item_code.item_name",
|
||||||
|
"fieldname": "item_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Item Name",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_qty",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"columns": 2,
|
||||||
|
"fieldname": "qty",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Quantity",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "uom",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "UOM",
|
||||||
|
"options": "UOM",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "so_detail",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Sales Order Item",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2026-07-16 00:00:00.000000",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Selling",
|
||||||
|
"name": "Proforma Invoice Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "creation",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class ProformaInvoiceItem(Document):
|
||||||
|
# begin: auto-generated types
|
||||||
|
# This code is auto-generated. Do not modify anything in this block.
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from frappe.types import DF
|
||||||
|
|
||||||
|
item_code: DF.Link
|
||||||
|
item_name: DF.Data | None
|
||||||
|
parent: DF.Data
|
||||||
|
parentfield: DF.Data
|
||||||
|
parenttype: DF.Data
|
||||||
|
qty: DF.Float
|
||||||
|
so_detail: DF.Data | None
|
||||||
|
uom: DF.Link | None
|
||||||
|
# end: auto-generated types
|
||||||
|
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user