fix: run set_missing_values before creating Purchase Order from MRP report

(cherry picked from commit 94d363851f)
This commit is contained in:
ljain112
2026-08-10 14:11:41 +05:30
parent e51df54439
commit 72114fa738
3 changed files with 197 additions and 17 deletions

View File

@@ -1327,13 +1327,7 @@ def make_order(selected_rows, company, warehouse=None, mps=None):
def make_purchase_orders(purchase_orders, company, warehouse=None, mps=None):
for (supplier, release_date), items in purchase_orders.items():
po = frappe.new_doc("Purchase Order")
po.supplier = supplier
po.company = company
po.mps = mps
po.transaction_date = release_date
po.set("items", [])
po_items = []
for item in items:
uom = item.purchase_uom or item.uom
if not uom:
@@ -1346,23 +1340,33 @@ def make_purchase_orders(purchase_orders, company, warehouse=None, mps=None):
if flt(item.required_qty) < flt(item.min_order_qty):
item.required_qty = item.min_order_qty
po.append(
"items",
po_items.append(
{
"item_code": item.item_code,
"qty": item.required_qty,
"uom": uom,
"schedule_date": item.delivery_date if item.delivery_date else today(),
"warehouse": warehouse or item.default_warehouse,
},
}
)
if len(po.items) > 0:
po.insert()
frappe.msgprint(
_("Purchase Order {0} created").format(frappe.bold(po.name)),
alert=True,
)
if not po_items:
continue
po = frappe.new_doc("Purchase Order")
po.supplier = supplier
po.company = company
po.mps = mps
po.transaction_date = release_date
po.set("items", po_items)
po.run_method("set_missing_values")
po.insert()
frappe.msgprint(
_("Purchase Order {0} created").format(frappe.bold(po.name)),
alert=True,
)
def make_work_orders(work_orders, company, warehouse=None, mps=None):

View File

