From 464c27e189a5c24a0c22af536a17bb2ff8d1ba0f Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 26 Sep 2016 14:29:17 +0530 Subject: [PATCH 1/4] [feature] scrap management --- erpnext/manufacturing/doctype/bom/bom.js | 15 ++ erpnext/manufacturing/doctype/bom/bom.json | 90 ++++++- erpnext/manufacturing/doctype/bom/bom.py | 15 +- .../doctype/bom_scrap_item/__init__.py | 0 .../bom_scrap_item/bom_scrap_item.json | 248 ++++++++++++++++++ .../doctype/bom_scrap_item/bom_scrap_item.py | 10 + 6 files changed, 368 insertions(+), 10 deletions(-) create mode 100644 erpnext/manufacturing/doctype/bom_scrap_item/__init__.py create mode 100644 erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json create mode 100644 erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.py diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 94f9d38f45b..dbff91bd06c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -83,8 +83,10 @@ var get_bom_material_detail= function(doc, cdt, cdn) { d = locals[cdt][cdn]; $.extend(d, r.message); refresh_field("items"); + refresh_field("scrap_items"); doc = locals[doc.doctype][doc.name]; erpnext.bom.calculate_rm_cost(doc); + erpnext.bom.calculate_sm_cost(doc); erpnext.bom.calculate_total(doc); }, freeze: true @@ -120,6 +122,7 @@ erpnext.bom.calculate_op_cost = function(doc) { refresh_field('operating_cost'); } +// rm : raw material erpnext.bom.calculate_rm_cost = function(doc) { var rm = doc.items || []; total_rm_cost = 0; @@ -133,6 +136,17 @@ erpnext.bom.calculate_rm_cost = function(doc) { cur_frm.set_value("raw_material_cost", total_rm_cost); } +//sm : scrap material +erpnext.bom.calculate_sm_cost = function(doc) { + var sm = doc.scrap_items || []; + total_sm_cost = 0; + for(var i=0;i Date: Wed, 28 Sep 2016 12:24:42 +0530 Subject: [PATCH 2/4] [enhance] scrap management --- erpnext/manufacturing/doctype/bom/bom.js | 2 - erpnext/manufacturing/doctype/bom/bom.py | 42 +++++++++++++++++++ .../production_order/production_order.json | 29 ++++++++++++- .../stock/doctype/stock_entry/stock_entry.py | 25 +++++++++-- 4 files changed, 92 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index dbff91bd06c..488235482e9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -143,8 +143,6 @@ erpnext.bom.calculate_sm_cost = function(doc) { for(var i=0;i Date: Wed, 5 Oct 2016 17:11:16 +0530 Subject: [PATCH 3/4] added get_scrap_items param to get_bom_items_as_dict --- erpnext/manufacturing/doctype/bom/bom.py | 48 ++----------------- .../production_order/production_order.js | 11 ++++- .../production_order/production_order.py | 16 +++++++ .../stock/doctype/stock_entry/stock_entry.py | 17 +++++-- 4 files changed, 42 insertions(+), 50 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e75d82ba99f..c6a3f1fcead 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -376,7 +376,7 @@ class BOM(Document): if not d.description: d.description = frappe.db.get_value('Operation', d.operation, 'description') -def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1): +def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_items=0): item_dict = {} # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss @@ -405,53 +405,13 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1): query = query.format(table="BOM Explosion Item", conditions="""and item.is_sub_contracted_item = 0""") items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True) + elif fetch_scrap_items: + query = query.format(table="BOM Scrap Item", conditions="") + items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True) else: query = query.format(table="BOM Item", conditions="") items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True) - # make unique - for item in items: - if item_dict.has_key(item.item_code): - item_dict[item.item_code]["qty"] += flt(item.qty) - else: - item_dict[item.item_code] = item - - for item, item_details in item_dict.items(): - for d in [["Account", "expense_account", "default_expense_account"], - ["Cost Center", "cost_center", "cost_center"], ["Warehouse", "default_warehouse", ""]]: - company_in_record = frappe.db.get_value(d[0], item_details.get(d[1]), "company") - if not item_details.get(d[1]) or (company_in_record and company != company_in_record): - item_dict[item][d[1]] = frappe.db.get_value("Company", company, d[2]) if d[2] else None - - return item_dict - -def get_bom_scrap_items_as_dict(bom, company, qty=1, fetch_exploded=1): - item_dict = {} - - # Did not use qty_consumed_per_unit in the query, as it leads to rounding loss - query = """select - bom_scrap_item.item_code, - item.item_name, - sum(bom_scrap_item.qty/ifnull(bom.quantity, 1)) * %(qty)s as qty, - item.description, - item.image, - item.stock_uom, - item.default_warehouse, - item.expense_account as expense_account, - item.buying_cost_center as cost_center - from - `tabBOM Scrap Item` bom_scrap_item, `tabBOM` bom, `tabItem` item - where - bom_scrap_item.parent = bom.name - and bom_scrap_item.docstatus < 2 - and bom_scrap_item.parent = %(bom)s - and item.name = bom_scrap_item.item_code - and is_stock_item = 1 - group by item_code, stock_uom""" - - items = frappe.db.sql(query, { "qty": qty, "bom": bom }, as_dict=True) - - # make unique for item in items: if item_dict.has_key(item.item_code): item_dict[item.item_code]["qty"] += flt(item.qty) diff --git a/erpnext/manufacturing/doctype/production_order/production_order.js b/erpnext/manufacturing/doctype/production_order/production_order.js index d0e0d86d2e3..d8eb25b4c36 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.js +++ b/erpnext/manufacturing/doctype/production_order/production_order.js @@ -225,6 +225,10 @@ $.extend(cur_frm.cscript, { $.each(["description", "stock_uom", "bom_no"], function(i, field) { cur_frm.set_value(field, r.message[field]); }); + + if(r.message["set_scrap_wh_mandatory"]){ + cur_frm.toggle_reqd("scrap_warehouse", true); + } } }); }, @@ -260,7 +264,12 @@ $.extend(cur_frm.cscript, { bom_no: function() { return this.frm.call({ doc: this.frm.doc, - method: "set_production_order_operations" + method: "set_production_order_operations", + callback: function(r) { + if(r.message["set_scrap_wh_mandatory"]){ + cur_frm.toggle_reqd("scrap_warehouse", true); + } + } }); }, diff --git a/erpnext/manufacturing/doctype/production_order/production_order.py b/erpnext/manufacturing/doctype/production_order/production_order.py index a0dc55433da..3934935206d 100644 --- a/erpnext/manufacturing/doctype/production_order/production_order.py +++ b/erpnext/manufacturing/doctype/production_order/production_order.py @@ -222,6 +222,8 @@ class ProductionOrder(Document): self.set('operations', operations) self.calculate_time() + return check_if_scrap_warehouse_mandatory(self.bom_no) + def calculate_time(self): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") @@ -457,11 +459,25 @@ def get_item_details(item): return {} res = res[0] + res["bom_no"] = frappe.db.get_value("BOM", filters={"item": item, "is_default": 1}) if not res["bom_no"]: variant_of= frappe.db.get_value("Item", item, "variant_of") if variant_of: res["bom_no"] = frappe.db.get_value("BOM", filters={"item": variant_of, "is_default": 1}) + + res.update(check_if_scrap_warehouse_mandatory(res["bom_no"])) + + return res + +@frappe.whitelist() +def check_if_scrap_warehouse_mandatory(bom_no): + res = {"set_scrap_wh_mandatory": False } + bom = frappe.get_doc("BOM", bom_no) + + if len(bom.scrap_items) > 0: + res["set_scrap_wh_mandatory"] = True + return res @frappe.whitelist() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 98924dbc602..a6f702d4b85 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -276,6 +276,13 @@ class StockEntry(StockController): if not d.t_warehouse: raw_material_cost += flt(d.basic_amount) + # get scrap items basic rate + if d.bom_no: + if not flt(d.basic_rate) and getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse: + basic_rate = flt(get_incoming_rate(args), self.precision("basic_rate", d)) + if basic_rate > 0: + d.basic_rate = basic_rate + number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse]) if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate: self.set_basic_rate_for_finished_goods(raw_material_cost) @@ -283,7 +290,7 @@ class StockEntry(StockController): def set_basic_rate_for_finished_goods(self, raw_material_cost): if self.purpose in ["Manufacture", "Repack"]: for d in self.get("items"): - if d.bom_no or d.t_warehouse: + if (d.bom_no or d.t_warehouse) and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse): d.basic_rate = flt(raw_material_cost / flt(d.transfer_qty), d.precision("basic_rate")) d.basic_amount = flt(raw_material_cost, d.precision("basic_amount")) @@ -302,7 +309,7 @@ class StockEntry(StockController): def update_valuation_rate(self): for d in self.get("items"): - d.amount = flt(d.basic_amount + flt(d.additional_cost), d.precision("amount")) + d.amount = flt(flt(d.basic_amount) + flt(d.additional_cost), d.precision("amount")) d.valuation_rate = flt( flt(d.basic_rate) + (flt(d.additional_cost) / flt(d.transfer_qty)), @@ -617,11 +624,11 @@ class StockEntry(StockController): return item_dict def get_bom_scrap_material(self, qty): - from erpnext.manufacturing.doctype.bom.bom import get_bom_scrap_items_as_dict + from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict # item dict = { item_code: {qty, description, stock_uom} } - item_dict = get_bom_scrap_items_as_dict(self.bom_no, self.company, qty=qty, - fetch_exploded = self.use_multi_level_bom) + item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, + fetch_exploded = 0, fetch_scrap_items = 1) for item in item_dict.values(): item.from_warehouse = "" From 8c3764a6efbe347b7d3511bd6e8010b01eaa128f Mon Sep 17 00:00:00 2001 From: Saurabh Date: Wed, 5 Oct 2016 20:41:29 +0530 Subject: [PATCH 4/4] test case for scrap item --- erpnext/manufacturing/doctype/bom/bom.js | 6 +-- erpnext/manufacturing/doctype/bom/bom.json | 6 ++- .../doctype/bom/test_records.json | 15 +++++- .../production_order/test_production_order.py | 40 ++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 48 +++++++++++-------- .../stock/doctype/warehouse/test_records.json | 7 +++ 6 files changed, 96 insertions(+), 26 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 488235482e9..ce4c4cbce85 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -86,7 +86,7 @@ var get_bom_material_detail= function(doc, cdt, cdn) { refresh_field("scrap_items"); doc = locals[doc.doctype][doc.name]; erpnext.bom.calculate_rm_cost(doc); - erpnext.bom.calculate_sm_cost(doc); + erpnext.bom.calculate_scrap_materials_cost(doc); erpnext.bom.calculate_total(doc); }, freeze: true @@ -137,7 +137,7 @@ erpnext.bom.calculate_rm_cost = function(doc) { } //sm : scrap material -erpnext.bom.calculate_sm_cost = function(doc) { +erpnext.bom.calculate_scrap_materials_cost = function(doc) { var sm = doc.scrap_items || []; total_sm_cost = 0; for(var i=0;i 0: d.basic_rate = basic_rate + d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) + + if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse: + + scrap_material_cost += flt(d.basic_amount) number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse]) if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate: - self.set_basic_rate_for_finished_goods(raw_material_cost) + self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost) - def set_basic_rate_for_finished_goods(self, raw_material_cost): + def set_basic_rate_for_finished_goods(self, raw_material_cost, scrap_material_cost): if self.purpose in ["Manufacture", "Repack"]: for d in self.get("items"): if (d.bom_no or d.t_warehouse) and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse): - d.basic_rate = flt(raw_material_cost / flt(d.transfer_qty), d.precision("basic_rate")) - d.basic_amount = flt(raw_material_cost, d.precision("basic_amount")) + d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate")) + d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount")) def distribute_additional_costs(self): if self.purpose == "Material Issue": @@ -355,14 +362,14 @@ class StockEntry(StockController): def validate_bom(self): for d in self.get('items'): - if d.bom_no and (d.t_warehouse != self.pro_doc.scrap_warehouse): + if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): validate_bom_no(d.item_code, d.bom_no) def validate_finished_goods(self): """validation: finished good quantity should be same as manufacturing quantity""" items_with_target_warehouse = [] for d in self.get('items'): - if d.bom_no and flt(d.transfer_qty) != flt(self.fg_completed_qty) and (d.t_warehouse != self.pro_doc.scrap_warehouse): + if d.bom_no and flt(d.transfer_qty) != flt(self.fg_completed_qty) and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ format(d.idx, d.transfer_qty, self.fg_completed_qty)) @@ -526,19 +533,7 @@ class StockEntry(StockController): if not self.posting_date or not self.posting_time: frappe.throw(_("Posting date and posting time is mandatory")) - if not getattr(self, "pro_doc", None): - self.pro_doc = None - - if self.production_order: - # common validations - if not self.pro_doc: - self.pro_doc = frappe.get_doc('Production Order', self.production_order) - - if self.pro_doc: - self.bom_no = self.pro_doc.bom_no - else: - # invalid production order - self.production_order = None + self.set_production_order_details() if self.bom_no: if self.purpose in ["Material Issue", "Material Transfer", "Manufacture", "Repack", @@ -584,6 +579,21 @@ class StockEntry(StockController): self.set_actual_qty() self.calculate_rate_and_amount() + def set_production_order_details(self): + if not getattr(self, "pro_doc", None): + self.pro_doc = None + + if self.production_order: + # common validations + if not self.pro_doc: + self.pro_doc = frappe.get_doc('Production Order', self.production_order) + + if self.pro_doc: + self.bom_no = self.pro_doc.bom_no + else: + # invalid production order + self.production_order = None + def load_items_from_bom(self): if self.production_order: item_code = self.pro_doc.production_item diff --git a/erpnext/stock/doctype/warehouse/test_records.json b/erpnext/stock/doctype/warehouse/test_records.json index 4dd9f6b46a4..af3bd231fc0 100644 --- a/erpnext/stock/doctype/warehouse/test_records.json +++ b/erpnext/stock/doctype/warehouse/test_records.json @@ -6,6 +6,13 @@ "warehouse_name": "_Test Warehouse", "is_group": 0 }, + { + "company": "_Test Company", + "create_account_under": "Stock Assets - _TC", + "doctype": "Warehouse", + "warehouse_name": "_Test Scrap Warehouse", + "is_group": 0 + }, { "company": "_Test Company", "create_account_under": "Stock Assets - _TC",