From 7120fbd14bb593e4d4c012b65138cbdba7f7fd38 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 20 Jan 2026 18:48:59 +0530 Subject: [PATCH] fix: calculate weighted average rate for customer provided items in subcontracting inward order (cherry picked from commit 37ee560eaea0db11e5890cf26ecf1b630204ded3) --- .../subcontracting_inward_controller.py | 14 ++++-- .../test_subcontracting_inward_order.py | 43 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/subcontracting_inward_controller.py b/erpnext/controllers/subcontracting_inward_controller.py index 056bfcdec9d..1a3ff66b825 100644 --- a/erpnext/controllers/subcontracting_inward_controller.py +++ b/erpnext/controllers/subcontracting_inward_controller.py @@ -720,6 +720,7 @@ class SubcontractingInwardController: item.db_set("scio_detail", scio_rm.name) if data: + precision = self.precision("customer_provided_item_cost", "items") result = frappe.get_all( "Subcontracting Inward Order Received Item", filters={ @@ -734,10 +735,17 @@ class SubcontractingInwardController: table = frappe.qb.DocType("Subcontracting Inward Order Received Item") case_expr_qty, case_expr_rate = Case(), Case() for d in result: - d.received_qty += ( - data[d.name].transfer_qty if self._action == "submit" else -data[d.name].transfer_qty + 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 + current_total = current_rate * 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 ) - d.rate += data[d.name].rate if self._action == "submit" else -data[d.name].rate if not d.required_qty and not d.received_qty: deleted_docs.append(d.name) 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 0e227ac4fa5..1d57660d6ef 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 @@ -51,6 +51,49 @@ class IntegrationTestSubcontractingInwardOrder(IntegrationTestCase): for item in rm_in.get("items"): self.assertEqual(item.customer_provided_item_cost, 15) + def test_customer_provided_item_cost_with_multiple_receipts(self): + """ + Validate that rate is calculated correctly (Weighted Average) when multiple receipts + occur for the same SCIO Received Item. + """ + so, scio = create_so_scio() + rm_item = "Basic RM" + + # Receipt 1: 5 Qty @ Unit Cost 10 + rm_in_1 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()) + rm_in_1.items = [item for item in rm_in_1.items if item.item_code == rm_item] + rm_in_1.items[0].qty = 5 + rm_in_1.items[0].basic_rate = 10 + rm_in_1.items[0].transfer_qty = 5 + rm_in_1.submit() + + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.rate, 10) + + # Receipt 2: 5 Qty @ Unit Cost 20 + rm_in_2 = frappe.new_doc("Stock Entry").update(scio.make_rm_stock_entry_inward()) + rm_in_2.items = [item for item in rm_in_2.items if item.item_code == rm_item] + rm_in_2.items[0].qty = 5 + rm_in_2.items[0].basic_rate = 20 + rm_in_2.items[0].transfer_qty = 5 + rm_in_2.save() + rm_in_2.submit() + + # Check 2: Rate should be Weighted Average + # (5 * 10 + 5 * 20) / 10 = 150 / 10 = 15 + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.rate, 15) + + # Cancel Receipt 2: Rate should revert to original + # (15 * 10 - 20 * 5) / 5 = 50 / 5 = 10 + rm_in_2.cancel() + scio.reload() + received_item = next(item for item in scio.received_items if item.rm_item_code == rm_item) + self.assertEqual(received_item.received_qty, 5) + self.assertEqual(received_item.rate, 10) + def test_add_extra_customer_provided_item(self): so, scio = create_so_scio()