diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 9ab329b8f19..9b7f5346502 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -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