From 1da4657530e15678ce221fca0f9dab8ee8473fe3 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2026 17:44:04 +0530 Subject: [PATCH] feat(selling): option to hide item qty on amount-based proforma print Add a "Hide Item Quantity in Print" option (Amount basis only) that omits the qty and rate columns from the printed proforma, for a clean value-based document that shows only item and amount. --- erpnext/public/js/sales_order_proforma.js | 7 +++++++ .../proforma_invoice/proforma_invoice.json | 10 +++++++++ .../proforma_invoice/proforma_invoice.py | 7 ++++++- .../proforma_invoice/test_proforma_invoice.py | 21 +++++++++++++++++++ .../proforma_invoice/proforma_invoice.json | 2 +- 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/sales_order_proforma.js b/erpnext/public/js/sales_order_proforma.js index 3363ffefc66..314e0e803ec 100644 --- a/erpnext/public/js/sales_order_proforma.js +++ b/erpnext/public/js/sales_order_proforma.js @@ -93,6 +93,12 @@ Object.assign(erpnext.proforma, { default: "Quantity", onchange: () => this.toggle_basis(dialog), }, + { + fieldname: "hide_item_qty", + fieldtype: "Check", + label: __("Hide Item Quantity in Print"), + depends_on: 'eval:doc.based_on=="Amount"', + }, { fieldname: "items", fieldtype: "Table", @@ -212,6 +218,7 @@ Object.assign(erpnext.proforma, { sales_order: frm.doc.name, items: JSON.stringify(items), based_on: values.based_on, + hide_item_qty: values.hide_item_qty ? 1 : 0, naming_series: values.naming_series, print_format: values.print_format, letter_head: values.letter_head, diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json index a8d6e035015..3b698d6d1bb 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.json @@ -14,6 +14,7 @@ "company", "currency", "based_on", + "hide_item_qty", "items_section", "items", "total_qty", @@ -105,6 +106,15 @@ "options": "Quantity\nAmount", "read_only": 1 }, + { + "default": "0", + "depends_on": "eval:doc.based_on==\"Amount\"", + "description": "Hide the item quantity and rate on the printed proforma.", + "fieldname": "hide_item_qty", + "fieldtype": "Check", + "label": "Hide Item Quantity in Print", + "read_only": 1 + }, { "fieldname": "items_section", "fieldtype": "Section Break", diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py index 956dcbc0b10..d633c7426d0 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py @@ -30,6 +30,7 @@ class ProformaInvoice(Document): customer_name: DF.Data | None emailed_to: DF.SmallText | None grand_total: DF.Currency + hide_item_qty: DF.Check items: DF.Table[ProformaInvoiceItem] letter_head: DF.Link | None naming_series: DF.Literal["PRO-.YYYY.-"] @@ -84,6 +85,7 @@ class ProformaInvoice(Document): sales_order.run_method("calculate_taxes_and_totals") sales_order.proforma_no = self.name sales_order.proforma_date = self.proforma_date + sales_order.hide_item_qty = self.hide_item_qty self.db_set("grand_total", sales_order.grand_total) return frappe.attach_print( "Sales Order", @@ -138,6 +140,7 @@ def make_proforma_invoice( sales_order: str, items: str, based_on: str = "Quantity", + hide_item_qty: bool | int = 0, naming_series: str | None = None, print_format: str | None = None, letter_head: str | None = None, @@ -145,7 +148,8 @@ def make_proforma_invoice( """The sole creation path for a Proforma Invoice (the doctype is `in_create`). `based_on` decides what the user edited per line: "Quantity" (rate fixed, amount = qty x rate) - or "Amount" (qty fixed at ordered, rate derived so the line totals the entered amount). + or "Amount" (both qty and amount entered, rate derived). `hide_item_qty` (Amount basis only) + hides the qty and rate on the printed proforma for a clean value-based document. """ validate_feature_enabled() selected = frappe.parse_json(items) @@ -155,6 +159,7 @@ def make_proforma_invoice( proforma = frappe.new_doc("Proforma Invoice") proforma.sales_order = sales_order proforma.based_on = based_on + proforma.hide_item_qty = 1 if (based_on == "Amount" and int(hide_item_qty or 0)) else 0 if naming_series: proforma.naming_series = naming_series proforma.print_format = print_format or frappe.db.get_single_value( diff --git a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py index 5ab30691208..a5e1789bb85 100644 --- a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py @@ -112,6 +112,27 @@ class TestProformaInvoice(ERPNextTestSuite): self.assertEqual(flt(data["proformed_qty"]), 3) self.assertEqual(flt(data["proformed_amount"]), 300) + def test_hide_item_qty_only_applies_to_amount_basis(self): + sales_order = make_sales_order(qty=10) + so_detail = sales_order.items[0].name + + amount_based = make_proforma_invoice( + sales_order.name, + json.dumps([{"so_detail": so_detail, "qty": 5, "amount": 250}]), + based_on="Amount", + hide_item_qty=1, + ) + self.assertEqual(frappe.db.get_value("Proforma Invoice", amount_based, "hide_item_qty"), 1) + + # ignored outside Amount basis + qty_based = make_proforma_invoice( + sales_order.name, + json.dumps([{"so_detail": so_detail, "qty": 4}]), + based_on="Quantity", + hide_item_qty=1, + ) + self.assertEqual(frappe.db.get_value("Proforma Invoice", qty_based, "hide_item_qty"), 0) + def test_feature_toggle_is_enforced(self): sales_order = make_sales_order(qty=10) frappe.db.set_single_value("Selling Settings", "enable_proforma_invoice", 0) diff --git a/erpnext/selling/print_format/proforma_invoice/proforma_invoice.json b/erpnext/selling/print_format/proforma_invoice/proforma_invoice.json index 52417275cdf..8ef8184928d 100644 --- a/erpnext/selling/print_format/proforma_invoice/proforma_invoice.json +++ b/erpnext/selling/print_format/proforma_invoice/proforma_invoice.json @@ -9,7 +9,7 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 0, - "html": "
\n\t\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
\n\t\t\t\t

{{ _(\"PROFORMA INVOICE\") }}

\n\t\t\t\t
{{ doc.company }}
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
{{ _(\"Proforma No\") }}{{ doc.proforma_no or doc.name }}
{{ _(\"Date\") }}{{ frappe.utils.formatdate(doc.proforma_date) }}
{{ _(\"Against Sales Order\") }}{{ doc.name }}
\n\t\t\t
\n\n\t
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
{{ _(\"Bill To\") }}
{{ doc.customer_name }}
\n\t\t\t\t{% if doc.customer_address %}{{ doc.get_formatted(\"address_display\") }}{% endif %}\n\t\t\t
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t{% for row in doc.items %}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t{% endfor %}\n\t\t\n\t
{{ _(\"Sr\") }}{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Rate\") }}{{ _(\"Amount\") }}
{{ loop.index }}{{ row.item_code }}{% if row.item_name != row.item_code %}
{{ row.item_name }}{% endif %}
{{ row.get_formatted(\"qty\") }} {{ row.uom }}{{ row.get_formatted(\"rate\", doc) }}{{ row.get_formatted(\"amount\", doc) }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% for tax in doc.taxes %}\n\t\t\t{% if tax.tax_amount %}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t{% endif %}\n\t\t{% endfor %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
{{ _(\"Net Total\") }}{{ doc.get_formatted(\"net_total\") }}
{{ tax.description }}{{ tax.get_formatted(\"tax_amount\", doc) }}
{{ _(\"Grand Total\") }}{{ doc.get_formatted(\"grand_total\") }}
\n\n\t
\n\t\t{{ _(\"This is a proforma invoice and is not a demand for payment or a tax invoice.\") }}\n\t
\n
\n", + "html": "
\n\t\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
\n\t\t\t\t

{{ _(\"PROFORMA INVOICE\") }}

\n\t\t\t\t
{{ doc.company }}
\n\t\t\t
\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t
{{ _(\"Proforma No\") }}{{ doc.proforma_no or doc.name }}
{{ _(\"Date\") }}{{ frappe.utils.formatdate(doc.proforma_date) }}
{{ _(\"Against Sales Order\") }}{{ doc.name }}
\n\t\t\t
\n\n\t
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
{{ _(\"Bill To\") }}
{{ doc.customer_name }}
\n\t\t\t\t{% if doc.customer_address %}{{ doc.get_formatted(\"address_display\") }}{% endif %}\n\t\t\t
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% if not doc.hide_item_qty %}{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}{% endif %}\n\t\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t{% for row in doc.items %}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t{% if not doc.hide_item_qty %}{% endif %}\n\t\t\t\t{% if not doc.hide_item_qty %}{% endif %}\n\t\t\t\t\n\t\t\t\n\t\t\t{% endfor %}\n\t\t\n\t
{{ _(\"Sr\") }}{{ _(\"Item\") }}{{ _(\"Qty\") }}{{ _(\"Rate\") }}{{ _(\"Amount\") }}
{{ loop.index }}{{ row.item_code }}{% if row.item_name != row.item_code %}
{{ row.item_name }}{% endif %}
{{ row.get_formatted(\"qty\") }} {{ row.uom }}{{ row.get_formatted(\"rate\", doc) }}{{ row.get_formatted(\"amount\", doc) }}
\n\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t{% for tax in doc.taxes %}\n\t\t\t{% if tax.tax_amount %}\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t{% endif %}\n\t\t{% endfor %}\n\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t
{{ _(\"Net Total\") }}{{ doc.get_formatted(\"net_total\") }}
{{ tax.description }}{{ tax.get_formatted(\"tax_amount\", doc) }}
{{ _(\"Grand Total\") }}{{ doc.get_formatted(\"grand_total\") }}
\n\n\t
\n\t\t{{ _(\"This is a proforma invoice and is not a demand for payment or a tax invoice.\") }}\n\t
\n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0,