Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Allocate tax loss to tax account head on early payment discount #34287

Merged
merged 20 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
768c3a4
fix: Taxes aren't discounted on early payment discount
marination Mar 3, 2023
6198248
Merge branch 'develop' into early-payment-loss
marination Mar 3, 2023
75ec0a0
fix: Recalculate difference amount after setting deductions
marination Mar 3, 2023
dc2998f
fix: Set deductions in base currency
marination Mar 3, 2023
2ae5834
fix: Back update discounted amount in Invoice based on discount type
marination Mar 6, 2023
c217bb2
test: PE from SI with early payment discount amount & PE assertions i…
marination Mar 6, 2023
b5f8081
Merge branch 'develop' into early-payment-loss
marination Mar 6, 2023
7f2e7ba
fix: Set deduction amount in company currency on Doctype
marination Mar 6, 2023
ee12313
Merge branch 'develop' into early-payment-loss
barredterra Mar 6, 2023
f02fc8a
fix: Don't add to deductions if amount is 0
marination Mar 7, 2023
761f68d
fix: Paid amount must be discounted considering accounting currency
marination Mar 8, 2023
b09c238
fix: Multi-currency SI with base currency PE
marination Mar 9, 2023
9abf0ef
test: Multi currency SI with multi-currency accounting and single cur…
marination Mar 9, 2023
ae0a8a6
Merge branch 'develop' into early-payment-loss
marination Mar 9, 2023
caa1a3d
fix: Handle rounding more gracefully
marination Mar 13, 2023
e1d2806
Merge branch 'develop' into early-payment-loss
marination Mar 13, 2023
d6d0163
fix: Provision to apply early payment discount if payment is recorded…
marination Mar 14, 2023
c5da7f5
Merge branch 'develop' into early-payment-loss
marination Mar 21, 2023
216a46b
feat: Make Tax loss booking optional
marination Mar 27, 2023
dae40df
Merge branch 'develop' into early-payment-loss
marination Mar 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions erpnext/accounts/doctype/payment_entry/payment_entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,6 @@ frappe.ui.form.on('Payment Entry', {
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references");

frm.set_currency_labels(["amount"], company_currency, "deductions");

cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));

Expand Down
214 changes: 191 additions & 23 deletions erpnext/accounts/doctype/payment_entry/payment_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,28 +416,45 @@ def update_payment_schedule(self, cancel=0):

for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name)
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount

if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all(
"Payment Schedule",
filters={"parent": ref.reference_name},
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
fields=[
"paid_amount",
"payment_amount",
"payment_term",
"discount",
"outstanding",
"discount_type",
],
)
for term in payment_schedule:
invoice_key = (term.payment_term, ref.reference_name)
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
term.discount / 100
)
if not (term.discount_type and term.discount):
continue

if term.discount_type == "Percentage":
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
term.discount / 100
)
else:
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount

for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
if not invoice_paid_amount_map.get(key):
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))

allocated_amount = self.get_allocated_amount_in_transaction_currency(
allocated_amount, key[2], key[1]
)

outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))

Expand Down Expand Up @@ -472,6 +489,33 @@ def update_payment_schedule(self, cancel=0):
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
)

def get_allocated_amount_in_transaction_currency(
self, allocated_amount, reference_doctype, reference_docname
):
"""
Payment Entry could be in base currency while reference's payment schedule
is always in transaction currency.
E.g.
* SI with base=INR and currency=USD
* SI with payment schedule in USD
* PE in INR (accounting done in base currency)
"""
ref_currency, ref_exchange_rate = frappe.db.get_value(
reference_doctype, reference_docname, ["currency", "conversion_rate"]
)
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
# PE in different currency
reference_is_multi_currency = self.paid_from_account_currency != ref_currency

if not (is_single_currency and reference_is_multi_currency):
return allocated_amount

allocated_amount = flt(
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
)

return allocated_amount

def set_status(self):
if self.docstatus == 2:
self.status = "Cancelled"
Expand Down Expand Up @@ -1669,8 +1713,8 @@ 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, doc
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
paid_amount, received_amount, doc, party_account_currency
)

pe = frappe.new_doc("Payment Entry")
Expand Down Expand Up @@ -1718,7 +1762,7 @@ def get_payment_entry(
):

for reference in get_reference_as_per_payment_terms(
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
pe.append("references", reference)
else:
Expand Down Expand Up @@ -1769,16 +1813,14 @@ def get_payment_entry(
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()

if discount_amount:
pe.set_gain_or_loss(
account_details={
"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": discount_amount * (-1 if payment_type == "Pay" else 1),
}
base_total_discount_loss = set_early_payment_discount_loss(pe, doc, valid_discounts)
set_pending_discount_loss(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
pe.set_difference_amount()

pe.set_difference_amount()

return pe

Expand Down Expand Up @@ -1889,20 +1931,26 @@ def set_paid_amount_and_received_amount(
return paid_amount, received_amount


def apply_early_payment_discount(paid_amount, received_amount, doc):
def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency):
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
is_multi_currency = party_account_currency != doc.company_currency

if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:

if term.discount_type == "Percentage":
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
discount_amount = flt(grand_total) * (term.discount / 100)
else:
discount_amount = term.discount

discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
# if accounting is done in the same currency, paid_amount = received_amount
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
discount_amount_in_foreign_currency = discount_amount * conversion_rate

if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
Expand All @@ -1911,23 +1959,143 @@ 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"))
currency = doc.get("currency") if is_multi_currency else doc.company_currency
money = frappe.utils.fmt_money(total_discount, currency=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_pending_discount_loss(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
):
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
if party_account_currency != doc.company_currency:
discount_amount = discount_amount * doc.get("conversion_rate", 1)

discount_amount -= base_total_discount_loss

# If pending base discount amount, set it in deductions
if discount_amount > 0.0:
positive_negative = -1 if pe.payment_type == "Pay" else 1
pe.set_gain_or_loss(
account_details={
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": discount_amount * positive_negative,
}
)


def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
"""Split early payment discount into Income Loss & Tax Loss."""
total_discount_percent = get_total_discount_percent(doc, valid_discounts)

if not total_discount_percent:
return 0.0

base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)

return flt(base_loss_on_income + base_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:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), precision)
base_loss_on_income = flt(loss_on_income * doc.get("conversion_rate", 1), precision)

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": base_loss_on_income,
},
)
return base_loss_on_income


def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"""Add loss on tax discount in base currency."""
tax_discount_loss = {}
base_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_percentage / 100), precision
)
base_tax_loss = flt(tax_loss * doc.get("conversion_rate", 1), precision)

account = tax.get("account_head")
if not tax_discount_loss.get(account):
tax_discount_loss[account] = base_tax_loss
else:
tax_discount_loss[account] += base_tax_loss

for account, loss in tax_discount_loss.items():
base_total_tax_loss += loss
if loss == 0.0:
continue

pe.append(
"deductions",
{
"account": account,
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": loss,
},
)

return base_total_tax_loss


def get_reference_as_per_payment_terms(
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
references = []
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
party_account_currency != doc.company_currency
)

for payment_term in payment_schedule:
payment_term_outstanding = flt(
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
)
if not is_multi_currency_acc:
# If accounting is done in company currency for multi-currency transaction
payment_term_outstanding = flt(
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
)

if payment_term_outstanding:
references.append(
Expand Down
Loading