From 2242f1b2303536b3c1be8ce5671833a0a232517e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 17 Jul 2026 17:37:01 +0530 Subject: [PATCH] feat(selling): make qty editable in amount-based proforma In Amount basis, both qty and amount are now user-entered and the rate is derived from them (rate = amount / qty). Previously qty was forced to the ordered qty, which ignored an edited qty when switching basis. --- erpnext/public/js/sales_order_proforma.js | 18 +++++++++++------- .../proforma_invoice/proforma_invoice.py | 3 ++- .../proforma_invoice/test_proforma_invoice.py | 10 +++++----- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/erpnext/public/js/sales_order_proforma.js b/erpnext/public/js/sales_order_proforma.js index fabbb67df69..3363ffefc66 100644 --- a/erpnext/public/js/sales_order_proforma.js +++ b/erpnext/public/js/sales_order_proforma.js @@ -117,8 +117,9 @@ Object.assign(erpnext.proforma, { label: __("Qty"), in_list_view: 1, onchange: function () { - // Keep the read-only Amount in sync while editing qty (Quantity basis). - if (this.doc) { + // In Quantity basis, Amount is derived (qty x rate). In Amount basis + // both are user-entered, so leave Amount alone. + if (this.doc && dialog.get_value("based_on") === "Quantity") { this.doc.amount = flt(this.doc.qty) * flt(this.doc.rate); this.grid_row?.refresh_field("amount"); } @@ -149,11 +150,11 @@ Object.assign(erpnext.proforma, { this.update_warning(dialog); }, - // Both Qty and Amount columns stay visible; only the one matching the chosen basis is editable. + // Qty is always editable; Amount is editable only in Amount basis (else it is derived). toggle_basis(dialog) { const by_amount = dialog.get_value("based_on") === "Amount"; const grid = dialog.get_field("items").grid; - grid.toggle_enable("qty", !by_amount); + grid.toggle_enable("qty", true); grid.toggle_enable("amount", by_amount); this.update_warning(dialog); }, @@ -192,10 +193,13 @@ Object.assign(erpnext.proforma, { create(frm, dialog, values) { const by_amount = values.based_on === "Amount"; - const field = by_amount ? "amount" : "qty"; const items = (values.items || []) - .filter((row) => flt(row[field]) > 0) - .map((row) => ({ so_detail: row.so_detail, [field]: row[field] })); + .filter((row) => flt(by_amount ? row.amount : row.qty) > 0) + .map((row) => + by_amount + ? { so_detail: row.so_detail, qty: row.qty, amount: row.amount } + : { so_detail: row.so_detail, qty: row.qty } + ); if (!items.length) { frappe.msgprint(__("Please enter a quantity or amount for at least one item.")); diff --git a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py index 77a5430b826..956dcbc0b10 100644 --- a/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/proforma_invoice.py @@ -180,7 +180,8 @@ def make_proforma_invoice( def _proforma_line(so_item, based_on: str, row: dict) -> dict | None: if based_on == "Amount": - qty = flt(so_item.qty) + # Amount basis: both qty and amount are user-entered; the rate is derived. + qty = flt(row.get("qty")) amount = flt(row.get("amount")) if amount <= 0 or qty <= 0: return None diff --git a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py index bcca75b897c..5ab30691208 100644 --- a/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py +++ b/erpnext/selling/doctype/proforma_invoice/test_proforma_invoice.py @@ -65,21 +65,21 @@ class TestProformaInvoice(ERPNextTestSuite): self.assertEqual(flt(proforma.grand_total), 440) def test_amount_based_proforma(self): - """Amount basis: qty stays ordered, rate is derived so the line totals the entered amount.""" - sales_order = make_sales_order(qty=10) # rate 100 -> ordered amount 1000 + """Amount basis: qty and amount are both entered; the rate is derived from them.""" + sales_order = make_sales_order(qty=10) # rate 100 so_detail = sales_order.items[0].name name = make_proforma_invoice( sales_order.name, - json.dumps([{"so_detail": so_detail, "amount": 250}]), + json.dumps([{"so_detail": so_detail, "qty": 5, "amount": 250}]), based_on="Amount", ) proforma = frappe.get_doc("Proforma Invoice", name) self.assertEqual(proforma.based_on, "Amount") item = proforma.items[0] - self.assertEqual(flt(item.qty), 10) - self.assertEqual(flt(item.rate), 25) + self.assertEqual(flt(item.qty), 5) + self.assertEqual(flt(item.rate), 50) # 250 / 5 self.assertEqual(flt(item.amount), 250) self.assertEqual(flt(proforma.grand_total), 250)