Why ERPNext Rounds Your Invoice Amounts Wrong, and the Precision Settings That Fix It
An invoice comes out one cent off. Then a purchase order total does not match the sum of its rows. Then a prorated subscription bills an amount that no one can reconstruct by hand. Each one looks like a fresh bug, gets a fresh forum thread, and gets closed with a version-specific patch. A year later the same class of problem surfaces in a different module, and everyone starts over.
It is not a hundred different bugs. It is one thing wearing different hats: how Frappe stores and rounds numbers, and how that interacts with the precision settings almost nobody configures on purpose. Once you understand where rounding actually happens, these stop being mysteries and become a checklist. This is the article I wish existed the third time I traced a "decimal point being shifted" report back to the same root cause.
ERPNext rounding surprises come from the interaction of the field's fieldtype, the global Currency Precision and Float Precision in System Settings, and per-field precision overrides, applied at three different layers (browser, server, database).
- Currency fields round to Currency Precision; Float fields round to Float Precision. If a monetary value lives in a Float field, it rounds by the wrong rule.
- Set System Settings > Currency Precision and Float Precision explicitly rather than leaving them to defaults, and keep them consistent with how your currency actually works.
- The rounding you see on screen (client JS) can differ from what the server stores. Always check the saved value in the database, not just the printed figure.
The symptom, in the words people report it
The reports read like this one:
Incorrect number format handling leads to decimal point being shifted
and its cousins: "invoice total is off by 0.01", "row amounts do not add up to the grand total", "subscription prorate charges a weird amount". They share a shape: a number that is almost right, off by a rounding step, or shifted because a value got rounded at one layer and then used at full precision at another. When you see "almost right, by a rounding amount," think precision settings, not arithmetic bugs.
Why it happens: fieldtype decides the rounding rule
In the Frappe ORM, a numeric field is one of a few fieldtypes, and the fieldtype, not the value, decides how it rounds:
| Fieldtype | Rounds to | Use for |
|---|---|---|
| Currency | Currency Precision (global, or per-field override) | Money: rates, amounts, totals, taxes |
| Float | Float Precision (global, or per-field override) | Quantities, ratios, conversion factors |
| Int | Whole number | Counts |
| Percent | Float Precision | Percentages |
Here is the first place things go wrong. Currency and Float round by different global settings. If a monetary amount is stored in a Float field (a custom field someone added as "Float" because it was the default, or a calculation that lands in a Float variable), it rounds to the Float Precision rule, which is typically three decimals, not the two your currency expects. Now a value like 1234.005 rounds one way as a Float and another as a Currency, and the totals stop reconciling.
The flt() function is where this is enforced in code. flt(value, precision) converts a value to a float and rounds it to precision decimal places. When precision is not passed explicitly, the effective precision comes from the field definition and the global settings. So two calculations that look identical in Python can round differently because one passed a precision and the other inherited a different one.
The three global levers, and the per-field override
Two settings in System Settings govern almost all of this:
- Currency Precision: the number of decimal places Currency fields round to across the system. Leave it unset and Frappe falls back to a default derived from context, which is exactly the ambiguity you want to remove. Set it explicitly to match your currency (2 for KES, USD, EUR; 0 if you genuinely bill in whole shillings and want no cents at all).
- Float Precision: the decimal places Float fields round to. This affects quantities and conversion factors. If your unit conversions need more precision (say you buy in tonnes and sell in grams), too few decimals here silently rounds quantities and throws off valuation.
- Number Format: controls the display grouping and decimal separator (for example
#,###.##). This is presentation, but a mismatched number format is a frequent cause of a value that looks shifted because the thousands and decimal separators were interpreted the wrong way round on import.
On top of the globals, an individual DocField can carry its own precision value, which overrides the global for that one field. This is powerful and is also how "we fixed it in one module" happens: someone set a field-level precision to patch a single report, and now that field rounds differently from every other monetary field, which resurfaces as an inconsistency somewhere else. When you audit, you have to check both the global settings and whether the specific field has an override.
Why subscriptions and prorated amounts are the worst offenders
A Sales Invoice mostly deals in amounts you entered, so rounding is bounded. A prorated Subscription computes an amount by multiplying a rate by a fraction of a billing period, which produces a value with many decimal places that must be rounded to something. If the intermediate fraction is a Float rounded to Float Precision, and the resulting amount is a Currency rounded to Currency Precision, you have two rounding steps with two rules feeding each other, and the final figure can differ from a hand calculation by more than one cent. Purchase Orders sit in between: row-level rate times quantity, each rounded, then summed, where the sum of rounded rows may not equal the rounded sum.
This is not unique to ERPNext; it is the classic "round each line vs round the total" problem that every accounting system has to pick a convention for. The point is that ERPNext's convention is governed by these precision settings, so the fix is to set them coherently rather than to patch each calculation.
A reproducible test case you can file or debug with
When you hit one of these, isolate it before blaming a module. Open bench console and reproduce the exact arithmetic with the exact precision, so you can see which layer rounds:
bench --site yoursite.local console
from frappe.utils import flt
rate = 1234.005
qty = 3
# What Float Precision (say 3) would do to an intermediate:
print(flt(rate, 3)) # rounds the rate as a float
# What Currency Precision (say 2) does to the amount:
print(flt(rate * qty, 2)) # the monetary result
# Sum-of-rounded-rows vs rounded-sum, the reconciliation gap:
rows = [10.005, 10.005, 10.005]
print(flt(sum(flt(r, 2) for r in rows), 2)) # round each, then sum
print(flt(sum(rows), 2)) # sum, then round
If the two totals differ, you have reproduced the sum-of-rounded-rows discrepancy in three lines, independent of any module. That snippet, plus your System Settings Currency and Float Precision values, is exactly what makes a filable bug report actionable instead of "the invoice is wrong."
The fix, in order
Step 1: set the global precision explicitly
Go to System Settings and set Currency Precision and Float Precision to values that match your actual currency and quantity needs, rather than leaving either blank. For most East African and single-currency setups that is Currency Precision 2 and a Float Precision high enough for your unit conversions. Save, and reload so the client picks up the new values.
Step 2: audit for a monetary value living in a Float field
Any custom field holding money must be fieldtype Currency, not Float. Check your customisations:
frappe.get_all("Custom Field",
filters={"fieldtype": "Float"},
fields=["dt", "fieldname", "label"])
For each result, ask whether it holds money. If it does, change it to Currency so it rounds by the currency rule. This is the single most common root cause of a monetary total that will not reconcile.
Step 3: hunt down stray per-field precision overrides
A field with its own precision set will ignore the global. Find fields that carry an override and confirm each one is intentional, because an accidental override in a child table is a classic source of "row amounts do not sum to the total."
What people get wrong
The recurring mistake is treating each occurrence as a code bug and patching the calculation, when the calculation is doing exactly what the precision settings told it to. Adding a manual round() in one place shifts the discrepancy somewhere else and leaves the next module to rediscover it. The other mistake is changing precision settings on a live system with historical documents already posted; the settings affect new rounding, and mixing conventions across a fiscal period can make old and new documents disagree. Change precision deliberately, ideally at a period boundary, and understand you are not retroactively re-rounding history. If your books also span currencies, the rounding question compounds with the FX question, which the piece on multi-currency ledgers and the FX gap covers.
Verification
Confirm the fix by comparing the printed figure with the stored figure, because the whole class of bug is the two disagreeing. Create a test invoice that previously misrounded, then read the value straight from the database rather than the print format:
doc = frappe.get_doc("Sales Invoice", "SINV-TEST-0001")
print(doc.grand_total, [ (r.item_code, r.amount) for r in doc.items ])
Verify the grand total equals the sum of the row amounts under your chosen convention, and that both match what the print format shows. Then reconcile the same document against the General Ledger; if the GL entries agree with the invoice total, the rounding is consistent all the way through. For finding these across many documents at once, the MariaDB query approach lets you pull totals and row-sums in bulk and flag the ones that disagree.
When it is still broken
If totals still misround after setting the globals, check for a field-level precision override you missed, and check whether a custom script or hook is recomputing an amount without passing the right precision to flt(). If imported data looks decimal-shifted, the culprit is almost always the Number Format separators being interpreted the wrong way at import, not rounding at all, so re-check the source file's decimal and thousands separators against your Number Format setting. And if a specific standard module still produces a figure you cannot reconstruct even in the console, capture the console reproduction above and file it upstream with your precise settings, because at that point it may be a genuine calculation bug rather than a configuration one.
Frequently asked questions
- Why is my ERPNext invoice total off by one cent?
- Almost always because rounding happens by different rules at different layers. Currency fields round to the Currency Precision setting while Float fields round to Float Precision, and the sum of individually rounded rows may not equal the rounded grand total. Check that all monetary fields are the Currency fieldtype and that Currency Precision in System Settings is set explicitly to match your currency.
- What is the difference between Currency Precision and Float Precision in ERPNext?
- Currency Precision is the number of decimal places that Currency fields round to, meant for money such as rates, amounts and taxes. Float Precision is the decimals that Float fields round to, meant for quantities, ratios and conversion factors. If a monetary value is stored in a Float field it rounds by the Float rule, which is usually the wrong number of decimals and a common cause of totals that will not reconcile.
- Where does ERPNext actually do the rounding?
- At three layers: the browser client script rounds for display as you type, the server-side validate and calculation code rounds via flt(value, precision) and produces the authoritative value, and the database column stores it at its own decimal scale. The figure printed on the invoice is the display layer, so to diagnose a rounding bug you must read the value stored in the database, not just the printed number.
- Is it safe to change the precision settings on a live ERPNext system?
- Change them deliberately, ideally at a fiscal period boundary. The settings affect how new documents round; they do not retroactively re-round documents you already posted. Mixing conventions within a period can make older and newer documents disagree, so set Currency and Float Precision explicitly early, and understand you are not rewriting history when you change them.