Merge pull request #24370 from deepeshgarg007/loan_report_and_enhancements

feat: Loan report and enhancements
This commit is contained in:
Deepesh Garg 2021-01-18 12:26:45 +05:30 committed by GitHub
commit c3d82a889a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 575 additions and 31 deletions

View File

@ -23,7 +23,7 @@
{
"hidden": 0,
"label": "Reports",
"links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Interest Accrual\",\n \"is_query_report\": true,\n \"label\": \"Loan Interest Report\",\n \"name\": \"Loan Interest Report\",\n \"route\": \"#query-report/Loan Interest Report\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Exposure\",\n \"name\": \"Loan Security Exposure\",\n \"route\": \"#query-report/Loan Security Exposure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security\",\n \"is_query_report\": true,\n \"label\": \"Applicant-Wise Loan Security Exposure\",\n \"name\": \"Applicant-Wise Loan Security Exposure\",\n \"route\": \"#query-report/Applicant-Wise Loan Security Exposure\",\n \"type\": \"report\"\n }\n]"
}
],
"category": "Modules",
@ -38,7 +38,7 @@
"idx": 0,
"is_standard": 1,
"label": "Loan",
"modified": "2020-10-17 12:59:50.336085",
"modified": "2021-01-17 07:21:22.092184",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -202,7 +202,9 @@ def request_loan_closure(loan, posting_date=None):
# checking greater than 0 as there may be some minor precision error
if pending_amount < write_off_limit:
# update status as loan closure requested
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@ -336,13 +338,13 @@ def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, a
return unpledge_request
def validate_employee_currency_with_company_currency(applicant, company):
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
frappe.throw(_("Please select Company"))
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
if not applicant:
frappe.throw(_("Please select Applicant"))
if not company:
frappe.throw(_("Please select Company"))
employee_currency = get_employee_currency(applicant)
company_currency = erpnext.get_company_currency(company)
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))

View File

@ -321,7 +321,7 @@ class TestLoan(unittest.TestCase):
self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertTrue(amounts['pending_principal_amount'] < 0)
self.assertEqual(amounts['pending_principal_amount'], 0)
self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0)
@ -473,7 +473,7 @@ class TestLoan(unittest.TestCase):
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
self.assertTrue(amounts['pending_principal_amount'] < 0.0)
self.assertEqual(amounts['pending_principal_amount'], 0.0)
def test_partial_unaccrued_interest_payment(self):
pledge = [{

View File

@ -22,6 +22,7 @@
"paid_principal_amount",
"column_break_14",
"interest_amount",
"total_pending_interest_amount",
"paid_interest_amount",
"penalty_amount",
"section_break_15",
@ -172,13 +173,19 @@
"hidden": 1,
"label": "Last Accrual Date",
"read_only": 1
},
{
"fieldname": "total_pending_interest_amount",
"fieldtype": "Currency",
"label": "Total Pending Interest Amount",
"options": "Company:company:default_currency"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-07 05:49:25.448875",
"modified": "2021-01-10 00:15:21.544140",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",

View File

@ -100,6 +100,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure')
args = frappe._dict({
'loan': loan.name,
'applicant_type': loan.applicant_type,
@ -108,7 +110,8 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest,
'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
'total_pending_interest_amount': pending_amounts['interest_amount'],
'penalty_amount': pending_amounts['penalty_amount'],
'process_loan_interest': process_loan_interest,
'posting_date': posting_date,
'accrual_type': accrual_type
@ -202,6 +205,7 @@ def make_loan_interest_accrual_entry(args):
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision)
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest

View File

@ -37,10 +37,8 @@ class TestLoanInterestAccrual(unittest.TestCase):
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = '2019-10-01'
@ -50,11 +48,46 @@ class TestLoanInterestAccrual(unittest.TestCase):
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
def test_accumulated_amounts(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
loan.submit()
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0))
next_start_date = '2019-10-31'
next_end_date = '2019-11-29'
no_of_days = date_diff(next_end_date, next_start_date) + 1
process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date)
new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0)
loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name,
'process_loan_interest_accrual': process})
self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount)

View File

@ -377,7 +377,7 @@ def get_amounts(amounts, against_loan, posting_date):
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
amounts["unaccrued_interest"] = unaccrued_interest
amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
if final_due_date:
amounts["due_date"] = final_due_date

View File

@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"column_break_2",
"uom",
@ -79,10 +80,18 @@
"label": "Loan Security Type",
"options": "Loan Security Type",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-06-11 03:41:33.900340",
"modified": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Price",

View File

@ -144,17 +144,17 @@
},
{
"allow_on_submit": 1,
"description": "Pending amount that will be automatically ignored on loan closure request ",
"description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount ",
"label": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-26 07:13:55.029811",
"modified": "2021-01-17 06:51:26.082879",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"loan_security_code",
"uom",
@ -85,11 +86,18 @@
"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
}
],
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:15.424937",
"modified": "2021-01-17 07:41:12.452514",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",

View File

@ -30,7 +30,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2020-02-01 08:14:05.845161",
"modified": "2021-01-17 03:59:14.494557",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Security Shortfall",
@ -45,7 +45,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
@ -57,7 +59,9 @@
"read": 1,
"report": 1,
"role": "Loan Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
}
],

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"qty",
"loan_security_price",
"amount",
@ -56,12 +57,19 @@
"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
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:37.542344",
"modified": "2021-01-17 07:29:01.671722",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"loan_security_code",
"haircut",
@ -61,12 +62,19 @@
"fieldtype": "Percent",
"label": "Haircut",
"read_only": 1
},
{
"fetch_from": "loan_security.loan_security_name",
"fieldname": "loan_security_name",
"fieldtype": "Data",
"label": "Loan Security Name",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-05 10:07:28.106961",
"modified": "2021-01-17 07:36:20.212342",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",

View File

@ -0,0 +1,16 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Applicant-Wise Loan Security Exposure"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
}
]
};

View File

@ -0,0 +1,29 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-01-15 23:48:38.913514",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-01-15 23:48:38.913514",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Applicant-Wise Loan Security Exposure",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Loan Security",
"report_name": "Applicant-Wise Loan Security Exposure",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Loan Manager"
}
]
}

View File

@ -0,0 +1,123 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import erpnext
from frappe import _
from frappe.utils import get_datetime, flt
from six import iteritems
def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_columns(filters):
columns = [
{"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
{"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
{"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
{"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
{"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
{"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
{"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
]
return columns
def get_data(filters):
data = []
loan_security_details = get_loan_security_details(filters)
pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
loan_security_details)
currency = erpnext.get_company_currency(filters.get('company'))
for key, qty in iteritems(pledge_values):
row = {}
current_value = flt(qty * loan_security_details.get(key[1])['latest_price'])
row.update(loan_security_details.get(key[1]))
row.update({
'applicant_type': applicant_type_map.get(key[0]),
'applicant_name': key[0],
'total_qty': qty,
'current_value': current_value,
'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2),
'currency': currency
})
data.append(row)
return data
def get_loan_security_details(filters):
security_detail_map = {}
loan_security_price_map = frappe._dict(frappe.db.sql("""
SELECT loan_security, loan_security_price
FROM `tabLoan Security Price` t1
WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
WHERE t1.loan_security = t2.loan_security)
""", as_list=1))
loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
'disabled'])
for security in loan_security_details:
security.update({'latest_price': flt(loan_security_price_map.get(security.loan_security))})
security_detail_map.setdefault(security.loan_security, security)
return security_detail_map
def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
current_pledges = {}
total_value_map = {}
applicant_type_map = {}
applicant_wise_unpledges = {}
conditions = ""
if filters.get('company'):
conditions = "AND company = %(company)s"
unpledges = frappe.db.sql("""
SELECT up.applicant, u.loan_security, sum(u.qty) as qty
FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
WHERE u.parent = up.name
AND up.status = 'Approved'
{conditions}
GROUP BY up.applicant, u.loan_security
""".format(conditions=conditions), filters, as_dict=1)
for unpledge in unpledges:
applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty)
pledges = frappe.db.sql("""
SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lp, `tabPledge`p
WHERE p.parent = lp.name
AND lp.status = 'Pledged'
{conditions}
GROUP BY lp.applicant, p.loan_security
""".format(conditions=conditions), filters, as_dict=1)
for security in pledges:
current_pledges.setdefault((security.applicant, security.loan_security), security.qty)
total_value_map.setdefault(security.applicant, 0.0)
applicant_type_map.setdefault(security.applicant, security.applicant_type)
current_pledges[(security.applicant, security.loan_security)] -= \
applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
* loan_security_details.get(security.loan_security)['latest_price']
return current_pledges, total_value_map, applicant_type_map

View File

@ -0,0 +1,16 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Loan Interest Report"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
}
]
};

View File

@ -0,0 +1,29 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2021-01-10 02:03:26.742693",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-01-10 02:03:26.742693",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Report",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Loan Interest Accrual",
"report_name": "Loan Interest Report",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Loan Manager"
}
]
}

View File

@ -0,0 +1,124 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import erpnext
from frappe import _
from frappe.utils import flt, getdate, add_days
def execute(filters=None):
columns = get_columns(filters)
data = get_active_loan_details(filters)
return columns, data
def get_columns(filters):
columns = [
{"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
{"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
{"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
{"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100},
{"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
{"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
]
return columns
def get_active_loan_details(filters):
filter_obj = {"status": ("!=", "Closed")}
if filters.get('company'):
filter_obj.update({'company': filters.get('company')})
loan_details = frappe.get_all("Loan",
fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type",
"disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid",
"total_interest_payable", "written_off_amount", "status"],
filters=filter_obj)
loan_list = [d.loan for d in loan_details]
sanctioned_amount_map = get_sanctioned_amount_map()
penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list)
accrual_map = get_interest_accruals(loan_list)
currency = erpnext.get_company_currency(filters.get('company'))
for loan in loan_details:
loan.update({
"sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
"principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \
- flt(loan.total_interest_payable) - flt(loan.written_off_amount),
"total_repayment": flt(payments.get(loan.loan)),
"accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
"interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
"penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
"penalty_interest": penal_interest_rate_map.get(loan.loan_type),
"undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
"currency": currency
})
loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ loan['penalty']
return loan_details
def get_sanctioned_amount_map():
return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"],
as_list=1))
def get_payments(loans):
return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"],
filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1))
def get_interest_accruals(loans):
accrual_map = {}
interest_accruals = frappe.get_all("Loan Interest Accrual",
fields=["loan", "interest_amount", "posting_date", "penalty_amount",
"paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc")
for entry in interest_accruals:
accrual_map.setdefault(entry.loan, {
"accrued_interest": 0.0,
"undue_interest": 0.0,
"interest_outstanding": 0.0,
"last_accrual_date": '',
"due_date": ''
})
if entry.accrual_type == 'Regular':
if not accrual_map[entry.loan]['due_date']:
accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1)
if not accrual_map[entry.loan]['last_accrual_date']:
accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date
due_date = accrual_map[entry.loan]['due_date']
last_accrual_date = accrual_map[entry.loan]['last_accrual_date']
if due_date and getdate(entry.posting_date) < getdate(due_date):
accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount
else:
accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount
accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
if last_accrual_date and getdate(entry.posting_date) == last_accrual_date:
accrual_map[entry.loan]["penalty"] = entry.penalty_amount
return accrual_map
def get_penal_interest_rate_map():
return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))

View File

@ -0,0 +1,16 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Loan Security Exposure"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
}
]
};

View File

@ -0,0 +1,29 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-01-16 08:08:01.694583",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-01-16 08:08:01.694583",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Exposure",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Loan Security",
"report_name": "Loan Security Exposure",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Loan Manager"
}
]
}

View File

@ -0,0 +1,79 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import erpnext
from frappe import _
from frappe.utils import flt
from six import iteritems
from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
import get_loan_security_details, get_applicant_wise_total_loan_security_qty
def execute(filters=None):
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_columns(filters):
columns = [
{"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
{"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
{"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
{"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
{"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
{"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
{"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
{"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
{"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
{"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
{"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
]
return columns
def get_data(filters):
data = []
loan_security_details = get_loan_security_details(filters)
current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
currency = erpnext.get_company_currency(filters.get('company'))
for security, value in iteritems(current_pledges):
row = {}
current_value = flt(value['qty'] * loan_security_details.get(security)['latest_price'])
row.update(loan_security_details.get(security))
row.update({
'total_qty': value['qty'],
'current_value': current_value,
'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
'pledged_applicant_count': value['applicant_count'],
'currency': currency
})
data.append(row)
return data
def get_company_wise_loan_security_details(filters, loan_security_details):
pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
loan_security_details)
total_portfolio_value = 0
security_wise_map = {}
for key, qty in iteritems(pledge_values):
security_wise_map.setdefault(key[1], {
'qty': 0.0,
'applicant_count': 0.0
})
security_wise_map[key[1]]['qty'] += qty
if qty:
security_wise_map[key[1]]['applicant_count'] += 1
total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price'])
return security_wise_map, total_portfolio_value