mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-24 05:47:15 +00:00
fix(manufacturing): classify MRP rows by purchase item, not missing BOM (#58510)
This commit is contained in:
@@ -14,7 +14,6 @@ from frappe.utils import (
|
||||
days_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_date_str,
|
||||
get_first_day,
|
||||
getdate,
|
||||
parse_json,
|
||||
@@ -449,7 +448,7 @@ class MaterialRequirementsPlanningReport:
|
||||
if row.get("is_adhoc"):
|
||||
row.planned_qty += row.adhoc_qty
|
||||
|
||||
for field in ["min_order_qty", "purchase_uom", "safety_stock"]:
|
||||
for field in ("min_order_qty", "purchase_uom", "safety_stock", "default_supplier"):
|
||||
if rm_details.get(field):
|
||||
row[field] = rm_details.get(field)
|
||||
|
||||
@@ -905,7 +904,13 @@ class MaterialRequirementsPlanningReport:
|
||||
item_wise_rm_details[item_code] = frappe.db.get_value(
|
||||
"Item",
|
||||
item_code,
|
||||
["default_bom as bom_no", "safety_stock", "min_order_qty", "purchase_uom"],
|
||||
[
|
||||
"default_bom as bom_no",
|
||||
"safety_stock",
|
||||
"min_order_qty",
|
||||
"purchase_uom",
|
||||
"is_purchase_item",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
@@ -1327,6 +1332,7 @@ def make_order(selected_rows: str | list, company: str, warehouse: str | None =
|
||||
purchase_orders = {}
|
||||
work_orders = []
|
||||
covered_rows = 0
|
||||
missing_bom = []
|
||||
for row in selected_rows:
|
||||
row = frappe._dict(row)
|
||||
# what is left to order once stock and the orders already placed are counted. rounding
|
||||
@@ -1339,8 +1345,14 @@ def make_order(selected_rows: str | list, company: str, warehouse: str | None =
|
||||
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 row.type_of_material == "Manufacture":
|
||||
if row.bom_no:
|
||||
work_orders.append(row)
|
||||
elif row.item_code not in missing_bom:
|
||||
missing_bom.append(row.item_code)
|
||||
|
||||
if missing_bom:
|
||||
frappe.throw(_("Default BOM for {0} not found").format(", ".join(missing_bom)))
|
||||
|
||||
if not purchase_orders and not work_orders:
|
||||
frappe.msgprint(
|
||||
|
||||
@@ -242,6 +242,68 @@ class TestMaterialRequirementsPlanningReport(ERPNextTestSuite):
|
||||
self.assertEqual(rows[plan.fg_item].lead_time, 3)
|
||||
self.assertEqual(component.delivery_date, rows[plan.fg_item].release_date)
|
||||
|
||||
def test_purchase_item_without_bom_is_purchased(self):
|
||||
plan = make_mps_item(
|
||||
self,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"is_purchase_item": 1,
|
||||
"item_defaults": [
|
||||
{"company": COMPANY, "default_warehouse": WAREHOUSE, "default_supplier": SUPPLIER}
|
||||
],
|
||||
},
|
||||
)
|
||||
self.assertEqual(plan.row.type_of_material, "Purchase")
|
||||
|
||||
make_order([plan.row], COMPANY, warehouse=WAREHOUSE, mps=plan.mps)
|
||||
|
||||
purchase_order = get_created_order(plan.mps, "Purchase Order")
|
||||
self.assertEqual([d.item_code for d in purchase_order.items], [plan.item])
|
||||
self.assertFalse(frappe.get_all("Work Order", filters={"mps": plan.mps}, pluck="name"))
|
||||
|
||||
def test_make_order_rejects_manufactured_item_without_bom(self):
|
||||
plan = make_mps_item(
|
||||
self,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"is_purchase_item": 0,
|
||||
"item_defaults": [{"company": COMPANY, "default_warehouse": WAREHOUSE}],
|
||||
},
|
||||
)
|
||||
self.assertEqual(plan.row.type_of_material, "Manufacture")
|
||||
self.assertFalse(plan.row.bom_no)
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as ctx:
|
||||
make_order([plan.row], COMPANY, warehouse=WAREHOUSE, mps=plan.mps)
|
||||
|
||||
self.assertIn("Default BOM", str(ctx.exception))
|
||||
self.assertFalse(frappe.get_all("Work Order", filters={"mps": plan.mps}, pluck="name"))
|
||||
self.assertFalse(frappe.get_all("Purchase Order", filters={"mps": plan.mps}, pluck="name"))
|
||||
|
||||
def test_make_order_uses_the_bom_passed_on_the_row(self):
|
||||
plan = make_mps_item(
|
||||
self,
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"is_purchase_item": 0,
|
||||
"item_defaults": [{"company": COMPANY, "default_warehouse": WAREHOUSE}],
|
||||
},
|
||||
)
|
||||
rm_item = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"is_purchase_item": 1,
|
||||
"item_defaults": [{"company": COMPANY, "default_warehouse": WAREHOUSE}],
|
||||
}
|
||||
).name
|
||||
plan.row.bom_no = make_bom(item=plan.item, raw_materials=[rm_item], rm_qty=1, rate=100).name
|
||||
|
||||
make_order([plan.row], COMPANY, warehouse=WAREHOUSE, mps=plan.mps)
|
||||
|
||||
work_order = get_created_order(plan.mps, "Work Order")
|
||||
self.assertEqual(work_order.production_item, plan.item)
|
||||
self.assertEqual(work_order.bom_no, plan.row.bom_no)
|
||||
|
||||
def test_make_order_creates_draft_purchase_and_work_orders(self):
|
||||
plan = make_mrp_plan(self)
|
||||
|
||||
@@ -368,6 +430,59 @@ class TestMaterialRequirementsPlanningReport(ERPNextTestSuite):
|
||||
)
|
||||
|
||||
|
||||
def make_chart_row(delivery_date, planned_qty=1):
|
||||
return frappe._dict(
|
||||
{
|
||||
"delivery_date": delivery_date,
|
||||
"planned_qty": planned_qty,
|
||||
"in_hand_qty": 0,
|
||||
"po_ordered_qty": 0,
|
||||
"wo_ordered_qty": 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def make_mps_item(test_case, item_properties, planned_qty=10):
|
||||
item = make_item(properties=item_properties).name
|
||||
mps = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Master Production Schedule",
|
||||
"company": COMPANY,
|
||||
"posting_date": today(),
|
||||
"from_date": today(),
|
||||
"parent_warehouse": WAREHOUSE,
|
||||
"items": [
|
||||
{
|
||||
"item_code": item,
|
||||
"warehouse": WAREHOUSE,
|
||||
"delivery_date": add_days(today(), 30),
|
||||
"planned_qty": planned_qty,
|
||||
"uom": frappe.get_cached_value("Item", item, "stock_uom"),
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
mps.insert()
|
||||
|
||||
_, data, _, _ = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": COMPANY,
|
||||
"from_date": today(),
|
||||
"to_date": add_days(today(), 90),
|
||||
"warehouse": WAREHOUSE,
|
||||
"mps": mps.name,
|
||||
"type_of_material": "All",
|
||||
"add_safety_stock": 0,
|
||||
}
|
||||
)
|
||||
)
|
||||
rows = [row for row in data if row.get("item_code")]
|
||||
test_case.assertTrue(rows, msg="the report returned no rows to create orders from")
|
||||
|
||||
return frappe._dict(item=item, mps=mps.name, row=rows[0], rows=rows)
|
||||
|
||||
|
||||
def make_mrp_plan(test_case, planned_qty=10, rm_qty=2):
|
||||
"""Build a finished good with a submitted BOM and an MPS demanding it, then return the
|
||||
report's own output rows -- the same payload the report's client sends to `make_order`."""
|
||||
@@ -474,15 +589,3 @@ def get_created_order(mps, doctype):
|
||||
frappe.throw(f"Expected exactly one {doctype} for {mps}, got {names}")
|
||||
|
||||
return frappe.get_doc(doctype, names[0])
|
||||
|
||||
|
||||
def make_chart_row(delivery_date, planned_qty=1):
|
||||
return frappe._dict(
|
||||
{
|
||||
"delivery_date": delivery_date,
|
||||
"planned_qty": planned_qty,
|
||||
"in_hand_qty": 0,
|
||||
"po_ordered_qty": 0,
|
||||
"wo_ordered_qty": 0,
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user