diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e3c..e8d35428353a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -78,6 +78,10 @@ "show_items", "show_operations", "web_long_description", + "reference_section", + "bom_creator", + "bom_creator_item", + "column_break_oxbz", "amended_from", "connections_tab" ], @@ -233,7 +237,7 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List" + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" }, { "allow_on_submit": 1, @@ -599,6 +603,32 @@ "fieldname": "operating_cost_per_bom_quantity", "fieldtype": "Currency", "label": "Operating Cost Per BOM Quantity" + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "bom_creator", + "fieldtype": "Link", + "label": "BOM Creator", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "bom_creator_item", + "fieldtype": "Data", + "label": "BOM Creator Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_oxbz", + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", @@ -606,7 +636,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-08-07 11:38:08.152294", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 8058a5f8b752..023166849dbf 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -206,6 +206,7 @@ def on_update(self): def on_submit(self): self.manage_default_bom() + self.update_bom_creator_status() def on_cancel(self): self.db_set("is_active", 0) @@ -214,6 +215,23 @@ def on_cancel(self): # check if used in any other bom self.validate_bom_links() self.manage_default_bom() + self.update_bom_creator_status() + + def update_bom_creator_status(self): + if not self.bom_creator: + return + + if self.bom_creator_item: + frappe.db.set_value( + "BOM Creator Item", + self.bom_creator_item, + "bom_created", + 1 if self.docstatus == 1 else 0, + update_modified=False, + ) + + doc = frappe.get_doc("BOM Creator", self.bom_creator) + doc.set_status(save=True) def on_update_after_submit(self): self.validate_bom_links() @@ -662,18 +680,19 @@ def calculate_rm_cost(self, save=False): for d in self.get("items"): old_rate = d.rate - d.rate = self.get_rm_rate( - { - "company": self.company, - "item_code": d.item_code, - "bom_no": d.bom_no, - "qty": d.qty, - "uom": d.uom, - "stock_uom": d.stock_uom, - "conversion_factor": d.conversion_factor, - "sourced_by_supplier": d.sourced_by_supplier, - } - ) + if self.rm_cost_as_per != "Manual": + d.rate = self.get_rm_rate( + { + "company": self.company, + "item_code": d.item_code, + "bom_no": d.bom_no, + "qty": d.qty, + "uom": d.uom, + "stock_uom": d.stock_uom, + "conversion_factor": d.conversion_factor, + "sourced_by_supplier": d.sourced_by_supplier, + } + ) d.base_rate = flt(d.rate) * flt(self.conversion_rate) d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")) @@ -964,7 +983,12 @@ def get_valuation_rate(data): .as_("valuation_rate") ) .where((bin_table.item_code == item_code) & (wh_table.company == company)) - ).run(as_dict=True)[0] + ) + + if data.get("set_rate_based_on_warehouse") and data.get("warehouse"): + item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse")) + + item_valuation = item_valuation.run(as_dict=True)[0] valuation_rate = item_valuation.get("valuation_rate") diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js new file mode 100644 index 000000000000..01dc89b08026 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -0,0 +1,201 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.provide("erpnext.bom"); + +frappe.ui.form.on("BOM Creator", { + setup(frm) { + frm.trigger("set_queries"); + }, + + setup_bom_creator(frm) { + frm.dashboard.clear_comment(); + + if (!frm.is_new()) { + if ((!frappe.bom_configurator + || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { + frm.trigger("build_tree"); + } + } else { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.trigger("make_new_entry"); + } + }, + + build_tree(frm) { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + frm.toggle_enable("item_code", false); + + frappe.require('bom_configurator.bundle.js').then(() => { + frappe.bom_configurator = new frappe.ui.BOMConfigurator({ + wrapper: $parent, + page: $parent, + frm: frm, + bom_configurator: frm.doc.name, + }); + }); + }, + + make_new_entry(frm) { + let dialog = new frappe.ui.Dialog({ + title: __("Multi-level BOM Creator"), + fields: [ + { + label: __("Name"), + fieldtype: "Data", + fieldname: "name", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Company"), + fieldtype: "Link", + fieldname: "company", + options: "Company", + reqd: 1, + default: frappe.defaults.get_user_default("Company"), + }, + { fieldtype: "Section Break" }, + { + label: __("Item Code (Final Product)"), + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + reqd: 1 + }, + { fieldtype: "Column Break" }, + { + label: __("Quantity"), + fieldtype: "Float", + fieldname: "qty", + reqd: 1, + default: 1.0 + }, + { fieldtype: "Section Break" }, + { + label: __("Currency"), + fieldtype: "Link", + fieldname: "currency", + options: "Currency", + reqd: 1, + default: frappe.defaults.get_global_default("currency") + }, + { fieldtype: "Column Break" }, + { + label: __("Conversion Rate"), + fieldtype: "Float", + fieldname: "conversion_rate", + reqd: 1, + default: 1.0 + }, + ], + 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); + }); + } + }) + + dialog.show(); + }, + + set_queries(frm) { + frm.set_query("bom_no", "items", function(doc, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + return { + filters: { + item: item.item_code, + } + } + }); + }, + + refresh(frm) { + frm.trigger("setup_bom_creator"); + frm.trigger("set_root_item"); + frm.trigger("add_custom_buttons"); + }, + + set_root_item(frm) { + if (frm.is_new() && frm.doc.items?.length) { + frappe.model.set_value(frm.doc.items[0].doctype, + frm.doc.items[0].name, "is_root", 1); + } + }, + + add_custom_buttons(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__("Rebuild Tree"), () => { + frm.trigger("build_tree"); + }); + } + } +}); + +frappe.ui.form.on("BOM Creator Item", { + item_code(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (item.item_code && item.is_root) { + frappe.model.set_value(cdt, cdn, "fg_item", item.item_code); + } + }, + + do_not_explode(frm, cdt, cdn) { + let item = frappe.get_doc(cdt, cdn); + if (!item.do_not_explode) { + frm.call({ + method: "get_default_bom", + doc: frm.doc, + args: { + item_code: item.item_code + }, + callback(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, "bom_no", r.message); + } + } + }) + } else { + frappe.model.set_value(cdt, cdn, "bom_no", ""); + } + } +}); + + +erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController { + conversion_rate(doc) { + if(this.frm.doc.currency === this.get_company_currency()) { + this.frm.set_value("conversion_rate", 1.0); + } else { + erpnext.bom.update_cost(doc); + } + } + + buying_price_list(doc) { + this.apply_price_list(); + } + + plc_conversion_rate(doc) { + if (!this.in_apply_price_list) { + this.apply_price_list(null, true); + } + } + + conversion_factor(doc, cdt, cdn) { + if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) { + var item = frappe.get_doc(cdt, cdn); + frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); + item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); + refresh_field("stock_qty", item.name, item.parentfield); + this.toggle_conversion_factor(item); + this.frm.events.update_cost(this.frm); + } + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm})); \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json new file mode 100644 index 000000000000..fb4c6c5c95aa --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -0,0 +1,330 @@ +{ + "actions": [], + "allow_import": 1, + "autoname": "prompt", + "creation": "2023-07-18 14:56:34.477800", + "default_view": "List", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "tab_2_tab", + "bom_creator", + "details_tab", + "section_break_ylsl", + "item_code", + "item_name", + "item_group", + "column_break_ikj7", + "qty", + "project", + "uom", + "raw_materials_tab", + "currency_detail", + "rm_cost_as_per", + "set_rate_based_on_warehouse", + "buying_price_list", + "price_list_currency", + "plc_conversion_rate", + "column_break_ivyw", + "currency", + "conversion_rate", + "section_break_zcfg", + "default_warehouse", + "column_break_tzot", + "company", + "materials_section", + "items", + "costing_detail", + "raw_material_cost", + "remarks_tab", + "remarks", + "section_break_yixm", + "status", + "column_break_irab", + "error_log", + "connections_tab", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "fieldname": "currency_detail", + "fieldtype": "Section Break", + "label": "Costing" + }, + { + "allow_on_submit": 1, + "default": "Valuation Rate", + "fieldname": "rm_cost_as_per", + "fieldtype": "Select", + "label": "Rate Of Materials Based On", + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"", + "fieldname": "buying_price_list", + "fieldtype": "Link", + "label": "Price List", + "options": "Price List" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.rm_cost_as_per=='Price List'", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate" + }, + { + "fieldname": "column_break_ivyw", + "fieldtype": "Column Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Currency", + "options": "Currency", + "reqd": 1 + }, + { + "default": "1", + "fieldname": "conversion_rate", + "fieldtype": "Float", + "label": "Conversion Rate", + "precision": "9" + }, + { + "fieldname": "materials_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "bom_materials", + "oldfieldtype": "Table", + "options": "BOM Creator Item" + }, + { + "fieldname": "costing_detail", + "fieldtype": "Section Break", + "label": "Costing Details" + }, + { + "fieldname": "raw_material_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Cost", + "no_copy": 1, + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "remarks", + "fieldtype": "Text Editor", + "label": "Remarks" + }, + { + "fieldname": "column_break_ikj7", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Finished Good", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "qty", + "fieldtype": "Float", + "label": "Quantity", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "tab_2_tab", + "fieldtype": "Tab Break", + "label": "BOM Tree" + }, + { + "fieldname": "details_tab", + "fieldtype": "Tab Break", + "label": "Final Product" + }, + { + "fieldname": "raw_materials_tab", + "fieldtype": "Tab Break", + "label": "Sub Assemblies & Raw Materials" + }, + { + "fieldname": "remarks_tab", + "fieldtype": "Tab Break", + "label": "Remarks" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "BOM Creator", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_zcfg", + "fieldtype": "Section Break", + "label": "Warehouse" + }, + { + "fieldname": "column_break_tzot", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_warehouse", + "fieldtype": "Link", + "label": "Default Source Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "bom_creator", + "fieldtype": "HTML" + }, + { + "fieldname": "section_break_ylsl", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"", + "fieldname": "set_rate_based_on_warehouse", + "fieldtype": "Check", + "label": "Set Valuation Rate Based on Source Warehouse" + }, + { + "fieldname": "section_break_yixm", + "fieldtype": "Section Break" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled", + "read_only": 1 + }, + { + "fieldname": "column_break_irab", + "fieldtype": "Column Break" + }, + { + "fieldname": "error_log", + "fieldtype": "Text", + "label": "Error Log", + "read_only": 1 + } + ], + "icon": "fa fa-sitemap", + "is_submittable": 1, + "links": [ + { + "link_doctype": "BOM", + "link_fieldname": "bom_creator" + } + ], + "modified": "2023-08-07 15:45:06.176313", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "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 new file mode 100644 index 000000000000..999d610dfae5 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -0,0 +1,424 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from collections import OrderedDict + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import flt + +from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate + +BOM_FIELDS = [ + "company", + "rm_cost_as_per", + "project", + "currency", + "conversion_rate", + "buying_price_list", +] + +BOM_ITEM_FIELDS = [ + "item_code", + "qty", + "uom", + "rate", + "stock_qty", + "stock_uom", + "conversion_factor", + "do_not_explode", +] + + +class BOMCreator(Document): + def before_save(self): + self.set_status() + self.set_is_expandable() + self.set_conversion_factor() + self.set_reference_id() + self.set_rate_for_items() + + def validate(self): + self.validate_items() + + def validate_items(self): + for row in self.items: + if row.is_expandable and row.item_code == self.item_code: + frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code)) + + def set_status(self, save=False): + self.status = { + 0: "Draft", + 1: "Submitted", + 2: "Cancelled", + }[self.docstatus] + + self.set_status_completed() + if save: + self.db_set("status", self.status) + + def set_status_completed(self): + if self.docstatus != 1: + return + + has_completed = True + for row in self.items: + if row.is_expandable and not row.bom_created: + has_completed = False + break + + if not frappe.get_cached_value( + "BOM", {"bom_creator": self.name, "item": self.item_code}, "name" + ): + has_completed = False + + if has_completed: + self.status = "Completed" + + def on_cancel(self): + self.set_status(True) + + def set_conversion_factor(self): + for row in self.items: + row.conversion_factor = 1.0 + + def before_submit(self): + self.validate_fields() + self.set_status() + + def set_reference_id(self): + parent_reference = {row.idx: row.name for row in self.items} + + for row in self.items: + if row.fg_reference_id: + continue + + if row.parent_row_no: + row.fg_reference_id = parent_reference.get(row.parent_row_no) + + @frappe.whitelist() + def add_boms(self): + self.submit() + + def set_rate_for_items(self): + if self.rm_cost_as_per == "Manual": + return + + amount = self.get_raw_material_cost() + self.raw_material_cost = amount + + def get_raw_material_cost(self, fg_reference_id=None, amount=0): + if not fg_reference_id: + fg_reference_id = self.name + + for row in self.items: + if row.fg_reference_id != fg_reference_id: + continue + + if not row.is_expandable: + row.rate = get_bom_item_rate( + { + "company": self.company, + "item_code": row.item_code, + "bom_no": "", + "qty": row.qty, + "uom": row.uom, + "stock_uom": row.stock_uom, + "conversion_factor": row.conversion_factor, + "sourced_by_supplier": row.sourced_by_supplier, + }, + self, + ) + + row.amount = flt(row.rate) * flt(row.qty) + + else: + row.amount = 0.0 + row.amount = self.get_raw_material_cost(row.name, row.amount) + row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor)) + + amount += flt(row.amount) + + return amount + + def set_is_expandable(self): + fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code] + for row in self.items: + row.is_expandable = 0 + if row.item_code in fg_items: + row.is_expandable = 1 + + def validate_fields(self): + fields = { + "items": "Items", + } + + for field, label in fields.items(): + if not self.get(field): + frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) + + def on_submit(self): + self.enqueue_create_boms() + + def enqueue_create_boms(self): + frappe.enqueue( + self.create_boms, + queue="short", + timeout=600, + is_async=True, + ) + + frappe.msgprint( + _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True + ) + + def create_boms(self): + """ + Sample data structure of production_item_wise_rm + production_item_wise_rm = { + (fg_item_code, name): { + "items": [], + "bom_no": "", + "fg_item_data": {} + } + } + """ + + self.db_set("status", "In Progress") + production_item_wise_rm = OrderedDict({}) + production_item_wise_rm.setdefault( + (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self}) + ) + + for row in self.items: + if row.is_expandable: + if (row.item_code, row.name) not in production_item_wise_rm: + production_item_wise_rm.setdefault( + (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}) + ) + + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) + + reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) + + try: + for d in reverse_tree: + fg_item_data = production_item_wise_rm.get(d).fg_item_data + self.create_bom(fg_item_data, production_item_wise_rm) + + frappe.msgprint(_("BOMs created successfully")) + except Exception: + traceback = frappe.get_traceback() + self.db_set( + { + "status": "Failed", + "error_log": traceback, + } + ) + + frappe.msgprint(_("BOMs creation failed")) + + def create_bom(self, row, production_item_wise_rm): + bom = frappe.new_doc("BOM") + bom.update( + { + "item": row.item_code, + "bom_type": "Production", + "quantity": row.qty, + "allow_alternative_item": 1, + "bom_creator": self.name, + "bom_creator_item": row.name if row.name != self.name else "", + "rm_cost_as_per": "Manual", + } + ) + + for field in BOM_FIELDS: + if self.get(field): + bom.set(field, self.get(field)) + + for item in production_item_wise_rm[(row.item_code, row.name)]["items"]: + bom_no = "" + item.do_not_explode = 1 + if (item.item_code, item.name) in production_item_wise_rm: + bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no + item.do_not_explode = 0 + + item_args = {} + for field in BOM_ITEM_FIELDS: + item_args[field] = item.get(field) + + item_args.update( + { + "bom_no": bom_no, + "allow_alternative_item": 1, + "allow_scrap_items": 1, + "include_item_in_manufacturing": 1, + } + ) + + bom.append("items", item_args) + + bom.save(ignore_permissions=True) + bom.submit() + + production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name + + @frappe.whitelist() + def get_default_bom(self, item_code) -> str: + return frappe.get_cached_value("Item", item_code, "default_bom") + + +@frappe.whitelist() +def get_children(doctype=None, parent=None, **kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + fields = [ + "item_code as value", + "is_expandable as expandable", + "parent as parent_id", + "qty", + "idx", + "'BOM Creator Item' as doctype", + "name", + "uom", + "rate", + "amount", + ] + + query_filters = { + "fg_item": parent, + "parent": kwargs.parent_id, + } + + if kwargs.name: + query_filters["name"] = kwargs.name + + return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") + + +@frappe.whitelist() +def add_item(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + item_info = get_item_details(kwargs.item_code) + kwargs.update( + { + "uom": item_info.stock_uom, + "stock_uom": item_info.stock_uom, + "conversion_factor": 1, + } + ) + + doc.append("items", kwargs) + doc.save() + + return doc + + +@frappe.whitelist() +def add_sub_assembly(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + bom_item = frappe.parse_json(kwargs.bom_item) + + name = kwargs.fg_reference_id + parent_row_no = "" + if not kwargs.convert_to_sub_assembly: + item_info = get_item_details(bom_item.item_code) + item_row = doc.append( + "items", + { + "item_code": bom_item.item_code, + "qty": bom_item.qty, + "uom": item_info.stock_uom, + "fg_item": kwargs.fg_item, + "conversion_factor": 1, + "fg_reference_id": name, + "stock_qty": bom_item.qty, + "fg_reference_id": name, + "do_not_explode": 1, + "is_expandable": 1, + "stock_uom": item_info.stock_uom, + }, + ) + + parent_row_no = item_row.idx + name = "" + + for row in bom_item.get("items"): + row = frappe._dict(row) + item_info = get_item_details(row.item_code) + doc.append( + "items", + { + "item_code": row.item_code, + "qty": row.qty, + "fg_item": bom_item.item_code, + "uom": item_info.stock_uom, + "fg_reference_id": name, + "parent_row_no": parent_row_no, + "conversion_factor": 1, + "do_not_explode": 1, + "stock_qty": row.qty, + "stock_uom": item_info.stock_uom, + }, + ) + + doc.save() + + return doc + + +def get_item_details(item_code): + return frappe.get_cached_value( + "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1 + ) + + +@frappe.whitelist() +def delete_node(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) + if kwargs.docname: + frappe.delete_doc("BOM Creator Item", kwargs.docname) + + for item in items: + frappe.delete_doc("BOM Creator Item", item.name) + if item.expandable: + delete_node(fg_item=item.value, parent=item.parent_id) + + doc = frappe.get_doc("BOM Creator", kwargs.parent) + doc.set_rate_for_items() + doc.save() + + return doc + + +@frappe.whitelist() +def edit_qty(doctype, docname, qty, parent): + frappe.db.set_value(doctype, docname, "qty", qty) + doc = frappe.get_doc("BOM Creator", parent) + doc.set_rate_for_items() + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js new file mode 100644 index 000000000000..423b721e0474 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js @@ -0,0 +1,18 @@ +frappe.listview_settings['BOM Creator'] = { + add_fields: ["status"], + get_indicator: function (doc) { + if (doc.status === "Draft") { + return [__("Draft"), "red", "status,=,Draft"]; + } else if (doc.status === "In Progress") { + return [__("In Progress"), "orange", "status,=,In Progress"]; + } else if (doc.status === "Completed") { + return [__("Completed"), "green", "status,=,Completed"]; + } else if (doc.status === "Cancelled") { + return [__("Cancelled"), "red", "status,=,Cancelled"]; + } else if (doc.status === "Failed") { + return [__("Failed"), "red", "status,=,Failed"]; + } else if (doc.status === "Submitted") { + return [__("Submitted"), "blue", "status,=,Submitted"]; + } + }, +}; diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py new file mode 100644 index 000000000000..d239d58131d1 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -0,0 +1,240 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import random + +import frappe +from frappe.tests.utils import FrappeTestCase + +from erpnext.manufacturing.doctype.bom_creator.bom_creator import ( + add_item, + add_sub_assembly, + delete_node, + edit_qty, +) +from erpnext.stock.doctype.item.test_item import make_item + + +class TestBOMCreator(FrappeTestCase): + def setUp(self) -> None: + create_items() + + def test_bom_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Sub Assembly", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_sub_assembly( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + bom_item={ + "item_code": "Frame Assembly", + "qty": 1, + "items": [ + { + "item_code": "Frame", + "qty": 1, + }, + { + "item_code": "Fork", + "qty": 1, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Frame Assembly") + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Frame Assembly") + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.items[0].amount, fg_valuation_rate) + + def test_bom_raw_material(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM with Raw Material", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].item_code, "Pedal Assembly") + self.assertEqual(doc.items[0].qty, 2) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Bicycle") + self.assertEqual(row.fg_reference_id, doc.name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + def test_convert_to_sub_assembly(self): + final_product = "Bicycle" + make_item( + final_product, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + doc = make_bom_creator( + name="Bicycle BOM", + company="_Test Company", + item_code=final_product, + qty=1, + rm_cosy_as_per="Valuation Rate", + currency="INR", + plc_conversion_rate=1, + conversion_rate=1, + ) + + add_item( + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.name, + item_code="Pedal Assembly", + qty=2, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 0) + + add_sub_assembly( + convert_to_sub_assembly=1, + parent=doc.name, + fg_item=final_product, + fg_reference_id=doc.items[0].name, + bom_item={ + "item_code": "Pedal Assembly", + "qty": 2, + "items": [ + { + "item_code": "Pedal Body", + "qty": 2, + }, + { + "item_code": "Pedal Axle", + "qty": 2, + }, + ], + }, + ) + + doc.reload() + self.assertEqual(doc.items[0].is_expandable, 1) + + fg_valuation_rate = 0 + for row in doc.items: + if not row.is_expandable: + fg_valuation_rate += row.amount + self.assertEqual(row.fg_item, "Pedal Assembly") + self.assertEqual(row.qty, 2.0) + self.assertEqual(row.fg_reference_id, doc.items[0].name) + + self.assertEqual(doc.raw_material_cost, fg_valuation_rate) + + +def create_items(): + raw_materials = [ + "Frame", + "Fork", + "Rim", + "Spokes", + "Hub", + "Tube", + "Tire", + "Pedal Body", + "Pedal Axle", + "Ball Bearings", + "Chain Links", + "Chain Pins", + "Seat", + "Seat Post", + "Seat Clamp", + ] + + for item in raw_materials: + valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10]) + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + "valuation_rate": valuation_rate, + }, + ) + + sub_assemblies = [ + "Frame Assembly", + "Wheel Assembly", + "Pedal Assembly", + "Chain Assembly", + "Seat Assembly", + ] + + for item in sub_assemblies: + make_item( + item, + { + "item_group": "Raw Material", + "stock_uom": "Nos", + }, + ) + + +def make_bom_creator(**kwargs): + if isinstance(kwargs, str) or isinstance(kwargs, dict): + kwargs = frappe.parse_json(kwargs) + + doc = frappe.new_doc("BOM Creator") + doc.update(kwargs) + doc.save() + + return doc diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json new file mode 100644 index 000000000000..fdb5d3ad338f --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -0,0 +1,243 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-18 14:35:50.307386", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "item_group", + "column_break_f63f", + "fg_item", + "source_warehouse", + "is_expandable", + "sourced_by_supplier", + "bom_created", + "description_section", + "description", + "quantity_and_rate_section", + "qty", + "rate", + "uom", + "column_break_bgnb", + "stock_qty", + "conversion_factor", + "stock_uom", + "amount_section", + "amount", + "column_break_yuca", + "base_rate", + "base_amount", + "section_break_wtld", + "do_not_explode", + "parent_row_no", + "fg_reference_id", + "column_break_sulm", + "instruction" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group" + }, + { + "fieldname": "column_break_f63f", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "fg_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "FG Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "source_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "is_expandable", + "fieldtype": "Check", + "label": "Is Expandable", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "description_section", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fetch_from": "item_code.description", + "fetch_if_empty": 1, + "fieldname": "description", + "fieldtype": "Small Text" + }, + { + "fieldname": "quantity_and_rate_section", + "fieldtype": "Section Break", + "label": "Quantity and Rate" + }, + { + "columns": 1, + "fieldname": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Qty" + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate" + }, + { + "columns": 1, + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "fieldname": "column_break_bgnb", + "fieldtype": "Column Break" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Stock Qty", + "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor" + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "no_copy": 1, + "options": "UOM", + "read_only": 1 + }, + { + "fieldname": "amount_section", + "fieldtype": "Section Break", + "label": "Amount" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_yuca", + "fieldtype": "Column Break" + }, + { + "default": "1", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "hidden": 1, + "label": "Do Not Explode" + }, + { + "fieldname": "instruction", + "fieldtype": "Small Text", + "label": "Instruction" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Amount" + }, + { + "fieldname": "base_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Rate" + }, + { + "default": "0", + "fieldname": "sourced_by_supplier", + "fieldtype": "Check", + "label": "Sourced by Supplier" + }, + { + "fieldname": "section_break_wtld", + "fieldtype": "Section Break" + }, + { + "fieldname": "fg_reference_id", + "fieldtype": "Data", + "label": "FG Reference", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_sulm", + "fieldtype": "Column Break" + }, + { + "fieldname": "parent_row_no", + "fieldtype": "Data", + "label": "Parent Row No", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "bom_created", + "fieldtype": "Check", + "hidden": 1, + "label": "BOM Created", + "no_copy": 1, + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-07 11:52:30.492233", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Creator Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py new file mode 100644 index 000000000000..350c9180b909 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BOMCreatorItem(Document): + pass diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 518ae14659ee..8e0785074faa 100644 --- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json +++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", + "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 17:11:37.032604", "custom_blocks": [], "docstatus": 0, @@ -316,7 +316,7 @@ "type": "Link" } ], - "modified": "2023-07-04 14:40:47.281125", + "modified": "2023-08-08 22:28:39.633891", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -336,6 +336,13 @@ "type": "URL", "url": "https://frappe.school/courses/manufacturing?utm_source=in_app" }, + { + "color": "Grey", + "doc_view": "List", + "label": "BOM Creator", + "link_to": "BOM Creator", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js new file mode 100644 index 000000000000..b3b2e9f9b86f --- /dev/null +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -0,0 +1,416 @@ +class BOMConfigurator { + constructor({ wrapper, page, frm, bom_configurator }) { + this.$wrapper = $(wrapper); + this.page = page; + this.bom_configurator = bom_configurator; + this.frm = frm; + + this.make(); + this.prepare_layout(); + this.bind_events(); + } + + add_boms() { + this.frm.call({ + method: "add_boms", + freeze: true, + doc: this.frm.doc, + }); + } + + make() { + let options = { + ...this.tree_options(), + ...this.tree_methods(), + }; + + frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options); + this.tree_view = frappe.views.trees["BOM Configurator"]; + } + + bind_events() { + frappe.views.trees["BOM Configurator"].events = { + frm: this.frm, + add_item: this.add_item, + add_sub_assembly: this.add_sub_assembly, + get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields, + convert_to_sub_assembly: this.convert_to_sub_assembly, + delete_node: this.delete_node, + edit_qty: this.edit_qty, + load_tree: this.load_tree, + set_default_qty: this.set_default_qty, + } + } + + tree_options() { + return { + parent: this.$wrapper.get(0), + body: this.$wrapper.get(0), + doctype: 'BOM Configurator', + page: this.page, + expandable: true, + title: __("Configure Product Assembly"), + breadcrumb: "Manufacturing", + get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children", + root_label: this.frm.doc.item_code, + disable_add_node: true, + get_tree_root: false, + show_expand_all: false, + extend_toolbar: false, + do_not_make_page: true, + do_not_setup_menu: true, + } + } + + tree_methods() { + let frm_obj = this; + let view = frappe.views.trees["BOM Configurator"]; + + return { + onload: function(me) { + me.args["parent_id"] = frm_obj.frm.doc.name; + me.args["parent"] = frm_obj.frm.doc.item_code; + me.parent = frm_obj.$wrapper.get(0); + me.body = frm_obj.$wrapper.get(0); + me.make_tree(); + }, + onrender(node) { + const qty = node.data.qty || frm_obj.frm.doc.qty; + const uom = node.data.uom || frm_obj.frm.doc.uom; + const docname = node.data.name || frm_obj.frm.doc.name; + let amount = node.data.amount; + if (node.data.value === frm_obj.frm.doc.item_code) { + amount = frm_obj.frm.doc.raw_material_cost; + } + + amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency }); + + $(` +