From dfc2301f083d98845471c532d2c865ddf0c9110f Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Thu, 20 Aug 2026 07:43:16 +0530 Subject: [PATCH] fix: skip covered rows when ordering from the mrp report (#58249) (#58312) --- .../material_requirements_planning_report.py | 19 +++++- ...t_material_requirements_planning_report.py | 65 +++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py index 5e0eb92dbbd..36d8e4567bc 100644 --- a/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py +++ b/erpnext/manufacturing/report/material_requirements_planning_report/material_requirements_planning_report.py @@ -1298,7 +1298,7 @@ def get_item_capacity(item_code, bucket_size): @frappe.whitelist() -def make_order(selected_rows, company, warehouse=None, mps=None): +def make_order(selected_rows: str | list, company: str, warehouse: str | None = None, mps: str | None = None): if not frappe.has_permission("Purchase Order", "create"): frappe.throw(_("Not permitted to make Purchase Orders"), frappe.PermissionError) @@ -1308,16 +1308,33 @@ def make_order(selected_rows, company, warehouse=None, mps=None): if not frappe.db.exists("Company", company): frappe.throw(_("Company {0} does not exist").format(company)) + qty_precision = frappe.get_precision("Purchase Order Item", "qty") purchase_orders = {} work_orders = [] + covered_rows = 0 for row in selected_rows: row = frappe._dict(row) + # what is left to order once stock and the orders already placed are counted. rounding + # to the precision an order is stored in, so what is left of a covered row after all the + # subtracting does not become an order line of its own + if flt(row.required_qty, qty_precision) <= 0: + covered_rows += 1 + continue + if row.type_of_material == "Purchase": purchase_orders.setdefault((row.default_supplier, row.release_date), []).append(row) if row.type_of_material == "Manufacture" and row.bom_no: work_orders.append(row) + if not purchase_orders and not work_orders: + frappe.msgprint( + _("Nothing to order, the selected rows are already covered by stock or existing orders") + if covered_rows + else _("Nothing to order from the selected rows") + ) + return + if purchase_orders: make_purchase_orders(purchase_orders, company, warehouse=warehouse, mps=mps) diff --git a/erpnext/manufacturing/report/material_requirements_planning_report/test_material_requirements_planning_report.py b/erpnext/manufacturing/report/material_requirements_planning_report/test_material_requirements_planning_report.py index 1b873c5b3d5..bda80e2fcd6 100644 --- a/erpnext/manufacturing/report/material_requirements_planning_report/test_material_requirements_planning_report.py +++ b/erpnext/manufacturing/report/material_requirements_planning_report/test_material_requirements_planning_report.py @@ -20,6 +20,7 @@ COMPANY = "_Test Company" WAREHOUSE = "_Test Warehouse - _TC" SUPPLIER = "_Test Supplier" TAX_TEMPLATE = "_Test Purchase Taxes and Charges Template - _TC" +WIP_WAREHOUSE = "_Test Warehouse 1 - _TC" class TestMaterialRequirementsPlanningReport(ERPNextTestSuite): @@ -105,6 +106,62 @@ class TestMaterialRequirementsPlanningReport(ERPNextTestSuite): self.assertEqual(work_order.bom_no, plan.bom) self.assertEqual(work_order.qty, plan.planned_qty) + def test_make_order_skips_rows_that_are_already_covered(self): + """ + A row whose requirement is met by stock or by an order placed earlier has nothing left + to order. It must be left out instead of failing, and the rows beside it must still be + created. + """ + plan = make_mrp_plan(self) + covered_row, remaining_rows = plan.rows[0], plan.rows[1:] + self.assertTrue(remaining_rows, msg="the plan needs a second row to order") + covered_row.required_qty = 0 + + make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps) + + ordered_items = [ + row.item_code + for doctype in ("Purchase Order", "Work Order") + for order in frappe.get_all(doctype, filters={"mps": plan.mps}, pluck="name") + for row in get_ordered_items(doctype, order) + ] + self.assertNotIn(covered_row.item_code, ordered_items) + self.assertEqual(sorted(ordered_items), sorted([row.item_code for row in remaining_rows])) + + def test_make_order_creates_nothing_when_every_row_is_covered(self): + plan = make_mrp_plan(self) + for row in plan.rows: + row.required_qty = 0 + + make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps) + + for doctype in ("Purchase Order", "Work Order"): + self.assertFalse(frappe.get_all(doctype, filters={"mps": plan.mps}, pluck="name")) + + def test_make_order_ignores_a_requirement_left_over_by_rounding(self): + """What is left of a covered row after subtracting is not a quantity worth ordering.""" + plan = make_mrp_plan(self) + for row in plan.rows: + row.required_qty = 0.0000000001 + + make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps) + + for doctype in ("Purchase Order", "Work Order"): + self.assertFalse(frappe.get_all(doctype, filters={"mps": plan.mps}, pluck="name")) + + def test_work_order_keeps_the_company_wip_warehouse(self): + """ + The item's own warehouse is where the finished goods go, the work in progress warehouse + stays the one the company keeps for it. + """ + plan = make_mrp_plan(self) + frappe.db.set_value("Company", COMPANY, "default_wip_warehouse", WIP_WAREHOUSE) + + make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps) + + work_order = get_created_order(plan.mps, "Work Order") + self.assertEqual(work_order.wip_warehouse, WIP_WAREHOUSE) + def test_purchase_order_gets_defaults_from_set_missing_values(self): plan = make_mrp_plan(self) make_tax_rule(tax_type="Purchase", purchase_tax_template=TAX_TEMPLATE, priority=1, save=1) @@ -204,6 +261,14 @@ def make_mrp_plan(test_case, planned_qty=10, rm_qty=2): ) +def get_ordered_items(doctype, order): + child_doctype = "Purchase Order Item" if doctype == "Purchase Order" else None + if not child_doctype: + return frappe.get_all(doctype, filters={"name": order}, fields=["production_item as item_code"]) + + return frappe.get_all(child_doctype, filters={"parent": order}, fields=["item_code"]) + + def get_created_order(mps, doctype): names = frappe.get_all(doctype, filters={"mps": mps}, pluck="name") if len(names) != 1: