fix: Updates in term loan processing

This commit is contained in:
Deepesh Garg 2021-10-20 19:55:00 +05:30
parent 6019f60d0a
commit 8116b9b62f
6 changed files with 181 additions and 27 deletions

View File

@ -240,12 +240,14 @@
"label": "Repayment Schedule"
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
"no_copy": 1,
"options": "Repayment Schedule"
"options": "Repayment Schedule",
"read_only": 1
},
{
"fieldname": "section_break_17",
@ -360,10 +362,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:10:32.360818",
"modified": "2021-10-20 08:28:16.796105",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View File

@ -65,7 +65,7 @@ class Loan(AccountsController):
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
self.monthly_repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self):
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
@ -207,7 +207,7 @@ def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_a
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest, repayment_periods):
def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *

View File

@ -297,6 +297,27 @@ class TestLoan(unittest.TestCase):
self.assertEqual(amounts[0], 11250.00)
self.assertEqual(amounts[1], 78303.00)
def test_repayment_schedule_update(self):
loan = create_loan(self.applicant2, "Personal Loan", 200000, "Repay Over Number of Periods", 4,
applicant_type='Customer', repayment_start_date='2021-04-30', posting_date='2021-04-01')
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date='2021-04-01')
process_loan_interest_accrual_for_term_loans(posting_date='2021-05-01')
process_loan_interest_accrual_for_term_loans(posting_date='2021-06-01')
repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2021-06-05', 120000)
repayment_entry.submit()
loan.load_from_db()
self.assertEqual(flt(loan.get('repayment_schedule')[3].principal_amount, 2), 32151.83)
self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 225.06)
self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 32376.89)
self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0)
def test_security_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
@ -940,18 +961,18 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
repayment_start_date=None, posting_date=None):
applicant_type=None, repayment_start_date=None, posting_date=None):
loan = frappe.get_doc({
"doctype": "Loan",
"applicant_type": "Employee",
"applicant_type": applicant_type or "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": loan_type,
"loan_amount": loan_amount,
"repayment_method": repayment_method,
"repayment_periods": repayment_periods,
"repayment_start_date": nowdate(),
"repayment_start_date": repayment_start_date or nowdate(),
"is_term_loan": 1,
"posting_date": posting_date or nowdate()
})

View File

@ -83,7 +83,7 @@ class LoanApplication(Document):
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
self.repayment_amount = get_monthly_repayment_amount(self.loan_amount, self.rate_of_interest, self.repayment_periods)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)

View File

