From 820f5498e74ca206d54c83dc57acdec677cd9629 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Thu, 2 Jul 2026 16:56:44 +0530 Subject: [PATCH] test: cover reserve stock gating on purchase receipt submit --- .../production_plan/test_production_plan.py | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index e720bd96319..ac2b38ea216 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2322,6 +2322,145 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(len(reserved_entries), 0) frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_no_stock_reservation_via_purchase_receipt_when_reserve_stock_disabled(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.material_request.mapper import make_purchase_order + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + bom_tree = {"FG For SR No Auto Reserve": {"RM For SR No Auto Reserve": {}}} + parent_bom = create_nested_bom(bom_tree, prefix="") + + warehouse = "_Test Warehouse - _TC" + + # reserve_stock is deliberately left unset (defaults to 0): this is what happens when + # "Auto Reserve Stock" is off and nobody ticks "Reserve Stock" on the Production Plan by hand. + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + + self.assertEqual(plan.reserve_stock, 0) + plan.submit() + + plan.submit_material_request = 1 + plan.make_material_request() + + material_requests = frappe.get_all( + "Material Request", filters={"production_plan": plan.name}, pluck="name" + ) + self.assertGreater(len(material_requests), 0) + + for mr_name in list(set(material_requests)): + po = make_purchase_order(mr_name) + po.supplier = "_Test Supplier" + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + sre = StockReservation(plan) + reserved_entries = sre.get_reserved_entries("Production Plan", plan.name) + self.assertEqual(len(reserved_entries), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + + def test_stock_reservation_ignores_production_plans_with_reserve_stock_off_on_shared_purchase_order(self): + from erpnext.buying.doctype.purchase_order.mapper import make_purchase_receipt + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1) + frappe.db.set_single_value("Stock Settings", "auto_reserve_stock", 0) + + warehouse = "_Test Warehouse - _TC" + + bom_reserve = create_nested_bom({"FG SR Mixed Reserve": {"RM SR Mixed Reserve": {}}}, prefix="") + bom_skip = create_nested_bom({"FG SR Mixed Skip": {"RM SR Mixed Skip": {}}}, prefix="") + + def make_submitted_plan(item_code, reserve_stock): + plan = create_production_plan( + item_code=item_code, + planned_qty=5, + ignore_existing_ordered_qty=1, + do_not_submit=1, + warehouse=warehouse, + for_warehouse=warehouse, + reserve_stock=reserve_stock, + ) + plan.get_sub_assembly_items() + plan.set("mr_items", []) + for d in get_items_for_material_requests(plan.as_dict()): + plan.append("mr_items", d) + plan.save() + plan.submit() + plan.submit_material_request = 1 + plan.make_material_request() + return plan + + plan_reserve = make_submitted_plan(bom_reserve.item, reserve_stock=1) + plan_skip = make_submitted_plan(bom_skip.item, reserve_stock=0) + + self.assertEqual(plan_reserve.reserve_stock, 1) + self.assertEqual(plan_skip.reserve_stock, 0) + + mr_reserve = frappe.get_all( + "Material Request", filters={"production_plan": plan_reserve.name}, pluck="name" + )[0] + mr_skip = frappe.get_all( + "Material Request", filters={"production_plan": plan_skip.name}, pluck="name" + )[0] + + # One Purchase Order pulling rows from both Material Requests, so the Purchase Receipt made + # from it has both a reservable and a non-reservable Production Plan reference in `doc.items`. + po = frappe.new_doc("Purchase Order") + po.supplier = "_Test Supplier" + po.company = plan_reserve.company + po.schedule_date = nowdate() + + for mr_name in (mr_reserve, mr_skip): + mr = frappe.get_doc("Material Request", mr_name) + for item in mr.items: + po.append( + "items", + { + "item_code": item.item_code, + "qty": item.qty, + "rate": 100, + "schedule_date": nowdate(), + "warehouse": warehouse, + "material_request": mr.name, + "material_request_item": item.name, + }, + ) + + po.submit() + + pr = make_purchase_receipt(po.name) + pr.submit() + + reserved_for_plan_reserve = StockReservation(plan_reserve).get_reserved_entries( + "Production Plan", plan_reserve.name + ) + reserved_for_plan_skip = StockReservation(plan_skip).get_reserved_entries( + "Production Plan", plan_skip.name + ) + + self.assertGreater(len(reserved_for_plan_reserve), 0) + self.assertEqual(len(reserved_for_plan_skip), 0) + + frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0) + def test_stock_reservation_restored_on_work_order_cancel(self): # Spec #5 (cancellation path): when a Work Order created from a Production Plan is cancelled, # the reservation that was transferred PP -> WO must flow back to the still-open Production