@@ -3,13 +3,24 @@
import frappe
from frappe.tests.classes.context_managers import freeze_time
from frappe.utils import add_days, formatdate, today
from frappe.utils import add_days, flt, formatdate, today
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.report.material_requirements_planning_report.material_requirements_planning_report import (
MaterialRequirementsPlanningReport,
execute,
get_item_lead_time,
make_order,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestSuite
COMPANY = "_Test Company"
WAREHOUSE = "_Test Warehouse - _TC"
SUPPLIER = "_Test Supplier"
TAX_TEMPLATE = "_Test Purchase Taxes and Charges Template - _TC"
class TestMaterialRequirementsPlanningReport(ERPNextTestSuite):
def test_detailed_chart_includes_full_date_range(self):
@@ -59,6 +70,147 @@ class TestMaterialRequirementsPlanningReport(ERPNextTestSuite):
[formatdate(delivery_date, "dd MMM") for delivery_date in delivery_dates],
)
def test_manufacture_lead_time_is_not_int_truncated(self):
"""lead_time = 1440 / manufacturing_time_in_mins + buffer_time. Both columns are Int;
integer/integer division truncates on Postgres (1440/7 -> 205) while MariaDB yields a
decimal, so the computed lead time (and the derived release date) diverged by engine."""
item = make_item("_Test MRP Lead Time Item", {"is_stock_item": 1}).name
frappe.get_doc(
{
"doctype": "Item Lead Time",
"item_code": item,
"manufacturing_time_in_mins": 7,
"buffer_time": 2,
}
).insert()
lead_time = get_item_lead_time(item, "Manufacture")
# 1440 / 7 + 2 = 207.714...; a truncating integer division on Postgres would give 207.
self.assertAlmostEqual(float(lead_time), 1440 / 7 + 2, places=2)
def test_make_order_creates_draft_purchase_and_work_orders(self):
plan = make_mrp_plan(self)
make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps)
purchase_order = get_created_order(plan.mps, "Purchase Order")
self.assertEqual(purchase_order.docstatus, 0)
self.assertEqual(purchase_order.supplier, SUPPLIER)
self.assertEqual([d.item_code for d in purchase_order.items], [plan.rm_item])
self.assertEqual(purchase_order.items[0].qty, plan.planned_qty * plan.rm_qty)
work_order = get_created_order(plan.mps, "Work Order")
self.assertEqual(work_order.docstatus, 0)
self.assertEqual(work_order.production_item, plan.fg_item)
self.assertEqual(work_order.bom_no, plan.bom)
self.assertEqual(work_order.qty, plan.planned_qty)
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)
frappe.get_doc(
{
"doctype": "Item Price",
"item_code": plan.rm_item,
"price_list": "Standard Buying",
"price_list_rate": 100,
}
).insert()
make_order(plan.rows, COMPANY, warehouse=WAREHOUSE, mps=plan.mps)
purchase_order = get_created_order(plan.mps, "Purchase Order")
self.assertEqual(purchase_order.buying_price_list, "Standard Buying")
self.assertEqual(purchase_order.items[0].rate, 100)
template = frappe.get_doc("Purchase Taxes and Charges Template", TAX_TEMPLATE)
self.assertEqual(purchase_order.taxes_and_charges, TAX_TEMPLATE)
self.assertEqual([d.rate for d in purchase_order.taxes], [d.rate for d in template.taxes])
net_total = flt(purchase_order.net_total)
self.assertEqual(
purchase_order.grand_total, net_total + net_total * flt(template.taxes[0].rate) / 100
)
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`."""
rm_item = make_item(
properties={
"is_stock_item": 1,
"is_purchase_item": 1,
"item_defaults": [
{"company": COMPANY, "default_warehouse": WAREHOUSE, "default_supplier": SUPPLIER}
],
}
).name
fg_item = make_item(
properties={
"is_stock_item": 1,
"item_defaults": [{"company": COMPANY, "default_warehouse": WAREHOUSE}],
}
).name
# on_submit sets Item.default_bom, which is how the report finds the raw materials
bom = make_bom(item=fg_item, raw_materials=[rm_item], rm_qty=rm_qty, rate=100).name
mps = frappe.get_doc(
{
"doctype": "Master Production Schedule",
"company": COMPANY,
"posting_date": today(),
"from_date": today(),
"parent_warehouse": WAREHOUSE,
"items": [
{
"item_code": fg_item,
"warehouse": WAREHOUSE,
"delivery_date": add_days(today(), 30),
"planned_qty": planned_qty,
"uom": frappe.get_cached_value("Item", fg_item, "stock_uom"),
}
],
}
)
# left in draft: on_submit enqueues MRP Log creation in a background job
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,
}
)
)
# the report separates each finished good with a blank row
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(
rm_item=rm_item,
fg_item=fg_item,
bom=bom,
mps=mps.name,
planned_qty=planned_qty,
rm_qty=rm_qty,
rows=rows,
)
def get_created_order(mps, doctype):
names = frappe.get_all(doctype, filters={"mps": mps}, pluck="name")
if len(names) != 1:
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(

View File

@@ -184,6 +184,7 @@ class BootStrapTestData:
self.make_loyalty_program()
self.make_shareholder()
self.make_sales_taxes_template()
self.make_purchase_taxes_template()
self.make_workstation()
self.make_operation()
self.make_bom()
@@ -2339,6 +2340,29 @@ class BootStrapTestData:
]
self.make_records(["title", "company"], records)
def make_purchase_taxes_template(self):
records = [
{
"company": "_Test Company",
"doctype": "Purchase Taxes and Charges Template",
"title": "_Test Purchase Taxes and Charges Template",
"taxes": [
{
"account_head": "_Test Account VAT - _TC",
"add_deduct_tax": "Add",
"category": "Total",
"charge_type": "On Net Total",
"cost_center": "Main - _TC",
"description": "VAT",
"doctype": "Purchase Taxes and Charges",
"parentfield": "taxes",
"rate": 6,
}
],
}
]
self.make_records(["title", "company"], records)
def make_asset_category(self):
records = [
{