From e8b81b0cc983fa16b539f01b6c92c36f676c49a2 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Wed, 25 Oct 2023 07:47:40 +0530 Subject: [PATCH] chore: complete remaining things --- lending/loan_management/doctype/loan/loan.py | 39 +-- .../loan_management/doctype/loan/test_loan.py | 65 ++--- .../loan_application/loan_application.js | 39 ++- .../loan_application/loan_application.py | 53 ++-- .../loan_assignment_detail.json | 53 ---- .../loan_disbursement/loan_disbursement.py | 57 ++++- .../test_loan_disbursement.py | 42 +++- .../test_loan_interest_accrual.py | 8 +- .../doctype/loan_repayment/loan_repayment.py | 14 +- .../loan_repayment_detail.json | 7 +- .../doctype/loan_security/loan_security.js | 22 +- .../doctype/loan_security/loan_security.json | 51 +++- .../doctype/loan_security/loan_security.py | 62 ++++- .../loan_security/loan_security_list.js | 11 + .../loan_security_assignment.js | 14 ++ .../loan_security_assignment.json | 41 ++-- .../loan_security_assignment.py | 232 ++++++++++++++---- .../loan_security_assignment_list.js | 1 - .../__init__.py | 0 ...ty_assignment_loan_application_detail.json | 32 +++ ...rity_assignment_loan_application_detail.py | 9 + .../__init__.py | 0 .../loan_security_assignment_loan_detail.json | 32 +++ .../loan_security_assignment_loan_detail.py} | 2 +- .../loan_security_price.py | 5 +- .../loan_security_release.py | 15 +- .../loan_security_type.json | 5 +- .../doctype/pledge/pledge.json | 9 +- .../proposed_pledge/proposed_pledge.json | 11 +- .../loan_security_status.js | 2 +- 30 files changed, 680 insertions(+), 253 deletions(-) delete mode 100644 lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.json create mode 100644 lending/loan_management/doctype/loan_security/loan_security_list.js rename lending/loan_management/doctype/{loan_assignment_detail => loan_security_assignment_loan_application_detail}/__init__.py (100%) create mode 100644 lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.json create mode 100644 lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.py create mode 100644 lending/loan_management/doctype/loan_security_assignment_loan_detail/__init__.py create mode 100644 lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.json rename lending/loan_management/doctype/{loan_assignment_detail/loan_assignment_detail.py => loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.py} (78%) diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 0608d8f2..0c8d6c28 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.query_builder import Order +from frappe.query_builder.functions import Sum from frappe.utils import ( add_days, cint, @@ -223,23 +224,31 @@ def validate_loan_amount(self): def link_loan_security_assignment(self): if self.is_secured_loan and self.loan_application: - maximum_loan_value = frappe.db.get_value( - "Loan Security Assignment", - {"loan_application": self.loan_application, "status": "Requested"}, - "sum(maximum_loan_value)", - ) - - if maximum_loan_value: - frappe.db.sql( - """ - UPDATE `tabLoan Security Assignment` - SET loan = %s, pledge_time = %s, status = 'Pledged' - WHERE status = 'Requested' and loan_application = %s - """, - (self.name, now_datetime(), self.loan_application), + lsa = frappe.qb.DocType("Loan Security Assignment") + lsalad = frappe.qb.DocType("Loan Security Assignment Loan Application Detail") + + lsa_and_maximum_loan_value = ( + frappe.qb.from_(lsa) + .inner_join(lsalad) + .on(lsalad.parent == lsa.name) + .select(lsa.name, Sum(lsa.maximum_loan_value)) + .where(lsa.status == "Requested") + .where(lsalad.loan_application == self.loan_application) + ).run() + + if lsa_and_maximum_loan_value: + lsa = frappe.get_doc("Loan Security Assignment", lsa_and_maximum_loan_value[0][0]) + lsa.append( + "allocated_loans", + { + "loan": self.name, + }, ) + lsa.pledge_time = now_datetime() + lsa.status = "Pledged" + lsa.save() - self.db_set("maximum_loan_amount", maximum_loan_value) + self.db_set("maximum_loan_amount", lsa_and_maximum_loan_value[0][1]) def accrue_loan_interest(self): from lending.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( diff --git a/lending/loan_management/doctype/loan/test_loan.py b/lending/loan_management/doctype/loan/test_loan.py index c358a697..64c7e252 100644 --- a/lending/loan_management/doctype/loan/test_loan.py +++ b/lending/loan_management/doctype/loan/test_loan.py @@ -23,7 +23,9 @@ request_loan_closure, unpledge_security, ) -from lending.loan_management.doctype.loan_application.loan_application import create_pledge +from lending.loan_management.doctype.loan_application.loan_application import ( + create_loan_security_assignment_from_loan_application, +) from lending.loan_management.doctype.loan_disbursement.loan_disbursement import ( get_disbursal_amount, ) @@ -174,7 +176,7 @@ def test_loan_with_security(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application @@ -188,7 +190,7 @@ def test_loan_disbursement(self): "_Test Company", self.applicant2, "Stock Loan", pledge, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application @@ -250,7 +252,7 @@ def test_sanctioned_amount_limit(self): loan_application = create_loan_application( "_Test Company", self.applicant3, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant3, "Demand Loan", loan_application, posting_date="2019-10-01" ) @@ -268,7 +270,7 @@ def test_regular_loan_repayment(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -328,8 +330,7 @@ def test_loan_closure(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) - + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" ) @@ -383,7 +384,7 @@ def test_loan_repayment_for_term_loan(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, @@ -428,7 +429,7 @@ def test_security_shortfall(self): "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application @@ -466,7 +467,7 @@ def test_loan_security_release(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -526,7 +527,7 @@ def test_partial_loan_security_release(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -562,7 +563,7 @@ def test_sanctioned_loan_security_release(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -591,7 +592,7 @@ def test_disbursal_check_with_shortfall(self): "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application @@ -630,7 +631,7 @@ def test_disbursal_check_without_shortfall(self): "_Test Company", self.applicant2, "Stock Loan", pledges, "Repay Over Number of Periods", 12 ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_loan_with_security( self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application @@ -648,7 +649,7 @@ def test_pending_loan_amount_after_closure_request(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -698,7 +699,7 @@ def test_partial_unaccrued_interest_payment(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -761,7 +762,7 @@ def test_loan_write_off_limit(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -813,7 +814,7 @@ def test_loan_repayment_against_partially_disbursed_loan(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -838,7 +839,7 @@ def test_loan_amount_write_off(self): loan_application = create_loan_application( "_Test Company", self.applicant2, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -950,7 +951,7 @@ def create_loan_scenario_for_penalty(doc): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] loan_application = create_loan_application("_Test Company", doc.applicant2, "Demand Loan", pledge) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( doc.applicant2, "Demand Loan", loan_application, posting_date="2019-10-01" ) @@ -1126,7 +1127,7 @@ def create_loan_security(): { "doctype": "Loan Security", "loan_security_type": "Stock", - "loan_security_code": "532779", + "loan_security_code": "Test Security 1", "loan_security_name": "Test Security 1", "unit_of_measure": "Nos", "haircut": 50.00, @@ -1138,7 +1139,7 @@ def create_loan_security(): { "doctype": "Loan Security", "loan_security_type": "Stock", - "loan_security_code": "531335", + "loan_security_code": "Test Security 2", "loan_security_name": "Test Security 2", "unit_of_measure": "Nos", "haircut": 50.00, @@ -1146,26 +1147,6 @@ def create_loan_security(): ).insert(ignore_permissions=True) -def create_loan_security_assignment(applicant, pledges, loan_application=None, loan=None): - - lsp = frappe.new_doc("Loan Security Assignment") - lsp.applicant_type = "Customer" - lsp.applicant = applicant - lsp.company = "_Test Company" - lsp.loan_application = loan_application - - if loan: - lsp.loan = loan - - for pledge in pledges: - lsp.append("securities", {"loan_security": pledge["loan_security"], "qty": pledge["qty"]}) - - lsp.save() - lsp.submit() - - return lsp - - def make_loan_disbursement_entry(loan, amount, disbursement_date=None): loan_disbursement_entry = frappe.get_doc( diff --git a/lending/loan_management/doctype/loan_application/loan_application.js b/lending/loan_management/doctype/loan_application/loan_application.js index a249e790..e65523b0 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.js +++ b/lending/loan_management/doctype/loan_application/loan_application.js @@ -4,11 +4,20 @@ lending.common.setup_filters("Loan Application"); frappe.ui.form.on('Loan Application', { + onload: function(frm) { + frm.set_query("loan_security", "proposed_pledges", function() { + return { + "filters": { + "status": "Pending Hypothecation", + } + }; + }); + }, setup: function(frm) { frm.make_methods = { 'Loan': function() { frm.trigger('create_loan') }, - 'Loan Security Assignment': function() { frm.trigger('create_loan_security_assignment') }, + 'Loan Security Assignment': function() { frm.trigger('create_loan_security_assignment_from_loan_application') }, } }, refresh: function(frm) { @@ -39,13 +48,9 @@ frappe.ui.form.on('Loan Application', { if (frm.doc.status == "Approved") { if (frm.doc.is_secured_loan) { - frappe.db.get_value("Loan Security Assignment", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { - if (Object.keys(r).length === 0) { - frm.add_custom_button(__('Loan Security Assignment'), function() { - frm.trigger('create_loan_security_assignment'); - },__('Create')) - } - }); + frm.add_custom_button(__('Loan Security Assignment'), function() { + frm.trigger('create_loan_security_assignment_from_loan_application'); + },__('Create')) } frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { @@ -69,14 +74,14 @@ frappe.ui.form.on('Loan Application', { frm: frm }); }, - create_loan_security_assignment: function(frm) { + create_loan_security_assignment_from_loan_application: function(frm) { if(!frm.doc.is_secured_loan) { frappe.throw(__("Loan Security Assignment can only be created for secured loans")); } frappe.call({ - method: "lending.loan_management.doctype.loan_application.loan_application.create_pledge", + method: "lending.loan_management.doctype.loan_application.loan_application.create_loan_security_assignment_from_loan_application", args: { loan_application: frm.doc.name }, @@ -95,14 +100,8 @@ frappe.ui.form.on('Loan Application', { calculate_amounts: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.qty) { - frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); - frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); - } else if (row.amount) { - frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price)); - frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); - frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); - } + frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); + frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100))); let maximum_amount = 0; @@ -134,11 +133,11 @@ frappe.ui.form.on("Proposed Pledge", { } }, - amount: function(frm, cdt, cdn) { + qty: function(frm, cdt, cdn) { frm.events.calculate_amounts(frm, cdt, cdn); }, - qty: function(frm, cdt, cdn) { + loan_security_price: function(frm, cdt, cdn) { frm.events.calculate_amounts(frm, cdt, cdn); }, }) diff --git a/lending/loan_management/doctype/loan_application/loan_application.py b/lending/loan_management/doctype/loan_application/loan_application.py index fc5f912a..b5c2d2d9 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.py +++ b/lending/loan_management/doctype/loan_application/loan_application.py @@ -91,13 +91,20 @@ def check_sanctioned_amount_limit(self): def set_pledge_amount(self): for proposed_pledge in self.proposed_pledges: - if not proposed_pledge.qty and not proposed_pledge.amount: - frappe.throw(_("Qty or Amount is mandatroy for loan security")) + if not proposed_pledge.qty: + frappe.throw(_("Qty is mandatory for loan security!")) - proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security) + if not proposed_pledge.loan_security_price: + loan_security_price = get_loan_security_price(proposed_pledge.loan_security) - if not proposed_pledge.qty: - proposed_pledge.qty = cint(proposed_pledge.amount / proposed_pledge.loan_security_price) + if loan_security_price: + proposed_pledge.loan_security_price = loan_security_price + else: + frappe.throw( + _("No valid Loan Security Price found for {0}").format( + frappe.bold(proposed_pledge.loan_security) + ) + ) proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price proposed_pledge.post_haircut_amount = cint( @@ -200,21 +207,31 @@ def update_accounts(source_doc, target_doc, source_parent): @frappe.whitelist() -def create_pledge(loan_application, loan=None): +def create_loan_security_assignment_from_loan_application(loan_application, loan=None): loan_application_doc = frappe.get_doc("Loan Application", loan_application) - lsp = frappe.new_doc("Loan Security Assignment") - lsp.applicant_type = loan_application_doc.applicant_type - lsp.applicant = loan_application_doc.applicant - lsp.loan_application = loan_application_doc.name - lsp.company = loan_application_doc.company + lsa = frappe.new_doc("Loan Security Assignment") + lsa.applicant_type = loan_application_doc.applicant_type + lsa.applicant = loan_application_doc.applicant + lsa.company = loan_application_doc.company + + lsa.append( + "allocated_loan_applications", + { + "loan_application": loan_application_doc.name, + }, + ) if loan: - lsp.loan = loan + lsa.append( + "allocated_loans", + { + "loan": loan, + }, + ) for pledge in loan_application_doc.proposed_pledges: - - lsp.append( + lsa.append( "securities", { "loan_security": pledge.loan_security, @@ -224,13 +241,13 @@ def create_pledge(loan_application, loan=None): }, ) - lsp.save() - lsp.submit() + lsa.save() + lsa.submit() - message = _("Loan Security Assignment Created : {0}").format(lsp.name) + message = _("Loan Security Assignment Created : {0}").format(lsa.name) frappe.msgprint(message) - return lsp.name + return lsa.name # This is a sandbox method to get the proposed pledges diff --git a/lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.json b/lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.json deleted file mode 100644 index 5f89e13d..00000000 --- a/lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-10-19 16:26:54.924575", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "loan", - "loan_application", - "loan_amount", - "pending_principal_amount" - ], - "fields": [ - { - "fieldname": "loan", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Loan", - "options": "Loan" - }, - { - "fieldname": "loan_application", - "fieldtype": "Link", - "label": "Loan Application", - "options": "Loan Application" - }, - { - "fetch_from": "loan.loan_amount", - "fieldname": "loan_amount", - "fieldtype": "Currency", - "label": "Loan Amount" - }, - { - "fieldname": "pending_principal_amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Pending Principal Amount" - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-10-19 17:24:13.961740", - "modified_by": "Administrator", - "module": "Loan Management", - "name": "Loan Assignment Detail", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index b7c4dace..6b591be3 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -4,12 +4,16 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import add_days, flt, get_datetime, 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_security_assignment.loan_security_assignment import ( + update_loan_securities_values, +) from lending.loan_management.doctype.loan_security_release.loan_security_release import ( get_pledged_security_qty, ) @@ -28,6 +32,10 @@ def on_submit(self): self.update_repayment_schedule_status() self.set_status_and_amounts() + + update_loan_securities_values(self.against_loan, self.disbursed_amount, self.doctype) + self.set_status_of_loan_securities() + self.withheld_security_deposit() self.make_gl_entries() @@ -53,6 +61,15 @@ def on_cancel(self): self.delete_security_deposit() self.set_status_and_amounts(cancel=1) + + update_loan_securities_values( + self.against_loan, + self.disbursed_amount, + self.doctype, + on_trigger_doc_cancel=1, + ) + self.set_status_of_loan_securities(cancel=1) + self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] @@ -89,10 +106,32 @@ def validate_disbursal_amount(self): if not self.disbursed_amount: frappe.throw(_("Disbursed amount cannot be zero")) - elif self.disbursed_amount > possible_disbursal_amount: frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount)) + def set_status_of_loan_securities(self, cancel=0): + if not frappe.db.get_value("Loan", self.against_loan, "is_secured_loan"): + return + + if not cancel: + new_status = "Hypothecated" + old_status = "Pending Hypothecation" + else: + new_status = "Pending Hypothecation" + old_status = "Hypothecated" + + frappe.db.sql( + """ + UPDATE `tabLoan Security` ls + JOIN `tabPledge` p ON p.loan_security=ls.name + JOIN `tabLoan Security Assignment` lsa ON lsa.name=p.parent + JOIN `tabLoan Security Assignment Loan Detail` lsald ON lsald.parent=lsa.name + SET ls.status=%s + WHERE lsald.loan=%s AND ls.status=%s + """, + (new_status, self.against_loan, old_status), + ) + def set_status_and_amounts(self, cancel=0): loan_details = frappe.get_all( "Loan", @@ -373,6 +412,16 @@ def get_disbursal_amount(loan, on_current_security_price=0): def get_maximum_amount_as_per_pledged_security(loan): - return flt( - frappe.db.get_value("Loan Security Assignment", {"loan": loan}, "sum(maximum_loan_value)") - ) + lsa = frappe.qb.DocType("Loan Security Assignment") + lsald = frappe.qb.DocType("Loan Security Assignment Loan Detail") + + maximum_loan_value = ( + frappe.qb.from_(lsa) + .inner_join(lsald) + .on(lsald.parent == lsa.name) + .select(Sum(lsa.maximum_loan_value)) + .where(lsa.status == "Pledged") + .where(lsald.loan == loan) + ).run() + + return maximum_loan_value[0][0] if maximum_loan_value else 0 diff --git a/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index ffb1e8c7..6f755eb9 100644 --- a/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -23,14 +23,15 @@ create_loan_application, create_loan_product, create_loan_security, - create_loan_security_assignment, create_loan_security_price, create_loan_security_type, create_repayment_entry, make_loan_disbursement_entry, set_loan_settings_in_company, ) -from lending.loan_management.doctype.loan_application.loan_application import create_pledge +from lending.loan_management.doctype.loan_application.loan_application import ( + create_loan_security_assignment_from_loan_application, +) from lending.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import ( days_in_year, get_interest_amount, @@ -84,7 +85,7 @@ def test_loan_topup(self): loan_application = create_loan_application( "_Test Company", self.applicant, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) @@ -133,7 +134,7 @@ def test_loan_topup_with_additional_pledge(self): loan_application = create_loan_application( "_Test Company", self.applicant, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date="2019-10-01" @@ -154,7 +155,9 @@ def test_loan_topup_with_additional_pledge(self): pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}] - create_loan_security_assignment(self.applicant, pledge1, loan=loan.name) + create_loan_security_assignment_with_applicant_and_pledge( + self.applicant, pledge1, loan=loan.name + ) # Topup 500000 make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1)) @@ -164,3 +167,32 @@ def test_loan_topup_with_additional_pledge(self): interest = get_interest_amount(15, 1500000, 13.5, "_Test Company", "2019-10-30") self.assertEqual(amounts["pending_principal_amount"], 1500000) + + +def create_loan_security_assignment_with_applicant_and_pledge( + applicant, pledges, loan_application=None, loan=None +): + lsa = frappe.new_doc("Loan Security Assignment") + lsa.applicant_type = "Customer" + lsa.applicant = applicant + lsa.company = "_Test Company" + + if loan_application: + lsa.append( + "allocated_loan_applications", + {"loan_application": loan_application}, + ) + + if loan: + lsa.append( + "allocated_loans", + {"loan": loan}, + ) + + for pledge in pledges: + lsa.append("securities", {"loan_security": pledge["loan_security"], "qty": pledge["qty"]}) + + lsa.save() + lsa.submit() + + return lsa diff --git a/lending/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/lending/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 6701e3a8..9a8e92cc 100644 --- a/lending/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/lending/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -17,7 +17,9 @@ make_loan_disbursement_entry, set_loan_settings_in_company, ) -from lending.loan_management.doctype.loan_application.loan_application import create_pledge +from lending.loan_management.doctype.loan_application.loan_application import ( + create_loan_security_assignment_from_loan_application, +) from lending.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import ( days_in_year, ) @@ -92,7 +94,7 @@ def test_loan_interest_accural(self): loan_application = create_loan_application( "_Test Company", self.applicant, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) ) @@ -187,7 +189,7 @@ def test_accumulated_amounts(self): loan_application = create_loan_application( "_Test Company", self.applicant, "Demand Loan", pledge ) - create_pledge(loan_application) + create_loan_security_assignment_from_loan_application(loan_application) loan = create_demand_loan( self.applicant, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()) ) diff --git a/lending/loan_management/doctype/loan_repayment/loan_repayment.py b/lending/loan_management/doctype/loan_repayment/loan_repayment.py index 823bdc27..3e7151e3 100644 --- a/lending/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/lending/loan_management/doctype/loan_repayment/loan_repayment.py @@ -15,6 +15,9 @@ get_last_accrual_date, get_per_day_interest, ) +from lending.loan_management.doctype.loan_security_assignment.loan_security_assignment import ( + update_loan_securities_values, +) from lending.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import ( update_shortfall_status, ) @@ -53,6 +56,8 @@ def on_submit(self): if self.repayment_type == "Charges Waiver": self.make_credit_note() + update_loan_securities_values(self.against_loan, self.principal_amount_paid, self.doctype) + self.make_gl_entries() def on_cancel(self): @@ -68,6 +73,13 @@ def on_cancel(self): frappe.db.set_value("Loan", self.against_loan, "days_past_due", self.days_past_due) + update_loan_securities_values( + self.against_loan, + self.principal_amount_paid, + self.doctype, + on_trigger_doc_cancel=1, + ) + self.ignore_linked_doctypes = [ "GL Entry", "Payment Ledger Entry", @@ -1073,7 +1085,7 @@ def get_pending_principal_amount(loan): ) else: pending_principal_amount = ( - flt(loan.disbursed_amount or loan.loan_amount) + flt(loan.disbursed_amount) + flt(loan.debit_adjustment_amount) - flt(loan.credit_adjustment_amount) - flt(loan.total_principal_paid) diff --git a/lending/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json b/lending/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json index 4b9b191e..20661022 100644 --- a/lending/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json +++ b/lending/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json @@ -14,18 +14,21 @@ { "fieldname": "loan_interest_accrual", "fieldtype": "Link", + "in_list_view": 1, "label": "Loan Interest Accrual", "options": "Loan Interest Accrual" }, { "fieldname": "paid_principal_amount", "fieldtype": "Currency", + "in_list_view": 1, "label": "Paid Principal Amount", "options": "Company:company:default_currency" }, { "fieldname": "paid_interest_amount", "fieldtype": "Currency", + "in_list_view": 1, "label": "Paid Interest Amount", "options": "Company:company:default_currency" }, @@ -34,6 +37,7 @@ "fetch_if_empty": 1, "fieldname": "accrual_type", "fieldtype": "Select", + "in_list_view": 1, "label": "Accrual Type", "options": "Regular\nRepayment\nDisbursement" } @@ -41,7 +45,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-23 08:09:18.267030", + "modified": "2023-10-25 00:04:58.101292", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Detail", @@ -49,5 +53,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_security/loan_security.js b/lending/loan_management/doctype/loan_security/loan_security.js index 0e815af7..0cb1b61b 100644 --- a/lending/loan_management/doctype/loan_security/loan_security.js +++ b/lending/loan_management/doctype/loan_security/loan_security.js @@ -2,7 +2,25 @@ // For license information, please see license.txt frappe.ui.form.on('Loan Security', { - // refresh: function(frm) { + refresh: function(frm) { + if (frm.doc.status === "Hypothecated") { + frm.add_custom_button(__("Release"), function() { + frm.trigger("release_loan_security"); + }) + } + }, - // } + release_loan_security: function(frm) { + frappe.confirm(__("Do you really want to release this loan security?"), function () { + frappe.call({ + args: { + "loan_security": frm.doc.name, + }, + method: "lending.loan_management.doctype.loan_security.loan_security.release_loan_security", + callback: function(r) { + cur_frm.reload_doc(); + } + }) + }) + }, }); diff --git a/lending/loan_management/doctype/loan_security/loan_security.json b/lending/loan_management/doctype/loan_security/loan_security.json index c698601e..a1cd7032 100644 --- a/lending/loan_management/doctype/loan_security/loan_security.json +++ b/lending/loan_management/doctype/loan_security/loan_security.json @@ -1,18 +1,25 @@ { "actions": [], "allow_rename": 1, + "autoname": "field:loan_security_code", "creation": "2019-09-02 15:07:08.885593", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "loan_security_code", "loan_security_name", "haircut", - "loan_security_code", + "status", "column_break_3", "loan_security_type", "unit_of_measure", - "disabled" + "released_date", + "disabled", + "section_break_iwsf", + "security_owner_type", + "column_break_uigr", + "security_owner" ], "fields": [ { @@ -25,7 +32,6 @@ }, { "fetch_from": "loan_security_type.haircut", - "fetch_if_empty": 1, "fieldname": "haircut", "fieldtype": "Percent", "label": "Haircut %" @@ -46,6 +52,7 @@ "fieldname": "loan_security_code", "fieldtype": "Data", "label": "Loan Security Code", + "reqd": 1, "unique": 1 }, { @@ -63,14 +70,49 @@ "options": "UOM", "read_only": 1, "reqd": 1 + }, + { + "default": "Pending Hypothecation", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Pending Hypothecation\nHypothecated\nReleased\nRepossessed", + "read_only": 1 + }, + { + "fieldname": "section_break_iwsf", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_uigr", + "fieldtype": "Column Break" + }, + { + "fieldname": "security_owner_type", + "fieldtype": "Select", + "label": "Security Owner Type", + "options": "Employee\nMember\nCustomer\nCompany" + }, + { + "fieldname": "security_owner", + "fieldtype": "Dynamic Link", + "label": "Security Owner", + "options": "security_owner_type" + }, + { + "fieldname": "released_date", + "fieldtype": "Date", + "label": "Released Date", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-26 07:34:48.601766", + "modified": "2023-10-25 06:15:57.151737", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -101,5 +143,6 @@ "search_fields": "loan_security_code", "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_security/loan_security.py b/lending/loan_management/doctype/loan_security/loan_security.py index 8087fc50..7881a5d6 100644 --- a/lending/loan_management/doctype/loan_security/loan_security.py +++ b/lending/loan_management/doctype/loan_security/loan_security.py @@ -1,11 +1,67 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt +import itertools -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document +from frappe.utils import nowdate class LoanSecurity(Document): - def autoname(self): - self.name = self.loan_security_name + pass + + +@frappe.whitelist() +def release_loan_security(loan_security): + active_loans_and_lsa = get_active_loan_securities(loan_security) + + if active_loans_and_lsa: + msg = _("Loan Security {0} is linked with active loans:").format(frappe.bold(loan_security)) + for loan_and_lsa in active_loans_and_lsa: + msg += "

" + msg += _("Loan {0} through Loan Security Assignment {1}").format( + frappe.bold(loan_and_lsa.loan), frappe.bold(loan_and_lsa.lsa) + ) + frappe.throw(msg, title=_("Loan Security cannot be released")) + else: + frappe.db.set_value( + "Loan Security", loan_security, {"status": "Released", "released_date": nowdate()} + ) + + +def get_active_loan_securities(loan_security): + active_loans_and_lsa = [] + + all_loans_and_lsa = frappe.db.sql( + """ + SELECT lsald.loan, lsa.name as lsa + FROM `tabLoan Security Assignment` lsa, `tabPledge` p, `tabLoan Security Assignment Loan Detail` lsald + WHERE p.loan_security = %s + AND p.parent = lsa.name + AND lsald.parent = lsa.name + AND lsa.status = 'Pledged' + """, + (loan_security), + as_dict=True, + ) + + loans_with_security_unpledged = frappe.db.sql( + """ + SELECT lsr.loan + FROM `tabLoan Security Release` lsr, `tabUnpledge` u + WHERE u.loan_security = %s + AND u.parent = lsr.name + AND lsr.status = 'Approved' + """, + (loan_security), + as_list=True, + ) + loans_with_security_unpledged = list(itertools.chain(*loans_with_security_unpledged)) + + for loan_and_lsa in all_loans_and_lsa: + if loan_and_lsa.loan not in loans_with_security_unpledged: + active_loans_and_lsa.append(loan_and_lsa) + + return active_loans_and_lsa diff --git a/lending/loan_management/doctype/loan_security/loan_security_list.js b/lending/loan_management/doctype/loan_security/loan_security_list.js new file mode 100644 index 00000000..63bfeaab --- /dev/null +++ b/lending/loan_management/doctype/loan_security/loan_security_list.js @@ -0,0 +1,11 @@ +frappe.listview_settings['Loan Security'] = { + get_indicator: function(doc) { + var status_color = { + "Pending Hypothecation": "grey", + "Hypothecated": "blue", + "Released": "green", + "Repossessed": "red" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + }, +}; \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.js b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.js index 49732615..49ceefc7 100644 --- a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.js +++ b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.js @@ -2,6 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on('Loan Security Assignment', { + onload: function(frm) { + frm.set_query("loan_security", "securities", function() { + return { + "filters": { + "status": "Pending Hypothecation", + } + }; + }); + }, + calculate_amounts: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price); @@ -40,4 +50,8 @@ frappe.ui.form.on("Pledge", { qty: function(frm, cdt, cdn) { frm.events.calculate_amounts(frm, cdt, cdn); }, + + loan_security_price: function(frm, cdt, cdn) { + frm.events.calculate_amounts(frm, cdt, cdn); + }, }); diff --git a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.json b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.json index b81961b5..0f735e6e 100644 --- a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.json +++ b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.json @@ -17,14 +17,14 @@ "securities", "loan_details_section", "allocated_loans", - "total_loan_amount", - "total_pending_principal", + "loan_applications_section", + "allocated_loan_applications", "section_break_10", "total_security_value", "utilized_security_value", "column_break_11", "maximum_loan_value", - "pending_security_value", + "available_security_value", "more_information_section", "reference_no", "column_break_18", @@ -42,7 +42,7 @@ "read_only": 1 }, { - "fetch_from": "loan_application.applicant", + "fetch_from": ".applicant", "fieldname": "applicant", "fieldtype": "Dynamic Link", "in_list_view": 1, @@ -80,6 +80,7 @@ "label": "Loan Details" }, { + "allow_on_submit": 1, "default": "Requested", "fieldname": "status", "fieldtype": "Select", @@ -90,6 +91,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "pledge_time", "fieldtype": "Datetime", "label": "Pledge Time", @@ -119,7 +121,7 @@ "reqd": 1 }, { - "fetch_from": "loan.applicant_type", + "fetch_from": ".", "fieldname": "applicant_type", "fieldtype": "Select", "label": "Applicant Type", @@ -153,7 +155,7 @@ "fieldname": "allocated_loans", "fieldtype": "Table", "label": "Allocated Loans", - "options": "Loan Assignment Detail" + "options": "Loan Security Assignment Loan Detail" }, { "fieldname": "applicant_details", @@ -164,29 +166,33 @@ "fieldname": "utilized_security_value", "fieldtype": "Currency", "label": "Utilized Security Value", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 }, { - "fieldname": "pending_security_value", + "fieldname": "available_security_value", "fieldtype": "Currency", - "label": "Pending Security Value", - "options": "Company:company:default_currency" + "label": "Available Security Value", + "options": "Company:company:default_currency", + "read_only": 1 }, { - "fieldname": "total_loan_amount", - "fieldtype": "Currency", - "label": "Total Loan Amount" + "fieldname": "loan_applications_section", + "fieldtype": "Section Break", + "label": "Loan Applications" }, { - "fieldname": "total_pending_principal", - "fieldtype": "Currency", - "label": "Total Pending Principal" + "allow_on_submit": 1, + "fieldname": "allocated_loan_applications", + "fieldtype": "Table", + "label": "Allocated Loan Applications", + "options": "Loan Security Assignment Loan Application Detail" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-19 16:50:45.847513", + "modified": "2023-10-25 04:35:24.455542", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Assignment", @@ -224,7 +230,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "applicant", "sort_field": "modified", "sort_order": "DESC", diff --git a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.py b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.py index a4c8aa0b..42b47a70 100644 --- a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.py +++ b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment.py @@ -7,13 +7,9 @@ from frappe.model.document import Document from frappe.utils import cint, flt, now_datetime -from lending.loan_management.doctype.loan_repayment.loan_repayment import ( - get_pending_principal_amount, +from lending.loan_management.doctype.loan_security_price.loan_security_price import ( + get_loan_security_price, ) - -# from lending.loan_management.doctype.loan_security_price.loan_security_price import ( -# get_loan_security_price, -# ) from lending.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import ( update_shortfall_status, ) @@ -21,25 +17,29 @@ class LoanSecurityAssignment(Document): def validate(self): - self.set_pledge_amount() - self.set_loan_and_pending_amounts() - self.validate_duplicate_securities() + self.validate_securities() self.validate_loan_security_type() + self.set_loan_and_security_values() def on_submit(self): - if self.loan: + if self.get("allocated_loans"): self.db_set("status", "Pledged") self.db_set("pledge_time", now_datetime()) - update_shortfall_status(self.loan, self.total_security_value) - update_loan(self.loan, self.maximum_loan_value) + for d in self.get("allocated_loans"): + update_shortfall_status(d.loan, self.total_security_value) + update_loan(d.loan, self.maximum_loan_value) + + def on_update_after_submit(self): + self.check_loan_securities_capability_to_book_additional_loans() def on_cancel(self): - if self.loan: + if self.get("allocated_loans"): self.db_set("status", "Cancelled") self.db_set("pledge_time", None) - update_loan(self.loan, self.maximum_loan_value, cancel=1) + for d in self.get("allocated_loans"): + update_loan(d.loan, self.maximum_loan_value, cancel=1) - def validate_duplicate_securities(self): + def validate_securities(self): security_list = [] for security in self.securities: if security.loan_security not in security_list: @@ -50,17 +50,27 @@ def validate_duplicate_securities(self): ) def validate_loan_security_type(self): - existing_pledge = "" + existing_lsa = "" for d in self.get("allocated_loans"): if d.loan: - existing_pledge = frappe.db.get_value( - "Loan Security Assignment", {"loan": d.loan, "docstatus": 1}, ["name"] - ) + lsa = frappe.qb.DocType("Loan Security Assignment") + lsald = frappe.qb.DocType("Loan Security Assignment Loan Detail") - if existing_pledge: + existing_lsa = ( + frappe.qb.from_(lsa) + .inner_join(lsald) + .on(lsald.parent == lsa.name) + .select(lsa.name) + .where(lsa.docstatus == 1) + .where(lsald.loan == d.loan) + ).run() + + break + + if existing_lsa: loan_security_type = frappe.db.get_value( - "Pledge", {"parent": existing_pledge}, ["loan_security_type"] + "Pledge", {"parent": existing_lsa[0][0]}, ["loan_security_type"] ) else: loan_security_type = self.securities[0].loan_security_type @@ -75,24 +85,25 @@ def validate_loan_security_type(self): if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio: frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan")) - def set_pledge_amount(self): + def set_loan_and_security_values(self): total_security_value = 0 maximum_loan_value = 0 for pledge in self.securities: - if not pledge.qty and not pledge.amount: - frappe.throw(_("Qty or Amount is mandatory for loan security!")) + if not pledge.qty: + frappe.throw(_("Qty is mandatory for loan security!")) - if not pledge.qty and pledge.loan_security_price: - pledge.qty = cint(pledge.amount / pledge.loan_security_price) - elif not pledge.loan_security_price: - pledge.qty = 1 + if not pledge.loan_security_price: + loan_security_price = get_loan_security_price(pledge.loan_security) - if pledge.loan_security_price: - pledge.amount = pledge.qty * pledge.loan_security_price - else: - pledge.loan_security_price = flt(pledge.amount / pledge.qty) + if loan_security_price: + pledge.loan_security_price = loan_security_price + else: + frappe.throw( + _("No valid Loan Security Price found for {0}").format(frappe.bold(pledge.loan_security)) + ) + pledge.amount = pledge.qty * pledge.loan_security_price pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut / 100)) total_security_value += pledge.amount @@ -101,18 +112,28 @@ def set_pledge_amount(self): self.total_security_value = total_security_value self.maximum_loan_value = maximum_loan_value - def set_loan_and_pending_amounts(self): - total_pending_principal = 0 - total_loan_amount = 0 - for loan in self.get("allocated_loans"): - loan.pending_principal_amount = get_pending_principal_amount(frappe.get_doc("Loan", loan.loan)) - total_pending_principal += loan.pending_principal_amount - total_loan_amount += loan.loan_amount + self.available_security_value = self.total_security_value - self.total_loan_amount = total_loan_amount - self.total_pending_principal = total_pending_principal - self.utilized_security_value = min(self.total_pending_principal, self.total_security_value) - self.pending_security_value = self.total_security_value - self.utilized_security_value + def check_loan_securities_capability_to_book_additional_loans(self): + total_security_value_needed = 0 + + for d in self.get("allocated_loans"): + loan_amount, status = frappe.db.get_value("Loan", d.loan, ["loan_amount", "status"]) + + if status != "Sanctioned": + continue + + total_security_value_needed += loan_amount + + if total_security_value_needed > self.available_security_value: + frappe.throw( + _("Loan Securities worth {0} needed more to book the loan").format( + frappe.bold(flt(total_security_value_needed - self.available_security_value, 2)), + ) + ) + + for d in self.get("allocated_loans"): + update_loan(d.loan, self.available_security_value) def update_loan(loan, maximum_value_against_pledge, cancel=0): @@ -130,3 +151,128 @@ def update_loan(loan, maximum_value_against_pledge, cancel=0): WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan), ) + + +def update_loan_securities_values( + loan, + amount, + trigger_doctype, + on_trigger_doc_cancel=0, +): + if not frappe.db.get_value("Loan", loan, "is_secured_loan"): + return + + utilized_value_increased = ( + True + if (trigger_doctype == "Loan Disbursement" and not on_trigger_doc_cancel) + or (trigger_doctype == "Loan Repayment" and on_trigger_doc_cancel) + else False + ) + + sorted_loan_security_assignments = _get_sorted_loan_security_assignments( + loan, utilized_value_increased + ) + + _update_loan_securities_values( + sorted_loan_security_assignments, + amount, + utilized_value_increased, + ) + + +def _get_sorted_loan_security_assignments(loan, utilized_value_increased): + loan_security_assignments_w_ratio = [] + + lsa = frappe.qb.DocType("Loan Security Assignment") + lsald = frappe.qb.DocType("Loan Security Assignment Loan Detail") + + loan_security_assignments = ( + frappe.qb.from_(lsa) + .inner_join(lsald) + .on(lsald.parent == lsa.name) + .select( + lsa.name, + lsa.total_security_value, + lsa.utilized_security_value, + lsa.available_security_value, + ) + .where(lsa.status == "Pledged") + .where(lsald.loan == loan) + ).run(as_dict=True) + + for loan_security_assignment in loan_security_assignments: + utilized_to_original_value_ratio = flt( + loan_security_assignment.utilized_security_value / loan_security_assignment.total_security_value + ) + + loan_security_assignments_w_ratio.append( + frappe._dict( + { + "name": loan_security_assignment.name, + "total_security_value": loan_security_assignment.total_security_value, + "utilized_security_value": loan_security_assignment.utilized_security_value, + "available_security_value": loan_security_assignment.available_security_value, + "ratio": utilized_to_original_value_ratio, + } + ) + ) + + sorted_loan_security_assignments = sorted( + loan_security_assignments_w_ratio, + key=lambda k: k["ratio"], + reverse=utilized_value_increased, + ) + + return sorted_loan_security_assignments + + +def _update_loan_securities_values( + sorted_loan_security_assignments, + amount, + utilized_value_increased, +): + for loan_security_assignment in sorted_loan_security_assignments: + if amount <= 0: + break + + if utilized_value_increased: + if ( + loan_security_assignment.utilized_security_value + amount + > loan_security_assignment.total_security_value + ): + new_utilized_security_value = loan_security_assignment.total_security_value + new_available_security_value = 0 + amount = ( + amount + + loan_security_assignment.utilized_security_value + - loan_security_assignment.total_security_value + ) + else: + new_utilized_security_value = loan_security_assignment.utilized_security_value + amount + new_available_security_value = loan_security_assignment.available_security_value - amount + amount = 0 + else: + if ( + loan_security_assignment.available_security_value + amount + > loan_security_assignment.total_security_value + ): + new_available_security_value = loan_security_assignment.total_security_value + new_utilized_security_value = 0 + amount = ( + amount + + loan_security_assignment.available_security_value + - loan_security_assignment.total_security_value + ) + else: + new_utilized_security_value = loan_security_assignment.utilized_security_value - amount + new_available_security_value = loan_security_assignment.available_security_value + amount + amount = 0 + + frappe.db.set_value( + "Loan Security Assignment", + loan_security_assignment.name, + { + "utilized_security_value": new_utilized_security_value, + "available_security_value": new_available_security_value, + }, + ) diff --git a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment_list.js b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment_list.js index 3744a399..7b533ca1 100644 --- a/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment_list.js +++ b/lending/loan_management/doctype/loan_security_assignment/loan_security_assignment_list.js @@ -8,7 +8,6 @@ frappe.listview_settings['Loan Security Assignment'] = { var status_color = { "Unpledged": "orange", "Pledged": "green", - "Partially Pledged": "green" }; return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; } diff --git a/lending/loan_management/doctype/loan_assignment_detail/__init__.py b/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/__init__.py similarity index 100% rename from lending/loan_management/doctype/loan_assignment_detail/__init__.py rename to lending/loan_management/doctype/loan_security_assignment_loan_application_detail/__init__.py diff --git a/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.json b/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.json new file mode 100644 index 00000000..58b904dd --- /dev/null +++ b/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-10-24 18:54:22.875354", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_application" + ], + "fields": [ + { + "fieldname": "loan_application", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan Application", + "options": "Loan Application" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-10-24 18:55:43.914701", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Assignment Loan Application Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.py b/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.py new file mode 100644 index 00000000..655abe8b --- /dev/null +++ b/lending/loan_management/doctype/loan_security_assignment_loan_application_detail/loan_security_assignment_loan_application_detail.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class LoanSecurityAssignmentLoanApplicationDetail(Document): + pass diff --git a/lending/loan_management/doctype/loan_security_assignment_loan_detail/__init__.py b/lending/loan_management/doctype/loan_security_assignment_loan_detail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.json b/lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.json new file mode 100644 index 00000000..663eef81 --- /dev/null +++ b/lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-10-19 16:26:54.924575", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan", + "options": "Loan" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-10-25 01:05:56.049682", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Assignment Loan Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.py b/lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.py similarity index 78% rename from lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.py rename to lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.py index 11683090..2d90d96d 100644 --- a/lending/loan_management/doctype/loan_assignment_detail/loan_assignment_detail.py +++ b/lending/loan_management/doctype/loan_security_assignment_loan_detail/loan_security_assignment_loan_detail.py @@ -5,5 +5,5 @@ from frappe.model.document import Document -class LoanAssignmentDetail(Document): +class LoanSecurityAssignmentLoanDetail(Document): pass diff --git a/lending/loan_management/doctype/loan_security_price/loan_security_price.py b/lending/loan_management/doctype/loan_security_price/loan_security_price.py index 45c4459a..12bc5e1d 100644 --- a/lending/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/lending/loan_management/doctype/loan_security_price/loan_security_price.py @@ -49,7 +49,4 @@ def get_loan_security_price(loan_security, valid_time=None): "loan_security_price", ) - if not loan_security_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) - else: - return loan_security_price + return loan_security_price diff --git a/lending/loan_management/doctype/loan_security_release/loan_security_release.py b/lending/loan_management/doctype/loan_security_release/loan_security_release.py index e742483d..0346c2c4 100644 --- a/lending/loan_management/doctype/loan_security_release/loan_security_release.py +++ b/lending/loan_management/doctype/loan_security_release/loan_security_release.py @@ -99,6 +99,12 @@ def validate_unpledge_qty(self): qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0) current_price = loan_security_price_map.get(security) + + if not current_price: + current_price = frappe.db.get_value( + "Pledge", {"loan_security": security}, "loan_security_price" + ) + security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: @@ -162,10 +168,11 @@ def get_pledged_security_qty(loan): frappe.db.sql( """ SELECT p.loan_security, sum(p.qty) as qty - FROM `tabLoan Security Assignment` lp, `tabPledge`p - WHERE lp.loan = %s - AND p.parent = lp.name - AND lp.status = 'Pledged' + FROM `tabLoan Security Assignment` lsa, `tabPledge` p, `tabLoan Security Assignment Loan Detail` lsald + WHERE lsald.loan = %s + AND p.parent = lsa.name + AND lsald.parent = lsa.name + AND lsa.status = 'Pledged' GROUP BY p.loan_security """, (loan), diff --git a/lending/loan_management/doctype/loan_security_type/loan_security_type.json b/lending/loan_management/doctype/loan_security_type/loan_security_type.json index 871e8256..0086fd5a 100644 --- a/lending/loan_management/doctype/loan_security_type/loan_security_type.json +++ b/lending/loan_management/doctype/loan_security_type/loan_security_type.json @@ -29,6 +29,7 @@ "unique": 1 }, { + "default": "0", "description": "Haircut percentage is the percentage difference between market value of the Loan Security and the value ascribed to that Loan Security when used as collateral for that loan.", "fieldname": "haircut", "fieldtype": "Percent", @@ -54,10 +55,11 @@ } ], "links": [], - "modified": "2020-05-16 09:38:45.988080", + "modified": "2023-10-25 05:42:28.605465", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -88,5 +90,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/loan_management/doctype/pledge/pledge.json b/lending/loan_management/doctype/pledge/pledge.json index c23479c8..7ba7c4d4 100644 --- a/lending/loan_management/doctype/pledge/pledge.json +++ b/lending/loan_management/doctype/pledge/pledge.json @@ -59,8 +59,7 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Loan Security Price", - "options": "Company:company:default_currency", - "read_only": 1 + "options": "Company:company:default_currency" }, { "fetch_from": "loan_security.haircut", @@ -74,7 +73,8 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 }, { "fieldname": "column_break_5", @@ -97,7 +97,7 @@ ], "istable": 1, "links": [], - "modified": "2021-01-17 07:41:12.452514", + "modified": "2023-10-24 17:56:31.698524", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", @@ -106,5 +106,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json b/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json index 18e0ab58..29f522fe 100644 --- a/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -19,15 +19,15 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Loan Security Price", - "options": "Company:company:default_currency", - "read_only": 1 + "options": "Company:company:default_currency" }, { "fieldname": "amount", "fieldtype": "Currency", "in_list_view": 1, "label": "Amount", - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 }, { "fetch_from": "loan_security.haircut", @@ -37,7 +37,7 @@ "read_only": 1 }, { - "fetch_from": "loan_security_assignment.qty", + "fetch_from": "loan_security_pledge.qty", "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, @@ -69,7 +69,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-17 07:29:01.671722", + "modified": "2023-10-25 03:53:41.439726", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", @@ -78,5 +78,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/loan_management/report/loan_security_status/loan_security_status.js b/lending/loan_management/report/loan_security_status/loan_security_status.js index 6e6191c7..a64f39bd 100644 --- a/lending/loan_management/report/loan_security_status/loan_security_status.js +++ b/lending/loan_management/report/loan_security_status/loan_security_status.js @@ -40,7 +40,7 @@ frappe.query_reports["Loan Security Status"] = { "fieldname":"pledge_status", "label": __("Pledge Status"), "fieldtype": "Select", - "options": ["", "Requested", "Pledged", "Partially Pledged", "Unpledged"], + "options": ["", "Requested", "Pledged", "Unpledged"], }, ] };