mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-25 13:03:02 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user