From 768c3a49278e35abc31a04a0b87d2dcd2e8794d8 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 13:21:38 +0530 Subject: [PATCH] fix: Taxes aren't discounted on early payment discount - Deductions in payment entry must be split into income loss and tax loss - Compute total discount in percentage, makes discounting different amounts proportionately easier --- .../doctype/payment_entry/payment_entry.py | 92 ++++++++++++++++++- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index cd5b6d5ce2b8..91d31ab0ec1a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1669,7 +1669,7 @@ def get_payment_entry( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) - paid_amount, received_amount, discount_amount = apply_early_payment_discount( + paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( paid_amount, received_amount, doc ) @@ -1769,7 +1769,9 @@ def get_payment_entry( if party_account and bank: pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() - if discount_amount: + + discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) + if discount_amount > 0: pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), @@ -1891,6 +1893,7 @@ def set_paid_amount_and_received_amount( def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount = 0 + valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule @@ -1911,13 +1914,96 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): received_amount -= discount_amount paid_amount -= discount_amount_in_foreign_currency + valid_discounts.append({"type": term.discount_type, "discount": term.discount}) total_discount += discount_amount if total_discount: money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency")) frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1) - return paid_amount, received_amount, total_discount + return paid_amount, received_amount, total_discount, valid_discounts + + +def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): + """Split early bird discount deductions into Income Loss & Tax Loss.""" + if not (discount_amount and valid_discounts): + return discount_amount + + total_discount_percent = get_total_discount_percent(doc, valid_discounts) + + if not total_discount_percent: + return discount_amount + + loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) + loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) + + return flt(discount_amount - (loss_on_income + loss_on_taxes)) + + +def get_total_discount_percent(doc, valid_discounts) -> float: + """Get total percentage and amount discount applied as a percentage.""" + total_discount_percent = ( + sum( + discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage" + ) + or 0.0 + ) + + # Operate in percentages only as it makes the income & tax split easier + total_discount_amount = ( + sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount") + or 0.0 + ) + + if total_discount_amount: + discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100 + total_discount_percent += discount_percentage + return total_discount_percent + + return total_discount_percent + + +def add_income_discount_loss(pe, doc, total_discount_percent) -> float: + loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), doc.precision("total")) + pe.append( + "deductions", + { + "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": loss_on_income, + }, + ) + return loss_on_income + + +def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: + tax_discount_loss = {} + total_tax_loss = 0 + precision = doc.precision("tax_amount_after_discount_amount", "taxes") + + # The same account head could be used more than once + for tax in doc.get("taxes", []): + tax_loss = flt( + tax.get("tax_amount_after_discount_amount") * (total_discount_percenatage / 100), precision + ) + account = tax.get("account_head") + if not tax_discount_loss.get(account): + tax_discount_loss[account] = tax_loss + else: + tax_discount_loss[account] += tax_loss + + for account, loss in tax_discount_loss.items(): + total_tax_loss += loss + pe.append( + "deductions", + { + "account": account, + "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), + "amount": loss, + }, + ) + + return total_tax_loss def get_reference_as_per_payment_terms(