diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 5fbc460a97..f6a8d27d44 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -188,12 +188,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals # scan description only if items are less than 50000 description_cond = 'or tabItem.description LIKE %(txt)s' - extra_cond = " and tabItem.has_variants=0" - if (filters and isinstance(filters, dict) - and filters.get("doctype") == "BOM"): - extra_cond = "" - del filters["doctype"] - return frappe.db.sql("""select tabItem.name, if(length(tabItem.item_name) > 40, concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name, @@ -204,10 +198,10 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals from tabItem where tabItem.docstatus < 2 and tabItem.disabled=0 + and tabItem.has_variants=0 and (tabItem.end_of_life > %(today)s or ifnull(tabItem.end_of_life, '0000-00-00')='0000-00-00') and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s) {description_cond}) - {extra_cond} {fcond} {mcond} order by if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), @@ -218,7 +212,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals key=searchfield, columns=columns, scond=searchfields, - extra_cond=extra_cond, fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'), description_cond = description_cond), diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 898955e51b..47b4207241 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -29,10 +29,7 @@ frappe.ui.form.on("BOM", { frm.set_query("item", function() { return { - query: "erpnext.controllers.queries.item_query", - filters: { - "doctype": "BOM" - } + query: "erpnext.manufacturing.doctype.bom.bom.item_query" }; }); @@ -44,9 +41,12 @@ frappe.ui.form.on("BOM", { }; }); - frm.set_query("item_code", "items", function() { + frm.set_query("item_code", "items", function(doc) { return { - query: "erpnext.controllers.queries.item_query" + query: "erpnext.manufacturing.doctype.bom.bom.item_query", + filters: { + "item_code": doc.item + } }; }); @@ -96,6 +96,12 @@ frappe.ui.form.on("BOM", { frm.trigger("make_work_order"); }, __("Create")); + if (frm.doc.has_variants) { + frm.add_custom_button(__("Variant BOM"), function() { + frm.trigger("make_variant_bom"); + }, __("Create")); + } + if (frm.doc.inspection_required) { frm.add_custom_button(__("Quality Inspection"), function() { frm.trigger("make_quality_inspection"); @@ -124,7 +130,7 @@ frappe.ui.form.on("BOM", { } - if (frm.doc.__onload && frm.doc.__onload["has_variants"]) { + if (frm.doc.has_variants) { frm.set_intro(__('This is a Template BOM and will be used to make the work order for {0} of the item {1}', [ `variants`, @@ -138,9 +144,52 @@ frappe.ui.form.on("BOM", { }, make_work_order: function(frm) { + frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", + args: { + bom_no: frm.doc.name, + item: item, + qty: data.qty || 0.0, + project: frm.doc.project, + variant_items: variant_items + }, + freeze: true, + callback: function(r) { + if(r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }); + }, + + make_variant_bom: function(frm) { + frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { + frappe.call({ + method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", + args: { + source_name: frm.doc.name, + bom_no: frm.doc.name, + item: item, + variant_items: variant_items + }, + freeze: true, + callback: function(r) { + if(r.message) { + let doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, true); + }, + + setup_variant_prompt: function(frm, title, callback, skip_qty_field) { const fields = []; - if (frm.doc.__onload && frm.doc.__onload["has_variants"]) { + if (frm.doc.has_variants) { fields.push({ fieldtype: 'Link', label: __('Variant Item'), @@ -158,34 +207,106 @@ frappe.ui.form.on("BOM", { }); } - fields.push({ - fieldtype: 'Float', - label: __('Qty To Manufacture'), - fieldname: 'qty', - reqd: 1, - default: 1 + if (!skip_qty_field) { + fields.push({ + fieldtype: 'Float', + label: __('Qty To Manufacture'), + fieldname: 'qty', + reqd: 1, + default: 1 + }); + } + + var has_template_rm = frm.doc.items.filter(d => d.has_variants === 1) || []; + if (has_template_rm && has_template_rm.length > 0) { + fields.push({ + fieldname: "items", + fieldtype: "Table", + label: __("Raw Materials"), + fields: [ + { + fieldname: "item_code", + options: "Item", + label: __("Template Item"), + fieldtype: "Link", + in_list_view: 1, + reqd: 1, + }, + { + fieldname: "varint_item_code", + options: "Item", + label: __("Variant Item"), + fieldtype: "Link", + in_list_view: 1, + reqd: 1, + get_query: function(data) { + if (!data.item_code) { + frappe.throw(__("Select template item")); + } + + return { + query: "erpnext.controllers.queries.item_query", + filters: { + "variant_of": data.item_code + } + }; + } + }, + { + fieldname: "qty", + label: __("Quantity"), + fieldtype: "Float", + in_list_view: 1, + reqd: 1, + }, + { + fieldname: "source_warehouse", + label: __("Source Warehouse"), + fieldtype: "Link", + options: "Warehouse" + }, + { + fieldname: "operation", + label: __("Operation"), + fieldtype: "Data", + hidden: 1, + } + ], + in_place_edit: true, + data: [], + get_data: function () { + return []; + }, + }); + } + + let dialog = frappe.prompt(fields, data => { + let item = data.item || frm.doc.item; + let variant_items = data.items || []; + + variant_items.forEach(d => { + if (!d.varint_item_code) { + frappe.throw(__("Select variant item code for the template item {0}", [d.item_code])); + } + }) + + callback(frm, item, data, variant_items); + + }, __(title), __("Create")); + + has_template_rm.forEach(d => { + dialog.fields_dict.items.df.data.push({ + "item_code": d.item_code, + "varint_item_code": "", + "qty": d.qty, + "source_warehouse": d.source_warehouse, + "operation": d.operation + }); }); - frappe.prompt(fields, data => { - let item = data.item || frm.doc.item; - - frappe.call({ - method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", - args: { - bom_no: frm.doc.name, - item: item, - qty: data.qty || 0.0, - project: frm.doc.project - }, - freeze: true, - callback: function(r) { - if(r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); - } - } - }); - }, __("Enter Value"), __("Create")); + if (has_template_rm) { + dialog.fields_dict.items.grid.refresh(); + } }, make_quality_inspection: function(frm) { diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index 4ce0ecf3f2..f551b91597 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -53,6 +53,7 @@ "section_break_25", "description", "column_break_27", + "has_variants", "section_break0", "exploded_items", "website_section", @@ -498,6 +499,17 @@ "options": "Currency", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item.has_variants", + "fieldname": "has_variants", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Has Variants", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "icon": "fa fa-sitemap", @@ -505,7 +517,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-05-05 14:29:32.634952", + "modified": "2020-05-21 12:29:32.634952", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6ac653e37a..1bdac5731e 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -3,13 +3,16 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, today from frappe import _ from erpnext.setup.utils import get_exchange_rate from frappe.website.website_generator import WebsiteGenerator from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_price_list_rate from frappe.core.doctype.version.version import get_diff +from erpnext.controllers.queries import get_match_cond +from erpnext.stock.doctype.item.item import get_item_details +from frappe.model.mapper import get_mapped_doc import functools @@ -59,11 +62,6 @@ class BOM(WebsiteGenerator): self.name = name - def onload(self): - super(BOM, self).onload() - if self.get("item") and cint(frappe.db.get_value("Item", self.item, "has_variants")): - self.set_onload("has_variants", True) - def validate(self): self.route = frappe.scrub(self.name).replace('_', '-') self.clear_operations() @@ -103,9 +101,7 @@ class BOM(WebsiteGenerator): self.manage_default_bom() def get_item_det(self, item_code): - item = frappe.db.sql("""select name, item_name, docstatus, description, image, - is_sub_contracted_item, stock_uom, default_bom, last_purchase_rate, include_item_in_manufacturing - from `tabItem` where name=%s""", item_code, as_dict = 1) + item = get_item_details(item_code) if not item: frappe.throw(_("Item: {0} does not exist in the system").format(item_code)) @@ -150,10 +146,10 @@ class BOM(WebsiteGenerator): item = self.get_item_det(args['item_code']) - args['bom_no'] = args['bom_no'] or item and cstr(item[0]['default_bom']) or '' + args['bom_no'] = args['bom_no'] or item and cstr(item['default_bom']) or '' args['transfer_for_manufacture'] = (cstr(args.get('include_item_in_manufacturing', '')) or - item and item[0].include_item_in_manufacturing or 0) - args.update(item[0]) + item and item.include_item_in_manufacturing or 0) + args.update(item) rate = self.get_rm_rate(args) ret_item = { @@ -185,40 +181,14 @@ class BOM(WebsiteGenerator): self.rm_cost_as_per = "Valuation Rate" if arg.get('scrap_items'): - rate = self.get_valuation_rate(arg) + rate = get_valuation_rate(arg) elif arg: #Customer Provided parts will have zero rate if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): if arg.get('bom_no') and self.set_rate_of_sub_assembly_item_based_on_bom: rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1) else: - if self.rm_cost_as_per == 'Valuation Rate': - rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1) - elif self.rm_cost_as_per == 'Last Purchase Rate': - rate = flt(arg.get('last_purchase_rate') \ - or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \ - * (arg.get("conversion_factor") or 1) - elif self.rm_cost_as_per == "Price List": - if not self.buying_price_list: - frappe.throw(_("Please select Price List")) - args = frappe._dict({ - "doctype": "BOM", - "price_list": self.buying_price_list, - "qty": arg.get("qty") or 1, - "uom": arg.get("uom") or arg.get("stock_uom"), - "stock_uom": arg.get("stock_uom"), - "transaction_type": "buying", - "company": self.company, - "currency": self.currency, - "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function - "conversion_factor": arg.get("conversion_factor") or 1, - "plc_conversion_rate": 1, - "ignore_party": True - }) - item_doc = frappe.get_doc("Item", arg.get("item_code")) - out = frappe._dict() - get_price_list_rate(args, item_doc, out) - rate = out.price_list_rate + rate = get_bom_item_rate(arg, self) if not rate: if self.rm_cost_as_per == "Price List": @@ -286,31 +256,6 @@ class BOM(WebsiteGenerator): where is_active = 1 and name = %s""", bom_no, as_dict=1) return bom and bom[0]['unit_cost'] or 0 - def get_valuation_rate(self, args): - """ Get weighted average of valuation rate from all warehouses """ - - total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 - for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` - where item_code=%s""", args['item_code'], as_dict=1): - total_qty += flt(d.actual_qty) - total_value += flt(d.stock_value) - - if total_qty: - valuation_rate = total_value / total_qty - - if valuation_rate <= 0: - last_valuation_rate = frappe.db.sql("""select valuation_rate - from `tabStock Ledger Entry` - where item_code = %s and valuation_rate > 0 - order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) - - valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 - - if not valuation_rate: - valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") - - return flt(valuation_rate) - def manage_default_bom(self): """ Uncheck others if current one is selected as default or check the current one as default if it the only bom for the selected item, @@ -624,6 +569,62 @@ class BOM(WebsiteGenerator): if not d.batch_size or d.batch_size <= 0: d.batch_size = 1 +def get_bom_item_rate(args, bom_doc): + if bom_doc.rm_cost_as_per == 'Valuation Rate': + rate = get_valuation_rate(args) * (args.get("conversion_factor") or 1) + elif bom_doc.rm_cost_as_per == 'Last Purchase Rate': + rate = ( flt(args.get('last_purchase_rate')) \ + or frappe.db.get_value("Item", args['item_code'], "last_purchase_rate")) \ + * (args.get("conversion_factor") or 1) + elif bom_doc.rm_cost_as_per == "Price List": + if not bom_doc.buying_price_list: + frappe.throw(_("Please select Price List")) + bom_args = frappe._dict({ + "doctype": "BOM", + "price_list": bom_doc.buying_price_list, + "qty": args.get("qty") or 1, + "uom": args.get("uom") or args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "transaction_type": "buying", + "company": bom_doc.company, + "currency": bom_doc.currency, + "conversion_rate": 1, # Passed conversion rate as 1 purposefully, as conversion rate is applied at the end of the function + "conversion_factor": args.get("conversion_factor") or 1, + "plc_conversion_rate": 1, + "ignore_party": True + }) + item_doc = frappe.get_cached_doc("Item", args.get("item_code")) + out = frappe._dict() + get_price_list_rate(bom_args, item_doc, out) + rate = out.price_list_rate + + return rate + +def get_valuation_rate(args): + """ Get weighted average of valuation rate from all warehouses """ + + total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0 + for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin` + where item_code=%s""", args['item_code'], as_dict=1): + total_qty += flt(d.actual_qty) + total_value += flt(d.stock_value) + + if total_qty: + valuation_rate = total_value / total_qty + + if valuation_rate <= 0: + last_valuation_rate = frappe.db.sql("""select valuation_rate + from `tabStock Ledger Entry` + where item_code = %s and valuation_rate > 0 + order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) + + valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 + + if not valuation_rate: + valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") + + return flt(valuation_rate) + def get_list_context(context): context.title = _("Bill of Materials") # context.introduction = _('Boms') @@ -639,6 +640,8 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty, item.image, bom.project, + bom_item.rate, + bom_item.amount, item.stock_uom, item.item_group, item.allow_alternative_item, @@ -655,6 +658,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite where bom_item.docstatus < 2 and bom.name = %(bom)s + and ifnull(item.has_variants, 0) = 0 and item.is_stock_item in (1, {is_stock_item}) {where_conditions} group by item_code, stock_uom @@ -897,3 +901,84 @@ def get_bom_diff(bom1, bom2): out.removed.append([df.fieldname, d.as_dict()]) return out + +def item_query(doctype, txt, searchfield, start, page_len, filters): + meta = frappe.get_meta("Item", cached=True) + searchfields = meta.get_search_fields() + + order_by = "idx desc, name, item_name" + + fields = ["name", "item_group", "item_name", "description"] + fields.extend([field for field in searchfields + if not field in ["name", "item_group", "description"]]) + + searchfields = searchfields + [field for field in [searchfield or "name", "item_code", "item_group", "item_name"] + if not field in searchfields] + + query_filters = { + "disabled": 0, + "ifnull(end_of_life, '5050-50-50')": (">", today()) + } + + or_cond_filters = {} + if txt: + for s_field in searchfields: + or_cond_filters[s_field] = ("like", "%{0}%".format(txt)) + + barcodes = frappe.get_all("Item Barcode", + fields=["distinct parent as item_code"], + filters = {"barcode": ("like", "%{0}%".format(txt))}) + + barcodes = [d.item_code for d in barcodes] + if barcodes: + or_cond_filters["name"] = ("in", barcodes) + + for cond in get_match_cond(doctype, as_condition=False): + for key, value in cond.items(): + if key == doctype: + key = "name" + + query_filters[key] = ("in", value) + + if filters and filters.get("item_code"): + has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") + if not has_variants: + query_filters["has_variants"] = 0 + + return frappe.get_all("Item", + fields = fields, filters=query_filters, + or_filters = or_cond_filters, order_by=order_by, + limit_start=start, limit_page_length=page_len, as_list=1) + +@frappe.whitelist() +def make_variant_bom(source_name, bom_no, item, variant_items, target_doc=None): + from erpnext.manufacturing.doctype.work_order.work_order import add_variant_item + + def postprocess(source, doc): + doc.item = item + doc.quantity = 1 + + item_data = get_item_details(item) + doc.update({ + "item_name": item_data.item_name, + "description": item_data.description, + "uom": item_data.stock_uom, + "allow_alternative_item": item_data.allow_alternative_item + }) + + add_variant_item(variant_items, doc, source_name) + + doc = get_mapped_doc('BOM', source_name, { + 'BOM': { + 'doctype': 'BOM', + 'validation': { + 'docstatus': ['=', 1] + } + }, + 'BOM Item': { + 'doctype': 'BOM Item', + 'condition': lambda doc: doc.has_variants == 0 + }, + }, target_doc, postprocess) + + return doc \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js index 2b06ed72ed..94cb466bd8 100644 --- a/erpnext/manufacturing/doctype/bom/bom_list.js +++ b/erpnext/manufacturing/doctype/bom/bom_list.js @@ -1,7 +1,9 @@ frappe.listview_settings['BOM'] = { - add_fields: ["is_active", "is_default", "total_cost"], + add_fields: ["is_active", "is_default", "total_cost", "has_variants"], get_indicator: function(doc) { - if(doc.is_default) { + if(doc.is_active && doc.has_variants) { + return [__("Template"), "orange", "has_variants,=,Yes"]; + } else if(doc.is_default) { return [__("Default"), "green", "is_default,=,Yes"]; } else if(doc.is_active) { return [__("Active"), "blue", "is_active,=,Yes"]; diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index f094be4c64..e34be61bc7 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -1,8 +1,10 @@ { + "actions": [], "creation": "2013-02-22 01:27:49", "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "item_code", "item_name", @@ -33,6 +35,7 @@ "scrap", "qty_consumed_per_unit", "section_break_27", + "has_variants", "include_item_in_manufacturing", "original_item" ], @@ -57,6 +60,7 @@ "label": "Item Name" }, { + "depends_on": "eval:parent.with_operations == 1", "fieldname": "operation", "fieldtype": "Link", "label": "Item operation", @@ -258,11 +262,22 @@ "label": "Original Item", "options": "Item", "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item_code.has_variants", + "fieldname": "has_variants", + "fieldtype": "Check", + "label": "Has Variants", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-11-22 11:38:52.087303", + "links": [], + "modified": "2020-04-09 14:30:26.535546", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index c125571960..a244f582c4 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -449,6 +449,32 @@ frappe.ui.form.on("Work Order Item", { } }); } + }, + + item_code: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.item_code) { + frappe.call({ + method: "erpnext.stock.doctype.item.item.get_item_details", + args: { + item_code: row.item_code, + company: frm.doc.company + }, + callback: function(r) { + if (r.message) { + frappe.model.set_value(cdt, cdn, { + "required_qty": 1, + "item_name": r.message.item_name, + "description": r.message.description, + "source_warehouse": r.message.default_warehouse, + "allow_alternative_item": r.message.allow_alternative_item, + "include_item_in_manufacturing": r.message.include_item_in_manufacturing + }); + } + } + }); + } } }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c2789559b0..e2233a3e2f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -8,9 +8,9 @@ import math from frappe import _ from frappe.utils import flt, get_datetime, getdate, date_diff, cint, nowdate, get_link_to_form, time_diff_in_hours from frappe.model.document import Document -from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict +from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items_as_dict, get_bom_item_rate from dateutil.relativedelta import relativedelta -from erpnext.stock.doctype.item.item import validate_end_of_life +from erpnext.stock.doctype.item.item import validate_end_of_life, get_item_defaults from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations @@ -541,6 +541,8 @@ class WorkOrder(Document): # For instance in BOM Explosion Item child table, the items coming from sub assembly items for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999): self.append('required_items', { + 'rate': item.rate, + 'amount': item.amount, 'operation': item.operation, 'item_code': item.item_code, 'item_name': item.item_name, @@ -637,9 +639,10 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): filters = filters, fields = ['operation'], as_list=1) @frappe.whitelist() -def get_item_details(item, project = None): +def get_item_details(item, project = None, skip_bom_info=False): res = frappe.db.sql(""" - select stock_uom, description + select stock_uom, description, item_name, allow_alternative_item, + include_item_in_manufacturing from `tabItem` where disabled=0 and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s) @@ -650,6 +653,7 @@ def get_item_details(item, project = None): return {} res = res[0] + if skip_bom_info: return res filters = {"item": item, "is_default": 1} @@ -681,7 +685,7 @@ def get_item_details(item, project = None): return res @frappe.whitelist() -def make_work_order(bom_no, item, qty=0, project=None): +def make_work_order(bom_no, item, qty=0, project=None, variant_items=None): if not frappe.has_permission("Work Order", "write"): frappe.throw(_("Not permitted"), frappe.PermissionError) @@ -696,8 +700,44 @@ def make_work_order(bom_no, item, qty=0, project=None): wo_doc.qty = flt(qty) wo_doc.get_items_and_operations_from_bom() + if variant_items: + add_variant_item(variant_items, wo_doc, bom_no, "required_items") + return wo_doc +def add_variant_item(variant_items, wo_doc, bom_no, table_name="items"): + if isinstance(variant_items, string_types): + variant_items = json.loads(variant_items) + + for item in variant_items: + args = frappe._dict({ + "item_code": item.get("varint_item_code"), + "required_qty": item.get("qty"), + "qty": item.get("qty"), # for bom + "source_warehouse": item.get("source_warehouse"), + "operation": item.get("operation") + }) + + bom_doc = frappe.get_cached_doc("BOM", bom_no) + item_data = get_item_details(args.item_code, skip_bom_info=True) + args.update(item_data) + + args["rate"] = get_bom_item_rate({ + "item_code": args.get("item_code"), + "qty": args.get("required_qty"), + "uom": args.get("stock_uom"), + "stock_uom": args.get("stock_uom"), + "conversion_factor": 1 + }, bom_doc) + + if not args.source_warehouse: + args["source_warehouse"] = get_item_defaults(item.get("varint_item_code"), + wo_doc.company).default_warehouse + + args["amount"] = flt(args.get("required_qty")) * flt(args.get("rate")) + args["uom"] = item_data.stock_uom + wo_doc.append(table_name, args) + @frappe.whitelist() def check_if_scrap_warehouse_mandatory(bom_no): res = {"set_scrap_wh_mandatory": False } diff --git a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json index 4442162636..3acf5727d1 100644 --- a/erpnext/manufacturing/doctype/work_order_item/work_order_item.json +++ b/erpnext/manufacturing/doctype/work_order_item/work_order_item.json @@ -1,526 +1,144 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-04-18 07:38:26.314642", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-04-18 07:38:26.314642", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "operation", + "item_code", + "source_warehouse", + "column_break_3", + "item_name", + "description", + "allow_alternative_item", + "include_item_in_manufacturing", + "qty_section", + "required_qty", + "rate", + "amount", + "column_break_11", + "transferred_qty", + "consumed_qty", + "available_qty_at_source_warehouse", + "available_qty_at_wip_warehouse" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "operation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Operation", - "length": 0, - "no_copy": 0, - "options": "Operation", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "operation", + "fieldtype": "Link", + "label": "Operation", + "options": "Operation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "source_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Source Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "qty_section", + "fieldtype": "Section Break", + "label": "Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Required Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!parent.skip_transfer", - "fieldname": "transferred_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Transferred Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!parent.skip_transfer", + "fieldname": "transferred_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Transferred Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_alternative_item", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allow Alternative Item", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "label": "Allow Alternative Item" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "include_item_in_manufacturing", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Include Item In Manufacturing", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "fieldname": "include_item_in_manufacturing", + "fieldtype": "Check", + "label": "Include Item In Manufacturing" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:!parent.skip_transfer", - "fieldname": "consumed_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Consumed Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!parent.skip_transfer", + "fieldname": "consumed_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Consumed Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "available_qty_at_source_warehouse", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Available Qty at Source Warehouse", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "available_qty_at_source_warehouse", + "fieldtype": "Float", + "label": "Available Qty at Source Warehouse", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "available_qty_at_wip_warehouse", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Available Qty at WIP Warehouse", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "available_qty_at_wip_warehouse", + "fieldtype": "Float", + "label": "Available Qty at WIP Warehouse", + "read_only": 1 + }, + { + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-11-20 19:04:38.508839", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Work Order Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-04-13 18:46:32.966416", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Work Order Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7a1c1279ea..3436a5d013 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -1143,6 +1143,17 @@ def set_item_default(item_code, company, fieldname, value): d.db_insert() item.clear_cache() +@frappe.whitelist() +def get_item_details(item_code, company=None): + out = frappe._dict() + if company: + out = get_item_defaults(item_code, company) or frappe._dict() + + doc = frappe.get_cached_doc("Item", item_code) + out.update(doc.as_dict()) + + return out + @frappe.whitelist() def get_uom_conv_factor(uom, stock_uom): uoms = [uom, stock_uom]