From 5131490515ee8b14696daaa8265f5a6704e4d4c3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Tue, 20 Jan 2026 16:18:06 +0530 Subject: [PATCH] fix: continuous raw material consumption with bom validation (cherry picked from commit 017cc9d9f99f71b4b3be2af4b23131127f84bc94) # Conflicts: # erpnext/stock/doctype/stock_entry/test_stock_entry.py --- .../doctype/work_order/work_order.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 31 ++++++- .../doctype/stock_entry/test_stock_entry.py | 84 +++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 9e0679a7cd1..5c34d9be76f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -731,7 +731,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 21f3245bbc4..ed6d36be87e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -858,7 +858,9 @@ class StockEntry(StockController): 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), @@ -923,12 +925,37 @@ class StockEntry(StockController): ) 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 0be02756207..4074a253a50 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -2233,7 +2233,91 @@ class TestStockEntry(FrappeTestCase): se.submit() +<<<<<<< HEAD def make_serialized_item(**args): +======= + warehouse = "_Test Warehouse - _TC" + retain_sample_item = make_item( + "Retain Sample Item", + properties={ + "is_stock_item": 1, + "retain_sample": 1, + "sample_quantity": 2, + "has_batch_no": 1, + "has_serial_no": 1, + "create_new_batch": 1, + "batch_number_series": "SAMPLE-RET-.#####", + "serial_no_series": "SAMPLE-RET-SN-.#####", + }, + ) + material_receipt = make_stock_entry( + item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt" + ) + + source_sabb = frappe.get_doc( + "Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle + ) + batch = source_sabb.entries[0].batch_no + serial_nos = [entry.serial_no for entry in source_sabb.entries] + + sample_entry = frappe.get_doc( + move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items) + ) + sample_entry.submit() + target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle) + + self.assertEqual(sample_entry.items[0].transfer_qty, 2) + 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": 0}) + def test_raw_material_missing_validation(self): + stock_entry = make_stock_entry( + item_code="_Test Item", + qty=1, + target="_Test Warehouse - _TC", + do_not_save=True, + ) + + stock_entry.purpose = "Manufacture" + stock_entry.stock_entry_type = "Manufacture" + stock_entry.items[0].is_finished_item = 1 + + self.assertRaises( + frappe.ValidationError, + stock_entry.save, + ) + + @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): +>>>>>>> 017cc9d9f9 (fix: continuous raw material consumption with bom validation) args = frappe._dict(args) se = frappe.copy_doc(test_records[0])