diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 645ad5d4cc5..97072a8642f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -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( diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c3aaeb28526..1aeb36fe536 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -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