refactor!: removing loan management module (#35522)

* chore: resolving conflicts

* refactor: bank_clearance and add hook for get_payment_entries_for_bank_clearance

* refactor: bank_reconciliation_tool and add hook for get_matching_vouchers_for_bank_reconciliation

* fix: remove sales invoice from bank_reconciliation_doctypes and use hook for voucher clearance

* refactor: remove loan tests from test_bank_transaction

* refactor: bank_clearance_summary and add hook for get_entries_for_bank_clearance_summary

* refactor: removed test_bank_reconciliation_statement

* refactor: bank_reconciliation_statement and add hook for get_amounts_not_reflected_in_system_for_bank_reconciliation_statement

* refactor: add missing hook and patches for module removal and deprecation warning

* refactor: remove loan management translations

* chore: add erpnext tests dependent on lending
This commit is contained in:
Anand Baburajan 2023-06-30 11:02:49 +05:30 committed by GitHub
parent 9f1cf0bbb0
commit 988d755906
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
250 changed files with 435 additions and 22651 deletions

View File

@ -5,7 +5,6 @@
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007

View File

@ -5,7 +5,6 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
import erpnext
@ -22,167 +21,24 @@ class BankClearance(Document):
if not self.account:
frappe.throw(_("Account is mandatory to get payment entries"))
condition = ""
if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
entries = []
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""".format(
condition=condition
),
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
if self.bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
posting_date ASC, name DESC
""".format(
condition=condition
),
{
"account": self.account,
"from": self.from_date,
"to": self.to_date,
"bank_account": self.bank_account,
},
as_dict=1,
)
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursed_amount.as_("credit"),
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
if not self.include_reconciled_entries:
query = query.where(loan_disbursement.clearance_date.isnull())
loan_disbursements = query.run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.amount_paid.as_("debit"),
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if not self.include_reconciled_entries:
query = query.where(loan_repayment.clearance_date.isnull())
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True)
pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
# get entries from all the apps
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"):
entries += (
frappe.get_attr(method_name)(
self.from_date,
self.to_date,
self.account,
self.bank_account,
self.include_reconciled_entries,
self.include_pos_transactions,
)
or []
)
entries = sorted(
list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(pos_purchase_invoices)
+ list(loan_disbursements)
+ list(loan_repayments),
entries,
key=lambda k: getdate(k["posting_date"]),
)
@ -235,3 +91,111 @@ class BankClearance(Document):
msgprint(_("Clearance Date updated"))
else:
msgprint(_("Clearance Date not mentioned"))
def get_payment_entries_for_bank_clearance(
from_date, to_date, account, bank_account, include_reconciled_entries, include_pos_transactions
):
entries = []
condition = ""
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""".format(
condition=condition
),
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
if bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
posting_date ASC, name DESC
""".format(
condition=condition
),
{
"account": account,
"from": from_date,
"to": to_date,
"bank_account": bank_account,
},
as_dict=1,
)
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
entries = (
list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(pos_purchase_invoices)
)
return entries

View File

@ -8,26 +8,75 @@ from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed
class TestBankClearance(unittest.TestCase):
@classmethod
def setUpClass(cls):
clear_payment_entries()
clear_loan_transactions()
make_bank_account()
create_loan_accounts()
create_loan_masters()
add_transactions()
# Basic test case to test if bank clearance tool doesn't break
# Detailed test can be added later
@if_lending_app_not_installed
def test_bank_clearance(self):
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
bank_clearance.to_date = getdate()
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 1)
@if_lending_app_installed
def test_bank_clearance_with_loan(self):
from lending.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(
loan.name, "_Test Customer", getdate(), loan.loan_amount
)
repayment_entry.save()
repayment_entry.submit()
create_loan_accounts()
create_loan_masters()
make_loan()
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
@ -36,6 +85,19 @@ class TestBankClearance(unittest.TestCase):
self.assertEqual(len(bank_clearance.payment_entries), 3)
def clear_payment_entries():
frappe.db.delete("Payment Entry")
@if_lending_app_installed
def clear_loan_transactions():
for dt in [
"Loan Disbursement",
"Loan Repayment",
]:
frappe.db.delete(dt)
def make_bank_account():
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
frappe.get_doc(
@ -49,42 +111,8 @@ def make_bank_account():
).insert()
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def add_transactions():
make_payment_entry()
make_loan()
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
repayment_entry.save()
repayment_entry.submit()
def make_payment_entry():

View File

