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 a0692c5a..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,10 +29,19 @@ "column_break_11", "maximum_loan_amount", "repayment_method", + "repayment_frequency", "repayment_periods", "monthly_repayment_amount", "repayment_start_date", + "moratorium_tenure", + "moratorium_type", + "treatment_of_interest", "is_term_loan", + "loan_credit_limits_section", + "limit_applicable_start", + "maximum_limit_amount", + "column_break_foeo", + "limit_applicable_end", "loan_classification_details_section", "days_past_due", "classification_code", @@ -44,6 +52,8 @@ "tenure_post_restructure", "accounting_dimensions_section", "cost_center", + "loan_charges_section", + "loan_charges", "account_info", "disbursement_account", "payment_account", @@ -133,7 +143,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 }, { @@ -154,7 +164,6 @@ "fieldname": "rate_of_interest", "fieldtype": "Percent", "label": "Rate of Interest (%) / Year", - "read_only": 1, "reqd": 1 }, { @@ -165,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" @@ -175,25 +184,26 @@ "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", "fieldtype": "Currency", "label": "Monthly Repayment Amount", + "no_copy": 1, "options": "Company:company:default_currency" }, { @@ -417,12 +427,6 @@ "fieldtype": "Check", "label": "Manual NPA" }, - { - "fieldname": "branch", - "fieldtype": "Link", - "label": "Branch", - "options": "Branch" - }, { "fieldname": "loan_restructure_count", "fieldtype": "Int", @@ -466,18 +470,80 @@ "fieldtype": "Section Break", "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", + "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" + }, + { + "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" + }, + { + "depends_on": "eval: doc.is_term_loan && doc.repayment_schedule_type != \"Line of Credit\"", + "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": "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" + }, { "fetch_from": "loan_product.loan_category", "fieldname": "loan_category", "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-29 19:34:09.691251", + "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 9d7fd578..38381248 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, @@ -36,17 +37,38 @@ def validate(self): self.validate_accounts() self.check_sanctioned_amount_limit() self.set_cyclic_date() - - if self.is_term_loan and not self.is_new(): - self.update_draft_schedule() + 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 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() - 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 [ @@ -72,28 +94,37 @@ 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": - 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) + if ( + self.repayment_schedule_type == "Monthly as per cycle date" + and self.repayment_frequency == "Monthly" + ): + cyclic_date = get_cyclic_date(self.loan_product, self.posting_date) + self.repayment_start_date = cyclic_date - last_day_of_month = get_last_day(self.posting_date) - cyclic_date = add_days(last_day_of_month, cycle_day) + if self.moratorium_tenure: + self.repayment_start_date = add_months(self.repayment_start_date, self.moratorium_tenure) - 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) + 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" + ) - self.repayment_start_date = cyclic_date + 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 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() @@ -137,42 +168,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, - } - ).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, - } - ) - schedule.save() - def submit_draft_schedule(self): draft_schedule = frappe.db.get_value( "Loan Repayment Schedule", {"loan": self.name, "docstatus": 0}, "name" @@ -194,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: - 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: @@ -218,7 +214,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): @@ -384,16 +380,39 @@ def close_unsecured_term_loan(loan): @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, + repayment_start_date=None, + repayment_frequency=None, + 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.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"): + 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: @@ -491,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 @@ -909,3 +928,22 @@ def move_unpaid_interest_to_suspense_ledger( jv.flags.ignore_mandatory = True jv.submit() + + +@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) + + 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_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.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..4120129e 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -7,13 +7,20 @@ "engine": "InnoDB", "field_order": [ "against_loan", + "sanctioned_loan_amount", + "current_disbursed_amount", "posting_date", "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", @@ -21,6 +28,7 @@ "clearance_date", "column_break_8", "disbursed_amount", + "broken_period_interest", "accounting_dimensions_section", "cost_center", "charges_section", @@ -226,12 +234,64 @@ "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", + "hidden": 1, + "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" + }, + { + "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" + }, + { + "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 + }, + { + "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-10-10 19:19:44.826241", + "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 bcf178d4..c631ce9b 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -4,12 +4,23 @@ import frappe from frappe import _ -from frappe.utils import add_days, flt, get_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + 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 get_cyclic_date from lending.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( get_pledged_security_qty, ) @@ -22,15 +33,155 @@ 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(): + self.update_draft_schedule() + + 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 get_schedule_details(self): + disbursed_amount = self.get_disbursed_amount() + + 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, + "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 + + 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 + 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 get_draft_schedule(self): + return frappe.db.get_value( + "Loan Repayment Schedule", {"loan": self.against_loan, "docstatus": 0}, "name" + ) + + 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: + schedule = frappe.get_doc("Loan Repayment Schedule", draft_schedule) + 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() + 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): + 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_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" @@ -39,17 +190,19 @@ 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.update_current_repayment_schedule(cancel=1) self.delete_security_deposit() self.set_status_and_amounts(cancel=1) @@ -66,6 +219,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( @@ -78,6 +234,24 @@ def withheld_security_deposit(self): ).insert() sd.submit() + def set_cyclic_date(self): + 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, + ["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}) @@ -85,14 +259,38 @@ 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", + "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.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", @@ -136,9 +334,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: @@ -168,26 +365,26 @@ 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" 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, @@ -196,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) @@ -369,7 +529,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_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_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_product/loan_product.json b/lending/loan_management/doctype/loan_product/loan_product.json index 75c3678c..63965ff4 100644 --- a/lending/loan_management/doctype/loan_product/loan_product.json +++ b/lending/loan_management/doctype/loan_product/loan_product.json @@ -198,7 +198,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\"", @@ -475,7 +475,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-11-29 19:33:39.463380", + "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.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/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 f308e706..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 @@ -8,19 +8,26 @@ "engine": "InnoDB", "field_order": [ "loan", + "loan_disbursement", "company", "loan_restructure", "loan_amount", + "disbursed_amount", "rate_of_interest", "posting_date", "adjusted_interest", + "broken_period_interest", "column_break_n6iy", "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", @@ -36,6 +43,7 @@ "reqd": 1 }, { + "fetch_from": "loan.loan_product", "fieldname": "loan_product", "fieldtype": "Link", "in_list_view": 1, @@ -59,6 +67,7 @@ "read_only": 1 }, { + "fetch_from": "loan.loan_amount", "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", @@ -83,10 +92,9 @@ "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, "fieldname": "repayment_start_date", "fieldtype": "Date", "label": "Repayment Start Date" @@ -143,12 +151,56 @@ "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "repayment_frequency", + "fieldtype": "Select", + "label": "Repayment Frequency", + "options": "Monthly\nDaily\nWeekly\nQuarterly\nOne Time" + }, + { + "fetch_from": "loan.treatment_of_interest", + "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" + }, + { + "fetch_from": "loan.moratorium_tenure", + "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" + }, + { + "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-10-02 22:14:24.172876", + "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 0e5a2aea..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 @@ -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): @@ -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): @@ -31,29 +34,60 @@ 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) + balance_amount = self.disbursed_amount or self.loan_amount + broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + 1 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) + broken_period_interest_days = date_diff(add_months(payment_date, -1), self.posting_date) + 1 while balance_amount > 0: + 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, self.repayment_frequency + ) + moratorium_interest = 0 + interest_amount, principal_amount, balance_amount, total_payment, days = self.get_amounts( payment_date, balance_amount, - 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.moratorium_tenure and self.repayment_frequency == "Monthly": + 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: + 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 @@ -64,7 +98,8 @@ def make_repayment_schedule(self): if ( self.repayment_method == "Repay Over Number of Periods" - and len(self.get("repayment_schedule")) >= self.repayment_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 @@ -75,15 +110,21 @@ 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 + 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_frequency == "Quarterly": + payment_date = add_months(payment_date, 3) - bmi_days = 0 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: @@ -99,37 +140,10 @@ def get_amounts( self, payment_date, balance_amount, - schedule_type, - repayment_date_on, additional_days, carry_forward_interest=0, ): - 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 - + 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) @@ -144,6 +158,56 @@ def get_amounts( return interest_amount, principal_amount, balance_amount, total_payment, 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": + days = 1 + months = 12 + else: + 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) + + if self.repayment_schedule_type == "Monthly as per cycle date": + days = date_diff(payment_date, add_months(payment_date, -1)) + if additional_days < 0: + days = date_diff(payment_date, self.posting_date) + additional_days = 0 + + if 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 + days = 30 + else: + 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) + 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 + + 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 ): @@ -167,9 +231,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) @@ -177,3 +244,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)