From 82386b18aa05ec3166942132fd42e6e5839bbe87 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Fri, 26 Sep 2025 17:51:26 +0530 Subject: [PATCH 1/3] fix: get unconsumed qty as per BOM qty (cherry picked from commit cf4b395ee38c72798d14e06d5eacad2dfdeaf2e3) --- erpnext/stock/doctype/stock_entry/stock_entry.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 1fe9134e42d..7ad2b9f3cd1 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2279,10 +2279,12 @@ class StockEntry(StockController): wo_item_qty = item.transferred_qty or item.required_qty - wo_qty_consumed = flt(wo_item_qty) - flt(item.consumed_qty) + wo_qty_unconsumed = flt(wo_item_qty) - flt(item.consumed_qty) wo_qty_to_produce = flt(work_order_qty) - flt(wo.produced_qty) + bom_qty_per_unit = item.required_qty / wo.qty # per-unit BOM qty - req_qty_each = (wo_qty_consumed) / (wo_qty_to_produce or 1) + req_qty_each = (wo_qty_unconsumed) / (wo_qty_to_produce or 1) + req_qty_each = min(req_qty_each, bom_qty_per_unit) qty = req_qty_each * flt(self.fg_completed_qty) From f548f0b2315da7a77c279856c4a69ee4e0d920b9 Mon Sep 17 00:00:00 2001 From: Kavin <78342682+kavin0411@users.noreply.github.com> Date: Tue, 30 Sep 2025 00:24:34 +0530 Subject: [PATCH 2/3] test: required_qty clamping in manufacture entry (cherry picked from commit 34d2c8d9c2dcd04229a0bb58bb05cd662cf7b092) # Conflicts: # erpnext/manufacturing/doctype/work_order/test_work_order.py --- .../doctype/work_order/test_work_order.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index c77d34950ec..6716e0855e9 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2828,6 +2828,111 @@ class TestWorkOrder(FrappeTestCase): wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) ) +<<<<<<< HEAD +======= + def test_allow_additional_material_transfer(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50) + wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + for row in wo_order.required_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=row.source_warehouse, + qty=row.required_qty * 2, + basic_rate=100, + ) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 2) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) + stock_entry.insert() + stock_entry.submit() + + wo_order.reload() + self.assertEqual(wo_order.material_transferred_for_manufacturing, 3) + frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0) + + def test_req_qty_clamping_in_manufacture_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import ( + make_stock_entry as make_stock_entry_test_record, + ) + + fg_item = "Test Unconsumed RM FG Item" + rm_item_1 = "Test Unconsumed RM Item 1" + rm_item_2 = "Test Unconsumed RM Item 2" + + source_warehouse = "_Test Warehouse - _TC" + wip_warehouse = "Stores - _TC" + fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company") + + make_item(fg_item, {"is_stock_item": 1}) + make_item(rm_item_1, {"is_stock_item": 1}) + make_item(rm_item_2, {"is_stock_item": 1}) + + # create a BOM: 1 FG = 1 RM1 + 1 RM2 + bom = make_bom( + item=fg_item, + source_warehouse=source_warehouse, + raw_materials=[rm_item_1, rm_item_2], + operating_cost_per_bom_quantity=1, + do_not_submit=True, + ) + + for row in bom.exploded_items: + make_stock_entry_test_record( + item_code=row.item_code, + target=source_warehouse, + qty=100, + basic_rate=100, + ) + + wo = make_wo_order_test_record( + item=fg_item, + qty=50, + source_warehouse=source_warehouse, + wip_warehouse=wip_warehouse, + ) + wo.submit() + + # first partial transfer & manufacture (6 units) + se_transfer_1 = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 6, wip_warehouse) + ) + se_transfer_1.insert() + se_transfer_1.submit() + + stock_entry_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 6, fg_warehouse)) + + # remove rm_2 from the items to simulate unconsumed RM scenario + stock_entry_1.items = [row for row in stock_entry_1.items if row.item_code != rm_item_2] + stock_entry_1.save() + stock_entry_1.submit() + + wo.reload() + + se_transfer_2 = frappe.get_doc( + make_stock_entry(wo.name, "Material Transfer for Manufacture", 20, wip_warehouse) + ) + se_transfer_2.insert() + se_transfer_2.submit() + + stock_entry_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 20, fg_warehouse)) + + # validate rm_item_2 quantity is clamped correctly (per-unit BOM = 1 → max 20) + for row in stock_entry_2.items: + if row.item_code == rm_item_2: + self.assertLessEqual(row.qty, 20) + self.assertGreaterEqual(row.qty, 0) + +>>>>>>> 34d2c8d9c2 (test: required_qty clamping in manufacture entry) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( From 0fb5f75e9345b4117ddaad1aae817b4738540c8d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 30 Sep 2025 18:43:38 +0530 Subject: [PATCH 3/3] chore: fix conflicts Removed the test for additional material transfer in work orders. --- .../doctype/work_order/test_work_order.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 6716e0855e9..0b9dc3348fe 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2828,38 +2828,6 @@ class TestWorkOrder(FrappeTestCase): wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10) ) -<<<<<<< HEAD -======= - def test_allow_additional_material_transfer(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import ( - make_stock_entry as make_stock_entry_test_record, - ) - - frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 50) - wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2) - for row in wo_order.required_items: - make_stock_entry_test_record( - item_code=row.item_code, - target=row.source_warehouse, - qty=row.required_qty * 2, - basic_rate=100, - ) - - stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2)) - stock_entry.insert() - stock_entry.submit() - - wo_order.reload() - self.assertEqual(wo_order.material_transferred_for_manufacturing, 2) - - stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 1)) - stock_entry.insert() - stock_entry.submit() - - wo_order.reload() - self.assertEqual(wo_order.material_transferred_for_manufacturing, 3) - frappe.db.set_single_value("Manufacturing Settings", "transfer_extra_materials_percentage", 0) - def test_req_qty_clamping_in_manufacture_entry(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import ( make_stock_entry as make_stock_entry_test_record, @@ -2932,7 +2900,6 @@ class TestWorkOrder(FrappeTestCase): self.assertLessEqual(row.qty, 20) self.assertGreaterEqual(row.qty, 0) ->>>>>>> 34d2c8d9c2 (test: required_qty clamping in manufacture entry) def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse): from erpnext.stock.doctype.stock_entry.test_stock_entry import (