From 106d2e8f91f0f3f93a50cab0a262d3bb04ff5ed2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 13 Oct 2023 23:09:45 +0530 Subject: [PATCH 1/9] feat: Loan booking updates --- .../loan_management/doctype/loan/loan.json | 22 +++++- lending/loan_management/doctype/loan/loan.py | 60 +++++++++++++--- .../loan_charge_reference.json | 10 ++- .../loan_disbursement/loan_disbursement.py | 3 + .../loan_disbursement_charge.json | 2 +- .../loan_repayment/loan_repayment.json | 10 ++- .../loan_repayment_schedule.json | 9 ++- .../loan_repayment_schedule.py | 70 ++++++++++++------- 8 files changed, 146 insertions(+), 40 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index c1600f3c..7d68b3fd 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -32,6 +32,7 @@ "repayment_periods", "monthly_repayment_amount", "repayment_start_date", + "repayment_frequency", "is_term_loan", "loan_classification_details_section", "days_past_due", @@ -43,6 +44,8 @@ "tenure_post_restructure", "accounting_dimensions_section", "cost_center", + "loan_charges_section", + "loan_charges", "account_info", "mode_of_payment", "disbursement_account", @@ -474,12 +477,29 @@ "fieldname": "loan_classification_details_section", "fieldtype": "Section Break", "label": "Loan Classification Details" + }, + { + "fieldname": "repayment_frequency", + "fieldtype": "Select", + "label": "Repayment Frequency", + "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" + }, + { + "fieldname": "loan_charges", + "fieldtype": "Table", + "label": "Loan Charges", + "options": "Loan Disbursement Charge" + }, + { + "fieldname": "loan_charges_section", + "fieldtype": "Section Break", + "label": "Loan Charges" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-11 13:26:31.406754", + "modified": "2023-10-13 19:15:47.245190", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 2411c43c..d3db994d 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -36,6 +36,7 @@ def validate(self): self.validate_accounts() self.check_sanctioned_amount_limit() self.set_cyclic_date() + self.set_default_charge_account() if self.is_term_loan and not self.is_new(): self.update_draft_schedule() @@ -72,7 +73,10 @@ def validate_cost_center(self): frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0")) def set_cyclic_date(self): - if self.repayment_schedule_type == "Monthly as per cycle date": + if ( + self.repayment_schedule_type == "Monthly as per cycle date" + and self.repayment_frequency == "Monthly" + ): cycle_day, min_days_bw_disbursement_first_repayment = frappe.db.get_value( "Loan Product", self.loan_product, @@ -89,6 +93,20 @@ def set_cyclic_date(self): self.repayment_start_date = cyclic_date + def set_default_charge_account(self): + for charge in self.get("loan_charges"): + if not charge.account: + account = frappe.get_cached_value( + "Loan Charges", {"parent": self.loan_product, "charge_type": charge.charge}, "income_account" + ) + + if not account: + account = frappe.get_cached_value( + "Item Default", {"parent": charge.charge, "company": self.company}, "income_account" + ) + + charge.account = account + def on_submit(self): self.link_loan_security_pledge() # Interest accrual for backdated term loans @@ -150,6 +168,7 @@ def make_draft_schedule(self): "loan_product": self.loan_product, "rate_of_interest": self.rate_of_interest, "posting_date": self.posting_date, + "repayment_frequency": self.repayment_frequency, } ).insert() @@ -169,6 +188,7 @@ def update_draft_schedule(self): "posting_date": self.posting_date, "loan_amount": self.loan_amount, "monthly_repayment_amount": self.monthly_repayment_amount, + "repayment_frequency": self.repayment_frequency, } ) schedule.save() @@ -389,16 +409,36 @@ def close_loan(loan, total_amount_paid): @frappe.whitelist() -def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0): +def make_loan_disbursement( + loan, + disbursement_amount=0, + as_dict=0, + submit=0, + posting_date=None, + disbursement_date=None, + bank_account=None, +): + loan_doc = frappe.get_doc("Loan", loan) disbursement_entry = frappe.new_doc("Loan Disbursement") - disbursement_entry.against_loan = loan - disbursement_entry.applicant_type = applicant_type - disbursement_entry.applicant = applicant - disbursement_entry.company = company - disbursement_entry.disbursement_date = nowdate() - disbursement_entry.posting_date = nowdate() - - disbursement_entry.disbursed_amount = pending_amount + disbursement_entry.against_loan = loan_doc.name + disbursement_entry.applicant_type = loan_doc.applicant_type + disbursement_entry.applicant = loan_doc.applicant + disbursement_entry.company = loan_doc.company + disbursement_entry.disbursement_date = posting_date or nowdate() + disbursement_entry.posting_date = disbursement_date or nowdate() + disbursement_entry.bank_account = bank_account + + disbursement_entry.disbursed_amount = disbursement_amount + + for charge in loan_doc.get("loan_charges"): + disbursement_entry.append( + "loan_disbursement_charges", + {"charge": charge.charge, "amount": charge.amount, "account": charge.account}, + ) + + if submit: + disbursement_entry.submit() + if as_dict: return disbursement_entry.as_dict() else: diff --git a/lending/loan_management/doctype/loan_charge_reference/loan_charge_reference.json b/lending/loan_management/doctype/loan_charge_reference/loan_charge_reference.json index 86d27d79..07a8e644 100644 --- a/lending/loan_management/doctype/loan_charge_reference/loan_charge_reference.json +++ b/lending/loan_management/doctype/loan_charge_reference/loan_charge_reference.json @@ -7,6 +7,7 @@ "engine": "InnoDB", "field_order": [ "sales_invoice", + "charge", "pending_charge_amount", "allocated_amount" ], @@ -29,12 +30,19 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated Amount" + }, + { + "fieldname": "charge", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Charge", + "options": "Item" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-16 09:14:13.972294", + "modified": "2023-10-11 16:50:15.722251", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Charge Reference", diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index bcf178d4..9b5cc6c2 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -66,6 +66,9 @@ def set_missing_values(self): if not self.posting_date: self.posting_date = self.disbursement_date or nowdate() + if not self.disbursement_account and self.bank_account: + self.disbursement_account = frappe.db.get_value("Bank Account", self.bank_account, "account") + def withheld_security_deposit(self): if self.withhold_security_deposit: sd = frappe.get_doc( diff --git a/lending/loan_management/doctype/loan_disbursement_charge/loan_disbursement_charge.json b/lending/loan_management/doctype/loan_disbursement_charge/loan_disbursement_charge.json index 094e71bc..fea8318b 100644 --- a/lending/loan_management/doctype/loan_disbursement_charge/loan_disbursement_charge.json +++ b/lending/loan_management/doctype/loan_disbursement_charge/loan_disbursement_charge.json @@ -35,7 +35,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-10 19:47:07.918342", + "modified": "2023-10-11 19:47:07.918342", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement Charge", diff --git a/lending/loan_management/doctype/loan_repayment/loan_repayment.json b/lending/loan_management/doctype/loan_repayment/loan_repayment.json index 6b05fd82..8970fccd 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.json @@ -11,6 +11,7 @@ "applicant", "loan_product", "repayment_type", + "select_charge_manually", "loan_restructure", "column_break_3", "company", @@ -373,12 +374,19 @@ "label": "Loan Restructure", "options": "Loan Restructure", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.repayment_type == \"Charges Waiver\"", + "fieldname": "select_charge_manually", + "fieldtype": "Check", + "label": "Select Charge Manually" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-11 09:25:15.123899", + "modified": "2023-10-11 17:45:09.856637", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index f308e706..e6873452 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -16,6 +16,7 @@ "adjusted_interest", "column_break_n6iy", "loan_product", + "repayment_frequency", "repayment_schedule_type", "repayment_method", "repayment_periods", @@ -143,12 +144,18 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "repayment_frequency", + "fieldtype": "Select", + "label": "Repayment Frequency", + "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-02 22:14:24.172876", + "modified": "2023-10-13 16:55:43.615976", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index 0e5a2aea..ca56501c 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -45,6 +45,7 @@ def make_repayment_schedule(self): interest_amount, principal_amount, balance_amount, total_payment, days = self.get_amounts( payment_date, balance_amount, + self.repayment_frequency, schedule_type_details.repayment_schedule_type, schedule_type_details.repayment_date_on, broken_period_interest_days, @@ -78,11 +79,16 @@ def make_repayment_schedule(self): schedule_type_details.repayment_schedule_type in ["Monthly as per repayment start date", "Monthly as per cycle date"] or schedule_type_details.repayment_date_on == "End of the current month" - ): + ) and self.repayment_frequency == "Monthly": next_payment_date = add_single_month(payment_date) payment_date = next_payment_date + elif self.repayment_frequency == "Weekly": + payment_date = add_days(payment_date, 7) + elif self.repayment_frequency == "Daily": + payment_date = add_days(payment_date, 1) + elif self.repayment_type == "Quarterly": + payment_date = add_months(payment_date, 3) - bmi_days = 0 carry_forward_interest = 0 def validate_repayment_method(self): @@ -99,36 +105,50 @@ def get_amounts( self, payment_date, balance_amount, + repayment_frequency, schedule_type, repayment_date_on, additional_days, carry_forward_interest=0, ): - if schedule_type == "Monthly as per repayment start date": + if repayment_frequency == "Monthly": + if schedule_type == "Monthly as per repayment start date": + days = 1 + months = 12 + else: + expected_payment_date = get_last_day(payment_date) + if repayment_date_on == "Start of the next month": + expected_payment_date = add_days(expected_payment_date, 1) + + if schedule_type == "Monthly as per cycle date": + days = date_diff(payment_date, add_months(payment_date, -1)) + if additional_days < 0: + days = date_diff(self.repayment_start_date, self.posting_date) + additional_days = 0 + + months = 365 + if additional_days: + days += additional_days + additional_days = 0 + elif expected_payment_date == payment_date: + # using 30 days for calculating interest for all full months + days = 30 + months = 365 + else: + days = date_diff(get_last_day(payment_date), payment_date) + months = 365 + elif repayment_frequency == "Weekly": + days = 7 + months = 52 + elif repayment_frequency == "Daily": days = 1 + months = 365 + elif repayment_frequency == "Quarterly": + days = 3 months = 12 - else: - expected_payment_date = get_last_day(payment_date) - if repayment_date_on == "Start of the next month": - expected_payment_date = add_days(expected_payment_date, 1) - - if schedule_type == "Monthly as per cycle date": - days = date_diff(payment_date, add_months(payment_date, -1)) - if additional_days < 0: - days = date_diff(self.repayment_start_date, self.posting_date) - additional_days = 0 - - months = 365 - if additional_days: - days += additional_days - additional_days = 0 - elif expected_payment_date == payment_date: - # using 30 days for calculating interest for all full months - days = 30 - months = 365 - else: - days = date_diff(get_last_day(payment_date), payment_date) - months = 365 + elif repayment_frequency == "One Time": + days = date_diff(self.repayment_start_date, self.posting_date) + months = 365 interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) principal_amount = self.monthly_repayment_amount - flt(interest_amount) From 3270617475451980b3743fa5aaa15abd98256913 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 26 Oct 2023 12:06:18 +0530 Subject: [PATCH 2/9] chore: Moratorium in loan schedule --- .../loan_management/doctype/loan/loan.json | 18 +++- lending/loan_management/doctype/loan/loan.py | 4 + .../loan_repayment_schedule.json | 24 ++++- .../loan_repayment_schedule.py | 95 +++++++++++-------- 4 files changed, 98 insertions(+), 43 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index 7d68b3fd..027e0cbe 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -33,6 +33,8 @@ "monthly_repayment_amount", "repayment_start_date", "repayment_frequency", + "moratorium_tenure", + "treatment_of_interest", "is_term_loan", "loan_classification_details_section", "days_past_due", @@ -157,7 +159,6 @@ "fieldname": "rate_of_interest", "fieldtype": "Percent", "label": "Rate of Interest (%) / Year", - "read_only": 1, "reqd": 1 }, { @@ -494,12 +495,25 @@ "fieldname": "loan_charges_section", "fieldtype": "Section Break", "label": "Loan Charges" + }, + { + "depends_on": "moratorium_tenure", + "fieldname": "treatment_of_interest", + "fieldtype": "Select", + "label": "Treatment of Interest", + "mandatory_depends_on": "moratorium_tenure", + "options": "Capitalize\nAdd to first repayment" + }, + { + "fieldname": "moratorium_tenure", + "fieldtype": "Int", + "label": "Moratorium Tenure" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-13 19:15:47.245190", + "modified": "2023-10-25 10:03:51.322866", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index d3db994d..e62142ee 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -169,6 +169,8 @@ def make_draft_schedule(self): "rate_of_interest": self.rate_of_interest, "posting_date": self.posting_date, "repayment_frequency": self.repayment_frequency, + "moratorium_tenure": self.moratorium_tenure, + "treatment_of_interest": self.treatment_of_interest, } ).insert() @@ -189,6 +191,8 @@ def update_draft_schedule(self): "loan_amount": self.loan_amount, "monthly_repayment_amount": self.monthly_repayment_amount, "repayment_frequency": self.repayment_frequency, + "moratorium_tenure": self.moratorium_tenure, + "treatment_of_interest": self.treatment_of_interest, } ) schedule.save() diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index e6873452..fe1358ba 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -18,10 +18,13 @@ "loan_product", "repayment_frequency", "repayment_schedule_type", + "repayment_date_on", "repayment_method", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", + "moratorium_tenure", + "treatment_of_interest", "section_break_6rpg", "repayment_schedule", "status", @@ -150,12 +153,31 @@ "fieldtype": "Select", "label": "Repayment Frequency", "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" + }, + { + "fieldname": "treatment_of_interest", + "fieldtype": "Select", + "label": "Treatment Of Interest", + "options": "Capitalize\nAdd to first repayment" + }, + { + "depends_on": "eval:doc.repayment_schedule_type==\"Pro-rated calendar months\"", + "fetch_from": "loan_product.repayment_date_on", + "fieldname": "repayment_date_on", + "fieldtype": "Select", + "label": "Repayment Date On", + "options": "Start of the next month\nEnd of the current month" + }, + { + "fieldname": "moratorium_tenure", + "fieldtype": "Int", + "label": "Moratorium Tenure" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-13 16:55:43.615976", + "modified": "2023-10-25 10:08:14.519533", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index ca56501c..a087a8be 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -31,30 +31,43 @@ def make_repayment_schedule(self): if not self.repayment_start_date: frappe.throw(_("Repayment Start Date is mandatory for term loans")) - schedule_type_details = frappe.db.get_value( - "Loan Product", self.loan_product, ["repayment_schedule_type", "repayment_date_on"], as_dict=1 - ) - self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) carry_forward_interest = self.adjusted_interest + moratorium_interest = 0 while balance_amount > 0: + if self.is_moratorium_applicable and getdate(payment_date) > getdate(self.moratorium_end_date): + if self.treatment_of_interest == "Capitalize" and moratorium_interest: + balance_amount = self.loan_amount + moratorium_interest + self.monthly_repayment_amount = get_monthly_repayment_amount( + balance_amount, self.rate_of_interest, self.repayment_periods + ) + moratorium_interest = 0 + interest_amount, principal_amount, balance_amount, total_payment, days = self.get_amounts( payment_date, balance_amount, - self.repayment_frequency, - schedule_type_details.repayment_schedule_type, - schedule_type_details.repayment_date_on, broken_period_interest_days, carry_forward_interest, ) - if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months": + if self.is_moratorium_applicable: + if getdate(payment_date) <= getdate(self.moratorium_end_date): + total_payment = 0 + moratorium_interest += interest_amount + elif self.treatment_of_interest == "Add to first repayment" and moratorium_interest: + if moratorium_interest + interest_amount <= total_payment: + interest_amount += moratorium_interest + principal_amount = total_payment - interest_amount + balance_amount = self.loan_amount - principal_amount + moratorium_interest = 0 + + if self.repayment_schedule_type == "Pro-rated calendar months": next_payment_date = get_last_day(payment_date) - if schedule_type_details.repayment_date_on == "Start of the next month": + if self.repayment_date_on == "Start of the next month": next_payment_date = add_days(next_payment_date, 1) payment_date = next_payment_date @@ -66,6 +79,7 @@ def make_repayment_schedule(self): if ( self.repayment_method == "Repay Over Number of Periods" and len(self.get("repayment_schedule")) >= self.repayment_periods + and not self.is_moratorium_applicable ): self.get("repayment_schedule")[-1].principal_amount += balance_amount self.get("repayment_schedule")[-1].balance_loan_amount = 0 @@ -76,9 +90,9 @@ def make_repayment_schedule(self): balance_amount = 0 if ( - schedule_type_details.repayment_schedule_type + self.repayment_schedule_type in ["Monthly as per repayment start date", "Monthly as per cycle date"] - or schedule_type_details.repayment_date_on == "End of the current month" + or self.repayment_date_on == "End of the current month" ) and self.repayment_frequency == "Monthly": next_payment_date = add_single_month(payment_date) payment_date = next_payment_date @@ -86,10 +100,11 @@ def make_repayment_schedule(self): payment_date = add_days(payment_date, 7) elif self.repayment_frequency == "Daily": payment_date = add_days(payment_date, 1) - elif self.repayment_type == "Quarterly": + elif self.repayment_frequency == "Quarterly": payment_date = add_months(payment_date, 3) carry_forward_interest = 0 + broken_period_interest_days = 0 def validate_repayment_method(self): if self.repayment_method == "Repay Over Number of Periods" and not self.repayment_periods: @@ -105,64 +120,64 @@ def get_amounts( self, payment_date, balance_amount, - repayment_frequency, - schedule_type, - repayment_date_on, additional_days, carry_forward_interest=0, ): - if repayment_frequency == "Monthly": - if schedule_type == "Monthly as per repayment start date": + days, months = self.get_days_and_months(payment_date, additional_days) + interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) + principal_amount = self.monthly_repayment_amount - flt(interest_amount) + balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) + if balance_amount < 0: + principal_amount += balance_amount + balance_amount = 0.0 + + if carry_forward_interest: + interest_amount += carry_forward_interest + + total_payment = principal_amount + interest_amount + + return interest_amount, principal_amount, balance_amount, total_payment, days + + def get_days_and_months(self, payment_date, additional_days): + if self.repayment_frequency == "Monthly": + if self.repayment_schedule_type == "Monthly as per repayment start date": days = 1 months = 12 else: + months = 365 expected_payment_date = get_last_day(payment_date) - if repayment_date_on == "Start of the next month": + if self.repayment_date_on == "Start of the next month": expected_payment_date = add_days(expected_payment_date, 1) - if schedule_type == "Monthly as per cycle date": - days = date_diff(payment_date, add_months(payment_date, -1)) + if self.repayment_schedule_type == "Monthly as per cycle date": + days = date_diff(payment_date, add_months(payment_date, -1)) + 1 if additional_days < 0: days = date_diff(self.repayment_start_date, self.posting_date) additional_days = 0 - months = 365 if additional_days: days += additional_days additional_days = 0 elif expected_payment_date == payment_date: # using 30 days for calculating interest for all full months days = 30 - months = 365 else: days = date_diff(get_last_day(payment_date), payment_date) - months = 365 - elif repayment_frequency == "Weekly": + + elif self.repayment_frequency == "Weekly": days = 7 months = 52 - elif repayment_frequency == "Daily": + elif self.repayment_frequency == "Daily": days = 1 months = 365 - elif repayment_frequency == "Quarterly": + elif self.repayment_frequency == "Quarterly": days = 3 months = 12 - elif repayment_frequency == "One Time": + elif self.repayment_frequency == "One Time": days = date_diff(self.repayment_start_date, self.posting_date) months = 365 - interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) - principal_amount = self.monthly_repayment_amount - flt(interest_amount) - balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) - if balance_amount < 0: - principal_amount += balance_amount - balance_amount = 0.0 - - if carry_forward_interest: - interest_amount += carry_forward_interest - - total_payment = principal_amount + interest_amount - - return interest_amount, principal_amount, balance_amount, total_payment, days + return days, months def add_repayment_schedule_row( self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount, days From ed03919f3aa1315b00bff54bb41ba9159a3141a9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 27 Oct 2023 17:51:16 +0530 Subject: [PATCH 3/9] feat: Moratorium in loan repayment schedule --- .../loan_management/doctype/loan/loan.json | 2 +- lending/loan_management/doctype/loan/loan.py | 4 +++ .../loan_repayment_schedule.py | 27 ++++++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index 027e0cbe..d56080ac 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -513,7 +513,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-25 10:03:51.322866", + "modified": "2023-10-27 10:03:51.322866", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index e62142ee..c456a3be 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -9,6 +9,7 @@ from frappe.query_builder import Order from frappe.utils import ( add_days, + add_months, cint, date_diff, flt, @@ -93,6 +94,9 @@ def set_cyclic_date(self): self.repayment_start_date = cyclic_date + if self.moratorium_tenure: + self.repayment_start_date = add_months(self.repayment_start_date, self.moratorium_tenure) + def set_default_charge_account(self): for charge in self.get("loan_charges"): if not charge.account: diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index a087a8be..51a0a388 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, add_months, date_diff, flt, get_last_day, getdate +from frappe.utils import add_days, add_months, cint, date_diff, flt, get_last_day, getdate class LoanRepaymentSchedule(Document): @@ -38,8 +38,13 @@ def make_repayment_schedule(self): carry_forward_interest = self.adjusted_interest moratorium_interest = 0 + if self.moratorium_tenure: + payment_date = add_months(self.repayment_start_date, -1 * self.moratorium_tenure) + moratorium_end_date = add_months(self.repayment_start_date, -1) + broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + 1 + while balance_amount > 0: - if self.is_moratorium_applicable and getdate(payment_date) > getdate(self.moratorium_end_date): + if self.moratorium_tenure and getdate(payment_date) > getdate(moratorium_end_date): if self.treatment_of_interest == "Capitalize" and moratorium_interest: balance_amount = self.loan_amount + moratorium_interest self.monthly_repayment_amount = get_monthly_repayment_amount( @@ -54,9 +59,10 @@ def make_repayment_schedule(self): carry_forward_interest, ) - if self.is_moratorium_applicable: - if getdate(payment_date) <= getdate(self.moratorium_end_date): + if self.moratorium_tenure: + if getdate(payment_date) <= getdate(moratorium_end_date): total_payment = 0 + balance_amount = self.loan_amount moratorium_interest += interest_amount elif self.treatment_of_interest == "Add to first repayment" and moratorium_interest: if moratorium_interest + interest_amount <= total_payment: @@ -76,11 +82,9 @@ def make_repayment_schedule(self): payment_date, principal_amount, interest_amount, total_payment, balance_amount, days ) - if ( - self.repayment_method == "Repay Over Number of Periods" - and len(self.get("repayment_schedule")) >= self.repayment_periods - and not self.is_moratorium_applicable - ): + if self.repayment_method == "Repay Over Number of Periods" and len( + self.get("repayment_schedule") + ) >= self.repayment_periods + cint(self.moratorium_tenure): self.get("repayment_schedule")[-1].principal_amount += balance_amount self.get("repayment_schedule")[-1].balance_loan_amount = 0 self.get("repayment_schedule")[-1].total_payment = ( @@ -150,9 +154,9 @@ def get_days_and_months(self, payment_date, additional_days): expected_payment_date = add_days(expected_payment_date, 1) if self.repayment_schedule_type == "Monthly as per cycle date": - days = date_diff(payment_date, add_months(payment_date, -1)) + 1 + days = date_diff(payment_date, add_months(payment_date, -1)) if additional_days < 0: - days = date_diff(self.repayment_start_date, self.posting_date) + days = date_diff(payment_date, self.posting_date) additional_days = 0 if additional_days: @@ -163,7 +167,6 @@ def get_days_and_months(self, payment_date, additional_days): days = 30 else: days = date_diff(get_last_day(payment_date), payment_date) - elif self.repayment_frequency == "Weekly": days = 7 months = 52 From 6ef0d60ab21ab537a28a8f464eb3bbf691d1f351 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 3 Nov 2023 12:04:55 +0530 Subject: [PATCH 4/9] feat: Line of credit loans --- .../loan_management/doctype/loan/loan.json | 40 ++++- lending/loan_management/doctype/loan/loan.py | 145 ++++++++++++------ .../loan_disbursement/loan_disbursement.js | 2 +- .../loan_disbursement/loan_disbursement.json | 36 ++++- .../loan_disbursement/loan_disbursement.py | 128 +++++++++++++++- .../loan_disbursement_dashboard.py | 2 +- .../doctype/loan_product/loan_product.json | 4 +- .../doctype/loan_repayment/loan_repayment.py | 2 +- .../loan_repayment_schedule.json | 17 +- .../loan_repayment_schedule.py | 42 +++-- 10 files changed, 341 insertions(+), 77 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index d56080ac..a8e78dcd 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -36,6 +36,12 @@ "moratorium_tenure", "treatment_of_interest", "is_term_loan", + "loan_credit_limits_section", + "limit_applicable_start", + "minimum_limit_amount", + "column_break_foeo", + "limit_applicable_end", + "maximum_limit_amount", "loan_classification_details_section", "days_past_due", "classification_code", @@ -138,7 +144,7 @@ "in_standard_filter": 1, "label": "Status", "no_copy": 1, - "options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed", + "options": "Sanctioned\nPartially Disbursed\nDisbursed\nActive\nLoan Closure Requested\nClosed", "read_only": 1 }, { @@ -198,6 +204,7 @@ "fieldname": "monthly_repayment_amount", "fieldtype": "Currency", "label": "Monthly Repayment Amount", + "no_copy": 1, "options": "Company:company:default_currency" }, { @@ -508,12 +515,41 @@ "fieldname": "moratorium_tenure", "fieldtype": "Int", "label": "Moratorium Tenure" + }, + { + "fieldname": "loan_credit_limits_section", + "fieldtype": "Section Break", + "label": "Loan Credit Limits" + }, + { + "fieldname": "limit_applicable_start", + "fieldtype": "Date", + "label": "Limit Applicable Start" + }, + { + "fieldname": "minimum_limit_amount", + "fieldtype": "Currency", + "label": "Minimum Limit Amount" + }, + { + "fieldname": "column_break_foeo", + "fieldtype": "Column Break" + }, + { + "fieldname": "limit_applicable_end", + "fieldtype": "Date", + "label": "Limit Applicable End" + }, + { + "fieldname": "maximum_limit_amount", + "fieldtype": "Currency", + "label": "Maximum Limit Amount" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-27 10:03:51.322866", + "modified": "2023-11-02 20:30:24.141977", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index c456a3be..38bc5446 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -39,15 +39,35 @@ def validate(self): self.set_cyclic_date() self.set_default_charge_account() - if self.is_term_loan and not self.is_new(): - self.update_draft_schedule() + if self.is_term_loan and not self.is_new() and self.repayment_schedule_type != "Line of Credit": + update_draft_schedule( + self.name, + self.repayment_method, + self.repayment_start_date, + self.repayment_periods, + self.monthly_repayment_amount, + self.posting_date, + self.repayment_frequency, + moratorium_tenure=self.moratorium_tenure, + treatment_of_interest=self.treatment_of_interest, + ) if not self.is_term_loan or (self.is_term_loan and not self.is_new()): self.calculate_totals() def after_insert(self): - if self.is_term_loan: - self.make_draft_schedule() + if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": + make_draft_schedule( + self.name, + self.repayment_method, + self.repayment_start_date, + self.repayment_periods, + self.monthly_repayment_amount, + self.posting_date, + self.repayment_frequency, + moratorium_tenure=self.moratorium_tenure, + treatment_of_interest=self.treatment_of_interest, + ) self.calculate_totals(on_insert=True) def validate_accounts(self): @@ -115,7 +135,9 @@ def on_submit(self): self.link_loan_security_pledge() # Interest accrual for backdated term loans self.accrue_loan_interest() - self.submit_draft_schedule() + + if self.repayment_schedule_type != "Line of Credit": + self.submit_draft_schedule() def on_cancel(self): self.unlink_loan_security_pledge() @@ -159,48 +181,6 @@ def check_sanctioned_amount_limit(self): ) ) - def make_draft_schedule(self): - frappe.get_doc( - { - "doctype": "Loan Repayment Schedule", - "loan": self.name, - "repayment_method": self.repayment_method, - "repayment_start_date": self.repayment_start_date, - "repayment_periods": self.repayment_periods, - "loan_amount": self.loan_amount, - "monthly_repayment_amount": self.monthly_repayment_amount, - "loan_product": self.loan_product, - "rate_of_interest": self.rate_of_interest, - "posting_date": self.posting_date, - "repayment_frequency": self.repayment_frequency, - "moratorium_tenure": self.moratorium_tenure, - "treatment_of_interest": self.treatment_of_interest, - } - ).insert() - - def update_draft_schedule(self): - draft_schedule = frappe.db.get_value( - "Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}, "name" - ) - if draft_schedule: - schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) - schedule.update( - { - "loan": self.name, - "loan_product": self.loan_product, - "repayment_periods": self.repayment_periods, - "repayment_method": self.repayment_method, - "repayment_start_date": self.repayment_start_date, - "posting_date": self.posting_date, - "loan_amount": self.loan_amount, - "monthly_repayment_amount": self.monthly_repayment_amount, - "repayment_frequency": self.repayment_frequency, - "moratorium_tenure": self.moratorium_tenure, - "treatment_of_interest": self.treatment_of_interest, - } - ) - schedule.save() - def submit_draft_schedule(self): draft_schedule = frappe.db.get_value( "Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}, "name" @@ -222,7 +202,7 @@ def calculate_totals(self, on_insert=False): self.total_interest_payable = 0 self.total_amount_paid = 0 - if self.is_term_loan: + if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": schedule = frappe.get_doc("Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}) for data in schedule.repayment_schedule: self.total_payment += data.total_payment @@ -246,7 +226,7 @@ def validate_loan_amount(self): msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_amount) frappe.throw(msg) - if not self.loan_amount: + if not self.loan_amount and self.repayment_schedule_type != "Line of Credit": frappe.throw(_("Loan amount is mandatory")) def link_loan_security_pledge(self): @@ -962,3 +942,70 @@ def move_unpaid_interest_to_suspense_ledger( jv.flags.ignore_mandatory = True jv.submit() + + +def make_draft_schedule( + loan, + repayment_method, + start_date, + repayment_periods, + frequency_repayment_amount, + posting_date, + repayment_frequency, + disbursed_amount=None, + moratorium_tenure=None, + treatment_of_interest=None, + loan_disbursement=None, +): + frappe.get_doc( + { + "doctype": "Loan Repayment Schedule", + "loan": loan, + "repayment_method": repayment_method, + "repayment_start_date": start_date, + "repayment_periods": repayment_periods, + "monthly_repayment_amount": frequency_repayment_amount, + "posting_date": posting_date, + "repayment_frequency": repayment_frequency, + "disbursed_amount": disbursed_amount, + "moratorium_tenure": moratorium_tenure, + "treatment_of_interest": treatment_of_interest, + "loan_disbursement": loan_disbursement, + } + ).insert() + + +def update_draft_schedule( + loan, + repayment_method, + start_date, + repayment_periods, + frequency_repayment_amount, + posting_date, + repayment_frequency, + disbursed_amount=None, + moratorium_tenure=None, + treatment_of_interest=None, + loan_disbursement=None, +): + draft_schedule = frappe.db.get_value( + "Loan Repayment Schedule", {"loan": loan, "docstatus": 0}, "name" + ) + if draft_schedule: + schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) + schedule.update( + { + "loan": loan, + "repayment_periods": repayment_periods, + "repayment_method": repayment_method, + "repayment_start_date": start_date, + "posting_date": posting_date, + "monthly_repayment_amount": frequency_repayment_amount, + "repayment_frequency": repayment_frequency, + "disbursed_amount": disbursed_amount, + "moratorium_tenure": moratorium_tenure, + "treatment_of_interest": treatment_of_interest, + "loan_disbursement": loan_disbursement, + } + ) + schedule.save() diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js index 6af9c049..d7b8a8b7 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js @@ -5,7 +5,7 @@ lending.common.setup_filters("Loan Disbursement"); frappe.ui.form.on('Loan Disbursement', { setup(frm) { - frm.ignore_doctypes_on_cancel_all = ["Loan Security Deposit"]; + frm.ignore_doctypes_on_cancel_all = ["Loan Security Deposit", "Loan Repayment Schedule"]; }, refresh: function(frm) { frm.set_query('against_loan', function() { diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json index e2d13870..57cb13c6 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -11,9 +11,14 @@ "applicant_type", "loan_product", "monthly_repayment_amount", + "tenure", "column_break_4", "company", "applicant", + "repayment_schedule_type", + "repayment_frequency", + "repayment_method", + "repayment_start_date", "is_term_loan", "withhold_security_deposit", "section_break_7", @@ -226,12 +231,41 @@ "fieldtype": "Table", "label": "Loan Disbursement Charges", "options": "Loan Disbursement Charge" + }, + { + "fieldname": "repayment_frequency", + "fieldtype": "Select", + "label": "Repayment Frequency", + "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" + }, + { + "fetch_from": "against_loan.repayment_schedule_type", + "fieldname": "repayment_schedule_type", + "fieldtype": "Data", + "label": "Repayment Schedule Type" + }, + { + "fieldname": "repayment_start_date", + "fieldtype": "Date", + "label": "Repayment Start Date" + }, + { + "fieldname": "tenure", + "fieldtype": "Int", + "label": "Tenure", + "mandatory_depends_on": "eval:doc.repayment_schedule_type==\"Line of Credit\"" + }, + { + "fieldname": "repayment_method", + "fieldtype": "Select", + "label": "Repayment Method", + "options": "Repay Over Number of Periods\nRepay Fixed Amount per Period" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-10 19:19:44.826241", + "modified": "2023-11-02 15:16:01.606208", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index 9b5cc6c2..f8b69b8f 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -4,12 +4,22 @@ import frappe from frappe import _ -from frappe.utils import add_days, flt, get_datetime, nowdate +from frappe.utils import ( + add_days, + cint, + date_diff, + flt, + get_datetime, + get_last_day, + getdate, + nowdate, +) import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController +from lending.loan_management.doctype.loan.loan import make_draft_schedule, update_draft_schedule from lending.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( get_pledged_security_qty, ) @@ -22,15 +32,67 @@ class LoanDisbursement(AccountsController): def validate(self): self.set_missing_values() self.validate_disbursal_amount() + if self.repayment_schedule_type == "Line of Credit": + self.set_cyclic_date() + + if self.is_term_loan and not self.is_new() and self.repayment_schedule_type == "Line of Credit": + update_draft_schedule( + self.against_loan, + self.repayment_method, + self.repayment_start_date, + self.tenure, + self.monthly_repayment_amount, + self.posting_date, + self.repayment_frequency, + disbursed_amount=self.disbursed_amount, + loan_disbursement=self.name, + ) + + def after_insert(self): + if self.is_term_loan and self.repayment_schedule_type == "Line of Credit": + make_draft_schedule( + self.against_loan, + self.repayment_method, + self.repayment_start_date, + self.tenure, + self.monthly_repayment_amount, + self.posting_date, + self.repayment_frequency, + disbursed_amount=self.disbursed_amount, + loan_disbursement=self.name, + ) def on_submit(self): if self.is_term_loan: + self.submit_repayment_schedule() self.update_repayment_schedule_status() self.set_status_and_amounts() self.withheld_security_deposit() self.make_gl_entries() + def submit_repayment_schedule(self): + if self.repayment_schedule_type == "Line of Credit": + filters = { + "loan": self.against_loan, + "docstatus": 0, + "status": "Initiated", + "loan_disbursement": self.name, + } + schedule = frappe.get_doc("Loan Repayment Schedule", filters) + schedule.submit() + + def cancel_and_delete_repayment_schedule(self): + if self.repayment_schedule_type == "Line of Credit": + filters = { + "loan": self.against_loan, + "docstatus": 1, + "status": "Active", + "loan_disbursement": self.name, + } + schedule = frappe.get_doc("Loan Repayment Schedule", filters) + schedule.cancel() + def update_repayment_schedule_status(self, cancel=0): if cancel: status = "Initiated" @@ -39,16 +101,17 @@ def update_repayment_schedule_status(self, cancel=0): status = "Active" current_status = "Initiated" + filters = {"loan": self.against_loan, "docstatus": 1, "status": current_status} schedule = frappe.db.get_value( "Loan Repayment Schedule", - {"loan": self.against_loan, "docstatus": 1, "status": current_status}, + filters, "name", ) - frappe.db.set_value("Loan Repayment Schedule", schedule, "status", status) def on_cancel(self): if self.is_term_loan: + self.cancel_and_delete_repayment_schedule() self.update_repayment_schedule_status(cancel=1) self.delete_security_deposit() @@ -81,6 +144,24 @@ def withheld_security_deposit(self): ).insert() sd.submit() + def set_cyclic_date(self): + if self.repayment_frequency == "Monthly": + cycle_day, min_days_bw_disbursement_first_repayment = frappe.db.get_value( + "Loan Product", + self.loan_product, + ["cyclic_day_of_the_month", "min_days_bw_disbursement_first_repayment"], + ) + cycle_day = cint(cycle_day) + + last_day_of_month = get_last_day(self.posting_date) + cyclic_date = add_days(last_day_of_month, cycle_day) + + broken_period_days = date_diff(cyclic_date, self.posting_date) + if broken_period_days < min_days_bw_disbursement_first_repayment: + cyclic_date = add_days(get_last_day(cyclic_date), cycle_day) + + self.repayment_start_date = cyclic_date + def delete_security_deposit(self): if self.withhold_security_deposit: sd = frappe.get_doc("Loan Security Deposit", {"loan_disbursement": self.name}) @@ -88,14 +169,44 @@ def delete_security_deposit(self): sd.delete() def validate_disbursal_amount(self): - possible_disbursal_amount = get_disbursal_amount(self.against_loan) + possible_disbursal_amount, pending_principal_amount = get_disbursal_amount(self.against_loan) + limit_details = frappe.db.get_value( + "Loan", + self.against_loan, + [ + "limit_applicable_start", + "limit_applicable_end", + "minimum_limit_amount", + "maximum_limit_amount", + ], + as_dict=1, + ) if not self.disbursed_amount: frappe.throw(_("Disbursed amount cannot be zero")) + elif self.repayment_schedule_type == "Line of Credit": + if ( + getdate(limit_details.limit_applicable_end) + < getdate(self.disbursement_date) + < getdate(limit_details.limit_applicable_start) + ): + frappe.throw("Disbursement date is out of approved limit dates") + elif self.disbursed_amount > possible_disbursal_amount: frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) + if limit_details.minimum_limit_amount and self.disbursed_amount < flt( + limit_details.minimum_limit_amount + ): + frappe.throw(_("Disbursement amount cannot be less than minimum credit limit amount")) + + if ( + limit_details.maximum_limit_amount + and pending_principal_amount + self.disbursed_amount > flt(limit_details.maximum_limit_amount) + ): + frappe.throw(_("Disbursement amount cannot be greater than maximum limit amount")) + def set_status_and_amounts(self, cancel=0): loan_details = frappe.get_all( "Loan", @@ -139,9 +250,8 @@ def get_values_on_cancel(self, loan_details): total_payment = total_payment - topup_amount - if disbursed_amount == 0: + if disbursed_amount <= 0: status = "Sanctioned" - elif disbursed_amount >= loan_details.loan_amount: status = "Disbursed" else: @@ -171,7 +281,9 @@ def get_values_on_submit(self, loan_details): total_payment = total_payment + topup_amount - if flt(disbursed_amount) >= loan_details.loan_amount: + if self.repayment_schedule_type == "Line of Credit": + status = "Active" + elif flt(disbursed_amount) >= loan_details.loan_amount: status = "Disbursed" else: status = "Partially Disbursed" @@ -372,7 +484,7 @@ def get_disbursal_amount(loan, on_current_security_price=0): ): disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount - return disbursal_amount + return disbursal_amount, pending_principal_amount def get_maximum_amount_as_per_pledged_security(loan): diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement_dashboard.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement_dashboard.py index 329b4abe..5ce01a5f 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement_dashboard.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement_dashboard.py @@ -3,7 +3,7 @@ def get_data(): "fieldname": "loan_disbursement", "transactions": [ { - "items": ["Loan Security Deposit"], + "items": ["Loan Security Deposit", "Loan Repayment Schedule"], }, ], } diff --git a/lending/loan_management/doctype/loan_product/loan_product.json b/lending/loan_management/doctype/loan_product/loan_product.json index 6367ee96..9768172a 100644 --- a/lending/loan_management/doctype/loan_product/loan_product.json +++ b/lending/loan_management/doctype/loan_product/loan_product.json @@ -190,7 +190,7 @@ "fieldtype": "Select", "label": "Repayment Schedule Type", "mandatory_depends_on": "is_term_loan", - "options": "\nMonthly as per repayment start date\nPro-rated calendar months\nMonthly as per cycle date" + "options": "\nMonthly as per repayment start date\nPro-rated calendar months\nMonthly as per cycle date\nLine of Credit" }, { "depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"", @@ -382,7 +382,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-12 13:35:48.346212", + "modified": "2023-10-29 16:00:12.844726", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Product", diff --git a/lending/loan_management/doctype/loan_repayment/loan_repayment.py b/lending/loan_management/doctype/loan_repayment/loan_repayment.py index 9d9a7908..71ad279e 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.py @@ -1061,7 +1061,7 @@ def regenerate_repayment_schedule(loan, cancel=0): def get_pending_principal_amount(loan): - if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount: + if loan.status in ("Disbursed", "Closed"): pending_principal_amount = ( flt(loan.total_payment) + flt(loan.debit_adjustment_amount) diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index fe1358ba..192d9dc6 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -8,9 +8,11 @@ "engine": "InnoDB", "field_order": [ "loan", + "loan_disbursement", "company", "loan_restructure", "loan_amount", + "disbursed_amount", "rate_of_interest", "posting_date", "adjusted_interest", @@ -40,6 +42,7 @@ "reqd": 1 }, { + "fetch_from": "loan.loan_product", "fieldname": "loan_product", "fieldtype": "Link", "in_list_view": 1, @@ -63,6 +66,7 @@ "read_only": 1 }, { + "fetch_from": "loan.loan_amount", "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", @@ -172,12 +176,23 @@ "fieldname": "moratorium_tenure", "fieldtype": "Int", "label": "Moratorium Tenure" + }, + { + "fieldname": "disbursed_amount", + "fieldtype": "Currency", + "label": "Disbursed Amount" + }, + { + "fieldname": "loan_disbursement", + "fieldtype": "Link", + "label": "Loan Disbursement", + "options": "Loan Disbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-25 10:08:14.519533", + "modified": "2023-11-02 11:23:35.469042", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index 51a0a388..80799bc4 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -18,7 +18,10 @@ def validate(self): def set_missing_fields(self): if self.repayment_method == "Repay Over Number of Periods": self.monthly_repayment_amount = get_monthly_repayment_amount( - self.loan_amount, self.rate_of_interest, self.repayment_periods + self.disbursed_amount or self.loan_amount, + self.rate_of_interest, + self.repayment_periods, + self.repayment_frequency, ) def set_repayment_period(self): @@ -33,22 +36,26 @@ def make_repayment_schedule(self): self.repayment_schedule = [] payment_date = self.repayment_start_date - balance_amount = self.loan_amount + balance_amount = self.disbursed_amount or self.loan_amount broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) carry_forward_interest = self.adjusted_interest moratorium_interest = 0 - if self.moratorium_tenure: + if self.moratorium_tenure and self.repayment_frequency == "Monthly": payment_date = add_months(self.repayment_start_date, -1 * self.moratorium_tenure) moratorium_end_date = add_months(self.repayment_start_date, -1) broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + 1 while balance_amount > 0: - if self.moratorium_tenure and getdate(payment_date) > getdate(moratorium_end_date): + if ( + self.moratorium_tenure + and self.repayment_frequency == "Monthly" + and getdate(payment_date) > getdate(moratorium_end_date) + ): if self.treatment_of_interest == "Capitalize" and moratorium_interest: balance_amount = self.loan_amount + moratorium_interest self.monthly_repayment_amount = get_monthly_repayment_amount( - balance_amount, self.rate_of_interest, self.repayment_periods + balance_amount, self.rate_of_interest, self.repayment_periods, self.repayment_frequency ) moratorium_interest = 0 @@ -59,7 +66,7 @@ def make_repayment_schedule(self): carry_forward_interest, ) - if self.moratorium_tenure: + if self.moratorium_tenure and self.repayment_frequency == "Monthly": if getdate(payment_date) <= getdate(moratorium_end_date): total_payment = 0 balance_amount = self.loan_amount @@ -82,9 +89,15 @@ def make_repayment_schedule(self): payment_date, principal_amount, interest_amount, total_payment, balance_amount, days ) - if self.repayment_method == "Repay Over Number of Periods" and len( - self.get("repayment_schedule") - ) >= self.repayment_periods + cint(self.moratorium_tenure): + tenure = self.repayment_periods + if self.repayment_frequency == "Monthly": + tenure += cint(self.moratorium_tenure) + + if ( + self.repayment_method == "Repay Over Number of Periods" + and self.repayment_frequency != "One Time" + and len(self.get("repayment_schedule")) >= tenure + ): self.get("repayment_schedule")[-1].principal_amount += balance_amount self.get("repayment_schedule")[-1].balance_loan_amount = 0 self.get("repayment_schedule")[-1].total_payment = ( @@ -205,9 +218,12 @@ def add_single_month(date): return add_months(date, 1) -def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods): +def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods, frequency): + if frequency == "One Time": + repayment_periods = 1 + if rate_of_interest: - monthly_interest_rate = flt(rate_of_interest) / (12 * 100) + monthly_interest_rate = flt(rate_of_interest) / (get_frequency(frequency) * 100) monthly_repayment_amount = math.ceil( (loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods) / ((1 + monthly_interest_rate) ** repayment_periods - 1) @@ -215,3 +231,7 @@ def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_period else: monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) return monthly_repayment_amount + + +def get_frequency(frequency): + return {"Monthly": 12, "Weekly": 52, "Daily": 365, "Quarterly": 4, "One Time": 1}.get(frequency) From f8d66ae250bb48c3b0e2c59643777ca239f69d17 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 15 Nov 2023 16:13:27 +0530 Subject: [PATCH 5/9] chore: shift repayment schedule generation from Loan to Loan Disbursement --- .../loan_management/doctype/loan/loan.json | 10 +- lending/loan_management/doctype/loan/loan.py | 164 ++++++------------ .../loan_disbursement/loan_disbursement.json | 20 ++- .../loan_disbursement/loan_disbursement.py | 150 +++++++++++----- .../loan_interest_accrual.py | 2 +- .../loan_repayment_schedule.json | 6 +- 6 files changed, 189 insertions(+), 163 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index a8e78dcd..a4b656b1 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -38,10 +38,9 @@ "is_term_loan", "loan_credit_limits_section", "limit_applicable_start", - "minimum_limit_amount", + "maximum_limit_amount", "column_break_foeo", "limit_applicable_end", - "maximum_limit_amount", "loan_classification_details_section", "days_past_due", "classification_code", @@ -526,11 +525,6 @@ "fieldtype": "Date", "label": "Limit Applicable Start" }, - { - "fieldname": "minimum_limit_amount", - "fieldtype": "Currency", - "label": "Minimum Limit Amount" - }, { "fieldname": "column_break_foeo", "fieldtype": "Column Break" @@ -549,7 +543,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-02 20:30:24.141977", + "modified": "2023-11-14 19:09:32.629112", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 6ed1ff73..8336921f 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -39,36 +39,36 @@ def validate(self): self.set_cyclic_date() self.set_default_charge_account() - if self.is_term_loan and not self.is_new() and self.repayment_schedule_type != "Line of Credit": - update_draft_schedule( - self.name, - self.repayment_method, - self.repayment_start_date, - self.repayment_periods, - self.monthly_repayment_amount, - self.posting_date, - self.repayment_frequency, - moratorium_tenure=self.moratorium_tenure, - treatment_of_interest=self.treatment_of_interest, - ) + # if self.is_term_loan and not self.is_new() and self.repayment_schedule_type != "Line of Credit": + # update_draft_schedule( + # self.name, + # self.repayment_method, + # self.repayment_start_date, + # self.repayment_periods, + # self.monthly_repayment_amount, + # self.posting_date, + # self.repayment_frequency, + # moratorium_tenure=self.moratorium_tenure, + # treatment_of_interest=self.treatment_of_interest, + # ) if not self.is_term_loan or (self.is_term_loan and not self.is_new()): self.calculate_totals() - def after_insert(self): - if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": - make_draft_schedule( - self.name, - self.repayment_method, - self.repayment_start_date, - self.repayment_periods, - self.monthly_repayment_amount, - self.posting_date, - self.repayment_frequency, - moratorium_tenure=self.moratorium_tenure, - treatment_of_interest=self.treatment_of_interest, - ) - self.calculate_totals(on_insert=True) + # def after_insert(self): + # if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": + # make_draft_schedule( + # self.name, + # self.repayment_method, + # self.repayment_start_date, + # self.repayment_periods, + # self.monthly_repayment_amount, + # self.posting_date, + # self.repayment_frequency, + # moratorium_tenure=self.moratorium_tenure, + # treatment_of_interest=self.treatment_of_interest, + # ) + # self.calculate_totals(on_insert=True) def validate_accounts(self): for fieldname in [ @@ -98,20 +98,7 @@ def set_cyclic_date(self): self.repayment_schedule_type == "Monthly as per cycle date" and self.repayment_frequency == "Monthly" ): - cycle_day, min_days_bw_disbursement_first_repayment = frappe.db.get_value( - "Loan Product", - self.loan_product, - ["cyclic_day_of_the_month", "min_days_bw_disbursement_first_repayment"], - ) - cycle_day = cint(cycle_day) - - last_day_of_month = get_last_day(self.posting_date) - cyclic_date = add_days(last_day_of_month, cycle_day) - - broken_period_days = date_diff(cyclic_date, self.posting_date) - if broken_period_days < min_days_bw_disbursement_first_repayment: - cyclic_date = add_days(get_last_day(cyclic_date), cycle_day) - + cyclic_date = get_cyclic_date(self.loan_product, self.posting_date) self.repayment_start_date = cyclic_date if self.moratorium_tenure: @@ -136,8 +123,8 @@ def on_submit(self): # Interest accrual for backdated term loans self.accrue_loan_interest() - if self.repayment_schedule_type != "Line of Credit": - self.submit_draft_schedule() + # if self.repayment_schedule_type != "Line of Credit": + # self.submit_draft_schedule() def on_cancel(self): self.unlink_loan_security_pledge() @@ -202,14 +189,15 @@ def calculate_totals(self, on_insert=False): self.total_interest_payable = 0 self.total_amount_paid = 0 - if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": - schedule = frappe.get_doc("Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}) - for data in schedule.repayment_schedule: - self.total_payment += data.total_payment - self.total_interest_payable += data.interest_amount + # if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": + # schedule = frappe.get_doc("Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}) + # for data in schedule.repayment_schedule: + # self.total_payment += data.total_payment + # self.total_interest_payable += data.interest_amount - self.monthly_repayment_amount = schedule.monthly_repayment_amount - else: + # self.monthly_repayment_amount = schedule.monthly_repayment_amount + + if not self.is_term_loan: self.total_payment = self.loan_amount if on_insert: @@ -939,68 +927,20 @@ def move_unpaid_interest_to_suspense_ledger( jv.submit() -def make_draft_schedule( - loan, - repayment_method, - start_date, - repayment_periods, - frequency_repayment_amount, - posting_date, - repayment_frequency, - disbursed_amount=None, - moratorium_tenure=None, - treatment_of_interest=None, - loan_disbursement=None, -): - frappe.get_doc( - { - "doctype": "Loan Repayment Schedule", - "loan": loan, - "repayment_method": repayment_method, - "repayment_start_date": start_date, - "repayment_periods": repayment_periods, - "monthly_repayment_amount": frequency_repayment_amount, - "posting_date": posting_date, - "repayment_frequency": repayment_frequency, - "disbursed_amount": disbursed_amount, - "moratorium_tenure": moratorium_tenure, - "treatment_of_interest": treatment_of_interest, - "loan_disbursement": loan_disbursement, - } - ).insert() +@frappe.whitelist() +def get_cyclic_date(loan_product, posting_date): + cycle_day, min_days_bw_disbursement_first_repayment = frappe.db.get_value( + "Loan Product", + loan_product, + ["cyclic_day_of_the_month", "min_days_bw_disbursement_first_repayment"], + ) + cycle_day = cint(cycle_day) + last_day_of_month = get_last_day(posting_date) + cyclic_date = add_days(last_day_of_month, cycle_day) -def update_draft_schedule( - loan, - repayment_method, - start_date, - repayment_periods, - frequency_repayment_amount, - posting_date, - repayment_frequency, - disbursed_amount=None, - moratorium_tenure=None, - treatment_of_interest=None, - loan_disbursement=None, -): - draft_schedule = frappe.db.get_value( - "Loan Repayment Schedule", {"loan": loan, "docstatus": 0}, "name" - ) - if draft_schedule: - schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) - schedule.update( - { - "loan": loan, - "repayment_periods": repayment_periods, - "repayment_method": repayment_method, - "repayment_start_date": start_date, - "posting_date": posting_date, - "monthly_repayment_amount": frequency_repayment_amount, - "repayment_frequency": repayment_frequency, - "disbursed_amount": disbursed_amount, - "moratorium_tenure": moratorium_tenure, - "treatment_of_interest": treatment_of_interest, - "loan_disbursement": loan_disbursement, - } - ) - schedule.save() + broken_period_days = date_diff(cyclic_date, posting_date) + if broken_period_days < min_days_bw_disbursement_first_repayment: + cyclic_date = add_days(get_last_day(cyclic_date), cycle_day) + + return cyclic_date diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json index 57cb13c6..996f4142 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -7,6 +7,8 @@ "engine": "InnoDB", "field_order": [ "against_loan", + "sanctioned_loan_amount", + "current_disbursed_amount", "posting_date", "applicant_type", "loan_product", @@ -245,6 +247,8 @@ "label": "Repayment Schedule Type" }, { + "fetch_from": "against_loan.repayment_start_date", + "fetch_if_empty": 1, "fieldname": "repayment_start_date", "fieldtype": "Date", "label": "Repayment Start Date" @@ -260,12 +264,26 @@ "fieldtype": "Select", "label": "Repayment Method", "options": "Repay Over Number of Periods\nRepay Fixed Amount per Period" + }, + { + "fetch_from": "against_loan.loan_amount", + "fieldname": "sanctioned_loan_amount", + "fieldtype": "Currency", + "label": "Sanctioned Loan Amount", + "read_only": 1 + }, + { + "fetch_from": "against_loan.disbursed_amount", + "fieldname": "current_disbursed_amount", + "fieldtype": "Currency", + "label": "Current Disbursed Amount", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-02 15:16:01.606208", + "modified": "2023-11-14 20:23:09.798987", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index f8b69b8f..f45cd27f 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -19,7 +19,7 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController -from lending.loan_management.doctype.loan.loan import make_draft_schedule, update_draft_schedule +from lending.loan_management.doctype.loan.loan import get_cyclic_date from lending.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( get_pledged_security_qty, ) @@ -35,35 +35,95 @@ def validate(self): if self.repayment_schedule_type == "Line of Credit": self.set_cyclic_date() - if self.is_term_loan and not self.is_new() and self.repayment_schedule_type == "Line of Credit": - update_draft_schedule( - self.against_loan, - self.repayment_method, - self.repayment_start_date, - self.tenure, - self.monthly_repayment_amount, - self.posting_date, - self.repayment_frequency, - disbursed_amount=self.disbursed_amount, - loan_disbursement=self.name, - ) + if self.is_term_loan and not self.is_new(): + self.update_draft_schedule() def after_insert(self): - if self.is_term_loan and self.repayment_schedule_type == "Line of Credit": - make_draft_schedule( - self.against_loan, - self.repayment_method, - self.repayment_start_date, - self.tenure, - self.monthly_repayment_amount, - self.posting_date, - self.repayment_frequency, - disbursed_amount=self.disbursed_amount, - loan_disbursement=self.name, + self.make_draft_schedule() + + def make_draft_schedule(self): + disbursed_amount = self.get_disbursed_amount() + + if self.repayment_frequency == "Monthly": + loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") + + if self.repayment_schedule_type != "Line of Credit": + tenure = frappe.db.get_value("Loan", self.against_loan, "repayment_periods") + self.posting_date = self.disbursement_date + self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date) + already_accrued_months = self.get_already_accrued_months() + self.tenure = tenure - already_accrued_months + + frappe.get_doc( + { + "doctype": "Loan Repayment Schedule", + "loan": self.against_loan, + "repayment_method": self.repayment_method, + "repayment_start_date": self.repayment_start_date, + "repayment_periods": self.tenure, + "monthly_repayment_amount": self.monthly_repayment_amount, + "posting_date": self.disbursement_date, + "repayment_frequency": self.repayment_frequency, + "disbursed_amount": disbursed_amount, + "loan_disbursement": self.name, + } + ).insert() + + def get_already_accrued_months(self): + already_accrued_months = 0 + existing_schedule = frappe.db.get_all( + "Loan Repayment Schedule", + {"loan": self.against_loan, "docstatus": 1, "status": ("in", ["Active", "Outdated"])}, + pluck="name", + ) + + if existing_schedule: + already_accrued_months = frappe.db.count( + "Repayment Schedule", {"parent": ("in", existing_schedule), "is_accrued": 1} + ) + + return already_accrued_months + + def get_disbursed_amount(self): + if self.repayment_schedule_type == "Line of Credit": + disbursed_amount = self.disbursed_amount + else: + current_disbursed_amount = frappe.db.get_value("Loan", self.against_loan, "disbursed_amount") + disbursed_amount = self.disbursed_amount + current_disbursed_amount + + return disbursed_amount + + def update_draft_schedule(self): + draft_schedule = frappe.db.get_value( + "Loan Repayment Schedule", {"loan": self.against_loan, "docstatus": 0}, "name" + ) + + if self.repayment_frequency == "Monthly": + loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") + self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date) + + if draft_schedule: + disbursed_amount = self.get_disbursed_amount() + + schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) + schedule.update( + { + "loan": self.against_loan, + "repayment_periods": self.tenure, + "repayment_method": self.repayment_method, + "repayment_start_date": self.repayment_start_date, + "posting_date": self.disbursement_date, + "monthly_repayment_amount": self.monthly_repayment_amount, + "repayment_frequency": self.repayment_frequency, + "disbursed_amount": disbursed_amount, + "loan_disbursement": self.name, + } ) + schedule.save() def on_submit(self): if self.is_term_loan: + self.update_current_repayment_schedule() self.submit_repayment_schedule() self.update_repayment_schedule_status() @@ -72,15 +132,14 @@ def on_submit(self): self.make_gl_entries() def submit_repayment_schedule(self): - if self.repayment_schedule_type == "Line of Credit": - filters = { - "loan": self.against_loan, - "docstatus": 0, - "status": "Initiated", - "loan_disbursement": self.name, - } - schedule = frappe.get_doc("Loan Repayment Schedule", filters) - schedule.submit() + filters = { + "loan": self.against_loan, + "docstatus": 0, + "status": "Initiated", + "loan_disbursement": self.name, + } + schedule = frappe.get_doc("Loan Repayment Schedule", filters) + schedule.submit() def cancel_and_delete_repayment_schedule(self): if self.repayment_schedule_type == "Line of Credit": @@ -93,6 +152,24 @@ def cancel_and_delete_repayment_schedule(self): schedule = frappe.get_doc("Loan Repayment Schedule", filters) schedule.cancel() + def update_current_repayment_schedule(self, cancel=0): + # Update status of existing schedule on topup + if cancel: + status = "Active" + current_status = "Outdated" + else: + status = "Outdated" + current_status = "Active" + + if self.repayment_schedule_type != "Line of Credit": + existing_schedule = frappe.db.get_value( + "Loan Repayment Schedule", + {"loan": self.against_loan, "docstatus": 1, "status": current_status}, + ) + + if existing_schedule: + frappe.db.set_value("Loan Repayment Schedule", existing_schedule, "status", status) + def update_repayment_schedule_status(self, cancel=0): if cancel: status = "Initiated" @@ -113,6 +190,7 @@ def on_cancel(self): if self.is_term_loan: self.cancel_and_delete_repayment_schedule() self.update_repayment_schedule_status(cancel=1) + self.update_current_repayment_schedule(cancel=1) self.delete_security_deposit() self.set_status_and_amounts(cancel=1) @@ -176,7 +254,6 @@ def validate_disbursal_amount(self): [ "limit_applicable_start", "limit_applicable_end", - "minimum_limit_amount", "maximum_limit_amount", ], as_dict=1, @@ -196,11 +273,6 @@ def validate_disbursal_amount(self): elif self.disbursed_amount > possible_disbursal_amount: frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) - if limit_details.minimum_limit_amount and self.disbursed_amount < flt( - limit_details.minimum_limit_amount - ): - frappe.throw(_("Disbursement amount cannot be less than minimum credit limit amount")) - if ( limit_details.maximum_limit_amount and pending_principal_amount + self.disbursed_amount > flt(limit_details.maximum_limit_amount) diff --git a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index b6d3f714..66a3901a 100644 --- a/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/lending/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -277,7 +277,7 @@ def get_term_loans(date, term_loan=None, loan_product=None): ) .where( (loan.docstatus == 1) - & (loan.status == "Disbursed") + & (loan.status.isin(["Disbursed", "Partially Disbursed", "Active"])) & (loan.is_term_loan == 1) & (loan_schedule.status == "Active") & (loan_repayment_schedule.principal_amount > 0) diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index 192d9dc6..ff97802d 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -91,7 +91,7 @@ "fieldtype": "Select", "hidden": 1, "label": "Status", - "options": "Initiated\nRejected\nActive\nRestructured\nDraft\nCancelled" + "options": "Initiated\nRejected\nActive\nRestructured\nOutdated\nDraft\nCancelled" }, { "fetch_if_empty": 1, @@ -159,6 +159,7 @@ "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" }, { + "fetch_from": "loan.treatment_of_interest", "fieldname": "treatment_of_interest", "fieldtype": "Select", "label": "Treatment Of Interest", @@ -173,6 +174,7 @@ "options": "Start of the next month\nEnd of the current month" }, { + "fetch_from": "loan.moratorium_tenure", "fieldname": "moratorium_tenure", "fieldtype": "Int", "label": "Moratorium Tenure" @@ -192,7 +194,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-02 11:23:35.469042", + "modified": "2023-11-15 09:47:25.739442", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", From dbb657fdf32ff28032e73437a9410ba4ff7b5c65 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Dec 2023 10:49:56 +0530 Subject: [PATCH 6/9] fix: Repayment start date fixes --- .../loan_disbursement/loan_disbursement.py | 45 ++++++++++++++----- .../loan_repayment_schedule.json | 3 +- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index f45cd27f..28e692e9 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -6,6 +6,7 @@ from frappe import _ from frappe.utils import ( add_days, + add_months, cint, date_diff, flt, @@ -41,18 +42,29 @@ def validate(self): def after_insert(self): self.make_draft_schedule() + def on_trash(self): + if self.docstatus == 0: + draft_schedule = self.get_draft_schedule() + frappe.delete_doc("Loan Repayment Schedule", draft_schedule) + def make_draft_schedule(self): disbursed_amount = self.get_disbursed_amount() if self.repayment_frequency == "Monthly": loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") - - if self.repayment_schedule_type != "Line of Credit": - tenure = frappe.db.get_value("Loan", self.against_loan, "repayment_periods") - self.posting_date = self.disbursement_date + if self.repayment_schedule_type != "Line of Credit" and not self.repayment_start_date: + loan_details = frappe.db.get_value( + "Loan", self.against_loan, ["repayment_periods", "moratorium_tenure", "status"], as_dict=1 + ) self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date) + + if loan_details.status == "Sanctioned" and loan_details.moratorium_tenure: + self.repayment_start_date = add_months( + self.repayment_start_date, loan_details.moratorium_tenure + ) + already_accrued_months = self.get_already_accrued_months() - self.tenure = tenure - already_accrued_months + self.tenure = loan_details.repayment_periods - already_accrued_months frappe.get_doc( { @@ -93,18 +105,27 @@ def get_disbursed_amount(self): return disbursed_amount - def update_draft_schedule(self): - draft_schedule = frappe.db.get_value( + def get_draft_schedule(self): + return frappe.db.get_value( "Loan Repayment Schedule", {"loan": self.against_loan, "docstatus": 0}, "name" ) - if self.repayment_frequency == "Monthly": - loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") - self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date) + def update_draft_schedule(self): + draft_schedule = self.get_draft_schedule() + + if self.repayment_frequency == "Monthly" and not self.repayment_start_date: + loan_details = frappe.db.get_value( + "Loan", self.against_loan, ["status", "moratorium_tenure", "loan_product"], as_dict=1 + ) + + self.repayment_start_date = get_cyclic_date(loan_details.loan_product, self.posting_date) + if loan_details.status == "Sanctioned" and loan_details.moratorium_tenure: + self.repayment_start_date = add_months( + self.repayment_start_date, loan_details.moratorium_tenure + ) if draft_schedule: disbursed_amount = self.get_disbursed_amount() - schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) schedule.update( { @@ -223,7 +244,7 @@ def withheld_security_deposit(self): sd.submit() def set_cyclic_date(self): - if self.repayment_frequency == "Monthly": + if self.repayment_frequency == "Monthly" and not self.repayment_start_date: cycle_day, min_days_bw_disbursement_first_repayment = frappe.db.get_value( "Loan Product", self.loan_product, diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index ff97802d..73cc03b5 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -94,7 +94,6 @@ "options": "Initiated\nRejected\nActive\nRestructured\nOutdated\nDraft\nCancelled" }, { - "fetch_if_empty": 1, "fieldname": "repayment_start_date", "fieldtype": "Date", "label": "Repayment Start Date" @@ -194,7 +193,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-15 09:47:25.739442", + "modified": "2023-12-05 22:50:03.030846", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", From a31b1cca799380b40c3710d57d2d425a4142df10 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 7 Dec 2023 12:57:40 +0530 Subject: [PATCH 7/9] fix: Repayment schedule calculaitons --- lending/loan_management/doctype/loan/loan.js | 7 ++- .../loan_management/doctype/loan/loan.json | 30 ++++----- lending/loan_management/doctype/loan/loan.py | 7 ++- .../loan_disbursement/loan_disbursement.json | 3 +- .../loan_disbursement/loan_disbursement.py | 63 ++++++++----------- .../loan_repayment_schedule.py | 25 ++++---- 6 files changed, 65 insertions(+), 70 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.js b/lending/loan_management/doctype/loan/loan.js index f9f72b5e..aa637736 100644 --- a/lending/loan_management/doctype/loan/loan.js +++ b/lending/loan_management/doctype/loan/loan.js @@ -128,9 +128,12 @@ frappe.ui.form.on('Loan', { "company": frm.doc.company, "applicant_type": frm.doc.applicant_type, "applicant": frm.doc.applicant, - "pending_amount": frm.doc.loan_amount - frm.doc.disbursed_amount > 0 ? + "posting_date": frm.doc.posting_date, + "repayment_start_date": frm.doc.repayment_start_date, + "disbursement_amount": frm.doc.loan_amount - frm.doc.disbursed_amount > 0 ? frm.doc.loan_amount - frm.doc.disbursed_amount : 0, - "as_dict": 1 + "as_dict": 1, + "repayment_frequency": frm.doc.repayment_frequency, }, method: "lending.loan_management.doctype.loan.loan.make_loan_disbursement", callback: function (r) { diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index 3d871356..28218fe6 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -12,7 +12,6 @@ "applicant", "applicant_name", "loan_application", - "branch", "column_break_3", "company", "posting_date", @@ -30,11 +29,12 @@ "column_break_11", "maximum_loan_amount", "repayment_method", + "repayment_frequency", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", - "repayment_frequency", "moratorium_tenure", + "moratorium_type", "treatment_of_interest", "is_term_loan", "loan_credit_limits_section", @@ -174,7 +174,7 @@ "no_copy": 1 }, { - "depends_on": "is_term_loan", + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fieldname": "repayment_start_date", "fieldtype": "Date", "label": "Repayment Start Date" @@ -184,20 +184,20 @@ "fieldtype": "Column Break" }, { - "depends_on": "is_term_loan", + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fieldname": "repayment_method", "fieldtype": "Select", "label": "Repayment Method", "options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods" }, { - "depends_on": "is_term_loan", + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fieldname": "repayment_periods", "fieldtype": "Int", - "label": "Repayment Period in Months" + "label": "Repayment Periods" }, { - "depends_on": "is_term_loan", + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fetch_from": "loan_application.repayment_amount", "fetch_if_empty": 1, "fieldname": "monthly_repayment_amount", @@ -427,12 +427,6 @@ "fieldtype": "Check", "label": "Manual NPA" }, - { - "fieldname": "branch", - "fieldtype": "Link", - "label": "Branch", - "options": "Branch" - }, { "fieldname": "loan_restructure_count", "fieldtype": "Int", @@ -477,6 +471,7 @@ "label": "Loan Classification Details" }, { + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fieldname": "repayment_frequency", "fieldtype": "Select", "label": "Repayment Frequency", @@ -502,6 +497,7 @@ "options": "Capitalize\nAdd to first repayment" }, { + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", "fieldname": "moratorium_tenure", "fieldtype": "Int", "label": "Moratorium Tenure" @@ -536,12 +532,18 @@ "fieldtype": "Link", "label": "Loan Category", "options": "Loan Category" + }, + { + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", + "fieldname": "moratorium_type", + "fieldtype": "Select", + "label": "Moratorium Type" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-14 19:09:32.629112", + "modified": "2023-12-06 20:36:53.297695", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 8336921f..38381248 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -385,6 +385,8 @@ def make_loan_disbursement( disbursement_amount=0, as_dict=0, submit=0, + repayment_start_date=None, + repayment_frequency=None, posting_date=None, disbursement_date=None, bank_account=None, @@ -398,7 +400,8 @@ def make_loan_disbursement( disbursement_entry.disbursement_date = posting_date or nowdate() disbursement_entry.posting_date = disbursement_date or nowdate() disbursement_entry.bank_account = bank_account - + disbursement_entry.repayment_start_date = repayment_start_date + disbursement_entry.repayment_frequency = repayment_frequency disbursement_entry.disbursed_amount = disbursement_amount for charge in loan_doc.get("loan_charges"): @@ -507,7 +510,7 @@ def unpledge_security( unpledge_request.status = "Approved" unpledge_request.save() else: - frappe.throw(_("Only submittted unpledge requests can be approved")) + frappe.throw(_("Only submitted unpledge requests can be approved")) if as_dict: return unpledge_request diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json index 996f4142..be9f6abf 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -244,6 +244,7 @@ "fetch_from": "against_loan.repayment_schedule_type", "fieldname": "repayment_schedule_type", "fieldtype": "Data", + "hidden": 1, "label": "Repayment Schedule Type" }, { @@ -283,7 +284,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-11-14 20:23:09.798987", + "modified": "2023-12-07 10:43:07.860874", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index 28e692e9..5122f87c 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -47,39 +47,39 @@ def on_trash(self): draft_schedule = self.get_draft_schedule() frappe.delete_doc("Loan Repayment Schedule", draft_schedule) - def make_draft_schedule(self): + def get_schedule_details(self): disbursed_amount = self.get_disbursed_amount() - if self.repayment_frequency == "Monthly": - loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") - if self.repayment_schedule_type != "Line of Credit" and not self.repayment_start_date: - loan_details = frappe.db.get_value( - "Loan", self.against_loan, ["repayment_periods", "moratorium_tenure", "status"], as_dict=1 - ) + return { + "doctype": "Loan Repayment Schedule", + "loan": self.against_loan, + "repayment_method": self.repayment_method, + "repayment_start_date": self.repayment_start_date, + "repayment_periods": self.tenure, + "monthly_repayment_amount": self.monthly_repayment_amount, + "posting_date": self.disbursement_date, + "repayment_frequency": self.repayment_frequency, + "disbursed_amount": disbursed_amount, + "loan_disbursement": self.name, + } + + def make_draft_schedule(self): + loan_product = frappe.db.get_value("Loan", self.against_loan, "loan_product") + loan_details = frappe.db.get_value( + "Loan", self.against_loan, ["repayment_periods", "moratorium_tenure", "status"], as_dict=1 + ) + if self.repayment_schedule_type != "Line of Credit": + if not self.repayment_start_date: self.repayment_start_date = get_cyclic_date(loan_product, self.posting_date) if loan_details.status == "Sanctioned" and loan_details.moratorium_tenure: self.repayment_start_date = add_months( self.repayment_start_date, loan_details.moratorium_tenure ) + already_accrued_months = self.get_already_accrued_months() + self.tenure = loan_details.repayment_periods - already_accrued_months - already_accrued_months = self.get_already_accrued_months() - self.tenure = loan_details.repayment_periods - already_accrued_months - - frappe.get_doc( - { - "doctype": "Loan Repayment Schedule", - "loan": self.against_loan, - "repayment_method": self.repayment_method, - "repayment_start_date": self.repayment_start_date, - "repayment_periods": self.tenure, - "monthly_repayment_amount": self.monthly_repayment_amount, - "posting_date": self.disbursement_date, - "repayment_frequency": self.repayment_frequency, - "disbursed_amount": disbursed_amount, - "loan_disbursement": self.name, - } - ).insert() + frappe.get_doc(self.get_schedule_details()).insert() def get_already_accrued_months(self): already_accrued_months = 0 @@ -125,21 +125,8 @@ def update_draft_schedule(self): ) if draft_schedule: - disbursed_amount = self.get_disbursed_amount() schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) - schedule.update( - { - "loan": self.against_loan, - "repayment_periods": self.tenure, - "repayment_method": self.repayment_method, - "repayment_start_date": self.repayment_start_date, - "posting_date": self.disbursement_date, - "monthly_repayment_amount": self.monthly_repayment_amount, - "repayment_frequency": self.repayment_frequency, - "disbursed_amount": disbursed_amount, - "loan_disbursement": self.name, - } - ) + schedule.update(self.get_schedule_details()) schedule.save() def on_submit(self): diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index 80799bc4..fd99d9de 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -156,12 +156,12 @@ def get_amounts( return interest_amount, principal_amount, balance_amount, total_payment, days def get_days_and_months(self, payment_date, additional_days): + months = 365 if self.repayment_frequency == "Monthly": if self.repayment_schedule_type == "Monthly as per repayment start date": days = 1 months = 12 else: - months = 365 expected_payment_date = get_last_day(payment_date) if self.repayment_date_on == "Start of the next month": expected_payment_date = add_days(expected_payment_date, 1) @@ -180,18 +180,17 @@ def get_days_and_months(self, payment_date, additional_days): days = 30 else: days = date_diff(get_last_day(payment_date), payment_date) - elif self.repayment_frequency == "Weekly": - days = 7 - months = 52 - elif self.repayment_frequency == "Daily": - days = 1 - months = 365 - elif self.repayment_frequency == "Quarterly": - days = 3 - months = 12 - elif self.repayment_frequency == "One Time": - days = date_diff(self.repayment_start_date, self.posting_date) - months = 365 + else: + if payment_date == self.repayment_start_date: + days = date_diff(payment_date, self.posting_date) + 1 + elif self.repayment_frequency == "Weekly": + days = 7 + elif self.repayment_frequency == "Daily": + days = 1 + elif self.repayment_frequency == "Quarterly": + days = 3 + elif self.repayment_frequency == "One Time": + days = date_diff(self.repayment_start_date, self.posting_date) return days, months From a987b5f2fb6d1b50992c2110edbc5337aa07c97d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Dec 2023 10:13:40 +0530 Subject: [PATCH 8/9] feat: broken period interest calculation --- .../loan_disbursement/loan_disbursement.json | 9 +- .../loan_disbursement/loan_disbursement.py | 125 +++++++----------- .../loan_repayment_schedule.json | 9 +- .../loan_repayment_schedule.py | 21 ++- 4 files changed, 77 insertions(+), 87 deletions(-) diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json index be9f6abf..4120129e 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -28,6 +28,7 @@ "clearance_date", "column_break_8", "disbursed_amount", + "broken_period_interest", "accounting_dimensions_section", "cost_center", "charges_section", @@ -279,12 +280,18 @@ "fieldtype": "Currency", "label": "Current Disbursed Amount", "read_only": 1 + }, + { + "fieldname": "broken_period_interest", + "fieldtype": "Currency", + "label": "Broken Period Interest", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-07 10:43:07.860874", + "modified": "2023-12-10 18:58:36.390130", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index 5122f87c..c631ce9b 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -56,7 +56,6 @@ def get_schedule_details(self): "repayment_method": self.repayment_method, "repayment_start_date": self.repayment_start_date, "repayment_periods": self.tenure, - "monthly_repayment_amount": self.monthly_repayment_amount, "posting_date": self.disbursement_date, "repayment_frequency": self.repayment_frequency, "disbursed_amount": disbursed_amount, @@ -79,7 +78,9 @@ def make_draft_schedule(self): already_accrued_months = self.get_already_accrued_months() self.tenure = loan_details.repayment_periods - already_accrued_months - frappe.get_doc(self.get_schedule_details()).insert() + schedule = frappe.get_doc(self.get_schedule_details()).insert() + self.monthly_repayment_amount = schedule.monthly_repayment_amount + self.broken_period_interest = schedule.broken_period_interest def get_already_accrued_months(self): already_accrued_months = 0 @@ -129,6 +130,9 @@ def update_draft_schedule(self): schedule.update(self.get_schedule_details()) schedule.save() + self.broken_period_interest = schedule.broken_period_interest + self.monthly_repayment_amount = schedule.monthly_repayment_amount + def on_submit(self): if self.is_term_loan: self.update_current_repayment_schedule() @@ -370,19 +374,17 @@ def get_values_on_submit(self, loan_details): return disbursed_amount, status, total_payment - def make_gl_entries(self, cancel=0, adv_adj=0): - gle_map = [] - - gle_map.append( + def add_gl_entry(self, gl_entries, account, against_account, amount, remarks=None): + gl_entries.append( self.get_gl_dict( { - "account": self.loan_account, - "against": self.disbursement_account, - "debit": self.disbursed_amount, - "debit_in_account_currency": self.disbursed_amount, + "account": account, + "against": against_account, + "debit": amount, + "debit_in_account_currency": amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -391,95 +393,58 @@ def make_gl_entries(self, cancel=0, adv_adj=0): ) ) - gle_map.append( + gl_entries.append( self.get_gl_dict( { - "account": self.disbursement_account, - "against": self.loan_account, - "credit": self.disbursed_amount, - "credit_in_account_currency": self.disbursed_amount, + "account": against_account, + "against": account, + "debit": -1 * amount, + "debit_in_account_currency": amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, + "remarks": remarks, "cost_center": self.cost_center, "posting_date": self.disbursement_date, } ) ) + def make_gl_entries(self, cancel=0, adv_adj=0): + gle_map = [] + remarks = _("Disbursement against loan:") + self.against_loan + + self.add_gl_entry( + gle_map, self.loan_account, self.disbursement_account, self.disbursed_amount, remarks + ) + if self.withhold_security_deposit: security_deposit_account = frappe.db.get_value( "Loan Product", self.loan_product, "security_deposit_account" ) - gle_map.append( - self.get_gl_dict( - { - "account": security_deposit_account, - "against": self.disbursement_account, - "credit": self.monthly_repayment_amount, - "credit_in_account_currency": self.monthly_repayment_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": self.disbursement_date, - } - ) - ) - gle_map.append( - self.get_gl_dict( - { - "account": self.disbursement_account, - "against": self.loan_account, - "credit": -1 * self.monthly_repayment_amount, - "credit_in_account_currency": -1 * self.monthly_repayment_amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": self.disbursement_date, - } - ) + self.add_gl_entry( + gle_map, + security_deposit_account, + self.disbursement_account, + -1 * self.monthly_repayment_amount, + remarks, ) - for charge in self.get("loan_disbursement_charges"): - gle_map.append( - self.get_gl_dict( - { - "account": charge.account, - "against": self.disbursement_account, - "credit": charge.amount, - "credit_in_account_currency": charge.amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": self.disbursement_date, - } - ) + if self.broken_period_interest: + broken_period_interest_account = frappe.db.get_value( + "Loan Product", self.loan_product, "broken_period_interest_recovery_account" ) - - gle_map.append( - self.get_gl_dict( - { - "account": self.disbursement_account, - "against": self.loan_account, - "credit": -1 * charge.amount, - "credit_in_account_currency": -1 * charge.amount, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Disbursement against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": self.disbursement_date, - } - ) + self.add_gl_entry( + gle_map, + broken_period_interest_account, + self.disbursement_account, + -1 * self.broken_period_interest, + remarks, ) + for charge in self.get("loan_disbursement_charges"): + self.add_gl_entry(gle_map, charge.account, self.disbursement_account, charge.amount, remarks) + if gle_map: make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj) diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index 73cc03b5..e6ce1136 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -16,6 +16,7 @@ "rate_of_interest", "posting_date", "adjusted_interest", + "broken_period_interest", "column_break_n6iy", "loan_product", "repayment_frequency", @@ -188,12 +189,18 @@ "fieldtype": "Link", "label": "Loan Disbursement", "options": "Loan Disbursement" + }, + { + "fieldname": "broken_period_interest", + "fieldtype": "Currency", + "label": "Broken Period Interest", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-05 22:50:03.030846", + "modified": "2023-12-10 18:53:50.681242", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Schedule", diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index fd99d9de..35b6fd51 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -37,7 +37,7 @@ def make_repayment_schedule(self): self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.disbursed_amount or self.loan_amount - broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + 1 carry_forward_interest = self.adjusted_interest moratorium_interest = 0 @@ -140,7 +140,7 @@ def get_amounts( additional_days, carry_forward_interest=0, ): - days, months = self.get_days_and_months(payment_date, additional_days) + days, months = self.get_days_and_months(payment_date, additional_days, balance_amount) interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) principal_amount = self.monthly_repayment_amount - flt(interest_amount) balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) @@ -155,7 +155,7 @@ def get_amounts( return interest_amount, principal_amount, balance_amount, total_payment, days - def get_days_and_months(self, payment_date, additional_days): + def get_days_and_months(self, payment_date, additional_days, balance_amount): months = 365 if self.repayment_frequency == "Monthly": if self.repayment_schedule_type == "Monthly as per repayment start date": @@ -173,7 +173,7 @@ def get_days_and_months(self, payment_date, additional_days): additional_days = 0 if additional_days: - days += additional_days + self.add_broken_period_interest(balance_amount, additional_days, payment_date) additional_days = 0 elif expected_payment_date == payment_date: # using 30 days for calculating interest for all full months @@ -182,7 +182,7 @@ def get_days_and_months(self, payment_date, additional_days): days = date_diff(get_last_day(payment_date), payment_date) else: if payment_date == self.repayment_start_date: - days = date_diff(payment_date, self.posting_date) + 1 + days = date_diff(payment_date, self.posting_date) elif self.repayment_frequency == "Weekly": days = 7 elif self.repayment_frequency == "Daily": @@ -194,6 +194,17 @@ def get_days_and_months(self, payment_date, additional_days): return days, months + def add_broken_period_interest(self, balance_amount, additional_days, payment_date): + interest_amount = flt( + balance_amount * flt(self.rate_of_interest) * additional_days / (365 * 100) + ) + payment_date = add_months(payment_date, -1) + self.add_repayment_schedule_row( + payment_date, 0, interest_amount, interest_amount, balance_amount, additional_days + ) + + self.broken_period_interest = interest_amount + def add_repayment_schedule_row( self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount, days ): From b7596a74383fa497cb68d1bc3caae799c7a26aeb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 11 Dec 2023 12:10:28 +0530 Subject: [PATCH 9/9] chore: tenure fixes --- .../loan_repayment_schedule.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index 35b6fd51..63f545e7 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -41,6 +41,13 @@ def make_repayment_schedule(self): carry_forward_interest = self.adjusted_interest moratorium_interest = 0 + tenure = self.repayment_periods + if self.repayment_frequency == "Monthly": + tenure += cint(self.moratorium_tenure) + + if broken_period_interest_days > 0: + tenure += 1 + if self.moratorium_tenure and self.repayment_frequency == "Monthly": payment_date = add_months(self.repayment_start_date, -1 * self.moratorium_tenure) moratorium_end_date = add_months(self.repayment_start_date, -1) @@ -89,10 +96,6 @@ def make_repayment_schedule(self): payment_date, principal_amount, interest_amount, total_payment, balance_amount, days ) - tenure = self.repayment_periods - if self.repayment_frequency == "Monthly": - tenure += cint(self.moratorium_tenure) - if ( self.repayment_method == "Repay Over Number of Periods" and self.repayment_frequency != "One Time"