From 3592637b5c88249ff9c0a721eb7d8d3b7df646c6 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 17:58:47 +0530 Subject: [PATCH 1/5] fix(taxes): increase rounding threshold for tax breakup calculations (cherry picked from commit 7f87a5e5c6fb7c283b59484da8e4d9cd798aea95) --- erpnext/controllers/taxes_and_totals.py | 2 +- .../tests/test_item_wise_tax_details.py | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 9d98eed668d..fa55aa1daa5 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -522,7 +522,7 @@ class calculate_taxes_and_totals: diff = flt(expected_amount - actual_breakup, 5) # TODO: fix rounding difference issues - if abs(diff) <= 0.5: + if abs(diff) <= 1: detail_row = self.doc._item_wise_tax_details[last_idx] detail_row["amount"] = flt(detail_row["amount"] + diff, 5) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index f6d94c61eca..d4a560284ef 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -124,3 +124,107 @@ class TestTaxesAndTotals(ERPNextTestSuite): ] self.assertEqual(actual_values, expected_values) + + def test_item_wise_tax_detail_with_multi_currency(self): + """ + For multi-item, multi-currency invoices, item-wise tax breakup should + still reconcile with base tax totals. + """ + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "USD", + "debit_to": "_Test Receivable USD - _TC", + "conversion_rate": 129.99, + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "rate": 47.41, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 2, + "rate": 33.33, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 16, + }, + { + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 1, + }, + ], + } + ) + doc.save() + + details_by_tax = {} + for detail in doc.item_wise_tax_details: + bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0, "taxable_amount": 0.0}) + bucket["amount"] += detail.amount + + for tax in doc.taxes: + detail_totals = details_by_tax[tax.name] + self.assertAlmostEqual( + detail_totals["amount"], tax.base_tax_amount_after_discount_amount, places=2 + ) + + def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): + """ + When the tax amount (in transaction currency) has more decimals than + the field precision, rounding must happen *before* multiplying by + conversion_rate — the same order used by _set_in_company_currency. + """ + doc = frappe.get_doc( + { + "doctype": "Sales Invoice", + "customer": "_Test Customer", + "company": "_Test Company", + "currency": "USD", + "debit_to": "_Test Receivable USD - _TC", + "conversion_rate": 129.99, + "items": [ + { + "item_code": "_Test Item", + "qty": 1, + "rate": 47.41, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + } + ], + "taxes": [ + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 16, + }, + ], + } + ) + doc.save() + + tax = doc.taxes[0] + detail = doc.item_wise_tax_details[0] + self.assertEqual(detail.amount, tax.base_tax_amount_after_discount_amount) From 6689b17b882f2bd8e5ea3a7ac10aba9e90d90345 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 18:37:07 +0530 Subject: [PATCH 2/5] fix(tests): update item code and quantity in tax detail test case (cherry picked from commit 3449ab063aac954f38bf3dbf7048f13457f60dcb) --- erpnext/controllers/tests/test_item_wise_tax_details.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index d4a560284ef..6b259429cc5 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -148,7 +148,7 @@ class TestTaxesAndTotals(ERPNextTestSuite): "cost_center": "_Test Cost Center - _TC", }, { - "item_code": "_Test Item", + "item_code": "_Test Item 2", "qty": 2, "rate": 33.33, "income_account": "Sales - _TC", From 6ad5e8960782f761ce4dcca305c2c94ea88b11fc Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Mon, 30 Mar 2026 19:37:59 +0530 Subject: [PATCH 3/5] fix(taxes): improve tax calculation accuracy and update test assertions (cherry picked from commit a18196f584d4d234e669e23842223f6042360c1d) --- erpnext/controllers/taxes_and_totals.py | 29 +++++++++++++++---- .../tests/test_item_wise_tax_details.py | 7 ++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index fa55aa1daa5..f0da61ad900 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -285,6 +285,13 @@ class calculate_taxes_and_totals: self.doc._item_wise_tax_details = item_wise_tax_details self.doc.item_wise_tax_details = [] + for tax in self.doc.get("taxes"): + if not tax.get("dont_recompute_tax"): + tax._running_txn_tax_total = 0.0 + tax._running_base_tax_total = 0.0 + tax._running_txn_taxable_total = 0.0 + tax._running_base_taxable_total = 0.0 + def determine_exclusive_rate(self): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): return @@ -521,8 +528,7 @@ class calculate_taxes_and_totals: actual_breakup = tax._total_tax_breakup diff = flt(expected_amount - actual_breakup, 5) - # TODO: fix rounding difference issues - if abs(diff) <= 1: + if abs(diff) <= 0.5: detail_row = self.doc._item_wise_tax_details[last_idx] detail_row["amount"] = flt(detail_row["amount"] + diff, 5) @@ -597,14 +603,25 @@ class calculate_taxes_and_totals: def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount): # store tax breakup for each item multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1 - item_wise_tax_amount = flt( - current_tax_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") + + # Error diffusion: derive each item's base amount as a delta of the running cumulative total + # so the sum always equals base_tax_amount_after_discount_amount. + tax._running_txn_tax_total += current_tax_amount * multiplier + new_base_tax_total = flt( + flt(tax._running_txn_tax_total, tax.precision("tax_amount")) * self.doc.conversion_rate, + tax.precision("base_tax_amount"), ) + item_wise_tax_amount = new_base_tax_total - tax._running_base_tax_total + tax._running_base_tax_total = new_base_tax_total if tax.charge_type != "On Item Quantity": - item_wise_taxable_amount = flt( - current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount") + tax._running_txn_taxable_total += current_net_amount * multiplier + new_base_taxable_total = flt( + flt(tax._running_txn_taxable_total, tax.precision("net_amount")) * self.doc.conversion_rate, + tax.precision("base_net_amount"), ) + item_wise_taxable_amount = new_base_taxable_total - tax._running_base_taxable_total + tax._running_base_taxable_total = new_base_taxable_total else: item_wise_taxable_amount = 0.0 diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index 6b259429cc5..cce4f2ce024 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -179,14 +179,11 @@ class TestTaxesAndTotals(ERPNextTestSuite): details_by_tax = {} for detail in doc.item_wise_tax_details: - bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0, "taxable_amount": 0.0}) + bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0}) bucket["amount"] += detail.amount for tax in doc.taxes: - detail_totals = details_by_tax[tax.name] - self.assertAlmostEqual( - detail_totals["amount"], tax.base_tax_amount_after_discount_amount, places=2 - ) + self.assertEqual(details_by_tax[tax.name]["amount"], tax.base_tax_amount_after_discount_amount) def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ From 5922d25210d434478a0947a0b6a68fda23164e0f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 30 Mar 2026 20:04:53 +0530 Subject: [PATCH 4/5] test: update item-wise tax detail test for high conversion rates (cherry picked from commit fc8437c499e3746c82cc64c26751f8b546fd8d94) # Conflicts: # erpnext/controllers/tests/test_item_wise_tax_details.py --- .../tests/test_item_wise_tax_details.py | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index cce4f2ce024..c7a61fcb47d 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -2,8 +2,12 @@ import json import frappe +<<<<<<< HEAD from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals from erpnext.tests.utils import ERPNextTestSuite +======= +from erpnext.tests.utils import ERPNextTestSuite, change_settings +>>>>>>> fc8437c499 (test: update item-wise tax detail test for high conversion rates) class TestTaxesAndTotals(ERPNextTestSuite): @@ -125,10 +129,22 @@ class TestTaxesAndTotals(ERPNextTestSuite): self.assertEqual(actual_values, expected_values) - def test_item_wise_tax_detail_with_multi_currency(self): + @change_settings("Selling Settings", {"allow_multiple_items": 1}) + def test_item_wise_tax_detail_high_conversion_rate(self): """ - For multi-item, multi-currency invoices, item-wise tax breakup should - still reconcile with base tax totals. + With a high conversion rate (e.g. USD -> KRW ~1300), independently rounding + each item's base tax amount causes per-item errors that accumulate and exceed + the 0.5-unit safety threshold, raising a validation error. + + Error diffusion fixes this: the cumulative base total after the last item + equals base_tax_amount_after_discount_amount exactly, so the sum of all + per-item amounts is always exact regardless of item count or rate magnitude. + + Analytically with conversion_rate=1300, rate=7.77 x3 items, VAT 16%: + per-item txn tax = 1.2432 + OLD independent: flt(1.2432 * 1300, 2) = 1616.16 -> sum 4848.48 + expected base: flt(flt(3.7296, 2) * 1300, 0) = flt(3.73 * 1300, 0) = 4849 + diff = 0.52 -> exceeds 0.5 threshold -> would throw with old code """ doc = frappe.get_doc( { @@ -137,20 +153,28 @@ class TestTaxesAndTotals(ERPNextTestSuite): "company": "_Test Company", "currency": "USD", "debit_to": "_Test Receivable USD - _TC", - "conversion_rate": 129.99, + "conversion_rate": 1300, "items": [ { "item_code": "_Test Item", "qty": 1, - "rate": 47.41, + "rate": 7.77, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", }, { - "item_code": "_Test Item 2", - "qty": 2, - "rate": 33.33, + "item_code": "_Test Item", + "qty": 1, + "rate": 7.77, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + { + "item_code": "_Test Item", + "qty": 1, + "rate": 7.77, "income_account": "Sales - _TC", "expense_account": "Cost of Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", @@ -179,11 +203,11 @@ class TestTaxesAndTotals(ERPNextTestSuite): details_by_tax = {} for detail in doc.item_wise_tax_details: - bucket = details_by_tax.setdefault(detail.tax_row, {"amount": 0.0}) - bucket["amount"] += detail.amount + bucket = details_by_tax.setdefault(detail.tax_row, 0.0) + details_by_tax[detail.tax_row] = bucket + detail.amount for tax in doc.taxes: - self.assertEqual(details_by_tax[tax.name]["amount"], tax.base_tax_amount_after_discount_amount) + self.assertEqual(details_by_tax[tax.name], tax.base_tax_amount_after_discount_amount) def test_item_wise_tax_detail_with_multi_currency_with_single_item(self): """ From 0dcacad79321878c7d660804d2249b85f3f16f7a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 31 Mar 2026 11:05:46 +0530 Subject: [PATCH 5/5] chore: resolve conflicts --- erpnext/controllers/tests/test_item_wise_tax_details.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/controllers/tests/test_item_wise_tax_details.py b/erpnext/controllers/tests/test_item_wise_tax_details.py index c7a61fcb47d..30f1e51d7f4 100644 --- a/erpnext/controllers/tests/test_item_wise_tax_details.py +++ b/erpnext/controllers/tests/test_item_wise_tax_details.py @@ -2,12 +2,7 @@ import json import frappe -<<<<<<< HEAD -from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals -from erpnext.tests.utils import ERPNextTestSuite -======= from erpnext.tests.utils import ERPNextTestSuite, change_settings ->>>>>>> fc8437c499 (test: update item-wise tax detail test for high conversion rates) class TestTaxesAndTotals(ERPNextTestSuite):