fix: Write Off amount handling in Loan accrual and closure
diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json
index 3bdd1ce..fc59c19 100644
--- a/erpnext/loan_management/desk_page/loan/loan.json
+++ b/erpnext/loan_management/desk_page/loan/loan.json
@@ -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",
diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js
index 0dc3bf8..8d101b8 100644
--- a/erpnext/loan_management/doctype/loan/loan.js
+++ b/erpnext/loan_management/doctype/loan/loan.js
@@ -64,12 +64,13 @@
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 @@
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 @@
})
},
+ 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() {
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index aa5e21b..312e9af 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -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",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 2d705fc..8405d6e 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -143,7 +143,7 @@
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 @@
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 @@
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:
@@ -240,6 +246,38 @@
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
if loan:
diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py
index 90d5ae2..7a8190f 100644
--- a/erpnext/loan_management/doctype/loan/loan_dashboard.py
+++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py
@@ -13,7 +13,7 @@
'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']
}
]
}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
index c437a98..89f671b 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json
@@ -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",
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 4517de0..e31b844 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -89,9 +89,10 @@
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 @@
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 @@
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)
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 940f82e..de5ba8f 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -31,8 +31,8 @@
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 @@
"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 @@
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:
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index b3eb600..d0d25e8 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -42,10 +42,10 @@
"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:
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index 669490a..5d9232d 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -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",
diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js
index 33a5de0..50b68da 100644
--- a/erpnext/loan_management/loan_common.js
+++ b/erpnext/loan_management/loan_common.js
@@ -8,7 +8,7 @@
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() {