From fe4ca0126b42ebe2ba34a2a85fe8e58af7d7ec88 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 13 Aug 2026 12:09:07 +0530 Subject: [PATCH] test(manufacturing): cover per-attribution returns and consumption drain --- .../doctype/work_order/test_work_order.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 4e7f84596cf..90e6a2918eb 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1722,6 +1722,76 @@ class TestWorkOrder(ERPNextTestSuite): work_order.reload() self.assertEqual(work_order.material_transferred_for_manufacturing, 2000.0) + def test_return_attribution_when_item_doubles_as_alternative(self): + """An item transferred both for its own requirement and as an alternative for + another requirement must return per attribution, not under one original_item.""" + work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 2) + ) + for item in transfer_entry.items: + if item.item_code == "_Test Item Home Desktop 100": + item.item_code = "_Test Item" + item.original_item = "_Test Item Home Desktop 100" + transfer_entry.submit() + + work_order.reload() + self.assertEqual(work_order.material_transferred_for_manufacturing, 2.0) + + return_entry = make_stock_return_entry(work_order.name) + return_entry.company = work_order.company + rows_by_attribution = {row.original_item: row for row in return_entry.items} + self.assertEqual(set(rows_by_attribution), {None, "_Test Item Home Desktop 100"}) + self.assertEqual(rows_by_attribution[None].qty, 2) + self.assertEqual(rows_by_attribution["_Test Item Home Desktop 100"].qty, 4) + + return_entry.remove(rows_by_attribution[None]) + return_entry.items[0].qty = 2 + return_entry.save() + return_entry.submit() + + work_order.reload() + returned_by_item = {row.item_code: row.returned_qty for row in work_order.required_items} + self.assertEqual(returned_by_item["_Test Item"], 0) + self.assertEqual(returned_by_item["_Test Item Home Desktop 100"], 2) + self.assertEqual(work_order.material_transferred_for_manufacturing, 1.0) + + def test_return_after_consumption_distributes_across_attributions(self): + """Manufacture consumption carries no original_item; it must drain attribution + buckets in transfer order so the return entry reflects what remains.""" + frappe.db.set_single_value( + "Manufacturing Settings", + "backflush_raw_materials_based_on", + "Material Transferred for Manufacture", + ) + work_order = make_wo_order_test_record(planned_start_date=now(), qty=2) + test_stock_entry.make_stock_entry( + item_code="_Test Item", target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0 + ) + + transfer_entry = frappe.get_doc( + make_stock_entry(work_order.name, "Material Transfer for Manufacture", 2) + ) + for item in transfer_entry.items: + if item.item_code == "_Test Item Home Desktop 100": + item.item_code = "_Test Item" + item.original_item = "_Test Item Home Desktop 100" + transfer_entry.submit() + + manufacture_entry = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1)) + raw_material_rows = [row for row in manufacture_entry.items if row.s_warehouse] + self.assertEqual(sorted(row.qty for row in raw_material_rows), [1.0, 2.0]) + manufacture_entry.submit() + + return_entry = make_stock_return_entry(work_order.name) + self.assertEqual(len(return_entry.items), 1) + self.assertEqual(return_entry.items[0].original_item, "_Test Item Home Desktop 100") + self.assertEqual(return_entry.items[0].qty, 3) + def test_status_in_process_when_only_one_required_item_transferred(self): """Stock Entry created from a Pick List that picked only one of the required items: min-fraction keeps material_transferred_for_manufacturing at 0, but the work order must