diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 31f73963c84..53af28df8a5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -193,6 +193,7 @@ class BOM(WebsiteGenerator): self.update_exploded_items(save=False) self.update_stock_qty() self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) + self.set_process_loss_qty() self.validate_scrap_items() def get_context(self, context): @@ -877,6 +878,10 @@ class BOM(WebsiteGenerator): """Get a complete tree representation preserving order of child items.""" return BOMTree(self.name) + def set_process_loss_qty(self): + if self.process_loss_percentage: + self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100 + def validate_scrap_items(self): must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number") diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 989861717e4..16f5c793720 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -384,36 +384,16 @@ class TestBOM(FrappeTestCase): def test_bom_with_process_loss_item(self): fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() - if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"): - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 - ) - bom_doc.submit() - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0 + fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110 ) - # PL Item qty can't be >= FG Item qty + # PL can't be > 100 self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100 - ) - # PL Item rate has to be 0 - self.assertRaises(frappe.ValidationError, bom_doc.submit) - - bom_doc = create_bom_with_process_loss_item( - fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0 - ) + bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20) # Items with whole UOMs can't be PL Items self.assertRaises(frappe.ValidationError, bom_doc.submit) - bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0 - ) - # FG Items in Scrap/Loss Table should have Is Process Loss set - self.assertRaises(frappe.ValidationError, bom_doc.submit) - def test_bom_item_query(self): query = partial( item_query, @@ -743,7 +723,9 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=qty, rate=rate) -def create_bom_with_process_loss_item(fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2): +def create_bom_with_process_loss_item( + fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0 +): bom_doc = frappe.new_doc("BOM") bom_doc.item = fg_item.item_code bom_doc.quantity = fg_qty @@ -757,18 +739,22 @@ def create_bom_with_process_loss_item(fg_item, bom_item, scrap_qty, scrap_rate, "rate": 100.0, }, ) - bom_doc.append( - "scrap_items", - { - "item_code": fg_item.item_code, - "qty": scrap_qty, - "stock_qty": scrap_qty, - "uom": fg_item.stock_uom, - "stock_uom": fg_item.stock_uom, - "rate": scrap_rate, - }, - ) + + if scrap_qty: + bom_doc.append( + "scrap_items", + { + "item_code": fg_item.item_code, + "qty": scrap_qty, + "stock_qty": scrap_qty, + "uom": fg_item.stock_uom, + "stock_uom": fg_item.stock_uom, + "rate": scrap_rate, + }, + ) + bom_doc.currency = "INR" + bom_doc.process_loss_percentage = process_loss_percentage return bom_doc diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 6c7483ca7d2..76040b29d59 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -846,20 +846,20 @@ class TestWorkOrder(FrappeTestCase): create_process_loss_bom_items, ) - qty = 4 + qty = 10 scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG source_warehouse = "Stores - _TC" wip_warehouse = "_Test Warehouse - _TC" fg_item_non_whole, _, bom_item = create_process_loss_bom_items() test_stock_entry.make_stock_entry( - item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 + item_code=bom_item.item_code, target=source_warehouse, qty=qty, basic_rate=100 ) bom_no = f"BOM-{fg_item_non_whole.item_code}-001" if not frappe.db.exists("BOM", bom_no): bom_doc = create_bom_with_process_loss_item( - fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1 + fg_item_non_whole, bom_item, fg_qty=1, process_loss_percentage=10 ) bom_doc.submit() @@ -883,19 +883,12 @@ class TestWorkOrder(FrappeTestCase): # Testing stock entry values items = se.get("items") - self.assertEqual(len(items), 3, "There should be 3 items including process loss.") + self.assertEqual(len(items), 2, "There should be 3 items including process loss.") + fg_item = items[1] - source_item, fg_item, pl_item = items - - total_pl_qty = qty * scrap_qty - actual_fg_qty = qty - total_pl_qty - - self.assertEqual(pl_item.qty, total_pl_qty) - self.assertEqual(fg_item.qty, actual_fg_qty) - - # Testing Work Order values - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty) - self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty) + self.assertEqual(fg_item.qty, qty - 1) + self.assertEqual(se.process_loss_percentage, 10) + self.assertEqual(se.process_loss_qty, 1) @timeout(seconds=60) def test_job_card_scrap_item(self):