diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 8574e58a498..a322c61e0e5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -7,31 +7,18 @@ "engine": "InnoDB", "field_order": [ "production_item_tab", + "final_product_section", "company", "item", + "column_break_ztxc", "quantity", - "cb0", - "is_active", - "is_default", - "allow_alternative_item", - "set_rate_of_sub_assembly_item_based_on_bom", - "is_phantom_bom", - "cost_allocation_section", + "uom", + "cost_allocation__process_loss_section", "cost_allocation_per", - "column_break_srby", "cost_allocation", - "process_loss_section", + "column_break_tgkb", "process_loss_percentage", - "column_break_ssj2", "process_loss_qty", - "currency_detail", - "rm_cost_as_per", - "buying_price_list", - "price_list_currency", - "plc_conversion_rate", - "column_break_ivyw", - "currency", - "conversion_rate", "operations_section_section", "with_operations", "track_semi_finished_goods", @@ -46,8 +33,27 @@ "operations", "materials_section", "items", - "secondary_items_tab", + "section_break_hygk", "secondary_items", + "bom_conf_tab", + "bom_configuration_section", + "column_break_zbzp", + "is_active", + "is_default", + "set_rate_of_sub_assembly_item_based_on_bom", + "cb0", + "is_phantom_bom", + "allow_alternative_item", + "quality_inspection_section_break", + "inspection_required", + "column_break_dxp7", + "quality_inspection_template", + "default_warehouse_section", + "default_source_warehouse", + "column_break_inep", + "default_target_warehouse", + "consume_components_section", + "backflush_based_on", "costing", "operating_cost", "raw_material_cost", @@ -59,23 +65,21 @@ "column_break_26", "total_cost", "base_total_cost", - "quality_inspection_tab", - "quality_inspection_section_break", - "inspection_required", - "column_break_dxp7", - "quality_inspection_template", "more_info_tab", + "currency_detail", + "rm_cost_as_per", + "buying_price_list", + "price_list_currency", + "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", "production_item_info_section", "item_name", - "uom", "image", "column_break_27", "description", "has_variants", - "default_warehouse_section", - "default_source_warehouse", - "column_break_inep", - "default_target_warehouse", "section_break_ouuf", "project", "section_break0", @@ -99,17 +103,18 @@ ], "fields": [ { - "description": "Item to be manufactured or repacked", + "description": "The final item that will be produced using this BOM.", "fieldname": "item", "fieldtype": "Link", "in_list_view": 1, "in_standard_filter": 1, - "label": "Item", + "label": "Item to Manufacture", "oldfieldname": "item", "oldfieldtype": "Link", "options": "Item", "reqd": 1, - "search_index": 1 + "search_index": 1, + "show_description_on_click": 1 }, { "fetch_from": "item.item_name", @@ -130,23 +135,26 @@ "read_only": 1 }, { + "depends_on": "item", "fetch_from": "item.stock_uom", "fieldname": "uom", "fieldtype": "Link", - "label": "Item UOM", + "label": "Unit Of Measure", "options": "UOM", "read_only": 1 }, { "default": "1", - "description": "Quantity of item obtained after manufacturing / repacking from given quantities of raw materials", + "depends_on": "item", + "description": "How many units of the final product this BOM makes.", "fieldname": "quantity", "fieldtype": "Float", - "label": "Quantity", + "label": "Quantity (Output Qty)", "non_negative": 1, "oldfieldname": "quantity", "oldfieldtype": "Currency", - "reqd": 1 + "reqd": 1, + "show_description_on_click": 1 }, { "fieldname": "cb0", @@ -288,14 +296,13 @@ { "fieldname": "materials_section", "fieldtype": "Section Break", - "label": "Raw Materials", "oldfieldtype": "Section Break" }, { "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", - "label": "Items", + "label": "Components", "oldfieldname": "bom_materials", "oldfieldtype": "Table", "options": "BOM Item", @@ -415,6 +422,7 @@ "depends_on": "eval:!doc.is_phantom_bom", "fieldname": "website_section", "fieldtype": "Tab Break", + "hidden": 1, "label": "Website" }, { @@ -528,11 +536,6 @@ "fieldtype": "Section Break", "label": "Operations" }, - { - "fieldname": "process_loss_section", - "fieldtype": "Section Break", - "label": "Process Loss" - }, { "fieldname": "process_loss_percentage", "fieldtype": "Percent", @@ -546,10 +549,6 @@ "non_negative": 1, "read_only": 1 }, - { - "fieldname": "column_break_ssj2", - "fieldtype": "Column Break" - }, { "fieldname": "more_info_tab", "fieldtype": "Tab Break", @@ -668,11 +667,6 @@ "fieldname": "section_break_ouuf", "fieldtype": "Section Break" }, - { - "fieldname": "quality_inspection_tab", - "fieldtype": "Tab Break", - "label": "Quality Inspection" - }, { "fieldname": "secondary_items", "fieldtype": "Table", @@ -697,20 +691,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "fieldname": "secondary_items_tab", - "fieldtype": "Tab Break", - "label": "Secondary Items" - }, - { - "fieldname": "cost_allocation_section", - "fieldtype": "Section Break", - "label": "Cost Allocation" - }, - { - "fieldname": "column_break_srby", - "fieldtype": "Column Break" - }, { "fieldname": "cost_allocation", "fieldtype": "Currency", @@ -725,6 +705,55 @@ "fieldtype": "Percent", "label": "% Cost Allocation", "non_negative": 1 + }, + { + "collapsible": 1, + "fieldname": "bom_configuration_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_zbzp", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_ztxc", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "cost_allocation__process_loss_section", + "fieldtype": "Section Break", + "label": "Cost Allocation / Process Loss" + }, + { + "fieldname": "column_break_tgkb", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_hygk", + "fieldtype": "Section Break" + }, + { + "fieldname": "final_product_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "bom_conf_tab", + "fieldtype": "Tab Break", + "label": "BOM Configuration" + }, + { + "fieldname": "consume_components_section", + "fieldtype": "Section Break", + "label": "Consume Components" + }, + { + "description": "Controls how raw materials are consumed during the \u2018Manufacture\u2019 stock entry.", + "fieldname": "backflush_based_on", + "fieldtype": "Select", + "label": "Based On", + "options": "\nBOM\nMaterial Transferred for Manufacture", + "show_description_on_click": 1 } ], "icon": "fa fa-sitemap", @@ -732,7 +761,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-02-26 14:13:34.040181", + "modified": "2026-04-17 15:22:33.598938", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8180284e9d5..7c032be36e4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -118,6 +118,7 @@ class BOM(WebsiteGenerator): allow_alternative_item: DF.Check amended_from: DF.Link | None + backflush_based_on: DF.Literal["", "BOM", "Material Transferred for Manufacture"] base_operating_cost: DF.Currency base_raw_material_cost: DF.Currency base_secondary_items_cost: DF.Currency @@ -1999,3 +2000,16 @@ def get_secondary_items_from_sub_assemblies(bom_no, company, qty, secondary_item get_secondary_items_from_sub_assemblies(row.bom_no, company, qty, secondary_items) return secondary_items + + +def get_backflush_based_on(bom_no): + backflush_based_on = None + if bom_no: + backflush_based_on = frappe.get_cached_value("BOM", bom_no, "backflush_based_on") + + if not backflush_based_on: + backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) + + return backflush_based_on diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index d0c5b6bb341..19b04032a3b 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2876,6 +2876,9 @@ def make_bom(**args): } ) + if args.backflush_based_on: + bom.backflush_based_on = args.backflush_based_on + if args.operating_cost_per_bom_quantity: bom.fg_based_operating_cost = 1 bom.operating_cost_per_bom_quantity = args.operating_cost_per_bom_quantity diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 628aa58b187..4e48224032b 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -4240,6 +4240,66 @@ class TestWorkOrder(ERPNextTestSuite): self.assertEqual(wo_order.operations[0].time_in_mins, 72) self.assertEqual(wo_order.operations[1].time_in_mins, 240) + def test_backflush_based_on_in_bom(self): + raw_material_1 = make_item(item_code="BOM RM 1", properties={"is_stock_item": 1}).name + raw_material_2 = make_item(item_code="BOM RM 2", properties={"is_stock_item": 1}).name + fg_item = make_item(item_code="BOM FG 1", properties={"is_stock_item": 1}).name + + frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM") + + backflush_based_on = frappe.db.get_single_value( + "Manufacturing Settings", "backflush_raw_materials_based_on" + ) + self.assertEqual(backflush_based_on, "BOM") + + for item_code in [raw_material_1, raw_material_2]: + test_stock_entry.make_stock_entry( + item_code=item_code, target="Stores - _TC", qty=1, basic_rate=100 + ) + + bom = make_bom( + item=fg_item, + quantity=1, + raw_materials=[raw_material_1], + backflush_based_on="Material Transferred for Manufacture", + ) + + wo_order = make_wo_order_test_record(item=fg_item, qty=1, source_warehouse="Stores - _TC") + + self.assertEqual(bom.name, wo_order.bom_no) + backflush_based_on = frappe.db.get_value("BOM", wo_order.bom_no, "backflush_based_on") + self.assertEqual(backflush_based_on, "Material Transferred for Manufacture") + + material_transfer_entry = frappe.get_doc( + make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1) + ) + material_transfer_entry.save() + + # Add second raw material in the material transfer entry which is not in the BOM to simulate backflush based on material transfer scenario + material_transfer_entry.append( + "items", + { + "item_code": raw_material_2, + "item_name": raw_material_2, + "item_group": frappe.get_value("Item", raw_material_2, "item_group"), + "uom": frappe.get_value("Item", raw_material_2, "stock_uom"), + "conversion_factor": 1, + "s_warehouse": "Stores - _TC", + "t_warehouse": material_transfer_entry.items[0].t_warehouse, + "qty": 1, + }, + ) + + material_transfer_entry.submit() + + manufacture_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 1)) + manufacture_entry.save() + + self.assertEqual(len(manufacture_entry.items), 3) + for row in manufacture_entry.items: + if row.s_warehouse: + self.assertIn(row.item_code, [raw_material_1, raw_material_2]) + def get_reserved_entries(voucher_no, warehouse=None): doctype = frappe.qb.DocType("Stock Reservation Entry") diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 4fa5c5d7e04..d26f6040f4c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -242,6 +242,11 @@ frappe.ui.form.on("Work Order", { frm.trigger("hide_reserve_stock_button"); frm.trigger("toggle_items_editable"); frm.trigger("set_fg_warehouse_mandatory"); + frm.trigger("toggle_hide_fields"); + }, + + toggle_hide_fields(frm) { + frm.toggle_display("operations", frm.doc?.operations && frm.doc.operations.length > 0); }, skip_transfer(frm) { @@ -638,6 +643,8 @@ frappe.ui.form.on("Work Order", { if (r.message["set_scrap_wh_mandatory"]) { frm.toggle_reqd("scrap_warehouse", true); } + + frm.trigger("toggle_hide_fields"); }, }); }, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 6e5295fbfa0..2d5145141ae 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -15,7 +15,6 @@ "column_break1", "qty", "sales_order", - "track_semi_finished_goods", "reserve_stock", "section_break_vrpa", "max_producible_qty", @@ -86,6 +85,7 @@ "product_bundle_item", "section_break_ynih", "status", + "track_semi_finished_goods", "column_break_cvuw", "amended_from", "connections_tab" @@ -608,6 +608,7 @@ "fetch_from": "bom_no.track_semi_finished_goods", "fieldname": "track_semi_finished_goods", "fieldtype": "Check", + "hidden": 1, "label": "Track Semi Finished Goods", "read_only": 1 }, @@ -705,7 +706,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2026-03-16 10:15:28.708688", + "modified": "2026-04-17 13:42:12.374055", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 548bb8c1fc6..23522a0319c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -162,6 +162,10 @@ class WorkOrder(Document): frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"), ) + if self.bom_no: + if based_on := frappe.get_cached_value("BOM", self.bom_no, "backflush_based_on"): + self.set_onload("backflush_raw_materials_based_on", based_on) + def show_create_job_card_button(self): operation_details = frappe._dict( frappe.get_all( diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8c7c896c94f..3209190f110 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2541,14 +2541,12 @@ class StockEntry(StockController, SubcontractingInwardController): frappe.throw(_("Posting date and posting time is mandatory")) self.set_work_order_details() - self.flags.backflush_based_on = frappe.db.get_single_value( + backflush_based_on = frappe.db.get_single_value( "Manufacturing Settings", "backflush_raw_materials_based_on" ) if self.bom_no: - backflush_based_on = frappe.db.get_single_value( - "Manufacturing Settings", "backflush_raw_materials_based_on" - ) + backflush_based_on = self.get_backflush_based_on() if self.purpose in [ "Material Issue", @@ -2573,7 +2571,7 @@ class StockEntry(StockController, SubcontractingInwardController): or self.purpose == "Material Consumption for Manufacture" ) and not self.pro_doc.skip_transfer - and self.flags.backflush_based_on == "Material Transferred for Manufacture" + and backflush_based_on == "Material Transferred for Manufacture" ): self.add_transfered_raw_materials_in_items() @@ -2583,7 +2581,7 @@ class StockEntry(StockController, SubcontractingInwardController): self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture" ) - and self.flags.backflush_based_on == "BOM" + and backflush_based_on == "BOM" and frappe.db.get_single_value("Manufacturing Settings", "material_consumption") == 1 ): self.get_unconsumed_raw_materials() @@ -2653,8 +2651,7 @@ class StockEntry(StockController, SubcontractingInwardController): if ( self.purpose not in ["Material Transfer for Manufacture"] - and frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on") - != "BOM" + and self.get_backflush_based_on() != "BOM" and not skip_transfer ): return @@ -2731,6 +2728,11 @@ class StockEntry(StockController, SubcontractingInwardController): row.idx = idx self.set("items", sorted_items) + def get_backflush_based_on(self): + from erpnext.manufacturing.doctype.bom.bom import get_backflush_based_on + + return get_backflush_based_on(self.bom_no) + def get_available_reserved_materials(self): reserved_entries = self.get_reserved_materials() if not reserved_entries: diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py index dd0649d84da..ae2ca3a67d6 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py @@ -114,6 +114,10 @@ class ManufactureEntry: "Manufacturing Settings", "backflush_raw_materials_based_on" ) + if self.bom_no: + if based_on := frappe.get_cached_value("BOM", self.bom_no, "backflush_based_on"): + backflush_based_on = based_on + available_serial_batches = frappe._dict({}) if backflush_based_on != "BOM": available_serial_batches = self.get_transferred_serial_batches()