Merge pull request #28034 from deepeshgarg007/term_loan_enhancement
fix: Updates in term loan processing
This commit is contained in:
commit
b1d8815556
@ -240,12 +240,14 @@
|
|||||||
"label": "Repayment Schedule"
|
"label": "Repayment Schedule"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.is_term_loan == 1",
|
"depends_on": "eval:doc.is_term_loan == 1",
|
||||||
"fieldname": "repayment_schedule",
|
"fieldname": "repayment_schedule",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Repayment Schedule",
|
"label": "Repayment Schedule",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Repayment Schedule"
|
"options": "Repayment Schedule",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_17",
|
"fieldname": "section_break_17",
|
||||||
@ -363,6 +365,7 @@
|
|||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan",
|
"name": "Loan",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@ import math
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import add_months, flt, getdate, now_datetime, nowdate
|
from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
@ -62,7 +62,7 @@ class Loan(AccountsController):
|
|||||||
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
|
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":
|
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):
|
def check_sanctioned_amount_limit(self):
|
||||||
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
|
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
|
||||||
@ -99,7 +99,7 @@ class Loan(AccountsController):
|
|||||||
"total_payment": total_payment,
|
"total_payment": total_payment,
|
||||||
"balance_loan_amount": balance_amount
|
"balance_loan_amount": balance_amount
|
||||||
})
|
})
|
||||||
next_payment_date = add_months(payment_date, 1)
|
next_payment_date = add_single_month(payment_date)
|
||||||
payment_date = next_payment_date
|
payment_date = next_payment_date
|
||||||
|
|
||||||
def set_repayment_period(self):
|
def set_repayment_period(self):
|
||||||
@ -211,7 +211,7 @@ def validate_repayment_method(repayment_method, loan_amount, monthly_repayment_a
|
|||||||
if monthly_repayment_amount > loan_amount:
|
if monthly_repayment_amount > loan_amount:
|
||||||
frappe.throw(_("Monthly Repayment Amount cannot be greater than 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:
|
if rate_of_interest:
|
||||||
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
|
monthly_interest_rate = flt(rate_of_interest) / (12 *100)
|
||||||
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
|
monthly_repayment_amount = math.ceil((loan_amount * monthly_interest_rate *
|
||||||
@ -395,3 +395,9 @@ def get_shortfall_applicants():
|
|||||||
"value": len(applicants),
|
"value": len(applicants),
|
||||||
"fieldtype": "Int"
|
"fieldtype": "Int"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def add_single_month(date):
|
||||||
|
if getdate(date) == get_last_day(date):
|
||||||
|
return get_last_day(add_months(date, 1))
|
||||||
|
else:
|
||||||
|
return add_months(date, 1)
|
@ -218,6 +218,14 @@ class TestLoan(unittest.TestCase):
|
|||||||
self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
|
self.assertEqual(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid -
|
||||||
penalty_amount - total_interest_paid, 0))
|
penalty_amount - total_interest_paid, 0))
|
||||||
|
|
||||||
|
# Check Repayment Entry cancel
|
||||||
|
repayment_entry.load_from_db()
|
||||||
|
repayment_entry.cancel()
|
||||||
|
|
||||||
|
loan.load_from_db()
|
||||||
|
self.assertEqual(loan.total_principal_paid, 0)
|
||||||
|
self.assertEqual(loan.total_principal_paid, 0)
|
||||||
|
|
||||||
def test_loan_closure(self):
|
def test_loan_closure(self):
|
||||||
pledge = [{
|
pledge = [{
|
||||||
"loan_security": "Test Security 1",
|
"loan_security": "Test Security 1",
|
||||||
@ -295,6 +303,27 @@ class TestLoan(unittest.TestCase):
|
|||||||
self.assertEqual(amounts[0], 11250.00)
|
self.assertEqual(amounts[0], 11250.00)
|
||||||
self.assertEqual(amounts[1], 78303.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), 41369.83)
|
||||||
|
self.assertEqual(flt(loan.get('repayment_schedule')[3].interest_amount, 2), 289.59)
|
||||||
|
self.assertEqual(flt(loan.get('repayment_schedule')[3].total_payment, 2), 41659.41)
|
||||||
|
self.assertEqual(flt(loan.get('repayment_schedule')[3].balance_loan_amount, 2), 0)
|
||||||
|
|
||||||
def test_security_shortfall(self):
|
def test_security_shortfall(self):
|
||||||
pledges = [{
|
pledges = [{
|
||||||
"loan_security": "Test Security 2",
|
"loan_security": "Test Security 2",
|
||||||
@ -938,18 +967,18 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
|
|||||||
|
|
||||||
|
|
||||||
def create_loan(applicant, loan_type, loan_amount, repayment_method, repayment_periods,
|
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({
|
loan = frappe.get_doc({
|
||||||
"doctype": "Loan",
|
"doctype": "Loan",
|
||||||
"applicant_type": "Employee",
|
"applicant_type": applicant_type or "Employee",
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"applicant": applicant,
|
"applicant": applicant,
|
||||||
"loan_type": loan_type,
|
"loan_type": loan_type,
|
||||||
"loan_amount": loan_amount,
|
"loan_amount": loan_amount,
|
||||||
"repayment_method": repayment_method,
|
"repayment_method": repayment_method,
|
||||||
"repayment_periods": repayment_periods,
|
"repayment_periods": repayment_periods,
|
||||||
"repayment_start_date": nowdate(),
|
"repayment_start_date": repayment_start_date or nowdate(),
|
||||||
"is_term_loan": 1,
|
"is_term_loan": 1,
|
||||||
"posting_date": posting_date or nowdate()
|
"posting_date": posting_date or nowdate()
|
||||||
})
|
})
|
||||||
|
@ -80,7 +80,7 @@ class LoanApplication(Document):
|
|||||||
|
|
||||||
if self.is_term_loan:
|
if self.is_term_loan:
|
||||||
if self.repayment_method == "Repay Over Number of Periods":
|
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":
|
if self.repayment_method == "Repay Fixed Amount per Period":
|
||||||
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
|
monthly_interest_rate = flt(self.rate_of_interest) / (12 *100)
|
||||||
|
@ -176,20 +176,19 @@ def get_total_pledged_security_value(loan):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_disbursal_amount(loan, on_current_security_price=0):
|
def get_disbursal_amount(loan, on_current_security_price=0):
|
||||||
|
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||||
|
get_pending_principal_amount,
|
||||||
|
)
|
||||||
|
|
||||||
loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
|
loan_details = frappe.get_value("Loan", loan, ["loan_amount", "disbursed_amount", "total_payment",
|
||||||
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
|
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan",
|
||||||
"maximum_loan_amount"], as_dict=1)
|
"maximum_loan_amount", "written_off_amount"], as_dict=1)
|
||||||
|
|
||||||
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
|
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
|
||||||
'status': 'Pending'}):
|
'status': 'Pending'}):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if loan_details.status == 'Disbursed':
|
pending_principal_amount = get_pending_principal_amount(loan_details)
|
||||||
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
|
|
||||||
- flt(loan_details.total_principal_paid)
|
|
||||||
else:
|
|
||||||
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
|
|
||||||
- flt(loan_details.total_principal_paid)
|
|
||||||
|
|
||||||
security_value = 0.0
|
security_value = 0.0
|
||||||
if loan_details.is_secured_loan and on_current_security_price:
|
if loan_details.is_secured_loan and on_current_security_price:
|
||||||
|
@ -74,6 +74,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:
|
if gle_map:
|
||||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
|
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
|
||||||
|
|
||||||
@ -82,7 +115,10 @@ class LoanInterestAccrual(AccountsController):
|
|||||||
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
|
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
|
||||||
# which means interest will be accrued for 30 days which should be equal to 11095.89
|
# which means interest will be accrued for 30 days which should be equal to 11095.89
|
||||||
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
|
def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest, accrual_type):
|
||||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
|
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||||
|
calculate_amounts,
|
||||||
|
get_pending_principal_amount,
|
||||||
|
)
|
||||||
|
|
||||||
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
|
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
|
||||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||||
@ -90,12 +126,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
|
|||||||
if no_of_days <= 0:
|
if no_of_days <= 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
if loan.status == 'Disbursed':
|
pending_principal_amount = get_pending_principal_amount(loan)
|
||||||
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
|
|
||||||
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
|
|
||||||
else:
|
|
||||||
pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
|
|
||||||
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
|
|
||||||
|
|
||||||
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
|
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
|
||||||
payable_interest = interest_per_day * no_of_days
|
payable_interest = interest_per_day * no_of_days
|
||||||
@ -133,7 +164,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
|
|||||||
|
|
||||||
if not open_loans:
|
if not open_loans:
|
||||||
open_loans = frappe.get_all("Loan",
|
open_loans = frappe.get_all("Loan",
|
||||||
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
|
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "loan_amount",
|
||||||
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
|
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
|
||||||
"rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
|
"rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
|
||||||
filters=query_filters)
|
filters=query_filters)
|
||||||
@ -190,7 +221,8 @@ def get_term_loans(date, term_loan=None, loan_type=None):
|
|||||||
AND l.is_term_loan =1
|
AND l.is_term_loan =1
|
||||||
AND rs.payment_date <= %s
|
AND rs.payment_date <= %s
|
||||||
AND rs.is_accrued=0 {0}
|
AND rs.is_accrued=0 {0}
|
||||||
AND l.status = 'Disbursed'""".format(condition), (getdate(date)), as_dict=1)
|
AND l.status = 'Disbursed'
|
||||||
|
ORDER BY rs.payment_date""".format(condition), (getdate(date)), as_dict=1)
|
||||||
|
|
||||||
return term_loans
|
return term_loans
|
||||||
|
|
||||||
|
@ -35,9 +35,12 @@ class LoanRepayment(AccountsController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_paid_amount()
|
self.update_paid_amount()
|
||||||
|
self.update_repayment_schedule()
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.check_future_accruals()
|
||||||
|
self.update_repayment_schedule(cancel=1)
|
||||||
self.mark_as_unpaid()
|
self.mark_as_unpaid()
|
||||||
self.ignore_linked_doctypes = ['GL Entry']
|
self.ignore_linked_doctypes = ['GL Entry']
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
@ -90,7 +93,7 @@ class LoanRepayment(AccountsController):
|
|||||||
|
|
||||||
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 flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
|
||||||
if not self.is_term_loan:
|
if not self.is_term_loan:
|
||||||
# get last loan interest accrual date
|
# get last loan interest accrual date
|
||||||
last_accrual_date = get_last_accrual_date(self.against_loan)
|
last_accrual_date = get_last_accrual_date(self.against_loan)
|
||||||
@ -121,7 +124,18 @@ class LoanRepayment(AccountsController):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def update_paid_amount(self):
|
def update_paid_amount(self):
|
||||||
loan = frappe.get_doc("Loan", self.against_loan)
|
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||||
|
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||||
|
'written_off_amount'], as_dict=1)
|
||||||
|
|
||||||
|
loan.update({
|
||||||
|
'total_amount_paid': loan.total_amount_paid + self.amount_paid,
|
||||||
|
'total_principal_paid': loan.total_principal_paid + self.principal_amount_paid
|
||||||
|
})
|
||||||
|
|
||||||
|
pending_principal_amount = get_pending_principal_amount(loan)
|
||||||
|
if not loan.is_secured_loan and pending_principal_amount <= 0:
|
||||||
|
loan.update({'status': 'Loan Closure Requested'})
|
||||||
|
|
||||||
for payment in self.repayment_details:
|
for payment in self.repayment_details:
|
||||||
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
||||||
@ -130,17 +144,31 @@ class LoanRepayment(AccountsController):
|
|||||||
WHERE name = %s""",
|
WHERE name = %s""",
|
||||||
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
|
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
|
||||||
|
|
||||||
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
|
frappe.db.sql(""" UPDATE `tabLoan`
|
||||||
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
|
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
|
||||||
loan.total_principal_paid + self.principal_amount_paid, self.against_loan))
|
WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status,
|
||||||
|
self.against_loan))
|
||||||
|
|
||||||
update_shortfall_status(self.against_loan, self.principal_amount_paid)
|
update_shortfall_status(self.against_loan, self.principal_amount_paid)
|
||||||
|
|
||||||
def mark_as_unpaid(self):
|
def mark_as_unpaid(self):
|
||||||
loan = frappe.get_doc("Loan", self.against_loan)
|
loan = frappe.get_value("Loan", self.against_loan, ['total_amount_paid', 'total_principal_paid',
|
||||||
|
'status', 'is_secured_loan', 'total_payment', 'loan_amount', 'total_interest_payable',
|
||||||
|
'written_off_amount'], as_dict=1)
|
||||||
|
|
||||||
no_of_repayments = len(self.repayment_details)
|
no_of_repayments = len(self.repayment_details)
|
||||||
|
|
||||||
|
loan.update({
|
||||||
|
'total_amount_paid': loan.total_amount_paid - self.amount_paid,
|
||||||
|
'total_principal_paid': loan.total_principal_paid - self.principal_amount_paid
|
||||||
|
})
|
||||||
|
|
||||||
|
if loan.status == 'Loan Closure Requested':
|
||||||
|
if loan.disbursed_amount >= loan.loan_amount:
|
||||||
|
loan['status'] = 'Disbursed'
|
||||||
|
else:
|
||||||
|
loan['status'] = 'Partially Disbursed'
|
||||||
|
|
||||||
for payment in self.repayment_details:
|
for payment in self.repayment_details:
|
||||||
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
||||||
SET paid_principal_amount = `paid_principal_amount` - %s,
|
SET paid_principal_amount = `paid_principal_amount` - %s,
|
||||||
@ -154,12 +182,20 @@ class LoanRepayment(AccountsController):
|
|||||||
lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
|
lia_doc = frappe.get_doc('Loan Interest Accrual', payment.loan_interest_accrual)
|
||||||
lia_doc.cancel()
|
lia_doc.cancel()
|
||||||
|
|
||||||
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
|
frappe.db.sql(""" UPDATE `tabLoan`
|
||||||
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
|
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
|
||||||
loan.total_principal_paid - self.principal_amount_paid, self.against_loan))
|
WHERE name = %s """, (loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan))
|
||||||
|
|
||||||
if loan.status == "Loan Closure Requested":
|
def check_future_accruals(self):
|
||||||
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
|
future_accrual_date = frappe.db.get_value("Loan Interest Accrual", {"posting_date": (">", self.posting_date),
|
||||||
|
"docstatus": 1, "loan": self.against_loan}, 'posting_date')
|
||||||
|
|
||||||
|
if future_accrual_date:
|
||||||
|
frappe.throw("Cannot cancel. Interest accruals already processed till {0}".format(get_datetime(future_accrual_date)))
|
||||||
|
|
||||||
|
def update_repayment_schedule(self, cancel=0):
|
||||||
|
if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount:
|
||||||
|
regenerate_repayment_schedule(self.against_loan, cancel)
|
||||||
|
|
||||||
def allocate_amounts(self, repayment_details):
|
def allocate_amounts(self, repayment_details):
|
||||||
self.set('repayment_details', [])
|
self.set('repayment_details', [])
|
||||||
@ -182,50 +218,93 @@ class LoanRepayment(AccountsController):
|
|||||||
|
|
||||||
interest_paid -= self.total_penalty_paid
|
interest_paid -= self.total_penalty_paid
|
||||||
|
|
||||||
total_interest_paid = 0
|
if self.is_term_loan:
|
||||||
# interest_paid = self.amount_paid - self.principal_amount_paid - self.penalty_amount
|
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:
|
if interest_paid > 0:
|
||||||
for lia, amounts in repayment_details.get('pending_accrual_entries', []).items():
|
for lia, amounts in repayment_details.get('pending_accrual_entries', []).items():
|
||||||
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
|
interest_amount = 0
|
||||||
|
if amounts['interest_amount'] <= interest_paid:
|
||||||
interest_amount = amounts['interest_amount']
|
interest_amount = amounts['interest_amount']
|
||||||
paid_principal = amounts['payable_principal_amount']
|
self.total_interest_paid += interest_amount
|
||||||
self.principal_amount_paid += paid_principal
|
interest_paid -= interest_amount
|
||||||
interest_paid -= (interest_amount + paid_principal)
|
|
||||||
elif interest_paid:
|
elif interest_paid:
|
||||||
if interest_paid >= amounts['interest_amount']:
|
if interest_paid >= amounts['interest_amount']:
|
||||||
interest_amount = amounts['interest_amount']
|
interest_amount = amounts['interest_amount']
|
||||||
paid_principal = interest_paid - interest_amount
|
self.total_interest_paid += interest_amount
|
||||||
self.principal_amount_paid += paid_principal
|
|
||||||
interest_paid = 0
|
interest_paid = 0
|
||||||
else:
|
else:
|
||||||
interest_amount = interest_paid
|
interest_amount = interest_paid
|
||||||
|
self.total_interest_paid += interest_amount
|
||||||
interest_paid = 0
|
interest_paid = 0
|
||||||
paid_principal=0
|
|
||||||
|
|
||||||
total_interest_paid += interest_amount
|
if interest_amount:
|
||||||
self.append('repayment_details', {
|
self.append('repayment_details', {
|
||||||
'loan_interest_accrual': lia,
|
'loan_interest_accrual': lia,
|
||||||
'paid_interest_amount': interest_amount,
|
'paid_interest_amount': interest_amount,
|
||||||
'paid_principal_amount': paid_principal
|
'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 repayment_details.get('pending_accrual_entries', []).items():
|
||||||
|
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:
|
if repayment_details['unaccrued_interest'] and interest_paid > 0:
|
||||||
# no of days for which to accrue interest
|
# no of days for which to accrue interest
|
||||||
# Interest can only be accrued for an entire day and not partial
|
# Interest can only be accrued for an entire day and not partial
|
||||||
if interest_paid > repayment_details['unaccrued_interest']:
|
if interest_paid > repayment_details['unaccrued_interest']:
|
||||||
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:
|
else:
|
||||||
# get no of days for which interest can be paid
|
# get no of days for which interest can be paid
|
||||||
per_day_interest = get_per_day_interest(self.pending_principal_amount,
|
per_day_interest = get_per_day_interest(self.pending_principal_amount,
|
||||||
self.rate_of_interest, self.posting_date)
|
self.rate_of_interest, self.posting_date)
|
||||||
|
|
||||||
no_of_days = cint(interest_paid/per_day_interest)
|
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
|
interest_paid -= no_of_days * per_day_interest
|
||||||
|
|
||||||
self.total_interest_paid = total_interest_paid
|
|
||||||
if interest_paid > 0:
|
if interest_paid > 0:
|
||||||
self.principal_amount_paid += interest_paid
|
self.principal_amount_paid += interest_paid
|
||||||
|
|
||||||
@ -361,6 +440,76 @@ def get_penalty_details(against_loan):
|
|||||||
else:
|
else:
|
||||||
return None, 0
|
return None, 0
|
||||||
|
|
||||||
|
def regenerate_repayment_schedule(loan, cancel=0):
|
||||||
|
from erpnext.loan_management.doctype.loan.loan import (
|
||||||
|
add_single_month,
|
||||||
|
get_monthly_repayment_amount,
|
||||||
|
)
|
||||||
|
|
||||||
|
loan_doc = frappe.get_doc('Loan', loan)
|
||||||
|
next_accrual_date = None
|
||||||
|
accrued_entries = 0
|
||||||
|
last_repayment_amount = 0
|
||||||
|
last_balance_amount = 0
|
||||||
|
|
||||||
|
for term in reversed(loan_doc.get('repayment_schedule')):
|
||||||
|
if not term.is_accrued:
|
||||||
|
next_accrual_date = term.payment_date
|
||||||
|
loan_doc.remove(term)
|
||||||
|
else:
|
||||||
|
accrued_entries += 1
|
||||||
|
if not last_repayment_amount:
|
||||||
|
last_repayment_amount = term.total_payment
|
||||||
|
if not last_balance_amount:
|
||||||
|
last_balance_amount = term.balance_loan_amount
|
||||||
|
|
||||||
|
loan_doc.save()
|
||||||
|
|
||||||
|
balance_amount = get_pending_principal_amount(loan_doc)
|
||||||
|
|
||||||
|
if loan_doc.repayment_method == 'Repay Fixed Amount per Period':
|
||||||
|
monthly_repayment_amount = flt(balance_amount/len(loan_doc.get('repayment_schedule')) - accrued_entries)
|
||||||
|
else:
|
||||||
|
if not cancel:
|
||||||
|
monthly_repayment_amount = get_monthly_repayment_amount(balance_amount,
|
||||||
|
loan_doc.rate_of_interest, loan_doc.repayment_periods - accrued_entries)
|
||||||
|
else:
|
||||||
|
monthly_repayment_amount = last_repayment_amount
|
||||||
|
balance_amount = last_balance_amount
|
||||||
|
|
||||||
|
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_single_month(payment_date)
|
||||||
|
payment_date = next_payment_date
|
||||||
|
|
||||||
|
loan_doc.save()
|
||||||
|
|
||||||
|
def get_pending_principal_amount(loan):
|
||||||
|
if loan.status in ('Disbursed', 'Closed') or loan.disbursed_amount >= loan.loan_amount:
|
||||||
|
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_principal_paid) \
|
||||||
|
- flt(loan.total_interest_payable) - flt(loan.written_off_amount)
|
||||||
|
else:
|
||||||
|
pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_principal_paid) \
|
||||||
|
- flt(loan.total_interest_payable) - flt(loan.written_off_amount)
|
||||||
|
|
||||||
|
return pending_principal_amount
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@ -408,12 +557,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
|||||||
if due_date and not final_due_date:
|
if due_date and not final_due_date:
|
||||||
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
|
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
|
||||||
|
|
||||||
if against_loan_doc.status in ('Disbursed', 'Closed') or against_loan_doc.disbursed_amount >= against_loan_doc.loan_amount:
|
pending_principal_amount = get_pending_principal_amount(against_loan_doc)
|
||||||
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
|
|
||||||
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
|
|
||||||
else:
|
|
||||||
pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
|
|
||||||
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
|
|
||||||
|
|
||||||
unaccrued_interest = 0
|
unaccrued_interest = 0
|
||||||
if due_date:
|
if due_date:
|
||||||
|
@ -27,6 +27,9 @@ class LoanSecurityUnpledge(Document):
|
|||||||
d.idx, frappe.bold(d.loan_security)))
|
d.idx, frappe.bold(d.loan_security)))
|
||||||
|
|
||||||
def validate_unpledge_qty(self):
|
def validate_unpledge_qty(self):
|
||||||
|
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||||
|
get_pending_principal_amount,
|
||||||
|
)
|
||||||
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
|
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
|
||||||
get_ltv_ratio,
|
get_ltv_ratio,
|
||||||
)
|
)
|
||||||
@ -43,15 +46,10 @@ class LoanSecurityUnpledge(Document):
|
|||||||
"valid_upto": (">=", get_datetime())
|
"valid_upto": (">=", get_datetime())
|
||||||
}, as_list=1))
|
}, as_list=1))
|
||||||
|
|
||||||
loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
|
loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', 'loan_amount',
|
||||||
'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
|
'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
|
||||||
|
|
||||||
if loan_details.status == 'Disbursed':
|
pending_principal_amount = get_pending_principal_amount(loan_details)
|
||||||
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
|
|
||||||
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
|
|
||||||
else:
|
|
||||||
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
|
|
||||||
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
|
|
||||||
|
|
||||||
security_value = 0
|
security_value = 0
|
||||||
unpledge_qty_map = {}
|
unpledge_qty_map = {}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user