From 35e06045bd48aa63c6cf83a0eb01d1d57eeba001 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 15 Jun 2026 14:16:07 +0530 Subject: [PATCH] fix: update weighted average rate calculation to consider returned and consumed quantities (cherry picked from commit 35e55d3e13414a44a95274eb530ba050e464dd19) --- .../subcontracting_inward_controller.py | 19 +++++++-- .../test_subcontracting_inward_order.py | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index abc91afd5b8..8e0aa56052d 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -743,7 +743,14 @@ class SubcontractingInwardController: "name": ["in", list(data.keys())], "docstatus": 1, }, - fields=["rate", "name", "required_qty", "received_qty"], + fields=[ + "rate", + "name", + "required_qty", + "received_qty", + "returned_qty", + "consumed_qty", + ], ) doc_updates = {} @@ -751,13 +758,17 @@ class SubcontractingInwardController: current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1) current_rate = flt(data[d.name].rate) - # Calculate weighted average rate - old_total = d.rate * d.received_qty + # Weighted average rate must be computed on the on-hand balance + balance_qty = d.received_qty - d.returned_qty - d.consumed_qty + old_total = d.rate * balance_qty current_total = current_rate * current_qty + new_balance_qty = balance_qty + current_qty d.received_qty = d.received_qty + current_qty d.rate = ( - flt((old_total + current_total) / d.received_qty, precision) if d.received_qty else 0.0 + flt((old_total + current_total) / new_balance_qty, precision) + if new_balance_qty > 0 + else 0.0 ) if not d.required_qty and not d.received_qty: diff --git a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py index d035f4ddcb9..f74d3d73391 100644 --- a/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py +++ b/erpnext/subcontracting/doctype/subcontracting_inward_order/test_subcontracting_inward_order.py @@ -88,6 +88,46 @@ class IntegrationTestSubcontractingInwardOrder(ERPNextTestSuite): self.assertEqual(received_item.received_qty, 5) self.assertEqual(received_item.rate, 10) + def test_customer_provided_item_rate_with_return_between_receipts(self): + """Weight the average rate on the on-hand balance, not gross received_qty. + + Receive 10 @ 100, return 5, receive 6 @ 130: + balance-weighted (correct) = (5 * 100 + 6 * 130) / 11 = 116.36 + gross-weighted (wrong) = (10 * 100 + 6 * 130) / 16 = 111.25 + """ + so, scio = create_so_scio() + rm_item = "Basic RM" + + def receive(qty, rate): + rm_in = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()) + rm_in.items = [item for item in rm_in.items if item.item_code == rm_item] + rm_in.items[0].qty = qty + rm_in.items[0].transfer_qty = qty + rm_in.items[0].basic_rate = rate + rm_in.submit() + scio.reload() + + # Receipt 1: 10 @ 100 + receive(10, 100) + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.rate, 100) + + # Return 5 to the customer + rm_return = frappe.new_doc("Stock Entry").update(scio.make_rm_return()) + rm_return.items = [item for item in rm_return.items if item.item_code == rm_item] + rm_return.items[0].qty = 5 + rm_return.items[0].transfer_qty = 5 + rm_return.submit() + scio.reload() + + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.returned_qty, 5) + + # Receipt 2: 6 @ 130 — must weight against the balance of 5, not gross 10 + receive(6, 130) + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertAlmostEqual(received_item.rate, (5 * 100 + 6 * 130) / 11, places=2) + def test_add_extra_customer_provided_item(self): so, scio = create_so_scio()