diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 1e7dc2b7f80..4b095e52377 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -76,6 +76,8 @@ frappe.ui.form.on("BOM", { }, }; }); + + frm.trigger("toggle_fields_for_semi_finished_goods"); }, validate: function (frm) { @@ -87,8 +89,27 @@ frappe.ui.form.on("BOM", { } }, + track_semi_finished_goods(frm) { + frm.trigger("toggle_fields_for_semi_finished_goods"); + }, + + toggle_fields_for_semi_finished_goods(frm) { + let fields = ["finished_good", "finished_good_qty", "bom_no"]; + + fields.forEach((field) => { + frm.fields_dict["operations"].grid.update_docfield_property( + field, + "read_only", + !frm.doc.track_semi_finished_goods + ); + }); + + refresh_field("operations"); + }, + with_operations: function (frm) { frm.set_df_property("fg_based_operating_cost", "hidden", frm.doc.with_operations ? 1 : 0); + frm.trigger("toggle_fields_for_semi_finished_goods"); }, fg_based_operating_cost: function (frm) { @@ -929,6 +950,19 @@ frappe.ui.form.on("BOM", { }, }); + let items = frm.doc.items.filter((item) => cint(item.operation_row_id) === cint(row.idx)); + if (items?.length) { + items.forEach((item) => { + frm._bom_rm_dialog.fields_dict.items.df.data.push({ + item_code: item.item_code, + qty: item.qty, + name: item.name, + }); + }); + + frm._bom_rm_dialog.fields_dict.items.grid.refresh(); + } + frm._bom_rm_dialog.show(); }, @@ -938,6 +972,7 @@ frappe.ui.form.on("BOM", { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", + data: [], reqd: 1, fields: [ { diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 604b1fdfff2..732084192a1 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -722,12 +722,43 @@ class BOM(WebsiteGenerator): row.update(get_item_details(row.get("item_code"))) row.operation_row_id = operation_row_id - row.idx = None - row.name = None - self.append("items", row) + + item_row = None + if row.name: + item_row = self.get_item_data(row.name) + + if item_row: + item_row.update( + { + "item_code": row.get("item_code"), + "qty": row.get("qty"), + } + ) + else: + row.idx = None + row.name = None + row.do_not_explode = 1 + row.is_sub_assembly_item = self.is_sub_assembly_item(row.item_code) + + self.append("items", row) self.save() + def is_sub_assembly_item(self, item_code): + if not self.operations: + return False + + for row in self.operations: + if row.finished_good == item_code: + return True + + return False + + def get_item_data(self, name): + for row in self.items: + if row.item_code == name: + return row + @frappe.whitelist() def add_materials_from_bom(self, finished_good, bom_no, operation_row_id, qty=None): if not frappe.db.exists("BOM", {"item": finished_good, "name": bom_no, "docstatus": 1}): @@ -745,6 +776,9 @@ class BOM(WebsiteGenerator): row.uom = row.stock_uom row.operation_row_id = operation_row_id row.idx = None + row.do_not_explode = 1 + row.is_sub_assembly_item = self.is_sub_assembly_item(row.item_code) + self.append("items", row) def traverse_tree(self, bom_list=None): @@ -946,6 +980,7 @@ class BOM(WebsiteGenerator): "item_code": d.item_code, "item_name": d.item_name, "operation": d.operation, + "is_sub_assembly_item": d.is_sub_assembly_item, "source_warehouse": d.source_warehouse, "description": d.description, "image": d.image, @@ -978,6 +1013,7 @@ class BOM(WebsiteGenerator): bom_item.description, bom_item.source_warehouse, bom_item.operation, + bom_item.is_sub_assembly_item, bom_item.stock_uom, bom_item.stock_qty, bom_item.rate, @@ -1008,6 +1044,7 @@ class BOM(WebsiteGenerator): "rate": flt(d["rate"]), "include_item_in_manufacturing": d.get("include_item_in_manufacturing", 0), "sourced_by_supplier": d.get("sourced_by_supplier", 0), + "is_sub_assembly_item": d.get("is_sub_assembly_item", 0), } ) ) diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json index 60a24c48ecd..b8a75b09822 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.json @@ -25,7 +25,8 @@ "stock_uom", "amount", "include_item_in_manufacturing", - "sourced_by_supplier" + "sourced_by_supplier", + "is_sub_assembly_item" ], "fields": [ { @@ -165,21 +166,30 @@ "fieldtype": "Check", "label": "Sourced by Supplier", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_sub_assembly_item", + "fieldtype": "Check", + "label": "Is Sub Assembly Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:06:40.935882", + "modified": "2025-08-12 20:02:32.694836", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Explosion Item", "naming_rule": "Random", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py index 98e254ffe96..eab9a01bdf4 100644 --- a/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py +++ b/erpnext/manufacturing/doctype/bom_explosion_item/bom_explosion_item.py @@ -18,6 +18,7 @@ class BOMExplosionItem(Document): description: DF.TextEditor | None image: DF.Attach | None include_item_in_manufacturing: DF.Check + is_sub_assembly_item: DF.Check item_code: DF.Link | None item_name: DF.Data | None operation: DF.Link | None diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index 1d530af34a2..1861207fd66 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -41,7 +41,8 @@ "include_item_in_manufacturing", "original_item", "column_break_33", - "sourced_by_supplier" + "sourced_by_supplier", + "is_sub_assembly_item" ], "fields": [ { @@ -300,19 +301,28 @@ "fieldname": "operation_row_id", "fieldtype": "Int", "label": "Operation ID" + }, + { + "default": "0", + "fieldname": "is_sub_assembly_item", + "fieldtype": "Check", + "label": "Is Sub Assembly Item", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-03-27 13:08:41.079752", + "modified": "2025-08-12 20:01:59.532613", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", "owner": "Administrator", "permissions": [], + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.py b/erpnext/manufacturing/doctype/bom_item/bom_item.py index 87430d7d47d..91177bc72ef 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.py +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.py @@ -26,6 +26,7 @@ class BOMItem(Document): image: DF.Attach | None include_item_in_manufacturing: DF.Check is_stock_item: DF.Check + is_sub_assembly_item: DF.Check item_code: DF.Link item_name: DF.Data | None operation: DF.Link | None diff --git a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json index 087b82d3b8b..8e9d4782fad 100644 --- a/erpnext/manufacturing/doctype/bom_operation/bom_operation.json +++ b/erpnext/manufacturing/doctype/bom_operation/bom_operation.json @@ -43,6 +43,7 @@ ], "fields": [ { + "columns": 2, "fieldname": "operation", "fieldtype": "Link", "in_list_view": 1, @@ -53,9 +54,11 @@ "reqd": 1 }, { + "columns": 2, "depends_on": "eval:!doc.workstation_type", "fieldname": "workstation", "fieldtype": "Link", + "in_list_view": 1, "label": "Workstation", "oldfieldname": "workstation", "oldfieldtype": "Link", @@ -83,7 +86,7 @@ "precision": "2" }, { - "columns": 1, + "columns": 3, "description": "In minutes", "fetch_from": "operation.total_operation_time", "fetch_if_empty": 1, @@ -191,7 +194,7 @@ "label": "Set Operating Cost Based On BOM Quantity" }, { - "columns": 1, + "columns": 3, "fieldname": "workstation_type", "fieldtype": "Link", "in_list_view": 1, @@ -199,26 +202,31 @@ "options": "Workstation Type" }, { + "columns": 3, + "depends_on": "eval:parent.track_semi_finished_goods === 1", "fieldname": "finished_good", "fieldtype": "Link", "in_list_view": 1, - "label": "Finished Goods / Semi Finished Goods Item", + "label": "FG / Semi FG Item", "options": "Item" }, { - "columns": 1, + "columns": 2, + "depends_on": "eval:parent.track_semi_finished_goods === 1", "fieldname": "bom_no", "fieldtype": "Link", + "in_list_view": 1, "label": "BOM No", "options": "BOM" }, { - "columns": 1, + "columns": 2, "default": "1", + "depends_on": "eval:parent.track_semi_finished_goods === 1", "fieldname": "finished_good_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Finished Goods Qty" + "label": "Qty to Produce" }, { "default": "0", @@ -264,7 +272,7 @@ "label": "Is Subcontracted" }, { - "depends_on": "eval:!doc.bom_no", + "depends_on": "eval:!doc.bom_no && parent.track_semi_finished_goods === 1", "fieldname": "add_raw_materials", "fieldtype": "Button", "label": "Add Raw Materials" @@ -287,7 +295,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-31 16:17:47.287117", + "modified": "2025-08-12 19:27:20.682797", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Operation", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index d733cb27845..32a67eae228 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -425,7 +425,6 @@ "fieldname": "sub_assembly_warehouse", "fieldtype": "Link", "label": "Sub Assembly Warehouse", - "mandatory_depends_on": "eval:doc.skip_available_sub_assembly_item === 1", "options": "Warehouse" }, { @@ -446,7 +445,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2025-05-09 18:55:45.500257", + "modified": "2025-08-12 19:48:09.302503", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index c1ce112e3fd..42ad3193cf4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1001,6 +1001,7 @@ class ProductionPlan(Document): sub_assembly_items_store = [] # temporary store to process all subassembly items bin_details = frappe._dict() + track_semi_finished_goods = True for row in self.po_items: if self.skip_available_sub_assembly_item and not self.sub_assembly_warehouse: frappe.throw(_("Row #{0}: Please select the Sub Assembly Warehouse").format(row.idx)) @@ -1011,8 +1012,18 @@ class ProductionPlan(Document): if not row.bom_no: frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx)) + if frappe.db.get_value("BOM", row.bom_no, "track_semi_finished_goods"): + frappe.msgprint( + _( + "Row #{0}: Since 'Track Semi Finished Goods' is enabled, the BOM {1} cannot be used for Sub Assembly Items" + ).format(row.idx, row.bom_no) + ) + continue + bom_data = [] + track_semi_finished_goods = False + get_sub_assembly_items( [item.production_item for item in sub_assembly_items_store], bin_details, @@ -1026,7 +1037,11 @@ class ProductionPlan(Document): self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) sub_assembly_items_store.extend(bom_data) - if not sub_assembly_items_store and self.skip_available_sub_assembly_item: + if ( + not track_semi_finished_goods + and not sub_assembly_items_store + and self.skip_available_sub_assembly_item + ): message = ( _( "As there are sufficient Sub Assembly Items, Work Order is not required for Warehouse {0}." @@ -1253,6 +1268,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p ) .where( (bei.docstatus < 2) + & (bei.is_sub_assembly_item == 0) & (bom.name == bom_no) & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) ) @@ -1321,6 +1337,7 @@ def get_subitems( ) .where( (bom.name == bom_no) + & (bom_item.is_sub_assembly_item == 0) & (bom_item.docstatus < 2) & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) ) @@ -2003,6 +2020,7 @@ def get_raw_materials_of_sub_assembly_items( ) .where( (bei.docstatus == 1) + & (bei.is_sub_assembly_item == 0) & (bom.name == bom_no) & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) )