diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 98cb7d8fe88..634eabc9a41 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -242,14 +242,14 @@ "depends_on": "eval:doc.backflush_raw_materials_based_on == \"BOM\"", "fieldname": "validate_components_quantities_per_bom", "fieldtype": "Check", - "label": "Validate Components Quantities Per BOM" + "label": "Validate Components and Quantities Per BOM" } ], "icon": "icon-wrench", "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-09-02 12:12:03.132567", + "modified": "2025-01-02 12:46:33.520853", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing Settings", @@ -267,4 +267,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 702c38bdf82..e1927747538 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2417,6 +2417,56 @@ class TestWorkOrder(IntegrationTestCase): frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0) + def test_components_as_per_bom_for_manufacture_entry(self): + frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") + frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1) + + fg_item = "Test FG Item For Component Validation 1" + source_warehouse = "Stores - _TC" + raw_materials = ["Test Component Validation RM Item 11", "Test Component Validation RM Item 12"] + + make_item(fg_item, {"is_stock_item": 1}) + for item in raw_materials: + make_item(item, {"is_stock_item": 1}) + test_stock_entry.make_stock_entry( + item_code=item, + target=source_warehouse, + qty=10, + basic_rate=100, + ) + + make_bom(item=fg_item, source_warehouse=source_warehouse, raw_materials=raw_materials) + + wo = make_wo_order_test_record( + item=fg_item, + qty=10, + source_warehouse=source_warehouse, + ) + + transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + transfer_entry.remove(transfer_entry.items[0]) + + self.assertRaises(frappe.ValidationError, transfer_entry.save) + + transfer_entry = frappe.get_doc(make_stock_entry(wo.name, "Material Transfer for Manufacture", 10)) + transfer_entry.save() + transfer_entry.submit() + + manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10)) + manufacture_entry.save() + + manufacture_entry.remove(manufacture_entry.items[0]) + + self.assertRaises(frappe.ValidationError, manufacture_entry.save) + manufacture_entry.delete() + + manufacture_entry = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 10)) + manufacture_entry.save() + manufacture_entry.submit() + + frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 0) + def make_operation(**kwargs): kwargs = frappe._dict(kwargs) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index aa112731c54..1fb36d9c963 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -236,7 +236,7 @@ class StockEntry(StockController): self.validate_serialized_batch() self.calculate_rate_and_amount() self.validate_putaway_capacity() - self.validate_component_quantities() + self.validate_component_and_quantities() if self.get("purpose") != "Manufacture": # ignore scrap item wh difference and empty source/target wh @@ -766,7 +766,7 @@ class StockEntry(StockController): title=_("Insufficient Stock"), ) - def validate_component_quantities(self): + def validate_component_and_quantities(self): if self.purpose not in ["Manufacture", "Material Transfer for Manufacture"]: return @@ -779,20 +779,31 @@ class StockEntry(StockController): raw_materials = self.get_bom_raw_materials(self.fg_completed_qty) precision = frappe.get_precision("Stock Entry Detail", "qty") - for row in self.items: - if not row.s_warehouse: - continue - - if details := raw_materials.get(row.item_code): - if flt(details.get("qty"), precision) != flt(row.qty, precision): + for item_code, details in raw_materials.items(): + 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( - frappe.bold(row.item_code), - flt(details.get("qty"), precision), + frappe.bold(item_code), + flt(details.get("qty")), get_link_to_form("BOM", self.bom_no), ), title=_("Incorrect Component Quantity"), ) + else: + frappe.throw( + _("According to the BOM {0}, the Item '{1}' is missing in the stock entry.").format( + get_link_to_form("BOM", self.bom_no), frappe.bold(item_code) + ), + title=_("Missing Item"), + ) + + def get_matched_items(self, item_code): + for row in self.items: + if row.item_code == item_code: + return row + + return {} @frappe.whitelist() def get_stock_and_rate(self):