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 9 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
123 changes: 114 additions & 9 deletions erpnext/accounts/doctype/payment_entry/payment_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,15 +424,28 @@ def update_payment_schedule(self, cancel=0):
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_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):
Expand Down Expand Up @@ -1669,7 +1682,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
)

Expand Down Expand Up @@ -1769,16 +1782,21 @@ 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:
# Set pending base discount amount in deductions
positive_negative = -1 if payment_type == "Pay" else 1
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),
"amount": discount_amount * positive_negative * doc.get("conversion_rate", 1),
}
)
pe.set_difference_amount()

pe.set_difference_amount()

return pe

Expand Down Expand Up @@ -1891,6 +1909,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

Expand All @@ -1911,13 +1930,99 @@ 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:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
loss_on_income = flt(doc.get("total") * (total_discount_percent / 100), 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": flt(loss_on_income * doc.get("conversion_rate", 1), precision),
},
)
return loss_on_income


def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float:
"""Add loss on tax discount in base currency."""
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": flt(loss * doc.get("conversion_rate", 1), precision),
},
)

return total_tax_loss


def get_reference_as_per_payment_terms(
Expand Down
67 changes: 59 additions & 8 deletions erpnext/accounts/doctype/payment_entry/test_payment_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ def test_payment_entry_against_payment_terms_with_discount(self):
si.submit()

pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")

self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 212.4)
self.assertEqual(pe.deductions[0].amount, 20.0) # Loss on Income
self.assertEqual(pe.deductions[1].amount, 3.6) # Loss on Tax
self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC")

pe.submit()
si.load_from_db()

Expand All @@ -269,6 +277,46 @@ def test_payment_entry_against_payment_terms_with_discount(self):
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)

def test_payment_entry_against_payment_terms_with_discount_amount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)

si.payment_terms_template = "Test Discount Amount Template"
create_payment_terms_template_with_discount(
name="30 Credit Days with Rs.50 Discount",
discount_type="Amount",
discount=50,
template_name="Test Discount Amount Template",
)
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")

si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
si.save()
si.submit()

pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 186)
self.assertEqual(pe.deductions[0].amount, 42.37) # Loss on Income
self.assertEqual(pe.deductions[1].amount, 7.63) # Loss on Tax
self.assertEqual(pe.deductions[1].account, "_Test Account Service Tax - _TC")

pe.submit()
si.load_from_db()

self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)

def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
Expand Down Expand Up @@ -839,24 +887,27 @@ def create_payment_terms_template():
).insert()


def create_payment_terms_template_with_discount():

create_payment_term("30 Credit Days with 10% Discount")
def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"

if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
payment_term_template = frappe.get_doc(
if not frappe.db.exists("Payment Terms Template", template_name):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "Test Discount Template",
"template_name": template_name,
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "30 Credit Days with 10% Discount",
"payment_term": name or "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
"discount": 10,
"discount_type": discount_type or "Percentage",
"discount": discount or 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"cost_center",
Expand All @@ -17,9 +18,7 @@
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "cost_center",
Expand All @@ -28,43 +27,37 @@
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"show_days": 1,
"show_seconds": 1
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-12 20:38:08.110674",
"modified": "2023-03-06 07:11:57.739619",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}