feat: BOM template (#21262)

Co-authored-by: Marica <maricadsouza221197@gmail.com>
This commit is contained in:
rohitwaghchaure
2020-05-21 18:10:13 +05:30
committed by GitHub
parent 5cef8db4db
commit 7978907cc8
10 changed files with 546 additions and 623 deletions

View File

@@ -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 # scan description only if items are less than 50000
description_cond = 'or tabItem.description LIKE %(txt)s' 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, return frappe.db.sql("""select tabItem.name,
if(length(tabItem.item_name) > 40, if(length(tabItem.item_name) > 40,
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name, 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 from tabItem
where tabItem.docstatus < 2 where tabItem.docstatus < 2
and tabItem.disabled=0 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 (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) and ({scond} or tabItem.item_code IN (select parent from `tabItem Barcode` where barcode LIKE %(txt)s)
{description_cond}) {description_cond})
{extra_cond}
{fcond} {mcond} {fcond} {mcond}
order by order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), 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, key=searchfield,
columns=columns, columns=columns,
scond=searchfields, scond=searchfields,
extra_cond=extra_cond,
fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'), fcond=get_filters_cond(doctype, filters, conditions).replace('%', '%%'),
mcond=get_match_cond(doctype).replace('%', '%%'), mcond=get_match_cond(doctype).replace('%', '%%'),
description_cond = description_cond), description_cond = description_cond),

View File

@@ -29,10 +29,7 @@ frappe.ui.form.on("BOM", {
frm.set_query("item", function() { frm.set_query("item", function() {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.manufacturing.doctype.bom.bom.item_query"
filters: {
"doctype": "BOM"
}
}; };
}); });
@@ -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 { 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"); frm.trigger("make_work_order");
}, __("Create")); }, __("Create"));
if (frm.doc.has_variants) {
frm.add_custom_button(__("Variant BOM"), function() {
frm.trigger("make_variant_bom");
}, __("Create"));
}
if (frm.doc.inspection_required) { if (frm.doc.inspection_required) {
frm.add_custom_button(__("Quality Inspection"), function() { frm.add_custom_button(__("Quality Inspection"), function() {
frm.trigger("make_quality_inspection"); 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}', frm.set_intro(__('This is a Template BOM and will be used to make the work order for {0} of the item {1}',
[ [
`<a class="variants-intro">variants</a>`, `<a class="variants-intro">variants</a>`,
@@ -138,9 +144,52 @@ frappe.ui.form.on("BOM", {
}, },
make_work_order: function(frm) { 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 = []; const fields = [];
if (frm.doc.__onload && frm.doc.__onload["has_variants"]) { if (frm.doc.has_variants) {
fields.push({ fields.push({
fieldtype: 'Link', fieldtype: 'Link',
label: __('Variant Item'), label: __('Variant Item'),
@@ -158,6 +207,7 @@ frappe.ui.form.on("BOM", {
}); });
} }
if (!skip_qty_field) {
fields.push({ fields.push({
fieldtype: 'Float', fieldtype: 'Float',
label: __('Qty To Manufacture'), label: __('Qty To Manufacture'),
@@ -165,27 +215,98 @@ frappe.ui.form.on("BOM", {
reqd: 1, reqd: 1,
default: 1 default: 1
}); });
}
frappe.prompt(fields, data => { var has_template_rm = frm.doc.items.filter(d => d.has_variants === 1) || [];
let item = data.item || frm.doc.item; if (has_template_rm && has_template_rm.length > 0) {
fields.push({
frappe.call({ fieldname: "items",
method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", fieldtype: "Table",
args: { label: __("Raw Materials"),
bom_no: frm.doc.name, fields: [
item: item, {
qty: data.qty || 0.0, fieldname: "item_code",
project: frm.doc.project options: "Item",
label: __("Template Item"),
fieldtype: "Link",
in_list_view: 1,
reqd: 1,
}, },
freeze: true, {
callback: function(r) { fieldname: "varint_item_code",
if(r.message) { options: "Item",
var doc = frappe.model.sync(r.message)[0]; label: __("Variant Item"),
frappe.set_route("Form", doc.doctype, doc.name); 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 [];
},
}); });
}, __("Enter Value"), __("Create")); }
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
});
});
if (has_template_rm) {
dialog.fields_dict.items.grid.refresh();
}
}, },
make_quality_inspection: function(frm) { make_quality_inspection: function(frm) {

View File

@@ -53,6 +53,7 @@
"section_break_25", "section_break_25",
"description", "description",
"column_break_27", "column_break_27",
"has_variants",
"section_break0", "section_break0",
"exploded_items", "exploded_items",
"website_section", "website_section",
@@ -498,6 +499,17 @@
"options": "Currency", "options": "Currency",
"print_hide": 1, "print_hide": 1,
"read_only": 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", "icon": "fa fa-sitemap",
@@ -505,7 +517,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-05 14:29:32.634952", "modified": "2020-05-21 12:29:32.634952",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@@ -3,13 +3,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.utils import cint, cstr, flt from frappe.utils import cint, cstr, flt, today
from frappe import _ from frappe import _
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from frappe.website.website_generator import WebsiteGenerator 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_conversion_factor
from erpnext.stock.get_item_details import get_price_list_rate from erpnext.stock.get_item_details import get_price_list_rate
from frappe.core.doctype.version.version import get_diff 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 import functools
@@ -59,11 +62,6 @@ class BOM(WebsiteGenerator):
self.name = name 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): def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-') self.route = frappe.scrub(self.name).replace('_', '-')
self.clear_operations() self.clear_operations()
@@ -103,9 +101,7 @@ class BOM(WebsiteGenerator):
self.manage_default_bom() self.manage_default_bom()
def get_item_det(self, item_code): def get_item_det(self, item_code):
item = frappe.db.sql("""select name, item_name, docstatus, description, image, item = get_item_details(item_code)
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)
if not item: if not item:
frappe.throw(_("Item: {0} does not exist in the system").format(item_code)) 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']) 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 args['transfer_for_manufacture'] = (cstr(args.get('include_item_in_manufacturing', '')) or
item and item[0].include_item_in_manufacturing or 0) item and item.include_item_in_manufacturing or 0)
args.update(item[0]) args.update(item)
rate = self.get_rm_rate(args) rate = self.get_rm_rate(args)
ret_item = { ret_item = {
@@ -185,40 +181,14 @@ class BOM(WebsiteGenerator):
self.rm_cost_as_per = "Valuation Rate" self.rm_cost_as_per = "Valuation Rate"
if arg.get('scrap_items'): if arg.get('scrap_items'):
rate = self.get_valuation_rate(arg) rate = get_valuation_rate(arg)
elif arg: elif arg:
#Customer Provided parts will have zero rate #Customer Provided parts will have zero rate
if not frappe.db.get_value('Item', arg["item_code"], 'is_customer_provided_item'): 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: 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) rate = flt(self.get_bom_unitcost(arg['bom_no'])) * (arg.get("conversion_factor") or 1)
else: else:
if self.rm_cost_as_per == 'Valuation Rate': rate = get_bom_item_rate(arg, self)
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
if not rate: if not rate:
if self.rm_cost_as_per == "Price List": 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) where is_active = 1 and name = %s""", bom_no, as_dict=1)
return bom and bom[0]['unit_cost'] or 0 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): def manage_default_bom(self):
""" Uncheck others if current one is selected as default or """ 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, 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: if not d.batch_size or d.batch_size <= 0:
d.batch_size = 1 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): def get_list_context(context):
context.title = _("Bill of Materials") context.title = _("Bill of Materials")
# context.introduction = _('Boms') # 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, sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * %(qty)s as qty,
item.image, item.image,
bom.project, bom.project,
bom_item.rate,
bom_item.amount,
item.stock_uom, item.stock_uom,
item.item_group, item.item_group,
item.allow_alternative_item, 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 where
bom_item.docstatus < 2 bom_item.docstatus < 2
and bom.name = %(bom)s and bom.name = %(bom)s
and ifnull(item.has_variants, 0) = 0
and item.is_stock_item in (1, {is_stock_item}) and item.is_stock_item in (1, {is_stock_item})
{where_conditions} {where_conditions}
group by item_code, stock_uom group by item_code, stock_uom
@@ -897,3 +901,84 @@ def get_bom_diff(bom1, bom2):
out.removed.append([df.fieldname, d.as_dict()]) out.removed.append([df.fieldname, d.as_dict()])
return out 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

View File

@@ -1,7 +1,9 @@
frappe.listview_settings['BOM'] = { 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) { 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"]; return [__("Default"), "green", "is_default,=,Yes"];
} else if(doc.is_active) { } else if(doc.is_active) {
return [__("Active"), "blue", "is_active,=,Yes"]; return [__("Active"), "blue", "is_active,=,Yes"];

View File

@@ -1,8 +1,10 @@
{ {
"actions": [],
"creation": "2013-02-22 01:27:49", "creation": "2013-02-22 01:27:49",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [ "field_order": [
"item_code", "item_code",
"item_name", "item_name",
@@ -33,6 +35,7 @@
"scrap", "scrap",
"qty_consumed_per_unit", "qty_consumed_per_unit",
"section_break_27", "section_break_27",
"has_variants",
"include_item_in_manufacturing", "include_item_in_manufacturing",
"original_item" "original_item"
], ],
@@ -57,6 +60,7 @@
"label": "Item Name" "label": "Item Name"
}, },
{ {
"depends_on": "eval:parent.with_operations == 1",
"fieldname": "operation", "fieldname": "operation",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Item operation", "label": "Item operation",
@@ -258,11 +262,22 @@
"label": "Original Item", "label": "Original Item",
"options": "Item", "options": "Item",
"read_only": 1 "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, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-22 11:38:52.087303", "links": [],
"modified": "2020-04-09 14:30:26.535546",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Item", "name": "BOM Item",

View File

@@ -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
});
}
}
});
}
} }
}); });

View File

@@ -8,9 +8,9 @@ import math
from frappe import _ 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.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 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 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.manufacturing.doctype.workstation.workstation import WorkstationHolidayError
from erpnext.projects.doctype.timesheet.timesheet import OverlapError from erpnext.projects.doctype.timesheet.timesheet import OverlapError
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations 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 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): for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', { self.append('required_items', {
'rate': item.rate,
'amount': item.amount,
'operation': item.operation, 'operation': item.operation,
'item_code': item.item_code, 'item_code': item.item_code,
'item_name': item.item_name, '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) filters = filters, fields = ['operation'], as_list=1)
@frappe.whitelist() @frappe.whitelist()
def get_item_details(item, project = None): def get_item_details(item, project = None, skip_bom_info=False):
res = frappe.db.sql(""" res = frappe.db.sql("""
select stock_uom, description select stock_uom, description, item_name, allow_alternative_item,
include_item_in_manufacturing
from `tabItem` from `tabItem`
where disabled=0 where disabled=0
and (end_of_life is null or end_of_life='0000-00-00' or end_of_life > %s) 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 {} return {}
res = res[0] res = res[0]
if skip_bom_info: return res
filters = {"item": item, "is_default": 1} filters = {"item": item, "is_default": 1}
@@ -681,7 +685,7 @@ def get_item_details(item, project = None):
return res return res
@frappe.whitelist() @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"): if not frappe.has_permission("Work Order", "write"):
frappe.throw(_("Not permitted"), frappe.PermissionError) 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.qty = flt(qty)
wo_doc.get_items_and_operations_from_bom() 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 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() @frappe.whitelist()
def check_if_scrap_warehouse_mandatory(bom_no): def check_if_scrap_warehouse_mandatory(bom_no):
res = {"set_scrap_wh_mandatory": False } res = {"set_scrap_wh_mandatory": False }

View File

@@ -1,526 +1,144 @@
{ {
"allow_copy": 0, "actions": [],
"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", "creation": "2016-04-18 07:38:26.314642",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "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": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "operation", "fieldname": "operation",
"fieldtype": "Link", "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", "label": "Operation",
"length": 0, "options": "Operation"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code", "label": "Item Code",
"length": 0, "options": "Item"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "source_warehouse", "fieldname": "source_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Source Warehouse", "label": "Source Warehouse",
"length": 0, "options": "Warehouse"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name", "fieldname": "item_name",
"fieldtype": "Data", "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", "label": "Item Name",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text", "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", "label": "Description",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty_section", "fieldname": "qty_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Qty"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Required Qty"
"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
}, },
{ {
"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", "depends_on": "eval:!parent.skip_transfer",
"fieldname": "transferred_qty", "fieldname": "transferred_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Transferred Qty", "label": "Transferred Qty",
"length": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "allow_alternative_item", "fieldname": "allow_alternative_item",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Allow Alternative Item"
"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
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "include_item_in_manufacturing", "fieldname": "include_item_in_manufacturing",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Include Item In Manufacturing"
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break", "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
}, },
{ {
"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", "depends_on": "eval:!parent.skip_transfer",
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Consumed Qty", "label": "Consumed Qty",
"length": 0, "read_only": 1
"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
}, },
{ {
"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", "fieldname": "available_qty_at_source_warehouse",
"fieldtype": "Float", "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", "label": "Available Qty at Source Warehouse",
"length": 0, "read_only": 1
"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
}, },
{ {
"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", "fieldname": "available_qty_at_wip_warehouse",
"fieldtype": "Float", "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", "label": "Available Qty at WIP Warehouse",
"length": 0, "read_only": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "rate",
"print_hide": 0, "fieldtype": "Currency",
"print_hide_if_no_value": 0, "label": "Rate",
"read_only": 1, "read_only": 1
"remember_last_selected_value": 0, },
"report_hide": 0, {
"reqd": 0, "fieldname": "amount",
"search_index": 0, "fieldtype": "Currency",
"set_only_once": 0, "label": "Amount",
"translatable": 0, "read_only": 1
"unique": 0
} }
], ],
"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, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2018-11-20 19:04:38.508839", "modified": "2020-04-13 18:46:32.966416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order Item", "name": "Work Order Item",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@@ -1143,6 +1143,17 @@ def set_item_default(item_code, company, fieldname, value):
d.db_insert() d.db_insert()
item.clear_cache() 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() @frappe.whitelist()
def get_uom_conv_factor(uom, stock_uom): def get_uom_conv_factor(uom, stock_uom):
uoms = [uom, stock_uom] uoms = [uom, stock_uom]