diff --git a/lending/loan_management/doctype/loan/loan.js b/lending/loan_management/doctype/loan/loan.js index aa637736..6c4a55a1 100644 --- a/lending/loan_management/doctype/loan/loan.js +++ b/lending/loan_management/doctype/loan/loan.js @@ -75,7 +75,7 @@ frappe.ui.form.on('Loan', { },__('Create')); } - if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) { + if (["Sanctioned", "Partially Disbursed", "Active"].includes(frm.doc.status)) { frm.add_custom_button(__('Loan Disbursement'), function() { frm.trigger("make_loan_disbursement"); },__('Create')); @@ -134,6 +134,7 @@ frappe.ui.form.on('Loan', { frm.doc.loan_amount - frm.doc.disbursed_amount : 0, "as_dict": 1, "repayment_frequency": frm.doc.repayment_frequency, + "is_term_loan": frm.doc.is_term_loan }, 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 7e45002c..8aedc491 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -154,6 +154,7 @@ "label": "Loan Details" }, { + "depends_on": "eval:doc.repayment_schedule_type!=\"Line of Credit\"", "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", @@ -404,6 +405,7 @@ "fieldname": "is_npa", "fieldtype": "Check", "label": "Is NPA", + "no_copy": 1, "read_only": 1 }, { @@ -411,6 +413,7 @@ "fetch_from": "loan_product.repayment_schedule_type", "fieldname": "repayment_schedule_type", "fieldtype": "Data", + "hidden": 1, "label": "Repayment Schedule Type", "read_only": 1 }, @@ -428,12 +431,14 @@ "description": "Manually marked as NPA", "fieldname": "manual_npa", "fieldtype": "Check", - "label": "Manual NPA" + "label": "Manual NPA", + "no_copy": 1 }, { "fieldname": "loan_restructure_count", "fieldtype": "Int", "label": "Loan Restructure Count", + "no_copy": 1, "read_only": 1 }, { @@ -444,12 +449,14 @@ "fieldname": "watch_period_end_date", "fieldtype": "Date", "label": "Watch Period End Date", + "no_copy": 1, "read_only": 1 }, { "fieldname": "tenure_post_restructure", "fieldtype": "Int", "label": "Tenure Post Restructure", + "no_copy": 1, "read_only": 1 }, { @@ -506,6 +513,7 @@ "label": "Moratorium Tenure" }, { + "depends_on": "eval:doc.repayment_schedule_type == \"Line of Credit\"", "fieldname": "loan_credit_limits_section", "fieldtype": "Section Break", "label": "Loan Credit Limits" @@ -513,7 +521,9 @@ { "fieldname": "limit_applicable_start", "fieldtype": "Date", - "label": "Limit Applicable Start" + "label": "Limit Applicable Start", + "mandatory_depends_on": "eval: doc.repayment_schedule_type == \"Line of Credit\"", + "no_copy": 1 }, { "fieldname": "column_break_foeo", @@ -522,13 +532,17 @@ { "fieldname": "limit_applicable_end", "fieldtype": "Date", - "label": "Limit Applicable End" + "label": "Limit Applicable End", + "mandatory_depends_on": "eval: doc.repayment_schedule_type == \"Line of Credit\"", + "no_copy": 1 }, { "allow_on_submit": 1, "fieldname": "maximum_limit_amount", "fieldtype": "Currency", - "label": "Maximum Limit Amount" + "label": "Maximum Limit Amount", + "mandatory_depends_on": "eval: doc.repayment_schedule_type == \"Line of Credit\"", + "no_copy": 1 }, { "fetch_from": "loan_product.loan_category", @@ -542,12 +556,14 @@ "fieldname": "available_limit_amount", "fieldtype": "Currency", "label": "Available Limit Amount", + "no_copy": 1, "read_only": 1 }, { "fieldname": "utilized_limit_amount", "fieldtype": "Currency", "label": "Utilized Limit Amount", + "no_copy": 1, "read_only": 1 }, { @@ -561,7 +577,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-01-07 21:43:54.755239", + "modified": "2024-01-11 16:49:57.074283", "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 69ba14c3..033ce35a 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -39,6 +39,7 @@ def validate(self): self.set_cyclic_date() self.set_default_charge_account() self.set_available_limit_amount() + self.validate_repayment_terms() # if self.is_term_loan and not self.is_new() and self.repayment_schedule_type != "Line of Credit": # update_draft_schedule( @@ -122,6 +123,11 @@ def set_default_charge_account(self): def set_available_limit_amount(self): self.available_limit_amount = self.maximum_limit_amount + def validate_repayment_terms(self): + if self.is_term_loan and self.repayment_schedule_type != "Line of Credit": + if not self.repayment_periods: + frappe.throw(_("Repayment periods is mandatory for term loans")) + def on_submit(self): self.link_loan_security_pledge() # Interest accrual for backdated term loans @@ -221,15 +227,18 @@ def calculate_totals(self, on_insert=False): self.db_set("total_payment", self.total_payment) def set_loan_amount(self): + if self.repayment_schedule_type == "Line of Credit": + self.loan_amount = self.maximum_limit_amount + if self.loan_application and not self.loan_amount: self.loan_amount = frappe.db.get_value("Loan Application", self.loan_application, "loan_amount") def validate_loan_amount(self): - if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount: - msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_amount) + if self.maximum_limit_amount and self.loan_amount > self.maximum_limit_amount: + msg = _("Loan amount cannot be greater than {0}").format(self.maximum_limit_amount) frappe.throw(msg) - if not self.loan_amount and self.repayment_schedule_type != "Line of Credit": + if not self.loan_amount: frappe.throw(_("Loan amount is mandatory")) def link_loan_security_pledge(self): @@ -405,6 +414,7 @@ def make_loan_disbursement( posting_date=None, disbursement_date=None, bank_account=None, + is_term_loan=None, ): loan_doc = frappe.get_doc("Loan", loan) disbursement_entry = frappe.new_doc("Loan Disbursement") @@ -418,6 +428,7 @@ def make_loan_disbursement( disbursement_entry.repayment_start_date = repayment_start_date disbursement_entry.repayment_frequency = repayment_frequency disbursement_entry.disbursed_amount = disbursement_amount + disbursement_entry.is_term_loan = is_term_loan for charge in loan_doc.get("loan_charges"): disbursement_entry.append( diff --git a/lending/loan_management/doctype/loan/loan_list.js b/lending/loan_management/doctype/loan/loan_list.js index 6591b729..2f09d6b3 100644 --- a/lending/loan_management/doctype/loan/loan_list.js +++ b/lending/loan_management/doctype/loan/loan_list.js @@ -7,6 +7,7 @@ frappe.listview_settings['Loan'] = { "Draft": "red", "Sanctioned": "blue", "Disbursed": "orange", + "Active": "orange", "Partially Disbursed": "yellow", "Loan Closure Requested": "green", "Closed": "green" diff --git a/lending/loan_management/doctype/loan_demand/loan_demand.json b/lending/loan_management/doctype/loan_demand/loan_demand.json index 4d3a051a..f4211001 100644 --- a/lending/loan_management/doctype/loan_demand/loan_demand.json +++ b/lending/loan_management/doctype/loan_demand/loan_demand.json @@ -7,6 +7,8 @@ "field_order": [ "loan_demand_details_section", "loan", + "loan_repayment_schedule", + "loan_disbursement", "repayment_schedule_detail", "applicant_type", "applicant", @@ -18,6 +20,7 @@ "column_break_qrjm", "company", "demand_date", + "disbursement_date", "demand_amount", "paid_amount", "waived_amount", @@ -116,10 +119,9 @@ }, { "fieldname": "demand_subtype", - "fieldtype": "Select", + "fieldtype": "Data", "in_list_view": 1, - "label": "Demand Subtype", - "options": "Principal\nInterest\nPenalty\nCharges" + "label": "Demand Subtype" }, { "fetch_from": "loan.company", @@ -151,12 +153,31 @@ "fieldtype": "Link", "label": "Process Loan Demand", "options": "Process Loan Demand" + }, + { + "fieldname": "loan_repayment_schedule", + "fieldtype": "Link", + "label": "Loan Repayment Schedule", + "options": "Loan Repayment Schedule" + }, + { + "fetch_from": ".", + "fieldname": "loan_disbursement", + "fieldtype": "Link", + "label": "Loan Disbursement", + "options": "Loan Disbursement" + }, + { + "fetch_from": "loan_repayment_schedule.posting_date", + "fieldname": "disbursement_date", + "fieldtype": "Date", + "label": "Disbursement Date" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-01-06 16:22:37.771957", + "modified": "2024-01-11 13:43:39.861311", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Demand", diff --git a/lending/loan_management/doctype/loan_demand/loan_demand.py b/lending/loan_management/doctype/loan_demand/loan_demand.py index 1b62dcdf..0a887b85 100644 --- a/lending/loan_management/doctype/loan_demand/loan_demand.py +++ b/lending/loan_management/doctype/loan_demand/loan_demand.py @@ -101,15 +101,19 @@ def make_loan_demand_for_term_loans( open_loans = frappe.db.get_all("Loan", filters=filters, pluck="name") - loan_repayment_schedule_map = frappe._dict( - frappe.db.get_all( - "Loan Repayment Schedule", - filters={"docstatus": 1, "status": "Active", "loan": ("in", open_loans)}, - fields=["name", "loan"], - as_list=1, - ) + loan_repayment_schedules = frappe.db.get_all( + "Loan Repayment Schedule", + filters={"docstatus": 1, "status": "Active", "loan": ("in", open_loans)}, + fields=["name", "loan", "loan_disbursement"], ) + loan_repayment_schedule_map = frappe._dict() + disbursement_map = frappe._dict() + + for schedule in loan_repayment_schedules: + loan_repayment_schedule_map[schedule.name] = schedule.loan + disbursement_map[schedule.name] = schedule.loan_disbursement + repayment_schedules = loan_repayment_schedule_map.keys() emi_rows = frappe.db.get_all( @@ -125,6 +129,8 @@ def make_loan_demand_for_term_loans( for row in emi_rows: create_loan_demand( loan_repayment_schedule_map.get(row.parent), + row.parent, + disbursement_map.get(row.parent), row.payment_date, "EMI", "Interest", @@ -134,6 +140,8 @@ def make_loan_demand_for_term_loans( ) create_loan_demand( loan_repayment_schedule_map.get(row.parent), + row.parent, + disbursement_map.get(row.parent), row.payment_date, "EMI", "Principal", @@ -145,6 +153,8 @@ def make_loan_demand_for_term_loans( def create_loan_demand( loan, + loan_repayment_schedule, + loan_disbursement, posting_date, demand_type, demand_subtype, @@ -156,6 +166,8 @@ def create_loan_demand( if amount: demand = frappe.new_doc("Loan Demand") demand.loan = loan + demand.loan_repayment_schedule = loan_repayment_schedule + demand.loan_disbursement = loan_disbursement demand.repayment_schedule_detail = repayment_schedule_detail demand.demand_date = posting_date demand.demand_type = demand_type diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js index 4a881909..621320ac 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.js @@ -12,7 +12,7 @@ frappe.ui.form.on('Loan Disbursement', { return { 'filters': { 'docstatus': 1, - "status": ["in",["Sanctioned","Active"]], + "status": ["in",["Sanctioned","Active", "Partially Disbursed"]], } } }) diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json index 2fdc2caa..8d83234a 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -13,13 +13,13 @@ "applicant_type", "loan_product", "monthly_repayment_amount", - "tenure", "column_break_4", "company", "applicant", "repayment_schedule_type", "repayment_frequency", "repayment_method", + "tenure", "repayment_start_date", "is_term_loan", "withhold_security_deposit", @@ -247,7 +247,8 @@ "fieldname": "repayment_schedule_type", "fieldtype": "Data", "hidden": 1, - "label": "Repayment Schedule Type" + "label": "Repayment Schedule Type", + "read_only": 1 }, { "fetch_from": "against_loan.repayment_start_date", @@ -294,7 +295,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-12-29 20:32:10.687328", + "modified": "2024-01-10 20:01:44.186607", "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 f782bff6..6ef5e919 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -124,15 +124,14 @@ def submit_repayment_schedule(self): 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() + 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 top up @@ -171,13 +170,13 @@ def update_repayment_schedule_status(self, cancel=0): def on_cancel(self): self.flags.ignore_links = ["GL Entry", "Loan Repayment Schedule"] + self.set_status_and_amounts(cancel=1) + 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) + self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] @@ -346,16 +345,9 @@ def get_values_on_cancel(self, loan_details): else: status = "Partially Disbursed" - new_available_limit_amount = ( - loan_details.available_limit_amount + self.disbursed_amount - if loan_details.available_limit_amount - else 0.0 - ) - new_utilized_limit_amount = ( - loan_details.utilized_limit_amount - self.disbursed_amount - if loan_details.utilized_limit_amount - else 0.0 - ) + new_available_limit_amount = loan_details.available_limit_amount + self.disbursed_amount + + new_utilized_limit_amount = loan_details.utilized_limit_amount - self.disbursed_amount return ( disbursed_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 74b8810f..2e5d456f 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 @@ -281,7 +281,15 @@ def calculate_penal_interest_for_loans(loan, posting_date, process_loan_interest loan_demand=demand.name, ) - create_loan_demand(loan.name, posting_date, "Penalty", "Penalty", penal_interest_amount) + create_loan_demand( + loan.name, + demand.loan_repayment_schedule, + loan.loan_disbursement, + posting_date, + "Penalty", + "Penalty", + penal_interest_amount, + ) def make_accrual_interest_entry_for_loans( diff --git a/lending/loan_management/doctype/loan_repayment/loan_repayment.json b/lending/loan_management/doctype/loan_repayment/loan_repayment.json index d9bff0ae..9d1a3a2a 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.json @@ -7,6 +7,8 @@ "engine": "InnoDB", "field_order": [ "against_loan", + "loan_disbursement", + "repayment_schedule_type", "applicant_type", "applicant", "loan_product", @@ -41,11 +43,12 @@ "total_paid_charges", "references_section", "reference_number", + "total_interest_paid", + "total_penalty_paid", "column_break_21", "reference_date", "principal_amount_paid", - "total_penalty_paid", - "total_interest_paid", + "total_charges_paid", "section_break_55wc", "repayment_details", "amended_from", @@ -374,12 +377,29 @@ "fieldname": "select_charge_manually", "fieldtype": "Check", "label": "Select Charge Manually" + }, + { + "fetch_from": "against_loan.repayment_schedule_type", + "fieldname": "repayment_schedule_type", + "fieldtype": "Data", + "label": "Repayment Schedule Type" + }, + { + "fieldname": "loan_disbursement", + "fieldtype": "Link", + "label": "Loan Disbursement", + "options": "Loan Disbursement" + }, + { + "fieldname": "total_charges_paid", + "fieldtype": "Currency", + "label": "Total Charges Paid" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-01-08 14:52:37.305970", + "modified": "2024-01-11 20:00:41.004851", "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 98cf0be4..de4909bb 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.py @@ -4,7 +4,7 @@ import frappe from frappe import _ -from frappe.utils import cint, date_diff, flt, get_datetime, getdate +from frappe.utils import cint, flt, get_datetime, getdate import erpnext from erpnext.accounts.general_ledger import make_gl_entries @@ -24,23 +24,15 @@ # create_process_loan_classification, # ) -# from lending.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( -# process_loan_interest_accrual_for_loans, -# ) - class LoanRepayment(AccountsController): def validate(self): amounts = calculate_amounts(self.against_loan, self.posting_date) - self.add_pending_charges() self.set_missing_values(amounts) self.check_future_entries() self.validate_amount() self.allocate_amount_against_demands(amounts) - # def before_submit(self): - # self.book_unaccrued_interest() - def on_submit(self): # if self.repayment_type == "Normal Repayment": # create_process_loan_classification( @@ -50,7 +42,6 @@ def on_submit(self): # payment_reference=self.name, # ) - # self.update_repayment_schedule() self.update_paid_amounts() if self.repayment_type == "Charges Waiver": @@ -60,7 +51,7 @@ def on_submit(self): def on_cancel(self): self.check_future_accruals() - # self.update_repayment_schedule(cancel=1) + if self.repayment_type == "Normal Repayment": self.mark_as_unpaid() if self.is_npa or self.manual_npa: @@ -138,17 +129,6 @@ def set_missing_values(self, amounts): if not self.payable_amount: self.payable_amount = flt(amounts["payable_amount"], precision) - if not self.get("pending_charges"): - for d in amounts.get("charges"): - self.append( - { - "sales_invoice": d.sales_invoice, - "pending_charge_amount": d.pending_charge_amount, - } - ) - - self.total_charges_payable = flt(amounts["total_charges_payable"], precision) - shortfall_amount = flt( frappe.db.get_value( "Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount" @@ -181,70 +161,8 @@ def validate_amount(self): if not self.amount_paid: frappe.throw(_("Amount paid cannot be zero")) - # def book_unaccrued_interest(self): - # precision = cint(frappe.db.get_default("currency_precision")) or 2 - # if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision): - # if not self.is_term_loan: - # # get last loan interest accrual date - # last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date) - - # # get posting date upto which interest has to be accrued - # per_day_interest = get_per_day_interest( - # self.pending_principal_amount, self.rate_of_interest, self.company, self.posting_date - # ) - - # no_of_days = ( - # flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0) - # - 1 - # ) - - # posting_date = add_days(last_accrual_date, no_of_days) - - # # book excess interest paid - # process = process_loan_interest_accrual_for_loans( - # posting_date=posting_date, loan=self.against_loan, accrual_type="Repayment" - # ) - - # # get loan interest accrual to update paid amount - # lia = frappe.db.get_value( - # "Loan Interest Accrual", - # {"process_loan_interest_accrual": process}, - # ["name", "interest_amount", "payable_principal_amount"], - # as_dict=1, - # ) - - # if lia: - # self.append( - # "repayment_details", - # { - # "loan_interest_accrual": lia.name, - # "paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision), - # "paid_principal_amount": 0.0, - # "accrual_type": "Repayment", - # }, - # ) - - def add_pending_charges(self): - self.set("pending_charges", []) - invoices = get_outstanding_invoices(self.against_loan, self.posting_date) - for d in invoices: - self.append( - "pending_charges", - { - "sales_invoice": d.voucher_no, - "pending_charge_amount": d.outstanding_amount, - }, - ) - def update_paid_amounts(self): - total_interest_paid = 0 - for payment in self.repayment_details: - if payment.demand_type == "Interest": - total_interest_paid += payment.paid_amount - elif payment.demand_type == "Principal": - self.principal_amount_paid += payment.paid_amount - loan_demand = frappe.qb.DocType("Loan Demand") frappe.qb.update(loan_demand).set( loan_demand.paid_amount, loan_demand.paid_amount + payment.paid_amount @@ -270,13 +188,17 @@ def update_paid_amounts(self): update_shortfall_status(self.against_loan, self.principal_amount_paid) - def mark_as_unpaid(self): - total_interest_paid = 0 + if self.repayment_schedule_type == "Line of Credit": + frappe.qb.update(loan).set( + loan.available_limit_amount, loan.available_limit_amount + self.principal_amount_paid + ).set( + loan.utilized_limit_amount, loan.utilized_limit_amount - self.principal_amount_paid + ).where( + loan.name == self.against_loan + ).run() + def mark_as_unpaid(self): for payment in self.repayment_details: - if payment.demand_type == "Interest": - total_interest_paid -= payment.paid_amount - loan_demand = frappe.qb.DocType("Loan Demand") frappe.qb.update(loan_demand).set( @@ -297,6 +219,15 @@ def mark_as_unpaid(self): loan.name == self.against_loan ).run() + if self.repayment_schedule_type == "Line of Credit": + frappe.qb.update(loan).set( + loan.available_limit_amount, loan.available_limit_amount - self.principal_amount_paid + ).set( + loan.utilized_limit_amount, loan.utilized_limit_amount + self.principal_amount_paid + ).where( + loan.name == self.against_loan + ).run() + def check_future_accruals(self): if self.is_term_loan: return @@ -314,15 +245,14 @@ def check_future_accruals(self): ) ) - # def update_repayment_schedule(self, cancel=0): - # if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount: - # regenerate_repayment_schedule(self.against_loan, cancel) - def allocate_amount_against_demands(self, amounts): # precision = cint(frappe.db.get_default("currency_precision")) or 2 self.set("repayment_details", []) self.principal_amount_paid = 0 self.total_penalty_paid = 0 + self.total_interest_paid = 0 + self.total_charges_paid = 0 + amount_paid = self.amount_paid if self.manual_npa: @@ -344,48 +274,20 @@ def allocate_amount_against_demands(self, amounts): allocation_order, amount_paid, amounts.get("unpaid_demands") ) + for payment in self.repayment_details: + if payment.demand_type == "Interest": + self.total_interest_paid += payment.paid_amount + elif payment.demand_type == "Principal": + self.principal_amount_paid += payment.paid_amount + elif payment.demand_type == "Penalty": + self.total_penalty_paid += payment.paid_amount + elif payment.demand_type == "Charges": + self.total_charges_paid += payment.paid_amount + if amount_paid > 0: - self.principal_amount_paid = amount_paid + self.principal_amount_paid += amount_paid amount_paid = 0 - # if interest_paid > 0 and not self.is_term_loan: - # if self.penalty_amount and interest_paid > self.penalty_amount: - # self.total_penalty_paid = flt(self.penalty_amount, precision) - # elif self.penalty_amount: - # self.total_penalty_paid = flt(interest_paid, precision) - - # interest_paid -= self.total_penalty_paid - - # interest_paid, updated_entries = self.allocate_interest_amount( - # interest_paid, repayment_details, {} - # ) - # self.allocate_excess_payment_for_demand_loans(interest_paid, repayment_details) - - # if self.is_term_loan: - # if self.repayment_type == "Normal Repayment": - # if self.offset_based_on_npa: - # self.offset_repayment_based_on_npa(interest_paid, repayment_details) - # else: - # interest_paid, updated_entries = self.allocate_interest_amount( - # interest_paid, repayment_details - # ) - # self.allocate_principal_amount_for_term_loans( - # interest_paid, repayment_details, updated_entries - # ) - # elif self.repayment_type in ("Principal Adjustment", "Principal Capitalization"): - # self.allocate_principal_amount_for_term_loans(interest_paid, repayment_details, {}) - # elif self.repayment_type in ( - # "Interest Waiver", - # "Interest Capitalization", - # "Interest Adjustment", - # "Interest Carry Forward", - # ): - # self.allocate_interest_amount(interest_paid, repayment_details) - # elif self.repayment_type in ("Penalty Waiver", "Penalty Capitalization"): - # self.allocate_penalty(interest_paid) - # elif self.repayment_type in ("Charges Waiver", "Charges Capitalization"): - # self.allocate_charges(interest_paid) - def apply_allocation_order(self, allocation_order, pending_amount, demands): """Allocate amount based on allocation order""" allocation_order_doc = frappe.get_doc("Loan Demand Offset Order", allocation_order) @@ -431,205 +333,6 @@ def adjust_component(self, amount_to_adjust, demand_type, demand_subtypes, deman return amount_to_adjust - # def offset_repayment_based_on_npa(self, interest_paid, repayment_details): - # if interest_paid > 0: - # offset_base_on = frappe.db.get_value( - # "Company", - # self.company, - # [ - # "collection_offset_logic_based_on", - # "days_past_due_threshold", - # "collection_offset_sequence_for_standard_asset", - # "collection_offset_sequence_for_sub_standard_asset", - # ], - # as_dict=1, - # ) - - # if ( - # offset_base_on.collection_offset_logic_based_on == "NPA Flag" - # and (self.is_npa or self.manual_npa) - # ) or ( - # offset_base_on.collection_offset_logic_based_on == "Days Past Due" - # and self.days_past_due > cint(offset_base_on.days_past_due_threshold) - # ): - # if offset_base_on.collection_offset_sequence_for_sub_standard_asset == "PPP...III...CCC": - # self.allocate_as_per_npa(interest_paid, repayment_details) - # else: - # self.allocate_as_per_non_npa(interest_paid, repayment_details) - # else: - # if offset_base_on.collection_offset_sequence_for_standard_asset == "IP...IP...IP...CCC": - # self.allocate_as_per_non_npa(interest_paid, repayment_details) - # else: - # self.allocate_as_per_npa(interest_paid, repayment_details) - - # def allocate_as_per_non_npa(self, interest_paid, repayment_details): - # self.total_interest_paid = 0 - # for lia, amounts in repayment_details.get("pending_accrual_entries", []).items(): - # interest_amount = 0 - # principal_amount = 0 - # if amounts["interest_amount"] <= interest_paid: - # interest_amount = amounts["interest_amount"] - # interest_paid -= interest_amount - # self.total_interest_paid += amounts["interest_amount"] - # if amounts["payable_principal_amount"] <= interest_paid: - # principal_amount = amounts["payable_principal_amount"] - # interest_paid -= principal_amount - # self.principal_amount_paid += principal_amount - # elif interest_paid: - # principal_amount = interest_paid - # interest_paid = 0 - # self.principal_amount_paid += principal_amount - # elif interest_paid: - # interest_amount = interest_paid - # interest_paid = 0 - # self.total_interest_paid += interest_amount - - # if interest_amount or principal_amount: - # self.append( - # "repayment_details", - # { - # "loan_interest_accrual": lia, - # "paid_principal_amount": principal_amount, - # "paid_interest_amount": interest_amount, - # }, - # ) - # interest_paid = self.allocate_penalty(interest_paid) - # self.allocate_charges(interest_paid) - - # def allocate_as_per_npa(self, interest_paid, repayment_details): - # interest_paid, updated_entries = self.allocate_principal_amount_for_term_loans( - # interest_paid, repayment_details, {} - # ) - # interest_paid, updated_entries = self.allocate_interest_amount( - # interest_paid, repayment_details, updated_entries - # ) - # interest_paid = self.allocate_penalty(interest_paid) - # self.allocate_charges(interest_paid) - - # def allocate_interest_amount(self, interest_paid, repayment_details, updated_entries=None): - # self.total_interest_paid = 0 - # idx = 1 - # if not updated_entries: - # updated_entries = {} - - # if interest_paid > 0: - # for lia, amounts in repayment_details.get("pending_accrual_entries", []).items(): - # interest_amount = 0 - # if amounts["interest_amount"] <= interest_paid: - # interest_amount = amounts["interest_amount"] - # self.total_interest_paid += interest_amount - # interest_paid -= interest_amount - # elif interest_paid: - # if interest_paid >= amounts["interest_amount"]: - # interest_amount = amounts["interest_amount"] - # self.total_interest_paid += interest_amount - # interest_paid = 0 - # else: - # interest_amount = interest_paid - # self.total_interest_paid += interest_amount - # interest_paid = 0 - - # if updated_entries.get(lia): - # idx = updated_entries.get(lia) - # self.get("repayment_details")[idx - 1].paid_interest_amount += interest_amount - # if interest_amount: - # self.append( - # "repayment_details", - # { - # "loan_interest_accrual": lia, - # "paid_interest_amount": interest_amount, - # "paid_principal_amount": 0, - # }, - # ) - # updated_entries[lia] = idx - # idx += 1 - - # return interest_paid, updated_entries - - # def allocate_principal_amount_for_term_loans( - # self, interest_paid, repayment_details, updated_entries - # ): - # if interest_paid > 0: - # for lia, amounts in repayment_details.get("pending_accrual_entries", []).items(): - # paid_principal = 0 - # if amounts["payable_principal_amount"] <= interest_paid: - # paid_principal = amounts["payable_principal_amount"] - # self.principal_amount_paid += paid_principal - # interest_paid -= paid_principal - # elif interest_paid: - # if interest_paid >= amounts["payable_principal_amount"]: - # paid_principal = amounts["payable_principal_amount"] - # self.principal_amount_paid += paid_principal - # interest_paid = 0 - # else: - # paid_principal = interest_paid - # self.principal_amount_paid += paid_principal - # interest_paid = 0 - - # if updated_entries.get(lia): - # idx = updated_entries.get(lia) - # self.get("repayment_details")[idx - 1].paid_principal_amount += paid_principal - # else: - # self.append( - # "repayment_details", - # { - # "loan_interest_accrual": lia, - # "paid_interest_amount": 0, - # "paid_principal_amount": paid_principal, - # }, - # ) - - # return interest_paid, updated_entries - - # def allocate_penalty(self, interest_paid): - # precision = cint(frappe.db.get_default("currency_precision")) or 2 - # if interest_paid > 0: - # if self.penalty_amount and interest_paid > self.penalty_amount: - # self.total_penalty_paid = flt(self.penalty_amount, precision) - # interest_paid -= self.penalty_amount - # elif self.penalty_amount: - # self.total_penalty_paid = flt(interest_paid, precision) - # interest_paid = 0 - - # return interest_paid - - # def allocate_charges(self, interest_paid): - # precision = cint(frappe.db.get_default("currency_precision")) or 2 - # self.total_paid_charges = 0 - # if interest_paid > 0: - # for charge in self.get("pending_charges"): - # charge.allocated_amount = 0 - # if charge.pending_charge_amount and interest_paid > charge.pending_charge_amount: - # charge.allocated_amount = charge.pending_charge_amount - # interest_paid -= charge.pending_charge_amount - # self.total_paid_charges += charge.allocated_amount - # elif charge.pending_charge_amount: - # charge.allocated_amount = interest_paid - # interest_paid = 0 - # self.total_paid_charges += charge.allocated_amount - - # charge.allocated_amount = flt(charge.allocated_amount, precision) - - # def allocate_excess_payment_to_principal(self, amount_paid): - # if repayment_details["unaccrued_interest"] and interest_paid > 0: - # # no of days for which to accrue interest - # # Interest can only be accrued for an entire day and not partial - # if interest_paid > repayment_details["unaccrued_interest"]: - # interest_paid -= repayment_details["unaccrued_interest"] - # self.total_interest_paid += repayment_details["unaccrued_interest"] - # else: - # # get no of days for which interest can be paid - # per_day_interest = get_per_day_interest( - # self.pending_principal_amount, self.rate_of_interest, self.company, self.posting_date - # ) - - # no_of_days = cint(interest_paid / per_day_interest) - # self.total_interest_paid += no_of_days * per_day_interest - # interest_paid -= no_of_days * per_day_interest - - # if interest_paid > 0: - # self.principal_amount_paid += interest_paid - def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] remarks = self.get_remarks() @@ -895,6 +598,8 @@ def get_unpaid_demands(against_loan, posting_date=None): frappe.qb.from_(loan_demand) .select( loan_demand.name, + loan_demand.loan_repayment_schedule, + loan_demand.loan_disbursement, loan_demand.demand_date, loan_demand.last_repayment_date, (loan_demand.demand_amount - loan_demand.paid_amount).as_("demand_amount"), @@ -907,9 +612,10 @@ def get_unpaid_demands(against_loan, posting_date=None): & (loan_demand.demand_date <= posting_date) & (loan_demand.demand_amount - loan_demand.paid_amount > 0) ) - .orderby(loan_demand.demand_date) - .orderby(loan_demand.demand_subtype) + .orderby(loan_demand.disbursement_date) .orderby(loan_demand.repayment_schedule_detail) + .orderby(loan_demand.demand_type) + .orderby(loan_demand.demand_subtype) .run(as_dict=1) ) @@ -932,75 +638,6 @@ def get_penalty_details(against_loan): return None, 0 -# def regenerate_repayment_schedule(loan, cancel=0): -# from lending.loan_management.doctype.loan.loan import ( -# add_single_month, -# get_monthly_repayment_amount, -# ) - -# precision = cint(frappe.db.get_default("currency_precision")) or 2 -# loan_doc = frappe.get_doc("Loan", loan) -# next_accrual_date = None -# accrued_entries = 0 -# last_repayment_amount = None -# last_balance_amount = None - -# for term in reversed(loan_doc.get("repayment_schedule")): -# if not term.is_accrued: -# next_accrual_date = term.payment_date -# loan_doc.remove(term) -# else: -# accrued_entries += 1 -# if last_repayment_amount is None: -# last_repayment_amount = term.total_payment -# if last_balance_amount is None: -# last_balance_amount = term.balance_loan_amount - -# loan_doc.save() - -# balance_amount = get_pending_principal_amount(loan_doc) - -# if loan_doc.repayment_method == "Repay Fixed Amount per Period": -# monthly_repayment_amount = flt( -# balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries -# ) -# else: -# repayment_period = loan_doc.repayment_periods - accrued_entries -# if not cancel and repayment_period > 0: -# monthly_repayment_amount = get_monthly_repayment_amount( -# balance_amount, loan_doc.rate_of_interest, repayment_period -# ) -# else: -# monthly_repayment_amount = last_repayment_amount -# balance_amount = last_balance_amount - -# payment_date = next_accrual_date - -# while flt(balance_amount, precision) > 0: -# interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12 * 100)) -# principal_amount = monthly_repayment_amount - interest_amount -# balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount) -# if balance_amount < 0: -# principal_amount += balance_amount -# balance_amount = 0.0 - -# total_payment = principal_amount + interest_amount -# loan_doc.append( -# "repayment_schedule", -# { -# "payment_date": payment_date, -# "principal_amount": principal_amount, -# "interest_amount": interest_amount, -# "total_payment": total_payment, -# "balance_loan_amount": balance_amount, -# }, -# ) -# next_payment_date = add_single_month(payment_date) -# payment_date = next_payment_date - -# loan_doc.save() - - def get_pending_principal_amount(loan, posting_date=None): precision = cint(frappe.db.get_default("currency_precision")) @@ -1038,95 +675,28 @@ def get_amounts(amounts, against_loan, posting_date, with_loan_details=False): precision = cint(frappe.db.get_default("currency_precision")) or 2 against_loan_doc = frappe.get_doc("Loan", against_loan) - # loan_product_detailsloan_product_details = frappe.get_doc("Loan Product", against_loan_doc.loan_product) unpaid_demands = get_unpaid_demands(against_loan_doc.name, posting_date) - - # computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan) - pending_demands = {} - - # if against_loan_doc.is_term_loan: - # pending_penalty_amount = 0 - # computed_penalty_date = None - total_pending_interest = 0 + charges = 0 penalty_amount = 0 payable_principal_amount = 0 final_due_date = "" - last_entry_due_date = "" - principal_demands = [] - interest_demands = [] - unaccrued_interest = 0 - penal_demands = [] - charges_demands = [] for demand in unpaid_demands: - # Loan repayment due date is one day after the loan interest is accrued - # no of late days are calculated based on loan repayment posting date - # and if no_of_late days are positive then penalty is levied - - # due_date_after_grace_period = add_days(entry.demand_date, loan_product_details.grace_period_in_days) - - # if computed_penalty_date and getdate(computed_penalty_date) >= getdate( - # due_date_after_grace_period - # ): - # due_date_after_grace_period = computed_penalty_date - - # no_of_late_days = date_diff(posting_date, due_date_after_grace_period) - - # if ( - # no_of_late_days > 0 - # and ( - # not (hasattr(against_loan_doc, "repay_from_salary") and against_loan_doc.repay_from_salary) - # ) - # and entry.accrual_type == "Regular" - # ): - # penalty_amount += ( - # (entry.interest_amount + entry.payable_principal_amount) - # * (loan_product_details.penalty_interest_rate / 100) - # * no_of_late_days - # ) / 365 - if demand.demand_subtype == "Interest": total_pending_interest += demand.demand_amount - principal_demands.append(demand) elif demand.demand_subtype == "Principal": payable_principal_amount += demand.demand_amount - interest_demands.append(demand) elif demand.demand_subtype == "Penalty": penalty_amount += demand.demand_amount - penal_demands.append(demand) - - # pending_demands.setdefault( - # demand.name, - # { - # "interest_amount": flt(entry.interest_amount, precision), - # "payable_principal_amount": flt(entry.payable_principal_amount, precision), - # }, - # ) - - last_entry_due_date = demand.demand_date - # if demand.demand_date and not final_due_date: - # final_due_date = add_days(demand.demand_date, loan_product_details.grace_period_in_days) + elif demand.demand_type == "Charges": + charges += demand.demand_amount pending_principal_amount = get_pending_principal_amount(against_loan_doc) unbooked_interest = 0 - pending_days = date_diff(posting_date, last_entry_due_date) - - # if pending_days > 0: - # if against_loan_doc.is_term_loan: - # principal_amount = flt(pending_principal_amount - payable_principal_amount, precision) - # else: - # principal_amount = flt(pending_principal_amount, precision) - - # per_day_interest = get_per_day_interest( - # principal_amount, - # loan_product_details.rate_of_interest, - # loan_product_details.company, - # posting_date, - # ) - # unaccrued_interest += pending_days * per_day_interest + amounts["charges"] = charges amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) amounts["interest_amount"] = flt(total_pending_interest, precision) @@ -1134,7 +704,6 @@ def get_amounts(amounts, against_loan, posting_date, with_loan_details=False): amounts["payable_amount"] = flt( payable_principal_amount + total_pending_interest + penalty_amount, precision ) - # amounts["pending_accrual_entries"] = pending_accrual_entries amounts["unbooked_interest"] = flt(unbooked_interest, precision) amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision) amounts["unpaid_demands"] = unpaid_demands @@ -1167,18 +736,6 @@ def calculate_amounts(against_loan, posting_date, payment_type="", with_loan_det else: amounts = get_amounts(amounts, against_loan, posting_date) - charges = [] - invoices = get_outstanding_invoices(against_loan, posting_date) - for d in invoices: - charges.append( - { - "sales_invoice": d.voucher_no, - "pending_charge_amount": d.outstanding_amount, - } - ) - amounts["total_charges_payable"] += d.outstanding_amount - - amounts["charges"] = charges amounts["payable_amount"] += amounts["total_charges_payable"] amounts["available_security_deposit"] = frappe.db.get_value( "Loan Security Deposit", {"loan": against_loan}, "sum(deposit_amount - allocated_amount)" @@ -1196,16 +753,3 @@ def calculate_amounts(against_loan, posting_date, payment_type="", with_loan_det return {"amounts": amounts, "loan_details": loan_details} else: return amounts - - -def get_outstanding_invoices(loan, posting_date): - return frappe.db.get_all( - "Sales Invoice", - filters={ - "loan": loan, - "outstanding_amount": ("!=", 0), - "docstatus": 1, - "due_date": ("<=", posting_date), - }, - fields=["name as voucher_no", "outstanding_amount"], - ) 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 f6da1c37..6a39033f 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 @@ -110,7 +110,7 @@ def make_repayment_schedule(self): if ( self.repayment_schedule_type - in ["Monthly as per repayment start date", "Monthly as per cycle date"] + in ["Monthly as per repayment start date", "Monthly as per cycle date", "Line of Credit"] or self.repayment_date_on == "End of the current month" ) and self.repayment_frequency == "Monthly": next_payment_date = add_single_month(payment_date)