From 37d6c19a347a833c6863bd14108815fe4497563a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Apr 2021 18:33:26 +0530 Subject: [PATCH 1/3] fix: Consider paid repayment entries in subsequent loan repayments --- .../loan_management/doctype/loan/test_loan.py | 74 ++++++++++++------- .../doctype/loan_repayment/loan_repayment.py | 28 +++++-- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 4b9a89486a..57f98869cc 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -523,33 +523,7 @@ class TestLoan(unittest.TestCase): self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) def test_penalty(self): - pledge = [{ - "loan_security": "Test Security 1", - "qty": 4000.00 - }] - - loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) - create_pledge(loan_application) - - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') - loan.submit() - - self.assertEquals(loan.loan_amount, 1000000) - - first_date = '2019-10-01' - last_date = '2019-10-30' - - make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) - process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - - amounts = calculate_amounts(loan.name, add_days(last_date, 1)) - paid_amount = amounts['interest_amount']/2 - - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), - paid_amount) - - repayment_entry.submit() - + loan = create_loan_scenario_for_penalty(self) # 30 days - grace period penalty_days = 30 - 4 penalty_applicable_amount = flt(amounts['interest_amount']/2) @@ -559,8 +533,28 @@ class TestLoan(unittest.TestCase): calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') + self.assertEquals(loan.loan_amount, 1000000) self.assertEquals(calculated_penalty_amount, penalty_amount) + def test_penalty_repayment(self): + loan = create_loan_scenario_for_penalty(self) + amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00') + + first_penalty = 10000 + second_penalty = amounts['penalty_amount'] - 10000 + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000) + repayment_entry.submit() + + amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01') + self.assertEquals(amounts['penalty_amount'], second_penalty) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty) + repayment_entry.submit() + + amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02') + self.assertEquals(amounts['penalty_amount'], 0) + def test_loan_write_off_limit(self): pledge = [{ "loan_security": "Test Security 1", @@ -651,6 +645,32 @@ class TestLoan(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) +def create_loan_scenario_for_penalty(doc): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + first_date = '2019-10-01' + last_date = '2019-10-30' + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + amounts = calculate_amounts(loan.name, add_days(last_date, 1)) + paid_amount = amounts['interest_amount']/2 + + repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5), + paid_amount) + + repayment_entry.submit() + + return loan def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a88e183ead..2a0221beb1 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -75,10 +75,6 @@ class LoanRepayment(AccountsController): if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) - if not self.shortfall_amount and self.amount_paid < self.penalty_amount: - msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) - frappe.throw(msg) - def book_unaccrued_interest(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 if self.total_interest_paid > self.interest_payable: @@ -327,6 +323,18 @@ def get_accrued_interest_entries(against_loan): return unpaid_accrued_entries +def get_penalty_details(against_loan): + penalty_details = frappe.db.sql(""" + SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount + FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment` + where against_loan = %s) and docstatus = 1 and against_loan = %s + """, (against_loan, against_loan)) + + if penalty_details: + return penalty_details[0][0], flt(penalty_details[0][1]) + else: + return None, 0 + # This function returns the amounts that are payable at the time of loan repayment based on posting date # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable @@ -337,6 +345,7 @@ def get_amounts(amounts, against_loan, posting_date): loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name) + computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan) pending_accrual_entries = {} total_pending_interest = 0 @@ -351,8 +360,13 @@ def get_amounts(amounts, against_loan, posting_date): # and if no_of_late days are positive then penalty is levied due_date = add_days(entry.posting_date, 1) - no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + 1 + due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days) + + # Consider one day after already calculated penalty + if computed_penalty_date and getdate(computed_penalty_date) >= due_date_after_grace_period: + due_date_after_grace_period = add_days(computed_penalty_date, 1) + + no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1 if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) @@ -390,7 +404,7 @@ def get_amounts(amounts, against_loan, posting_date): amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["interest_amount"] = flt(total_pending_interest, precision) - amounts["penalty_amount"] = flt(penalty_amount, precision) + amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision) amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) amounts["pending_accrual_entries"] = pending_accrual_entries amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) From f38a653a08006a97b360bf864dedd9173fbc4ca3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Apr 2021 18:34:51 +0530 Subject: [PATCH 2/3] fix: Show exact error message in datetime --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 834c98cb3e..baa45a4704 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -75,7 +75,7 @@ class LoanRepayment(AccountsController): "docstatus": 1, "against_loan": self.against_loan}, 'posting_date') if future_repayment_date: - frappe.throw("Repayment already made till date {0}".format(getdate(future_repayment_date))) + frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date))) def validate_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 From b58590251b4537c9c1a927312f3ba1e633e8e746 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Apr 2021 19:55:36 +0530 Subject: [PATCH 3/3] fix: minor improvements and tests --- .../loan_management/doctype/loan/loan.json | 9 ++++++- .../loan_management/doctype/loan/test_loan.py | 6 ++--- .../loan_disbursement/loan_disbursement.json | 25 ++++++++++++++++++- .../loan_repayment/loan_repayment.json | 6 +++-- .../doctype/loan_repayment/loan_repayment.py | 12 +++++++-- .../loan_security_unpledge.py | 8 ++++-- 6 files changed, 55 insertions(+), 11 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index acf09f5c03..4f8ceb0de8 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -23,6 +23,7 @@ "rate_of_interest", "is_secured_loan", "disbursement_date", + "closure_date", "disbursed_amount", "column_break_11", "maximum_loan_amount", @@ -348,12 +349,18 @@ "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 + }, + { + "fieldname": "closure_date", + "fieldtype": "Date", + "label": "Closure Date", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-24 12:27:23.208240", + "modified": "2021-04-10 09:28:21.946972", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 57f98869cc..6f8da3166f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -523,7 +523,7 @@ class TestLoan(unittest.TestCase): self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) def test_penalty(self): - loan = create_loan_scenario_for_penalty(self) + loan, amounts = create_loan_scenario_for_penalty(self) # 30 days - grace period penalty_days = 30 - 4 penalty_applicable_amount = flt(amounts['interest_amount']/2) @@ -537,7 +537,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(calculated_penalty_amount, penalty_amount) def test_penalty_repayment(self): - loan = create_loan_scenario_for_penalty(self) + loan, dummy = create_loan_scenario_for_penalty(self) amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00') first_penalty = 10000 @@ -670,7 +670,7 @@ def create_loan_scenario_for_penalty(doc): repayment_entry.submit() - return loan + return loan, amounts def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index cd5df4d3cd..662c626b8d 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -20,6 +20,10 @@ "cost_center", "customer_details_section", "bank_account", + "disbursement_references_section", + "reference_date", + "column_break_17", + "reference_number", "amended_from" ], "fields": [ @@ -126,12 +130,31 @@ { "fieldname": "column_break_8", "fieldtype": "Column Break" + }, + { + "fieldname": "disbursement_references_section", + "fieldtype": "Section Break", + "label": "Disbursement References" + }, + { + "fieldname": "reference_date", + "fieldtype": "Date", + "label": "Reference Date" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "reference_number", + "fieldtype": "Data", + "label": "Reference Number" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-06 10:04:30.882322", + "modified": "2021-04-10 10:03:41.502210", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 86ea59dc24..8fbf233be5 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -239,14 +239,16 @@ { "fieldname": "total_penalty_paid", "fieldtype": "Currency", + "hidden": 1, "label": "Total Penalty Paid", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-05 13:45:19.137896", + "modified": "2021-04-10 10:00:31.859076", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index baa45a4704..728eadf22a 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -227,6 +227,14 @@ class LoanRepayment(AccountsController): gle_map = [] loan_details = frappe.get_doc("Loan", self.against_loan) + if self.shortfall_amount and self.amount_paid > self.shortfall_amount: + remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, + self.against_loan) + elif self.shortfall_amount: + remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount) + else: + remarks = _("Repayment against Loan: ") + self.against_loan + if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ @@ -267,7 +275,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against Loan: ") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) @@ -283,7 +291,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against Loan: ") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index c4c2d68378..b24dc2f7c2 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_datetime, flt +from frappe.utils import get_datetime, flt, getdate import json from six import iteritems from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price @@ -113,7 +113,11 @@ class LoanSecurityUnpledge(Document): pledged_qty += qty if not pledged_qty: - frappe.db.set_value('Loan', self.loan, 'status', 'Closed') + frappe.db.set_value('Loan', self.loan, + { + 'status': 'Closed', + 'closure_date': getdate() + }) @frappe.whitelist() def get_pledged_security_qty(loan):