From e5999b22c79a71cb1a39735d45ca3aea27b0bece Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 31 Jul 2026 21:53:59 +0530 Subject: [PATCH 1/2] fix: preserve UOM conversion factor precision in transactions calculate_item_values rounds every Float field on an item row to the site's Float Precision (3 by default), and conversion_factor was one of them. The factor is a ratio, not a rate: UOM Conversion Factor.value is stored at precision 9, and Material Request keeps the full value because it has no currency field and so never runs the calculation. Mapping a Material Request to a Purchase Order therefore truncated the factor - 0.453592292 for Pound -> Kg became 0.454 - and stock_qty, which is recomputed as qty * conversion_factor, drifted from the quantity that was requested, leaving the Material Request unable to close. Exclude conversion_factor from the rounded fields on the server and on the client. Factors below the site precision would otherwise round to zero outright. (cherry picked from commit 269cc6ee3bcbfed10e487561f829845f40cf2c4e) --- erpnext/controllers/buying_controller.py | 2 +- erpnext/controllers/taxes_and_totals.py | 7 ++++++- .../public/js/controllers/taxes_and_totals.js | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 88bb4b29a84..b2e8a73d065 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -467,7 +467,7 @@ class BuyingController(SubcontractingController): self.precision("item_tax_amount", item), ) - self.round_floats_in(item) + self.round_floats_in(item, do_not_round_fields=["conversion_factor"]) if flt(item.conversion_factor) == 0.0: item.conversion_factor = ( get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index fcf10b019d8..bd8db461524 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -227,7 +227,12 @@ class calculate_taxes_and_totals: if self.doc.get("is_consolidated") or self.discount_amount_applied: return - do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"] + do_not_round_fields = [ + "valuation_rate", + "incoming_rate", + "sales_incoming_rate", + "conversion_factor", + ] for item in self.doc.items: self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) self.calculate_item_rate(item) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 8a0719c6d3f..af993a6a082 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -138,11 +138,26 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } + get_item_fields_to_round() { + const [item] = this.frm.doc.items || []; + if (!item) { + return []; + } + + const do_not_round_fields = ["conversion_factor"]; + return frappe.meta + .get_fieldnames(item.doctype, item.parent, { + fieldtype: ["in", ["Currency", "Float"]], + }) + .filter((fieldname) => !do_not_round_fields.includes(fieldname)); + } + calculate_item_values() { var me = this; if (!this.discount_amount_applied) { + const fields_to_round = this.get_item_fields_to_round(); for (const item of this.frm.doc.items || []) { - frappe.model.round_floats_in(item); + frappe.model.round_floats_in(item, fields_to_round); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; From 5463bd93aa48c83ec78c69a2f02720910bbad907 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 31 Jul 2026 21:54:05 +0530 Subject: [PATCH 2/2] test: fractional conversion factor survives Material Request to Purchase Order Fails before the fix with 0.45 != 0.453592292 on a site with Float Precision 2, and 0.454 on the default of 3. (cherry picked from commit f4d70c2d60f7f2d8a6ff3bc98f436366c94de2d1) --- .../material_request/test_material_request.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index b23118f524c..51ad01517a4 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -847,6 +847,28 @@ class TestMaterialRequest(ERPNextTestSuite): mr = frappe.get_doc("Material Request", mr.name) self.assertEqual(mr.per_ordered, 100) + def test_fractional_conversion_factor_for_purchase(self): + item = create_item("_Test Fractional Conversion Item", stock_uom="Kg", is_purchase_item=1) + conversion_factor = 0.453592292 + + mr = make_material_request( + item_code=item.name, + qty=1000, + uom="Pound", + conversion_factor=conversion_factor, + ) + mr.reload() + + self.assertEqual(mr.items[0].conversion_factor, conversion_factor) + + po = make_purchase_order(mr.name) + po.supplier = "_Test Supplier" + po.insert() + po.reload() + + self.assertEqual(po.items[0].conversion_factor, conversion_factor) + self.assertEqual(po.items[0].stock_qty, mr.items[0].stock_qty) + def test_customer_provided_parts_mr(self): create_item("CUST-0987", is_customer_provided_item=1, customer="_Test Customer", is_purchase_item=0) existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")