From 0a257ea63d6820a49b13b713f1ffa6a4185ad479 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 7 Apr 2026 09:59:54 +0530 Subject: [PATCH] fix: process loss with bom path disassembly (cherry picked from commit 93ad48bc1bf7f21e280c0fc068c8e04ab8c422ec) --- .../doctype/work_order/test_work_order.py | 32 ++++++++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 6 ++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index e5ae8ec39ec..8c135d297d9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -6,7 +6,7 @@ from collections import defaultdict import frappe from frappe.tests import timeout -from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, today +from frappe.utils import add_days, add_months, add_to_date, cint, flt, now, nowdate, nowtime, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.job_card.job_card import make_stock_entry as make_stock_entry_from_jc @@ -2657,6 +2657,36 @@ class TestWorkOrder(ERPNextTestSuite): msg=f"Raw material {bom_item.item_code} qty mismatch", ) + # -- BOM-path disassembly (no source_stock_entry, no work_order) -- + bom_disassemble_qty = 2 + bom_se = frappe.get_doc( + { + "doctype": "Stock Entry", + "stock_entry_type": "Disassemble", + "purpose": "Disassemble", + "from_bom": 1, + "bom_no": bom.name, + "fg_completed_qty": bom_disassemble_qty, + "from_warehouse": wo.fg_warehouse, + "to_warehouse": wo.wip_warehouse, + "company": wo.company, + "posting_date": nowdate(), + "posting_time": nowtime(), + } + ) + bom_se.get_items() + bom_se.save() + bom_se.submit() + + bom_scrap_row = next((i for i in bom_se.items if i.item_code == scrap_item), None) + self.assertIsNotNone(bom_scrap_row, "Scrap item must appear in BOM-path disassembly") + # Without fix 3: qty = 10 * 2 = 20; with fix 3 (process_loss_per=10%): qty = 9 * 2 = 18 + self.assertEqual( + bom_scrap_row.qty, + 18, + f"BOM-path disassembly must apply process_loss_per; expected 18, got {bom_scrap_row.qty}", + ) + def test_disassembly_with_additional_rm_not_in_bom(self): """ Test that SE-linked disassembly includes additional raw materials diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 3cbd6a5b4db..0f954c88493 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2474,6 +2474,12 @@ class StockEntry(StockController, SubcontractingInwardController): item["to_warehouse"] = "" item["is_finished_item"] = 0 + if item.get("process_loss_per"): + item["qty"] -= flt( + item["qty"] * (item["process_loss_per"] / 100), + self.precision("fg_completed_qty"), + ) + self.add_to_stock_entry_detail(secondary_items, bom_no=self.bom_no) # Finished goods