Merge pull request #25271 from deepeshgarg007/loan_penalty_new_fix
fix: Consider paid repayment entries in subsequent loan repayments
This commit is contained in:
commit
1466e2d758
@ -23,6 +23,7 @@
|
|||||||
"rate_of_interest",
|
"rate_of_interest",
|
||||||
"is_secured_loan",
|
"is_secured_loan",
|
||||||
"disbursement_date",
|
"disbursement_date",
|
||||||
|
"closure_date",
|
||||||
"disbursed_amount",
|
"disbursed_amount",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
"maximum_loan_amount",
|
"maximum_loan_amount",
|
||||||
@ -348,12 +349,18 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "closure_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Closure Date",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-24 12:27:23.208240",
|
"modified": "2021-04-10 09:28:21.946972",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan",
|
"name": "Loan",
|
||||||
|
|||||||
@ -523,33 +523,7 @@ class TestLoan(unittest.TestCase):
|
|||||||
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
|
self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0))
|
||||||
|
|
||||||
def test_penalty(self):
|
def test_penalty(self):
|
||||||
pledge = [{
|
loan, amounts = create_loan_scenario_for_penalty(self)
|
||||||
"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()
|
|
||||||
|
|
||||||
# 30 days - grace period
|
# 30 days - grace period
|
||||||
penalty_days = 30 - 4
|
penalty_days = 30 - 4
|
||||||
penalty_applicable_amount = flt(amounts['interest_amount']/2)
|
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',
|
calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual',
|
||||||
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
|
{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount')
|
||||||
|
|
||||||
|
self.assertEquals(loan.loan_amount, 1000000)
|
||||||
self.assertEquals(calculated_penalty_amount, penalty_amount)
|
self.assertEquals(calculated_penalty_amount, penalty_amount)
|
||||||
|
|
||||||
|
def test_penalty_repayment(self):
|
||||||
|
loan, dummy = 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):
|
def test_loan_write_off_limit(self):
|
||||||
pledge = [{
|
pledge = [{
|
||||||
"loan_security": "Test Security 1",
|
"loan_security": "Test Security 1",
|
||||||
@ -651,6 +645,32 @@ class TestLoan(unittest.TestCase):
|
|||||||
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
|
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
|
||||||
self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0)
|
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, amounts
|
||||||
|
|
||||||
def create_loan_accounts():
|
def create_loan_accounts():
|
||||||
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
|
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):
|
||||||
|
|||||||
@ -20,6 +20,10 @@
|
|||||||
"cost_center",
|
"cost_center",
|
||||||
"customer_details_section",
|
"customer_details_section",
|
||||||
"bank_account",
|
"bank_account",
|
||||||
|
"disbursement_references_section",
|
||||||
|
"reference_date",
|
||||||
|
"column_break_17",
|
||||||
|
"reference_number",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -126,12 +130,31 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_8",
|
"fieldname": "column_break_8",
|
||||||
"fieldtype": "Column Break"
|
"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,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-06 10:04:30.882322",
|
"modified": "2021-04-10 10:03:41.502210",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Disbursement",
|
"name": "Loan Disbursement",
|
||||||
|
|||||||
@ -239,14 +239,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "total_penalty_paid",
|
"fieldname": "total_penalty_paid",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
|
"hidden": 1,
|
||||||
"label": "Total Penalty Paid",
|
"label": "Total Penalty Paid",
|
||||||
"options": "Company:company:default_currency"
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-05 13:45:19.137896",
|
"modified": "2021-04-10 10:00:31.859076",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Repayment",
|
"name": "Loan Repayment",
|
||||||
|
|||||||
@ -75,7 +75,7 @@ class LoanRepayment(AccountsController):
|
|||||||
"docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
|
"docstatus": 1, "against_loan": self.against_loan}, 'posting_date')
|
||||||
|
|
||||||
if future_repayment_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):
|
def validate_amount(self):
|
||||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||||
@ -83,10 +83,6 @@ class LoanRepayment(AccountsController):
|
|||||||
if not self.amount_paid:
|
if not self.amount_paid:
|
||||||
frappe.throw(_("Amount paid cannot be zero"))
|
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):
|
def book_unaccrued_interest(self):
|
||||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||||
if self.total_interest_paid > self.interest_payable:
|
if self.total_interest_paid > self.interest_payable:
|
||||||
@ -231,6 +227,14 @@ class LoanRepayment(AccountsController):
|
|||||||
gle_map = []
|
gle_map = []
|
||||||
loan_details = frappe.get_doc("Loan", self.against_loan)
|
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:
|
if self.total_penalty_paid:
|
||||||
gle_map.append(
|
gle_map.append(
|
||||||
self.get_gl_dict({
|
self.get_gl_dict({
|
||||||
@ -271,7 +275,7 @@ class LoanRepayment(AccountsController):
|
|||||||
"debit_in_account_currency": self.amount_paid,
|
"debit_in_account_currency": self.amount_paid,
|
||||||
"against_voucher_type": "Loan",
|
"against_voucher_type": "Loan",
|
||||||
"against_voucher": self.against_loan,
|
"against_voucher": self.against_loan,
|
||||||
"remarks": _("Repayment against Loan: ") + self.against_loan,
|
"remarks": remarks,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date)
|
"posting_date": getdate(self.posting_date)
|
||||||
})
|
})
|
||||||
@ -287,7 +291,7 @@ class LoanRepayment(AccountsController):
|
|||||||
"credit_in_account_currency": self.amount_paid,
|
"credit_in_account_currency": self.amount_paid,
|
||||||
"against_voucher_type": "Loan",
|
"against_voucher_type": "Loan",
|
||||||
"against_voucher": self.against_loan,
|
"against_voucher": self.against_loan,
|
||||||
"remarks": _("Repayment against Loan: ") + self.against_loan,
|
"remarks": remarks,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date)
|
"posting_date": getdate(self.posting_date)
|
||||||
})
|
})
|
||||||
@ -338,6 +342,18 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
|
|||||||
|
|
||||||
return unpaid_accrued_entries
|
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
|
# 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
|
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
|
||||||
|
|
||||||
@ -348,6 +364,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
|||||||
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
|
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
|
||||||
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date)
|
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date)
|
||||||
|
|
||||||
|
computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan)
|
||||||
pending_accrual_entries = {}
|
pending_accrual_entries = {}
|
||||||
|
|
||||||
total_pending_interest = 0
|
total_pending_interest = 0
|
||||||
@ -362,8 +379,13 @@ def get_amounts(amounts, against_loan, posting_date):
|
|||||||
# and if no_of_late days are positive then penalty is levied
|
# and if no_of_late days are positive then penalty is levied
|
||||||
|
|
||||||
due_date = add_days(entry.posting_date, 1)
|
due_date = add_days(entry.posting_date, 1)
|
||||||
no_of_late_days = date_diff(posting_date,
|
due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days)
|
||||||
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
|
|
||||||
|
# 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':
|
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)
|
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)
|
||||||
@ -401,7 +423,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
|||||||
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
|
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
|
||||||
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
|
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
|
||||||
amounts["interest_amount"] = flt(total_pending_interest, 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["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
|
||||||
amounts["pending_accrual_entries"] = pending_accrual_entries
|
amounts["pending_accrual_entries"] = pending_accrual_entries
|
||||||
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
|
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
|
||||||
|
|||||||
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import get_datetime, flt
|
from frappe.utils import get_datetime, flt, getdate
|
||||||
import json
|
import json
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
|
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
|
pledged_qty += qty
|
||||||
|
|
||||||
if not pledged_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()
|
@frappe.whitelist()
|
||||||
def get_pledged_security_qty(loan):
|
def get_pledged_security_qty(loan):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user