Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: backdated DPD #231

Open
wants to merge 2 commits into
base: lending-uat
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 45 additions & 36 deletions lending/loan_management/doctype/loan/loan.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@


import json
from datetime import date, timedelta

import frappe
from frappe import _
Expand Down Expand Up @@ -735,24 +736,13 @@ def update_days_past_due_in_loans(
loan_disbursement=disbursement,
)
else:
demand_date = get_oldest_outstanding_demand_date(
get_oldest_outstanding_demand_date(
loan_name,
posting_date=posting_date,
loan_product=loan_product,
loan_disbursement=disbursement,
)

dpd_date = posting_date

if posting_date == add_days(getdate(), -1):
days_past_due = date_diff(getdate(dpd_date), getdate(demand_date)) + 1
else:
days_past_due = date_diff(getdate(dpd_date), getdate(demand_date))

if days_past_due < 0:
days_past_due = 0

create_dpd_record(loan_name, disbursement, posting_date, days_past_due)
return

threshold_map = get_dpd_threshold_map()
Expand Down Expand Up @@ -832,6 +822,12 @@ def update_days_past_due_in_loans(
create_dpd_record(loan_name, disbursement, posting_date, 0, process_loan_classification)


def daterange(start_date: date, end_date: date):
days = int((end_date - start_date).days)
for n in range(days):
yield start_date + timedelta(n)


def get_oldest_outstanding_demand_date(loan, posting_date, loan_product, loan_disbursement):
"""Get outstanding demands for a loan"""
precision = cint(frappe.db.get_default("currency_precision")) or 2
Expand All @@ -851,19 +847,18 @@ def get_oldest_outstanding_demand_date(loan, posting_date, loan_product, loan_di
WHERE loan = %s
AND docstatus = 1
AND demand_type = "EMI"
AND demand_date <= %s
{0}
GROUP BY demand_date, demand_subtype
ORDER BY demand_date
""".format(
where_conditions
),
(loan, posting_date),
(loan),
as_dict=1,
)

if demands:
first_demand_date = demands[0].demand_date
prev_demand_date = demands[0].demand_date

if loan_product:
payment_conditions += f"AND loan_product = '{loan_product}'"
Expand All @@ -873,37 +868,51 @@ def get_oldest_outstanding_demand_date(loan, posting_date, loan_product, loan_di

payment_against_demand = frappe.db.sql(
"""
SELECT SUM(principal_amount_paid) as total_principal_paid, SUM(total_interest_paid) as total_interest_paid
SELECT posting_date, SUM(principal_amount_paid) as total_principal_paid, SUM(total_interest_paid) as total_interest_paid
FROM `tabLoan Repayment`
WHERE against_loan = %s
and docstatus = 1
and posting_date BETWEEN %s AND %s
GROUP BY posting_date
ORDER BY posting_date
{0}
""".format(
payment_conditions
),
(loan, first_demand_date, posting_date),
(loan),
as_dict=1,
)

paid_principal_amount = 0
paid_interest_amount = 0
if payment_against_demand:
paid_principal_amount = flt(payment_against_demand[0].total_principal_paid)
paid_interest_amount = flt(payment_against_demand[0].total_interest_paid)

for demand in demands:
if demand.demand_subtype == "Interest":
paid_interest_amount -= demand.demand_amount
elif demand.demand_subtype == "Principal":
paid_principal_amount -= demand.demand_amount

if (
flt(paid_principal_amount, precision) < 0
or flt(paid_interest_amount, precision) < 0
or flt(demand.outstanding_amount, precision) > 0
):
return demand.demand_date
for idx, payment in enumerate(payment_against_demand):
next_payment_date = (
payment_against_demand[idx + 1].posting_date
if idx + 1 < len(payment_against_demand)
else getdate()
)
for demand in demands:
if demand.demand_date <= getdate(payment.posting_date):
if demand.demand_subtype == "Interest" and payment.total_interest_paid > 0:
paid_interest = min(payment.total_interest_paid, demand.demand_amount)
demand.demand_amount -= paid_interest
payment.total_interest_paid -= paid_interest

if demand.demand_subtype == "Principal" and payment.total_principal_paid > 0:
paid_principal = min(payment.total_principal_paid, demand.demand_amount)
demand.demand_amount -= paid_principal
payment.total_principal_paid -= paid_principal

dpd_counter = 0
for payment_date in daterange(getdate(payment.posting_date), getdate(next_payment_date)):
if any(d.demand_date <= payment_date and d.demand_amount > 0 for d in demands):
dpd_counter += 1 # Increment the DPD for active demands
create_dpd_record(loan, loan_disbursement, payment_date, dpd_counter)
else:
dpd_counter = 0 # Reset DPD when no active demands
create_dpd_record(loan, loan_disbursement, payment_date, 0)

# Ensure DPD is 0 after the last payment date if no demands exist
if idx == len(payment_against_demand) - 1:
for payment_date in daterange(getdate(next_payment_date), getdate()):
create_dpd_record(loan, loan_disbursement, payment_date, 0)


def create_loan_write_off(loan, posting_date):
Expand Down
77 changes: 77 additions & 0 deletions lending/loan_management/doctype/loan/test_loan.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,83 @@ def test_backdated_pre_payment(self):
)
repayment_entry.submit()

def test_dpd_calulation(self):
loan = create_loan(
"_Test Customer 1",
"Term Loan Product 2",
100000,
"Repay Over Number of Periods",
30,
repayment_start_date="2024-10-05",
posting_date="2024-09-15",
rate_of_interest=10,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(
loan.name, loan.loan_amount, disbursement_date="2024-09-15", repayment_start_date="2024-10-05"
)
process_daily_loan_demands(posting_date="2024-10-05", loan=loan.name)

for date in ["2024-10-05", "2024-10-06", "2024-10-07", "2024-10-08", "2024-10-09", "2024-10-10"]:
create_process_loan_classification(posting_date=date, loan=loan.name)

repayment_entry = create_repayment_entry(loan.name, "2024-10-05", 3000)
repayment_entry.submit()

repayment_entry = create_repayment_entry(loan.name, "2024-10-09", 782)
repayment_entry.submit()

process_daily_loan_demands(posting_date="2024-11-05", loan=loan.name)

repayment_entry = create_repayment_entry(loan.name, "2024-11-05", 3000)
repayment_entry.submit()

repayment_entry = create_repayment_entry(loan.name, "2024-11-10", 782)
repayment_entry.submit()

dpd_logs = frappe.db.sql(
"""
SELECT posting_date, days_past_due
FROM `tabDays Past Due Log`
WHERE loan = %s
ORDER BY posting_date
""",
(loan.name),
as_dict=1,
)

expected_dpd_values = {
"2024-10-05": 1,
"2024-10-06": 2,
"2024-10-07": 3,
"2024-10-08": 4,
"2024-10-09": 0, # Fully repaid
"2024-10-10": 0,
"2024-11-04": 0,
"2024-11-05": 1, # DPD starts again after repayment
"2024-11-06": 2,
"2024-11-07": 3,
"2024-11-08": 4,
"2024-11-09": 5,
"2024-11-10": 0, # Fully repaid
}

repayment_date = datetime.strptime("2024-10-09", "%Y-%m-%d").date()

for log in dpd_logs:
posting_date = log["posting_date"]
dpd_value = log["days_past_due"]

posting_date_str = posting_date.strftime("%Y-%m-%d")

expected_dpd = expected_dpd_values.get(posting_date_str, 0)
self.assertEqual(
dpd_value,
expected_dpd,
f"DPD mismatch for {posting_date}: Expected {expected_dpd}, got {dpd_value}",
)


def create_secured_demand_loan(applicant, disbursement_amount=None):
frappe.db.set_value(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan ",
"options": "Loan"
Expand Down Expand Up @@ -69,7 +70,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-11-13 16:41:34.462122",
"modified": "2025-01-10 13:24:29.700403",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Classification",
Expand Down
Loading