@ -76,6 +76,39 @@ class LoanInterestAccrual(AccountsController):
})
)
if self.payable_principal_amount:
gle_map.append(
self.get_gl_dict({
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.interest_income_account,
"debit": self.payable_principal_amount,
"debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
)
gle_map.append(
self.get_gl_dict({
"account": self.interest_income_account,
"against": self.loan_account,
"credit": self.payable_principal_amount,
"credit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan),
"cost_center": erpnext.get_default_cost_center(self.company),
"posting_date": self.posting_date
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate
from frappe.utils import add_days, add_months, cint, date_diff, flt, get_datetime, getdate
from six import iteritems
import erpnext
@ -38,10 +38,12 @@ class LoanRepayment(AccountsController):
def on_submit(self):
self.update_paid_amount()
self.update_repayment_schedule()
self.make_gl_entries()
def on_cancel(self):
self.mark_as_unpaid()
self.update_repayment_schedule()
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
@ -164,6 +166,10 @@ class LoanRepayment(AccountsController):
if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def update_repayment_schedule(self):
if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount:
regenerate_repayment_schedule(self.against_loan)
def allocate_amounts(self, repayment_details):
self.set('repayment_details', [])
self.principal_amount_paid = 0
@ -185,50 +191,93 @@ class LoanRepayment(AccountsController):
interest_paid -= self.total_penalty_paid
total_interest_paid = 0
# interest_paid = self.amount_paid - self.principal_amount_paid - self.penalty_amount
if self.is_term_loan:
interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
self.allocate_principal_amount_for_term_loans(interest_paid, repayment_details, updated_entries)
else:
interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
self.allocate_excess_payment_for_demand_loans(interest_paid, repayment_details)
def allocate_interest_amount(self, interest_paid, repayment_details):
updated_entries = {}
self.total_interest_paid = 0
idx = 1
if interest_paid > 0:
for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = 0
if amounts['interest_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount']
self.principal_amount_paid += paid_principal
interest_paid -= (interest_amount + paid_principal)
self.total_interest_paid += interest_amount
interest_paid -= interest_amount
elif interest_paid:
if interest_paid >= amounts['interest_amount']:
interest_amount = amounts['interest_amount']
paid_principal = interest_paid - interest_amount
self.principal_amount_paid += paid_principal
self.total_interest_paid += interest_amount
interest_paid = 0
else:
interest_amount = interest_paid
self.total_interest_paid += interest_amount
interest_paid = 0
paid_principal=0
total_interest_paid += interest_amount
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount,
'paid_principal_amount': paid_principal
})
if interest_amount:
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount,
'paid_principal_amount': 0
})
updated_entries[lia] = idx
idx += 1
return interest_paid, updated_entries
def allocate_principal_amount_for_term_loans(self, interest_paid, repayment_details, updated_entries):
if interest_paid > 0:
for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])):
paid_principal = 0
if amounts['payable_principal_amount'] <= interest_paid:
paid_principal = amounts['payable_principal_amount']
self.principal_amount_paid += paid_principal
interest_paid -= paid_principal
elif interest_paid:
if interest_paid >= amounts['payable_principal_amount']:
paid_principal = amounts['payable_principal_amount']
self.principal_amount_paid += paid_principal
interest_paid = 0
else:
paid_principal = interest_paid
self.principal_amount_paid += paid_principal
interest_paid = 0
if updated_entries.get(lia):
idx = updated_entries.get(lia)
self.get('repayment_details')[idx-1].paid_principal_amount += paid_principal
else:
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': 0,
'paid_principal_amount': paid_principal
})
if interest_paid > 0:
self.principal_amount_paid += interest_paid
def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details):
if repayment_details['unaccrued_interest'] and interest_paid > 0:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
interest_paid -= repayment_details['unaccrued_interest']
total_interest_paid += repayment_details['unaccrued_interest']
self.total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
per_day_interest = get_per_day_interest(self.pending_principal_amount,
self.rate_of_interest, self.posting_date)
no_of_days = cint(interest_paid/per_day_interest)
total_interest_paid += no_of_days * per_day_interest
self.total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
if interest_paid > 0:
self.principal_amount_paid += interest_paid
@ -364,6 +413,54 @@ def get_penalty_details(against_loan):
else:
return None, 0
def regenerate_repayment_schedule(loan):
from erpnext.loan_management.doctype.loan.loan import get_monthly_repayment_amount
loan_doc = frappe.get_doc('Loan', loan)
next_accrual_date = None
for term in reversed(loan_doc.get('repayment_schedule')):
if not term.is_accrued:
next_accrual_date = term.payment_date
if not term.is_accrued:
loan_doc.remove(term)
loan_doc.save()
if loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
balance_amount = loan_doc.total_payment - loan_doc.total_principal_paid \
- loan_doc.total_interest_payable - loan_doc.written_off_amount
else:
balance_amount = loan_doc.disbursed_amount - loan_doc.total_principal_paid \
- loan_doc.total_interest_payable - loan_doc.written_off_amount
monthly_repayment_amount = get_monthly_repayment_amount(loan_doc.loan_amount,
loan_doc.rate_of_interest, loan_doc.repayment_periods)
payment_date = next_accrual_date
while(balance_amount > 0):
interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12*100))
principal_amount = monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
loan_doc.append("repayment_schedule", {
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount
})
next_payment_date = add_months(payment_date, 1)
payment_date = next_payment_date
loan_doc.save()
# 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