diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 18560fba888..b7417eb72e1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -572,6 +572,8 @@ class StockEntry(StockController, SubcontractingInwardController): frappe.get_cached_value("BOM", self.bom_no, "cost_allocation_per") if self.bom_no else None ) + secondary_items_cost_basis = self.get_secondary_items_cost_basis(outgoing_items_cost) + zero_valuation_items = [] finished_items_last = sorted(self.get("items"), key=lambda row: cint(row.is_finished_item)) for d in finished_items_last: @@ -591,11 +593,26 @@ class StockEntry(StockController, SubcontractingInwardController): zero_valuation_items, bom_cost_allocation_per, has_consumption_basis, + secondary_items_cost_basis, ) if zero_valuation_items: self._notify_zero_valuation_rate(zero_valuation_items) + def get_secondary_items_cost_basis(self, outgoing_items_cost) -> float: + """The cost a BOM allocation splits: the consumed rows, or the entry that replaced them.""" + if outgoing_items_cost or self.purpose != "Manufacture" or not self.work_order: + return outgoing_items_cost + + settings = frappe.get_single("Manufacturing Settings") + if not (settings.material_consumption and settings.get_rm_cost_from_consumption_entry): + return outgoing_items_cost + + if not self.get_consumption_entries(): + return outgoing_items_cost + + return self._fetch_consumption_entry_cost() + def has_consumption_basis(self) -> bool: """Whether the cost of the consumed items is known, even when that cost is zero.""" if any(d.s_warehouse for d in self.get("items")): @@ -629,6 +646,7 @@ class StockEntry(StockController, SubcontractingInwardController): zero_valuation_items, bom_cost_allocation_per=None, has_consumption_basis=False, + secondary_items_cost_basis=0, ): has_derived_rate = False @@ -653,7 +671,7 @@ class StockEntry(StockController, SubcontractingInwardController): frappe.get_value("BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per") ) if flt(d.transfer_qty): - d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty + d.basic_rate = (secondary_items_cost_basis * (cost_allocation_per / 100)) / d.transfer_qty has_derived_rate = True # A rate of zero that was derived rather than left unset is a real cost. Falling back to diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index fe6553176f3..5a29b5c37cb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2917,6 +2917,66 @@ class TestStockEntry(ERPNextTestSuite): self.assertEqual(flt(fg_row.basic_amount), 1000.0) self.assertEqual(flt(se.value_difference), 0.0) + @ERPNextTestSuite.change_settings( + "Manufacturing Settings", {"material_consumption": 1, "get_rm_cost_from_consumption_entry": 1} + ) + def test_secondary_item_allocation_uses_consumption_entry_cost(self): + """A BOM allocation splits the consumption entry's cost, not an empty set of consumed rows.""" + from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_stock_entry_from_wo, + ) + + rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name + fg_item = make_item(properties={"is_stock_item": 1}).name + scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name + warehouse = "_Test Warehouse - _TC" + + bom = frappe.get_doc( + { + "doctype": "BOM", + "item": fg_item, + "currency": "INR", + "quantity": 10, + "company": "_Test Company", + } + ) + bom.append("items", {"item_code": rm_item, "qty": 10}) + bom.append( + "secondary_items", + { + "secondary_item_type": "Scrap", + "item_code": scrap_item, + "item_name": scrap_item, + "qty": 5, + "cost_allocation_per": 25, + "process_loss_per": 0, + }, + ) + bom.insert() + bom.submit() + + make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100) + wo = make_wo_order_test_record( + production_item=fg_item, bom_no=bom.name, qty=10, skip_transfer=1, source_warehouse=warehouse + ) + + consumption = frappe.get_doc( + make_stock_entry_from_wo(wo.name, "Material Consumption for Manufacture", 10) + ) + consumption.submit() + self.assertEqual(flt(consumption.total_outgoing_value), 1000.0) + + se = frappe.get_doc(make_stock_entry_from_wo(wo.name, "Manufacture", 10)) + se.save() + + scrap_row = next(d for d in se.items if d.secondary_item_type) + fg_row = next(d for d in se.items if d.is_finished_item) + + self.assertEqual(flt(fg_row.basic_amount), 750.0) + self.assertEqual(flt(scrap_row.basic_amount), 250.0) + self.assertEqual(flt(se.total_incoming_value), 1000.0) + def _make_wo_for_free_raw_material(self, rm_item, fg_item, bom_no): from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.work_order import (