From cac7a358ddf5f0c1339afbae9be5a376c7c4a55a Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 16 Apr 2026 18:55:54 +0530 Subject: [PATCH] feat: make fg phantom-able in bom creator (#54332) --- .../doctype/bom_creator/bom_creator.js | 44 ++++++++++++++++--- .../doctype/bom_creator/bom_creator.json | 13 +++++- .../doctype/bom_creator/bom_creator.py | 21 +++++++-- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js index 045aa5c7968..2c6dbd84fa6 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -64,6 +64,13 @@ frappe.ui.form.on("BOM Creator", { options: "Item", reqd: 1, }, + { + label: __("Is Phantom BOM"), + fieldtype: "Check", + fieldname: "is_phantom", + default: 0, + change: toggle_filter, + }, { fieldtype: "Column Break" }, { label: __("Quantity"), @@ -72,7 +79,7 @@ frappe.ui.form.on("BOM Creator", { reqd: 1, default: 1.0, }, - { fieldtype: "Section Break" }, + { fieldtype: "Section Break", depends_on: "eval:!doc.is_phantom" }, { label: __("Currency"), fieldtype: "Link", @@ -89,7 +96,7 @@ frappe.ui.form.on("BOM Creator", { reqd: 1, default: 1.0, }, - { fieldtype: "Section Break" }, + { fieldtype: "Section Break", depends_on: "eval:!doc.is_phantom" }, { label: __("Routing"), fieldtype: "Link", @@ -99,14 +106,39 @@ frappe.ui.form.on("BOM Creator", { ], primary_action_label: __("Create"), primary_action: (values) => { - values.doctype = frm.doc.doctype; - frappe.db.insert(values).then((doc) => { - frappe.set_route("Form", doc.doctype, doc.name); + frappe.db.get_value("Item", values.item_code, "is_stock_item").then((r) => { + if (r.message) { + if (r.message.is_stock_item && values.is_phantom) { + frappe.throw( + __("Phantom BOM cannot be created for stock item {0}.", [values.item_code]) + ); + } else if (!r.message.is_stock_item && !values.is_phantom) { + frappe.throw( + __("Non-phantom BOM cannot be created for non-stock item {0}.", [ + values.item_code, + ]) + ); + } else { + values.doctype = frm.doc.doctype; + frappe.db.insert(values).then((doc) => { + frappe.set_route("Form", doc.doctype, doc.name); + }); + } + } }); }, }); - dialog.fields_dict.item_code.get_query = "erpnext.controllers.queries.item_query"; + function toggle_filter() { + dialog.fields_dict.item_code.get_query = { + query: "erpnext.controllers.queries.item_query", + filters: { + is_stock_item: !dialog.fields_dict.is_phantom.value, + }, + }; + } + toggle_filter(); + dialog.show(); }, diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index 96298a913e1..4bca0510e3e 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -13,6 +13,7 @@ "details_tab", "section_break_ylsl", "item_code", + "is_phantom", "item_name", "item_group", "column_break_ikj7", @@ -291,6 +292,13 @@ "fieldtype": "Link", "label": "Routing", "options": "Routing" + }, + { + "default": "0", + "fieldname": "is_phantom", + "fieldtype": "Check", + "label": "Is Phantom Item", + "read_only": 1 } ], "hide_toolbar": 1, @@ -302,7 +310,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2024-11-25 16:41:03.047835", + "modified": "2026-04-16 17:39:38.232864", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", @@ -336,9 +344,10 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "creation", "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index bc34c0bb689..b26cf931dfb 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -52,6 +52,7 @@ class BOMCreator(Document): currency: DF.Link default_warehouse: DF.Link | None error_log: DF.Text | None + is_phantom: DF.Check item_code: DF.Link item_group: DF.Link | None item_name: DF.Data | None @@ -77,6 +78,7 @@ class BOMCreator(Document): self.set_rate_for_items() def validate(self): + self.validate_finished_good() self.validate_items() self.validate_duplicate_item() @@ -102,6 +104,15 @@ class BOMCreator(Document): else: item_map[key] = row.idx + def validate_finished_good(self): + is_stock_item = frappe.get_cached_value("Item", self.item_code, "is_stock_item") + if is_stock_item and self.is_phantom: + frappe.throw(_("Phantom BOM cannot be created for stock item {0}.").format(self.item_code)) + elif not is_stock_item and not self.is_phantom: + frappe.throw( + _("Non-phantom BOM cannot be created for non-stock item {0}.").format(self.item_code) + ) + def validate_items(self): for row in self.items: if row.is_expandable and row.item_code == self.item_code: @@ -334,10 +345,12 @@ class BOMCreator(Document): } ) - if row.item_code == self.item_code and (self.routing or self.has_operations()): - bom.routing = self.routing - bom.with_operations = 1 - bom.transfer_material_against = "Work Order" + if row.item_code == self.item_code: + bom.is_phantom_bom = self.is_phantom + if not self.is_phantom and (self.routing or self.has_operations()): + bom.routing = self.routing + bom.with_operations = 1 + bom.transfer_material_against = "Work Order" for field in BOM_FIELDS: if self.get(field):