diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 94f9d38f45b..ce4c4cbce85 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_scrap_materials_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,15 @@ erpnext.bom.calculate_rm_cost = function(doc) { cur_frm.set_value("raw_material_cost", total_rm_cost); } +//sm : scrap material +erpnext.bom.calculate_scrap_materials_cost = function(doc) { + var sm = doc.scrap_items || []; + total_sm_cost = 0; + for(var i=0;i 0: + res["set_scrap_wh_mandatory"] = True + return res @frappe.whitelist() diff --git a/erpnext/manufacturing/doctype/production_order/test_production_order.py b/erpnext/manufacturing/doctype/production_order/test_production_order.py index 49db2993284..1454c10ce05 100644 --- a/erpnext/manufacturing/doctype/production_order/test_production_order.py +++ b/erpnext/manufacturing/doctype/production_order/test_production_order.py @@ -233,6 +233,45 @@ class TestProductionOrder(unittest.TestCase): self.pro_order.reload() self.assertEqual(len(self.pro_order.required_items), 0) + def test_scrap_material_qty(self): + prod_order = make_prod_order_test_record(planned_start_date=now(), qty=2) + + # add raw materials to stores + test_stock_entry.make_stock_entry(item_code="_Test Item", + target="Stores - _TC", qty=10, basic_rate=5000.0) + test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100", + target="Stores - _TC", qty=10, basic_rate=1000.0) + + s = frappe.get_doc(make_stock_entry(prod_order.name, "Material Transfer for Manufacture", 2)) + for d in s.get("items"): + d.s_warehouse = "Stores - _TC" + s.insert() + s.submit() + + s = frappe.get_doc(make_stock_entry(prod_order.name, "Manufacture", 2)) + s.insert() + s.submit() + + prod_order_details = frappe.db.get_value("Production Order", prod_order.name, + ["scrap_warehouse", "qty", "produced_qty", "bom_no"], as_dict=1) + + scrap_item_details = get_scrap_item_details(prod_order_details.bom_no) + + self.assertEqual(prod_order_details.produced_qty, 2) + + for item in s.items: + if item.bom_no and item.item_code in scrap_item_details: + self.assertEqual(prod_order_details.scrap_warehouse, item.t_warehouse) + self.assertEqual(flt(prod_order_details.qty)*flt(scrap_item_details[item.item_code]), item.qty) + +def get_scrap_item_details(bom_no): + scrap_items = {} + for item in frappe.db.sql("""select item_code, qty from `tabBOM Scrap Item` + where parent = %s""", bom_no, as_dict=1): + scrap_items[item.item_code] = item.qty + + return scrap_items + def make_prod_order_test_record(**args): args = frappe._dict(args) @@ -243,6 +282,7 @@ def make_prod_order_test_record(**args): pro_order.qty = args.qty or 10 pro_order.wip_warehouse = args.wip_warehouse or "_Test Warehouse - _TC" pro_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC" + pro_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC" pro_order.company = args.company or "_Test Company" pro_order.stock_uom = args.stock_uom or "_Test UOM" pro_order.set_production_order_operations() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index ec3873e8a27..cf3d6049767 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -148,7 +148,7 @@ class StockEntry(StockController): if not d.t_warehouse: frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) - elif self.pro_doc and cstr(d.t_warehouse) != self.pro_doc.fg_warehouse: + elif self.pro_doc and (cstr(d.t_warehouse) != self.pro_doc.fg_warehouse and cstr(d.t_warehouse) != self.pro_doc.scrap_warehouse): frappe.throw(_("Target warehouse in row {0} must be same as Production Order").format(d.idx)) else: @@ -238,6 +238,7 @@ class StockEntry(StockController): d.serial_no = transferred_serial_no def get_stock_and_rate(self): + self.set_production_order_details() self.set_transfer_qty() self.set_actual_qty() self.calculate_rate_and_amount() @@ -252,6 +253,7 @@ class StockEntry(StockController): def set_basic_rate(self, force=False, update_finished_item_rate=True): """get stock and incoming rate on posting date""" raw_material_cost = 0.0 + scrap_material_cost = 0.0 fg_basic_rate = 0.0 for d in self.get('items'): @@ -276,16 +278,28 @@ 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 + 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: - 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")) + 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 - 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": @@ -302,7 +316,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)), @@ -348,14 +362,14 @@ class StockEntry(StockController): def validate_bom(self): for d in self.get('items'): - if d.bom_no: + 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): + 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)) @@ -519,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", @@ -557,7 +559,15 @@ class StockEntry(StockController): item["from_warehouse"] = self.pro_doc.wip_warehouse item["to_warehouse"] = self.to_warehouse if self.purpose=="Subcontract" else "" + self.add_to_stock_entry_detail(item_dict) + + scrap_item_dict = self.get_bom_scrap_material(self.fg_completed_qty) + for item in scrap_item_dict.values(): + if self.pro_doc and self.pro_doc.scrap_warehouse: + item["to_warehouse"] = self.pro_doc.scrap_warehouse + self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no) + # fetch the serial_no of the first stock entry for the second stock entry if self.production_order and self.purpose == "Manufacture": self.set_serial_nos(self.production_order) @@ -569,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 @@ -607,7 +632,18 @@ class StockEntry(StockController): for item in item_dict.values(): item.from_warehouse = self.from_warehouse or item.default_warehouse return item_dict + + def get_bom_scrap_material(self, qty): + from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict + + # item dict = { item_code: {qty, description, stock_uom} } + 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 = "" + return item_dict + def get_transfered_raw_materials(self): transferred_materials = frappe.db.sql(""" select 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",