From 6972f161b8188cfe3147d7af0675d3e268bb5417 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:33:03 +0200 Subject: [PATCH] fix(accounts): dynamic zero cutoff (#48899) * fix(accounts): dynamic zero cutoff The cutoff for displaying zero values in accounting reports has so far been hardcoded to 0.005, giving wrong results for currencies that require a higher precision. This PR changes this to a dynamic value calculated from the Currency's _Fraction Units_. * style: fix typo --- .../consolidated_financial_statement.py | 3 ++- .../dimension_wise_accounts_balance_report.py | 3 ++- .../accounts/report/financial_statements.py | 4 ++-- .../profitability_analysis.py | 3 ++- .../report/trial_balance/trial_balance.py | 3 ++- erpnext/accounts/test/test_utils.py | 6 +++++ erpnext/accounts/utils.py | 24 +++++++++++++++++++ 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 710a898ef1c..4d4eb520933 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -38,6 +38,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement get_report_summary as get_pl_summary, ) from erpnext.accounts.report.utils import convert, convert_to_presentation_currency +from erpnext.accounts.utils import get_zero_cutoff def execute(filters=None): @@ -564,7 +565,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com row[company] = flt(d.get(company, 0.0), 3) - if abs(row[company]) >= 0.005: + if abs(row[company]) >= get_zero_cutoff(filters.presentation_currency): # ignore zero values has_value = True total += flt(row[company]) diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index ed30ad415d0..2d0015a461e 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import ( filter_out_zero_value_rows, ) from erpnext.accounts.report.trial_balance.trial_balance import validate_filters +from erpnext.accounts.utils import get_zero_cutoff def execute(filters=None): @@ -154,7 +155,7 @@ def prepare_data(accounts, filters, company_currency, dimension_list): for dimension in dimension_list: row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(dimension)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True total += flt(d.get(frappe.scrub(dimension), 0.0), 3) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 6966c00e926..b1d8e8d515c 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -18,7 +18,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_dimension_with_children, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import get_fiscal_year, get_zero_cutoff def get_period_list( @@ -306,7 +306,7 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum row[period.key] = flt(d.get(period.key, 0.0), 3) - if abs(row[period.key]) >= 0.005: + if abs(row[period.key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True total += flt(row[period.key]) diff --git a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py index dfb941d9123..5f3215fe7e2 100644 --- a/erpnext/accounts/report/profitability_analysis/profitability_analysis.py +++ b/erpnext/accounts/report/profitability_analysis/profitability_analysis.py @@ -12,6 +12,7 @@ from erpnext.accounts.report.financial_statements import ( filter_out_zero_value_rows, ) from erpnext.accounts.report.trial_balance.trial_balance import validate_filters +from erpnext.accounts.utils import get_zero_cutoff value_fields = ("income", "expense", "gross_profit_loss") @@ -149,7 +150,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on): for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) - if abs(row[key]) >= 0.005: + if abs(row[key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index df6e547eae3..95fbd68f381 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -18,6 +18,7 @@ from erpnext.accounts.report.financial_statements import ( set_gl_entries_by_account, ) from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.utils import get_zero_cutoff value_fields = ( "opening_debit", @@ -412,7 +413,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): for key in value_fields: row[key] = flt(d.get(key, 0.0), 3) - if abs(row[key]) >= 0.005: + if abs(row[key]) >= get_zero_cutoff(company_currency): # ignore zero values has_value = True diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 9333652ccc6..fd2dc61096e 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -10,6 +10,7 @@ from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.utils import ( get_future_stock_vouchers, get_voucherwise_gl_entries, + get_zero_cutoff, sort_stock_vouchers_by_posting_date, ) from erpnext.stock.doctype.item.test_item import make_item @@ -157,6 +158,11 @@ class TestUtils(IntegrationTestCase): self.assertSequenceEqual(doc_name[0:2], ("SUP", fiscal_year)) frappe.db.set_default("supp_master_name", "Supplier Name") + def test_get_zero_cutoff(self): + self.assertEqual(get_zero_cutoff(None), 0.005) + self.assertEqual(get_zero_cutoff("EUR"), 0.005) + self.assertEqual(get_zero_cutoff("BHD"), 0.0005) + ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f45b7b8b499..0876b4d50f0 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -27,6 +27,7 @@ from frappe.utils import ( now, nowdate, ) +from frappe.utils.caching import site_cache from pypika import Order from pypika.functions import Coalesce from pypika.terms import ExistsCriterion @@ -1147,6 +1148,29 @@ def get_currency_precision(): return precision +def get_fraction_units(currency: str) -> int: + """Returns the number of fraction units for a currency.""" + fraction_units = frappe.db.get_value("Currency", currency, "fraction_units") + + if fraction_units is None: + fraction_units = 100 + + return fraction_units + + +@site_cache() +def get_zero_cutoff(currency: str) -> float: + """Returns the zero cutoff for a currency. + + For example, if the Fraction Units for a currency are set to 100, then the zero cutoff is 0.005. + We don't want to display values less than the zero cutoff. + This value was chosen for compatibility with the previous hard-coded value of 0.005. + """ + fraction_units = get_fraction_units(currency) + + return 0.5 / (fraction_units or 1) + + def get_held_invoices(party_type, party): """ Returns a list of names Purchase Invoices for the given party that are on hold