@ -7,7 +7,6 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
from erpnext import get_default_cost_center
@ -419,19 +418,7 @@ def check_matching(
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
# combine all types of vouchers
subquery = get_queries(
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
)
filters = {
"amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
@ -443,21 +430,29 @@ def check_matching(
matching_vouchers = []
matching_vouchers.extend(
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
)
for query in subquery:
# get matching vouchers from all the apps
for method_name in frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation"):
matching_vouchers.extend(
frappe.db.sql(
query,
frappe.get_attr(method_name)(
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
filters,
)
or []
)
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
def get_queries(
def get_matching_vouchers_for_bank_reconciliation(
bank_account,
company,
transaction,
@ -468,6 +463,7 @@ def get_queries(
from_reference_date,
to_reference_date,
exact_match,
filters,
):
# get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
@ -492,7 +488,17 @@ def get_queries(
or []
)
return queries
vouchers = []
for query in queries:
vouchers.extend(
frappe.db.sql(
query,
filters,
)
)
return vouchers
def get_matching_queries(
@ -550,18 +556,6 @@ def get_matching_queries(
return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
vouchers = []
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
return vouchers
def get_bt_matching_query(exact_match, transaction):
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
@ -595,85 +589,6 @@ def get_bt_matching_query(exact_match, transaction):
"""
def get_ld_matching_query(bank_account, exact_match, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get(
"party_type"
) and loan_disbursement.applicant == filters.get("party")
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
query = (
frappe.qb.from_(loan_disbursement)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date,
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account == bank_account)
)
if exact_match:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
query.where(loan_disbursement.disbursed_amount > 0.0)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, exact_match, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get(
"party_type"
) and loan_repayment.applicant == filters.get("party")
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
query = (
frappe.qb.from_(loan_repayment)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if exact_match:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
query.where(loan_repayment.amount_paid > 0.0)
vouchers = query.run()
return vouchers
def get_pe_matching_query(
exact_match,
account_from_to,

View File

@ -343,14 +343,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
def set_voucher_clearance(doctype, docname, clearance_date, self):
if doctype in [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
if doctype in get_doctypes_for_bank_reconciliation():
if (
doctype == "Payment Entry"
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"

View File

@ -16,6 +16,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_paymen
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import if_lending_app_installed
test_dependencies = ["Item", "Cost Center"]
@ -23,14 +24,13 @@ test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(FrappeTestCase):
def setUp(self):
for dt in [
"Loan Repayment",
"Bank Transaction",
"Payment Entry",
"Payment Entry Reference",
"POS Profile",
]:
frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile()
add_transactions()
add_vouchers()
@ -160,8 +160,9 @@ class TestBankTransaction(FrappeTestCase):
is not None
)
@if_lending_app_installed
def test_matching_loan_repayment(self):
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
from lending.loan_management.doctype.loan.test_loan import create_loan_accounts
create_loan_accounts()
bank_account = frappe.get_doc(
@ -190,6 +191,11 @@ class TestBankTransaction(FrappeTestCase):
self.assertEqual(linked_payments[0][2], repayment_entry.name)
@if_lending_app_installed
def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
frappe.get_doc(
@ -400,16 +406,18 @@ def add_vouchers():
si.submit()
@if_lending_app_installed
def create_loan_and_repayment():
from erpnext.loan_management.doctype.loan.test_loan import (
from lending.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
from lending.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_term_loans,
)
from erpnext.setup.doctype.employee.test_employee import make_employee
create_loan_type(

View File

@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import getdate, nowdate
@ -61,7 +60,28 @@ def get_conditions(filters):
def get_entries(filters):
entries = []
# get entries from all the apps
for method_name in frappe.get_hooks("get_entries_for_bank_clearance_summary"):
entries += (
frappe.get_attr(method_name)(
filters,
)
or []
)
return sorted(
entries,
key=lambda k: k[2] or getdate(nowdate()),
)
def get_entries_for_bank_clearance_summary(filters):
entries = []
conditions = get_conditions(filters)
journal_entries = frappe.db.sql(
"""SELECT
"Journal Entry", jv.name, jv.posting_date, jv.cheque_no,
@ -92,65 +112,6 @@ def get_entries(filters):
as_list=1,
)
# Loan Disbursement
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
entries = journal_entries + payment_entries
query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document_type"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.reference_number.as_("cheque_no"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.applicant.as_("against"),
-loan_disbursement.disbursed_amount.as_("amount"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= filters["from_date"])
.where(loan_disbursement.disbursement_date <= filters["to_date"])
.where(loan_disbursement.disbursement_account == filters["account"])
.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
loan_disbursements = query.run(as_list=1)
# Loan Repayment
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document_type"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.posting_date.as_("posting_date"),
loan_repayment.reference_number.as_("cheque_no"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against"),
loan_repayment.amount_paid.as_("amount"),
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= filters["from_date"])
.where(loan_repayment.posting_date <= filters["to_date"])
.where(loan_repayment.payment_account == filters["account"])
.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
.orderby(loan_repayment.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_repayment.posting_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_repayment.posting_date <= filters["to_date"])
loan_repayments = query.run(as_list=1)
return sorted(
journal_entries + payment_entries + loan_disbursements + loan_repayments,
key=lambda k: k[2] or getdate(nowdate()),
)
return entries

View File

@ -4,10 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from pypika import CustomFunction
from erpnext.accounts.utils import get_balance_on
@ -113,20 +110,27 @@ def get_columns():
def get_entries(filters):
entries = []
for method_name in frappe.get_hooks("get_entries_for_bank_reconciliation_statement"):
entries += frappe.get_attr(method_name)(filters) or []
return sorted(
entries,
key=lambda k: getdate(k["posting_date"]),
)
def get_entries_for_bank_reconciliation_statement(filters):
journal_entries = get_journal_entries(filters)
payment_entries = get_payment_entries(filters)
loan_entries = get_loan_entries(filters)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
return sorted(
list(payment_entries) + list(journal_entries + list(pos_entries) + list(loan_entries)),
key=lambda k: getdate(k["posting_date"]),
)
return list(journal_entries) + list(payment_entries) + list(pos_entries)
def get_journal_entries(filters):
@ -188,47 +192,19 @@ def get_pos_entries(filters):
)
def get_loan_entries(filters):
loan_docs = []
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction("IFNULL", ["value", "default"])
if doctype == "Loan Disbursement":
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
query = (
frappe.qb.from_(loan_doc)
.select(
ConstantColumn(doctype).as_("payment_document"),
(loan_doc.name).as_("payment_entry"),
(loan_doc.reference_number).as_("reference_no"),
(loan_doc.reference_date).as_("ref_date"),
amount_field,
posting_date,
)
.where(loan_doc.docstatus == 1)
.where(account == filters.get("account"))
.where(posting_date <= getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
)
if doctype == "Loan Repayment" and frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_doc.repay_from_salary == 0))
entries = query.run(as_dict=1)
loan_docs.extend(entries)
return loan_docs
def get_amounts_not_reflected_in_system(filters):
amount = 0.0
# get amounts from all the apps
for method_name in frappe.get_hooks(
"get_amounts_not_reflected_in_system_for_bank_reconciliation_statement"
):
amount += frappe.get_attr(method_name)(filters) or 0.0
return amount
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
je_amount = frappe.db.sql(
"""
select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
@ -252,42 +228,7 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
loan_amount = get_loan_amount(filters)
return je_amount + pe_amount + loan_amount
def get_loan_amount(filters):
total_amount = 0
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction("IFNULL", ["value", "default"])
if doctype == "Loan Disbursement":
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
query = (
frappe.qb.from_(loan_doc)
.select(amount_field)
.where(loan_doc.docstatus == 1)
.where(account == filters.get("account"))
.where(posting_date > getdate(filters.get("report_date")))
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))
)
if doctype == "Loan Repayment" and frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_doc.repay_from_salary == 0))
amount = query.run()[0][0]
total_amount += flt(amount)
return total_amount
return je_amount + pe_amount
def get_balance_row(label, amount, account_currency):

View File

@ -4,28 +4,32 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
create_loan_and_repayment,
)
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
execute,
)
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
from erpnext.tests.utils import if_lending_app_installed
class TestBankReconciliationStatement(FrappeTestCase):
def setUp(self):
for dt in [
"Loan Repayment",
"Loan Disbursement",
"Journal Entry",
"Journal Entry Account",
"Payment Entry",
]:
frappe.db.delete(dt)
clear_loan_transactions()
@if_lending_app_installed
def test_loan_entries_in_bank_reco_statement(self):
from lending.loan_management.doctype.loan.test_loan import create_loan_accounts
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import (
create_loan_and_repayment,
)
create_loan_accounts()
repayment_entry = create_loan_and_repayment()
filters = frappe._dict(
@ -38,3 +42,12 @@ class TestBankReconciliationStatement(FrappeTestCase):
result = execute(filters)
self.assertEqual(result[1][0].payment_entry, repayment_entry.name)
@if_lending_app_installed
def clear_loan_transactions():
for dt in [
"Loan Disbursement",
"Loan Repayment",
]:
frappe.db.delete(dt)

View File

@ -419,13 +419,10 @@ scheduler_events = {
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans",
],
}
@ -471,9 +468,6 @@ bank_reconciliation_doctypes = [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Sales Invoice",
"Loan Repayment",
"Loan Disbursement",
]
accounting_dimension_doctypes = [
@ -521,11 +515,22 @@ accounting_dimension_doctypes = [
"Account Closing Balance",
]
# get matching queries for Bank Reconciliation
get_matching_queries = (
"erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_queries"
)
get_matching_vouchers_for_bank_reconciliation = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_matching_vouchers_for_bank_reconciliation"
get_amounts_not_reflected_in_system_for_bank_reconciliation_statement = "erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement.get_amounts_not_reflected_in_system_for_bank_reconciliation_statement"
get_payment_entries_for_bank_clearance = (
"erpnext.accounts.doctype.bank_clearance.bank_clearance.get_payment_entries_for_bank_clearance"
)
get_entries_for_bank_clearance_summary = "erpnext.accounts.report.bank_clearance_summary.bank_clearance_summary.get_entries_for_bank_clearance_summary"
get_entries_for_bank_reconciliation_statement = "erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement.get_entries_for_bank_reconciliation_statement"
regional_overrides = {
"France": {
"erpnext.tests.test_regional.test_method": "erpnext.regional.france.utils.test_method"
@ -593,7 +598,6 @@ global_search_doctypes = {
{"doctype": "Branch", "index": 35},
{"doctype": "Department", "index": 36},
{"doctype": "Designation", "index": 38},
{"doctype": "Loan", "index": 44},
{"doctype": "Maintenance Schedule", "index": 45},
{"doctype": "Maintenance Visit", "index": 46},
{"doctype": "Warranty Claim", "index": 47},

View File

@ -1,29 +0,0 @@
{
"based_on": "disbursement_date",
"chart_name": "Loan Disbursements",
"chart_type": "Sum",
"creation": "2021-02-06 18:40:36.148470",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "Loan Disbursement",
"dynamic_filters_json": "[]",
"filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_standard": 1,
"modified": "2021-02-06 18:40:49.308663",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursements",
"number_of_groups": 0,
"owner": "Administrator",
"source": "",
"time_interval": "Daily",
"timeseries": 1,
"timespan": "Last Month",
"type": "Line",
"use_report_chart": 0,
"value_based_on": "disbursed_amount",
"y_axis": []
}

View File

@ -1,31 +0,0 @@
{
"based_on": "posting_date",
"chart_name": "Loan Interest Accrual",
"chart_type": "Sum",
"color": "#39E4A5",
"creation": "2021-02-18 20:07:04.843876",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "Loan Interest Accrual",
"dynamic_filters_json": "[]",
"filters_json": "[[\"Loan Interest Accrual\",\"docstatus\",\"=\",\"1\",false]]",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_standard": 1,
"last_synced_on": "2021-02-21 21:01:26.022634",
"modified": "2021-02-21 21:01:44.930712",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
"number_of_groups": 0,
"owner": "Administrator",
"source": "",
"time_interval": "Monthly",
"timeseries": 1,
"timespan": "Last Year",
"type": "Line",
"use_report_chart": 0,
"value_based_on": "interest_amount",
"y_axis": []
}

View File

@ -1,31 +0,0 @@
{
"based_on": "creation",
"chart_name": "New Loans",
"chart_type": "Count",
"color": "#449CF0",
"creation": "2021-02-06 16:59:27.509170",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "Loan",
"dynamic_filters_json": "[]",
"filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false]]",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_standard": 1,
"last_synced_on": "2021-02-21 20:55:33.515025",
"modified": "2021-02-21 21:00:33.900821",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "New Loans",
"number_of_groups": 0,
"owner": "Administrator",
"source": "",
"time_interval": "Daily",
"timeseries": 1,
"timespan": "Last Month",
"type": "Bar",
"use_report_chart": 0,
"value_based_on": "",
"y_axis": []
}

View File

@ -1,31 +0,0 @@
{
"based_on": "",
"chart_name": "Top 10 Pledged Loan Securities",
"chart_type": "Custom",
"color": "#EC864B",
"creation": "2021-02-06 22:02:46.284479",
"docstatus": 0,
"doctype": "Dashboard Chart",
"document_type": "",
"dynamic_filters_json": "[]",
"filters_json": "[]",
"group_by_type": "Count",
"idx": 0,
"is_public": 0,
"is_standard": 1,
"last_synced_on": "2021-02-21 21:00:57.043034",
"modified": "2021-02-21 21:01:10.048623",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Top 10 Pledged Loan Securities",
"number_of_groups": 0,
"owner": "Administrator",
"source": "Top 10 Pledged Loan Securities",
"time_interval": "Yearly",
"timeseries": 0,
"timespan": "Last Year",
"type": "Bar",
"use_report_chart": 0,
"value_based_on": "",
"y_axis": []
}

View File

@ -1,14 +0,0 @@
frappe.provide('frappe.dashboards.chart_sources');
frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = {
method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data",
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company")
}
]
};

View File

@ -1,13 +0,0 @@
{
"creation": "2021-02-06 22:01:01.332628",
"docstatus": 0,
"doctype": "Dashboard Chart Source",
"idx": 0,
"modified": "2021-02-06 22:01:01.332628",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Top 10 Pledged Loan Securities",
"owner": "Administrator",
"source_name": "Top 10 Pledged Loan Securities ",
"timeseries": 0
}

View File

@ -1,99 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils.dashboard import cache_source
from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure import (
get_loan_security_details,
)
@frappe.whitelist()
@cache_source
def get_data(
chart_name=None,
chart=None,
no_cache=None,
filters=None,
from_date=None,
to_date=None,
timespan=None,
time_interval=None,
heatmap_year=None,
):
if chart_name:
chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
filters = {}
current_pledges = {}
if filters:
filters = frappe.parse_json(filters)[0]
conditions = ""
labels = []
values = []
if filters.get("company"):
conditions = "AND company = %(company)s"
loan_security_details = get_loan_security_details()
unpledges = frappe._dict(
frappe.db.sql(
"""
SELECT 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 u.loan_security
""".format(
conditions=conditions
),
filters,
as_list=1,
)
)
pledges = frappe._dict(
frappe.db.sql(
"""
SELECT 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 p.loan_security
""".format(
conditions=conditions
),
filters,
as_list=1,
)
)
for security, qty in pledges.items():
current_pledges.setdefault(security, qty)
current_pledges[security] -= unpledges.get(security, 0.0)
sorted_pledges = dict(sorted(current_pledges.items(), key=lambda item: item[1], reverse=True))
count = 0
for security, qty in sorted_pledges.items():
values.append(qty * loan_security_details.get(security, {}).get("latest_price", 0))
labels.append(security)
count += 1
## Just need top 10 securities
if count == 10:
break
return {
"labels": labels,
"datasets": [{"name": "Top 10 Securities", "chartType": "bar", "values": values}],
}

View File

@ -1,281 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan', {
setup: function(frm) {
frm.make_methods = {
'Loan Disbursement': function() { frm.trigger('make_loan_disbursement') },
'Loan Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') },
'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') }
}
},
onload: function (frm) {
// Ignore loan security pledge on cancel of loan
frm.ignore_doctypes_on_cancel_all = ["Loan Security Pledge"];
frm.set_query("loan_application", function () {
return {
"filters": {
"applicant": frm.doc.applicant,
"docstatus": 1,
"status": "Approved"
}
};
});
frm.set_query("loan_type", function () {
return {
"filters": {
"docstatus": 1,
"company": frm.doc.company
}
};
});
$.each(["penalty_income_account", "interest_income_account"], function(i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
});
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
})
},
refresh: function (frm) {
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
}
if (frm.doc.docstatus == 1) {
if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) {
frm.add_custom_button(__('Request Loan Closure'), function() {
frm.trigger("request_loan_closure");
},__('Status'));
frm.add_custom_button(__('Loan Repayment'), function() {
frm.trigger("make_repayment_entry");
},__('Create'));
}
if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Disbursement'), function() {
frm.trigger("make_loan_disbursement");
},__('Create'));
}
if (frm.doc.status == "Loan Closure Requested") {
frm.add_custom_button(__('Loan Security Unpledge'), function() {
frm.trigger("create_loan_security_unpledge");
},__('Create'));
}
if (["Loan Closure Requested", "Disbursed", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Write Off'), function() {
frm.trigger("make_loan_write_off_entry");
},__('Create'));
frm.add_custom_button(__('Loan Refund'), function() {
frm.trigger("make_loan_refund");
},__('Create'));
}
if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
frm.add_custom_button(__('Close Loan'), function() {
frm.trigger("close_unsecured_term_loan");
},__('Status'));
}
}
frm.trigger("toggle_fields");
},
repayment_schedule_type: function(frm) {
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
} else {
frm.set_df_property("repayment_start_date", "label", "Repayment Start Date");
}
},
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", frm.doc.is_term_loan);
},
make_loan_disbursement: function (frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"pending_amount": frm.doc.loan_amount - frm.doc.disbursed_amount > 0 ?
frm.doc.loan_amount - frm.doc.disbursed_amount : 0,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_disbursement",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_repayment_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"loan_type": frm.doc.loan_type,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_repayment_entry",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_loan_write_off_entry: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name,
"company": frm.doc.company,
"as_dict": 1
},
method: "erpnext.loan_management.doctype.loan.loan.make_loan_write_off",
callback: function (r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
make_loan_refund: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.make_refund_jv",
callback: function (r) {
if (r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
})
},
close_unsecured_term_loan: function(frm) {
frappe.call({
args: {
"loan": frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
callback: function () {
frm.refresh();
}
})
},
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {
frappe.call({
args: {
'loan': frm.doc.name
},
method: "erpnext.loan_management.doctype.loan.loan.request_loan_closure",
callback: function() {
frm.reload_doc();
}
});
}
);
},
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",
args : {
"loan": frm.doc.name,
"as_dict": 1
},
callback: function(r) {
if (r.message)
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
})
},
loan_application: function (frm) {
if(frm.doc.loan_application){
return frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.get_loan_application",
args: {
"loan_application": frm.doc.loan_application
},
callback: function (r) {
if (!r.exc && r.message) {
let loan_fields = ["loan_type", "loan_amount", "repayment_method",
"monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"]
loan_fields.forEach(field => {
frm.set_value(field, r.message[field]);
});
if (frm.doc.is_secured_loan) {
$.each(r.message.proposed_pledges, function(i, d) {
let row = frm.add_child("securities");
row.loan_security = d.loan_security;
row.qty = d.qty;
row.loan_security_price = d.loan_security_price;
row.amount = d.amount;
row.haircut = d.haircut;
});
frm.refresh_fields("securities");
}
}
}
});
}
},
repayment_method: function (frm) {
frm.trigger("toggle_fields")
},
toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});

View File

@ -1,452 +0,0 @@
{
"actions": [],
"allow_import": 1,
"autoname": "ACC-LOAN-.YYYY.-.#####",
"creation": "2022-01-25 10:30:02.294967",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicant_type",
"applicant",
"applicant_name",
"loan_application",
"column_break_3",
"company",
"posting_date",
"status",
"section_break_8",
"loan_type",
"repayment_schedule_type",
"loan_amount",
"rate_of_interest",
"is_secured_loan",
"disbursement_date",
"closure_date",
"disbursed_amount",
"column_break_11",
"maximum_loan_amount",
"repayment_method",
"repayment_periods",
"monthly_repayment_amount",
"repayment_start_date",
"is_term_loan",
"accounting_dimensions_section",
"cost_center",
"account_info",
"mode_of_payment",
"disbursement_account",
"payment_account",
"column_break_9",
"loan_account",
"interest_income_account",
"penalty_income_account",
"section_break_15",
"repayment_schedule",
"section_break_17",
"total_payment",
"total_principal_paid",
"written_off_amount",
"refund_amount",
"debit_adjustment_amount",
"credit_adjustment_amount",
"is_npa",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
"amended_from"
],
"fields": [
{
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
},
{
"fieldname": "applicant_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Applicant Name",
"read_only": 1
},
{
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
"no_copy": 1,
"options": "Loan Application"
},
{
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"no_copy": 1,
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
"reqd": 1
},
{
"default": "Sanctioned",
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Sanctioned\nPartially Disbursed\nDisbursed\nLoan Closure Requested\nClosed",
"read_only": 1
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"label": "Loan Details"
},
{
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
"non_negative": 1,
"options": "Company:company:default_currency"
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate of Interest (%) / Year",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "eval:doc.status==\"Disbursed\"",
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date",
"no_copy": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_start_date",
"fieldtype": "Date",
"label": "Repayment Start Date",
"mandatory_depends_on": "is_term_loan"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"depends_on": "is_term_loan",
"fetch_from": "loan_application.repayment_amount",
"fetch_if_empty": 1,
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
},
{
"collapsible": 1,
"fieldname": "account_info",
"fieldtype": "Section Break",
"label": "Account Info"
},
{
"fetch_from": "loan_type.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.payment_account",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan_type.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "loan_type.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Repayment Schedule"
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_term_loan == 1",
"fieldname": "repayment_schedule",
"fieldtype": "Table",
"label": "Repayment Schedule",
"no_copy": 1,
"options": "Repayment Schedule",
"read_only": 1
},
{
"fieldname": "section_break_17",
"fieldtype": "Section Break",
"label": "Totals"
},
{
"default": "0",
"fieldname": "total_payment",
"fieldtype": "Currency",
"label": "Total Payable Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_19",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "is_term_loan",
"fieldname": "total_interest_payable",
"fieldtype": "Currency",
"label": "Total Interest Payable",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "total_amount_paid",
"fieldtype": "Currency",
"label": "Total Amount Paid",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"fetch_from": "loan_type.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "total_principal_paid",
"fieldtype": "Currency",
"label": "Total Principal Paid",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"depends_on": "eval:doc.is_secured_loan",
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "written_off_amount",
"fieldtype": "Currency",
"label": "Written Off Amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "closure_date",
"fieldtype": "Date",
"label": "Closure Date",
"read_only": 1
},
{
"fetch_from": "loan_type.disbursement_account",
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund amount",
"no_copy": 1,
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "credit_adjustment_amount",
"fieldtype": "Currency",
"label": "Credit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "debit_adjustment_amount",
"fieldtype": "Currency",
"label": "Debit Adjustment Amount",
"options": "Company:company:default_currency"
},
{
"default": "0",
"description": "Mark Loan as a Nonperforming asset",
"fieldname": "is_npa",
"fieldtype": "Check",
"label": "Is NPA"
},
{
"depends_on": "is_term_loan",
"fetch_from": "loan_type.repayment_schedule_type",
"fieldname": "repayment_schedule_type",
"fieldtype": "Data",
"label": "Repayment Schedule Type",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-09-30 10:36:47.902903",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"read": 1,
"role": "Employee"
}
],
"search_fields": "posting_date",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,611 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import math
import frappe
from frappe import _
from frappe.utils import (
add_days,
add_months,
date_diff,
flt,
get_last_day,
getdate,
now_datetime,
nowdate,
)
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
get_pledged_security_qty,
)
class Loan(AccountsController):
def validate(self):
self.set_loan_amount()
self.validate_loan_amount()
self.set_missing_fields()
self.validate_cost_center()
self.validate_accounts()
self.check_sanctioned_amount_limit()
if self.is_term_loan:
validate_repayment_method(
self.repayment_method,
self.loan_amount,
self.monthly_repayment_amount,
self.repayment_periods,
self.is_term_loan,
)
self.make_repayment_schedule()
self.set_repayment_period()
self.calculate_totals()
def validate_accounts(self):
for fieldname in [
"payment_account",
"loan_account",
"interest_income_account",
"penalty_income_account",
]:
company = frappe.get_value("Account", self.get(fieldname), "company")
if company != self.company:
frappe.throw(
_("Account {0} does not belongs to company {1}").format(
frappe.bold(self.get(fieldname)), frappe.bold(self.company)
)
)
def validate_cost_center(self):
if not self.cost_center and self.rate_of_interest != 0.0:
self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
if not self.cost_center:
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
def on_submit(self):
self.link_loan_security_pledge()
# Interest accrual for backdated term loans
self.accrue_loan_interest()
def on_cancel(self):
self.unlink_loan_security_pledge()
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_fields(self):
if not self.company:
self.company = erpnext.get_default_company()
if not self.posting_date:
self.posting_date = nowdate()
if self.loan_type and not self.rate_of_interest:
self.rate_of_interest = frappe.db.get_value("Loan Type", self.loan_type, "rate_of_interest")
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(
self.loan_amount, self.rate_of_interest, self.repayment_periods
)
def check_sanctioned_amount_limit(self):
sanctioned_amount_limit = get_sanctioned_amount_limit(
self.applicant_type, self.applicant, self.company
)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(
sanctioned_amount_limit
):
frappe.throw(
_("Sanctioned Amount limit crossed for {0} {1}").format(
self.applicant_type, frappe.bold(self.applicant)
)
)
def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
schedule_type_details = frappe.db.get_value(
"Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1
)
self.repayment_schedule = []
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while balance_amount > 0:
interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts(
payment_date,
balance_amount,
schedule_type_details.repayment_schedule_type,
schedule_type_details.repayment_date_on,
)
if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months":
next_payment_date = get_last_day(payment_date)
if schedule_type_details.repayment_date_on == "Start of the next month":
next_payment_date = add_days(next_payment_date, 1)
payment_date = next_payment_date
self.add_repayment_schedule_row(
payment_date, principal_amount, interest_amount, total_payment, balance_amount
)
if (
schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date"
or schedule_type_details.repayment_date_on == "End of the current month"
):
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on):
if schedule_type == "Monthly as per repayment start date":
days = 1
months = 12
else:
expected_payment_date = get_last_day(payment_date)
if repayment_date_on == "Start of the next month":
expected_payment_date = add_days(expected_payment_date, 1)
if expected_payment_date == payment_date:
# using 30 days for calculating interest for all full months
days = 30
months = 365
else:
days = date_diff(get_last_day(payment_date), payment_date)
months = 365
interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
return interest_amount, principal_amount, balance_amount, total_payment
def add_repayment_schedule_row(
self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount
):
self.append(
"repayment_schedule",
{
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_loan_amount,
},
)
def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
repayment_periods = len(self.repayment_schedule)
self.repayment_periods = repayment_periods
def calculate_totals(self):
self.total_payment = 0
self.total_interest_payable = 0
self.total_amount_paid = 0
if self.is_term_loan:
for data in self.repayment_schedule:
self.total_payment += data.total_payment
self.total_interest_payable += data.interest_amount
else:
self.total_payment = self.loan_amount
def set_loan_amount(self):
if self.loan_application and not self.loan_amount:
self.loan_amount = frappe.db.get_value("Loan Application", self.loan_application, "loan_amount")
def validate_loan_amount(self):
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
msg = _("Loan amount cannot be greater than {0}").format(self.maximum_loan_amount)
frappe.throw(msg)
if not self.loan_amount:
frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self):
if self.is_secured_loan and self.loan_application:
maximum_loan_value = frappe.db.get_value(
"Loan Security Pledge",
{"loan_application": self.loan_application, "status": "Requested"},
"sum(maximum_loan_value)",
)
if maximum_loan_value:
frappe.db.sql(
"""
UPDATE `tabLoan Security Pledge`
SET loan = %s, pledge_time = %s, status = 'Pledged'
WHERE status = 'Requested' and loan_application = %s
""",
(self.name, now_datetime(), self.loan_application),
)
self.db_set("maximum_loan_amount", maximum_loan_value)
def accrue_loan_interest(self):
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_term_loans,
)
if getdate(self.repayment_start_date) < getdate() and self.is_term_loan:
process_loan_interest_accrual_for_term_loans(
posting_date=getdate(), loan_type=self.loan_type, loan=self.name
)
def unlink_loan_security_pledge(self):
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
pledge_list = [d.name for d in pledges]
if pledge_list:
frappe.db.sql(
"""UPDATE `tabLoan Security Pledge` SET
loan = '', status = 'Unpledged'
where name in (%s) """
% (", ".join(["%s"] * len(pledge_list))),
tuple(pledge_list),
) # nosec
def update_total_amount_paid(doc):
total_amount_paid = 0
for data in doc.repayment_schedule:
if data.paid:
total_amount_paid += data.total_payment
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company):
pending_amount = 0
loan_details = frappe.db.get_all(
"Loan",
filters={
"applicant_type": applicant_type,
"company": company,
"applicant": applicant,
"docstatus": 1,
"status": ("!=", "Closed"),
},
fields=[
"status",
"total_payment",
"disbursed_amount",
"total_interest_payable",
"total_principal_paid",
"written_off_amount",
],
)
interest_amount = flt(
frappe.db.get_value(
"Loan Interest Accrual",
{"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1},
"sum(interest_amount - paid_interest_amount)",
)
)
for loan in loan_details:
if loan.status in ("Disbursed", "Loan Closure Requested"):
pending_amount += (
flt(loan.total_payment)
- flt(loan.total_interest_payable)
- flt(loan.total_principal_paid)
- flt(loan.written_off_amount)
)
elif loan.status == "Partially Disbursed":
pending_amount += (
flt(loan.disbursed_amount)
- flt(loan.total_interest_payable)
- flt(loan.total_principal_paid)
- flt(loan.written_off_amount)
)
elif loan.status == "Sanctioned":
pending_amount += flt(loan.total_payment)
pending_amount += interest_amount
return pending_amount
def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value(
"Sanctioned Loan Amount",
{"applicant_type": applicant_type, "company": company, "applicant": applicant},
"sanctioned_amount_limit",
)
def validate_repayment_method(
repayment_method, loan_amount, monthly_repayment_amount, repayment_periods, is_term_loan
):
if is_term_loan and not repayment_method:
frappe.throw(_("Repayment Method is mandatory for term loans"))
if repayment_method == "Repay Over Number of Periods" and not repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))
if repayment_method == "Repay Fixed Amount per Period":
if not monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if monthly_repayment_amount > loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))
def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 * 100)
monthly_repayment_amount = math.ceil(
(loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods)
/ ((1 + monthly_interest_rate) ** repayment_periods - 1)
)
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
@frappe.whitelist()
def request_loan_closure(loan, posting_date=None):
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = (
amounts["pending_principal_amount"]
+ amounts["unaccrued_interest"]
+ amounts["interest_amount"]
+ amounts["penalty_amount"]
)
loan_type = frappe.get_value("Loan", loan, "loan_type")
write_off_limit = frappe.get_value("Loan Type", loan_type, "write_off_amount")
if pending_amount and abs(pending_amount) < write_off_limit:
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
elif pending_amount > 0:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
frappe.db.set_value("Loan", loan, "status", "Loan Closure Requested")
@frappe.whitelist()
def get_loan_application(loan_application):
loan = frappe.get_doc("Loan Application", loan_application)
if loan:
return loan.as_dict()
@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
)
if (
loan_details.status == "Loan Closure Requested"
and loan_details.is_term_loan
and not loan_details.is_secured_loan
):
frappe.db.set_value("Loan", loan, "status", "Closed")
else:
frappe.throw(_("Cannot close this loan until full repayment"))
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
@frappe.whitelist()
def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0):
disbursement_entry = frappe.new_doc("Loan Disbursement")
disbursement_entry.against_loan = loan
disbursement_entry.applicant_type = applicant_type
disbursement_entry.applicant = applicant
disbursement_entry.company = company
disbursement_entry.disbursement_date = nowdate()
disbursement_entry.posting_date = nowdate()
disbursement_entry.disbursed_amount = pending_amount
if as_dict:
return disbursement_entry.as_dict()
else:
return disbursement_entry
@frappe.whitelist()
def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as_dict=0):
repayment_entry = frappe.new_doc("Loan Repayment")
repayment_entry.against_loan = loan
repayment_entry.applicant_type = applicant_type
repayment_entry.applicant = applicant
repayment_entry.company = company
repayment_entry.loan_type = loan_type
repayment_entry.posting_date = nowdate()
if as_dict:
return repayment_entry.as_dict()
else:
return repayment_entry
@frappe.whitelist()
def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict=0):
if not company:
company = frappe.get_value("Loan", loan, "company")
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts["pending_principal_amount"]
if amount and (amount > pending_amount):
frappe.throw(_("Write Off amount cannot be greater than pending loan amount"))
if not amount:
amount = pending_amount
# get default write off account from company master
write_off_account = frappe.get_value("Company", company, "write_off_account")
write_off = frappe.new_doc("Loan Write Off")
write_off.loan = loan
write_off.posting_date = posting_date
write_off.write_off_account = write_off_account
write_off.write_off_amount = amount
write_off.save()
if as_dict:
return write_off.as_dict()
else:
return write_off
@frappe.whitelist()
def unpledge_security(
loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0
):
# if no security_map is passed it will be considered as full unpledge
if security_map and isinstance(security_map, str):
security_map = json.loads(security_map)
if loan:
pledge_qty_map = security_map or get_pledged_security_qty(loan)
loan_doc = frappe.get_doc("Loan", loan)
unpledge_request = create_loan_security_unpledge(
pledge_qty_map, loan_doc.name, loan_doc.company, loan_doc.applicant_type, loan_doc.applicant
)
# will unpledge qty based on loan security pledge
elif loan_security_pledge:
security_map = {}
pledge_doc = frappe.get_doc("Loan Security Pledge", loan_security_pledge)
for security in pledge_doc.securities:
security_map.setdefault(security.loan_security, security.qty)
unpledge_request = create_loan_security_unpledge(
security_map,
pledge_doc.loan,
pledge_doc.company,
pledge_doc.applicant_type,
pledge_doc.applicant,
)
if save:
unpledge_request.save()
if submit:
unpledge_request.submit()
if approve:
if unpledge_request.docstatus == 1:
unpledge_request.status = "Approved"
unpledge_request.save()
else:
frappe.throw(_("Only submittted unpledge requests can be approved"))
if as_dict:
return unpledge_request
else:
return unpledge_request
def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant):
unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type
unpledge_request.applicant = applicant
unpledge_request.loan = loan
unpledge_request.company = company
for security, qty in unpledge_map.items():
if qty:
unpledge_request.append("securities", {"loan_security": security, "qty": qty})
return unpledge_request
@frappe.whitelist()
def get_shortfall_applicants():
loans = frappe.get_all("Loan Security Shortfall", {"status": "Pending"}, pluck="loan")
applicants = set(frappe.get_all("Loan", {"name": ("in", loans)}, pluck="name"))
return {"value": len(applicants), "fieldtype": "Int"}
def add_single_month(date):
if getdate(date) == get_last_day(date):
return get_last_day(add_months(date, 1))
else:
return add_months(date, 1)
@frappe.whitelist()
def make_refund_jv(loan, amount=0, reference_number=None, reference_date=None, submit=0):
loan_details = frappe.db.get_value(
"Loan",
loan,
[
"applicant_type",
"applicant",
"loan_account",
"payment_account",
"posting_date",
"company",
"name",
"total_payment",
"total_principal_paid",
],
as_dict=1,
)
loan_details.doctype = "Loan"
loan_details[loan_details.applicant_type.lower()] = loan_details.applicant
if not amount:
amount = flt(loan_details.total_principal_paid - loan_details.total_payment)
if amount < 0:
frappe.throw(_("No excess amount pending for refund"))
refund_jv = get_payment_entry(
loan_details,
{
"party_type": loan_details.applicant_type,
"party_account": loan_details.loan_account,
"amount_field_party": "debit_in_account_currency",
"amount_field_bank": "credit_in_account_currency",
"amount": amount,
"bank_account": loan_details.payment_account,
},
)
if reference_number:
refund_jv.cheque_no = reference_number
if reference_date:
refund_jv.cheque_date = reference_date
if submit:
refund_jv.submit()
return refund_jv

View File

@ -1,19 +0,0 @@
def get_data():
return {
"fieldname": "loan",
"non_standard_fieldnames": {
"Loan Disbursement": "against_loan",
"Loan Repayment": "against_loan",
},
"transactions": [
{"items": ["Loan Security Pledge", "Loan Security Shortfall", "Loan Disbursement"]},
{
"items": [
"Loan Repayment",
"Loan Interest Accrual",
"Loan Write Off",
"Loan Security Unpledge",
]
},
],
}

View File

@ -1,16 +0,0 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.listview_settings['Loan'] = {
get_indicator: function(doc) {
var status_color = {
"Draft": "red",
"Sanctioned": "blue",
"Disbursed": "orange",
"Partially Disbursed": "yellow",
"Loan Closure Requested": "green",
"Closed": "green"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},
};

File diff suppressed because it is too large Load Diff

View File

@ -1,144 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Application', {
setup: function(frm) {
frm.make_methods = {
'Loan': function() { frm.trigger('create_loan') },
'Loan Security Pledge': function() { frm.trigger('create_loan_security_pledge') },
}
},
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
frm.set_query('loan_type', () => {
return {
filters: {
company: frm.doc.company
}
};
});
},
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = "";
frm.trigger("toggle_fields");
frm.trigger("toggle_required");
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
if (frm.doc.is_secured_loan) {
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() {
frm.trigger('create_loan_security_pledge');
},__('Create'))
}
});
}
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan');
},__('Create'))
} else {
frm.set_df_property('status', 'read_only', 1);
}
});
}
},
create_loan: function(frm) {
if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved"));
}
frappe.model.open_mapped_doc({
method: 'erpnext.loan_management.doctype.loan_application.loan_application.create_loan',
frm: frm
});
},
create_loan_security_pledge: function(frm) {
if(!frm.doc.is_secured_loan) {
frappe.throw(__("Loan Security Pledge can only be created for secured loans"));
}
frappe.call({
method: "erpnext.loan_management.doctype.loan_application.loan_application.create_pledge",
args: {
loan_application: frm.doc.name
},
callback: function(r) {
frappe.set_route("Form", "Loan Security Pledge", r.message);
}
})
},
is_term_loan: function(frm) {
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);
},
calculate_amounts: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.qty) {
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
} else if (row.amount) {
frappe.model.set_value(cdt, cdn, 'qty', cint(row.amount / row.loan_security_price));
frappe.model.set_value(cdt, cdn, 'amount', row.qty * row.loan_security_price);
frappe.model.set_value(cdt, cdn, 'post_haircut_amount', cint(row.amount - (row.amount * row.haircut/100)));
}
let maximum_amount = 0;
$.each(frm.doc.proposed_pledges || [], function(i, item){
maximum_amount += item.post_haircut_amount;
});
if (flt(maximum_amount)) {
frm.set_value('maximum_loan_amount', flt(maximum_amount));
}
}
});
frappe.ui.form.on("Proposed Pledge", {
loan_security: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.loan_security) {
frappe.call({
method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price",
args: {
loan_security: row.loan_security
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message);
frm.events.calculate_amounts(frm, cdt, cdn);
}
})
}
},
amount: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
qty: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
})

View File

@ -1,282 +0,0 @@
{
"actions": [],
"autoname": "ACC-LOAP-.YYYY.-.#####",
"creation": "2019-08-29 17:46:49.201740",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicant_type",
"applicant",
"applicant_name",
"column_break_2",
"company",
"posting_date",
"status",
"section_break_4",
"loan_type",
"is_term_loan",
"loan_amount",
"is_secured_loan",
"rate_of_interest",
"column_break_7",
"description",
"loan_security_details_section",
"proposed_pledges",
"maximum_loan_amount",
"repayment_info",
"repayment_method",
"total_payable_amount",
"column_break_11",
"repayment_periods",
"repayment_amount",
"total_payable_interest",
"amended_from"
],
"fields": [
{
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_global_search": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
},
{
"depends_on": "applicant",
"fieldname": "applicant_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Applicant Name",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"no_copy": 1,
"options": "Open\nApproved\nRejected",
"permlevel": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break",
"label": "Loan Info"
},
{
"fieldname": "loan_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Type",
"options": "Loan Type",
"reqd": 1
},
{
"bold": 1,
"fieldname": "loan_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Loan Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Reason"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fieldname": "repayment_info",
"fieldtype": "Section Break",
"label": "Repayment Info"
},
{
"depends_on": "eval: doc.is_term_loan == 1",
"fetch_if_empty": 1,
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate of Interest",
"read_only": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "total_payable_interest",
"fieldtype": "Currency",
"label": "Total Payable Interest",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
},
{
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"label": "Total Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Application",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "is_secured_loan",
"fieldtype": "Check",
"label": "Is Secured Loan"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"depends_on": "eval:doc.is_secured_loan == 1",
"fieldname": "proposed_pledges",
"fieldtype": "Table",
"label": "Proposed Pledges",
"options": "Proposed Pledge"
},
{
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fetch_from": "loan_type.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:24:40.119647",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Application",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1
}

View File

@ -1,257 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import math
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, rounded
from erpnext.loan_management.doctype.loan.loan import (
get_monthly_repayment_amount,
get_sanctioned_amount_limit,
get_total_loan_amount,
validate_repayment_method,
)
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import (
get_loan_security_price,
)
class LoanApplication(Document):
def validate(self):
self.set_pledge_amount()
self.set_loan_amount()
self.validate_loan_amount()
if self.is_term_loan:
validate_repayment_method(
self.repayment_method,
self.loan_amount,
self.repayment_amount,
self.repayment_periods,
self.is_term_loan,
)
self.validate_loan_type()
self.get_repayment_details()
self.check_sanctioned_amount_limit()
def validate_loan_type(self):
company = frappe.get_value("Loan Type", self.loan_type, "company")
if company != self.company:
frappe.throw(_("Please select Loan Type for company {0}").format(frappe.bold(self.company)))
def validate_loan_amount(self):
if not self.loan_amount:
frappe.throw(_("Loan Amount is mandatory"))
maximum_loan_limit = frappe.db.get_value("Loan Type", self.loan_type, "maximum_loan_amount")
if maximum_loan_limit and self.loan_amount > maximum_loan_limit:
frappe.throw(
_("Loan Amount cannot exceed Maximum Loan Amount of {0}").format(maximum_loan_limit)
)
if self.maximum_loan_amount and self.loan_amount > self.maximum_loan_amount:
frappe.throw(
_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(
self.maximum_loan_amount
)
)
def check_sanctioned_amount_limit(self):
sanctioned_amount_limit = get_sanctioned_amount_limit(
self.applicant_type, self.applicant, self.company
)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(
sanctioned_amount_limit
):
frappe.throw(
_("Sanctioned Amount limit crossed for {0} {1}").format(
self.applicant_type, frappe.bold(self.applicant)
)
)
def set_pledge_amount(self):
for proposed_pledge in self.proposed_pledges:
if not proposed_pledge.qty and not proposed_pledge.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
proposed_pledge.loan_security_price = get_loan_security_price(proposed_pledge.loan_security)
if not proposed_pledge.qty:
proposed_pledge.qty = cint(proposed_pledge.amount / proposed_pledge.loan_security_price)
proposed_pledge.amount = proposed_pledge.qty * proposed_pledge.loan_security_price
proposed_pledge.post_haircut_amount = cint(
proposed_pledge.amount - (proposed_pledge.amount * proposed_pledge.haircut / 100)
)
def get_repayment_details(self):
if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(
self.loan_amount, self.rate_of_interest, self.repayment_periods
)
if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 * 100)
if monthly_interest_rate:
min_repayment_amount = self.loan_amount * monthly_interest_rate
if self.repayment_amount - min_repayment_amount <= 0:
frappe.throw(_("Repayment Amount must be greater than " + str(flt(min_repayment_amount, 2))))
self.repayment_periods = math.ceil(
(math.log(self.repayment_amount) - math.log(self.repayment_amount - min_repayment_amount))
/ (math.log(1 + monthly_interest_rate))
)
else:
self.repayment_periods = self.loan_amount / self.repayment_amount
self.calculate_payable_amount()
else:
self.total_payable_amount = self.loan_amount
def calculate_payable_amount(self):
balance_amount = self.loan_amount
self.total_payable_amount = 0
self.total_payable_interest = 0
while balance_amount > 0:
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
self.total_payable_interest += interest_amount
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:
frappe.throw(_("Proposed Pledges are mandatory for secured Loans"))
if self.is_secured_loan 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:
self.loan_amount = self.maximum_loan_amount
@frappe.whitelist()
def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all(
"Loan Type",
fields=[
"mode_of_payment",
"payment_account",
"loan_account",
"interest_income_account",
"penalty_income_account",
],
filters={"name": source_doc.loan_type},
)[0]
if source_doc.is_secured_loan:
target_doc.maximum_loan_amount = 0
target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account
target_doc.loan_account = account_details.loan_account
target_doc.interest_income_account = account_details.interest_income_account
target_doc.penalty_income_account = account_details.penalty_income_account
target_doc.loan_application = source_name
doclist = get_mapped_doc(
"Loan Application",
source_name,
{
"Loan Application": {
"doctype": "Loan",
"validation": {"docstatus": ["=", 1]},
"postprocess": update_accounts,
}
},
target_doc,
)
if submit:
doclist.submit()
return doclist
@frappe.whitelist()
def create_pledge(loan_application, loan=None):
loan_application_doc = frappe.get_doc("Loan Application", loan_application)
lsp = frappe.new_doc("Loan Security Pledge")
lsp.applicant_type = loan_application_doc.applicant_type
lsp.applicant = loan_application_doc.applicant
lsp.loan_application = loan_application_doc.name
lsp.company = loan_application_doc.company
if loan:
lsp.loan = loan
for pledge in loan_application_doc.proposed_pledges:
lsp.append(
"securities",
{
"loan_security": pledge.loan_security,
"qty": pledge.qty,
"loan_security_price": pledge.loan_security_price,
"haircut": pledge.haircut,
},
)
lsp.save()
lsp.submit()
message = _("Loan Security Pledge Created : {0}").format(lsp.name)
frappe.msgprint(message)
return lsp.name
# This is a sandbox method to get the proposed pledges
@frappe.whitelist()
def get_proposed_pledge(securities):
if isinstance(securities, str):
securities = json.loads(securities)
proposed_pledges = {"securities": []}
maximum_loan_amount = 0
for security in securities:
security = frappe._dict(security)
if not security.qty and not security.amount:
frappe.throw(_("Qty or Amount is mandatroy for loan security"))
security.loan_security_price = get_loan_security_price(security.loan_security)
if not security.qty:
security.qty = cint(security.amount / security.loan_security_price)
security.amount = security.qty * security.loan_security_price
security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut / 100))
maximum_loan_amount += security.post_haircut_amount
proposed_pledges["securities"].append(security)
proposed_pledges["maximum_loan_amount"] = maximum_loan_amount
return proposed_pledges

View File

@ -1,7 +0,0 @@
def get_data():
return {
"fieldname": "loan_application",
"transactions": [
{"items": ["Loan", "Loan Security Pledge"]},
],
}

View File

@ -1,62 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts, create_loan_type
from erpnext.setup.doctype.employee.test_employee import make_employee
class TestLoanApplication(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type(
"Home Loan",
500000,
9.2,
0,
1,
0,
"Cash",
"Disbursement Account - _TC",
"Payment Account - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
"Repay Over Number of Periods",
18,
)
self.applicant = make_employee("kate_loan@loan.com", "_Test Company")
self.create_loan_application()
def create_loan_application(self):
loan_application = frappe.new_doc("Loan Application")
loan_application.update(
{
"applicant": self.applicant,
"loan_type": "Home Loan",
"rate_of_interest": 9.2,
"loan_amount": 250000,
"repayment_method": "Repay Over Number of Periods",
"repayment_periods": 18,
"company": "_Test Company",
}
)
loan_application.insert()
def test_loan_totals(self):
loan_application = frappe.get_doc("Loan Application", {"applicant": self.applicant})
self.assertEqual(loan_application.total_payable_interest, 18599)
self.assertEqual(loan_application.total_payable_amount, 268599)
self.assertEqual(loan_application.repayment_amount, 14923)
loan_application.repayment_periods = 24
loan_application.save()
loan_application.reload()
self.assertEqual(loan_application.total_payable_interest, 24657)
self.assertEqual(loan_application.total_payable_amount, 274657)
self.assertEqual(loan_application.repayment_amount, 11445)

View File

@ -1,8 +0,0 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Balance Adjustment', {
// refresh: function(frm) {
// }
});

View File

@ -1,189 +0,0 @@
{
"actions": [],
"autoname": "LM-ADJ-.#####",
"creation": "2022-06-28 14:48:47.736269",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"adjustment_account",
"column_break_11",
"adjustment_type",
"amount",
"reference_number",
"remarks",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Adjustment Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Balance Adjustment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "adjustment_account",
"fieldtype": "Link",
"label": "Adjustment Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "adjustment_type",
"fieldtype": "Select",
"label": "Adjustment Type",
"options": "Credit Adjustment\nDebit Adjustment",
"reqd": 1
},
{
"fieldname": "remarks",
"fieldtype": "Data",
"label": "Remarks"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-07-08 16:48:54.480066",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Balance Adjustment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,143 +0,0 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, nowdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanBalanceAdjustment(AccountsController):
"""
Add credit/debit adjustments to loan ledger.
"""
def validate(self):
if self.amount == 0:
frappe.throw(_("Amount cannot be zero"))
if self.amount < 0:
frappe.throw(_("Amount cannot be negative"))
self.set_missing_values()
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.posting_date:
self.posting_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.db.get_value(
"Loan",
self.loan,
[
"loan_amount",
"credit_adjustment_amount",
"debit_adjustment_amount",
"total_payment",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
],
as_dict=1,
)
if cancel:
adjustment_amount = self.get_values_on_cancel(loan_details)
else:
adjustment_amount = self.get_values_on_submit(loan_details)
if self.adjustment_type == "Credit Adjustment":
adj_field = "credit_adjustment_amount"
elif self.adjustment_type == "Debit Adjustment":
adj_field = "debit_adjustment_amount"
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
def get_values_on_cancel(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
return adjustment_amount
def get_values_on_submit(self, loan_details):
if self.adjustment_type == "Credit Adjustment":
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
elif self.adjustment_type == "Debit Adjustment":
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(
posting_date=add_days(self.posting_date, -1),
loan=self.loan,
accrual_type=self.adjustment_type,
)
return adjustment_amount
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
if self.reference_number:
remarks += " with reference no. {}".format(self.reference_number)
loan_entry = {
"account": loan_account,
"against": self.adjustment_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.posting_date,
}
company_entry = {
"account": self.adjustment_account,
"against": loan_account,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": self.posting_date,
}
if self.adjustment_type == "Credit Adjustment":
loan_entry["credit"] = self.amount
loan_entry["credit_in_account_currency"] = self.amount
company_entry["debit"] = self.amount
company_entry["debit_in_account_currency"] = self.amount
elif self.adjustment_type == "Debit Adjustment":
loan_entry["debit"] = self.amount
loan_entry["debit_in_account_currency"] = self.amount
company_entry["credit"] = self.amount
company_entry["credit_in_account_currency"] = self.amount
gle_map.append(self.get_gl_dict(loan_entry))
gle_map.append(self.get_gl_dict(company_entry))
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)

View File

@ -1,9 +0,0 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanBalanceAdjustment(FrappeTestCase):
pass

View File

@ -1,17 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Disbursement', {
refresh: function(frm) {
frm.set_query('against_loan', function() {
return {
'filters': {
'docstatus': 1,
'status': 'Sanctioned'
}
}
})
}
});

View File

@ -1,231 +0,0 @@
{
"actions": [],
"autoname": "LM-DIS-.#####",
"creation": "2019-09-07 12:44:49.125452",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"against_loan",
"posting_date",
"applicant_type",
"column_break_4",
"company",
"applicant",
"section_break_7",
"disbursement_date",
"clearance_date",
"column_break_8",
"disbursed_amount",
"accounting_dimensions_section",
"cost_center",
"accounting_details",
"disbursement_account",
"column_break_16",
"loan_account",
"bank_account",
"disbursement_references_section",
"reference_date",
"column_break_17",
"reference_number",
"amended_from"
],
"fields": [
{
"fieldname": "against_loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Against Loan ",
"options": "Loan",
"reqd": 1
},
{
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date",
"reqd": 1
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Disbursement",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "against_loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Applicant",
"options": "applicant_type",
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"hidden": 1,
"label": "Posting Date",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Disbursement Details"
},
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "bank_account",
"fieldtype": "Link",
"label": "Bank Account",
"options": "Bank Account"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "disbursement_references_section",
"fieldtype": "Section Break",
"label": "Disbursement References"
},
{
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Reference Date"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.disbursement_account",
"fetch_if_empty": 1,
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-08-04 17:16:04.922444",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,257 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, flt, get_datetime, nowdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
get_pledged_security_qty,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanDisbursement(AccountsController):
def validate(self):
self.set_missing_values()
self.validate_disbursal_amount()
def on_submit(self):
self.set_status_and_amounts()
self.make_gl_entries()
def on_cancel(self):
self.set_status_and_amounts(cancel=1)
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def set_missing_values(self):
if not self.disbursement_date:
self.disbursement_date = nowdate()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.posting_date:
self.posting_date = self.disbursement_date or nowdate()
def validate_disbursal_amount(self):
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
def set_status_and_amounts(self, cancel=0):
loan_details = frappe.get_all(
"Loan",
fields=[
"loan_amount",
"disbursed_amount",
"total_payment",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
],
filters={"name": self.against_loan},
)[0]
if cancel:
disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details)
else:
disbursed_amount, status, total_payment = self.get_values_on_submit(loan_details)
frappe.db.set_value(
"Loan",
self.against_loan,
{
"disbursement_date": self.disbursement_date,
"disbursed_amount": disbursed_amount,
"status": status,
"total_payment": total_payment,
},
)
def get_values_on_cancel(self, loan_details):
disbursed_amount = loan_details.disbursed_amount - self.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.disbursed_amount > loan_details.loan_amount:
topup_amount = loan_details.disbursed_amount - loan_details.loan_amount
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment - topup_amount
if disbursed_amount == 0:
status = "Sanctioned"
elif disbursed_amount >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def get_values_on_submit(self, loan_details):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(
posting_date=add_days(self.disbursement_date, -1),
loan=self.against_loan,
accrual_type="Disbursement",
)
if disbursed_amount > loan_details.loan_amount:
topup_amount = disbursed_amount - loan_details.loan_amount
if topup_amount < 0:
topup_amount = 0
if topup_amount > self.disbursed_amount:
topup_amount = self.disbursed_amount
total_payment = total_payment + topup_amount
if flt(disbursed_amount) >= loan_details.loan_amount:
status = "Disbursed"
else:
status = "Partially Disbursed"
return disbursed_amount, status, total_payment
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
gle_map.append(
self.get_gl_dict(
{
"account": self.loan_account,
"against": self.disbursement_account,
"debit": self.disbursed_amount,
"debit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": self.disbursement_date,
}
)
)
gle_map.append(
self.get_gl_dict(
{
"account": self.disbursement_account,
"against": self.loan_account,
"credit": self.disbursed_amount,
"credit_in_account_currency": self.disbursed_amount,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Disbursement against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": self.disbursement_date,
}
)
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
def get_total_pledged_security_value(loan):
update_time = get_datetime()
loan_security_price_map = frappe._dict(
frappe.get_all(
"Loan Security Price",
fields=["loan_security", "loan_security_price"],
filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)},
as_list=1,
)
)
hair_cut_map = frappe._dict(
frappe.get_all("Loan Security", fields=["name", "haircut"], as_list=1)
)
security_value = 0.0
pledged_securities = get_pledged_security_qty(loan)
for security, qty in pledged_securities.items():
after_haircut_percentage = 100 - hair_cut_map.get(security)
security_value += (
loan_security_price_map.get(security, 0) * qty * after_haircut_percentage
) / 100
return security_value
@frappe.whitelist()
def get_disbursal_amount(loan, on_current_security_price=0):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
loan_details = frappe.get_value(
"Loan",
loan,
[
"loan_amount",
"disbursed_amount",
"total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"total_principal_paid",
"total_interest_payable",
"status",
"is_term_loan",
"is_secured_loan",
"maximum_loan_amount",
"written_off_amount",
],
as_dict=1,
)
if loan_details.is_secured_loan and frappe.get_all(
"Loan Security Shortfall", filters={"loan": loan, "status": "Pending"}
):
return 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:
security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan 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:
security_value = flt(loan_details.loan_amount)
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
if (
loan_details.is_term_loan
and (disbursal_amount + loan_details.loan_amount) > loan_details.loan_amount
):
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount
def get_maximum_amount_as_per_pledged_security(loan):
return flt(frappe.db.get_value("Loan Security Pledge", {"loan": loan}, "sum(maximum_loan_value)"))

View File

@ -1,162 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import (
add_days,
add_to_date,
date_diff,
flt,
get_datetime,
get_first_day,
get_last_day,
nowdate,
)
from erpnext.loan_management.doctype.loan.test_loan import (
create_demand_loan,
create_loan_accounts,
create_loan_application,
create_loan_security,
create_loan_security_pledge,
create_loan_security_price,
create_loan_security_type,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (
days_in_year,
get_per_day_interest,
)
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoanDisbursement(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type(
"Demand Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"Disbursement Account - _TC",
"Payment Account - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"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))
)
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}]
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 = get_first_day(nowdate())
last_date = get_last_day(nowdate())
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().year) * 100
)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1))
# Should not be able to create loan disbursement entry before repayment
self.assertRaises(
frappe.ValidationError, make_loan_disbursement_entry, loan.name, 500000, first_date
)
repayment_entry = create_repayment_entry(
loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89
)
repayment_entry.submit()
loan.reload()
# After repayment loan disbursement entry should go through
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
# check for disbursement accrual
loan_interest_accrual = frappe.db.get_value(
"Loan Interest Accrual", {"loan": loan.name, "accrual_type": "Disbursement"}
)
self.assertTrue(loan_interest_accrual)
def test_loan_topup_with_additional_pledge(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="2019-10-01"
)
loan.submit()
self.assertEqual(loan.loan_amount, 1000000)
first_date = "2019-10-01"
last_date = "2019-10-30"
# Disbursed 10,00,000 amount
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 1))
previous_interest = amounts["interest_amount"]
pledge1 = [{"loan_security": "Test Security 1", "qty": 2000.00}]
create_loan_security_pledge(self.applicant, pledge1, loan=loan.name)
# Topup 500000
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 1))
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 15))
amounts = calculate_amounts(loan.name, add_days(last_date, 15))
per_day_interest = get_per_day_interest(1500000, 13.5, "2019-10-30")
interest = per_day_interest * 15
self.assertEqual(amounts["pending_principal_amount"], 1500000)

View File

@ -1,10 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Interest Accrual', {
// refresh: function(frm) {
// }
});

View File

@ -1,239 +0,0 @@
{
"actions": [],
"autoname": "LM-LIA-.#####",
"creation": "2019-09-09 22:34:36.346812",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"interest_income_account",
"loan_account",
"column_break_4",
"company",
"posting_date",
"accrual_type",
"is_term_loan",
"section_break_7",
"pending_principal_amount",
"payable_principal_amount",
"paid_principal_amount",
"column_break_14",
"interest_amount",
"total_pending_interest_amount",
"paid_interest_amount",
"penalty_amount",
"section_break_15",
"process_loan_interest_accrual",
"repayment_schedule_name",
"last_accrual_date",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan",
"options": "Loan"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date"
},
{
"fieldname": "pending_principal_amount",
"fieldtype": "Currency",
"label": "Pending Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Interest Accrual",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.interest_income_account",
"fieldname": "interest_income_account",
"fieldtype": "Data",
"label": "Interest Income Account"
},
{
"fetch_from": "loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Data",
"label": "Loan Account"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Amounts"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"default": "0",
"fetch_from": "loan.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"depends_on": "is_term_loan",
"fieldname": "payable_principal_amount",
"fieldtype": "Currency",
"label": "Payable Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break"
},
{
"fieldname": "process_loan_interest_accrual",
"fieldtype": "Link",
"label": "Process Loan Interest Accrual",
"options": "Process Loan Interest Accrual"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fieldname": "repayment_schedule_name",
"fieldtype": "Data",
"hidden": 1,
"label": "Repayment Schedule Name",
"read_only": 1
},
{
"depends_on": "eval:doc.is_term_loan",
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "accrual_type",
"fieldtype": "Select",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
},
{
"fieldname": "penalty_amount",
"fieldtype": "Currency",
"label": "Penalty Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "last_accrual_date",
"fieldtype": "Date",
"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": "2022-06-30 11:51:31.911794",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,331 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate, nowdate
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
class LoanInterestAccrual(AccountsController):
def validate(self):
if not self.loan:
frappe.throw(_("Loan is mandatory"))
if not self.posting_date:
self.posting_date = nowdate()
if not self.interest_amount and not self.payable_principal_amount:
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
if not self.last_accrual_date:
self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
def on_submit(self):
self.make_gl_entries()
def on_cancel(self):
if self.repayment_schedule_name:
self.update_is_accrued()
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
def update_is_accrued(self):
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
cost_center = frappe.db.get_value("Loan", self.loan, "cost_center")
if self.interest_amount:
gle_map.append(
self.get_gl_dict(
{
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": self.interest_income_account,
"debit": self.interest_amount,
"debit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan
),
"cost_center": cost_center,
"posting_date": self.posting_date,
}
)
)
gle_map.append(
self.get_gl_dict(
{
"account": self.interest_income_account,
"against": self.loan_account,
"credit": self.interest_amount,
"credit_in_account_currency": self.interest_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": ("Interest accrued from {0} to {1} against loan: {2}").format(
self.last_accrual_date, self.posting_date, self.loan
),
"cost_center": cost_center,
"posting_date": self.posting_date,
}
)
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj)
# For Eg: If Loan disbursement date is '01-09-2019' and disbursed amount is 1000000 and
# rate of interest is 13.5 then first loan interest accural will be on '01-10-2019'
# which means interest will be accrued for 30 days which should be equal to 11095.89
def calculate_accrual_amount_for_demand_loans(
loan, posting_date, process_loan_interest, accrual_type
):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
calculate_amounts,
get_pending_principal_amount,
)
no_of_days = get_no_of_days_for_interest_accural(loan, posting_date)
precision = cint(frappe.db.get_default("currency_precision")) or 2
if no_of_days <= 0:
return
pending_principal_amount = get_pending_principal_amount(loan)
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,
"applicant": loan.applicant,
"interest_income_account": loan.interest_income_account,
"loan_account": loan.loan_account,
"pending_principal_amount": pending_principal_amount,
"interest_amount": payable_interest,
"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,
}
)
if flt(payable_interest, precision) > 0.0:
make_loan_interest_accrual_entry(args)
def make_accrual_interest_entry_for_demand_loans(
posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"
):
query_filters = {
"status": ("in", ["Disbursed", "Partially Disbursed"]),
"docstatus": 1,
"is_term_loan": 0,
}
if loan_type:
query_filters.update({"loan_type": loan_type})
if not open_loans:
open_loans = frappe.get_all(
"Loan",
fields=[
"name",
"total_payment",
"total_amount_paid",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"loan_account",
"interest_income_account",
"loan_amount",
"is_term_loan",
"status",
"disbursement_date",
"disbursed_amount",
"applicant_type",
"applicant",
"rate_of_interest",
"total_interest_payable",
"written_off_amount",
"total_principal_paid",
"repayment_start_date",
],
filters=query_filters,
)
for loan in open_loans:
calculate_accrual_amount_for_demand_loans(
loan, posting_date, process_loan_interest, accrual_type
)
def make_accrual_interest_entry_for_term_loans(
posting_date, process_loan_interest, term_loan=None, loan_type=None, accrual_type="Regular"
):
curr_date = posting_date or add_days(nowdate(), 1)
term_loans = get_term_loans(curr_date, term_loan, loan_type)
accrued_entries = []
for loan in term_loans:
accrued_entries.append(loan.payment_entry)
args = frappe._dict(
{
"loan": loan.name,
"applicant_type": loan.applicant_type,
"applicant": loan.applicant,
"interest_income_account": loan.interest_income_account,
"loan_account": loan.loan_account,
"interest_amount": loan.interest_amount,
"payable_principal": loan.principal_amount,
"process_loan_interest": process_loan_interest,
"repayment_schedule_name": loan.payment_entry,
"posting_date": posting_date,
"accrual_type": accrual_type,
}
)
make_loan_interest_accrual_entry(args)
if accrued_entries:
frappe.db.sql(
"""UPDATE `tabRepayment Schedule`
SET is_accrued = 1 where name in (%s)""" # nosec
% ", ".join(["%s"] * len(accrued_entries)),
tuple(accrued_entries),
)
def get_term_loans(date, term_loan=None, loan_type=None):
condition = ""
if term_loan:
condition += " AND l.name = %s" % frappe.db.escape(term_loan)
if loan_type:
condition += " AND l.loan_type = %s" % frappe.db.escape(loan_type)
term_loans = frappe.db.sql(
"""SELECT l.name, l.total_payment, l.total_amount_paid, l.loan_account,
l.interest_income_account, l.is_term_loan, l.disbursement_date, l.applicant_type, l.applicant,
l.rate_of_interest, l.total_interest_payable, l.repayment_start_date, rs.name as payment_entry,
rs.payment_date, rs.principal_amount, rs.interest_amount, rs.is_accrued , rs.balance_loan_amount
FROM `tabLoan` l, `tabRepayment Schedule` rs
WHERE rs.parent = l.name
AND l.docstatus=1
AND l.is_term_loan =1
AND rs.payment_date <= %s
AND rs.is_accrued=0 {0}
AND rs.principal_amount > 0
AND l.status = 'Disbursed'
ORDER BY rs.payment_date""".format(
condition
),
(getdate(date)),
as_dict=1,
)
return term_loans
def make_loan_interest_accrual_entry(args):
precision = cint(frappe.db.get_default("currency_precision")) or 2
loan_interest_accrual = frappe.new_doc("Loan Interest Accrual")
loan_interest_accrual.loan = args.loan
loan_interest_accrual.applicant_type = args.applicant_type
loan_interest_accrual.applicant = args.applicant
loan_interest_accrual.interest_income_account = args.interest_income_account
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
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.accrual_type = args.accrual_type
loan_interest_accrual.save()
loan_interest_accrual.submit()
def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days
def get_last_accrual_date(loan, posting_date):
last_posting_date = frappe.db.sql(
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s and docstatus = 1""",
(loan),
)
if last_posting_date[0][0]:
last_interest_accrual_date = last_posting_date[0][0]
# interest for last interest accrual date is already booked, so add 1 day
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
if last_disbursement_date and getdate(last_disbursement_date) > add_days(
getdate(last_interest_accrual_date), 1
):
last_interest_accrual_date = last_disbursement_date
return add_days(last_interest_accrual_date, 1)
else:
return frappe.db.get_value("Loan", loan, "disbursement_date")
def get_last_disbursement_date(loan, posting_date):
last_disbursement_date = frappe.db.get_value(
"Loan Disbursement",
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
"MAX(posting_date)",
)
return last_disbursement_date
def days_in_year(year):
days = 365
if (year % 4 == 0) and (year % 100 != 0) or (year % 400 == 0):
days = 366
return days
def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
if not posting_date:
posting_date = getdate()
return flt(
(principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
)

View File

@ -1,127 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_to_date, date_diff, flt, get_datetime, get_first_day, nowdate
from erpnext.loan_management.doctype.loan.test_loan import (
create_demand_loan,
create_loan_accounts,
create_loan_application,
create_loan_security,
create_loan_security_price,
create_loan_security_type,
create_loan_type,
make_loan_disbursement_entry,
)
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (
days_in_year,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
class TestLoanInterestAccrual(unittest.TestCase):
def setUp(self):
create_loan_accounts()
create_loan_type(
"Demand Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"Disbursement Account - _TC",
"Payment Account - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"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))
)
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_interest_accural(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_accural = frappe.get_doc("Loan Interest Accrual", {"loan": loan.name})
self.assertEqual(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.assertEqual(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.assertEqual(
flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount
)

View File

@ -1,8 +0,0 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Refund', {
// refresh: function(frm) {
// }
});

View File

@ -1,176 +0,0 @@
{
"actions": [],
"autoname": "LM-RF-.#####",
"creation": "2022-06-24 15:51:03.165498",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"posting_date",
"accounting_dimensions_section",
"cost_center",
"section_break_9",
"refund_account",
"column_break_11",
"refund_amount",
"reference_number",
"amended_from"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"label": "Applicant ",
"options": "applicant_type",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Refund Details"
},
{
"fieldname": "refund_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Refund Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "refund_amount",
"fieldtype": "Currency",
"label": "Refund Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Refund",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-06-24 16:13:48.793486",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Refund",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,97 +0,0 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanRefund(AccountsController):
"""
Add refund if total repayment is more than that is owed.
"""
def validate(self):
self.set_missing_values()
self.validate_refund_amount()
def set_missing_values(self):
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
def validate_refund_amount(self):
loan = frappe.get_doc("Loan", self.loan)
pending_amount = get_pending_principal_amount(loan)
if pending_amount >= 0:
frappe.throw(_("No excess amount to refund."))
else:
excess_amount = pending_amount * -1
if self.refund_amount > excess_amount:
frappe.throw(_("Refund amount cannot be greater than excess amount {0}").format(excess_amount))
def on_submit(self):
self.update_outstanding_amount()
self.make_gl_entries()
def on_cancel(self):
self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def update_outstanding_amount(self, cancel=0):
refund_amount = frappe.db.get_value("Loan", self.loan, "refund_amount")
if cancel:
refund_amount -= self.refund_amount
else:
refund_amount += self.refund_amount
frappe.db.set_value("Loan", self.loan, "refund_amount", refund_amount)
def make_gl_entries(self, cancel=0):
gl_entries = []
loan_details = frappe.get_doc("Loan", self.loan)
gl_entries.append(
self.get_gl_dict(
{
"account": self.refund_account,
"against": loan_details.loan_account,
"credit": self.refund_amount,
"credit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": self.refund_account,
"debit": self.refund_amount,
"debit_in_account_currency": self.refund_amount,
"against_voucher_type": "Loan",
"against_voucher": self.loan,
"remarks": _("Against Loan:") + self.loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)

View File

@ -1,9 +0,0 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestLoanRefund(FrappeTestCase):
pass

View File

@ -1,64 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include 'erpnext/loan_management/loan_common.js' %};
frappe.ui.form.on('Loan Repayment', {
// refresh: function(frm) {
// }
onload: function(frm) {
frm.set_query('against_loan', function() {
return {
'filters': {
'docstatus': 1
}
};
});
if (frm.doc.against_loan && frm.doc.posting_date && frm.doc.docstatus == 0) {
frm.trigger('calculate_repayment_amounts');
}
},
posting_date : function(frm) {
frm.trigger('calculate_repayment_amounts');
},
against_loan: function(frm) {
if (frm.doc.posting_date) {
frm.trigger('calculate_repayment_amounts');
}
},
payment_type: function(frm) {
if (frm.doc.posting_date) {
frm.trigger('calculate_repayment_amounts');
}
},
calculate_repayment_amounts: function(frm) {
frappe.call({
method: 'erpnext.loan_management.doctype.loan_repayment.loan_repayment.calculate_amounts',
args: {
'against_loan': frm.doc.against_loan,
'posting_date': frm.doc.posting_date,
'payment_type': frm.doc.payment_type
},
callback: function(r) {
let amounts = r.message;
frm.set_value('amount_paid', 0.0);
frm.set_df_property('amount_paid', 'read_only', frm.doc.payment_type == "Loan Closure" ? 1:0);
frm.set_value('pending_principal_amount', amounts['pending_principal_amount']);
if (frm.doc.is_term_loan || frm.doc.payment_type == "Loan Closure") {
frm.set_value('payable_principal_amount', amounts['payable_principal_amount']);
frm.set_value('amount_paid', amounts['payable_amount']);
}
frm.set_value('interest_payable', amounts['interest_amount']);
frm.set_value('penalty_amount', amounts['penalty_amount']);
frm.set_value('payable_amount', amounts['payable_amount']);
}
});
}
});

View File

@ -1,339 +0,0 @@
{
"actions": [],
"autoname": "LM-REP-.####",
"creation": "2022-01-25 10:30:02.767941",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"against_loan",
"applicant_type",
"applicant",
"loan_type",
"column_break_3",
"company",
"posting_date",
"clearance_date",
"rate_of_interest",
"is_term_loan",
"payment_details_section",
"due_date",
"pending_principal_amount",
"interest_payable",
"payable_amount",
"column_break_9",
"shortfall_amount",
"payable_principal_amount",
"penalty_amount",
"amount_paid",
"accounting_dimensions_section",
"cost_center",
"references_section",
"reference_number",
"column_break_21",
"reference_date",
"principal_amount_paid",
"total_penalty_paid",
"total_interest_paid",
"repayment_details",
"amended_from",
"accounting_details_section",
"payment_account",
"penalty_income_account",
"column_break_36",
"loan_account"
],
"fields": [
{
"fieldname": "against_loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Against Loan",
"options": "Loan",
"reqd": 1
},
{
"fieldname": "posting_date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"fieldname": "payment_details_section",
"fieldtype": "Section Break",
"label": "Payment Details"
},
{
"fieldname": "penalty_amount",
"fieldtype": "Currency",
"label": "Penalty Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "interest_payable",
"fieldtype": "Currency",
"label": "Interest Payable",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Applicant",
"options": "applicant_type",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "against_loan.loan_type",
"fieldname": "loan_type",
"fieldtype": "Link",
"label": "Loan Type",
"options": "Loan Type",
"read_only": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "payable_amount",
"fieldtype": "Currency",
"label": "Payable Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"bold": 1,
"fieldname": "amount_paid",
"fieldtype": "Currency",
"label": "Amount Paid",
"non_negative": 1,
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Repayment",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "pending_principal_amount",
"fieldtype": "Currency",
"label": "Pending Principal Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"fetch_from": "against_loan.is_term_loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan",
"read_only": 1
},
{
"depends_on": "eval:doc.payment_type==\"Loan Closure\" || doc.is_term_loan",
"fieldname": "payable_principal_amount",
"fieldtype": "Currency",
"label": "Payable Principal Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "references_section",
"fieldtype": "Section Break",
"label": "Payment References"
},
{
"fieldname": "reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Reference Date"
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"default": "0.0",
"fieldname": "principal_amount_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Principal Amount Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "against_loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"read_only": 1
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{
"fieldname": "repayment_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Repayment Details",
"options": "Loan Repayment Detail"
},
{
"fieldname": "total_interest_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Total Interest Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fetch_from": "loan_type.rate_of_interest",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate Of Interest",
"read_only": 1
},
{
"fieldname": "shortfall_amount",
"fieldtype": "Currency",
"label": "Shortfall Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_penalty_paid",
"fieldtype": "Currency",
"hidden": 1,
"label": "Total Penalty Paid",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "clearance_date",
"fieldtype": "Date",
"label": "Clearance Date",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "against_loan.payment_account",
"fetch_if_empty": 1,
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Repayment Account",
"options": "Account"
},
{
"fieldname": "column_break_36",
"fieldtype": "Column Break"
},
{
"fetch_from": "against_loan.loan_account",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"read_only": 1
},
{
"fetch_from": "against_loan.penalty_income_account",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"hidden": 1,
"label": "Penalty Income Account",
"options": "Account"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-08-04 17:13:51.964203",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,777 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import (
get_last_accrual_date,
get_per_day_interest,
)
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
update_shortfall_status,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_demand_loans,
)
class LoanRepayment(AccountsController):
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date)
self.set_missing_values(amounts)
self.check_future_entries()
self.validate_amount()
self.allocate_amounts(amounts)
def before_submit(self):
self.book_unaccrued_interest()
def on_submit(self):
self.update_paid_amount()
self.update_repayment_schedule()
self.make_gl_entries()
def on_cancel(self):
self.check_future_accruals()
self.update_repayment_schedule(cancel=1)
self.mark_as_unpaid()
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if not self.posting_date:
self.posting_date = get_datetime()
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.interest_payable:
self.interest_payable = flt(amounts["interest_amount"], precision)
if not self.penalty_amount:
self.penalty_amount = flt(amounts["penalty_amount"], precision)
if not self.pending_principal_amount:
self.pending_principal_amount = flt(amounts["pending_principal_amount"], precision)
if not self.payable_principal_amount and self.is_term_loan:
self.payable_principal_amount = flt(amounts["payable_principal_amount"], precision)
if not self.payable_amount:
self.payable_amount = flt(amounts["payable_amount"], precision)
shortfall_amount = flt(
frappe.db.get_value(
"Loan Security Shortfall", {"loan": self.against_loan, "status": "Pending"}, "shortfall_amount"
)
)
if shortfall_amount:
self.shortfall_amount = shortfall_amount
if amounts.get("due_date"):
self.due_date = amounts.get("due_date")
def check_future_entries(self):
future_repayment_date = frappe.db.get_value(
"Loan Repayment",
{"posting_date": (">", self.posting_date), "docstatus": 1, "against_loan": self.against_loan},
"posting_date",
)
if future_repayment_date:
frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date)))
def validate_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero"))
def book_unaccrued_interest(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
if not self.is_term_loan:
# get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
# get posting date upto which interest has to be accrued
per_day_interest = get_per_day_interest(
self.pending_principal_amount, self.rate_of_interest, self.posting_date
)
no_of_days = (
flt(flt(self.total_interest_paid - self.interest_payable, precision) / per_day_interest, 0)
- 1
)
posting_date = add_days(last_accrual_date, no_of_days)
# book excess interest paid
process = process_loan_interest_accrual_for_demand_loans(
posting_date=posting_date, loan=self.against_loan, accrual_type="Repayment"
)
# get loan interest accrual to update paid amount
lia = frappe.db.get_value(
"Loan Interest Accrual",
{"process_loan_interest_accrual": process},
["name", "interest_amount", "payable_principal_amount"],
as_dict=1,
)
if lia:
self.append(
"repayment_details",
{
"loan_interest_accrual": lia.name,
"paid_interest_amount": flt(self.total_interest_paid - self.interest_payable, precision),
"paid_principal_amount": 0.0,
"accrual_type": "Repayment",
},
)
def update_paid_amount(self):
loan = frappe.get_value(
"Loan",
self.against_loan,
[
"total_amount_paid",
"total_principal_paid",
"status",
"is_secured_loan",
"total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"loan_amount",
"disbursed_amount",
"total_interest_payable",
"written_off_amount",
],
as_dict=1,
)
loan.update(
{
"total_amount_paid": loan.total_amount_paid + self.amount_paid,
"total_principal_paid": loan.total_principal_paid + self.principal_amount_paid,
}
)
pending_principal_amount = get_pending_principal_amount(loan)
if not loan.is_secured_loan and pending_principal_amount <= 0:
loan.update({"status": "Loan Closure Requested"})
for payment in self.repayment_details:
frappe.db.sql(
""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
(
flt(payment.paid_principal_amount),
flt(payment.paid_interest_amount),
payment.loan_interest_accrual,
),
)
frappe.db.sql(
""" UPDATE `tabLoan`
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
WHERE name = %s """,
(loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan),
)
update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self):
loan = frappe.get_value(
"Loan",
self.against_loan,
[
"total_amount_paid",
"total_principal_paid",
"status",
"is_secured_loan",
"total_payment",
"loan_amount",
"disbursed_amount",
"total_interest_payable",
"written_off_amount",
],
as_dict=1,
)
no_of_repayments = len(self.repayment_details)
loan.update(
{
"total_amount_paid": loan.total_amount_paid - self.amount_paid,
"total_principal_paid": loan.total_principal_paid - self.principal_amount_paid,
}
)
if loan.status == "Loan Closure Requested":
if loan.disbursed_amount >= loan.loan_amount:
loan["status"] = "Disbursed"
else:
loan["status"] = "Partially Disbursed"
for payment in self.repayment_details:
frappe.db.sql(
""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
paid_interest_amount = `paid_interest_amount` - %s
WHERE name = %s""",
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual),
)
# Cancel repayment interest accrual
# checking idx as a preventive measure, repayment accrual will always be the last entry
if payment.accrual_type == "Repayment" and payment.idx == no_of_repayments:
lia_doc = frappe.get_doc("Loan Interest Accrual", payment.loan_interest_accrual)
lia_doc.cancel()
frappe.db.sql(
""" UPDATE `tabLoan`
SET total_amount_paid = %s, total_principal_paid = %s, status = %s
WHERE name = %s """,
(loan.total_amount_paid, loan.total_principal_paid, loan.status, self.against_loan),
)
def check_future_accruals(self):
future_accrual_date = frappe.db.get_value(
"Loan Interest Accrual",
{"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan},
"posting_date",
)
if future_accrual_date:
frappe.throw(
"Cannot cancel. Interest accruals already processed till {0}".format(
get_datetime(future_accrual_date)
)
)
def update_repayment_schedule(self, cancel=0):
if self.is_term_loan and self.principal_amount_paid > self.payable_principal_amount:
regenerate_repayment_schedule(self.against_loan, cancel)
def allocate_amounts(self, repayment_details):
precision = cint(frappe.db.get_default("currency_precision")) or 2
self.set("repayment_details", [])
self.principal_amount_paid = 0
self.total_penalty_paid = 0
interest_paid = self.amount_paid
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
self.principal_amount_paid = self.shortfall_amount
elif self.shortfall_amount:
self.principal_amount_paid = self.amount_paid
interest_paid -= self.principal_amount_paid
if interest_paid > 0:
if self.penalty_amount and interest_paid > self.penalty_amount:
self.total_penalty_paid = flt(self.penalty_amount, precision)
elif self.penalty_amount:
self.total_penalty_paid = flt(interest_paid, precision)
interest_paid -= self.total_penalty_paid
if self.is_term_loan:
interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
self.allocate_principal_amount_for_term_loans(interest_paid, repayment_details, updated_entries)
else:
interest_paid, updated_entries = self.allocate_interest_amount(interest_paid, repayment_details)
self.allocate_excess_payment_for_demand_loans(interest_paid, repayment_details)
def allocate_interest_amount(self, interest_paid, repayment_details):
updated_entries = {}
self.total_interest_paid = 0
idx = 1
if interest_paid > 0:
for lia, amounts in repayment_details.get("pending_accrual_entries", []).items():
interest_amount = 0
if amounts["interest_amount"] <= interest_paid:
interest_amount = amounts["interest_amount"]
self.total_interest_paid += interest_amount
interest_paid -= interest_amount
elif interest_paid:
if interest_paid >= amounts["interest_amount"]:
interest_amount = amounts["interest_amount"]
self.total_interest_paid += interest_amount
interest_paid = 0
else:
interest_amount = interest_paid
self.total_interest_paid += interest_amount
interest_paid = 0
if interest_amount:
self.append(
"repayment_details",
{
"loan_interest_accrual": lia,
"paid_interest_amount": interest_amount,
"paid_principal_amount": 0,
},
)
updated_entries[lia] = idx
idx += 1
return interest_paid, updated_entries
def allocate_principal_amount_for_term_loans(
self, interest_paid, repayment_details, updated_entries
):
if interest_paid > 0:
for lia, amounts in repayment_details.get("pending_accrual_entries", []).items():
paid_principal = 0
if amounts["payable_principal_amount"] <= interest_paid:
paid_principal = amounts["payable_principal_amount"]
self.principal_amount_paid += paid_principal
interest_paid -= paid_principal
elif interest_paid:
if interest_paid >= amounts["payable_principal_amount"]:
paid_principal = amounts["payable_principal_amount"]
self.principal_amount_paid += paid_principal
interest_paid = 0
else:
paid_principal = interest_paid
self.principal_amount_paid += paid_principal
interest_paid = 0
if updated_entries.get(lia):
idx = updated_entries.get(lia)
self.get("repayment_details")[idx - 1].paid_principal_amount += paid_principal
else:
self.append(
"repayment_details",
{
"loan_interest_accrual": lia,
"paid_interest_amount": 0,
"paid_principal_amount": paid_principal,
},
)
if interest_paid > 0:
self.principal_amount_paid += interest_paid
def allocate_excess_payment_for_demand_loans(self, interest_paid, repayment_details):
if repayment_details["unaccrued_interest"] and interest_paid > 0:
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details["unaccrued_interest"]:
interest_paid -= repayment_details["unaccrued_interest"]
self.total_interest_paid += repayment_details["unaccrued_interest"]
else:
# get no of days for which interest can be paid
per_day_interest = get_per_day_interest(
self.pending_principal_amount, self.rate_of_interest, self.posting_date
)
no_of_days = cint(interest_paid / per_day_interest)
self.total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
if interest_paid > 0:
self.principal_amount_paid += interest_paid
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
self.shortfall_amount, self.against_loan
)
elif self.shortfall_amount:
remarks = "Shortfall repayment of {0} against loan {1}".format(
self.shortfall_amount, self.against_loan
)
else:
remarks = "Repayment against loan " + self.against_loan
if self.reference_number:
remarks += " with reference no. {}".format(self.reference_number)
if hasattr(self, "repay_from_salary") and self.repay_from_salary:
payment_account = self.payroll_payable_account
else:
payment_account = self.payment_account
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict(
{
"account": self.loan_account,
"against": payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date),
}
)
)
gle_map.append(
self.get_gl_dict(
{
"account": self.penalty_income_account,
"against": self.loan_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gle_map.append(
self.get_gl_dict(
{
"account": payment_account,
"against": self.loan_account + ", " + self.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
gle_map.append(
self.get_gl_dict(
{
"account": self.loan_account,
"party_type": self.applicant_type,
"party": self.applicant,
"against": payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _(remarks),
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date),
}
)
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(
loan,
applicant,
company,
posting_date,
loan_type,
payment_type,
interest_payable,
payable_principal_amount,
amount_paid,
penalty_amount=None,
payroll_payable_account=None,
):
lr = frappe.get_doc(
{
"doctype": "Loan Repayment",
"against_loan": loan,
"payment_type": payment_type,
"company": company,
"posting_date": posting_date,
"applicant": applicant,
"penalty_amount": penalty_amount,
"interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid,
"loan_type": loan_type,
"payroll_payable_account": payroll_payable_account,
}
).insert()
return lr
def get_accrued_interest_entries(against_loan, posting_date=None):
if not posting_date:
posting_date = getdate()
precision = cint(frappe.db.get_default("currency_precision")) or 2
unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
payable_principal_amount - paid_principal_amount as payable_principal_amount,
accrual_type
FROM
`tabLoan Interest Accrual`
WHERE
loan = %s
AND posting_date <= %s
AND (interest_amount - paid_interest_amount > 0 OR
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
ORDER BY posting_date
""",
(against_loan, posting_date),
as_dict=1,
)
# Skip entries with zero interest amount & payable principal amount
unpaid_accrued_entries = [
d
for d in unpaid_accrued_entries
if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0
]
return unpaid_accrued_entries
def get_penalty_details(against_loan):
penalty_details = frappe.db.sql(
"""
SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount
FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment`
where against_loan = %s) and docstatus = 1 and against_loan = %s
""",
(against_loan, against_loan),
)
if penalty_details:
return penalty_details[0][0], flt(penalty_details[0][1])
else:
return None, 0
def regenerate_repayment_schedule(loan, cancel=0):
from erpnext.loan_management.doctype.loan.loan import (
add_single_month,
get_monthly_repayment_amount,
)
loan_doc = frappe.get_doc("Loan", loan)
next_accrual_date = None
accrued_entries = 0
last_repayment_amount = None
last_balance_amount = None
for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued:
next_accrual_date = term.payment_date
loan_doc.remove(term)
else:
accrued_entries += 1
if last_repayment_amount is None:
last_repayment_amount = term.total_payment
if last_balance_amount is None:
last_balance_amount = term.balance_loan_amount
loan_doc.save()
balance_amount = get_pending_principal_amount(loan_doc)
if loan_doc.repayment_method == "Repay Fixed Amount per Period":
monthly_repayment_amount = flt(
balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
)
else:
repayment_period = loan_doc.repayment_periods - accrued_entries
if not cancel and repayment_period > 0:
monthly_repayment_amount = get_monthly_repayment_amount(
balance_amount, loan_doc.rate_of_interest, repayment_period
)
else:
monthly_repayment_amount = last_repayment_amount
balance_amount = last_balance_amount
payment_date = next_accrual_date
while balance_amount > 0:
interest_amount = flt(balance_amount * flt(loan_doc.rate_of_interest) / (12 * 100))
principal_amount = monthly_repayment_amount - interest_amount
balance_amount = flt(balance_amount + interest_amount - monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
total_payment = principal_amount + interest_amount
loan_doc.append(
"repayment_schedule",
{
"payment_date": payment_date,
"principal_amount": principal_amount,
"interest_amount": interest_amount,
"total_payment": total_payment,
"balance_loan_amount": balance_amount,
},
)
next_payment_date = add_single_month(payment_date)
payment_date = next_payment_date
loan_doc.save()
def get_pending_principal_amount(loan):
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
pending_principal_amount = (
flt(loan.total_payment)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
else:
pending_principal_amount = (
flt(loan.disbursed_amount)
+ flt(loan.debit_adjustment_amount)
- flt(loan.credit_adjustment_amount)
- flt(loan.total_principal_paid)
- flt(loan.total_interest_payable)
- flt(loan.written_off_amount)
+ flt(loan.refund_amount)
)
return pending_principal_amount
# This function returns the amounts that are payable at the time of loan repayment based on posting date
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
def get_amounts(amounts, against_loan, posting_date):
precision = cint(frappe.db.get_default("currency_precision")) or 2
against_loan_doc = frappe.get_doc("Loan", against_loan)
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date)
computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan)
pending_accrual_entries = {}
total_pending_interest = 0
penalty_amount = 0
payable_principal_amount = 0
final_due_date = ""
due_date = ""
for entry in accrued_interest_entries:
# Loan repayment due date is one day after the loan interest is accrued
# no of late days are calculated based on loan repayment posting date
# and if no_of_late days are positive then penalty is levied
due_date = add_days(entry.posting_date, 1)
due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days)
# Consider one day after already calculated penalty
if computed_penalty_date and getdate(computed_penalty_date) >= due_date_after_grace_period:
due_date_after_grace_period = add_days(computed_penalty_date, 1)
no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1
if (
no_of_late_days > 0
and (
not (hasattr(against_loan_doc, "repay_from_salary") and against_loan_doc.repay_from_salary)
)
and entry.accrual_type == "Regular"
):
penalty_amount += (
entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days
)
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
pending_accrual_entries.setdefault(
entry.name,
{
"interest_amount": flt(entry.interest_amount, precision),
"payable_principal_amount": flt(entry.payable_principal_amount, precision),
},
)
if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
pending_principal_amount = get_pending_principal_amount(against_loan_doc)
unaccrued_interest = 0
if due_date:
pending_days = date_diff(posting_date, due_date) + 1
else:
last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
pending_days = date_diff(posting_date, last_accrual_date) + 1
if pending_days > 0:
principal_amount = flt(pending_principal_amount, precision)
per_day_interest = get_per_day_interest(
principal_amount, loan_type_details.rate_of_interest, posting_date
)
unaccrued_interest += pending_days * per_day_interest
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
amounts["interest_amount"] = flt(total_pending_interest, precision)
amounts["penalty_amount"] = flt(penalty_amount + pending_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"] = flt(unaccrued_interest, precision)
amounts["written_off_amount"] = flt(against_loan_doc.written_off_amount, precision)
if final_due_date:
amounts["due_date"] = final_due_date
return amounts
@frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type=""):
amounts = {
"penalty_amount": 0.0,
"interest_amount": 0.0,
"pending_principal_amount": 0.0,
"payable_principal_amount": 0.0,
"payable_amount": 0.0,
"unaccrued_interest": 0.0,
"due_date": "",
}
amounts = get_amounts(amounts, against_loan, posting_date)
# update values for closure
if payment_type == "Loan Closure":
amounts["payable_principal_amount"] = amounts["pending_principal_amount"]
amounts["interest_amount"] += amounts["unaccrued_interest"]
amounts["payable_amount"] = (
amounts["payable_principal_amount"] + amounts["interest_amount"] + amounts["penalty_amount"]
)
return amounts

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanRepayment(unittest.TestCase):
pass

View File

@ -1,53 +0,0 @@
{
"actions": [],
"creation": "2020-04-15 18:31:54.026923",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_interest_accrual",
"paid_principal_amount",
"paid_interest_amount",
"accrual_type"
],
"fields": [
{
"fieldname": "loan_interest_accrual",
"fieldtype": "Link",
"label": "Loan Interest Accrual",
"options": "Loan Interest Accrual"
},
{
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
},
{
"fetch_from": "loan_interest_accrual.accrual_type",
"fetch_if_empty": 1,
"fieldname": "accrual_type",
"fieldtype": "Select",
"label": "Accrual Type",
"options": "Regular\nRepayment\nDisbursement"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-10-23 08:09:18.267030",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,10 +0,0 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LoanRepaymentDetail(Document):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Security', {
// refresh: function(frm) {
// }
});

View File

@ -1,105 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2019-09-02 15:07:08.885593",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_security_name",
"haircut",
"loan_security_code",
"column_break_3",
"loan_security_type",
"unit_of_measure",
"disabled"
],
"fields": [
{
"fieldname": "loan_security_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Loan Security Name",
"reqd": 1,
"unique": 1
},
{
"fetch_from": "loan_security_type.haircut",
"fetch_if_empty": 1,
"fieldname": "haircut",
"fieldtype": "Percent",
"label": "Haircut %"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "loan_security_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Security Type",
"options": "Loan Security Type",
"reqd": 1
},
{
"fieldname": "loan_security_code",
"fieldtype": "Data",
"label": "Loan Security Code",
"unique": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fetch_from": "loan_security_type.unit_of_measure",
"fieldname": "unit_of_measure",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Unit Of Measure",
"options": "UOM",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-10-26 07:34:48.601766",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
}
],
"search_fields": "loan_security_code",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,11 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LoanSecurity(Document):
def autoname(self):
self.name = self.loan_security_name

View File

@ -1,8 +0,0 @@
def get_data():
return {
"fieldname": "loan_security",
"transactions": [
{"items": ["Loan Application", "Loan Security Price"]},
{"items": ["Loan Security Pledge", "Loan Security Unpledge"]},
],
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurity(unittest.TestCase):
pass

View File

@ -1,43 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
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)));
let amount = 0;
let maximum_amount = 0;
$.each(frm.doc.securities || [], function(i, item){
amount += item.amount;
maximum_amount += item.post_haircut_amount;
});
frm.set_value('total_security_value', amount);
frm.set_value('maximum_loan_value', maximum_amount);
}
});
frappe.ui.form.on("Pledge", {
loan_security: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.loan_security) {
frappe.call({
method: "erpnext.loan_management.doctype.loan_security_price.loan_security_price.get_loan_security_price",
args: {
loan_security: row.loan_security
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'loan_security_price', r.message);
frm.events.calculate_amounts(frm, cdt, cdn);
}
});
}
},
qty: function(frm, cdt, cdn) {
frm.events.calculate_amounts(frm, cdt, cdn);
},
});

