Skip to content

Commit

Permalink
feat: Penalty accrual and calculation for loans
Browse files Browse the repository at this point in the history
  • Loading branch information
deepeshgarg007 committed Dec 15, 2023
1 parent d8f73b6 commit 84ba0c5
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 81 deletions.
5 changes: 3 additions & 2 deletions lending/loan_management/doctype/loan/loan.json
Original file line number Diff line number Diff line change
Expand Up @@ -553,13 +553,14 @@
"depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"",
"fieldname": "moratorium_type",
"fieldtype": "Select",
"label": "Moratorium Type"
"label": "Moratorium Type",
"options": "EMI\nInterest"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-12-06 20:36:53.297695",
"modified": "2023-12-12 15:28:10.241845",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
Expand Down
24 changes: 22 additions & 2 deletions lending/loan_management/doctype/loan_demand/loan_demand.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
"demand_amount",
"paid_amount",
"waived_amount",
"outstanding_amount"
"outstanding_amount",
"sales_invoice",
"last_repayment_date"
],
"fields": [
{
Expand All @@ -31,17 +33,20 @@
{
"fieldname": "demand_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Demand Date"
},
{
"fieldname": "demand_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Demand Type",
"options": "EMI\nPenalty\nNormal\nCharges"
},
{
"fieldname": "demand_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Demand Amount"
},
{
Expand All @@ -67,6 +72,7 @@
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Paid Amount"
},
{
Expand Down Expand Up @@ -109,6 +115,7 @@
{
"fieldname": "demand_subtype",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Demand Subtype",
"options": "Principal\nInterest\nPenalty\nCharges"
},
Expand All @@ -118,18 +125,30 @@
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"label": "Sales Invoice",
"options": "Sales Invoice"
},
{
"fieldname": "last_repayment_date",
"fieldtype": "Date",
"label": "Last Repayment Date"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-12-04 13:07:44.221871",
"modified": "2023-12-13 22:28:15.756117",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Demand",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
Expand All @@ -139,6 +158,7 @@
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
Expand Down
10 changes: 7 additions & 3 deletions lending/loan_management/doctype/loan_demand/loan_demand.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ def validate(self):
pass

def on_submit(self):
self.make_gl_entries()
self.update_repayment_schedule()
if self.demand_subtype in ("Principal", "Interest", "Penalty"):
self.make_gl_entries()

if self.demand_type == "EMI":
self.update_repayment_schedule()

def update_repayment_schedule(self, cancel=0):
if self.repayment_schedule_detail:
Expand All @@ -23,13 +26,14 @@ def update_repayment_schedule(self, cancel=0):
)

def on_cancel(self):
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
self.update_repayment_schedule(cancel=1)

def make_gl_entries(self, cancel=0):
gl_entries = []

if self.demand_subtype == "Principal":
if self.demand_subtype in ("Principal", "Charges"):
return

if self.demand_subtype == "Interest":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ def get_disbursed_amount(self):

return disbursed_amount

def update_draft_schedule(self):
def get_draft_schedule(self):
draft_schedule = frappe.db.get_value(
"Loan Repayment Schedule", {"loan": self.against_loan, "docstatus": 0}, "name"
)
return draft_schedule

if self.repayment_frequency == "Monthly":
def update_draft_schedule(self):
draft_schedule = self.get_draft_schedule()

if self.repayment_frequency == "Monthly" and not self.repayment_start_date:
loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product")
self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date)

Expand Down Expand Up @@ -306,6 +310,8 @@ def set_status_and_amounts(self, cancel=0):
disbursed_amount,
status,
total_payment,
total_interest_payable,
monthly_repayment_amount,
new_available_limit_amount,
new_utilized_limit_amount,
) = self.get_values_on_cancel(loan_details)
Expand Down Expand Up @@ -368,6 +374,8 @@ def get_values_on_cancel(self, loan_details):
disbursed_amount,
status,
total_payment,
0,
0,
new_available_limit_amount,
new_utilized_limit_amount,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan",
"options": "Loan"
"options": "Loan",
"reqd": 1
},
{
"fieldname": "posting_date",
Expand Down Expand Up @@ -245,7 +246,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-11-22 16:57:11.982248",
"modified": "2023-12-11 15:08:17.332488",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@

class LoanInterestAccrual(AccountsController):
def validate(self):
if not self.loan:
frappe.throw(_("Loan is mandatory"))

if not self.posting_date:
self.posting_date = nowdate()

Expand Down Expand Up @@ -139,6 +136,7 @@ def calculate_accrual_amount_for_loans(loan, posting_date, process_loan_interest
"posting_date": posting_date,
"due_date": posting_date,
"accrual_type": accrual_type,
"interest_type": "Normal Interest",
}
)

