fix: get total without rounding off tax amounts for distributing discount (backport #47155)

Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2025-04-22 17:53:10 +05:30
committed by GitHub
parent 2ba6c78e5e
commit 8050e653ab
3 changed files with 102 additions and 68 deletions

View File

@@ -2688,13 +2688,13 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
To test if after applying discount on grand total, To test if after applying discount on grand total,
the grand total is calculated correctly without any rounding errors the grand total is calculated correctly without any rounding errors
""" """
invoice = make_purchase_invoice(qty=2, rate=100, do_not_save=True, do_not_submit=True) invoice = make_purchase_invoice(qty=3, rate=100, do_not_save=True, do_not_submit=True)
invoice.append( invoice.append(
"items", "items",
{ {
"item_code": "_Test Item", "item_code": "_Test Item",
"qty": 1, "qty": 3,
"rate": 21.39, "rate": 50.3,
}, },
) )
invoice.append( invoice.append(
@@ -2703,18 +2703,19 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
"charge_type": "On Net Total", "charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC", "account_head": "_Test Account VAT - _TC",
"description": "VAT", "description": "VAT",
"rate": 15.5, "rate": 15,
}, },
) )
# the grand total here will be 255.71 # the grand total here will be 518.54
invoice.disable_rounded_total = 1 invoice.disable_rounded_total = 1
# apply discount on grand total to adjust the grand total to 255 # apply discount on grand total to adjust the grand total to 518
invoice.discount_amount = 0.71 invoice.discount_amount = 0.54
invoice.save() invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors # check if grand total is 518 and not something like 517.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255) self.assertEqual(invoice.grand_total, 518)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self): def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
""" """

View File

@@ -377,20 +377,22 @@ class calculate_taxes_and_totals:
self._calculate() self._calculate()
def calculate_taxes(self): def calculate_taxes(self):
self.grand_total_diff = 0 doc = self.doc
if not doc.get("taxes"):
return
# maintain actual tax rate based on idx # maintain actual tax rate based on idx
actual_tax_dict = dict( actual_tax_dict = dict(
[ [
[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
for tax in self.doc.get("taxes") for tax in doc.taxes
if tax.charge_type == "Actual" if tax.charge_type == "Actual"
] ]
) )
for n, item in enumerate(self._items): for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate) item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
for i, tax in enumerate(self.doc.get("taxes")): for i, tax in enumerate(doc.taxes):
# tax_amount represents the amount of tax for the current step # tax_amount represents the amount of tax for the current step
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map) current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
if frappe.flags.round_row_wise_tax: if frappe.flags.round_row_wise_tax:
@@ -425,30 +427,39 @@ class calculate_taxes_and_totals:
tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount) tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
else: else:
tax.grand_total_for_current_item = flt( tax.grand_total_for_current_item = flt(
self.doc.get("taxes")[i - 1].grand_total_for_current_item + current_tax_amount doc.taxes[i - 1].grand_total_for_current_item + current_tax_amount
) )
# set precision in the last item iteration discount_amount_applied = self.discount_amount_applied
if n == len(self._items) - 1: if doc.apply_discount_on == "Grand Total" and (
self.round_off_totals(tax) discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) ):
tax_amount_precision = doc.taxes[0].precision("tax_amount")
self.round_off_base_values(tax) for i, tax in enumerate(doc.taxes):
self.set_cumulative_total(i, tax) if discount_amount_applied:
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount, tax_amount_precision
)
self._set_in_company_currency(tax, ["total"]) self.set_cumulative_total(i, tax)
# adjust Discount Amount loss in last tax iteration if not discount_amount_applied:
if ( self.grand_total_for_distributing_discount = doc.taxes[-1].total
i == (len(self.doc.get("taxes")) - 1) else:
and self.discount_amount_applied self.grand_total_diff = flt(
and self.doc.discount_amount self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
and self.doc.apply_discount_on == "Grand Total" doc.precision("grand_total"),
): )
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total, for i, tax in enumerate(doc.taxes):
self.doc.precision("rounding_adjustment"), self.round_off_totals(tax)
) self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"])
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, ["total"])
def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax): def get_tax_amount_if_for_valuation_or_deduction(self, tax_amount, tax):
# if just for valuation, do not add the tax amount in total # if just for valuation, do not add the tax amount in total
@@ -571,16 +582,20 @@ class calculate_taxes_and_totals:
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")): if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.grand_total_diff = diff self.grand_total_diff = diff
else:
self.grand_total_diff = 0
def calculate_totals(self): def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + self.grand_total_diff self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
else: else:
self.doc.grand_total = flt(self.doc.net_total) self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"): if self.doc.get("taxes"):
self.doc.total_taxes_and_charges = flt( self.doc.total_taxes_and_charges = flt(
self.doc.grand_total - self.doc.net_total - self.grand_total_diff, self.doc.grand_total - self.doc.net_total - grand_total_diff,
self.doc.precision("total_taxes_and_charges"), self.doc.precision("total_taxes_and_charges"),
) )
else: else:
@@ -725,7 +740,8 @@ class calculate_taxes_and_totals:
self.doc.base_discount_amount = 0 self.doc.base_discount_amount = 0
def get_total_for_discount_amount(self): def get_total_for_discount_amount(self):
if self.doc.apply_discount_on == "Net Total": doc = self.doc
if doc.apply_discount_on == "Net Total" or not doc.get("taxes"):
return self.doc.net_total return self.doc.net_total
total_actual_tax = 0 total_actual_tax = 0
@@ -745,7 +761,7 @@ class calculate_taxes_and_totals:
"cumulative_tax_amount": total_actual_tax, "cumulative_tax_amount": total_actual_tax,
} }
for tax in self.doc.get("taxes"): for tax in doc.taxes:
if tax.charge_type in ["Actual", "On Item Quantity"]: if tax.charge_type in ["Actual", "On Item Quantity"]:
update_actual_tax_dict(tax, tax.tax_amount) update_actual_tax_dict(tax, tax.tax_amount)
continue continue
@@ -764,7 +780,7 @@ class calculate_taxes_and_totals:
) )
update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100) update_actual_tax_dict(tax, base_tax_amount * tax.rate / 100)
return self.doc.grand_total - total_actual_tax return getattr(self, "grand_total_for_distributing_discount", doc.grand_total) - total_actual_tax
def calculate_total_advance(self): def calculate_total_advance(self):
if not self.doc.docstatus.is_cancelled(): if not self.doc.docstatus.is_cancelled():

