diff --git a/lending/loan_management/doctype/loan/loan.js b/lending/loan_management/doctype/loan/loan.js index f9f72b5e..4bbb02bb 100644 --- a/lending/loan_management/doctype/loan/loan.js +++ b/lending/loan_management/doctype/loan/loan.js @@ -97,7 +97,7 @@ frappe.ui.form.on('Loan', { },__('Create')); } - if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) { + if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && frm.doc.loan_security_preference === "Unsecured") { frm.add_custom_button(__('Close Loan'), function() { frm.trigger("close_unsecured_term_loan"); },__('Status')); @@ -245,13 +245,13 @@ frappe.ui.form.on('Loan', { if (!r.exc && r.message) { let loan_fields = ["loan_product", "loan_amount", "repayment_method", - "monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"] + "monthly_repayment_amount", "repayment_periods", "rate_of_interest", "loan_security_preference"] loan_fields.forEach(field => { frm.set_value(field, r.message[field]); }); - if (frm.doc.is_secured_loan) { + if (frm.doc.loan_security_preference !== "Unsecured") { $.each(r.message.proposed_pledges, function(i, d) { let row = frm.add_child("securities"); row.loan_security = d.loan_security; diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index c1600f3c..15bcb366 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -22,7 +22,7 @@ "repayment_schedule_type", "loan_amount", "rate_of_interest", - "is_secured_loan", + "loan_security_preference", "disbursement_date", "closure_date", "disbursed_amount", @@ -288,12 +288,6 @@ "print_hide": 1, "read_only": 1 }, - { - "default": "0", - "fieldname": "is_secured_loan", - "fieldtype": "Check", - "label": "Is Secured Loan" - }, { "default": "0", "fetch_from": "loan_product.is_term_loan", @@ -328,7 +322,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.is_secured_loan", + "depends_on": "eval:doc.loan_security_preference != \"Unsecured\"", "fieldname": "maximum_loan_amount", "fieldtype": "Currency", "label": "Maximum Loan Amount", @@ -474,12 +468,19 @@ "fieldname": "loan_classification_details_section", "fieldtype": "Section Break", "label": "Loan Classification Details" + }, + { + "default": "Unsecured", + "fieldname": "loan_security_preference", + "fieldtype": "Select", + "label": "Loan Security Preference", + "options": "Unsecured\nSemi-secured\nSecured" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-11 13:26:31.406754", + "modified": "2023-10-18 05:45:54.077529", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 2411c43c..90acc6fc 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -222,7 +222,7 @@ def validate_loan_amount(self): frappe.throw(_("Loan amount is mandatory")) def link_loan_security_pledge(self): - if self.is_secured_loan and self.loan_application: + if self.loan_security_preference != "Unsecured" and self.loan_application: maximum_loan_value = frappe.db.get_value( "Loan Security Pledge", {"loan_application": self.loan_application, "status": "Requested"}, @@ -370,13 +370,13 @@ def get_loan_application(loan_application): @frappe.whitelist() def close_unsecured_term_loan(loan): loan_details = frappe.db.get_value( - "Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1 + "Loan", {"name": loan}, ["status", "is_term_loan", "loan_security_preference"], as_dict=1 ) if ( loan_details.status == "Loan Closure Requested" and loan_details.is_term_loan - and not loan_details.is_secured_loan + and loan_details.loan_security_preference == "Unsecured" ): frappe.db.set_value("Loan", loan, "status", "Closed") else: diff --git a/lending/loan_management/doctype/loan/test_loan.py b/lending/loan_management/doctype/loan/test_loan.py index ed1c2de9..0511fc28 100644 --- a/lending/loan_management/doctype/loan/test_loan.py +++ b/lending/loan_management/doctype/loan/test_loan.py @@ -109,26 +109,26 @@ def setUp(self): "Penalty Income Account - _TC", ) - create_loan_security_type() - create_loan_security() - - create_loan_security_price( - "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) - ) - create_loan_security_price( - "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) - ) - - self.applicant1 = make_employee("robert_loan@loan.com") if not frappe.db.exists("Customer", "_Test Loan Customer"): frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) if not frappe.db.exists("Customer", "_Test Loan Customer 1"): frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True) + self.applicant1 = make_employee("robert_loan@loan.com") self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name") + create_loan_security_type() + create_loan_security(self.applicant2) + + create_loan_security_price( + "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + create_loan_security_price( + "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) + ) + def test_loan(self): loan = create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) @@ -1114,11 +1114,12 @@ def create_loan_security_type(): "unit_of_measure": "Nos", "haircut": 50.00, "loan_to_value_ratio": 50, + "quantifiable": 1, } ).insert(ignore_permissions=True) -def create_loan_security(): +def create_loan_security(applicant): if not frappe.db.exists("Loan Security", "Test Security 1"): frappe.get_doc( { @@ -1128,6 +1129,10 @@ def create_loan_security(): "loan_security_name": "Test Security 1", "unit_of_measure": "Nos", "haircut": 50.00, + "loan_security_owner_type": "Customer", + "loan_security_owner": applicant, + "quantity": 5, + "original_security_price": 100, } ).insert(ignore_permissions=True) @@ -1140,6 +1145,10 @@ def create_loan_security(): "loan_security_name": "Test Security 2", "unit_of_measure": "Nos", "haircut": 50.00, + "loan_security_owner_type": "Customer", + "loan_security_owner": applicant, + "quantity": 5, + "original_security_price": 100, } ).insert(ignore_permissions=True) @@ -1236,7 +1245,7 @@ def create_loan_application( loan_application.applicant = applicant loan_application.loan_product = loan_product loan_application.posting_date = posting_date or nowdate() - loan_application.is_secured_loan = 1 + loan_application.loan_security_preference = "Secured" if repayment_method: loan_application.repayment_method = repayment_method @@ -1306,7 +1315,7 @@ def create_loan_with_security( "applicant": applicant, "loan_product": loan_product, "is_term_loan": 1, - "is_secured_loan": 1, + "loan_security_preference": "Secured", "repayment_method": repayment_method, "repayment_periods": repayment_periods, "repayment_start_date": repayment_start_date or nowdate(), @@ -1335,7 +1344,7 @@ def create_demand_loan(applicant, loan_product, loan_application, posting_date=N "applicant": applicant, "loan_product": loan_product, "is_term_loan": 0, - "is_secured_loan": 1, + "loan_security_preference": "Secured", "mode_of_payment": frappe.db.get_value("Mode of Payment", {"type": "Cash"}, "name"), "payment_account": "Payment Account - _TC", "loan_account": "Loan Account - _TC", diff --git a/lending/loan_management/doctype/loan_application/loan_application.js b/lending/loan_management/doctype/loan_application/loan_application.js index 96136cee..46c38f1e 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.js +++ b/lending/loan_management/doctype/loan_application/loan_application.js @@ -38,7 +38,7 @@ frappe.ui.form.on('Loan Application', { add_toolbar_buttons: function(frm) { if (frm.doc.status == "Approved") { - if (frm.doc.is_secured_loan) { + if (frm.doc.loan_security_preference !== "Unsecured") { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { if (Object.keys(r).length === 0) { frm.add_custom_button(__('Loan Security Pledge'), function() { @@ -71,7 +71,7 @@ frappe.ui.form.on('Loan Application', { }, create_loan_security_pledge: function(frm) { - if(!frm.doc.is_secured_loan) { + if(frm.doc.loan_security_preference === "Unsecured") { frappe.throw(__("Loan Security Pledge can only be created for secured loans")); } @@ -89,8 +89,12 @@ frappe.ui.form.on('Loan Application', { frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan); frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan); }, - is_secured_loan: function(frm) { - frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan); + loan_security_preference: function(frm) { + if (frm.doc.loan_security_preference === "Unsecured") { + frm.set_df_property('proposed_pledges', 'reqd', 0); + } else { + frm.set_df_property('proposed_pledges', 'reqd', 1); + } }, calculate_amounts: function(frm, cdt, cdn) { diff --git a/lending/loan_management/doctype/loan_application/loan_application.json b/lending/loan_management/doctype/loan_application/loan_application.json index b2089cac..99c5d8ab 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.json +++ b/lending/loan_management/doctype/loan_application/loan_application.json @@ -17,7 +17,7 @@ "loan_product", "is_term_loan", "loan_amount", - "is_secured_loan", + "loan_security_preference", "rate_of_interest", "column_break_7", "description", @@ -178,19 +178,13 @@ "read_only": 1 }, { - "default": "0", - "fieldname": "is_secured_loan", - "fieldtype": "Check", - "label": "Is Secured Loan" - }, - { - "depends_on": "eval:doc.is_secured_loan == 1", + "depends_on": "eval:doc.loan_security_preference != \"Unsecured\"", "fieldname": "loan_security_details_section", "fieldtype": "Section Break", "label": "Loan Security Details" }, { - "depends_on": "eval:doc.is_secured_loan == 1", + "depends_on": "eval:doc.loan_security_preference != \"Unsecured\"", "fieldname": "proposed_pledges", "fieldtype": "Table", "label": "Proposed Pledges", @@ -210,15 +204,23 @@ "fieldtype": "Check", "label": "Is Term Loan", "read_only": 1 + }, + { + "default": "Unsecured", + "fieldname": "loan_security_preference", + "fieldtype": "Select", + "label": "Loan Security Preference", + "options": "Unsecured\nSemi-secured\nSecured" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-10-02 22:14:22.606618", + "modified": "2023-10-18 05:33:57.484668", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Application", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { diff --git a/lending/loan_management/doctype/loan_application/loan_application.py b/lending/loan_management/doctype/loan_application/loan_application.py index 067f03c4..55c465a2 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.py +++ b/lending/loan_management/doctype/loan_application/loan_application.py @@ -143,15 +143,17 @@ def calculate_payable_amount(self): self.total_payable_amount = self.loan_amount + self.total_payable_interest def set_loan_amount(self): - if self.is_secured_loan and not self.proposed_pledges: + if self.loan_security_preference != "Unsecured" and not self.proposed_pledges: frappe.throw(_("Proposed Pledges are mandatory for secured Loans")) - if self.is_secured_loan and self.proposed_pledges: + if self.loan_security_preference != "Unsecured" and self.proposed_pledges: self.maximum_loan_amount = 0 for security in self.proposed_pledges: self.maximum_loan_amount += flt(security.post_haircut_amount) - if not self.loan_amount and self.is_secured_loan and self.proposed_pledges: + if ( + not self.loan_amount and self.loan_security_preference != "Unsecured" and self.proposed_pledges + ): self.loan_amount = self.maximum_loan_amount @@ -170,7 +172,7 @@ def update_accounts(source_doc, target_doc, source_parent): filters={"name": source_doc.loan_product}, )[0] - if source_doc.is_secured_loan: + if source_doc.loan_security_preference != "Unsecured": target_doc.maximum_loan_amount = 0 target_doc.mode_of_payment = account_details.mode_of_payment diff --git a/lending/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py b/lending/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py index 50871777..69f8d3b6 100644 --- a/lending/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py +++ b/lending/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py @@ -66,7 +66,7 @@ def set_status_and_amounts(self, cancel=0): "total_interest_payable", "status", "is_term_loan", - "is_secured_loan", + "loan_security_preference", ], as_dict=1, ) diff --git a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py index bcf178d4..9848ca57 100644 --- a/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -10,6 +10,9 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.controllers.accounts_controller import AccountsController +from lending.loan_management.doctype.loan_security.loan_security import ( + update_utilized_loan_securities_values, +) from lending.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import ( get_pledged_security_qty, ) @@ -29,6 +32,12 @@ def on_submit(self): self.set_status_and_amounts() self.withheld_security_deposit() + + update_utilized_loan_securities_values( + self.against_loan, self.disbursed_amount, "Loan Disbursement", self.name, disbursement=True + ) + set_status_of_loan_securities(self.against_loan) + self.make_gl_entries() def update_repayment_schedule_status(self, cancel=0): @@ -52,6 +61,15 @@ def on_cancel(self): self.update_repayment_schedule_status(cancel=1) self.delete_security_deposit() + update_utilized_loan_securities_values( + self.against_loan, + self.disbursed_amount, + "Loan Disbursement", + self.name, + disbursement=True, + on_trigger_doc_cancel=1, + ) + set_status_of_loan_securities(self.against_loan, cancel=1) self.set_status_and_amounts(cancel=1) self.make_gl_entries(cancel=1) self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] @@ -104,7 +122,7 @@ def set_status_and_amounts(self, cancel=0): "total_interest_payable", "status", "is_term_loan", - "is_secured_loan", + "loan_security_preference", ], filters={"name": self.against_loan}, )[0] @@ -337,14 +355,14 @@ def get_disbursal_amount(loan, on_current_security_price=0): "total_interest_payable", "status", "is_term_loan", - "is_secured_loan", + "loan_security_preference", "maximum_loan_amount", "written_off_amount", ], as_dict=1, ) - if loan_details.is_secured_loan and frappe.get_all( + if loan_details.loan_security_preference != "Unsecured" and frappe.get_all( "Loan Security Shortfall", filters={"loan": loan, "status": "Pending"} ): return 0 @@ -352,13 +370,13 @@ def get_disbursal_amount(loan, on_current_security_price=0): pending_principal_amount = get_pending_principal_amount(loan_details) security_value = 0.0 - if loan_details.is_secured_loan and on_current_security_price: + if loan_details.loan_security_preference != "Unsecured" and on_current_security_price: security_value = get_total_pledged_security_value(loan) - if loan_details.is_secured_loan and not on_current_security_price: + if loan_details.loan_security_preference != "Unsecured" and not on_current_security_price: security_value = get_maximum_amount_as_per_pledged_security(loan) - if not security_value and not loan_details.is_secured_loan: + if not security_value and not loan_details.loan_security_preference != "Unsecured": security_value = flt(loan_details.loan_amount) disbursal_amount = flt(security_value) - flt(pending_principal_amount) @@ -374,3 +392,24 @@ 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 Pledge", {"loan": loan}, "sum(maximum_loan_value)")) + + +def set_status_of_loan_securities(loan, cancel=0): + if not cancel: + new_status = "Hypothecated" + old_status = "Pending Hypothecation" + else: + new_status = "Pending Hypothecation" + old_status = "Hypothecated" + + frappe.db.sql( + """ + UPDATE `tabLoan Security` + JOIN `tabPledge` ON `tabPledge`.`loan_security`=`tabLoan Security`.`name` + JOIN `tabLoan Security Pledge` ON `tabLoan Security Pledge`.`name`=`tabPledge`.`parent` + JOIN `tabLoan` ON `tabLoan`.`name`=`tabLoan Security Pledge`.`loan` + SET `tabLoan Security`.`status`=%s + WHERE `tabLoan`.`name`=%s AND `tabLoan Security`.`status`=%s + """, + (new_status, loan, old_status), + ) 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 b3698220..a1ad6400 100644 --- a/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/lending/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -63,8 +63,13 @@ def setUp(self): "Penalty Income Account - _TC", ) + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") + create_loan_security_type() - create_loan_security() + create_loan_security(self.applicant) create_loan_security_price( "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) @@ -73,11 +78,6 @@ def setUp(self): "Test Security 2", 250, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) ) - if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - - self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") - def test_loan_topup(self): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] 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..4f335200 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 @@ -72,18 +72,18 @@ def setUp(self): days_past_due_threshold_for_npa=90, ) + if not frappe.db.exists("Customer", "_Test Loan Customer"): + frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) + + self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") + create_loan_security_type() - create_loan_security() + create_loan_security(self.applicant) create_loan_security_price( "Test Security 1", 500, "Nos", get_datetime(), get_datetime(add_to_date(nowdate(), hours=24)) ) - if not frappe.db.exists("Customer", "_Test Loan Customer"): - frappe.get_doc(get_customer_dict("_Test Loan Customer")).insert(ignore_permissions=True) - - self.applicant = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name") - setup_loan_classification_ranges("_Test Company") def test_loan_interest_accural(self): diff --git a/lending/loan_management/doctype/loan_repayment/loan_repayment.py b/lending/loan_management/doctype/loan_repayment/loan_repayment.py index 9d9a7908..891b7ad3 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.loan_security import ( + update_utilized_loan_securities_values, +) from lending.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import ( update_shortfall_status, ) @@ -53,6 +56,10 @@ def on_submit(self): if self.repayment_type == "Charges Waiver": self.make_credit_note() + update_utilized_loan_securities_values( + self.against_loan, self.amount_paid, "Loan Repayment", self.name, repayment=True + ) + self.make_gl_entries() def on_cancel(self): @@ -68,6 +75,14 @@ def on_cancel(self): frappe.db.set_value("Loan", self.against_loan, "days_past_due", self.days_past_due) + update_utilized_loan_securities_values( + self.against_loan, + self.amount_paid, + "Loan Repayment", + self.name, + repayment=True, + on_trigger_doc_cancel=1, + ) self.ignore_linked_doctypes = [ "GL Entry", "Payment Ledger Entry", @@ -242,7 +257,7 @@ def update_paid_amount(self): "total_amount_paid", "total_principal_paid", "status", - "is_secured_loan", + "loan_security_preference", "total_payment", "debit_adjustment_amount", "credit_adjustment_amount", @@ -263,7 +278,7 @@ def update_paid_amount(self): ) pending_principal_amount = get_pending_principal_amount(loan) - if not loan.is_secured_loan and pending_principal_amount <= 0: + if loan.loan_security_preference == "Unsecured" and pending_principal_amount <= 0: loan.update({"status": "Loan Closure Requested"}) for payment in self.repayment_details: @@ -297,7 +312,7 @@ def mark_as_unpaid(self): "total_amount_paid", "total_principal_paid", "status", - "is_secured_loan", + "loan_security_preference", "total_payment", "loan_amount", "disbursed_amount", 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..0f67a983 100644 --- a/lending/loan_management/doctype/loan_security/loan_security.json +++ b/lending/loan_management/doctype/loan_security/loan_security.json @@ -1,18 +1,42 @@ { "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_name", - "haircut", + "section_break_xfky", "loan_security_code", + "loan_security_name", + "loan_security_owner_type", + "loan_security_owner", "column_break_3", "loan_security_type", + "status", + "quantifiable", + "disabled", + "section_break_gvsp", + "haircut", + "column_break_aegu", + "loan_to_value_ratio", + "section_break_krns", "unit_of_measure", - "disabled" + "original_security_price", + "column_break_ecmw", + "quantity", + "section_break_qwjv", + "original_security_value", + "utilized_security_value", + "column_break_zkmd", + "original_post_haircut_security_value", + "available_security_value", + "section_break_qhsn", + "description", + "column_break_mpwb", + "released_at", + "amended_from" ], "fields": [ { @@ -46,31 +70,180 @@ "fieldname": "loan_security_code", "fieldtype": "Data", "label": "Loan Security Code", + "reqd": 1, "unique": 1 }, { + "allow_on_submit": 1, "default": "0", "fieldname": "disabled", "fieldtype": "Check", "label": "Disabled" }, { + "fieldname": "section_break_gvsp", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_aegu", + "fieldtype": "Column Break" + }, + { + "fieldname": "utilized_security_value", + "fieldtype": "Currency", + "label": "Utilized Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fetch_from": "loan_security_type.loan_to_value_ratio", + "fetch_if_empty": 1, + "fieldname": "loan_to_value_ratio", + "fieldtype": "Percent", + "label": "Loan To Value Ratio" + }, + { + "fieldname": "section_break_xfky", + "fieldtype": "Section Break" + }, + { + "default": "Pending Hypothecation", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Pending Hypothecation\nHypothecated\nReleased\nRepossessed", + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_qhsn", + "fieldtype": "Section Break", + "label": "More Information" + }, + { + "allow_on_submit": 1, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description" + }, + { + "fieldname": "column_break_mpwb", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_qwjv", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_zkmd", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.status == \"Released\"", + "fieldname": "released_at", + "fieldtype": "Datetime", + "label": "Released At", + "mandatory_depends_on": "eval:doc.status == \"Released\"", + "read_only": 1 + }, + { + "fieldname": "loan_security_owner_type", + "fieldtype": "Select", + "label": "Loan Security Owner Type", + "options": "\nEmployee\nMember\nCustomer\nCompany", + "reqd": 1 + }, + { + "fieldname": "loan_security_owner", + "fieldtype": "Dynamic Link", + "label": "Loan Security Owner", + "options": "loan_security_owner_type", + "reqd": 1 + }, + { + "fieldname": "available_security_value", + "fieldtype": "Currency", + "label": "Available Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "depends_on": "eval:doc.loan_security_type", + "fieldname": "original_security_value", + "fieldtype": "Currency", + "label": "Original Security Value", + "mandatory_depends_on": "eval:!doc.quantifiable", + "options": "Company:company:default_currency", + "read_only_depends_on": "eval:doc.quantifiable" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "original_post_haircut_security_value", + "fieldtype": "Currency", + "label": "Original Post Haircut Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan_security_type.quantifiable", + "fieldname": "quantifiable", + "fieldtype": "Check", + "label": "Quantifiable", + "read_only": 1 + }, + { + "fieldname": "section_break_krns", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.quantifiable", + "fieldname": "original_security_price", + "fieldtype": "Currency", + "label": "Original Security Price (per UoM)", + "mandatory_depends_on": "eval:doc.quantifiable", + "options": "Company:company:default_currency" + }, + { + "fieldname": "column_break_ecmw", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.quantifiable", "fetch_from": "loan_security_type.unit_of_measure", "fieldname": "unit_of_measure", "fieldtype": "Link", - "in_list_view": 1, "label": "Unit Of Measure", + "mandatory_depends_on": "eval:doc.quantifiable", "options": "UOM", - "read_only": 1, - "reqd": 1 + "read_only": 1 + }, + { + "depends_on": "eval:doc.quantifiable", + "fieldname": "quantity", + "fieldtype": "Float", + "label": "Quantity", + "mandatory_depends_on": "eval:doc.quantifiable", + "non_negative": 1 } ], "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2020-10-26 07:34:48.601766", + "modified": "2023-10-18 11:27:55.213220", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -101,5 +274,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..17290e17 100644 --- a/lending/loan_management/doctype/loan_security/loan_security.py +++ b/lending/loan_management/doctype/loan_security/loan_security.py @@ -1,11 +1,198 @@ # 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 flt, now_datetime + +from lending.loan_management.doctype.loan_security_utilized_and_available_value_log.loan_security_utilized_and_available_value_log import ( + create_loan_security_utilized_and_available_value_log, +) class LoanSecurity(Document): - def autoname(self): - self.name = self.loan_security_name + def validate(self): + self.set_missing_values() + + def set_missing_values(self): + if self.quantifiable: + self.original_security_value = flt(self.quantity * self.original_security_price) + else: + self.quantity = 1 + self.original_security_price = self.original_security_value + + original_post_haircut_security_value = flt( + self.original_security_value - (self.original_security_value * self.haircut / 100) + ) + self.original_post_haircut_security_value = original_post_haircut_security_value + self.available_security_value = original_post_haircut_security_value + + +def update_utilized_loan_securities_values( + loan, + amount, + trigger_doctype, + trigger_doc, + disbursement=False, + repayment=False, + on_trigger_doc_cancel=0, +): + if (disbursement and repayment) or (not disbursement and not repayment): + frappe.throw(_("The action needs to be either disbursement or repayment")) + + if frappe.db.get_value("Loan", loan, "loan_security_preference") == "Unsecured": + return + + loan_securities_w_ratio = [] + + for loan_security_pledge in frappe.db.get_all( + "Loan Security Pledge", {"loan": loan}, pluck="name" + ): + loan_securities = frappe.db.get_all( + "Pledge", {"parent": loan_security_pledge}, pluck="loan_security" + ) + + for loan_security in loan_securities: + ( + utilized_security_value, + original_post_haircut_security_value, + available_security_value, + ) = frappe.db.get_value( + "Loan Security", + loan_security, + [ + "utilized_security_value", + "original_post_haircut_security_value", + "available_security_value", + ], + ) + utilized_to_original_security_ratio = flt( + utilized_security_value / original_post_haircut_security_value + ) + loan_securities_w_ratio.append( + frappe._dict( + { + "loan_security": loan_security, + "utilized_security_value": utilized_security_value, + "available_security_value": available_security_value, + "ratio": utilized_to_original_security_ratio, + } + ) + ) + + utilized_value_increased = ( + True + if (disbursement and not on_trigger_doc_cancel) or (repayment and on_trigger_doc_cancel) + else False + ) + + sorted_loan_securities_w_ratio = sorted( + loan_securities_w_ratio, + key=lambda k: k["ratio"], + reverse=utilized_value_increased, + ) + + for loan_security in sorted_loan_securities_w_ratio: + if amount <= 0: + break + + if utilized_value_increased: + if loan_security.utilized_security_value + amount > loan_security.available_security_value: + new_utilized_security_value = loan_security.available_security_value + new_available_security_value = 0 + amount = ( + amount - loan_security.available_security_value + loan_security.utilized_security_value + ) + else: + new_utilized_security_value = loan_security.utilized_security_value + amount + new_available_security_value = loan_security.available_security_value - amount + amount = 0 + else: + if loan_security.utilized_security_value >= amount: + new_utilized_security_value = loan_security.utilized_security_value - amount + new_available_security_value = loan_security.available_security_value + amount + amount = 0 + else: + new_utilized_security_value = 0 + new_available_security_value = ( + loan_security.available_security_value + new_utilized_security_value + ) + amount = amount - loan_security.utilized_security_value + + frappe.db.set_value( + "Loan Security", + loan_security.loan_security, + { + "utilized_security_value": new_utilized_security_value, + "available_security_value": new_available_security_value, + }, + ) + + create_loan_security_utilized_and_available_value_log( + loan_security=loan_security.loan_security, + trigger_doctype=trigger_doctype, + trigger_document=trigger_doc, + on_trigger_doc_cancel=on_trigger_doc_cancel, + new_available_security_value=new_available_security_value, + new_utilized_security_value=new_utilized_security_value, + previous_available_security_value=loan_security.available_security_value, + previous_utilized_security_value=loan_security.utilized_security_value, + ) + + +def get_active_pledges_linked_to_loan_security(loan_security): + active_pledges = [] + + pledges = frappe.db.sql( + """ + SELECT lp.loan, lp.name as pledge + FROM `tabLoan Security Pledge` lp, `tabPledge`p + WHERE p.loan_security = %s + AND p.parent = lp.name + AND lp.status = 'Pledged' + """, + (loan_security), + as_dict=True, + ) + + unpledges = frappe.db.sql( + """ + SELECT up.loan + FROM `tabLoan Security Unpledge` up, `tabUnpledge` u + WHERE u.loan_security = %s + AND u.parent = up.name + AND up.status = 'Approved' + """, + (loan_security), + as_list=True, + ) + unpledges = list(itertools.chain(*unpledges)) + + for loan_and_pledge in pledges: + if loan_and_pledge.loan not in unpledges: + active_pledges.append(loan_and_pledge) + + return active_pledges + + +@frappe.whitelist() +def release_loan_security(loan_security): + active_pledges = get_active_pledges_linked_to_loan_security(loan_security) + + if active_pledges: + msg = _("Loan Security {0} is still linked with active loans:").format( + frappe.bold(loan_security) + ) + for loan_and_pledge in active_pledges: + msg += "

" + msg += _("Loan {0} through Loan Security Pledge {1}").format( + frappe.bold(loan_and_pledge.loan), frappe.bold(loan_and_pledge.pledge) + ) + frappe.throw(msg, title=_("Security cannot be released")) + else: + frappe.db.set_value( + "Loan Security", loan_security, {"status": "Released", "released_at": now_datetime()} + ) 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..6da1f940 --- /dev/null +++ b/lending/loan_management/doctype/loan_security/loan_security_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +// License: GNU General Public License v3. See license.txt + +frappe.listview_settings['Loan Security'] = { + get_indicator: function(doc) { + var status_color = { + "Pending Hypothecation": "grey", + "Hypothecated": "green", + "Released": "orange", + "Repossessed": "red" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + }, +}; diff --git a/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.js b/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.js index e61868e8..45609737 100644 --- a/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.js +++ b/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.js @@ -5,13 +5,16 @@ frappe.ui.form.on('Loan Security Pledge', { calculate_amounts: function(frm, cdt, cdn) { let row = locals[cdt][cdn]; 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))); + + if (row.quantifiable) { + frappe.model.set_value(cdt, cdn, 'available_security_value', cint(row.qty); + } let amount = 0; let maximum_amount = 0; $.each(frm.doc.securities || [], function(i, item){ amount += item.amount; - maximum_amount += item.post_haircut_amount; + maximum_amount += item.available_security_value; }); frm.set_value('total_security_value', amount); diff --git a/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 5b68157a..14c1017b 100644 --- a/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/lending/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -20,6 +20,8 @@ def validate(self): self.set_pledge_amount() self.validate_duplicate_securities() self.validate_loan_security_type() + self.validate_loan_security_quantities() + self.validate_utilized_loan_securities_values() def on_submit(self): if self.loan: @@ -69,6 +71,39 @@ 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 validate_loan_security_quantities(self): + for security in self.securities: + if not security.quantifiable and security.qty != 1: + frappe.throw( + _("Row {0}: {1}'s quantity needs to be set to 1").format( + security.idx, + frappe.bold(security.loan_security), + ) + ) + + def validate_utilized_loan_securities_values(self): + if not self.loan: + return + + loan_amount = frappe.db.get_value("Loan", self.loan, "loan_amount") + + total_utilized_securities_value = 0 + for loan_security in self.securities: + total_utilized_securities_value += frappe.db.get_value( + "Loan Security", loan_security, "utilized_security_value" + ) + + extra_security_value_needed = ( + loan_amount + total_utilized_securities_value - self.maximum_loan_value + ) + + if extra_security_value_needed > 0: + frappe.throw( + _("Loan Securities worth {0} needed more to book the loan.").format( + frappe.bold(extra_security_value_needed), + ) + ) + def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 @@ -105,7 +140,7 @@ def update_loan(loan, maximum_value_against_pledge, cancel=0): ) else: frappe.db.sql( - """ UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1 + """ UPDATE `tabLoan` SET maximum_loan_amount=%s, loan_security_preference="Secured" WHERE name=%s""", (maximum_loan_value + maximum_value_against_pledge, loan), ) diff --git a/lending/loan_management/doctype/loan_security_price/loan_security_price.json b/lending/loan_management/doctype/loan_security_price/loan_security_price.json index b6e87637..c6a2420f 100644 --- a/lending/loan_management/doctype/loan_security_price/loan_security_price.json +++ b/lending/loan_management/doctype/loan_security_price/loan_security_price.json @@ -8,15 +8,24 @@ "field_order": [ "loan_security", "loan_security_name", - "loan_security_type", "column_break_2", - "uom", + "loan_security_type", + "haircut", + "quantifiable", "section_break_4", + "uom", "loan_security_price", + "column_break_cege", + "quantity", + "section_break_kszb", + "loan_security_value", + "column_break_pmdj", + "loan_post_haircut_security_value", "section_break_6", "valid_from", "column_break_8", - "valid_upto" + "valid_upto", + "amended_from" ], "fields": [ { @@ -32,6 +41,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.quantifiable", "fetch_from": "loan_security.unit_of_measure", "fieldname": "uom", "fieldtype": "Link", @@ -44,12 +54,13 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:doc.quantifiable", "fieldname": "loan_security_price", "fieldtype": "Currency", "in_list_view": 1, - "label": "Loan Security Price", - "options": "Company:company:default_currency", - "reqd": 1 + "label": "Loan Security Price (per UoM)", + "mandatory_depends_on": "eval:doc.quantifiable", + "options": "Company:company:default_currency" }, { "fieldname": "section_break_6", @@ -87,14 +98,77 @@ "fieldtype": "Data", "label": "Loan Security Name", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.loan_security", + "fetch_from": "loan_security.quantifiable", + "fieldname": "quantifiable", + "fieldtype": "Check", + "label": "Quantifiable", + "read_only": 1 + }, + { + "fieldname": "column_break_cege", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.quantifiable", + "fetch_from": "loan_security.quantity", + "fieldname": "quantity", + "fieldtype": "Float", + "label": "Quantity", + "read_only": 1 + }, + { + "depends_on": "eval:doc.loan_security", + "fieldname": "loan_security_value", + "fieldtype": "Currency", + "label": "Loan Security Value", + "mandatory_depends_on": "eval:!doc.quantifiable", + "options": "Company:company:default_currency", + "read_only_depends_on": "eval:doc.quantifiable" + }, + { + "fieldname": "loan_post_haircut_security_value", + "fieldtype": "Currency", + "label": "Loan Post Haircut Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "section_break_kszb", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_pmdj", + "fieldtype": "Column Break" + }, + { + "fetch_from": "loan_security.haircut", + "fieldname": "haircut", + "fieldtype": "Percent", + "label": "Haircut %", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Security Price", + "print_hide": 1, + "read_only": 1 } ], "index_web_pages_for_search": 1, + "is_submittable": 1, "links": [], - "modified": "2021-01-17 07:41:49.598086", + "modified": "2023-10-18 11:53:55.187301", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Price", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -122,8 +196,8 @@ "write": 1 } ], - "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/loan_security_price/loan_security_price.py b/lending/loan_management/doctype/loan_security_price/loan_security_price.py index 45c4459a..19a89b41 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 @@ -5,21 +5,42 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import get_datetime +from frappe.utils import flt, get_datetime + +from lending.loan_management.doctype.loan_security_utilized_and_available_value_log.loan_security_utilized_and_available_value_log import ( + create_loan_security_utilized_and_available_value_log, +) class LoanSecurityPrice(Document): def validate(self): self.validate_dates() + self.set_missing_values() - def validate_dates(self): + def set_missing_values(self): + if self.quantifiable: + self.loan_security_value = flt(self.quantity * self.loan_security_price) + else: + self.quantity = 1 + self.loan_security_price = self.loan_security_value + + self.loan_post_haircut_security_value = flt( + self.loan_security_value - (self.loan_security_value * self.haircut / 100) + ) + + def on_submit(self): + self.update_loan_available_security_value() + def on_cancel(self): + self.update_loan_available_security_value(cancel=True) + + def validate_dates(self): if self.valid_from > self.valid_upto: frappe.throw(_("Valid From Time must be lesser than Valid Upto Time.")) existing_loan_security = frappe.db.sql( """ SELECT name from `tabLoan Security Price` - WHERE loan_security = %s AND name != %s AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, + WHERE loan_security = %s AND name != %s AND docstatus = 1 AND (valid_from BETWEEN %s and %s OR valid_upto BETWEEN %s and %s) """, ( self.loan_security, self.name, @@ -33,6 +54,57 @@ def validate_dates(self): if existing_loan_security: frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0])) + def update_loan_available_security_value(self, cancel=False): + available_security_value, original_post_haircut_security_value = frappe.db.get_value( + "Loan Security", + self.loan_security, + ["available_security_value", "original_post_haircut_security_value"], + ) + + if not cancel: + latest_post_haircut_security_value = frappe.db.get_list( + "Loan Security Price", + filters={"loan_security": self.loan_security, "docstatus": 1}, + fields=["loan_post_haircut_security_value"], + order_by="creation desc", + page_length=1, + as_list=True, + ) + + if latest_post_haircut_security_value: + latest_post_haircut_security_value = latest_post_haircut_security_value[0][0] + else: + latest_post_haircut_security_value = original_post_haircut_security_value + + new_available_security_value = flt( + (available_security_value * latest_post_haircut_security_value) + / original_post_haircut_security_value + ) + else: + new_available_security_value = frappe.db.get_list( + "Loan Security Utilized and Available Value Log", + filters={"loan_security": self.loan_security, "trigger_document": self.name}, + fields=["previous_available_security_value"], + order_by="creation desc", + page_length=1, + as_list=True, + )[0][0] + + frappe.db.set_value( + "Loan Security", self.loan_security, "available_security_value", new_available_security_value + ) + + create_loan_security_utilized_and_available_value_log( + loan_security=self.loan_security, + trigger_doctype="Loan Security Price", + trigger_document=self.name, + on_trigger_doc_cancel=cancel, + new_available_security_value=new_available_security_value, + new_utilized_security_value=None, + previous_available_security_value=available_security_value, + previous_utilized_security_value=None, + ) + @frappe.whitelist() def get_loan_security_price(loan_security, valid_time=None): @@ -45,11 +117,12 @@ def get_loan_security_price(loan_security, valid_time=None): "loan_security": loan_security, "valid_from": ("<=", valid_time), "valid_upto": (">=", valid_time), + "docstatus": 1, }, "loan_security_price", ) - if not loan_security_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) - else: + if loan_security_price: return loan_security_price + else: + return frappe.db.get_value("Loan Security", loan_security, "original_security_price") diff --git a/lending/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/lending/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 4fa18136..a9bf30ea 100644 --- a/lending/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/lending/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -84,7 +84,10 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): "disbursed_amount", "status", ], - filters={"status": ("in", ["Disbursed", "Partially Disbursed"]), "is_secured_loan": 1}, + filters={ + "status": ("in", ["Disbursed", "Partially Disbursed"]), + "loan_security_preference": ("in", ["Semi-secured", "Secured"]), + }, ) loan_shortfall_map = frappe._dict( 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..effca3f0 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 @@ -7,8 +7,9 @@ "engine": "InnoDB", "field_order": [ "loan_security_type", - "unit_of_measure", "haircut", + "quantifiable", + "unit_of_measure", "column_break_5", "loan_to_value_ratio", "disabled" @@ -35,12 +36,14 @@ "label": "Haircut %" }, { + "default": "Nos", + "depends_on": "eval:doc.quantifiable", "fieldname": "unit_of_measure", "fieldtype": "Link", "in_list_view": 1, "label": "Unit Of Measure", - "options": "UOM", - "reqd": 1 + "mandatory_depends_on": "eval:doc.quantifiable", + "options": "UOM" }, { "fieldname": "column_break_5", @@ -51,13 +54,20 @@ "fieldname": "loan_to_value_ratio", "fieldtype": "Percent", "label": "Loan To Value Ratio" + }, + { + "default": "0", + "fieldname": "quantifiable", + "fieldtype": "Check", + "label": "Quantifiable" } ], "links": [], - "modified": "2020-05-16 09:38:45.988080", + "modified": "2023-10-17 19:23:26.675506", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Type", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -88,5 +98,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/loan_security_utilized_and_available_value_log/__init__.py b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.js b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.js new file mode 100644 index 00000000..dc282939 --- /dev/null +++ b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Loan Security Utilized and Available Value Log", { +// refresh(frm) { + +// }, +// }); diff --git a/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.json b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.json new file mode 100644 index 00000000..9eb700f0 --- /dev/null +++ b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.json @@ -0,0 +1,122 @@ +{ + "actions": [], + "creation": "2023-10-17 22:28:37.653463", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "loan_security", + "trigger_doctype", + "trigger_document_docstatus", + "column_break_bohv", + "posting_date", + "trigger_document", + "section_break_ilvz", + "previous_utilized_security_value", + "previous_available_security_value", + "column_break_kgfk", + "new_utilized_security_value", + "new_available_security_value" + ], + "fields": [ + { + "fieldname": "loan_security", + "fieldtype": "Link", + "label": "Loan Security", + "options": "Loan Security", + "read_only": 1 + }, + { + "fieldname": "previous_utilized_security_value", + "fieldtype": "Currency", + "label": "Previous Utilized Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "new_utilized_security_value", + "fieldtype": "Currency", + "label": "New Utilized Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "previous_available_security_value", + "fieldtype": "Currency", + "label": "Previous Available Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_kgfk", + "fieldtype": "Column Break" + }, + { + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "read_only": 1 + }, + { + "fieldname": "new_available_security_value", + "fieldtype": "Currency", + "label": "New Available Security Value", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "trigger_doctype", + "fieldtype": "Link", + "label": "Trigger DocType", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "trigger_document", + "fieldtype": "Dynamic Link", + "label": "Trigger Document", + "options": "trigger_doctype", + "read_only": 1 + }, + { + "fieldname": "trigger_document_docstatus", + "fieldtype": "Select", + "label": "Trigger Document Docstatus", + "options": "\n1\n2", + "read_only": 1 + }, + { + "fieldname": "column_break_bohv", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ilvz", + "fieldtype": "Section Break" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-10-17 23:20:37.604195", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Security Utilized and Available Value Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.py b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.py new file mode 100644 index 00000000..66b6892e --- /dev/null +++ b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/loan_security_utilized_and_available_value_log.py @@ -0,0 +1,44 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.utils import nowdate + + +class LoanSecurityUtilizedandAvailableValueLog(Document): + pass + + +def create_loan_security_utilized_and_available_value_log( + loan_security, + trigger_doctype, + trigger_document, + on_trigger_doc_cancel, + new_available_security_value=None, + new_utilized_security_value=None, + previous_available_security_value=None, + previous_utilized_security_value=None, +): + doc = frappe.new_doc("Loan Security Utilized and Available Value Log") + + doc.loan_security = loan_security + doc.posting_date = nowdate() + doc.trigger_doctype = trigger_doctype + doc.trigger_document = trigger_document + doc.trigger_document_docstatus = 2 if on_trigger_doc_cancel else 1 + + old_available_security_value, old_utilized_security_value = frappe.db.get_value( + "Loan Security", loan_security, ["available_security_value", "utilized_security_value"] + ) + + doc.new_utilized_security_value = new_utilized_security_value or old_utilized_security_value + doc.new_available_security_value = new_available_security_value or old_available_security_value + doc.previous_available_security_value = ( + previous_available_security_value or old_available_security_value + ) + doc.previous_utilized_security_value = ( + previous_utilized_security_value or old_utilized_security_value + ) + + doc.insert() diff --git a/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/test_loan_security_utilized_and_available_value_log.py b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/test_loan_security_utilized_and_available_value_log.py new file mode 100644 index 00000000..246d52b9 --- /dev/null +++ b/lending/loan_management/doctype/loan_security_utilized_and_available_value_log/test_loan_security_utilized_and_available_value_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestLoanSecurityUtilizedandAvailableValueLog(FrappeTestCase): + pass diff --git a/lending/loan_management/doctype/pledge/pledge.json b/lending/loan_management/doctype/pledge/pledge.json index c23479c8..6854493e 100644 --- a/lending/loan_management/doctype/pledge/pledge.json +++ b/lending/loan_management/doctype/pledge/pledge.json @@ -8,14 +8,14 @@ "loan_security", "loan_security_name", "loan_security_type", - "loan_security_code", "uom", - "column_break_5", "qty", + "quantifiable", + "column_break_5", "haircut", "loan_security_price", "amount", - "post_haircut_amount" + "available_security_value" ], "fields": [ { @@ -34,20 +34,17 @@ "options": "Loan Security Type", "read_only": 1 }, - { - "fetch_from": "loan_security.loan_security_code", - "fieldname": "loan_security_code", - "fieldtype": "Data", - "label": "Loan Security Code" - }, { "fetch_from": "loan_security.unit_of_measure", "fieldname": "uom", "fieldtype": "Link", "label": "UOM", - "options": "UOM" + "options": "UOM", + "read_only": 1 }, { + "fetch_from": "loan_security.quantity", + "fetch_if_empty": 1, "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, @@ -74,30 +71,41 @@ "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", "fieldtype": "Column Break" }, - { - "fieldname": "post_haircut_amount", - "fieldtype": "Currency", - "label": "Post Haircut Amount", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "fetch_from": "loan_security.loan_security_name", "fieldname": "loan_security_name", "fieldtype": "Data", "label": "Loan Security Name", "read_only": 1 + }, + { + "default": "0", + "fetch_from": "loan_security.quantifiable", + "fieldname": "quantifiable", + "fieldtype": "Check", + "label": "Quantifiable", + "read_only": 1 + }, + { + "fetch_from": "loan_security.available_security_value", + "fetch_if_empty": 1, + "fieldname": "available_security_value", + "fieldtype": "Currency", + "label": "Available Security Value", + "options": "Company:company:default_currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2021-01-17 07:41:12.452514", + "modified": "2023-10-18 19:23:32.236784", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", @@ -106,5 +114,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/process_loan_security_shortfall/process_loan_security_shortfall.py b/lending/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py index 1681df8a..161df9f7 100644 --- a/lending/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py +++ b/lending/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.py @@ -27,4 +27,6 @@ def create_process_loan_security_shortfall(): def check_for_secured_loans(): - return frappe.db.count("Loan", {"docstatus": 1, "is_secured_loan": 1}) + return frappe.db.count( + "Loan", {"docstatus": 1, "loan_security_preference": ("in", ["Semi-secured", "Secured"])} + ) diff --git a/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json b/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json index a0b3a79b..2a46c7c0 100644 --- a/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/lending/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -27,7 +27,8 @@ "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 +38,8 @@ "read_only": 1 }, { - "fetch_from": "loan_security_pledge.qty", + "fetch_from": "loan_security.quantity", + "fetch_if_empty": 1, "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, @@ -69,7 +71,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-17 07:29:01.671722", + "modified": "2023-10-18 03:18:15.038921", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", @@ -78,5 +80,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/unpledge/unpledge.json b/lending/loan_management/doctype/unpledge/unpledge.json index 0091e6c4..b07bc183 100644 --- a/lending/loan_management/doctype/unpledge/unpledge.json +++ b/lending/loan_management/doctype/unpledge/unpledge.json @@ -74,7 +74,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-01-17 07:36:20.212342", + "modified": "2023-10-18 03:39:02.822656", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", @@ -83,5 +83,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/lending/patches.txt b/lending/patches.txt index e20037fa..1d67cd91 100644 --- a/lending/patches.txt +++ b/lending/patches.txt @@ -26,4 +26,5 @@ lending.patches.v15_0.rename_loan_partner_charge_type lending.patches.v15_0.migrate_loan_type_to_loan_product lending.patches.v15_0.add_loan_product_code_and_rename_loan_name lending.patches.v15_0.update_penalty_interest_method_in_loan_products -lending.patches.v15_0.update_min_bpi_application_days \ No newline at end of file +lending.patches.v15_0.update_min_bpi_application_days +lending.patches.v15_0.update_loan_security_preference \ No newline at end of file diff --git a/lending/patches/v15_0/update_loan_security_preference.py b/lending/patches/v15_0/update_loan_security_preference.py new file mode 100644 index 00000000..06b1ac1d --- /dev/null +++ b/lending/patches/v15_0/update_loan_security_preference.py @@ -0,0 +1,32 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe + + +def execute(): + loan = frappe.qb.DocType("Loan") + + ( + frappe.qb.update(loan).set( + loan.loan_security_preference, + ( + frappe.qb.terms.Case() + .when(loan.is_secured_loan == 0, "Unsecured") + .when(loan.is_secured_loan == 1, "Secured") + ), + ) + ).run() + + la = frappe.qb.DocType("Loan Application") + + ( + frappe.qb.update(la).set( + la.loan_security_preference, + ( + frappe.qb.terms.Case() + .when(la.is_secured_loan == 0, "Unsecured") + .when(la.is_secured_loan == 1, "Secured") + ), + ) + ).run()