From 9945ccc0cc76b08fe4f9e09fae134f066010a10b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Oct 2020 22:25:24 +0530 Subject: [PATCH] fix: Write Off amount handling in Loan accrual and closure --- .../loan_management/desk_page/loan/loan.json | 7 +-- erpnext/loan_management/doctype/loan/loan.js | 25 +++++++++- .../loan_management/doctype/loan/loan.json | 10 +++- erpnext/loan_management/doctype/loan/loan.py | 50 ++++++++++++++++--- .../doctype/loan/loan_dashboard.py | 2 +- .../loan_disbursement/loan_disbursement.json | 25 +++++++--- .../loan_interest_accrual.py | 9 ++-- .../doctype/loan_repayment/loan_repayment.py | 10 ++-- .../loan_security_unpledge.py | 6 +-- .../doctype/loan_type/loan_type.json | 17 ++++--- erpnext/loan_management/loan_common.js | 2 +- 11 files changed, 126 insertions(+), 37 deletions(-) diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json index 3bdd1ce56e..fc59c19325 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 0dc3bf8563..8d101b862a 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -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() { diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index aa5e21b426..312e9affb9 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 2d705fc296..8405d6ec62 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -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 diff --git a/erpnext/loan_management/doctype/loan/loan_dashboard.py b/erpnext/loan_management/doctype/loan/loan_dashboard.py index 90d5ae2650..7a8190f745 100644 --- a/erpnext/loan_management/doctype/loan/loan_dashboard.py +++ b/erpnext/loan_management/doctype/loan/loan_dashboard.py @@ -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'] } ] } \ 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 c437a987eb..89f671bcc0 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 4517de0c59..e31b844953 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 @@ 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) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 940f82ee34..de5ba8fcd5 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 @@ 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: 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 b3eb6001e4..d0d25e8897 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 @@ 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: diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 669490a448..5d9232d711 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 33a5de0566..50b68da30e 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -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() {