From ea32868a6bd0c026a79de2fdc05aac049cd03edf Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 6 Jun 2023 14:22:43 +0530 Subject: [PATCH 1/9] feat: Multi-level BOM Creator --- erpnext/manufacturing/doctype/bom/bom.json | 18 +- .../doctype/bom_configurator/__init__.py | 0 .../bom_configurator/bom_configurator.js | 190 +++++++++++ .../bom_configurator/bom_configurator.json | 300 +++++++++++++++++ .../bom_configurator/bom_configurator.py | 245 ++++++++++++++ .../bom_configurator/test_bom_configurator.py | 9 + .../doctype/bom_configurator_item/__init__.py | 0 .../bom_configurator_item.json | 206 ++++++++++++ .../bom_configurator_item.py | 9 + .../bom_configurator.bundle.js | 309 ++++++++++++++++++ erpnext/public/js/utils.js | 4 + 11 files changed, 1289 insertions(+), 1 deletion(-) create mode 100644 erpnext/manufacturing/doctype/bom_configurator/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js create mode 100644 erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json create mode 100644 erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py create mode 100644 erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py create mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json create mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py create mode 100644 erpnext/public/js/bom_configurator/bom_configurator.bundle.js diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index d02402299e3c..05e77dcb4104 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -78,6 +78,8 @@ "show_items", "show_operations", "web_long_description", + "reference_section", + "bom_configurator", "amended_from", "connections_tab" ], @@ -599,6 +601,20 @@ "fieldname": "operating_cost_per_bom_quantity", "fieldtype": "Currency", "label": "Operating Cost Per BOM Quantity" + }, + { + "fieldname": "reference_section", + "fieldtype": "Section Break", + "label": "Reference" + }, + { + "fieldname": "bom_configurator", + "fieldtype": "Link", + "label": "BOM Configurator", + "no_copy": 1, + "options": "BOM Configurator", + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-sitemap", @@ -606,7 +622,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-04-06 12:47:58.514795", + "modified": "2023-07-19 13:53:43.903724", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom_configurator/__init__.py b/erpnext/manufacturing/doctype/bom_configurator/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js new file mode 100644 index 000000000000..f83e6ff9aba7 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js @@ -0,0 +1,190 @@ +// 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 Configurator", { + setup(frm) { + frm.trigger("set_queries"); + }, + + onload(frm) { + frm.trigger("setup_bom_creator"); + }, + + setup_bom_creator(frm) { + frm.dashboard.clear_comment(); + + if (!frm.is_new()) { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + + 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, + }); + }); + } else { + frm.trigger("make_new_entry"); + } + }, + + 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("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) { + // + } +}); + +frappe.ui.form.on("BOM Configurator 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_configurator/bom_configurator.json b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json new file mode 100644 index 000000000000..8496203d8b8b --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json @@ -0,0 +1,300 @@ +{ + "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", + "connections_tab", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "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" + }, + { + "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 + }, + { + "fieldname": "conversion_rate", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Conversion Rate", + "precision": "9", + "reqd": 1 + }, + { + "fieldname": "materials_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "bom_materials", + "oldfieldtype": "Table", + "options": "BOM Configurator Item" + }, + { + "fieldname": "costing_detail", + "fieldtype": "Section Break", + "label": "Costing Details" + }, + { + "fieldname": "raw_material_cost", + "fieldtype": "Currency", + "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", + "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 Configurator", + "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 Rate Based on Default Warehouse" + } + ], + "icon": "fa fa-sitemap", + "is_submittable": 1, + "links": [ + { + "link_doctype": "BOM", + "link_fieldname": "bom_configurator" + } + ], + "modified": "2023-08-03 17:09:02.811034", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Configurator", + "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_configurator/bom_configurator.py b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py new file mode 100644 index 000000000000..e6f726e6781b --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py @@ -0,0 +1,245 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document + +BOM_FIELDS = [ + "company", + "rm_cost_as_per", + "project", + "currency", + "conversion_rate", + "buying_price_list", +] + + +class BOMConfigurator(Document): + def before_save(self): + self.set_is_expandable() + + def before_submit(self): + self.validate_fields() + + @frappe.whitelist() + def add_boms(self): + self.submit() + + def set_is_expandable(self): + fg_items = [row.fg_item for row in self.items] + 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 Configurator {1}").format(label, self.name)) + + def on_submit(self): + self.create_boms() + + def create_boms(self): + production_item_wise_rm = frappe._dict({}) + for row in self.items: + row.bom_no = "" + if row.fg_item not in production_item_wise_rm: + production_item_wise_rm.setdefault(row.fg_item, frappe._dict({"items": [], "bom_no": ""})) + + rm_item = production_item_wise_rm[row.fg_item]["items"] + rm_item.append(row) + + for row in self.items[::-1]: + if row.fg_item in production_item_wise_rm: + if production_item_wise_rm.get(row.fg_item).bom_no: + continue + + self.create_bom(row, production_item_wise_rm) + + frappe.msgprint(_("BOMs created successfully")) + + def create_bom(self, row, production_item_wise_rm): + production_item_wise_rm + + bom = frappe.new_doc("BOM") + bom.update( + { + "item": row.fg_item, + "bom_type": "Production", + "quantity": row.qty, + "allow_alternative_item": 1, + "bom_configurator": self.name, + } + ) + + for field in BOM_FIELDS: + if self.get(field): + bom.set(field, self.get(field)) + + for item in production_item_wise_rm[row.fg_item]["items"]: + bom_no = "" + if item.item_code in production_item_wise_rm: + bom_no = production_item_wise_rm.get(item.item_code).bom_no + item.do_not_explode = 0 + + if not bom_no and not item.do_not_explode and not item.bom_no: + bom_no = self.get_default_bom(item.item_code) + + bom.append( + "items", + { + "item_code": item.item_code, + "qty": item.qty, + "uom": item.uom, + "conversion_factor": item.conversion_factor, + "stock_qty": item.stock_qty, + "stock_uom": item.stock_uom, + "do_not_explode": item.do_not_explode, + "bom_no": bom_no, + "allow_alternative_item": 1, + "allow_scrap_items": 1, + "include_item_in_manufacturing": 1, + }, + ) + + bom.save(ignore_permissions=True) + bom.submit() + + production_item_wise_rm[row.fg_item].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(**kwargs): + if isinstance(kwargs, str): + kwargs = frappe.parse_json(kwargs) + + if isinstance(kwargs, dict): + kwargs = frappe._dict(kwargs) + + print(kwargs) + + fields = [ + "item_code as value", + "is_expandable as expandable", + "parent as parent_id", + "qty", + "idx", + "'BOM Configurator Item' as doctype", + "name", + "uom", + ] + + query_filters = { + "fg_item": kwargs.parent, + "parent": kwargs.parent_id, + } + + if kwargs.name: + query_filters["name"] = kwargs.name + + data = frappe.get_all( + "BOM Configurator Item", fields=fields, filters=query_filters, order_by="idx", debug=1 + ) + + frappe.errprint(data) + return data + + +@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 Configurator", kwargs.parent) + doc.append("items", kwargs) + doc.save() + + +@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 Configurator", kwargs.parent) + bom_item = frappe.parse_json(kwargs.bom_item) + + if not kwargs.convert_to_sub_assembly: + item_info = get_item_details(bom_item.item_code) + 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, + "stock_qty": bom_item.qty, + "do_not_explode": 1, + "stock_uom": item_info.stock_uom, + }, + ) + + 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, + "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 Configurator Item", kwargs.docname) + + for item in items: + frappe.delete_doc("BOM Configurator Item", item.name) + if item.expandable: + delete_node(fg_item=item.value, parent=item.parent_id) + + +@frappe.whitelist() +def edit_qty(doctype, docname, qty): + frappe.db.set_value(doctype, docname, "qty", qty) diff --git a/erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py b/erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py new file mode 100644 index 000000000000..cf5b69368f5d --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestBOMConfigurator(FrappeTestCase): + pass diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/__init__.py b/erpnext/manufacturing/doctype/bom_configurator_item/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json new file mode 100644 index 000000000000..d3df51ac7814 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json @@ -0,0 +1,206 @@ +{ + "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", + "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", + "explode_item_section", + "do_not_explode", + "bom_no", + "instruction_section", + "instruction" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, + { + "fieldname": "column_break_f63f", + "fieldtype": "Column Break" + }, + { + "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" + }, + { + "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" + }, + { + "fieldname": "stock_qty", + "fieldtype": "Float", + "label": "Stock Qty", + "read_only": 1 + }, + { + "fieldname": "column_break_bgnb", + "fieldtype": "Column Break" + }, + { + "columns": 1, + "fieldname": "uom", + "fieldtype": "Link", + "in_list_view": 1, + "label": "UOM", + "options": "UOM" + }, + { + "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": "source_warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "instruction_section", + "fieldtype": "Section Break", + "label": "Instruction" + }, + { + "fieldname": "instruction", + "fieldtype": "Small Text" + }, + { + "depends_on": "eval:!doc.do_not_explode", + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "Exploded BOM", + "no_copy": 1, + "options": "BOM" + }, + { + "columns": 2, + "fieldname": "fg_item", + "fieldtype": "Link", + "in_list_view": 1, + "label": "FG Item", + "options": "Item", + "reqd": 1 + }, + { + "fieldname": "explode_item_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Explode Item" + }, + { + "default": "1", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "label": "Do Not Explode" + }, + { + "default": "0", + "fieldname": "is_expandable", + "fieldtype": "Check", + "label": "Is Expandable", + "read_only": 1 + }, + { + "columns": 2, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate" + }, + { + "fieldname": "amount_section", + "fieldtype": "Section Break", + "label": "Amount" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fieldname": "column_break_yuca", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-03 17:01:00.173363", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "BOM Configurator Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py new file mode 100644 index 000000000000..f623532e7643 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_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 BOMConfiguratorItem(Document): + pass 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..2256f83dc477 --- /dev/null +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -0,0 +1,309 @@ +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); + } + + bind_events() { + frappe.views.trees["BOM Configurator"].events = { + 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, + frm: this.frm, + // datatable: this.datatable, + } + } + + 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_configurator.bom_configurator.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; + + $(`${qty} ${uom} + `).insertBefore(node.$ul); + }, + toolbar: this.frm?.doc.docstatus === 0 ? [ + { + label:__(frappe.utils.icon('edit', 'sm') + " Qty"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.edit_qty(node); + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Raw Material"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_item(node); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.add_sub_assembly(node, view); + }, + condition: function(node) { + return node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.convert_to_sub_assembly(node, view); + }, + condition: function(node) { + return !node.expandable; + }, + btnClass: "hidden-xs" + }, + { + label:__(frappe.utils.icon('delete', 'sm') + __(" Item")), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + view.events.delete_node(node, view); + }, + condition: function(node) { + return !node.is_root; + }, + btnClass: "hidden-xs" + }, + ] : [], + } + } + + add_item(node) { + frappe.prompt([ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + if (!node.data.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_item", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + item_code: data.item_code, + qty: data.qty, + }, + callback: (r) => { + frappe.views.trees["BOM Configurator"].tree.load_children(node); + } + }); + }, + __("Add Item"), + __("Add")); + } + + add_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(), + title: __("Add Sub Assembly"), + }); + + dialog.show(); + + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + if (!node.data?.parent_id) { + node.data.parent_id = this.frm.doc.name; + } + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + bom_item: bom_item, + }, + callback: (r) => { + frappe.views.trees["BOM Configurator"].tree.load_children(node); + view.events.frm.doc = r.message; + } + }); + + dialog.hide(); + }); + + } + + get_sub_assembly_modal_fields(read_only=false) { + return [ + { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only }, + { fieldtype: "Column Break" }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only }, + { fieldtype: "Section Break" }, + { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1, + fields: [ + { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 }, + { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 }, + ] + }, + ] + } + + convert_to_sub_assembly(node, view) { + let dialog = new frappe.ui.Dialog({ + fields: view.events.get_sub_assembly_modal_fields(true), + title: __("Add Sub Assembly"), + }); + + dialog.set_values({ + item_code: node.data.value, + qty: node.data.qty, + }); + + dialog.show(); + dialog.set_primary_action(__("Add"), () => { + let bom_item = dialog.get_values(); + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_sub_assembly", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + bom_item: bom_item, + convert_to_sub_assembly: true, + }, + callback: (r) => { + node.expandable = true; + frappe.views.trees["BOM Configurator"].tree.load_children(node); + view.events.frm.doc = r.message; + } + }); + + dialog.hide(); + }); + } + + delete_node(node, view) { + frappe.confirm(__("Are you sure you want to delete this Item?"), () => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.delete_node", + args: { + parent: node.data.parent_id, + fg_item: node.data.value, + doctype: node.data.doctype, + docname: node.data.name, + }, + callback: (r) => { + frappe.views.trees["BOM Configurator"].tree.load_children(node.parent_node); + } + }); + }); + } + + edit_qty(node) { + let qty = node.data.qty || this.frm.doc.qty; + frappe.prompt([ + { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }, + ], + (data) => { + let doctype = node.data.doctype || this.frm.doc.doctype; + let docname = node.data.name || this.frm.doc.name; + + frappe.call({ + method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.edit_qty", + args: { + doctype: doctype, + docname: docname, + qty: data.qty, + }, + callback: (r) => { + node.data.qty = data.qty; + let uom = node.data.uom || this.frm.doc.uom; + $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom); + } + }); + }, + __("Edit Qty"), + __("Update")); + } + + prepare_layout() { + let main_div = $(this.page)[0]; + + main_div.style.marginBottom = "15px"; + $(main_div).find(".tree-children")[0].style.minHeight = "370px"; + $(main_div).find(".tree-children")[0].style.maxHeight = "370px"; + $(main_div).find(".tree-children")[0].style.overflowY = "auto"; + } +} + +frappe.ui.BOMConfigurator = BOMConfigurator; \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index f456e5e500ce..e10dffb328d9 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -114,6 +114,10 @@ $.extend(erpnext.utils, { }, view_serial_batch_nos: function(frm) { + if (!frm.doc?.items) { + return; + } + let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle); if (bundle_ids?.length) { From db42a8dfe01ca5609fab19ba0c1afa65f85b8db9 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Aug 2023 11:44:21 +0530 Subject: [PATCH 2/9] fix: renamed BOM Configurator to BOM Creator --- erpnext/manufacturing/doctype/bom/bom.json | 4 +- .../__init__.py | 0 .../bom_creator.js} | 32 +-- .../bom_creator.json} | 13 +- .../bom_creator.py} | 29 +-- .../test_bom_creator.py} | 2 +- .../doctype/bom_creator_item/__init__.py | 0 .../bom_creator_item/bom_creator_item.json | 220 ++++++++++++++++++ .../bom_creator_item/bom_creator_item.py | 9 + .../bom_configurator.bundle.js | 33 ++- 10 files changed, 292 insertions(+), 50 deletions(-) rename erpnext/manufacturing/doctype/{bom_configurator => bom_creator}/__init__.py (100%) rename erpnext/manufacturing/doctype/{bom_configurator/bom_configurator.js => bom_creator/bom_creator.js} (88%) rename erpnext/manufacturing/doctype/{bom_configurator/bom_configurator.json => bom_creator/bom_creator.json} (95%) rename erpnext/manufacturing/doctype/{bom_configurator/bom_configurator.py => bom_creator/bom_creator.py} (88%) rename erpnext/manufacturing/doctype/{bom_configurator/test_bom_configurator.py => bom_creator/test_bom_creator.py} (78%) create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json create mode 100644 erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 05e77dcb4104..caccc0bce2da 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -612,7 +612,7 @@ "fieldtype": "Link", "label": "BOM Configurator", "no_copy": 1, - "options": "BOM Configurator", + "options": "BOM Creator", "print_hide": 1, "read_only": 1 } @@ -622,7 +622,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-07-19 13:53:43.903724", + "modified": "2023-08-03 17:21:42.240881", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom_configurator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py similarity index 100% rename from erpnext/manufacturing/doctype/bom_configurator/__init__.py rename to erpnext/manufacturing/doctype/bom_creator/__init__.py diff --git a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js similarity index 88% rename from erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js rename to erpnext/manufacturing/doctype/bom_creator/bom_creator.js index f83e6ff9aba7..2c4b786ef428 100644 --- a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -2,30 +2,29 @@ // For license information, please see license.txt frappe.provide("erpnext.bom"); -frappe.ui.form.on("BOM Configurator", { +frappe.ui.form.on("BOM Creator", { setup(frm) { frm.trigger("set_queries"); }, - onload(frm) { - frm.trigger("setup_bom_creator"); - }, - setup_bom_creator(frm) { frm.dashboard.clear_comment(); if (!frm.is_new()) { - let $parent = $(frm.fields_dict["bom_creator"].wrapper); - $parent.empty(); - - 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, + if ((!frappe.bom_configurator + || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { + let $parent = $(frm.fields_dict["bom_creator"].wrapper); + $parent.empty(); + + 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, + }); }); - }); + } } else { frm.trigger("make_new_entry"); } @@ -110,6 +109,7 @@ frappe.ui.form.on("BOM Configurator", { }, refresh(frm) { + frm.trigger("setup_bom_creator"); frm.trigger("set_root_item"); frm.trigger("add_custom_buttons"); }, @@ -126,7 +126,7 @@ frappe.ui.form.on("BOM Configurator", { } }); -frappe.ui.form.on("BOM Configurator Item", { +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) { diff --git a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json similarity index 95% rename from erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json rename to erpnext/manufacturing/doctype/bom_creator/bom_creator.json index 8496203d8b8b..a44e8d1c43a5 100644 --- a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -63,7 +63,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, @@ -116,12 +116,13 @@ "oldfieldtype": "Section Break" }, { + "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", "label": "Items", "oldfieldname": "bom_materials", "oldfieldtype": "Table", - "options": "BOM Configurator Item" + "options": "BOM Creator Item" }, { "fieldname": "costing_detail", @@ -215,7 +216,7 @@ "fieldtype": "Link", "label": "Amended From", "no_copy": 1, - "options": "BOM Configurator", + "options": "BOM Creator", "print_hide": 1, "read_only": 1 }, @@ -247,7 +248,7 @@ "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"", "fieldname": "set_rate_based_on_warehouse", "fieldtype": "Check", - "label": "Set Rate Based on Default Warehouse" + "label": "Set Valuation Rate Based on Source Warehouse" } ], "icon": "fa fa-sitemap", @@ -258,10 +259,10 @@ "link_fieldname": "bom_configurator" } ], - "modified": "2023-08-03 17:09:02.811034", + "modified": "2023-08-04 11:42:42.146052", "modified_by": "Administrator", "module": "Manufacturing", - "name": "BOM Configurator", + "name": "BOM Creator", "naming_rule": "Set by user", "owner": "Administrator", "permissions": [ diff --git a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py similarity index 88% rename from erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py rename to erpnext/manufacturing/doctype/bom_creator/bom_creator.py index e6f726e6781b..d6b43dbd7529 100644 --- a/erpnext/manufacturing/doctype/bom_configurator/bom_configurator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -15,7 +15,7 @@ ] -class BOMConfigurator(Document): +class BOMCreator(Document): def before_save(self): self.set_is_expandable() @@ -40,7 +40,7 @@ def validate_fields(self): for field, label in fields.items(): if not self.get(field): - frappe.throw(_("Please set {0} in BOM Configurator {1}").format(label, self.name)) + frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) def on_submit(self): self.create_boms() @@ -74,7 +74,7 @@ def create_bom(self, row, production_item_wise_rm): "bom_type": "Production", "quantity": row.qty, "allow_alternative_item": 1, - "bom_configurator": self.name, + "bom_creator": self.name, } ) @@ -119,41 +119,36 @@ def get_default_bom(self, item_code) -> str: @frappe.whitelist() -def get_children(**kwargs): +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) - print(kwargs) - fields = [ "item_code as value", "is_expandable as expandable", "parent as parent_id", "qty", "idx", - "'BOM Configurator Item' as doctype", + "'BOM Creator Item' as doctype", "name", "uom", ] query_filters = { - "fg_item": kwargs.parent, + "fg_item": parent, "parent": kwargs.parent_id, } if kwargs.name: query_filters["name"] = kwargs.name - data = frappe.get_all( - "BOM Configurator Item", fields=fields, filters=query_filters, order_by="idx", debug=1 + return frappe.get_all( + "BOM Creator Item", fields=fields, filters=query_filters, order_by="idx", debug=1 ) - frappe.errprint(data) - return data - @frappe.whitelist() def add_item(**kwargs): @@ -163,7 +158,7 @@ def add_item(**kwargs): if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) - doc = frappe.get_doc("BOM Configurator", kwargs.parent) + doc = frappe.get_doc("BOM Creator", kwargs.parent) doc.append("items", kwargs) doc.save() @@ -176,7 +171,7 @@ def add_sub_assembly(**kwargs): if isinstance(kwargs, dict): kwargs = frappe._dict(kwargs) - doc = frappe.get_doc("BOM Configurator", kwargs.parent) + doc = frappe.get_doc("BOM Creator", kwargs.parent) bom_item = frappe.parse_json(kwargs.bom_item) if not kwargs.convert_to_sub_assembly: @@ -232,10 +227,10 @@ def delete_node(**kwargs): items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent) if kwargs.docname: - frappe.delete_doc("BOM Configurator Item", kwargs.docname) + frappe.delete_doc("BOM Creator Item", kwargs.docname) for item in items: - frappe.delete_doc("BOM Configurator Item", item.name) + frappe.delete_doc("BOM Creator Item", item.name) if item.expandable: delete_node(fg_item=item.value, parent=item.parent_id) diff --git a/erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py similarity index 78% rename from erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py rename to erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py index cf5b69368f5d..58f2533102c2 100644 --- a/erpnext/manufacturing/doctype/bom_configurator/test_bom_configurator.py +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -5,5 +5,5 @@ from frappe.tests.utils import FrappeTestCase -class TestBOMConfigurator(FrappeTestCase): +class TestBOMCreator(FrappeTestCase): pass 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..f8fc0a4a6ee4 --- /dev/null +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -0,0 +1,220 @@ +{ + "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", + "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", + "explode_item_section", + "do_not_explode", + "bom_no", + "instruction_section", + "instruction" + ], + "fields": [ + { + "columns": 2, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, + { + "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" + }, + { + "fieldname": "explode_item_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Explode Item" + }, + { + "default": "1", + "fieldname": "do_not_explode", + "fieldtype": "Check", + "label": "Do Not Explode" + }, + { + "depends_on": "eval:!doc.do_not_explode", + "fieldname": "bom_no", + "fieldtype": "Link", + "label": "Exploded BOM", + "no_copy": 1, + "options": "BOM" + }, + { + "collapsible": 1, + "fieldname": "instruction_section", + "fieldtype": "Section Break", + "label": "Instruction" + }, + { + "fieldname": "instruction", + "fieldtype": "Small Text" + }, + { + "fieldname": "base_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Amount" + }, + { + "fieldname": "base_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Base Rate" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-03 17:50:16.574197", + "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/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 2256f83dc477..6b2a1b98b622 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -49,7 +49,7 @@ class BOMConfigurator { expandable: true, title: __("Configure Product Assembly"), breadcrumb: "Manufacturing", - get_tree_nodes: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.get_children", + 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, @@ -117,6 +117,25 @@ class BOMConfigurator { }, btnClass: "hidden-xs" }, + { + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }, { label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"), click: function(node) { @@ -154,7 +173,7 @@ class BOMConfigurator { } frappe.call({ - method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_item", + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item", args: { parent: node.data.parent_id, fg_item: node.data.value, @@ -186,7 +205,7 @@ class BOMConfigurator { } frappe.call({ - method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_sub_assembly", + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", args: { parent: node.data.parent_id, fg_item: node.data.value, @@ -194,7 +213,6 @@ class BOMConfigurator { }, callback: (r) => { frappe.views.trees["BOM Configurator"].tree.load_children(node); - view.events.frm.doc = r.message; } }); @@ -234,7 +252,7 @@ class BOMConfigurator { let bom_item = dialog.get_values(); frappe.call({ - method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.add_sub_assembly", + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly", args: { parent: node.data.parent_id, fg_item: node.data.value, @@ -244,7 +262,6 @@ class BOMConfigurator { callback: (r) => { node.expandable = true; frappe.views.trees["BOM Configurator"].tree.load_children(node); - view.events.frm.doc = r.message; } }); @@ -255,7 +272,7 @@ class BOMConfigurator { delete_node(node, view) { frappe.confirm(__("Are you sure you want to delete this Item?"), () => { frappe.call({ - method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.delete_node", + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node", args: { parent: node.data.parent_id, fg_item: node.data.value, @@ -279,7 +296,7 @@ class BOMConfigurator { let docname = node.data.name || this.frm.doc.name; frappe.call({ - method: "erpnext.manufacturing.doctype.bom_configurator.bom_configurator.edit_qty", + method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty", args: { doctype: doctype, docname: docname, From 03ae314fb5cfa8bf4376a4057cac8108027d314f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Aug 2023 17:13:39 +0530 Subject: [PATCH 3/9] fix: added Cost in the tree --- erpnext/manufacturing/doctype/bom/bom.json | 8 +- erpnext/manufacturing/doctype/bom/bom.py | 32 ++-- .../doctype/bom_creator/bom_creator.json | 7 +- .../doctype/bom_creator/bom_creator.py | 147 +++++++++++++++--- .../bom_creator_item/bom_creator_item.json | 62 ++++---- .../bom_configurator.bundle.js | 50 ++++-- 6 files changed, 226 insertions(+), 80 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index caccc0bce2da..b68229334ccf 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -79,7 +79,7 @@ "show_operations", "web_long_description", "reference_section", - "bom_configurator", + "bom_creator", "amended_from", "connections_tab" ], @@ -235,7 +235,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, @@ -608,7 +608,7 @@ "label": "Reference" }, { - "fieldname": "bom_configurator", + "fieldname": "bom_creator", "fieldtype": "Link", "label": "BOM Configurator", "no_copy": 1, @@ -622,7 +622,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-03 17:21:42.240881", + "modified": "2023-08-04 14:27:32.961309", "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..3ed6fe84bef6 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -662,18 +662,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 +965,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/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index a44e8d1c43a5..6ad1901b6134 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -63,7 +63,8 @@ "fieldname": "rm_cost_as_per", "fieldtype": "Select", "label": "Rate Of Materials Based On", - "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual" + "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual", + "reqd": 1 }, { "allow_on_submit": 1, @@ -256,10 +257,10 @@ "links": [ { "link_doctype": "BOM", - "link_fieldname": "bom_configurator" + "link_fieldname": "bom_creator" } ], - "modified": "2023-08-04 11:42:42.146052", + "modified": "2023-08-04 14:29:51.375361", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index d6b43dbd7529..95946a8bdeda 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -1,9 +1,14 @@ # 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", @@ -17,15 +22,77 @@ class BOMCreator(Document): def before_save(self): + self.set_conversion_factor() + self.set_reference_id() self.set_is_expandable() + self.set_rate_for_items() + + def set_conversion_factor(self): + for row in self.items: + row.conversion_factor = 1.0 def before_submit(self): self.validate_fields() + 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 + + self.set_rate_for_raw_materials() + self.set_rate_for_sub_assemblies() + + def set_rate_for_raw_materials(self): + for row in self.items: + if row.is_expandable: + continue + + 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) + + def set_rate_for_sub_assemblies(self): + sub_assemblies = frappe._dict({}) + for row in self.items: + if row.fg_reference_id == self.name: + continue + + sub_assemblies.setdefault((row.fg_reference_id), []).append(flt(row.amount)) + + self.raw_material_cost = 0 + for row in self.items: + if row.name in sub_assemblies: + row.amount = sum(sub_assemblies.get(row.name)) + row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor)) + + if row.fg_reference_id == self.name: + self.raw_material_cost += flt(row.amount) + def set_is_expandable(self): fg_items = [row.fg_item for row in self.items] for row in self.items: @@ -46,35 +113,49 @@ def on_submit(self): self.create_boms() def create_boms(self): - production_item_wise_rm = frappe._dict({}) + """ + Sample data structure of production_item_wise_rm + production_item_wise_rm = { + (fg_item_code, name): { + "items": [], + "bom_no": "", + "fg_item_data": {} + } + } + """ + + 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: - row.bom_no = "" - if row.fg_item not in production_item_wise_rm: - production_item_wise_rm.setdefault(row.fg_item, frappe._dict({"items": [], "bom_no": ""})) + 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}) + ) - rm_item = production_item_wise_rm[row.fg_item]["items"] - rm_item.append(row) + production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row) - for row in self.items[::-1]: - if row.fg_item in production_item_wise_rm: - if production_item_wise_rm.get(row.fg_item).bom_no: - continue + reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) - self.create_bom(row, production_item_wise_rm) + 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")) def create_bom(self, row, production_item_wise_rm): - production_item_wise_rm - bom = frappe.new_doc("BOM") bom.update( { - "item": row.fg_item, + "item": row.item_code, "bom_type": "Production", "quantity": row.qty, "allow_alternative_item": 1, "bom_creator": self.name, + "rm_cost_as_per": "Manual", } ) @@ -82,21 +163,19 @@ def create_bom(self, row, production_item_wise_rm): if self.get(field): bom.set(field, self.get(field)) - for item in production_item_wise_rm[row.fg_item]["items"]: + for item in production_item_wise_rm[(row.item_code, row.name)]["items"]: bom_no = "" - if item.item_code in production_item_wise_rm: - bom_no = production_item_wise_rm.get(item.item_code).bom_no + 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 - if not bom_no and not item.do_not_explode and not item.bom_no: - bom_no = self.get_default_bom(item.item_code) - bom.append( "items", { "item_code": item.item_code, "qty": item.qty, "uom": item.uom, + "rate": item.rate, "conversion_factor": item.conversion_factor, "stock_qty": item.stock_qty, "stock_uom": item.stock_uom, @@ -111,7 +190,7 @@ def create_bom(self, row, production_item_wise_rm): bom.save(ignore_permissions=True) bom.submit() - production_item_wise_rm[row.fg_item].bom_no = bom.name + production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name @frappe.whitelist() def get_default_bom(self, item_code) -> str: @@ -135,6 +214,8 @@ def get_children(doctype=None, parent=None, **kwargs): "'BOM Creator Item' as doctype", "name", "uom", + "rate", + "amount", ] query_filters = { @@ -145,9 +226,7 @@ def get_children(doctype=None, parent=None, **kwargs): if kwargs.name: query_filters["name"] = kwargs.name - return frappe.get_all( - "BOM Creator Item", fields=fields, filters=query_filters, order_by="idx", debug=1 - ) + return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx") @frappe.whitelist() @@ -159,6 +238,15 @@ def add_item(**kwargs): 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() @@ -174,9 +262,11 @@ def add_sub_assembly(**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) - doc.append( + item_row = doc.append( "items", { "item_code": bom_item.item_code, @@ -184,12 +274,17 @@ def add_sub_assembly(**kwargs): "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, "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) @@ -200,6 +295,8 @@ def add_sub_assembly(**kwargs): "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, diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index f8fc0a4a6ee4..572c51b72d84 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -13,6 +13,7 @@ "fg_item", "source_warehouse", "is_expandable", + "sourced_by_supplier", "description_section", "description", "quantity_and_rate_section", @@ -28,10 +29,11 @@ "column_break_yuca", "base_rate", "base_amount", - "explode_item_section", + "section_break_wtld", "do_not_explode", - "bom_no", - "instruction_section", + "parent_row_no", + "fg_reference_id", + "column_break_sulm", "instruction" ], "fields": [ @@ -41,7 +43,8 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item Code", - "options": "Item" + "options": "Item", + "reqd": 1 }, { "fetch_from": "item_code.item_name", @@ -162,35 +165,17 @@ "fieldname": "column_break_yuca", "fieldtype": "Column Break" }, - { - "fieldname": "explode_item_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Explode Item" - }, { "default": "1", "fieldname": "do_not_explode", "fieldtype": "Check", + "hidden": 1, "label": "Do Not Explode" }, - { - "depends_on": "eval:!doc.do_not_explode", - "fieldname": "bom_no", - "fieldtype": "Link", - "label": "Exploded BOM", - "no_copy": 1, - "options": "BOM" - }, - { - "collapsible": 1, - "fieldname": "instruction_section", - "fieldtype": "Section Break", - "label": "Instruction" - }, { "fieldname": "instruction", - "fieldtype": "Small Text" + "fieldtype": "Small Text", + "label": "Instruction" }, { "fieldname": "base_amount", @@ -203,12 +188,37 @@ "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", + "read_only": 1 + }, + { + "fieldname": "column_break_sulm", + "fieldtype": "Column Break" + }, + { + "fieldname": "parent_row_no", + "fieldtype": "Data", + "label": "Parent Row No" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-03 17:50:16.574197", + "modified": "2023-08-04 12:32:28.555867", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator Item", diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 6b2a1b98b622..ad18f578cbde 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -36,7 +36,6 @@ class BOMConfigurator { delete_node: this.delete_node, edit_qty: this.edit_qty, frm: this.frm, - // datatable: this.datatable, } } @@ -76,14 +75,26 @@ class BOMConfigurator { 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 || frm_obj.frm.doc.raw_material_cost; + amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency }); + + $(` +
+
${qty} ${uom}
+
+ ${amount} +
+
- $(`${qty} ${uom} `).insertBefore(node.$ul); }, toolbar: this.frm?.doc.docstatus === 0 ? [ @@ -158,7 +169,25 @@ class BOMConfigurator { }, btnClass: "hidden-xs" }, - ] : [], + ] : [{ + label:__("Expand All"), + click: function(node) { + let view = frappe.views.trees["BOM Configurator"]; + + if (!node.expanded) { + view.tree.load_children(node, true); + $(node.parent[0]).find(".tree-children").show(); + node.$toolbar.find(".expand-all-btn").html("Collapse All"); + } else { + node.$tree_link.trigger("click"); + node.$toolbar.find(".expand-all-btn").html("Expand All"); + } + }, + condition: function(node) { + return node.expandable && node.is_root; + }, + btnClass: "hidden-xs expand-all-btn" + }], } } @@ -178,6 +207,7 @@ class BOMConfigurator { parent: node.data.parent_id, fg_item: node.data.value, item_code: data.item_code, + fg_reference_id: node.data.name || this.frm.doc.name, qty: data.qty, }, callback: (r) => { @@ -209,6 +239,7 @@ class BOMConfigurator { args: { parent: node.data.parent_id, fg_item: node.data.value, + fg_reference_id: node.data.name || this.frm.doc.name, bom_item: bom_item, }, callback: (r) => { @@ -257,6 +288,7 @@ class BOMConfigurator { parent: node.data.parent_id, fg_item: node.data.value, bom_item: bom_item, + fg_reference_id: node.data.name || this.frm.doc.name, convert_to_sub_assembly: true, }, callback: (r) => { From 8b056be7d816a0517ff601ffb124b6b4446271f1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 4 Aug 2023 20:56:20 +0530 Subject: [PATCH 4/9] fix: finished good cost --- .../doctype/bom_creator/bom_creator.py | 9 +++++ .../bom_configurator.bundle.js | 34 ++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 95946a8bdeda..0be2dcf70897 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -250,6 +250,8 @@ def add_item(**kwargs): doc.append("items", kwargs) doc.save() + return doc + @frappe.whitelist() def add_sub_assembly(**kwargs): @@ -305,6 +307,7 @@ def add_sub_assembly(**kwargs): ) doc.save() + return doc @@ -331,6 +334,12 @@ def delete_node(**kwargs): 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_sub_assemblies() + doc.save() + + return doc + @frappe.whitelist() def edit_qty(doctype, docname, qty): diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index ad18f578cbde..308985f84ca3 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -36,6 +36,7 @@ class BOMConfigurator { delete_node: this.delete_node, edit_qty: this.edit_qty, frm: this.frm, + load_tree: this.load_tree, } } @@ -85,12 +86,12 @@ class BOMConfigurator { font-weight:450; margin-right: 40px; display: inline-flex; - width: 128px; + min-width: 128px; border: 1px solid var(--bg-gray); " data-bom-qty-docname="${docname}">
${qty} ${uom}
-
+
${amount}
@@ -110,7 +111,7 @@ class BOMConfigurator { label:__(frappe.utils.icon('add', 'sm') + " Raw Material"), click: function(node) { let view = frappe.views.trees["BOM Configurator"]; - view.events.add_item(node); + view.events.add_item(node, view); }, condition: function(node) { return node.expandable; @@ -191,7 +192,7 @@ class BOMConfigurator { } } - add_item(node) { + add_item(node, view) { frappe.prompt([ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 }, { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 }, @@ -211,7 +212,7 @@ class BOMConfigurator { qty: data.qty, }, callback: (r) => { - frappe.views.trees["BOM Configurator"].tree.load_children(node); + view.events.load_tree(r, node); } }); }, @@ -243,7 +244,7 @@ class BOMConfigurator { bom_item: bom_item, }, callback: (r) => { - frappe.views.trees["BOM Configurator"].tree.load_children(node); + view.events.load_tree(r, node); } }); @@ -293,7 +294,7 @@ class BOMConfigurator { }, callback: (r) => { node.expandable = true; - frappe.views.trees["BOM Configurator"].tree.load_children(node); + view.events.load_tree(r, node); } }); @@ -312,7 +313,7 @@ class BOMConfigurator { docname: node.data.name, }, callback: (r) => { - frappe.views.trees["BOM Configurator"].tree.load_children(node.parent_node); + view.events.load_tree(r, node.parent_node); } }); }); @@ -353,6 +354,23 @@ class BOMConfigurator { $(main_div).find(".tree-children")[0].style.maxHeight = "370px"; $(main_div).find(".tree-children")[0].style.overflowY = "auto"; } + + load_tree(response, node) { + frappe.views.trees["BOM Configurator"].tree.load_children(node); + let parent_dom = $(node.parent.get(0)); + if (node?.parent_node) { + parent_dom = $(node.parent_node.$tree_link.get(0)); + } + + let total_amount = frappe.format( + response.message.raw_material_cost, { + fieldtype: "Currency", + currency: this.frm.doc.currency + } + ); + + $(parent_dom).find(".fg-item-amt").html(total_amount); + } } frappe.ui.BOMConfigurator = BOMConfigurator; \ No newline at end of file From dea033379b2abb006254c46c83ad6acc33995505 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sun, 6 Aug 2023 10:41:18 +0530 Subject: [PATCH 5/9] fix: valuation rate in tree ui --- .../doctype/bom_creator/bom_creator.py | 64 +++++++++---------- .../bom_configurator.bundle.js | 46 +++++++++---- 2 files changed, 65 insertions(+), 45 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 0be2dcf70897..61e3d50f7b35 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -52,46 +52,42 @@ def set_rate_for_items(self): if self.rm_cost_as_per == "Manual": return - self.set_rate_for_raw_materials() - self.set_rate_for_sub_assemblies() + amount = self.get_raw_material_cost() + self.raw_material_cost = amount - def set_rate_for_raw_materials(self): - for row in self.items: - if row.is_expandable: - continue + def get_raw_material_cost(self, fg_reference_id=None, amount=0): + if not fg_reference_id: + fg_reference_id = self.name - 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) - - def set_rate_for_sub_assemblies(self): - sub_assemblies = frappe._dict({}) for row in self.items: - if row.fg_reference_id == self.name: + if row.fg_reference_id != fg_reference_id: continue - sub_assemblies.setdefault((row.fg_reference_id), []).append(flt(row.amount)) - - self.raw_material_cost = 0 - for row in self.items: - if row.name in sub_assemblies: - row.amount = sum(sub_assemblies.get(row.name)) + 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)) - if row.fg_reference_id == self.name: - self.raw_material_cost += flt(row.amount) + amount += flt(row.amount) + + return amount def set_is_expandable(self): fg_items = [row.fg_item for row in self.items] @@ -335,7 +331,7 @@ def delete_node(**kwargs): delete_node(fg_item=item.value, parent=item.parent_id) doc = frappe.get_doc("BOM Creator", kwargs.parent) - doc.set_rate_for_sub_assemblies() + doc.set_rate_for_items() doc.save() return doc diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 308985f84ca3..6b0181a38ff1 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -25,6 +25,7 @@ class BOMConfigurator { }; frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options); + this.tree_view = frappe.views.trees["BOM Configurator"]; } bind_events() { @@ -76,7 +77,11 @@ class BOMConfigurator { 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 || frm_obj.frm.doc.raw_material_cost; + 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 }); $(` @@ -356,20 +361,39 @@ class BOMConfigurator { } load_tree(response, node) { + let item_row = ""; + let parent_dom = "" + let total_amount = response.message.raw_material_cost; + frappe.views.trees["BOM Configurator"].tree.load_children(node); - let parent_dom = $(node.parent.get(0)); - if (node?.parent_node) { - parent_dom = $(node.parent_node.$tree_link.get(0)); - } - let total_amount = frappe.format( - response.message.raw_material_cost, { - fieldtype: "Currency", - currency: this.frm.doc.currency + while (true) { + item_row = response.message.items.filter(item => item.name === node.data.name); + + if (item_row?.length) { + node.data.amount = item_row[0].amount; + total_amount = node.data.amount + } else { + total_amount = response.message.raw_material_cost; + } + + parent_dom = $(node.parent.get(0)); + total_amount = frappe.format( + total_amount, { + fieldtype: "Currency", + currency: this.frm.doc.currency + } + ); + + $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount); + + if (node.is_root) { + break; } - ); - $(parent_dom).find(".fg-item-amt").html(total_amount); + node = node.parent_node; + } + } } From ff730fb7fd50948875c8ecdb56d443f203fb43a3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 7 Aug 2023 11:22:30 +0530 Subject: [PATCH 6/9] chore: conflicts and removed unnecessary files --- erpnext/manufacturing/doctype/bom/bom.json | 18 +- erpnext/manufacturing/doctype/bom/bom.py | 18 ++ .../doctype/bom_configurator_item/__init__.py | 0 .../bom_configurator_item.json | 206 ------------------ .../bom_configurator_item.py | 9 - .../doctype/bom_creator/bom_creator.json | 34 ++- .../doctype/bom_creator/bom_creator.py | 99 +++++++-- .../doctype/bom_creator/bom_creator_list.js | 18 ++ .../bom_creator_item/bom_creator_item.json | 17 +- .../doctype/holiday_list/holiday_list.py | 2 + 10 files changed, 183 insertions(+), 238 deletions(-) delete mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/__init__.py delete mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json delete mode 100644 erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py create mode 100644 erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index b68229334ccf..e8d35428353a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -80,6 +80,8 @@ "web_long_description", "reference_section", "bom_creator", + "bom_creator_item", + "column_break_oxbz", "amended_from", "connections_tab" ], @@ -610,11 +612,23 @@ { "fieldname": "bom_creator", "fieldtype": "Link", - "label": "BOM Configurator", + "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", @@ -622,7 +636,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-08-04 14:27:32.961309", + "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 3ed6fe84bef6..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() diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/__init__.py b/erpnext/manufacturing/doctype/bom_configurator_item/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json deleted file mode 100644 index d3df51ac7814..000000000000 --- a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.json +++ /dev/null @@ -1,206 +0,0 @@ -{ - "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", - "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", - "explode_item_section", - "do_not_explode", - "bom_no", - "instruction_section", - "instruction" - ], - "fields": [ - { - "columns": 2, - "fieldname": "item_code", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Item Code", - "options": "Item" - }, - { - "fieldname": "column_break_f63f", - "fieldtype": "Column Break" - }, - { - "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" - }, - { - "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" - }, - { - "fieldname": "stock_qty", - "fieldtype": "Float", - "label": "Stock Qty", - "read_only": 1 - }, - { - "fieldname": "column_break_bgnb", - "fieldtype": "Column Break" - }, - { - "columns": 1, - "fieldname": "uom", - "fieldtype": "Link", - "in_list_view": 1, - "label": "UOM", - "options": "UOM" - }, - { - "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": "source_warehouse", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Source Warehouse", - "options": "Warehouse" - }, - { - "collapsible": 1, - "fieldname": "instruction_section", - "fieldtype": "Section Break", - "label": "Instruction" - }, - { - "fieldname": "instruction", - "fieldtype": "Small Text" - }, - { - "depends_on": "eval:!doc.do_not_explode", - "fieldname": "bom_no", - "fieldtype": "Link", - "label": "Exploded BOM", - "no_copy": 1, - "options": "BOM" - }, - { - "columns": 2, - "fieldname": "fg_item", - "fieldtype": "Link", - "in_list_view": 1, - "label": "FG Item", - "options": "Item", - "reqd": 1 - }, - { - "fieldname": "explode_item_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Explode Item" - }, - { - "default": "1", - "fieldname": "do_not_explode", - "fieldtype": "Check", - "label": "Do Not Explode" - }, - { - "default": "0", - "fieldname": "is_expandable", - "fieldtype": "Check", - "label": "Is Expandable", - "read_only": 1 - }, - { - "columns": 2, - "fieldname": "rate", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Rate" - }, - { - "fieldname": "amount_section", - "fieldtype": "Section Break", - "label": "Amount" - }, - { - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount", - "read_only": 1 - }, - { - "fieldname": "column_break_yuca", - "fieldtype": "Column Break" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-08-03 17:01:00.173363", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "BOM Configurator Item", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py b/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py deleted file mode 100644 index f623532e7643..000000000000 --- a/erpnext/manufacturing/doctype/bom_configurator_item/bom_configurator_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# 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 BOMConfiguratorItem(Document): - pass diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index 6ad1901b6134..0a494ba930a2 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -39,6 +39,10 @@ "raw_material_cost", "remarks_tab", "remarks", + "section_break_yixm", + "status", + "column_break_irab", + "error_log", "connections_tab", "amended_from" ], @@ -46,7 +50,6 @@ { "fieldname": "company", "fieldtype": "Link", - "in_list_view": 1, "label": "Company", "options": "Company", "remember_last_selected_value": 1, @@ -106,7 +109,6 @@ { "fieldname": "conversion_rate", "fieldtype": "Float", - "in_list_view": 1, "label": "Conversion Rate", "precision": "9", "reqd": 1 @@ -133,6 +135,7 @@ { "fieldname": "raw_material_cost", "fieldtype": "Currency", + "in_list_view": 1, "label": "Total Cost", "no_copy": 1, "options": "currency", @@ -156,6 +159,8 @@ { "fieldname": "item_code", "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, "label": "Finished Good", "options": "Item", "reqd": 1 @@ -250,6 +255,29 @@ "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", @@ -260,7 +288,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2023-08-04 14:29:51.375361", + "modified": "2023-08-07 13:42:20.270471", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 61e3d50f7b35..1791f8ba3a63 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -19,20 +19,65 @@ "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_conversion_factor() self.set_reference_id() self.set_is_expandable() self.set_rate_for_items() + 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} @@ -106,7 +151,19 @@ def validate_fields(self): frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name)) def on_submit(self): - self.create_boms() + 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): """ @@ -120,6 +177,7 @@ def create_boms(self): } """ + 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}) @@ -136,11 +194,22 @@ def create_boms(self): reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items()))) - 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) + 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")) + 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") @@ -151,6 +220,7 @@ def create_bom(self, row, production_item_wise_rm): "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", } ) @@ -165,24 +235,21 @@ def create_bom(self, row, production_item_wise_rm): bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no item.do_not_explode = 0 - bom.append( - "items", + item_args = {} + for field in BOM_ITEM_FIELDS: + item_args[field] = item.get(field) + + item_args.update( { - "item_code": item.item_code, - "qty": item.qty, - "uom": item.uom, - "rate": item.rate, - "conversion_factor": item.conversion_factor, - "stock_qty": item.stock_qty, - "stock_uom": item.stock_uom, - "do_not_explode": item.do_not_explode, "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() 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_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json index 572c51b72d84..fdb5d3ad338f 100644 --- a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json +++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json @@ -14,6 +14,7 @@ "source_warehouse", "is_expandable", "sourced_by_supplier", + "bom_created", "description_section", "description", "quantity_and_rate_section", @@ -203,6 +204,7 @@ "fieldname": "fg_reference_id", "fieldtype": "Data", "label": "FG Reference", + "no_copy": 1, "read_only": 1 }, { @@ -212,13 +214,24 @@ { "fieldname": "parent_row_no", "fieldtype": "Data", - "label": "Parent Row No" + "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-04 12:32:28.555867", + "modified": "2023-08-07 11:52:30.492233", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator Item", diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 526bc2ba4ac2..4b42f839bfb4 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -9,6 +9,8 @@ from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today +from holidays import country_holidays +from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError): From f6a5713abc7df07bd9e09831659082fa14009aee Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 7 Aug 2023 16:22:58 +0530 Subject: [PATCH 7/9] test: test cases for BOM Creator --- .../doctype/bom_creator/bom_creator.json | 6 +- .../doctype/bom_creator/test_bom_creator.py | 235 +++++++++++++++++- 2 files changed, 236 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json index 0a494ba930a2..fb4c6c5c95aa 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json @@ -107,11 +107,11 @@ "reqd": 1 }, { + "default": "1", "fieldname": "conversion_rate", "fieldtype": "Float", "label": "Conversion Rate", - "precision": "9", - "reqd": 1 + "precision": "9" }, { "fieldname": "materials_section", @@ -288,7 +288,7 @@ "link_fieldname": "bom_creator" } ], - "modified": "2023-08-07 13:42:20.270471", + "modified": "2023-08-07 15:45:06.176313", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Creator", diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py index 58f2533102c2..d239d58131d1 100644 --- a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py @@ -1,9 +1,240 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +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): - pass + 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 From 2ac4d46363f7017b967a911c2a3711e6387aee6b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 8 Aug 2023 15:39:01 +0530 Subject: [PATCH 8/9] fix: added shortcut for the BOM Creator --- .../workspace/manufacturing/manufacturing.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index 518ae14659ee..f0bb183e4837 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\":\"ycOi0I1bMV\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Multi-level 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 15:32:20.304160", "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": "Multi-level BOM Creator", + "link_to": "BOM Creator", + "type": "DocType" + }, { "color": "Grey", "doc_view": "List", From 2e7025544d47e6e6e8b6f5d1bd1db351173f46a4 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 8 Aug 2023 16:58:36 +0530 Subject: [PATCH 9/9] fix: added validation for Final Product --- .../doctype/bom_creator/bom_creator.js | 35 ++++++++++++------- .../doctype/bom_creator/bom_creator.py | 21 +++++++++-- .../manufacturing/manufacturing.json | 6 ++-- .../bom_configurator.bundle.js | 28 +++++++++++---- .../doctype/holiday_list/holiday_list.py | 2 -- 5 files changed, 66 insertions(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js index 2c4b786ef428..01dc89b08026 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -13,23 +13,30 @@ frappe.ui.form.on("BOM Creator", { if (!frm.is_new()) { if ((!frappe.bom_configurator || frappe.bom_configurator.bom_configurator !== frm.doc.name)) { - let $parent = $(frm.fields_dict["bom_creator"].wrapper); - $parent.empty(); - - 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, - }); - }); + 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"), @@ -122,7 +129,11 @@ frappe.ui.form.on("BOM Creator", { }, add_custom_buttons(frm) { - // + if (!frm.is_new()) { + frm.add_custom_button(__("Rebuild Tree"), () => { + frm.trigger("build_tree"); + }); + } } }); diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py index 1791f8ba3a63..999d610dfae5 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py @@ -34,11 +34,19 @@ class BOMCreator(Document): def before_save(self): self.set_status() + self.set_is_expandable() self.set_conversion_factor() self.set_reference_id() - self.set_is_expandable() 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", @@ -135,7 +143,7 @@ def get_raw_material_cost(self, fg_reference_id=None, amount=0): return amount def set_is_expandable(self): - fg_items = [row.fg_item for row in self.items] + 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: @@ -231,6 +239,7 @@ def create_bom(self, row, production_item_wise_rm): 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 @@ -343,6 +352,7 @@ def add_sub_assembly(**kwargs): "stock_qty": bom_item.qty, "fg_reference_id": name, "do_not_explode": 1, + "is_expandable": 1, "stock_uom": item_info.stock_uom, }, ) @@ -405,5 +415,10 @@ def delete_node(**kwargs): @frappe.whitelist() -def edit_qty(doctype, docname, qty): +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/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json index f0bb183e4837..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\":\"ycOi0I1bMV\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Multi-level 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}}]", + "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-08-08 15:32:20.304160", + "modified": "2023-08-08 22:28:39.633891", "modified_by": "Administrator", "module": "Manufacturing", "name": "Manufacturing", @@ -339,7 +339,7 @@ { "color": "Grey", "doc_view": "List", - "label": "Multi-level BOM Creator", + "label": "BOM Creator", "link_to": "BOM Creator", "type": "DocType" }, diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js index 6b0181a38ff1..b3b2e9f9b86f 100644 --- a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js +++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js @@ -30,14 +30,15 @@ class BOMConfigurator { 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, - frm: this.frm, load_tree: this.load_tree, + set_default_qty: this.set_default_qty, } } @@ -93,9 +94,8 @@ class BOMConfigurator { display: inline-flex; min-width: 128px; border: 1px solid var(--bg-gray); - " - data-bom-qty-docname="${docname}"> -
${qty} ${uom}
+ "> +
${qty} ${uom}
${amount}
@@ -108,7 +108,7 @@ class BOMConfigurator { label:__(frappe.utils.icon('edit', 'sm') + " Qty"), click: function(node) { let view = frappe.views.trees["BOM Configurator"]; - view.events.edit_qty(node); + view.events.edit_qty(node, view); }, btnClass: "hidden-xs" }, @@ -232,6 +232,7 @@ class BOMConfigurator { }); dialog.show(); + view.events.set_default_qty(dialog); dialog.set_primary_action(__("Add"), () => { let bom_item = dialog.get_values(); @@ -285,6 +286,8 @@ class BOMConfigurator { }); dialog.show(); + view.events.set_default_qty(dialog); + dialog.set_primary_action(__("Add"), () => { let bom_item = dialog.get_values(); @@ -307,6 +310,17 @@ class BOMConfigurator { }); } + set_default_qty(dialog) { + dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) { + if (event) { + let name = $(event.currentTarget).closest('.grid-row').attr("data-name") + let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc; + item_row.qty = 1; + dialog.fields_dict.items.grid.refresh() + } + } + } + delete_node(node, view) { frappe.confirm(__("Are you sure you want to delete this Item?"), () => { frappe.call({ @@ -324,7 +338,7 @@ class BOMConfigurator { }); } - edit_qty(node) { + edit_qty(node, view) { let qty = node.data.qty || this.frm.doc.qty; frappe.prompt([ { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 }, @@ -339,11 +353,13 @@ class BOMConfigurator { doctype: doctype, docname: docname, qty: data.qty, + parent: node.data.parent_id, }, callback: (r) => { node.data.qty = data.qty; let uom = node.data.uom || this.frm.doc.uom; $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom); + view.events.load_tree(r, node); } }); }, diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 4b42f839bfb4..526bc2ba4ac2 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -9,8 +9,6 @@ from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today -from holidays import country_holidays -from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError):