mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-23 12:06:43 +00:00
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:
@@ -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."));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user