diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index 4c175382..82da4105 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -37,6 +37,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", @@ -139,7 +145,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 }, { @@ -199,6 +205,7 @@ "fieldname": "monthly_repayment_amount", "fieldtype": "Currency", "label": "Monthly Repayment Amount", + "no_copy": 1, "options": "Company:company:default_currency" }, { @@ -509,12 +516,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 cd19fcd9..1d7b3bc4 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -40,15 +40,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): @@ -116,7 +136,9 @@ def on_submit(self): self.link_loan_security_assignment() # 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_assignment() @@ -160,48 +182,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" @@ -223,7 +203,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 @@ -247,7 +227,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_assignment(self): @@ -974,3 +954,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 47675a49..48b15e5b 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -5,12 +5,22 @@ import frappe from frappe import _ from frappe.query_builder.functions import Sum -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_assignment.loan_security_assignment import ( update_loan_securities_values, ) @@ -26,9 +36,39 @@ 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() @@ -39,6 +79,28 @@ def on_submit(self): 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" @@ -47,16 +109,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() @@ -98,6 +161,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}) @@ -105,12 +186,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.disbursed_amount > possible_disbursal_amount: frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) + 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_of_loan_securities(self, cancel=0): if not frappe.db.get_value("Loan", self.against_loan, "is_secured_loan"): @@ -178,9 +291,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: @@ -210,7 +322,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" @@ -411,7 +525,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 5d4188da..13e3ff8d 100644 --- a/lending/loan_management/doctype/loan_product/loan_product.json +++ b/lending/loan_management/doctype/loan_product/loan_product.json @@ -206,7 +206,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\"", @@ -483,7 +483,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-18 18:32:08.421781", + "modified": "2023-10-30 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 3e7151e3..ab95d75e 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.py @@ -1073,7 +1073,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 29e6461e..a03f212f 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-26 10:08:14.519533", + "modified": "2023-11-03 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)