From c9d7c6cd424ccf828d419a362b20f4a9d63be2a1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:56:27 +0000 Subject: [PATCH] fix: continuous raw material consumption with bom validation (backport #51914) (#51919) Co-authored-by: Mihir Kandoi --- .../doctype/work_order/work_order.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 31 +++++++++++++++++-- .../doctype/stock_entry/test_stock_entry.py | 27 ++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 0d99a923a00..e816c4690df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -829,7 +829,7 @@ erpnext.work_order = { } } if (counter > 0) { - var consumption_btn = frm.add_custom_button( + frm.add_custom_button( __("Material Consumption"), function () { const backflush_raw_materials_based_on = diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 5d973cdc3a0..ed3c0d7658f 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -938,7 +938,9 @@ class StockEntry(StockController, SubcontractingInwardController): if matched_item := self.get_matched_items(item_code): if flt(details.get("qty"), precision) != flt(matched_item.qty, precision): frappe.throw( - _("For the item {0}, the quantity should be {1} according to the BOM {2}.").format( + _( + "For the item {0}, the consumed quantity should be {1} according to the BOM {2}." + ).format( frappe.bold(item_code), flt(details.get("qty")), get_link_to_form("BOM", self.bom_no), @@ -1003,12 +1005,37 @@ class StockEntry(StockController, SubcontractingInwardController): ) def get_matched_items(self, item_code): - for row in self.items: + items = [item for item in self.items if item.s_warehouse] + for row in items or self.get_consumed_items(): if row.item_code == item_code or row.original_item == item_code: return row return {} + def get_consumed_items(self): + """Get all raw materials consumed through consumption entries""" + parent = frappe.qb.DocType("Stock Entry") + child = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(parent) + .join(child) + .on(parent.name == child.parent) + .select( + child.item_code, + Sum(child.qty).as_("qty"), + child.original_item, + ) + .where( + (parent.docstatus == 1) + & (parent.purpose == "Material Consumption for Manufacture") + & (parent.work_order == self.work_order) + ) + .groupby(child.item_code, child.original_item) + ) + + return query.run(as_dict=True) + @frappe.whitelist() def get_stock_and_rate(self): """ diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 1bcfc567fda..ced7d946f6f 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2362,6 +2362,33 @@ class TestStockEntry(IntegrationTestCase): self.assertEqual(target_sabb.entries[0].batch_no, batch) self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2]) + @IntegrationTestCase.change_settings( + "Manufacturing Settings", + { + "material_consumption": 1, + "backflush_raw_materials_based_on": "BOM", + "validate_components_quantities_per_bom": 1, + }, + ) + def test_validation_as_per_bom_with_continuous_raw_material_consumption(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry + from erpnext.manufacturing.doctype.work_order.work_order import make_work_order + + fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name + rm_item1 = make_item("_Battery", properties={"is_stock_item": 1}).name + warehouse = "Stores - WP" + bom_no = make_bom(item=fg_item, raw_materials=[rm_item1]).name + make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, rate=10, purpose="Material Receipt") + + work_order = make_work_order(bom_no, fg_item, 5) + work_order.skip_transfer = 1 + work_order.fg_warehouse = warehouse + work_order.submit() + + frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit() + frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit() + def make_serialized_item(self, **args): args = frappe._dict(args)