fix(manufacturing): fall back to UOM Conversion Factor in Production Plan (backport #57553) (#57554)

fix(manufacturing): fall back to UOM Conversion Factor in Production Plan

Production Plan read the conversion factor straight off the item's own
UOM child table, so an item with a purchase UOM but no matching row threw
"UOM Conversion factor not found" while Stock Entry silently resolved it
from the item's variant template or the UOM Conversion Factor doctype.
Resolve it the same way, and keep returning None when nothing is
configured anywhere so the missing-setup error still fires.
This commit is contained in:
Mihir Kandoi
2026-07-28 20:32:05 +05:30
committed by GitHub
parent 7a606ab91c
commit 8f36753705
2 changed files with 121 additions and 1 deletions

View File

@@ -32,6 +32,7 @@ from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_childr
from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_or_make_bin
@@ -1333,9 +1334,16 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
def get_uom_conversion_factor(item_code, uom):
return frappe.db.get_value(
item = frappe.get_cached_value("Item", item_code, ["variant_of", "stock_uom"], as_dict=True)
conversion_factor = frappe.db.get_value(
"UOM Conversion Detail", {"parent": item_code, "uom": uom}, "conversion_factor"
)
if not conversion_factor and item.variant_of:
conversion_factor = frappe.db.get_value(
"UOM Conversion Detail", {"parent": item.variant_of, "uom": uom}, "conversion_factor"
)
return conversion_factor or get_uom_conv_factor(uom, item.stock_uom)
def get_subitems(

View File

@@ -1730,6 +1730,118 @@ class TestProductionPlan(ERPNextTestSuite):
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12.0)
def test_purchase_uom_falls_back_to_uom_conversion_factor(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
if not frappe.db.exists("UOM Conversion Factor", {"from_uom": "Kg", "to_uom": "Gram"}):
frappe.get_doc(
doctype="UOM Conversion Factor",
category="Mass",
from_uom="Kg",
to_uom="Gram",
value=1000,
).insert()
rm = make_item("Test RM Item Global CF", {"is_stock_item": 1, "stock_uom": "Gram"})
rm.purchase_uom = "Kg"
rm.save()
self.assertFalse([row for row in rm.uoms if row.uom == "Kg"])
bom_tree = {"Test FG Item Global CF": {rm.name: {}}}
parent_bom = create_nested_bom(bom_tree, prefix="")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=2000,
ignore_existing_ordered_qty=1,
skip_getting_mr_items=1,
do_not_submit=1,
warehouse="_Test Warehouse - _TC",
)
plan.for_warehouse = "_Test Warehouse - _TC"
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
)
row = frappe._dict(next(item for item in items if item["item_code"] == rm.name))
self.assertEqual(row.uom, "Kg")
self.assertEqual(row.conversion_factor, 1000)
self.assertEqual(row.quantity, 2)
def test_variant_inherits_purchase_uom_conversion_factor_of_template(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
template = make_item(
"TRMVCF",
{
"is_stock_item": 1,
"stock_uom": "Nos",
"has_variants": 1,
"attributes": [{"attribute": "Colour"}],
},
)
if not [row for row in template.uoms if row.uom == "Box"]:
template.purchase_uom = "Box"
template.append("uoms", {"uom": "Box", "conversion_factor": 12})
template.save()
if not frappe.db.exists("Item", "TRMVCF-RED"):
create_variant("TRMVCF", {"Colour": "Red"}).insert()
variant = frappe.get_doc("Item", "TRMVCF-RED")
variant.uoms = [row for row in variant.uoms if row.uom != "Box"]
variant.purchase_uom = "Box"
variant.save()
bom_tree = {"Test FG Item Variant CF": {variant.name: {}}}
parent_bom = create_nested_bom(bom_tree, prefix="")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=24,
ignore_existing_ordered_qty=1,
skip_getting_mr_items=1,
do_not_submit=1,
warehouse="_Test Warehouse - _TC",
)
plan.for_warehouse = "_Test Warehouse - _TC"
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
)
row = frappe._dict(next(item for item in items if item["item_code"] == variant.name))
self.assertEqual(row.conversion_factor, 12)
self.assertEqual(row.quantity, 2)
def test_missing_purchase_uom_conversion_factor_throws(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
rm = make_item("Test RM Item Missing CF", {"is_stock_item": 1, "stock_uom": "Nos"})
rm.purchase_uom = "Box"
rm.save()
bom_tree = {"Test FG Item Missing CF": {rm.name: {}}}
parent_bom = create_nested_bom(bom_tree, prefix="")
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
ignore_existing_ordered_qty=1,
skip_getting_mr_items=1,
do_not_submit=1,
warehouse="_Test Warehouse - _TC",
)
plan.for_warehouse = "_Test Warehouse - _TC"
with self.assertRaises(frappe.ValidationError) as error:
get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": "_Test Warehouse - _TC"}]
)
self.assertIn("UOM Conversion factor", str(error.exception))
def test_mr_qty_for_complex_bom(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse