Merge pull request #47155 from sagarvora/fix-dr-cr-mismatch

fix: get total without rounding off tax amounts for distributing discount
This commit is contained in:
ruthra kumar
2025-04-22 17:19:00 +05:30
committed by GitHub
3 changed files with 103 additions and 77 deletions

View File

@@ -2705,13 +2705,13 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
To test if after applying discount on grand total,
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(
"items",
{
"item_code": "_Test Item",
"qty": 1,
"rate": 21.39,
"qty": 3,
"rate": 50.3,
},
)
invoice.append(
@@ -2720,18 +2720,19 @@ class TestPurchaseInvoice(IntegrationTestCase, StockTestMixin):
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"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
# apply discount on grand total to adjust the grand total to 255
invoice.discount_amount = 0.71
# apply discount on grand total to adjust the grand total to 518
invoice.discount_amount = 0.54
invoice.save()
# check if grand total is 496 and not something like 254.99 due to rounding errors
self.assertEqual(invoice.grand_total, 255)
# check if grand total is 518 and not something like 517.99 due to rounding errors
self.assertEqual(invoice.grand_total, 518)
def test_apply_discount_on_grand_total_with_previous_row_total_tax(self):
"""

View File

@@ -21,8 +21,6 @@ from erpnext.deprecation_dumpster import deprecated
from erpnext.stock.get_item_details import ItemDetailsCtx, _get_item_tax_template, get_item_tax_map
from erpnext.utilities.regional import temporary_flag
logger = frappe.logger(__name__)
ItemWiseTaxDetail = frappe._dict
@@ -384,22 +382,22 @@ class calculate_taxes_and_totals:
self._calculate()
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
actual_tax_dict = dict(
[
[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"
]
)
logger.debug(f"{self.doc} ...")
for n, item in enumerate(self._items):
item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
logger.debug(f" Item {n}: {item.item_code}" + (f" - {item_tax_map}" if item_tax_map else ""))
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
current_net_amount, current_tax_amount = self.get_current_tax_and_net_amount(
item, tax, item_tax_map
@@ -438,37 +436,42 @@ class calculate_taxes_and_totals:
tax.grand_total_for_current_item = flt(item.net_amount + current_tax_amount)
else:
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
if n == len(self._items) - 1:
self.round_off_totals(tax)
self._set_in_company_currency(
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_amount"]
discount_amount_applied = self.discount_amount_applied
if doc.apply_discount_on == "Grand Total" and (
discount_amount_applied or doc.discount_amount or doc.additional_discount_percentage
):
tax_amount_precision = doc.taxes[0].precision("tax_amount")
for i, tax in enumerate(doc.taxes):
if discount_amount_applied:
tax.tax_amount_after_discount_amount = flt(
tax.tax_amount_after_discount_amount, tax_amount_precision
)
self.round_off_base_values(tax)
self.set_cumulative_total(i, tax)
self.set_cumulative_total(i, tax)
self._set_in_company_currency(tax, ["total"])
# adjust Discount Amount loss in last tax iteration
if (
i == (len(self.doc.get("taxes")) - 1)
and self.discount_amount_applied
and self.doc.discount_amount
and self.doc.apply_discount_on == "Grand Total"
):
self.grand_total_diff = flt(
self.doc.grand_total - flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"),
)
logger.debug(
f" net_amount: {current_net_amount:<20} tax_amount: {current_tax_amount:<20} - {tax.description}"
if not discount_amount_applied:
self.grand_total_for_distributing_discount = doc.taxes[-1].total
else:
self.grand_total_diff = flt(
self.grand_total_for_distributing_discount - doc.discount_amount - doc.taxes[-1].total,
doc.precision("grand_total"),
)
for i, tax in enumerate(doc.taxes):
self.round_off_totals(tax)
self._set_in_company_currency(
tax, ["tax_amount", "tax_amount_after_discount_amount", "net_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):
# if just for valuation, do not add the tax amount in total
# if tax/charges is for deduction, multiply by -1
@@ -612,16 +615,20 @@ class calculate_taxes_and_totals:
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
self.grand_total_diff = diff
else:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
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:
self.doc.grand_total = flt(self.doc.net_total)
if self.doc.get("taxes"):
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"),
)
else:
@@ -766,7 +773,8 @@ class calculate_taxes_and_totals:
self.doc.base_discount_amount = 0
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
total_actual_tax = 0
@@ -786,7 +794,7 @@ class calculate_taxes_and_totals:
"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"]:
update_actual_tax_dict(tax, tax.tax_amount)
continue
@@ -805,7 +813,7 @@ class calculate_taxes_and_totals:
)
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):
if not self.doc.docstatus.is_cancelled():

View File

@@ -343,12 +343,14 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
var me = this;
this.grand_total_diff = 0;
var actual_tax_dict = {};
// 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") {
actual_tax_dict[tax.idx] = flt(tax.tax_amount, precision("tax_amount", tax));
}
@@ -356,7 +358,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
$.each(this.frm._items || [], function(n, item) {
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
var current_tax_amount = me.get_current_tax_amount(item, tax, item_tax_map);
if (frappe.flags.round_row_wise_tax) {
@@ -401,29 +403,40 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
tax.grand_total_for_current_item =
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) {
@@ -586,10 +599,12 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
calculate_totals() {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
var me = this;
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
const me = this;
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["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);
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
@@ -621,7 +636,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.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"]);
@@ -744,8 +759,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
get_total_for_discount_amount() {
if(this.frm.doc.apply_discount_on == "Net Total")
return this.frm.doc.net_total;
const doc = this.frm.doc;
if (doc.apply_discount_on == "Net Total" || !doc.taxes?.length)
return doc.net_total;
let total_actual_tax = 0.0;
let actual_taxes_dict = {};
@@ -760,7 +777,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)) {
update_actual_taxes_dict(tax, tax.tax_amount);
return;
@@ -775,7 +792,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
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) {