mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-05 22:48:27 +00:00
Merge pull request #53925 from ljain112/fix-item-tax-rounding
This commit is contained in:
@@ -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 = item_wise_tax_details
|
||||||
self.doc.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):
|
def determine_exclusive_rate(self):
|
||||||
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")):
|
||||||
return
|
return
|
||||||
@@ -521,7 +528,6 @@ class calculate_taxes_and_totals:
|
|||||||
actual_breakup = tax._total_tax_breakup
|
actual_breakup = tax._total_tax_breakup
|
||||||
diff = flt(expected_amount - actual_breakup, 5)
|
diff = flt(expected_amount - actual_breakup, 5)
|
||||||
|
|
||||||
# TODO: fix rounding difference issues
|
|
||||||
if abs(diff) <= 0.5:
|
if abs(diff) <= 0.5:
|
||||||
detail_row = self.doc._item_wise_tax_details[last_idx]
|
detail_row = self.doc._item_wise_tax_details[last_idx]
|
||||||
detail_row["amount"] = flt(detail_row["amount"] + diff, 5)
|
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):
|
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount, current_net_amount):
|
||||||
# store tax breakup for each item
|
# store tax breakup for each item
|
||||||
multiplier = -1 if tax.get("add_deduct_tax") == "Deduct" else 1
|
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":
|
if tax.charge_type != "On Item Quantity":
|
||||||
item_wise_taxable_amount = flt(
|
tax._running_txn_taxable_total += current_net_amount * multiplier
|
||||||
current_net_amount * self.doc.conversion_rate * multiplier, tax.precision("tax_amount")
|
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:
|
else:
|
||||||
item_wise_taxable_amount = 0.0
|
item_wise_taxable_amount = 0.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.tests.utils import ERPNextTestSuite
|
from erpnext.tests.utils import ERPNextTestSuite, change_settings
|
||||||
|
|
||||||
|
|
||||||
class TestTaxesAndTotals(ERPNextTestSuite):
|
class TestTaxesAndTotals(ERPNextTestSuite):
|
||||||
@@ -121,3 +121,124 @@ class TestTaxesAndTotals(ERPNextTestSuite):
|
|||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(actual_values, expected_values)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user