fix: Loan Security Unpledge fixes
This commit is contained in:
parent
a4567a446f
commit
dd340c15de
@ -248,8 +248,7 @@ def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_d
|
|||||||
for loan_security in loan_security_pledge_details:
|
for loan_security in loan_security_pledge_details:
|
||||||
unpledge_request.append('securities', {
|
unpledge_request.append('securities', {
|
||||||
"loan_security": loan_security.loan_security,
|
"loan_security": loan_security.loan_security,
|
||||||
"qty": loan_security.qty,
|
"qty": loan_security.qty
|
||||||
"against_pledge": loan_security.parent
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if as_dict:
|
if as_dict:
|
||||||
|
|||||||
@ -15,6 +15,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
|
|||||||
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
|
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
|
||||||
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
|
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
|
||||||
from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge
|
from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge
|
||||||
|
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
|
||||||
|
|
||||||
class TestLoan(unittest.TestCase):
|
class TestLoan(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -152,7 +153,7 @@ class TestLoan(unittest.TestCase):
|
|||||||
repayment_entry.save()
|
repayment_entry.save()
|
||||||
repayment_entry.submit()
|
repayment_entry.submit()
|
||||||
|
|
||||||
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
|
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
|
||||||
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
|
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
|
||||||
|
|
||||||
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
|
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
|
||||||
@ -305,7 +306,7 @@ class TestLoan(unittest.TestCase):
|
|||||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
|
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
|
||||||
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
|
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
|
||||||
|
|
||||||
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
|
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
|
||||||
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
|
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
|
||||||
repayment_entry.submit()
|
repayment_entry.submit()
|
||||||
|
|
||||||
@ -319,13 +320,12 @@ class TestLoan(unittest.TestCase):
|
|||||||
unpledge_request.submit()
|
unpledge_request.submit()
|
||||||
unpledge_request.status = 'Approved'
|
unpledge_request.status = 'Approved'
|
||||||
unpledge_request.save()
|
unpledge_request.save()
|
||||||
|
|
||||||
loan_security_pledge.load_from_db()
|
|
||||||
loan.load_from_db()
|
loan.load_from_db()
|
||||||
|
|
||||||
|
pledged_qty = get_pledged_security_qty(loan.name)
|
||||||
|
|
||||||
self.assertEqual(loan.status, 'Closed')
|
self.assertEqual(loan.status, 'Closed')
|
||||||
for security in loan_security_pledge.securities:
|
self.assertEquals(sum(pledged_qty.values()), 0)
|
||||||
self.assertEquals(security.qty, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def create_loan_accounts():
|
def create_loan_accounts():
|
||||||
|
|||||||
@ -264,6 +264,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
|
|||||||
penalty_amount = 0
|
penalty_amount = 0
|
||||||
payable_principal_amount = 0
|
payable_principal_amount = 0
|
||||||
final_due_date = ''
|
final_due_date = ''
|
||||||
|
due_date = ''
|
||||||
|
|
||||||
for entry in accrued_interest_entries:
|
for entry in accrued_interest_entries:
|
||||||
# Loan repayment due date is one day after the loan interest is accrued
|
# Loan repayment due date is one day after the loan interest is accrued
|
||||||
@ -272,7 +273,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
|
|||||||
|
|
||||||
due_date = add_days(entry.posting_date, 1)
|
due_date = add_days(entry.posting_date, 1)
|
||||||
no_of_late_days = date_diff(posting_date,
|
no_of_late_days = date_diff(posting_date,
|
||||||
add_days(due_date, loan_type_details.grace_period_in_days)) + 1
|
add_days(due_date, loan_type_details.grace_period_in_days))
|
||||||
|
|
||||||
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
|
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
|
||||||
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
|
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
|
||||||
@ -290,9 +291,9 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
|
|||||||
|
|
||||||
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
|
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
|
||||||
|
|
||||||
if payment_type == "Loan Closure" and not payable_principal_amount:
|
if payment_type == "Loan Closure":
|
||||||
if final_due_date:
|
if due_date:
|
||||||
pending_days = date_diff(posting_date, final_due_date)
|
pending_days = date_diff(posting_date, due_date) + 1
|
||||||
else:
|
else:
|
||||||
pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
|
pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1
|
||||||
|
|
||||||
|
|||||||
@ -69,7 +69,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
|
|||||||
loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100)
|
loan_security_map[loan.name]['security_value'] += current_loan_security_amount - (current_loan_security_amount * loan.haircut/100)
|
||||||
|
|
||||||
for loan, value in iteritems(loan_security_map):
|
for loan, value in iteritems(loan_security_map):
|
||||||
if (value["security_value"]/value["loan_amount"]) < ltv_ratio:
|
if (value["loan_amount"]/value['security_value'] * 100) > ltv_ratio:
|
||||||
create_loan_security_shortfall(loan, value, process_loan_security_shortfall)
|
create_loan_security_shortfall(loan, value, process_loan_security_shortfall)
|
||||||
|
|
||||||
def create_loan_security_shortfall(loan, value, process_loan_security_shortfall):
|
def create_loan_security_shortfall(loan, value, process_loan_security_shortfall):
|
||||||
|
|||||||
@ -4,10 +4,8 @@
|
|||||||
frappe.ui.form.on('Loan Security Unpledge', {
|
frappe.ui.form.on('Loan Security Unpledge', {
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
|
|
||||||
frm.set_query("against_pledge", "securities", () => {
|
if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') {
|
||||||
return {
|
frm.set_df_property('status', 'read_only', 1);
|
||||||
filters : [["status", "in", ["Pledged", "Partially Pledged"]]]
|
}
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -8,12 +8,13 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import get_datetime, flt
|
from frappe.utils import get_datetime, flt
|
||||||
import json
|
import json
|
||||||
|
from six import iteritems
|
||||||
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
|
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price
|
||||||
|
|
||||||
class LoanSecurityUnpledge(Document):
|
class LoanSecurityUnpledge(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_pledges()
|
|
||||||
self.validate_duplicate_securities()
|
self.validate_duplicate_securities()
|
||||||
|
self.validate_unpledge_qty()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_loan_security_pledge(cancel=1)
|
self.update_loan_security_pledge(cancel=1)
|
||||||
@ -23,80 +24,52 @@ class LoanSecurityUnpledge(Document):
|
|||||||
def validate_duplicate_securities(self):
|
def validate_duplicate_securities(self):
|
||||||
security_list = []
|
security_list = []
|
||||||
for d in self.securities:
|
for d in self.securities:
|
||||||
security = [d.loan_security, d.against_pledge]
|
if d.loan_security not in security_list:
|
||||||
if security not in security_list:
|
security_list.append(d.loan_security)
|
||||||
security_list.append(security)
|
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Row {0}: Loan Security {1} against Loan Security Pledge {2} added multiple times").format(
|
frappe.throw(_("Row {0}: Loan Security {1} added multiple times").format(
|
||||||
d.idx, frappe.bold(d.loan_security), frappe.bold(d.against_pledge)))
|
d.idx, frappe.bold(d.loan_security)))
|
||||||
|
|
||||||
def validate_pledges(self):
|
def validate_unpledge_qty(self):
|
||||||
pledge_qty_map = self.get_pledge_details()
|
pledge_qty_map = get_pledged_security_qty(self.loan)
|
||||||
loan = frappe.get_doc("Loan", self.loan)
|
|
||||||
|
|
||||||
remaining_qty = 0
|
ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
|
||||||
unpledge_value = 0
|
fields=["name", "loan_to_value_ratio"], as_list=1))
|
||||||
|
|
||||||
|
loan_security_price_map = frappe._dict(frappe.get_all("Loan Security Price",
|
||||||
|
fields=["loan_security", "loan_security_price"],
|
||||||
|
filters = {
|
||||||
|
"valid_from": ("<=", get_datetime()),
|
||||||
|
"valid_upto": (">=", get_datetime())
|
||||||
|
}, as_list=1))
|
||||||
|
|
||||||
|
loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid'])
|
||||||
|
pending_principal_amount = loan_amount - principal_paid
|
||||||
|
security_value = 0
|
||||||
|
|
||||||
for security in self.securities:
|
for security in self.securities:
|
||||||
pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0)
|
pledged_qty = pledge_qty_map.get(security.loan_security)
|
||||||
if not pledged_qty:
|
|
||||||
frappe.throw(_("Zero qty of {0} pledged against loan {1}").format(frappe.bold(security.loan_security),
|
|
||||||
frappe.bold(self.loan)))
|
|
||||||
|
|
||||||
unpledge_qty = pledged_qty - security.qty
|
if security.qty > pledged_qty:
|
||||||
security_price = security.qty * get_loan_security_price(security.loan_security)
|
frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
|
||||||
|
You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
|
||||||
|
frappe.bold(security.loan_security), frappe.bold(self.loan)))
|
||||||
|
|
||||||
if unpledge_qty < 0:
|
qty_after_unpledge = pledged_qty - security.qty
|
||||||
frappe.throw(_("""Row {0}: Cannot unpledge more than {1} qty of {2} against
|
ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
|
||||||
Loan Security Pledge {3}""").format(security.idx, frappe.bold(pledged_qty),
|
|
||||||
frappe.bold(security.loan_security), frappe.bold(security.against_pledge)))
|
|
||||||
|
|
||||||
remaining_qty += unpledge_qty
|
security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security)
|
||||||
unpledge_value += security_price - flt(security_price * security.haircut/100)
|
|
||||||
|
|
||||||
if unpledge_value > loan.total_principal_paid:
|
if not security_value and pending_principal_amount > 0:
|
||||||
frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount"))
|
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
|
||||||
|
|
||||||
def get_pledge_details(self):
|
if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio:
|
||||||
pledge_qty_map = {}
|
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
|
||||||
|
|
||||||
pledge_details = frappe.db.sql("""
|
|
||||||
SELECT p.parent, p.loan_security, p.qty FROM
|
|
||||||
`tabLoan Security Pledge` lsp,
|
|
||||||
`tabPledge` p
|
|
||||||
WHERE
|
|
||||||
p.parent = lsp.name
|
|
||||||
AND lsp.loan = %s
|
|
||||||
AND lsp.docstatus = 1
|
|
||||||
AND lsp.status in ('Pledged', 'Partially Pledged')
|
|
||||||
""", (self.loan), as_dict=1)
|
|
||||||
|
|
||||||
for pledge in pledge_details:
|
|
||||||
pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty)
|
|
||||||
|
|
||||||
return pledge_qty_map
|
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if self.status == "Approved":
|
if self.status == "Approved":
|
||||||
self.update_loan_security_pledge()
|
|
||||||
self.update_loan_status()
|
self.update_loan_status()
|
||||||
|
self.db_set('unpledge_time', get_datetime())
|
||||||
def update_loan_security_pledge(self, cancel=0):
|
|
||||||
if cancel:
|
|
||||||
new_qty = 'p.qty + u.qty'
|
|
||||||
else:
|
|
||||||
new_qty = 'p.qty - u.qty'
|
|
||||||
|
|
||||||
frappe.db.sql("""
|
|
||||||
UPDATE
|
|
||||||
`tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, `tabLoan Security Unpledge` lsu
|
|
||||||
SET p.qty = {new_qty}
|
|
||||||
WHERE
|
|
||||||
lsp.loan = %s
|
|
||||||
AND p.parent = u.against_pledge
|
|
||||||
AND p.parent = lsp.name
|
|
||||||
AND lsp.docstatus = 1
|
|
||||||
AND p.loan_security = u.loan_security""".format(new_qty=new_qty),(self.loan))
|
|
||||||
|
|
||||||
def update_loan_status(self, cancel=0):
|
def update_loan_status(self, cancel=0):
|
||||||
if cancel:
|
if cancel:
|
||||||
@ -104,10 +77,45 @@ class LoanSecurityUnpledge(Document):
|
|||||||
if loan_status == 'Closed':
|
if loan_status == 'Closed':
|
||||||
frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested')
|
frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested')
|
||||||
else:
|
else:
|
||||||
pledge_qty = frappe.db.sql("""SELECT SUM(c.qty)
|
pledged_qty = 0
|
||||||
FROM `tabLoan Security Pledge` p, `tabPledge` c
|
current_pledges = get_pledged_security_qty(self.loan)
|
||||||
WHERE p.loan = %s AND c.parent = p.name""", (self.loan))[0][0]
|
|
||||||
|
|
||||||
if not pledge_qty:
|
for security, qty in iteritems(current_pledges):
|
||||||
|
pledged_qty += qty
|
||||||
|
|
||||||
|
if not pledged_qty:
|
||||||
frappe.db.set_value('Loan', self.loan, 'status', 'Closed')
|
frappe.db.set_value('Loan', self.loan, 'status', 'Closed')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_pledged_security_qty(loan):
|
||||||
|
|
||||||
|
current_pledges = {}
|
||||||
|
|
||||||
|
unpledges = frappe._dict(frappe.db.sql("""
|
||||||
|
SELECT u.loan_security, sum(u.qty) as qty
|
||||||
|
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
|
||||||
|
WHERE up.loan = %s
|
||||||
|
AND u.parent = up.name
|
||||||
|
AND up.status = 'Approved'
|
||||||
|
GROUP BY u.loan_security
|
||||||
|
""", (loan)))
|
||||||
|
|
||||||
|
pledges = frappe._dict(frappe.db.sql("""
|
||||||
|
SELECT p.loan_security, sum(p.qty) as qty
|
||||||
|
FROM `tabLoan Security Pledge` lp, `tabPledge`p
|
||||||
|
WHERE lp.loan = %s
|
||||||
|
AND p.parent = lp.name
|
||||||
|
AND lp.status = 'Pledged'
|
||||||
|
GROUP BY p.loan_security
|
||||||
|
""", (loan)))
|
||||||
|
|
||||||
|
for security, qty in iteritems(pledges):
|
||||||
|
current_pledges.setdefault(security, qty)
|
||||||
|
current_pledges[security] -= unpledges.get(security, 0.0)
|
||||||
|
|
||||||
|
return current_pledges
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2019-09-21 13:22:19.793797",
|
"creation": "2019-09-21 13:22:19.793797",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"loan_security",
|
"loan_security",
|
||||||
"against_pledge",
|
|
||||||
"loan_security_type",
|
"loan_security_type",
|
||||||
"loan_security_code",
|
"loan_security_code",
|
||||||
"haircut",
|
"haircut",
|
||||||
@ -54,14 +54,6 @@
|
|||||||
"label": "Quantity",
|
"label": "Quantity",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "against_pledge",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Against Pledge",
|
|
||||||
"options": "Loan Security Pledge",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fetch_from": "loan_security.haircut",
|
"fetch_from": "loan_security.haircut",
|
||||||
"fieldname": "haircut",
|
"fieldname": "haircut",
|
||||||
@ -71,7 +63,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-10-02 12:48:18.588236",
|
"links": [],
|
||||||
|
"modified": "2020-05-06 10:50:18.448552",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Unpledge",
|
"name": "Unpledge",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user