From f46cfc2563e247ecc3117c5c2f8c7948f302171e Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Fri, 10 Jan 2025 18:36:33 +0530 Subject: [PATCH 1/2] fix: backdated DPD --- lending/loan_management/doctype/loan/loan.py | 79 ++++++++++--------- .../loan_management/doctype/loan/test_loan.py | 65 +++++++++++++++ .../process_loan_classification.json | 3 +- 3 files changed, 110 insertions(+), 37 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index c370260a..f00a12bd 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -3,6 +3,7 @@ import json +from datetime import date, timedelta import frappe from frappe import _ @@ -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() @@ -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 @@ -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}'" @@ -873,37 +868,49 @@ 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 + + for payment_date in daterange(getdate(payment.posting_date), getdate(next_payment_date)): + if any(d.demand_amount > 0 for d in demands): + dpd = max(0, date_diff(payment_date, getdate(demand.demand_date)) + 1) + create_dpd_record(loan, loan_disbursement, payment_date, dpd) + else: + 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 and not any(d.demand_amount > 0 for d in demands): + 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): diff --git a/lending/loan_management/doctype/loan/test_loan.py b/lending/loan_management/doctype/loan/test_loan.py index 826c5002..4fd44109 100644 --- a/lending/loan_management/doctype/loan/test_loan.py +++ b/lending/loan_management/doctype/loan/test_loan.py @@ -1464,6 +1464,71 @@ 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() + + 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, + } + + repayment_date = datetime.strptime("2024-10-09", "%Y-%m-%d").date() + + for log in dpd_logs: + posting_date = log.get("posting_date") + dpd_value = log.get("days_past_due") + + if posting_date > repayment_date: + self.assertEqual( + dpd_value, + 0, + f"Expected DPD for {posting_date} to be 0 after full repayment on 2024-10-09, but got {dpd_value}", + ) + else: + self.assertEqual( + dpd_value, + expected_dpd_values.get(str(posting_date), 0), + f"Expected DPD for {posting_date} to be {expected_dpd_values.get(str(posting_date), 0)}, but got {dpd_value}", + ) + def create_secured_demand_loan(applicant, disbursement_amount=None): frappe.db.set_value( diff --git a/lending/loan_management/doctype/process_loan_classification/process_loan_classification.json b/lending/loan_management/doctype/process_loan_classification/process_loan_classification.json index e1c1a795..3dceb8e5 100644 --- a/lending/loan_management/doctype/process_loan_classification/process_loan_classification.json +++ b/lending/loan_management/doctype/process_loan_classification/process_loan_classification.json @@ -32,6 +32,7 @@ { "fieldname": "loan", "fieldtype": "Link", + "in_list_view": 1, "in_standard_filter": 1, "label": "Loan ", "options": "Loan" @@ -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", From 412cfea28b2a9c01e1ede0333906b9c960c6614a Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Fri, 10 Jan 2025 21:43:28 +0530 Subject: [PATCH 2/2] fix: backdated DPD --- lending/loan_management/doctype/loan/loan.py | 10 +++-- .../loan_management/doctype/loan/test_loan.py | 44 ++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index f00a12bd..0d82ae1c 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -900,15 +900,17 @@ def get_oldest_outstanding_demand_date(loan, posting_date, loan_product, loan_di 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_amount > 0 for d in demands): - dpd = max(0, date_diff(payment_date, getdate(demand.demand_date)) + 1) - create_dpd_record(loan, loan_disbursement, payment_date, dpd) + 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 and not any(d.demand_amount > 0 for d in demands): + 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) diff --git a/lending/loan_management/doctype/loan/test_loan.py b/lending/loan_management/doctype/loan/test_loan.py index 4fd44109..21bcee1a 100644 --- a/lending/loan_management/doctype/loan/test_loan.py +++ b/lending/loan_management/doctype/loan/test_loan.py @@ -1491,6 +1491,14 @@ def test_dpd_calulation(self): 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 @@ -1507,27 +1515,31 @@ def test_dpd_calulation(self): "2024-10-06": 2, "2024-10-07": 3, "2024-10-08": 4, - "2024-10-09": 0, + "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.get("posting_date") - dpd_value = log.get("days_past_due") - - if posting_date > repayment_date: - self.assertEqual( - dpd_value, - 0, - f"Expected DPD for {posting_date} to be 0 after full repayment on 2024-10-09, but got {dpd_value}", - ) - else: - self.assertEqual( - dpd_value, - expected_dpd_values.get(str(posting_date), 0), - f"Expected DPD for {posting_date} to be {expected_dpd_values.get(str(posting_date), 0)}, but got {dpd_value}", - ) + 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):