Merge pull request #56589 from aerele/fix-production-plan-reservation

fix: reserve projected stock for production plan based on BOM qty
This commit is contained in:
Mihir Kandoi
2026-06-29 20:54:57 +05:30
committed by GitHub
2 changed files with 19 additions and 9 deletions

View File

@@ -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)

View File

@@ -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."