View File

@ -1,245 +0,0 @@
{
"actions": [],
"autoname": "LS-.{applicant}.-.#####",
"creation": "2019-08-29 18:48:51.371674",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_details_section",
"applicant_type",
"applicant",
"loan",
"loan_application",
"column_break_3",
"company",
"pledge_time",
"status",
"loan_security_details_section",
"securities",
"section_break_10",
"total_security_value",
"column_break_11",
"maximum_loan_value",
"more_information_section",
"reference_no",
"column_break_18",
"description",
"amended_from"
],
"fields": [
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Security Pledge",
"print_hide": 1,
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fetch_from": "loan_application.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan",
"fieldtype": "Link",
"label": "Loan",
"options": "Loan",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_application",
"fieldtype": "Link",
"label": "Loan Application",
"options": "Loan Application",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "total_security_value",
"fieldtype": "Currency",
"label": "Total Security Value",
"options": "Company:company:default_currency",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "maximum_loan_value",
"fieldtype": "Currency",
"label": "Maximum Loan Value",
"options": "Company:company:default_currency",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "loan_details_section",
"fieldtype": "Section Break",
"label": "Loan Details",
"show_days": 1,
"show_seconds": 1
},
{
"default": "Requested",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Requested\nUnpledged\nPledged\nPartially Pledged\nCancelled",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "pledge_time",
"fieldtype": "Datetime",
"label": "Pledge Time",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "securities",
"fieldtype": "Table",
"label": "Securities",
"options": "Pledge",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"label": "Totals",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No",
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"show_days": 1,
"show_seconds": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-29 17:15:16.082256",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Pledge",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"search_fields": "applicant",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,111 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, now_datetime
from erpnext.loan_management.doctype.loan_security_price.loan_security_price import (
get_loan_security_price,
)
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
update_shortfall_status,
)
class LoanSecurityPledge(Document):
def validate(self):
self.set_pledge_amount()
self.validate_duplicate_securities()
self.validate_loan_security_type()
def on_submit(self):
if self.loan:
self.db_set("status", "Pledged")
self.db_set("pledge_time", now_datetime())
update_shortfall_status(self.loan, self.total_security_value)
update_loan(self.loan, self.maximum_loan_value)
def on_cancel(self):
if self.loan:
self.db_set("status", "Cancelled")
self.db_set("pledge_time", None)
update_loan(self.loan, self.maximum_loan_value, cancel=1)
def validate_duplicate_securities(self):
security_list = []
for security in self.securities:
if security.loan_security not in security_list:
security_list.append(security.loan_security)
else:
frappe.throw(
_("Loan Security {0} added multiple times").format(frappe.bold(security.loan_security))
)
def validate_loan_security_type(self):
existing_pledge = ""
if self.loan:
existing_pledge = frappe.db.get_value(
"Loan Security Pledge", {"loan": self.loan, "docstatus": 1}, ["name"]
)
if existing_pledge:
loan_security_type = frappe.db.get_value(
"Pledge", {"parent": existing_pledge}, ["loan_security_type"]
)
else:
loan_security_type = self.securities[0].loan_security_type
ltv_ratio_map = frappe._dict(
frappe.get_all("Loan Security Type", fields=["name", "loan_to_value_ratio"], as_list=1)
)
ltv_ratio = ltv_ratio_map.get(loan_security_type)
for security in self.securities:
if ltv_ratio_map.get(security.loan_security_type) != ltv_ratio:
frappe.throw(_("Loan Securities with different LTV ratio cannot be pledged against one loan"))
def set_pledge_amount(self):
total_security_value = 0
maximum_loan_value = 0
for pledge in self.securities:
if not pledge.qty and not pledge.amount:
frappe.throw(_("Qty or Amount is mandatory for loan security!"))
if not (self.loan_application and pledge.loan_security_price):
pledge.loan_security_price = get_loan_security_price(pledge.loan_security)
if not pledge.qty:
pledge.qty = cint(pledge.amount / pledge.loan_security_price)
pledge.amount = pledge.qty * pledge.loan_security_price
pledge.post_haircut_amount = cint(pledge.amount - (pledge.amount * pledge.haircut / 100))
total_security_value += pledge.amount
maximum_loan_value += pledge.post_haircut_amount
self.total_security_value = total_security_value
self.maximum_loan_value = maximum_loan_value
def update_loan(loan, maximum_value_against_pledge, cancel=0):
maximum_loan_value = frappe.db.get_value("Loan", {"name": loan}, ["maximum_loan_amount"])
if cancel:
frappe.db.sql(
""" UPDATE `tabLoan` SET maximum_loan_amount=%s
WHERE name=%s""",
(maximum_loan_value - maximum_value_against_pledge, loan),
)
else:
frappe.db.sql(
""" UPDATE `tabLoan` SET maximum_loan_amount=%s, is_secured_loan=1
WHERE name=%s""",
(maximum_loan_value + maximum_value_against_pledge, loan),
)

