From e36fc1fb82d3a3ebb1e6c6743c13b7dc74421f9f Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 13:21:38 +0530 Subject: [PATCH 01/15] 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 (cherry picked from commit 768c3a49278e35abc31a04a0b87d2dcd2e8794d8) --- .../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 f0d7d57fc648..a054276f424c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1822,7 +1822,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= 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 ) @@ -1922,7 +1922,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= reference_doc = doc 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"), @@ -2069,6 +2071,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 @@ -2089,13 +2092,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( From 2c5f7c2ce6b13e0cf66ab11e173d6dd5c6b8a0a3 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 14:13:27 +0530 Subject: [PATCH 02/15] fix: Recalculate difference amount after setting deductions (cherry picked from commit 75ec0a0a85a010415765518f5a9e36bb13d08b22) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a054276f424c..a08a9484baf3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1925,6 +1925,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) if discount_amount > 0: + # Set pending discount amount in deductions pe.set_gain_or_loss( account_details={ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"), @@ -1933,7 +1934,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= "amount": discount_amount * (-1 if payment_type == "Pay" else 1), } ) - pe.set_difference_amount() + + pe.set_difference_amount() return pe From a2422ba5c950b861f039c74fb1a75e2b92d07348 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 3 Mar 2023 17:24:43 +0530 Subject: [PATCH 03/15] fix: Set deductions in base currency - Use field precision to get more accurate values (cherry picked from commit dc2998f5442613e3c3624493896686fc75f3c388) --- .../doctype/payment_entry/payment_entry.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a08a9484baf3..48678dccf25e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1925,13 +1925,14 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= discount_amount = set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount) if discount_amount > 0: - # Set pending discount amount in deductions + # 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), } ) @@ -2144,19 +2145,22 @@ def get_total_discount_percent(doc, valid_discounts) -> float: 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")) + """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": loss_on_income, + "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") @@ -2179,7 +2183,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": loss, + "amount": flt(loss * doc.get("conversion_rate", 1), precision), }, ) From e7bb2edcb2c6618c3b599f91733fb58fcf0a44fd Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 14:54:46 +0530 Subject: [PATCH 04/15] fix: Back update discounted amount in Invoice based on discount type - Discount value was always trated as a percentage on back updation (cherry picked from commit 2ae58342907c0cfb9ae7658176e1549fb51d1cb3) --- .../doctype/payment_entry/payment_entry.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 48678dccf25e..4a97f4695f25 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -448,15 +448,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(iteritems(invoice_payment_amount_map), 1): if not invoice_paid_amount_map.get(key): From beae543d1b7d4641551d0fd46a4bf622c72a581f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 15:02:32 +0530 Subject: [PATCH 05/15] test: PE from SI with early payment discount amount & PE assertions in discount % test (cherry picked from commit c217bb201878327fb6dfa341fbf65c19761916a5) --- .../payment_entry/test_payment_entry.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 004c84c0221f..783862e4a118 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -256,6 +256,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() @@ -265,6 +273,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", @@ -856,24 +904,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, } From bf0476e48f642490d59a1b50750f7a4e34e2505d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Mar 2023 17:43:40 +0530 Subject: [PATCH 06/15] fix: Set deduction amount in company currency on Doctype - Even via JS, deductions amount is always in company currency - Since there is nothing dynamic about this field, set it in the doctype spec itself - fixed: Inconsistency between label currency and field currency formatted value (cherry picked from commit 7f2e7badffab44355a4525369eb1a044b2f9e5c1) --- .../doctype/payment_entry/payment_entry.js | 2 -- .../payment_entry_deduction.json | 29 +++++++------------ 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 6be0920d2a8e..2e5674874ccc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -256,8 +256,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)); 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 61a1462dd7a4..1c31829f0ea6 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 From 6e2fc56afd2d09736e5eea49002e69e3ced6c1e5 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 7 Mar 2023 12:12:45 +0530 Subject: [PATCH 07/15] fix: Don't add to deductions if amount is 0 - misc: better docstring (cherry picked from commit f02fc8acf0d50fcc178b713a1385595a40cb19f0) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4a97f4695f25..e3e4e1fd1369 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2119,7 +2119,7 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): - """Split early bird discount deductions into Income Loss & Tax Loss.""" + """Split early payment discount into Income Loss & Tax Loss.""" if not (discount_amount and valid_discounts): return discount_amount @@ -2191,12 +2191,16 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: for account, loss in tax_discount_loss.items(): total_tax_loss += loss + amount = flt(loss * doc.get("conversion_rate", 1), precision) + if amount == 0.0: + continue + 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), + "amount": amount, }, ) From abf3c56cc9ecb6602701bd33cfd8241502ea098b Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 8 Mar 2023 17:20:48 +0530 Subject: [PATCH 08/15] fix: Paid amount must be discounted considering accounting currency - Accounting is in the same currency if party currency and company currency is the same - If accounting is in the same currency, paid and recvd amount is in the base currency - Then, discount amount must also be in the base currency as it is deducted from paid amount - Received amount must be in base currency if not multi currency - cleanup: Deductions setting broken into smaller functions (cherry picked from commit 761f68d7bf0b8539f26a79993245c8ffcbcde5f1) --- .../doctype/payment_entry/payment_entry.py | 58 ++++++++++++------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e3e4e1fd1369..e2f19f76240b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1836,7 +1836,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= ) paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( - paid_amount, received_amount, doc + paid_amount, received_amount, doc, party_account_currency ) pe = frappe.new_doc("Payment Entry") @@ -1936,17 +1936,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() - 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 * positive_negative * doc.get("conversion_rate", 1), - } + if discount_amount: + 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() @@ -2085,7 +2078,7 @@ 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"] @@ -2094,12 +2087,17 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): 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: + is_multi_currency = party_account_currency != doc.company_currency + 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 @@ -2118,20 +2116,38 @@ def apply_early_payment_discount(paid_amount, received_amount, doc): return paid_amount, received_amount, total_discount, valid_discounts -def set_early_payment_discount_loss(pe, doc, valid_discounts, discount_amount): - """Split early payment discount into Income Loss & Tax Loss.""" - if not (discount_amount and valid_discounts): - return discount_amount +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={ + "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 * 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 discount_amount + return 0.0 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)) + return flt(loss_on_income + loss_on_taxes) def get_total_discount_percent(doc, valid_discounts) -> float: From 3fb07a4eaaa386efd7828ada92fbbe0cd993a56e Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 9 Mar 2023 16:25:45 +0530 Subject: [PATCH 09/15] fix: Multi-currency SI with base currency PE - Return total discount loss in base currency - Allocate payment based on terms: Set allocated amount in references table in base currency if accounting is in that currency - Allocate payment based on terms: While back updating set paid amount (payment schedule) in transaction currency always - minor: discount msgprint in correct currency (cherry picked from commit b09c2381ca144c63098d0fedf79c92fa5f7b929a) --- .../doctype/payment_entry/payment_entry.py | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index e2f19f76240b..218430ac4354 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -440,7 +440,7 @@ 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 @@ -458,7 +458,7 @@ def update_payment_schedule(self, cancel=0): ], ) 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 if not (term.discount_type and term.discount): @@ -475,6 +475,10 @@ def update_payment_schedule(self, cancel=0): 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")) @@ -509,6 +513,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" @@ -1884,7 +1915,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= ): 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: @@ -2083,11 +2114,11 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun 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: - is_multi_currency = party_account_currency != doc.company_currency if term.discount_type == "Percentage": grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") @@ -2110,7 +2141,8 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun 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, valid_discounts @@ -2130,7 +2162,6 @@ def set_pending_discount_loss( positive_negative = -1 if pe.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 * positive_negative, } @@ -2144,10 +2175,10 @@ def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: if not total_discount_percent: return 0.0 - loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent) - loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent) + 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(loss_on_income + loss_on_taxes) + return flt(base_loss_on_income + base_loss_on_taxes) def get_total_discount_percent(doc, valid_discounts) -> float: @@ -2177,38 +2208,41 @@ 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": flt(loss_on_income * doc.get("conversion_rate", 1), precision), + "amount": base_loss_on_income, }, ) - return loss_on_income + return base_loss_on_income -def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: +def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: """Add loss on tax discount in base currency.""" tax_discount_loss = {} - total_tax_loss = 0 + 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_percenatage / 100), precision + 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] = tax_loss + tax_discount_loss[account] = base_tax_loss else: - tax_discount_loss[account] += tax_loss + tax_discount_loss[account] += base_tax_loss for account, loss in tax_discount_loss.items(): - total_tax_loss += loss - amount = flt(loss * doc.get("conversion_rate", 1), precision) - if amount == 0.0: + base_total_tax_loss += loss + if loss == 0.0: continue pe.append( @@ -2216,21 +2250,30 @@ def add_tax_discount_loss(pe, doc, total_discount_percenatage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": amount, + "amount": loss, }, ) - return total_tax_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( From b7376900558d296f23ce6f707b742a436592e9d1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 9 Mar 2023 16:32:36 +0530 Subject: [PATCH 10/15] test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount (cherry picked from commit 9abf0ef615d38d806e27b0c2fcce48125fd75fa1) # Conflicts: # erpnext/accounts/doctype/payment_entry/test_payment_entry.py --- .../payment_entry/test_payment_entry.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 783862e4a118..b8ae722d6b53 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -5,6 +5,10 @@ import frappe from frappe import qb +<<<<<<< HEAD +======= +from frappe.tests.utils import FrappeTestCase, change_settings +>>>>>>> 9abf0ef615 (test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount) from frappe.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( @@ -313,6 +317,132 @@ def test_payment_entry_against_payment_terms_with_discount_amount(self): self.assertEqual(si.payment_schedule[0].outstanding, 0) self.assertEqual(si.payment_schedule[0].discounted_amount, 50) + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1}, + ) + def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount( + self, + ): + """ + 1. Multi-currency SI with single currency accounting (company currency) + 2. PE with early payment discount + 3. Test if Paid Amount is calculated in company currency + 4. Test if deductions are calculated in company currency + + SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency. + """ + si = create_sales_invoice( + customer="_Test Customer", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Bank - _TC", + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency + self.assertEqual(pe.received_amount, 4500.0) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + self.assertEqual(pe.difference_amount, 0.0) + + pe.insert() + pe.submit() + + expected_gle = dict( + (d[0], d) + for d in [ + ["Debtors - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4500, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + + def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self): + """ + 1. Multi-currency SI with multi-currency accounting + 2. PE with early payment discount and also exchange loss + 3. Test if Paid Amount is calculated in transaction currency + 4. Test if deductions are calculated in base/company currency + 5. Test if exchange loss is reflected in difference + """ + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + do_not_save=1, + ) + create_payment_terms_template_with_discount() + si.payment_terms_template = "Test Discount Template" + + frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC") + si.save() + si.submit() + + pe = get_payment_entry( + "Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700 + ) + pe.reference_no = si.name + pe.reference_date = nowdate() + + # Early payment discount loss on income + self.assertEqual(pe.paid_amount, 90.0) + self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss) + self.assertEqual(pe.deductions[0].amount, 500.0) + self.assertEqual(pe.deductions[0].account, "Write Off - _TC") + + # Exchange loss + self.assertEqual(pe.difference_amount, 300.0) + + pe.append( + "deductions", + { + "account": "_Test Exchange Gain/Loss - _TC", + "cost_center": "_Test Cost Center - _TC", + "amount": 300.0, + }, + ) + + pe.insert() + pe.submit() + + self.assertEqual(pe.difference_amount, 0.0) + + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Bank - _TC", 4200, 0, None], + ["Write Off - _TC", 500.0, 0, None], + ["_Test Exchange Gain/Loss - _TC", 300.0, 0, None], + ] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) + self.assertEqual(outstanding_amount, 0) + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice( supplier="_Test Supplier USD", From 83989541495ac7629d595df3f7e7bbbddc9b0b68 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 13 Mar 2023 13:55:55 +0530 Subject: [PATCH 11/15] fix: Handle rounding more gracefully - Round off pending discount loss to avoid miniscule losses rounded to 0.0 that are added in deductions - Use base amounts to calculate base losses instead of using conversion factor which increases rounding error - Round of total base loss instead of individual income and tax losses to reduce rounding error - Use default round off account for pending rounding loss in deductions (cherry picked from commit caa1a3dccf66b8ff379a4482841e8309f4a7fa6d) --- .../doctype/payment_entry/payment_entry.py | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 218430ac4354..4813c08f8f8a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2155,13 +2155,15 @@ def set_pending_discount_loss( if party_account_currency != doc.company_currency: discount_amount = discount_amount * doc.get("conversion_rate", 1) - discount_amount -= base_total_discount_loss + # Avoid considering miniscule losses + discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total")) - # If pending base discount amount, set it in deductions + # If pending base discount amount (mostly rounding loss), 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={ + "account": frappe.get_cached_value("Company", pe.company, "round_off_account"), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, } @@ -2178,7 +2180,8 @@ def set_early_payment_discount_loss(pe, doc, valid_discounts) -> float: 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) + # Round off total loss rather than individual losses to reduce rounding error + return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total")) def get_total_discount_percent(doc, valid_discounts) -> float: @@ -2207,18 +2210,18 @@ def get_total_discount_percent(doc, valid_discounts) -> float: 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) + base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100) 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, + "amount": flt(base_loss_on_income, precision), }, ) - return base_loss_on_income + + return base_loss_on_income # Return loss without rounding def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: @@ -2229,10 +2232,9 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: # 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 = tax.get("base_tax_amount_after_discount_amount") * ( + total_discount_percentage / 100 ) - base_tax_loss = flt(tax_loss * doc.get("conversion_rate", 1), precision) account = tax.get("account_head") if not tax_discount_loss.get(account): @@ -2250,11 +2252,11 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float: { "account": account, "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), - "amount": loss, + "amount": flt(loss, precision), }, ) - return base_total_tax_loss + return base_total_tax_loss # Return loss without rounding def get_reference_as_per_payment_terms( From 9eab2f8cde5f7501b66e92313f7da90681d2b187 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 14 Mar 2023 13:40:55 +0530 Subject: [PATCH 12/15] fix: Provision to apply early payment discount if payment is recorded late - Party could have paid on time but payment is recorded late - Prompt for reference date so that discount is applied while mapping - Prompt only if discount in payment schedule of valid doctypes - test: Reference date and impact on PE - `make_payment_entry` (JS) must be able to access `this` (cherry picked from commit d6d0163514882a9d7ae16a61be54b7776c001e94) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.py # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js # erpnext/buying/doctype/purchase_order/purchase_order.js # erpnext/public/js/controllers/transaction.js --- .../doctype/payment_entry/payment_entry.py | 23 ++++++- .../payment_entry/test_payment_entry.py | 9 +++ .../purchase_invoice/purchase_invoice.js | 9 +++ .../doctype/sales_invoice/sales_invoice.js | 9 ++- .../doctype/purchase_order/purchase_order.js | 9 +++ erpnext/public/js/controllers/transaction.js | 60 +++++++++++++++++-- 6 files changed, 107 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 4813c08f8f8a..ac3bcad200de 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1741,8 +1741,21 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ) +<<<<<<< HEAD def get_amounts_based_on_reference_doctype( reference_doctype, ref_doc, party_account_currency, company_currency, reference_name +======= +@frappe.whitelist() +def get_payment_entry( + dt, + dn, + party_amount=None, + bank_account=None, + bank_amount=None, + party_type=None, + payment_type=None, + reference_date=None, +>>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) ): total_amount = outstanding_amount = exchange_rate = None if reference_doctype == "Fees": @@ -1866,8 +1879,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) + reference_date = getdate(reference_date) paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount( - paid_amount, received_amount, doc, party_account_currency + paid_amount, received_amount, doc, party_account_currency, reference_date ) pe = frappe.new_doc("Payment Entry") @@ -1875,6 +1889,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.company = doc.company pe.cost_center = doc.get("cost_center") pe.posting_date = nowdate() + pe.reference_date = reference_date pe.mode_of_payment = doc.get("mode_of_payment") pe.party_type = party_type pe.party = doc.get(scrub(party_type)) @@ -2109,7 +2124,9 @@ def set_paid_amount_and_received_amount( return paid_amount, received_amount -def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency): +def apply_early_payment_discount( + paid_amount, received_amount, doc, party_account_currency, reference_date +): total_discount = 0 valid_discounts = [] eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"] @@ -2118,7 +2135,7 @@ def apply_early_payment_discount(paid_amount, received_amount, doc, party_accoun 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 not term.discounted_amount and term.discount and reference_date <= term.discount_date: if term.discount_type == "Percentage": grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total") diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index b8ae722d6b53..c26e1e5bea9f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -302,6 +302,15 @@ def test_payment_entry_against_payment_terms_with_discount_amount(self): si.save() si.submit() + # Set reference date past discount cut off date + pe_1 = get_payment_entry( + "Sales Invoice", + si.name, + bank_account="_Test Cash - _TC", + reference_date=frappe.utils.add_days(si.posting_date, 2), + ) + self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied + 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) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 0208975513b0..03d3ab3047f3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -81,8 +81,17 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } if(doc.docstatus == 1 && doc.outstanding_amount != 0 +<<<<<<< HEAD && !(doc.is_return && doc.return_against)) { this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); +======= + && !(doc.is_return && doc.return_against) && !doc.on_hold) { + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); +>>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 462233524f80..a624c638c369 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -75,9 +75,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte if (doc.docstatus == 1 && doc.outstanding_amount!=0 && !(cint(doc.is_return) && doc.return_against)) { - cur_frm.add_custom_button(__('Payment'), - this.make_payment_entry, __('Create')); - cur_frm.page.set_inner_btn_group_as_primary(__('Create')); + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); + this.frm.page.set_inner_btn_group_as_primary(__('Create')); } if(doc.docstatus==1 && !doc.is_return) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 2559ce76da6e..310e379ce11a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -191,8 +191,17 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); +<<<<<<< HEAD if(flt(doc.per_billed)==0 && doc.status != "Delivered") { cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); +======= + if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { + this.frm.add_custom_button( + __('Payment'), + () => this.make_payment_entry(), + __('Create') + ); +>>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) } if(flt(doc.per_billed)==0) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index eb78ca72bc17..e50c5d756f37 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1987,22 +1987,70 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, +<<<<<<< HEAD make_payment_entry: function() { +======= + make_payment_entry() { + let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry; + if(this.has_discount_in_schedule() && !via_journal_entry) { + // If early payment discount is applied, ask user for reference date + this.prompt_user_for_reference_date(); + } else { + this.make_mapped_payment_entry(); + } + } + + make_mapped_payment_entry(args) { + var me = this; + args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name }; +>>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) return frappe.call({ - method: cur_frm.cscript.get_method_for_payment(), - args: { - "dt": cur_frm.doc.doctype, - "dn": cur_frm.doc.name - }, + method: me.get_method_for_payment(), + args: args, callback: function(r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - // cur_frm.refresh_fields() } }); }, +<<<<<<< HEAD make_quality_inspection: function () { +======= + prompt_user_for_reference_date(){ + var me = this; + frappe.prompt({ + label: __("Cheque/Reference Date"), + fieldname: "reference_date", + fieldtype: "Date", + reqd: 1, + }, (values) => { + let args = { + "dt": me.frm.doc.doctype, + "dn": me.frm.doc.name, + "reference_date": values.reference_date + } + me.make_mapped_payment_entry(args); + }, + __("Reference Date for Early Payment Discount"), + __("Continue") + ); + } + + has_discount_in_schedule() { + let is_eligible = in_list( + ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"], + this.frm.doctype + ); + let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length; + if(!is_eligible || !has_payment_schedule) return false; + + let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date); + return has_discount; + } + + make_quality_inspection() { +>>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) let data = []; const fields = [ { From 00191b9dacc4cd564b0aa35f8ac6c190487366c9 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 27 Mar 2023 16:11:00 +0530 Subject: [PATCH 13/15] feat: Make Tax loss booking optional - Checkbox in Accounts Settings - Apply checkbox in PE deductions setting logic - Adjust tests (cherry picked from commit 216a46bd6615aab47a30ff79ddf78503080121c1) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json --- .../accounts_settings/accounts_settings.json | 101 ++++++++++++++++++ .../doctype/payment_entry/payment_entry.py | 17 ++- .../payment_entry/test_payment_entry.py | 34 ++++-- 3 files changed, 139 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index ea427aa7d803..c5c13a1f3687 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -37,6 +37,14 @@ "automatically_process_deferred_accounting_entry", "book_deferred_entries_via_journal_entry", "submit_journal_entries", +<<<<<<< HEAD +======= + "tax_settings_section", + "determine_address_tax_category_from", + "column_break_19", + "add_taxes_from_item_tax_template", + "book_tax_discount_loss", +>>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -279,19 +287,112 @@ "label": "Enable Common Party Accounting" }, { +<<<<<<< HEAD "default": "0", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account" } +======= + "fieldname": "enable_features_section", + "fieldtype": "Section Break", + "label": "Invoice Cancellation" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_25", + "fieldtype": "Column Break" + }, + { + "fieldname": "asset_settings_section", + "fieldtype": "Section Break", + "label": "Asset Settings" + }, + { + "fieldname": "invoicing_settings_tab", + "fieldtype": "Tab Break", + "label": "Credit Limits" + }, + { + "fieldname": "assets_tab", + "fieldtype": "Tab Break", + "label": "Assets" + }, + { + "fieldname": "closing_settings_tab", + "fieldtype": "Tab Break", + "label": "Accounts Closing" + }, + { + "fieldname": "pos_setting_section", + "fieldtype": "Section Break", + "label": "POS Setting" + }, + { + "fieldname": "invoice_and_billing_tab", + "fieldtype": "Tab Break", + "label": "Invoice and Billing" + }, + { + "fieldname": "invoicing_features_section", + "fieldtype": "Section Break", + "label": "Invoicing Features" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "pos_tab", + "fieldtype": "Tab Break", + "label": "POS" + }, + { + "fieldname": "report_setting_section", + "fieldtype": "Section Break", + "label": "Report Setting" + }, + { + "default": "0", + "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", + "fieldname": "allow_multi_currency_invoices_against_single_party_account", + "fieldtype": "Check", + "label": "Allow multi-currency invoices against single party account " + }, + { + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" + }, + { + "default": "0", + "description": "Split Early Payment Discount Loss into Income and Tax Loss", + "fieldname": "book_tax_discount_loss", + "fieldtype": "Check", + "label": "Book Tax Loss on Early Payment Discount" + } +>>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) ], "icon": "icon-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-07-11 13:37:50.605141", +======= + "modified": "2023-03-28 09:50:20.375233", +>>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ac3bcad200de..aa43b78e5383 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1983,7 +1983,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.set_amounts() if discount_amount: - base_total_discount_loss = set_early_payment_discount_loss(pe, doc, valid_discounts) + base_total_discount_loss = 0 + if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"): + base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts) + set_pending_discount_loss( pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) @@ -2175,19 +2178,25 @@ def set_pending_discount_loss( # Avoid considering miniscule losses discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total")) - # If pending base discount amount (mostly rounding loss), set it in deductions + # Set base discount amount (discount loss/pending rounding loss) in deductions if discount_amount > 0.0: positive_negative = -1 if pe.payment_type == "Pay" else 1 + + # If tax loss booking is enabled, pending loss will be rounding loss. + # Otherwise it will be the total discount loss. + book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss") + account_type = "round_off_account" if book_tax_loss else "default_discount_account" + pe.set_gain_or_loss( account_details={ - "account": frappe.get_cached_value("Company", pe.company, "round_off_account"), + "account": frappe.get_cached_value("Company", pe.company, account_type), "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: +def split_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) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index c26e1e5bea9f..025342177768 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -256,17 +256,24 @@ def test_payment_entry_against_payment_terms_with_discount(self): }, ) si.save() - si.submit() + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + + self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount") + self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0) + self.assertEqual(pe_with_tax_loss.paid_amount, 212.4) + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) 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") + self.assertEqual(pe.deductions[0].amount, 23.6) pe.submit() si.load_from_db() @@ -311,12 +318,18 @@ def test_payment_entry_against_payment_terms_with_discount_amount(self): ) self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied + # Test if tax loss is booked on enabling configuration + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1) + pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income + self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax + self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC") + + frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0) 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") + self.assertEqual(pe.deductions[0].amount, 50.0) pe.submit() si.load_from_db() @@ -328,7 +341,10 @@ def test_payment_entry_against_payment_terms_with_discount_amount(self): @change_settings( "Accounts Settings", - {"allow_multi_currency_invoices_against_single_party_account": 1}, + { + "allow_multi_currency_invoices_against_single_party_account": 1, + "book_tax_discount_loss": 1, + }, ) def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount( self, From e692f2753785081f110b58bb9bd048199d85d7fc Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 4 Apr 2023 11:58:06 +0530 Subject: [PATCH 14/15] fix: Merge conflicts --- .../accounts_settings/accounts_settings.json | 109 ++---------------- .../doctype/payment_entry/payment_entry.py | 22 ++-- .../payment_entry/test_payment_entry.py | 5 +- .../purchase_invoice/purchase_invoice.js | 5 - .../doctype/purchase_order/purchase_order.js | 5 - erpnext/public/js/controllers/transaction.js | 14 +-- 6 files changed, 20 insertions(+), 140 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index c5c13a1f3687..07b4318a9dc8 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -26,6 +26,7 @@ "determine_address_tax_category_from", "column_break_19", "add_taxes_from_item_tax_template", + "book_tax_discount_loss", "period_closing_settings_section", "acc_frozen_upto", "frozen_accounts_modifier", @@ -37,14 +38,6 @@ "automatically_process_deferred_accounting_entry", "book_deferred_entries_via_journal_entry", "submit_journal_entries", -<<<<<<< HEAD -======= - "tax_settings_section", - "determine_address_tax_category_from", - "column_break_19", - "add_taxes_from_item_tax_template", - "book_tax_discount_loss", ->>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) "print_settings", "show_inclusive_tax_in_print", "column_break_12", @@ -287,112 +280,26 @@ "label": "Enable Common Party Accounting" }, { -<<<<<<< HEAD "default": "0", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldtype": "Check", "label": "Allow multi-currency invoices against single party account" + }, + { + "default": "0", + "description": "Split Early Payment Discount Loss into Income and Tax Loss", + "fieldname": "book_tax_discount_loss", + "fieldtype": "Check", + "label": "Book Tax Loss on Early Payment Discount" } -======= - "fieldname": "enable_features_section", - "fieldtype": "Section Break", - "label": "Invoice Cancellation" - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "fieldname": "column_break_25", - "fieldtype": "Column Break" - }, - { - "fieldname": "asset_settings_section", - "fieldtype": "Section Break", - "label": "Asset Settings" - }, - { - "fieldname": "invoicing_settings_tab", - "fieldtype": "Tab Break", - "label": "Credit Limits" - }, - { - "fieldname": "assets_tab", - "fieldtype": "Tab Break", - "label": "Assets" - }, - { - "fieldname": "closing_settings_tab", - "fieldtype": "Tab Break", - "label": "Accounts Closing" - }, - { - "fieldname": "pos_setting_section", - "fieldtype": "Section Break", - "label": "POS Setting" - }, - { - "fieldname": "invoice_and_billing_tab", - "fieldtype": "Tab Break", - "label": "Invoice and Billing" - }, - { - "fieldname": "invoicing_features_section", - "fieldtype": "Section Break", - "label": "Invoicing Features" - }, - { - "fieldname": "column_break_17", - "fieldtype": "Column Break" - }, - { - "fieldname": "pos_tab", - "fieldtype": "Tab Break", - "label": "POS" - }, - { - "fieldname": "report_setting_section", - "fieldtype": "Section Break", - "label": "Report Setting" - }, - { - "default": "0", - "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", - "fieldname": "allow_multi_currency_invoices_against_single_party_account", - "fieldtype": "Check", - "label": "Allow multi-currency invoices against single party account " - }, - { - "fieldname": "tab_break_dpet", - "fieldtype": "Tab Break", - "label": "Chart Of Accounts" - }, - { - "default": "1", - "fieldname": "show_balance_in_coa", - "fieldtype": "Check", - "label": "Show Balances in Chart Of Accounts" - }, - { - "default": "0", - "description": "Split Early Payment Discount Loss into Income and Tax Loss", - "fieldname": "book_tax_discount_loss", - "fieldtype": "Check", - "label": "Book Tax Loss on Early Payment Discount" - } ->>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) ], "icon": "icon-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-07-11 13:37:50.605141", -======= "modified": "2023-03-28 09:50:20.375233", ->>>>>>> 216a46bd66 (feat: Make Tax loss booking optional) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index aa43b78e5383..5e46b7d2938c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1741,21 +1741,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre ) -<<<<<<< HEAD def get_amounts_based_on_reference_doctype( reference_doctype, ref_doc, party_account_currency, company_currency, reference_name -======= -@frappe.whitelist() -def get_payment_entry( - dt, - dn, - party_amount=None, - bank_account=None, - bank_amount=None, - party_type=None, - payment_type=None, - reference_date=None, ->>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) ): total_amount = outstanding_amount = exchange_rate = None if reference_doctype == "Fees": @@ -1858,7 +1845,14 @@ def get_bill_no_and_update_amounts( @frappe.whitelist() -def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None): +def get_payment_entry( + dt, + dn, + party_amount=None, + bank_account=None, + bank_amount=None, + reference_date=None, +): reference_doc = None doc = frappe.get_doc(dt, dn) if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 025342177768..740f62a360a7 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -5,10 +5,7 @@ import frappe from frappe import qb -<<<<<<< HEAD -======= -from frappe.tests.utils import FrappeTestCase, change_settings ->>>>>>> 9abf0ef615 (test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount) +from frappe.tests.utils import change_settings from frappe.utils import flt, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import ( diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 03d3ab3047f3..76b85ecf0e25 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -81,17 +81,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ } if(doc.docstatus == 1 && doc.outstanding_amount != 0 -<<<<<<< HEAD - && !(doc.is_return && doc.return_against)) { - this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create')); -======= && !(doc.is_return && doc.return_against) && !doc.on_hold) { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), __('Create') ); ->>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) cur_frm.page.set_inner_btn_group_as_primary(__('Create')); } diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 310e379ce11a..72329e9a22e6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -191,17 +191,12 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); -<<<<<<< HEAD - if(flt(doc.per_billed)==0 && doc.status != "Delivered") { - cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create')); -======= if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), __('Create') ); ->>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) } if(flt(doc.per_billed)==0) { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e50c5d756f37..972e6b1516c9 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1987,9 +1987,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, -<<<<<<< HEAD - make_payment_entry: function() { -======= make_payment_entry() { let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry; if(this.has_discount_in_schedule() && !via_journal_entry) { @@ -1998,12 +1995,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } else { this.make_mapped_payment_entry(); } - } + }, make_mapped_payment_entry(args) { var me = this; args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name }; ->>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) return frappe.call({ method: me.get_method_for_payment(), args: args, @@ -2014,9 +2010,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); }, -<<<<<<< HEAD - make_quality_inspection: function () { -======= prompt_user_for_reference_date(){ var me = this; frappe.prompt({ @@ -2035,7 +2028,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ __("Reference Date for Early Payment Discount"), __("Continue") ); - } + }, has_discount_in_schedule() { let is_eligible = in_list( @@ -2047,10 +2040,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date); return has_discount; - } + }, make_quality_inspection() { ->>>>>>> d6d0163514 (fix: Provision to apply early payment discount if payment is recorded late) let data = []; const fields = [ { From ba987a90a273e860937903320f47ff510bf35877 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 4 Apr 2023 12:36:18 +0530 Subject: [PATCH 15/15] fix: 'Donation' does not have `company_currency` field - Make sure check uses this field only for eligible documents --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5e46b7d2938c..44b8dbe5326f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2128,9 +2128,11 @@ def apply_early_payment_discount( 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: + # Non eligible documents may not have `company_currency` field + is_multi_currency = party_account_currency != doc.company_currency + for term in doc.payment_schedule: if not term.discounted_amount and term.discount and reference_date <= term.discount_date: