From 7e9c1efab72057cd1fad10ef2e027df99ef102e7 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 2 Jun 2026 17:19:25 +0530 Subject: [PATCH 1/2] fix(manufacturing): fetch raw materials from work order --- .../stock/doctype/stock_entry/stock_entry.py | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 87715e1e516..1f0616a771f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2467,9 +2467,24 @@ class StockEntry(StockController): ): self.get_unconsumed_raw_materials() + elif self.pro_doc and ( + self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture" + ): + if not self.fg_completed_qty: + frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty")))) + + item_dict = self.get_work_order_raw_materials(self.fg_completed_qty) + + for item in item_dict.values(): + if self.pro_doc.from_wip_warehouse: + item["from_warehouse"] = self.pro_doc.wip_warehouse + item["to_warehouse"] = "" + + self.add_to_stock_entry_detail(item_dict) + else: if not self.fg_completed_qty: - frappe.throw(_("Manufacturing Quantity is mandatory")) + frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label("fg_completed_qty")))) item_dict = self.get_bom_raw_materials(self.fg_completed_qty) @@ -2724,6 +2739,56 @@ class StockEntry(StockController): return item_dict + def get_work_order_raw_materials(self, qty): + item_dict = frappe._dict() + + used_alternative_items = get_used_alternative_items( + subcontract_order_field=self.subcontract_data.order_field, work_order=self.work_order + ) + + for d in self.pro_doc.get("required_items"): + item_qty = flt( + (d.required_qty / self.pro_doc.qty) * qty, frappe.get_precision("Stock Entry Detail", "qty") + ) + from_warehouse = ( + d.source_warehouse + if self.pro_doc.skip_transfer and not self.pro_doc.from_wip_warehouse + else self.from_warehouse or d.source_warehouse + ) + + item_row = frappe._dict( + { + "item_code": d.item_code, + "item_name": d.item_name, + "description": d.description, + "qty": item_qty, + "stock_uom": d.stock_uom, + "uom": d.stock_uom, + "conversion_factor": 1, + "from_warehouse": from_warehouse, + "allow_alternative_item": d.allow_alternative_item + and self.pro_doc.allow_alternative_item, + } + ) + + if d.item_code in used_alternative_items: + alt = used_alternative_items.get(d.item_code) + item_row.update( + { + "item_code": alt.item_code, + "item_name": alt.item_name, + "stock_uom": alt.stock_uom, + "uom": alt.uom, + "conversion_factor": alt.conversion_factor, + "description": alt.description, + "original_item": d.item_code, + } + ) + + item_dict[d.item_code] = item_row + + return item_dict + def get_bom_scrap_material(self, qty): from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict From b22096a640715963fe42566eba3a0e0c535e0611 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 16 Jun 2026 16:51:30 +0530 Subject: [PATCH 2/2] test(manufacturing): add test to validate the transferred raw materials from work order --- .../doctype/work_order/test_work_order.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index f4a0d6f6145..bf56182ee41 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -3631,6 +3631,58 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(bin1_at_completion.reserved_qty_for_production, 0) + @change_settings( + "Manufacturing Settings", + {"allow_editing_of_items_and_quantities_in_work_order": 1}, + ) + def test_manufacture_se_fetches_edited_qty_from_work_order(self): + """When a raw material qty is edited on the Work Order, the Manufacture Stock Entry + must consume the edited quantity (scaled to fg_completed_qty) from the Work Order, + not the original BOM quantity.""" + warehouse = "_Test Warehouse - _TC" + wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse + ) + + # edit a required item's qty + wo_order.required_items[0].db_set("required_qty", flt(wo_order.required_items[0].required_qty) + 7) + wo_order.reload() + edited_row = wo_order.required_items[0] + + fg_qty = 5 + se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", fg_qty)) + se_qty = {row.item_code: row.qty for row in se.items if row.s_warehouse} + + precision = frappe.get_precision("Stock Entry Detail", "qty") + expected = flt(edited_row.required_qty / wo_order.qty * fg_qty, precision) + self.assertEqual(flt(se_qty.get(edited_row.item_code)), expected) + + @change_settings( + "Manufacturing Settings", + {"allow_editing_of_items_and_quantities_in_work_order": 1}, + ) + def test_manufacture_se_fetches_item_not_in_bom_from_work_order(self): + """A raw material that is present on the Work Order but not on the BOM must still be + fetched into the Manufacture Stock Entry, proving items are sourced from the Work + Order's required_items rather than re-derived from the BOM.""" + extra_item = make_item( + "_Test WO Extra Raw Material", {"is_stock_item": 1, "valuation_rate": 100} + ).name + warehouse = "_Test Warehouse - _TC" + wo_order = make_wo_order_test_record( + item="_Test FG Item", qty=10, skip_transfer=1, source_warehouse=warehouse + ) + + original_item = wo_order.required_items[0].item_code + wo_order.required_items[0].db_set("item_code", extra_item) + wo_order.reload() + + se = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 5)) + se_items = [row.item_code for row in se.items if row.s_warehouse] + + self.assertIn(extra_item, se_items) + self.assertNotIn(original_item, se_items) + def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import (