From d3df0bf387c452d831e2d2cd7acf7074380307a3 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 27 Jun 2026 23:20:11 +0530 Subject: [PATCH 1/2] fix: reserve projected stock for production plan based on BOM qty --- .../doctype/production_plan/production_plan.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 67323d42d40..cb8f24fc9f1 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -9,6 +9,7 @@ from collections import defaultdict import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1375,7 +1376,7 @@ def get_material_request_items( get_conversion_factor(row.item_code, item_details.purchase_uom).get("conversion_factor") or 1.0 ) - if required_qty > 0: + if flt(row.get("qty")) > 0: return { "item_code": row.item_code, "item_name": row.item_name, @@ -1880,7 +1881,12 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * child.conversion_factor)) + .select( + Sum( + (Case().when(child.quantity == 0, child.required_bom_qty).else_(child.quantity)) + * child.conversion_factor + ) + ) .where( (table.docstatus == 1) & (child.item_code == item_code) From b39024e0e3380c67e9c2a48d4b77dbeb3c6adc32 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sat, 27 Jun 2026 23:20:11 +0530 Subject: [PATCH 2/2] test: update cascading test to assert zero-qty reservation for stock-covered items --- .../production_plan/test_production_plan.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 5af1fdb36b5..e64a8c16b84 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -212,13 +212,15 @@ class TestProductionPlan(FrappeTestCase): quantities = [d["quantity"] for d in mr_items] rm_qty = sum(quantities) - # Only 2 MR item created - the first SO's requirement is fully covered by stock (v15 behaviour) - self.assertEqual(len(mr_items), 2) - self.assertEqual(rm_qty, 2, "Cascading failed: total MR qty should be 2 (3 needed - 1 in stock)") + # 3 MR items: SO1's requirement is covered by stock (qty=0 but reserved), SO2 and SO3 need 1 each + self.assertEqual(len(mr_items), 3) + self.assertEqual( + rm_qty, 2, "Cascading failed: total purchase qty should be 2 (3 needed - 1 in stock)" + ) self.assertEqual( quantities, - [1, 1], - "Cascading failed: only second and third SO should need procurement (qty=1) since first SO consumed stock", + [0, 1, 1], + "SO1 stock-covered item should appear with qty=0 for reservation; SO2 and SO3 need qty=1", ) sr.cancel() @@ -251,11 +253,13 @@ class TestProductionPlan(FrappeTestCase): pln = create_production_plan( item_code="Test Production Item 1", use_multi_level_bom=0, ignore_existing_ordered_qty=0 ) - self.assertFalse(len(pln.mr_items)) + items_needing_purchase = [row.item_code for row in pln.mr_items if row.quantity > 0] + self.assertFalse(len(items_needing_purchase)) + + pln.cancel() sr1.cancel() sr2.cancel() - pln.cancel() def test_production_plan_sales_orders(self): "Test if previously fulfilled SO (with WO) is pulled into Prod Plan."