mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 04:45:09 +00:00
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:
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user