Merge branch 'develop' of https://github.com/frappe/erpnext into #34282-Record-advance-payment-as-a-liability
This commit is contained in:
commit
fda2d2bd59
@ -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
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -173,50 +173,52 @@ class SubcontractingController(StockController):
|
||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
||||
|
||||
def __get_transferred_items(self):
|
||||
fields = [
|
||||
f"`tabStock Entry`.`{self.subcontract_data.order_field}`",
|
||||
"`tabStock Entry`.`name` as voucher_no",
|
||||
]
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
se_detail = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
alias_dict = {
|
||||
"item_code": "rm_item_code",
|
||||
"subcontracted_item": "main_item_code",
|
||||
"basic_rate": "rate",
|
||||
}
|
||||
|
||||
child_table_fields = [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"description",
|
||||
"qty",
|
||||
"basic_rate",
|
||||
"amount",
|
||||
"serial_no",
|
||||
"serial_and_batch_bundle",
|
||||
"uom",
|
||||
"subcontracted_item",
|
||||
"stock_uom",
|
||||
"batch_no",
|
||||
"conversion_factor",
|
||||
"s_warehouse",
|
||||
"t_warehouse",
|
||||
"item_group",
|
||||
self.subcontract_data.rm_detail_field,
|
||||
]
|
||||
query = (
|
||||
frappe.qb.from_(se)
|
||||
.inner_join(se_detail)
|
||||
.on(se.name == se_detail.parent)
|
||||
.select(
|
||||
se[self.subcontract_data.order_field],
|
||||
se.name.as_("voucher_no"),
|
||||
se_detail.item_code.as_("rm_item_code"),
|
||||
se_detail.item_name,
|
||||
se_detail.description,
|
||||
(
|
||||
frappe.qb.terms.Case()
|
||||
.when(((se.purpose == "Material Transfer") & (se.is_return == 1)), -1 * se_detail.qty)
|
||||
.else_(se_detail.qty)
|
||||
).as_("qty"),
|
||||
se_detail.basic_rate.as_("rate"),
|
||||
se_detail.amount,
|
||||
se_detail.serial_no,
|
||||
se_detail.serial_and_batch_bundle,
|
||||
se_detail.uom,
|
||||
se_detail.subcontracted_item.as_("main_item_code"),
|
||||
se_detail.stock_uom,
|
||||
se_detail.batch_no,
|
||||
se_detail.conversion_factor,
|
||||
se_detail.s_warehouse,
|
||||
se_detail.t_warehouse,
|
||||
se_detail.item_group,
|
||||
se_detail[self.subcontract_data.rm_detail_field],
|
||||
)
|
||||
.where(
|
||||
(se.docstatus == 1)
|
||||
& (se[self.subcontract_data.order_field].isin(self.subcontract_orders))
|
||||
& (
|
||||
(se.purpose == "Send to Subcontractor")
|
||||
| ((se.purpose == "Material Transfer") & (se.is_return == 1))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if self.backflush_based_on == "BOM":
|
||||
child_table_fields.append("original_item")
|
||||
query = query.select(se_detail.original_item)
|
||||
|
||||
for field in child_table_fields:
|
||||
fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
|
||||
|
||||
filters = [
|
||||
["Stock Entry", "docstatus", "=", 1],
|
||||
["Stock Entry", "purpose", "=", "Send to Subcontractor"],
|
||||
["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders],
|
||||
]
|
||||
|
||||
return frappe.get_all("Stock Entry", fields=fields, filters=filters)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def __set_alternative_item_details(self, row):
|
||||
if row.get("original_item"):
|
||||
|
@ -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},
|
||||
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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": []
|
||||
}
|
@ -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")
|
||||
}
|
||||
]
|
||||
};
|
@ -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
|
||||
}
|
@ -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}],
|
||||
}
|
@ -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")
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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",
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
@ -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
@ -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);
|
||||
},
|
||||
})
|
@ -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
|
||||
}
|
@ -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
|
@ -1,7 +0,0 @@
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "loan_application",
|
||||
"transactions": [
|
||||
{"items": ["Loan", "Loan Security Pledge"]},
|
||||
],
|
||||
}
|
@ -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)
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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)
|
@ -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
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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)"))
|
@ -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)
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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)
|
||||
)
|
@ -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
|
||||
)
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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)
|
@ -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
|
@ -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']);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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"]},
|
||||
],
|
||||
}
|
@ -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
|
@ -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);
|
||||
},
|
||||
});
|
@ -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
|
||||
}
|
@ -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),
|
||||
)
|
@ -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];
|
||||
}
|
||||
};
|
@ -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
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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},
|
||||
)
|
@ -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
|
@ -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) {
|
||||
|
||||
// },
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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"]},
|
||||
],
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
@ -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
|
||||
}
|
@ -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
|
@ -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];
|
||||
}
|
||||
};
|
@ -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
|
@ -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
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
@ -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": []
|
||||
}
|
@ -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"))
|
@ -1,5 +0,0 @@
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "loan_type",
|
||||
"transactions": [{"items": ["Loan Repayment", "Loan"]}, {"items": ["Loan Application"]}],
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user