View File

@@ -342,12 +342,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
calculate_taxes() { calculate_taxes() {
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
var me = this; var me = this;
this.grand_total_diff = 0;
var actual_tax_dict = {}; var actual_tax_dict = {};
// maintain actual tax rate based on idx // maintain actual tax rate based on idx
$.each(this.frm.doc["taxes"] || [], function(i, tax) { $.each(doc.taxes, function(i, tax) {
if (tax.charge_type == "Actual") { if (tax.charge_type == "Actual") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax)); actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
} }
@@ -355,7 +357,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
$.each(this.frm._items || [], function(n, item) { $.each(this.frm._items || [], function(n, item) {
var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var item_tax_map = me._load_item_tax_rate(item.item_tax_rate);
$.each(me.frm.doc["taxes"] || [], function(i, tax) { $.each(doc.taxes, function(i, tax) {
// tax_amount represents the amount of tax for the current step // tax_amount represents the amount of tax for the current step
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map); var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
if (frappe.flags.round_row_wise_tax) { if (frappe.flags.round_row_wise_tax) {
@@ -400,29 +402,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
tax.grand_total_for_current_item = tax.grand_total_for_current_item =
flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount); flt(me.frm.doc["taxes"][i-1].grand_total_for_current_item + current_tax_amount);
} }
// set precision in the last item iteration
if (n == me.frm._items.length - 1) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each item
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
// adjust Discount Amount loss in last tax iteration
if ((i == me.frm.doc["taxes"].length - 1) && me.discount_amount_applied
&& me.frm.doc.apply_discount_on == "Grand Total" && me.frm.doc.discount_amount) {
me.grand_total_diff = flt(me.frm.doc.grand_total -
flt(me.frm.doc.discount_amount) - tax.total, precision("rounding_adjustment"));
}
}
}); });
}); });
const discount_amount_applied = this.discount_amount_applied;
if (doc.apply_discount_on === "Grand Total" && (discount_amount_applied || doc.discount_amount || doc.additional_discount_percentage)) {
const tax_amount_precision = precision("tax_amount", doc.taxes[0]);
for (const [i, tax] of doc.taxes.entries()) {
if (discount_amount_applied)
tax.tax_amount_after_discount_amount = flt(tax.tax_amount_after_discount_amount, tax_amount_precision);
this.set_cumulative_total(i, tax);
}
if (!this.discount_amount_applied) {
this.grand_total_for_distributing_discount = doc.taxes[doc.taxes.length - 1].total;
} else {
this.grand_total_diff = flt(
this.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[doc.taxes.length - 1].total, precision("grand_total"));
}
}
for (const [i, tax] of doc.taxes.entries()) {
me.round_off_totals(tax);
me.set_in_company_currency(tax,
["tax_amount", "tax_amount_after_discount_amount"]);
me.round_off_base_values(tax);
// in tax.total, accumulate grand total for each tax
me.set_cumulative_total(i, tax);
me.set_in_company_currency(tax, ["total"]);
}
} }
set_cumulative_total(row_idx, tax) { set_cumulative_total(row_idx, tax) {
@@ -571,10 +584,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_totals() { calculate_totals() {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency // Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
var me = this; const me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0; const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
this.frm.doc.grand_total = flt(tax_count this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + this.grand_total_diff ? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff
: this.frm.doc.net_total); : this.frm.doc.net_total);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) { if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -606,7 +621,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total this.frm.doc.total_taxes_and_charges = flt(this.frm.doc.grand_total - this.frm.doc.net_total
- this.grand_total_diff, precision("total_taxes_and_charges")); - grand_total_diff, precision("total_taxes_and_charges"));
this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]); this.set_in_company_currency(this.frm.doc, ["total_taxes_and_charges"]);
@@ -729,8 +744,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
} }
get_total_for_discount_amount() { get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total") const doc = this.frm.doc;
return this.frm.doc.net_total;
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
return doc.net_total;
let total_actual_tax = 0.0; let total_actual_tax = 0.0;
let actual_taxes_dict = {}; let actual_taxes_dict = {};
@@ -745,7 +762,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}; };
} }
$.each(this.frm.doc["taxes"] || [], function(i, tax) { doc.taxes.forEach(tax => {
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) { if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
update_actual_taxes_dict(tax, tax.tax_amount); update_actual_taxes_dict(tax, tax.tax_amount);
return; return;
@@ -760,7 +777,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100); update_actual_taxes_dict(tax, base_tax_amount * tax.rate / 100);
}); });
return this.frm.doc.grand_total - total_actual_tax; return (this.grand_total_for_distributing_discount || doc.grand_total) - total_actual_tax;
} }
calculate_total_advance(update_paid_amount) { calculate_total_advance(update_paid_amount) {