fix: skip covered rows when ordering from the mrp report (#58249)

* test: cover the rows that have nothing left to order in the mrp report

a row whose requirement is already met by stock or by an order placed earlier
fails the order it is selected for, takes the rows beside it down with it, and
what rounding leaves behind of it is ordered as if it were a real quantity. the
work order made from a row of the schedule also has to keep the work in progress
warehouse the company keeps for it.

* fix: skip covered rows when ordering from the mrp report

a row whose requirement is already met by stock or by orders that were placed
earlier nets down to a required qty of zero. making an order from it threw
"Qty To Manufacture cannot be 0", and since nothing caught it, none of the other
selected rows were created either. such rows are now left alone, and selecting
only covered rows says so instead of failing.

the quantity ordered stays the one that is still needed. taking the planned qty
instead would order everything that stock and the open orders already cover. it
is read at the precision an order stores it in, so what is left of a covered row
after all the subtracting does not become an order line of its own.
This commit is contained in:
Pandiyan P
2026-08-18 11:22:18 +05:30
committed by GitHub
parent 1f83906189
commit 1fa057b943
2 changed files with 82 additions and 0 deletions

View File

@@ -1307,16 +1307,33 @@ def make_order(selected_rows: str | list, company: str, warehouse: str | 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)

View File

@@ -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)
@@ -216,6 +273,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: