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()