fix: Write Off amount handling in Loan accrual and closure

This commit is contained in:
Deepesh Garg 2020-10-18 22:25:24 +05:30
parent c0e24735e3
commit 9945ccc0cc
11 changed files with 126 additions and 37 deletions

View File

@ -3,7 +3,7 @@
{
"hidden": 0,
"label": "Loan",
"links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n { \"dependencies\": [\n \"Loan Type\"\n ],\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -13,7 +13,7 @@
{
"hidden": 0,
"label": "Disbursement and Repayment",
"links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -34,10 +34,11 @@
"docstatus": 0,
"doctype": "Desk Page",
"extends_another_page": 0,
"hide_custom": 0,
"idx": 0,
"is_standard": 1,
"label": "Loan",
"modified": "2020-06-07 19:42:14.947902",
"modified": "2020-10-17 12:59:50.336085",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -64,12 +64,13 @@ frappe.ui.form.on('Loan', {
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 (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') {
if (["Sanctioned", "Partially Disbursed"].includes(frm.doc.status)) {
frm.add_custom_button(__('Loan Disbursement'), function() {
frm.trigger("make_loan_disbursement");
},__('Create'));
@ -80,6 +81,12 @@ frappe.ui.form.on('Loan', {
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.trigger("toggle_fields");
},
@ -130,6 +137,22 @@ frappe.ui.form.on('Loan', {
})
},
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);
}
})
},
request_loan_closure: function(frm) {
frappe.confirm(__("Do you really want to close this loan"),
function() {

View File

@ -43,6 +43,7 @@
"section_break_17",
"total_payment",
"total_principal_paid",
"written_off_amount",
"column_break_19",
"total_interest_payable",
"total_amount_paid",
@ -330,11 +331,18 @@
"label": "Maximum Loan Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "written_off_amount",
"fieldtype": "Currency",
"label": "Written Off Amount",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-08-01 12:36:11.255233",
"modified": "2020-10-17 10:35:44.361836",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -143,7 +143,7 @@ class Loan(AccountsController):
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))
where name in (%s) """ % (', '.join(['%s']*len(pledge_list))), tuple(pledge_list)) #nosec
def update_total_amount_paid(doc):
total_amount_paid = 0
@ -187,17 +187,22 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest
return monthly_repayment_amount
@frappe.whitelist()
def request_loan_closure(loan):
amounts = calculate_amounts(loan, getdate())
def request_loan_closure(loan, posting_date=None):
if not posting_date:
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest']
loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error
if pending_amount > 0:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
else:
if pending_amount < write_off_limit:
# update status as loan closure requested
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@frappe.whitelist()
def get_loan_application(loan_application):
@ -217,6 +222,7 @@ def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amo
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:
@ -239,6 +245,38 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
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, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge

View File

@ -13,7 +13,7 @@ def get_data():
'items': ['Loan Security Pledge', 'Loan Security Shortfall', 'Loan Disbursement']
},
{
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Security Unpledge']
'items': ['Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off', 'Loan Security Unpledge']
}
]
}

View File

@ -26,19 +26,23 @@
{
"fieldname": "against_loan",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Against Loan ",
"options": "Loan"
"options": "Loan",
"reqd": 1
},
{
"fieldname": "disbursement_date",
"fieldtype": "Date",
"label": "Disbursement Date"
"label": "Disbursement Date",
"reqd": 1
},
{
"fieldname": "disbursed_amount",
"fieldtype": "Currency",
"label": "Disbursed Amount",
"options": "Company:company:default_currency"
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "amended_from",
@ -53,17 +57,21 @@
"fetch_from": "against_loan.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"read_only": 1
"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
"read_only": 1,
"reqd": 1
},
{
"collapsible": 1,
@ -102,9 +110,11 @@
"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
"read_only": 1,
"reqd": 1
},
{
"fieldname": "bank_account",
@ -117,9 +127,10 @@
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-04-29 05:20:41.629911",
"modified": "2020-10-16 10:04:26.229216",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Disbursement",

View File

@ -89,9 +89,10 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
else:
pending_principal_amount = loan.disbursed_amount
pending_principal_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
@ -128,7 +129,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
"rate_of_interest", "total_interest_payable", "written_off_amount", "total_principal_paid", "repayment_start_date"],
filters=query_filters)
for loan in open_loans:
@ -239,5 +240,5 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None):
precision = cint(frappe.db.get_default("currency_precision")) or 2
return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), 2)
return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)

View File

@ -31,8 +31,8 @@ class LoanRepayment(AccountsController):
def on_cancel(self):
self.mark_as_unpaid()
self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
self.make_gl_entries(cancel=1)
def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -235,7 +235,7 @@ class LoanRepayment(AccountsController):
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid ,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Against Loan:") + self.against_loan,
@ -344,9 +344,11 @@ def get_amounts(amounts, against_loan, posting_date):
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else:
pending_principal_amount = against_loan_doc.disbursed_amount
pending_principal_amount = against_loan_doc.disbursed_amount - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
unaccrued_interest = 0
if due_date:

View File

@ -42,10 +42,10 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime())
}, as_list=1))
total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable'])
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable', 'written_off_amount'])
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid)
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0
for security in self.securities:

View File

@ -11,6 +11,7 @@
"rate_of_interest",
"penalty_interest_rate",
"grace_period_in_days",
"write_off_amount",
"column_break_2",
"company",
"is_term_loan",
@ -76,7 +77,6 @@
"reqd": 1
},
{
"description": "This account is used for booking loan repayments from the borrower and also disbursing loans to the borrower",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment Account",
@ -84,7 +84,6 @@
"reqd": 1
},
{
"description": "This account is capital account which is used to allocate capital for loan disbursal account ",
"fieldname": "loan_account",
"fieldtype": "Link",
"label": "Loan Account",
@ -96,7 +95,6 @@
"fieldtype": "Column Break"
},
{
"description": "This account will be used for booking loan interest accruals",
"fieldname": "interest_income_account",
"fieldtype": "Link",
"label": "Interest Income Account",
@ -104,7 +102,6 @@
"reqd": 1
},
{
"description": "This account will be used for booking penalties levied due to delayed repayments",
"fieldname": "penalty_income_account",
"fieldtype": "Link",
"label": "Penalty Income Account",
@ -113,7 +110,6 @@
},
{
"default": "0",
"description": "If this is not checked the loan by default will be considered as a Demand Loan",
"fieldname": "is_term_loan",
"fieldtype": "Check",
"label": "Is Term Loan"
@ -145,11 +141,20 @@
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_on_submit": 1,
"description": "Pending amount that will be automatically ignored on loan closure request ",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
"label": "Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-06-07 18:55:59.346292",
"modified": "2020-10-17 11:41:17.907683",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",

View File

@ -8,7 +8,7 @@ frappe.ui.form.on(cur_frm.doctype, {
frm.refresh_field('applicant_type');
}
if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual'].includes(frm.doc.doctype)
if (['Loan Disbursement', 'Loan Repayment', 'Loan Interest Accrual', 'Loan Write Off'].includes(frm.doc.doctype)
&& frm.doc.docstatus > 0) {
frm.add_custom_button(__("Accounting Ledger"), function() {