View File

@ -1,15 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['Loan Security Pledge'] = {
add_fields: ["status"],
get_indicator: function(doc) {
var status_color = {
"Unpledged": "orange",
"Pledged": "green",
"Partially Pledged": "green"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
}
};

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurityPledge(unittest.TestCase):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Security Price', {
// refresh: function(frm) {
// }
});

View File

@ -1,129 +0,0 @@
{
"actions": [],
"autoname": "LM-LSP-.####",
"creation": "2019-09-03 18:20:31.382887",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_security",
"loan_security_name",
"loan_security_type",
"column_break_2",
"uom",
"section_break_4",
"loan_security_price",
"section_break_6",
"valid_from",
"column_break_8",
"valid_upto"
],
"fields": [
{
"fieldname": "loan_security",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Security",
"options": "Loan Security",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fetch_from": "loan_security.unit_of_measure",
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "loan_security_price",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Loan Security Price",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "valid_from",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Valid From",
"reqd": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "valid_upto",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Valid Upto",
"reqd": 1
},
{
"fetch_from": "loan_security.loan_security_type",
"fieldname": "loan_security_type",
"fieldtype": "Link",
"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": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Price",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,55 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_datetime
class LoanSecurityPrice(Document):
def validate(self):
self.validate_dates()
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) """,
(
self.loan_security,
self.name,
self.valid_from,
self.valid_upto,
self.valid_from,
self.valid_upto,
),
)
if existing_loan_security:
frappe.throw(_("Loan Security Price overlapping with {0}").format(existing_loan_security[0][0]))
@frappe.whitelist()
def get_loan_security_price(loan_security, valid_time=None):
if not valid_time:
valid_time = get_datetime()
loan_security_price = frappe.db.get_value(
"Loan Security Price",
{
"loan_security": loan_security,
"valid_from": ("<=", valid_time),
"valid_upto": (">=", valid_time),
},
"loan_security_price",
)
if not loan_security_price:
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security)))
else:
return loan_security_price

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurityPrice(unittest.TestCase):
pass

View File

@ -1,25 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Security Shortfall', {
refresh: function(frm) {
frm.add_custom_button(__("Add Loan Security"), function() {
frm.trigger('shortfall_action');
});
},
shortfall_action: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.add_security",
args: {
'loan': frm.doc.loan
},
callback: function(r) {
if (r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
}
}
});
}
});

View File

@ -1,159 +0,0 @@
{
"actions": [],
"autoname": "LM-LSS-.#####",
"creation": "2019-09-06 11:33:34.709540",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan",
"applicant_type",
"applicant",
"status",
"column_break_3",
"shortfall_time",
"section_break_3",
"loan_amount",
"shortfall_amount",
"column_break_8",
"security_value",
"shortfall_percentage",
"section_break_8",
"process_loan_security_shortfall"
],
"fields": [
{
"fieldname": "loan",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Loan ",
"options": "Loan",
"read_only": 1
},
{
"fieldname": "loan_amount",
"fieldtype": "Currency",
"label": "Loan Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "security_value",
"fieldtype": "Currency",
"label": "Security Value ",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "shortfall_amount",
"fieldtype": "Currency",
"label": "Shortfall Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"description": "America/New_York",
"fieldname": "shortfall_time",
"fieldtype": "Datetime",
"label": "Shortfall Time",
"read_only": 1
},
{
"default": "Pending",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "\nPending\nCompleted",
"read_only": 1
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "process_loan_security_shortfall",
"fieldtype": "Link",
"label": "Process Loan Security Shortfall",
"options": "Process Loan Security Shortfall",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"fieldname": "shortfall_percentage",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Shortfall Percentage",
"read_only": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer"
},
{
"fetch_from": "loan.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Applicant",
"options": "applicant_type"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-06-30 11:57:09.378089",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Shortfall",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -1,175 +0,0 @@
# Copyright (c) 2019, 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 flt, get_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import (
get_pledged_security_qty,
)
class LoanSecurityShortfall(Document):
pass
def update_shortfall_status(loan, security_value, on_cancel=0):
loan_security_shortfall = frappe.db.get_value(
"Loan Security Shortfall",
{"loan": loan, "status": "Pending"},
["name", "shortfall_amount"],
as_dict=1,
)
if not loan_security_shortfall:
return
if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value(
"Loan Security Shortfall",
loan_security_shortfall.name,
{
"status": "Completed",
"shortfall_amount": loan_security_shortfall.shortfall_amount,
"shortfall_percentage": 0,
},
)
else:
frappe.db.set_value(
"Loan Security Shortfall",
loan_security_shortfall.name,
"shortfall_amount",
loan_security_shortfall.shortfall_amount - security_value,
)
@frappe.whitelist()
def add_security(loan):
loan_details = frappe.db.get_value(
"Loan", loan, ["applicant", "company", "applicant_type"], as_dict=1
)
loan_security_pledge = frappe.new_doc("Loan Security Pledge")
loan_security_pledge.loan = loan
loan_security_pledge.company = loan_details.company
loan_security_pledge.applicant_type = loan_details.applicant_type
loan_security_pledge.applicant = loan_details.applicant
return loan_security_pledge.as_dict()
def check_for_ltv_shortfall(process_loan_security_shortfall):
update_time = get_datetime()
loan_security_price_map = frappe._dict(
frappe.get_all(
"Loan Security Price",
fields=["loan_security", "loan_security_price"],
filters={"valid_from": ("<=", update_time), "valid_upto": (">=", update_time)},
as_list=1,
)
)
loans = frappe.get_all(
"Loan",
fields=[
"name",
"loan_amount",
"total_principal_paid",
"total_payment",
"total_interest_payable",
"disbursed_amount",
"status",
],
filters={"status": ("in", ["Disbursed", "Partially Disbursed"]), "is_secured_loan": 1},
)
loan_shortfall_map = frappe._dict(
frappe.get_all(
"Loan Security Shortfall", fields=["loan", "name"], filters={"status": "Pending"}, as_list=1
)
)
loan_security_map = {}
for loan in loans:
if loan.status == "Disbursed":
outstanding_amount = (
flt(loan.total_payment) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid)
)
else:
outstanding_amount = (
flt(loan.disbursed_amount) - flt(loan.total_interest_payable) - flt(loan.total_principal_paid)
)
pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = 0.0
security_value = 0.0
for security, qty in pledged_securities.items():
if not ltv_ratio:
ltv_ratio = get_ltv_ratio(security)
security_value += flt(loan_security_price_map.get(security)) * flt(qty)
current_ratio = (outstanding_amount / security_value) * 100 if security_value else 0
if current_ratio > ltv_ratio:
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
create_loan_security_shortfall(
loan.name,
outstanding_amount,
security_value,
shortfall_amount,
current_ratio,
process_loan_security_shortfall,
)
elif loan_shortfall_map.get(loan.name):
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
if shortfall_amount <= 0:
shortfall = loan_shortfall_map.get(loan.name)
update_pending_shortfall(shortfall)
def create_loan_security_shortfall(
loan,
loan_amount,
security_value,
shortfall_amount,
shortfall_ratio,
process_loan_security_shortfall,
):
existing_shortfall = frappe.db.get_value(
"Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name"
)
if existing_shortfall:
ltv_shortfall = frappe.get_doc("Loan Security Shortfall", existing_shortfall)
else:
ltv_shortfall = frappe.new_doc("Loan Security Shortfall")
ltv_shortfall.loan = loan
ltv_shortfall.shortfall_time = get_datetime()
ltv_shortfall.loan_amount = loan_amount
ltv_shortfall.security_value = security_value
ltv_shortfall.shortfall_amount = shortfall_amount
ltv_shortfall.shortfall_percentage = shortfall_ratio
ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall
ltv_shortfall.save()
def get_ltv_ratio(loan_security):
loan_security_type = frappe.db.get_value("Loan Security", loan_security, "loan_security_type")
ltv_ratio = frappe.db.get_value("Loan Security Type", loan_security_type, "loan_to_value_ratio")
return ltv_ratio
def update_pending_shortfall(shortfall):
# Get all pending loan security shortfall
frappe.db.set_value(
"Loan Security Shortfall",
shortfall,
{"status": "Completed", "shortfall_amount": 0, "shortfall_percentage": 0},
)

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurityShortfall(unittest.TestCase):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Security Type', {
// refresh: function(frm) {
// },
});

View File

@ -1,92 +0,0 @@
{
"actions": [],
"autoname": "field:loan_security_type",
"creation": "2019-08-29 18:46:07.322056",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_security_type",
"unit_of_measure",
"haircut",
"column_break_5",
"loan_to_value_ratio",
"disabled"
],
"fields": [
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "loan_security_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Loan Security Type",
"reqd": 1,
"unique": 1
},
{
"description": "Haircut percentage is the percentage difference between market value of the Loan Security and the value ascribed to that Loan Security when used as collateral for that loan.",
"fieldname": "haircut",
"fieldtype": "Percent",
"label": "Haircut %"
},
{
"fieldname": "unit_of_measure",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Unit Of Measure",
"options": "UOM",
"reqd": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"description": "Loan To Value Ratio expresses the ratio of the loan amount to the value of the security pledged. A loan security shortfall will be triggered if this falls below the specified value for any loan ",
"fieldname": "loan_to_value_ratio",
"fieldtype": "Percent",
"label": "Loan To Value Ratio"
}
],
"links": [],
"modified": "2020-05-16 09:38:45.988080",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,10 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LoanSecurityType(Document):
pass

View File

@ -1,8 +0,0 @@
def get_data():
return {
"fieldname": "loan_security_type",
"transactions": [
{"items": ["Loan Security", "Loan Security Price"]},
{"items": ["Loan Security Pledge", "Loan Security Unpledge"]},
],
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurityType(unittest.TestCase):
pass

View File

@ -1,11 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Security Unpledge', {
refresh: function(frm) {
if (frm.doc.docstatus == 1 && frm.doc.status == 'Approved') {
frm.set_df_property('status', 'read_only', 1);
}
}
});

View File

@ -1,183 +0,0 @@
{
"actions": [],
"autoname": "LSU-.{applicant}.-.#####",
"creation": "2019-09-21 13:23:16.117028",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_details_section",
"loan",
"applicant_type",
"applicant",
"column_break_3",
"company",
"unpledge_time",
"status",
"loan_security_details_section",
"securities",
"more_information_section",
"reference_no",
"column_break_13",
"description",
"amended_from"
],
"fields": [
{
"fieldname": "loan_details_section",
"fieldtype": "Section Break",
"label": "Loan Details"
},
{
"fetch_from": "loan_application.applicant",
"fieldname": "applicant",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Applicant",
"options": "applicant_type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "loan",
"fieldtype": "Link",
"label": "Loan",
"options": "Loan",
"reqd": 1
},
{
"allow_on_submit": 1,
"default": "Requested",
"depends_on": "eval:doc.docstatus == 1",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Requested\nApproved",
"permlevel": 1
},
{
"fieldname": "unpledge_time",
"fieldtype": "Datetime",
"label": "Unpledge Time",
"read_only": 1
},
{
"fieldname": "loan_security_details_section",
"fieldtype": "Section Break",
"label": "Loan Security Details"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Security Unpledge",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "securities",
"fieldtype": "Table",
"label": "Securities",
"options": "Unpledge",
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fetch_from": "loan.applicant_type",
"fieldname": "applicant_type",
"fieldtype": "Select",
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:12:01.401744",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Unpledge",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"search_fields": "applicant",
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,179 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_datetime, getdate
class LoanSecurityUnpledge(Document):
def validate(self):
self.validate_duplicate_securities()
self.validate_unpledge_qty()
def on_cancel(self):
self.update_loan_status(cancel=1)
self.db_set("status", "Requested")
def validate_duplicate_securities(self):
security_list = []
for d in self.securities:
if d.loan_security not in security_list:
security_list.append(d.loan_security)
else:
frappe.throw(
_("Row {0}: Loan Security {1} added multiple times").format(
d.idx, frappe.bold(d.loan_security)
)
)
def validate_unpledge_qty(self):
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import (
get_ltv_ratio,
)
pledge_qty_map = get_pledged_security_qty(self.loan)
ltv_ratio_map = frappe._dict(
frappe.get_all("Loan Security Type", 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_details = frappe.get_value(
"Loan",
self.loan,
[
"total_payment",
"debit_adjustment_amount",
"credit_adjustment_amount",
"refund_amount",
"total_principal_paid",
"loan_amount",
"total_interest_payable",
"written_off_amount",
"disbursed_amount",
"status",
],
as_dict=1,
)
pending_principal_amount = get_pending_principal_amount(loan_details)
security_value = 0
unpledge_qty_map = {}
ltv_ratio = 0
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty:
msg = _("Row {0}: {1} {2} of {3} is pledged against Loan {4}.").format(
security.idx,
pledged_qty,
security.uom,
frappe.bold(security.loan_security),
frappe.bold(self.loan),
)
msg += "<br>"
msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
unpledge_qty_map.setdefault(security.loan_security, 0)
unpledge_qty_map[security.loan_security] += security.qty
for security in pledge_qty_map:
if not ltv_ratio:
ltv_ratio = get_ltv_ratio(security)
qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0)
current_price = loan_security_price_map.get(security)
security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
self._throw(security_value, pending_principal_amount, ltv_ratio)
if security_value and flt(pending_principal_amount / security_value) * 100 > ltv_ratio:
self._throw(security_value, pending_principal_amount, ltv_ratio)
def _throw(self, security_value, pending_principal_amount, ltv_ratio):
msg = _("Loan Security Value after unpledge is {0}").format(frappe.bold(security_value))
msg += "<br>"
msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2)))
msg += "<br>"
msg += _("Loan To Security Value ratio must always be {0}").format(frappe.bold(ltv_ratio))
frappe.throw(msg, title=_("Loan To Value ratio breach"))
def on_update_after_submit(self):
self.approve()
def approve(self):
if self.status == "Approved" and not self.unpledge_time:
self.update_loan_status()
self.db_set("unpledge_time", get_datetime())
def update_loan_status(self, cancel=0):
if cancel:
loan_status = frappe.get_value("Loan", self.loan, "status")
if loan_status == "Closed":
frappe.db.set_value("Loan", self.loan, "status", "Loan Closure Requested")
else:
pledged_qty = 0
current_pledges = get_pledged_security_qty(self.loan)
for security, qty in current_pledges.items():
pledged_qty += qty
if not pledged_qty:
frappe.db.set_value("Loan", self.loan, {"status": "Closed", "closure_date": getdate()})
@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 pledges.items():
current_pledges.setdefault(security, qty)
current_pledges[security] -= unpledges.get(security, 0.0)
return current_pledges

View File

@ -1,14 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['Loan Security Unpledge'] = {
add_fields: ["status"],
get_indicator: function(doc) {
var status_color = {
"Requested": "orange",
"Approved": "green",
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
}
};

View File

@ -1,9 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestLoanSecurityUnpledge(unittest.TestCase):
pass

View File

@ -1,30 +0,0 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Loan Type', {
onload: function(frm) {
$.each(["penalty_income_account", "interest_income_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Income",
"is_group": 0
}
};
});
});
$.each(["payment_account", "loan_account", "disbursement_account"], function (i, field) {
frm.set_query(field, function () {
return {
"filters": {
"company": frm.doc.company,
"root_type": "Asset",
"is_group": 0
}
};
});
});
}
});

View File

@ -1,215 +0,0 @@
{
"actions": [],
"autoname": "field:loan_name",
"creation": "2019-08-29 18:08:38.159726",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_name",
"maximum_loan_amount",
"rate_of_interest",
"penalty_interest_rate",
"grace_period_in_days",
"write_off_amount",
"column_break_2",
"company",
"is_term_loan",
"disabled",
"repayment_schedule_type",
"repayment_date_on",
"description",
"account_details_section",
"mode_of_payment",
"disbursement_account",
"payment_account",
"column_break_12",
"loan_account",
"interest_income_account",
"penalty_income_account",
"amended_from"
],
"fields": [
{
"fieldname": "loan_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Loan Name",
"reqd": 1,
"unique": 1
},
{
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
"options": "Company:company:default_currency"
},
{
"default": "0",
"fieldname": "rate_of_interest",
"fieldtype": "Percent",
"label": "Rate of Interest (%) Yearly",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
},
{
"fieldname": "account_details_section",
"fieldtype": "Section Break",
"label": "Account Details"
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"reqd": 1
},
{
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Repayment Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
"options": "Account",
"reqd": 1
},
{
"default": "0",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan"
},
{
"description": "Penalty Interest Rate is levied on the pending interest amount on a daily basis in case of delayed repayment ",
"fieldname": "penalty_interest_rate",
"fieldtype": "Percent",
"label": "Penalty Interest Rate (%) Per Day"
},
{
"description": "No. of days from due date until which penalty won't be charged in case of delay in loan repayment",
"fieldname": "grace_period_in_days",
"fieldtype": "Int",
"label": "Grace Period in Days"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Loan Type",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_on_submit": 1,
"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": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
},
{
"fieldname": "disbursement_account",
"fieldtype": "Link",
"label": "Disbursement Account",
"options": "Account",
"reqd": 1
},
{
"depends_on": "is_term_loan",
"description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)",
"fieldname": "repayment_schedule_type",
"fieldtype": "Select",
"label": "Repayment Schedule Type",
"mandatory_depends_on": "is_term_loan",
"options": "\nMonthly as per repayment start date\nPro-rated calendar months"
},
{
"depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
"description": "Select whether the repayment date should be the end of the current month or start of the upcoming month",
"fieldname": "repayment_date_on",
"fieldtype": "Select",
"label": "Repayment Date On",
"mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"",
"options": "\nStart of the next month\nEnd of the current month"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-10-22 17:43:03.954201",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Loan Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"read": 1,
"role": "Employee"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -1,31 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class LoanType(Document):
def validate(self):
self.validate_accounts()
def validate_accounts(self):
for fieldname in [
"payment_account",
"loan_account",
"interest_income_account",
"penalty_income_account",
]:
company = frappe.get_value("Account", self.get(fieldname), "company")
if company and company != self.company:
frappe.throw(
_("Account {0} does not belong to company {1}").format(
frappe.bold(self.get(fieldname)), frappe.bold(self.company)
)
)
if self.get("loan_account") == self.get("payment_account"):
frappe.throw(_("Loan Account and Payment Account cannot be same"))

Some files were not shown because too many files have changed in this diff Show More