From 7886bd2cab5169878e465eb986c4df34a77b74ec Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 3 Aug 2026 15:49:09 +0530 Subject: [PATCH] fix(stock): stop treating a Repack secondary item as a finished good (#57735) * fix(stock): stop treating a Repack secondary item as a finished good mark_finished_and_secondary_items flagged every incoming Repack row as a finished item, secondary rows included. Two things followed from that. The row never reached the secondary-item branch in _set_incoming_item_rate, so its own cost_allocation_per was never applied, and the BOM's finished-good percentage was applied to every incoming row rather than to the finished good alone. Value was destroyed as a result. Repacking 1000 of raw material under a BOM that allocates 75% to the finished good and 25% to scrap booked 500 to the finished good and 250 to the scrap: 750 in against 1000 out. Leave secondary rows unflagged so each side takes the share the BOM declares. * test(stock): cover cost allocation for a Repack secondary item A BOM allocating 75% to the finished good and 25% to scrap must split 1000 of raw material into 750 and 250, leaving no difference. --- .../stock/doctype/stock_entry/stock_entry.py | 4 +- .../doctype/stock_entry/test_stock_entry.py | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index e9a15a7e40d..314b9f19d2e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -913,7 +913,9 @@ class StockEntry(StockController, SubcontractingInwardController): for d in self.items: if d.t_warehouse and not d.s_warehouse: - if self.purpose == "Repack" or d.item_code == finished_item: + if d.secondary_item_type or d.is_legacy_scrap_item: + d.is_finished_item = 0 + elif self.purpose == "Repack" or d.item_code == finished_item: d.is_finished_item = 1 else: d.is_finished_item = 0 diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index af4e0f42c9a..1a6574c9cc7 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2808,6 +2808,62 @@ class TestStockEntry(ERPNextTestSuite): self.assertEqual(flt(se.total_outgoing_value), 1000.0) self.assertEqual(flt(se.value_difference), 0.0) + def test_repack_allocates_cost_to_secondary_item(self): + """A Repack secondary item takes its own BOM share, not the finished good's.""" + 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() + self.assertEqual(flt(bom.cost_allocation_per), 75.0) + + make_stock_entry(item_code=rm_item, target=warehouse, qty=100, basic_rate=100) + + se = frappe.new_doc("Stock Entry") + se.purpose = se.stock_entry_type = "Repack" + se.company = "_Test Company" + se.from_bom = 1 + se.bom_no = bom.name + se.fg_completed_qty = 10 + se.from_warehouse = warehouse + se.to_warehouse = warehouse + se.get_items() + se.save() + + fg_row = next(d for d in se.items if d.is_finished_item) + scrap_row = next(d for d in se.items if d.secondary_item_type) + + self.assertFalse(scrap_row.is_finished_item) + self.assertEqual(flt(scrap_row.basic_amount), 250.0) + self.assertEqual(flt(fg_row.basic_amount), 750.0) + + self.assertEqual(flt(se.total_incoming_value), 1000.0) + self.assertEqual(flt(se.total_outgoing_value), 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 (