diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 314b9f19d2e..18560fba888 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -630,7 +630,7 @@ class StockEntry(StockController, SubcontractingInwardController): bom_cost_allocation_per=None, has_consumption_basis=False, ): - rate_derived_from_consumption = False + has_derived_rate = False if d.allow_zero_valuation_rate and d.basic_rate and self.purpose != "Receive from Customer": d.basic_rate = 0.0 @@ -640,26 +640,25 @@ class StockEntry(StockController, SubcontractingInwardController): d.basic_rate = self.get_basic_rate_for_manufactured_item( d.transfer_qty, outgoing_items_cost, has_consumption_basis ) - rate_derived_from_consumption = has_consumption_basis + has_derived_rate = has_consumption_basis elif self.purpose == "Repack": d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) # Repack rate comes from consumed source-warehouse rows, not consumption entries - rate_derived_from_consumption = any(item.s_warehouse for item in self.get("items")) + has_derived_rate = any(item.s_warehouse for item in self.get("items")) if self.bom_no: d.basic_rate *= bom_cost_allocation_per / 100 elif d.secondary_item_type and d.bom_secondary_item: - cost_allocation_per = frappe.get_value( - "BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per" + cost_allocation_per = flt( + frappe.get_value("BOM Secondary Item", d.bom_secondary_item, "cost_allocation_per") ) - # Only recalculate when cost is actually allocated; otherwise preserve the - # user-entered rate (or fall through to get_valuation_rate below) - if cost_allocation_per and flt(d.transfer_qty): + if flt(d.transfer_qty): d.basic_rate = (outgoing_items_cost * (cost_allocation_per / 100)) / d.transfer_qty + has_derived_rate = True - # A rate of zero derived from the consumed items is their actual cost, not a missing - # rate. Falling back to the item's valuation here would value free inputs as output. - if not d.basic_rate and not d.allow_zero_valuation_rate and not rate_derived_from_consumption: + # A rate of zero that was derived rather than left unset is a real cost. Falling back to + # the item's valuation here would value free inputs, or an unallocated row, as output. + if not d.basic_rate and not d.allow_zero_valuation_rate and not has_derived_rate: d.basic_rate = get_valuation_rate( d.item_code, d.t_warehouse, diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1a6574c9cc7..fe6553176f3 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2864,6 +2864,59 @@ class TestStockEntry(ERPNextTestSuite): self.assertEqual(flt(se.total_outgoing_value), 1000.0) self.assertEqual(flt(se.value_difference), 0.0) + def test_secondary_item_with_zero_cost_allocation_carries_no_value(self): + """A BOM that allocates 0% to a secondary item gives the finished good everything.""" + 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": 0, + "process_loss_per": 0, + }, + ) + bom.insert() + bom.submit() + self.assertEqual(flt(bom.cost_allocation_per), 100.0) + + 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 + ) + + 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(scrap_row.basic_rate), 0.0) + self.assertEqual(flt(scrap_row.basic_amount), 0.0) + self.assertEqual(flt(fg_row.basic_amount), 1000.0) + self.assertEqual(flt(se.value_difference), 0.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 (