test: cover reserve stock gating on purchase receipt submit

(cherry picked from commit 820f5498e7)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/test_production_plan.py
This commit is contained in:
pandiyan
2026-07-02 16:56:44 +05:30
committed by Mergify
parent 929e29c7b8
commit 784e8798fd

View File

@@ -2322,6 +2322,216 @@ class TestProductionPlan(ERPNextTestSuite):
self.assertTrue(len(reserved_entries) == 0)
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
<<<<<<< HEAD
=======
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
# Plan, not silently vanish.
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 1)
try:
bom_tree = {
"FG For SR Cancel": {"Sub Assembly For SR Cancel 1": {"Raw Material For SR Cancel 1": {}}}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
warehouse = "_Test Warehouse - _TC"
# Plenty of stock so the Production Plan reserves everything directly on submit.
for item_code in ["Sub Assembly For SR Cancel 1", "Raw Material For SR Cancel 1"]:
make_stock_entry(item_code=item_code, target=warehouse, qty=20, basic_rate=100)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
skip_available_sub_assembly_item=1,
ignore_existing_ordered_qty=1,
do_not_submit=1,
warehouse=warehouse,
sub_assembly_warehouse=warehouse,
for_warehouse=warehouse,
reserve_stock=1,
)
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()
def pp_reserved():
return sum(
r.reserved_qty
for r in StockReservation(plan).get_reserved_entries("Production Plan", plan.name)
)
reserved_before = pp_reserved()
self.assertGreater(reserved_before, 0, "Production Plan should reserve stock on submit")
plan.make_work_order()
work_orders = frappe.get_all("Work Order", filters={"production_plan": plan.name}, pluck="name")
work_orders = list(set(work_orders))
for wo_name in work_orders:
wo_doc = frappe.get_doc("Work Order", wo_name)
wo_doc.source_warehouse = warehouse
wo_doc.wip_warehouse = warehouse
wo_doc.fg_warehouse = warehouse
wo_doc.submit()
# After all Work Orders are submitted the reservation has fully transferred off the plan.
self.assertEqual(pp_reserved(), 0, "Reservation should transfer PP -> WO on submit")
# Cancelling the Work Orders must return the reservation to the Production Plan.
for wo_name in work_orders:
frappe.get_doc("Work Order", wo_name).cancel()
self.assertEqual(
pp_reserved(), reserved_before, "Cancelling the Work Order must restore the PP reservation"
)
finally:
frappe.db.set_single_value("Stock Settings", "enable_stock_reservation", 0)
>>>>>>> 820f5498e7 (test: cover reserve stock gating on purchase receipt submit)
def test_stock_reservation_of_serial_nos_against_production_plan(self):
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom