Merge branch 'develop' into early-payment-loss
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 91374ae..5a56a6b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -245,8 +245,6 @@
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));
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index cd5b6d5..6e612ee 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -424,15 +424,28 @@
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):
@@ -1669,7 +1682,7 @@
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,16 +1782,21 @@
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
@@ -1891,6 +1909,7 @@
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 +1930,99 @@
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(
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 123b5df..fe80bad 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -260,6 +260,14 @@
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()
@@ -269,6 +277,46 @@
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",
@@ -839,24 +887,27 @@
).insert()
-def create_payment_terms_template_with_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"
- create_payment_term("30 Credit Days with 10% Discount")
-
- 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,
}
diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
index 61a1462..1c31829 100644
--- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
+++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
@@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"account",
"cost_center",
@@ -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",
@@ -28,37 +27,30 @@
"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",
@@ -66,5 +58,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file