Merge pull request #53925 from ljain112/fix-item-tax-rounding

This commit is contained in:
Smit Vora
2026-03-30 20:46:21 +05:30
committed by GitHub
2 changed files with 144 additions and 6 deletions

View File

@@ -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,7 +528,6 @@ 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) <= 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

View File

@@ -1,6 +1,6 @@
import frappe
from erpnext.tests.utils import ERPNextTestSuite
from erpnext.tests.utils import ERPNextTestSuite, change_settings
class TestTaxesAndTotals(ERPNextTestSuite):
@@ -121,3 +121,124 @@ class TestTaxesAndTotals(ERPNextTestSuite):
]
self.assertEqual(actual_values, expected_values)
@change_settings("Selling Settings", {"allow_multiple_items": 1})
def test_item_wise_tax_detail_high_conversion_rate(self):
"""
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(
{
"doctype": "Sales Invoice",
"customer": "_Test Customer",
"company": "_Test Company",
"currency": "USD",
"debit_to": "_Test Receivable USD - _TC",
"conversion_rate": 1300,
"items": [
{
"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",
},
{
"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",
},
],
"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, 0.0)
details_by_tax[detail.tax_row] = bucket + detail.amount
for tax in doc.taxes:
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):
"""
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)