Expand All @@ -147,6 +145,45 @@ def calculate_accrual_amount_for_loans(loan, posting_date, process_loan_interest
generate_loan_demand(loan, posting_date, payable_interest)


def calculate_penal_interest_for_loans(loan, posting_date, process_loan_interest, accrual_type):
from lending.loan_management.doctype.loan_repayment.loan_repayment import get_unpaid_demands

demands = get_unpaid_demands(loan.name, posting_date)

loan_product = frappe.get_value("Loan", loan.name, "loan_product")
penal_interest_rate = frappe.get_value("Loan Product", loan_product, "penalty_interest_rate")
penal_interest_amount = 0

for demand in demands:
if demand.demand_subtype in ("Principal", "Interest"):
if getdate(demand.demand_date) < getdate(posting_date):
penal_interest_amount += (
demand.demand_amount
* penal_interest_rate
* date_diff(posting_date, demand.last_repayment_date or demand.demand_date)
/ 36500
)

args = frappe._dict(
{
"loan": loan.name,
"applicant_type": loan.applicant_type,
"applicant": loan.applicant,
"interest_income_account": loan.penalty_income_account,
"loan_account": loan.loan_account,
"interest_amount": penal_interest_amount,
"process_loan_interest": process_loan_interest,
"posting_date": posting_date,
"accrual_type": accrual_type,
"interest_type": "Penal Interest",
}
)

if penal_interest_amount > 0:
make_loan_interest_accrual_entry(args)
create_loan_demand(loan.name, posting_date, "Penalty", "Penalty", penal_interest_amount)


def make_accrual_interest_entry_for_loans(
posting_date,
process_loan_interest=None,
Expand Down Expand Up @@ -177,6 +214,7 @@ def make_accrual_interest_entry_for_loans(
"refund_amount",
"loan_account",
"interest_income_account",
"penalty_income_account",
"loan_amount",
"is_term_loan",
"status",
Expand All @@ -194,31 +232,48 @@ def make_accrual_interest_entry_for_loans(
filters=query_filters,
)

open_loans += get_term_loans(term_loan=loan, loan_product=loan_product)
open_loans += get_term_loans(term_loan=loan, loan_product=loan_product, posting_date=posting_date)

for loan in open_loans:
calculate_accrual_amount_for_loans(loan, posting_date, process_loan_interest, accrual_type)
calculate_penal_interest_for_loans(loan, posting_date, process_loan_interest, accrual_type)


def generate_loan_demand(loan, posting_date, payable_interest):
print(loan.is_term_loan, loan.payment_date, getdate(loan.payment_date), getdate(posting_date))
def generate_loan_demand(
loan, posting_date, payable_interest, demand_subtype=None, demand_type=None
):
if not loan.is_term_loan:
create_loan_demand(loan.name, posting_date, "Normal", "Interest", payable_interest)
elif (
loan.is_term_loan
and loan.get("payment_date")
and getdate(loan.get("payment_date")) <= getdate(posting_date)
elif loan.is_term_loan and (
(loan.get("payment_date") and getdate(loan.get("payment_date")) <= getdate(posting_date))
or demand_type == "Penalty"
):
create_loan_demand(
loan.name, posting_date, "EMI", "Interest", loan.interest_amount, loan.payment_entry
loan.name,
posting_date,
demand_type or "EMI",
demand_subtype or "Interest",
loan.interest_amount,
loan.payment_entry,
)
create_loan_demand(
loan.name, posting_date, "EMI", "Principal", loan.principal_amount, loan.payment_entry
loan.name,
posting_date,
demand_type or "EMI",
demand_subtype or "Principal",
loan.principal_amount,
loan.payment_entry,
)


def create_loan_demand(
loan, posting_date, demand_type, demand_subtype, amount, repayment_schedule_detail=None
loan,
posting_date,
demand_type,
demand_subtype,
amount,
repayment_schedule_detail=None,
sales_invoice=None,
):
demand = frappe.new_doc("Loan Demand")
demand.loan = loan
Expand All @@ -227,6 +282,7 @@ def create_loan_demand(
demand.demand_type = demand_type
demand.demand_subtype = demand_subtype
demand.demand_amount = amount
demand.sales_invoice = sales_invoice
demand.save()
demand.submit()

Expand Down Expand Up @@ -270,7 +326,7 @@ def create_loan_demand(
# )


def get_term_loans(term_loan=None, loan_product=None):
def get_term_loans(term_loan=None, loan_product=None, posting_date=None):
loan = frappe.qb.DocType("Loan")
loan_schedule = frappe.qb.DocType("Loan Repayment Schedule")
loan_repayment_schedule = frappe.qb.DocType("Repayment Schedule")
Expand Down Expand Up @@ -308,7 +364,7 @@ def get_term_loans(term_loan=None, loan_product=None):
& (loan.status.isin(["Disbursed", "Partially Disbursed", "Active"]))
& (loan.is_term_loan == 1)
& (loan_schedule.status == "Active")
& (loan_repayment_schedule.principal_amount > 0)
& (loan_repayment_schedule.total_payment > 0)
& (loan_repayment_schedule.demand_generated == 0)
& (loan_repayment_schedule.docstatus == 1)
)
Expand All @@ -323,7 +379,16 @@ def get_term_loans(term_loan=None, loan_product=None):

term_loans = query.run(as_dict=1)

return term_loans
considered_loans = []
filtered_loans = []

for loan in term_loans:
if loan.name not in considered_loans:
filtered_loans.append(loan)
considered_loans.append(loan.name)

print(filtered_loans)
return filtered_loans


def make_loan_interest_accrual_entry(args):
Expand All @@ -348,6 +413,7 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.accrual_type = args.accrual_type
loan_interest_accrual.due_date = args.due_date
loan_interest_accrual.interest_type = args.interest_type

loan_interest_accrual.save()
loan_interest_accrual.submit()
Expand Down
Loading

0 comments on commit 84ba0c5

Please sign in to comment.