From a464a6e4a1fcc4d062e5c7cc47fe92a9f45d9d5f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 13:04:45 +0530 Subject: [PATCH 1/2] fix: UOM whole number check truncated instead of rounding cint truncates, so a stock_qty of 1999.9998 (dust from qty times conversion factor) compared as abs(1999 - 2000.0) > epsilon and was rejected as fractional even though it rounds to a whole number at field precision, with the error confusingly printing the rounded value: 'Quantity (2000.0) cannot be a fraction'. Round to field precision first, then require the result to be a whole number. Dust above an integer already passed; this fixes the asymmetry for dust below. --- erpnext/utilities/transaction_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index e49660f33c0..07e9c40ebd7 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -618,13 +618,13 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): for f in qty_fields: qty = d.get(f) if qty: - precision = d.precision(f) - if abs(cint(qty) - flt(qty, precision)) > 0.0000001: + qty = flt(qty, d.precision(f)) + if qty != cint(qty): frappe.throw( _( "Row {1}: Quantity ({0}) cannot be a fraction. To allow this, disable '{2}' in UOM {3}." ).format( - flt(qty, precision), + qty, d.idx, frappe.bold(_("Must be Whole Number")), frappe.bold(d.get(uom_field)), From e6a6458ebeab882a47459965d192aac254b00892 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Fri, 7 Aug 2026 13:06:57 +0530 Subject: [PATCH 2/2] test: UOM whole number check tolerates conversion dust --- .../purchase_order/test_purchase_order.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 05c6542939d..a9da89a8306 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -724,6 +724,25 @@ class TestPurchaseOrder(ERPNextTestSuite): below_minimum.items[0].conversion_factor = 0.6 self.assertRaises(frappe.ValidationError, below_minimum.insert) + def test_uom_integer_check_tolerates_conversion_dust(self): + from erpnext.utilities.transaction_base import UOMMustBeIntegerError + + item_doc = make_item(properties={"stock_uom": "Nos"}) + item_doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.6}) + item_doc.save() + item = item_doc.name + + precision = frappe.get_precision("Purchase Order Item", "stock_qty") + po = create_purchase_order(item_code=item, qty=flt(2000 / 0.6, precision), do_not_save=1) + po.items[0].uom = "Kg" + po.items[0].conversion_factor = 0.6 + po.insert() + + fractional = create_purchase_order(item_code=item, qty=3333.9, do_not_save=1) + fractional.items[0].uom = "Kg" + fractional.items[0].conversion_factor = 0.6 + self.assertRaises(UOMMustBeIntegerError, fractional.insert) + def test_ordered_qty_for_closing_po(self): bin = frappe.get_all( "Bin",