From 4373e295de8d600a3aef1fc9ed8df831d073f0b6 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 31 Jul 2026 21:53:59 +0530 Subject: [PATCH 1/3] 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) # Conflicts: # erpnext/controllers/taxes_and_totals.py --- erpnext/controllers/buying_controller.py | 2 +- erpnext/controllers/taxes_and_totals.py | 12 ++++++++++++ .../public/js/controllers/taxes_and_totals.js | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index dea76428d90..5b8df2cf767 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -358,7 +358,7 @@ class BuyingController(SubcontractingController): ) valuation_amount_adjustment -= item.item_tax_amount - 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 e218e9a44cb..bbfc9eba9cb 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -184,8 +184,20 @@ class calculate_taxes_and_totals: if self.doc.get("is_consolidated"): return +<<<<<<< HEAD if not self.discount_amount_applied: 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) +>>>>>>> 269cc6ee3b (fix: preserve UOM conversion factor precision in transactions) for item in self.doc.items: self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 1f091f3934d..4d980d7e277 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -126,11 +126,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 5a5e20e167a93bff3ae5e78f430cbb410a1bdf40 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 31 Jul 2026 21:54:05 +0530 Subject: [PATCH 2/3] 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 ac11b2fb7d9..6ae289625bc 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -795,6 +795,28 @@ class TestMaterialRequest(FrappeTestCase): 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") From 00df8652e341bd7755da312546c6f7fd711c23ef Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 2 Aug 2026 12:03:47 +0530 Subject: [PATCH 3/3] fix: resolve version 15 backport conflict --- erpnext/controllers/taxes_and_totals.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index bbfc9eba9cb..a8a48140bdd 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -184,20 +184,13 @@ class calculate_taxes_and_totals: if self.doc.get("is_consolidated"): return -<<<<<<< HEAD if not self.discount_amount_applied: - 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) ->>>>>>> 269cc6ee3b (fix: preserve UOM conversion factor precision in transactions) + 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)