diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2dc741ce54d..68fd44cfdd6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -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): """ diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index ff186316501..239e47b15ac 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -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(): diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a65fee43897..7e9ad067cab 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -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) {