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.
This commit is contained in:
Nabin Hait
2026-07-17 17:37:01 +05:30
parent 77540403ab
commit 2242f1b230
3 changed files with 18 additions and 13 deletions

View File

@@ -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."));

View File

@@ -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

View File

@@ -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)