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