From 44260b469f325770f7764d77a28ac920a8df92c5 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:48:19 +0530 Subject: [PATCH 1/4] refactor: remove unreachable UOM conversion in production plan The division by conversion_factor in _adjust_required_qty_for_uom sits directly after frappe.throw inside the same block, so it can never run. It has been dead since commit 2a8cd05b44 (#27278) re-indented it into the throw branch; the actual purchase-UOM conversion happens in _material_request_item_row via _mr_purchase_conversion_factor. --- .../doctype/production_plan/services/material_request.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/services/material_request.py b/erpnext/manufacturing/doctype/production_plan/services/material_request.py index 9e6d26bd963..c9073d5e648 100644 --- a/erpnext/manufacturing/doctype/production_plan/services/material_request.py +++ b/erpnext/manufacturing/doctype/production_plan/services/material_request.py @@ -533,7 +533,6 @@ def _adjust_required_qty_for_uom(row, required_qty): row["purchase_uom"], row["stock_uom"], row.item_code ) ) - required_qty = required_qty / row["conversion_factor"] if frappe.db.get_value("UOM", row["purchase_uom"], "must_be_whole_number"): required_qty = ceil(required_qty) From ffc515f04618caf294892f579be4091682266cea Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:48:42 +0530 Subject: [PATCH 2/4] fix: round production plan mr_items quantity to field precision The stock-UOM qty is rounded in _accumulate_so_items, but the purchase UOM conversion divided it by the conversion factor without re-rounding, storing values like 5738748.300863984 in mr_items.quantity. The raw value flowed into Material Request qty and the raw materials CSV, and make_material_request compares quantity to requested_qty with exact float equality, so any rounding downstream left dust quantities. --- .../doctype/production_plan/services/material_request.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/services/material_request.py b/erpnext/manufacturing/doctype/production_plan/services/material_request.py index c9073d5e648..ac689ba0a3f 100644 --- a/erpnext/manufacturing/doctype/production_plan/services/material_request.py +++ b/erpnext/manufacturing/doctype/production_plan/services/material_request.py @@ -559,10 +559,11 @@ def _material_request_item_row( or row.get("default_warehouse") or item_group_defaults.get("default_warehouse") ) + precision = frappe.get_precision("Material Request Plan Item", "quantity") return { "item_code": row.item_code, "item_name": row.item_name, - "quantity": required_qty / conversion_factor, + "quantity": flt(required_qty / conversion_factor, precision), "conversion_factor": conversion_factor, "required_bom_qty": row.get("qty"), "stock_uom": row.get("stock_uom"), @@ -639,7 +640,7 @@ def _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_m if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) - item["quantity"] = required_qty / item.get("conversion_factor") + item["quantity"] = flt(required_qty / item.get("conversion_factor"), precision) new_mr_items.append(item) From f5157bf3c42f6b3550c6b4eda97603dba36d60a3 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:49:42 +0530 Subject: [PATCH 3/4] test: mr_items quantity is rounded to field precision --- .../production_plan/test_production_plan.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 5917804c411..6a666f21d49 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1367,6 +1367,29 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(row.uom, "Nos") self.assertEqual(row.qty, 1) + def test_material_request_item_quantity_rounded_to_precision(self): + from erpnext.stock.doctype.item.test_item import make_item + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 3}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=10, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1" + ) + + precision = frappe.get_precision("Material Request Plan Item", "quantity") + self.assertEqual(len(pln.mr_items), 1) + self.assertEqual(pln.mr_items[0].quantity, flt(10 / 3, precision)) + def test_material_request_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom From 75145cc72c9ed67921b53f4efca0e863bf7ff515 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 6 Aug 2026 20:59:04 +0530 Subject: [PATCH 4/4] test: remaining purchase qty is rounded to field precision Covers the _add_remaining_purchase_request path: partial stock in another warehouse is allocated as a transfer and the residual purchase qty goes through the second rounding site. --- .../production_plan/test_production_plan.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 6a666f21d49..a7891b10be0 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2275,6 +2275,40 @@ class TestProductionPlan(ERPNextTestSuite): self.assertEqual(row.get("uom"), "Nos") self.assertEqual(row.get("conversion_factor"), 10.0) + def test_remaining_purchase_qty_rounded_to_precision(self): + from erpnext.stock.doctype.item.test_item import make_item + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name + bom_item = make_item( + properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"} + ).name + + store_warehouse = create_warehouse("Store Warehouse", company="_Test Company") + rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company") + + make_stock_entry(item_code=bom_item, qty=4, target=store_warehouse, rate=100) + + if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}): + doc = frappe.get_doc("Item", bom_item) + doc.append("uoms", {"uom": "Nos", "conversion_factor": 3}) + doc.save() + + make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC") + + pln = create_production_plan( + item_code=fg_item, planned_qty=30, stock_uom="_Test UOM 1", do_not_submit=1 + ) + pln.for_warehouse = rm_warehouse + pln.ignore_existing_ordered_qty = 1 + items = get_items_for_material_requests(pln.as_dict(), warehouses=[{"warehouse": store_warehouse}]) + + rows_by_type = {row.get("material_request_type"): row for row in items} + self.assertEqual(rows_by_type["Material Transfer"].get("quantity"), 4) + + precision = frappe.get_precision("Material Request Plan Item", "quantity") + self.assertEqual(rows_by_type["Purchase"].get("quantity"), flt(26 / 3, precision)) + def test_unreserve_qty_on_closing_of_pp(self): from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.utils import get_or_make_bin