From 51c6cf692ef4cc550fbc034142b25c819e7cc341 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:20:36 +0530 Subject: [PATCH 01/40] fix: Add method for loan closure --- erpnext/loan_management/doctype/loan/loan.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index d1b7589a17..1fb0805d38 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -9,6 +9,7 @@ from frappe import _ from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.controllers.accounts_controller import AccountsController +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class Loan(AccountsController): def validate(self): @@ -182,6 +183,19 @@ def get_monthly_repayment_amount(repayment_method, loan_amount, rate_of_interest monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods) return monthly_repayment_amount +@frappe.whitelist() +def request_loan_closure(loan): + amounts = calculate_amounts(loan, getdate()) + + pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest'] + + # 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: + # update status as loan closure requested + 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) From 6d27adccfce718c37b0a019fdf164c685c8271eb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:28:39 +0530 Subject: [PATCH 02/40] fix: Button to close loan --- erpnext/loan_management/doctype/loan/loan.js | 40 ++++++++++++++++---- erpnext/loan_management/loan_common.js | 2 +- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 9b4c21770e..682b574fea 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -21,6 +21,14 @@ frappe.ui.form.on('Loan', { }; }); + frm.set_query("loan_type", function () { + return { + "filters": { + "docstatus": 1 + } + }; + }); + $.each(["penalty_income_account", "interest_income_account"], function(i, field) { frm.set_query(field, function () { return { @@ -49,19 +57,21 @@ frappe.ui.form.on('Loan', { refresh: function (frm) { 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 (frm.doc.status == "Sanctioned" || frm.doc.status == 'Partially Disbursed') { frm.add_custom_button(__('Loan Disbursement'), function() { frm.trigger("make_loan_disbursement"); },__('Create')); } - if (["Disbursed", "Partially Disbursed"].includes(frm.doc.status) && (!frm.doc.repay_from_salary)) { - frm.add_custom_button(__('Loan Repayment'), function() { - frm.trigger("make_repayment_entry"); - },__('Create')); - - } - if (frm.doc.status == "Loan Closure Requested") { frm.add_custom_button(__('Loan Security Unpledge'), function() { frm.trigger("create_loan_security_unpledge"); @@ -117,6 +127,22 @@ frappe.ui.form.on('Loan', { }) }, + 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", diff --git a/erpnext/loan_management/loan_common.js b/erpnext/loan_management/loan_common.js index d9dd415296..33a5de0566 100644 --- a/erpnext/loan_management/loan_common.js +++ b/erpnext/loan_management/loan_common.js @@ -15,7 +15,7 @@ frappe.ui.form.on(cur_frm.doctype, { frappe.route_options = { voucher_no: frm.doc.name, company: frm.doc.company, - from_date: frm.doc.posting_date, + from_date: moment(frm.doc.posting_date).format('YYYY-MM-DD'), to_date: moment(frm.doc.modified).format('YYYY-MM-DD'), show_cancelled_entries: frm.doc.docstatus === 2 }; From 6dccf79250ff1a380b712a7ff3625a648d7f71f3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 10 Oct 2020 23:32:57 +0530 Subject: [PATCH 03/40] fix: Add accrual type and penalty field in interest accrual --- .../loan_disbursement/loan_disbursement.py | 2 +- .../loan_disbursement/test_loan_disbursement.py | 3 +-- .../loan_interest_accrual.json | 17 ++++++++++++++++- .../process_loan_interest_accrual.json | 10 +++++++++- .../process_loan_interest_accrual.py | 5 +++-- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 260fada893..bfdc8b403e 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -74,7 +74,7 @@ class LoanDisbursement(AccountsController): if loan_details.status == "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) + loan=self.against_loan, accrual_type="Disbursement") if disbursed_amount > loan_details.loan_amount: topup_amount = disbursed_amount - loan_details.loan_amount diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 2cb2637612..3ade5c549b 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -60,8 +60,7 @@ class TestLoanDisbursement(unittest.TestCase): 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), - "Regular Payment", 611095.89) + repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), 611095.89) repayment_entry.submit() loan.reload() diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index 5fc3e8f4b6..a0abc5b57e 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -14,6 +14,7 @@ "column_break_4", "company", "posting_date", + "accrual_type", "is_term_loan", "section_break_7", "pending_principal_amount", @@ -22,6 +23,7 @@ "column_break_14", "interest_amount", "paid_interest_amount", + "penalty_amount", "section_break_15", "process_loan_interest_accrual", "repayment_schedule_name", @@ -149,12 +151,25 @@ "fieldtype": "Currency", "label": "Paid Interest Amount", "options": "Company:company:default_currency" + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nDisbursement" + }, + { + "fieldname": "penalty_amount", + "fieldtype": "Currency", + "label": "Penalty Amount", + "options": "Company:company:default_currency" } ], "in_create": 1, + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-16 11:24:23.258404", + "modified": "2020-10-10 03:12:58.204501", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 0ef098f278..1e05a42de7 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,6 +10,7 @@ "loan_type", "loan", "process_type", + "accrual_type", "amended_from" ], "fields": [ @@ -47,11 +48,18 @@ "hidden": 1, "label": "Process Type", "read_only": 1 + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nAccrual" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-09 22:52:53.911416", + "modified": "2020-10-08 12:20:11.124769", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 0fa96860d0..1eeb18b7ea 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,19 +20,20 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type) + open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, loan_type=self.loan_type) -def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None): +def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): loan_process = frappe.new_doc('Process Loan Interest Accrual') loan_process.posting_date = posting_date or nowdate() loan_process.loan_type = loan_type loan_process.process_type = 'Demand Loans' loan_process.loan = loan + loan_process.accrual_type = accrual_type loan_process.submit() From d86b7c55373c0a6e72d18ab0f9106649489575d5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 11 Oct 2020 00:37:34 +0530 Subject: [PATCH 04/40] fix: Remove repayment type --- .../loan_management/doctype/loan/test_loan.py | 136 ++++++++++++++---- .../loan_interest_accrual.py | 29 +++- .../loan_repayment/loan_repayment.json | 31 ++-- .../doctype/loan_repayment/loan_repayment.py | 116 +++++++++------ 4 files changed, 220 insertions(+), 92 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 5a4a19a0fb..7b653652ea 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import unpledge_security +from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -132,7 +132,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -149,23 +149,23 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111118.68) repayment_entry.save() repayment_entry.submit() penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) - amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', - 'paid_principal_amount']) + amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) loan.load_from_db() - self.assertEquals(amounts[0], repayment_entry.interest_payable) + total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] + self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid - - penalty_amount - amounts[0], 2)) + penalty_amount - total_interest_paid, 2)) - def test_loan_closure_repayment(self): + def test_loan_closure(self): pledge = [{ "loan_security": "Test Security 1", "qty": 4000.00 @@ -174,7 +174,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -196,14 +196,16 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) - self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2)) + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -230,8 +232,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), - "Regular Payment", 89768.75) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(nowdate(), 5), 89768.75) repayment_entry.submit() @@ -281,7 +282,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -299,10 +300,10 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") @@ -317,9 +318,9 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) - amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") - self.assertEqual(amounts['pending_principal_amount'], 0) - self.assertEqual(amounts['payable_principal_amount'], 0) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertTrue(amounts['pending_principal_amount'] < 0) + self.assertTrue(amounts['payable_principal_amount'] < 0) self.assertEqual(amounts['interest_amount'], 0) def test_disbursal_check_with_shortfall(self): @@ -381,7 +382,7 @@ class TestLoan(unittest.TestCase): loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) create_pledge(loan_application) - loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate())) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') loan.submit() self.assertEquals(loan.loan_amount, 1000000) @@ -399,20 +400,102 @@ class TestLoan(unittest.TestCase): 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, 6), "Regular Repayment") + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), - "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', 'paid_principal_amount']) + request_loan_closure(loan.name) loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment") - self.assertEquals(amounts['pending_principal_amount'], 0.0) + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertTrue(amounts['pending_principal_amount'] < 0.0) + + def test_partial_unaccrued_interest_payment(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 5.5 + + # get partial unaccrued interest amount + paid_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) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + paid_amount) + + repayment_entry.submit() + repayment_entry.load_from_db() + + partial_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 5) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + interest_amount = flt(amounts['interest_amount'] + partial_accrued_interest_amount, 2) + self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) + + def test_penalty(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + 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)) + paid_amount = amounts['interest_amount']/2 + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + paid_amount) + + repayment_entry.submit() + + # 30 days - grace period + penalty_days = 30 - 5 + penalty_applicable_amount = flt(amounts['interest_amount']/2, 2) + penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days)/365, 2) + process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') + + calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', + {'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') + + self.assertEquals(calculated_penalty_amount, penalty_amount) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): @@ -582,12 +665,11 @@ def create_loan_security_price(loan_security, loan_security_price, uom, from_dat "valid_upto": to_date }).insert(ignore_permissions=True) -def create_repayment_entry(loan, applicant, posting_date, payment_type, paid_amount): +def create_repayment_entry(loan, applicant, posting_date, paid_amount): lr = frappe.get_doc({ "doctype": "Loan Repayment", "against_loan": loan, - "payment_type": payment_type, "company": "_Test Company", "posting_date": posting_date or nowdate(), "applicant": applicant, 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 2d959bf3be..4517de0c59 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 @@ -22,7 +22,6 @@ class LoanInterestAccrual(AccountsController): if not self.interest_amount and not self.payable_principal_amount: frappe.throw(_("Interest Amount or Principal Amount is mandatory")) - def on_submit(self): self.make_gl_entries() @@ -79,8 +78,11 @@ class LoanInterestAccrual(AccountsController): # 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): +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 + 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 @@ -91,7 +93,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i else: pending_principal_amount = loan.disbursed_amount - interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100) + interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date) payable_interest = interest_per_day * no_of_days args = frappe._dict({ @@ -102,13 +104,16 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i 'loan_account': loan.loan_account, 'pending_principal_amount': pending_principal_amount, 'interest_amount': payable_interest, + 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'], 'process_loan_interest': process_loan_interest, - 'posting_date': posting_date + 'posting_date': posting_date, + 'accrual_type': accrual_type }) - make_loan_interest_accrual_entry(args) + 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): +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 @@ -127,7 +132,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte filters=query_filters) for loan in open_loans: - calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_interest) + 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): curr_date = posting_date or add_days(nowdate(), 1) @@ -192,10 +197,12 @@ def make_loan_interest_accrual_entry(args): 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.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() @@ -226,3 +233,11 @@ def days_in_year(year): return days +def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): + if not posting_date: + posting_date = getdate() + + 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) + diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 5942455919..60b20369dc 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -10,11 +10,11 @@ "applicant_type", "applicant", "loan_type", - "payment_type", "column_break_3", "company", "posting_date", "is_term_loan", + "rate_of_interest", "payment_details_section", "due_date", "pending_principal_amount", @@ -31,6 +31,7 @@ "column_break_21", "reference_date", "principal_amount_paid", + "total_interest_paid", "repayment_details", "amended_from" ], @@ -95,15 +96,6 @@ "fieldname": "column_break_9", "fieldtype": "Column Break" }, - { - "default": "Regular Payment", - "fieldname": "payment_type", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Payment Type", - "options": "\nRegular Payment\nLoan Closure", - "reqd": 1 - }, { "fieldname": "payable_amount", "fieldtype": "Currency", @@ -195,6 +187,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Principal Amount Paid", + "options": "Company:company:default_currency", "read_only": 1 }, { @@ -217,11 +210,27 @@ "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 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-16 09:40:15.581165", + "modified": "2020-10-10 03:49:01.827593", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 97dbc44bf1..c8344d4667 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,19 +14,18 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries 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 +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest class LoanRepayment(AccountsController): def validate(self): - amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) + amounts = calculate_amounts(self.against_loan, self.posting_date) self.set_missing_values(amounts) self.validate_amount() - self.allocate_amounts(amounts['pending_accrual_entries']) - - def before_submit(self): - self.book_unaccrued_interest() + self.allocate_amounts(amounts) def on_submit(self): + self.book_unaccrued_interest() self.update_paid_amount() self.make_gl_entries() @@ -72,29 +71,35 @@ class LoanRepayment(AccountsController): msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) frappe.throw(msg) - if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision): - msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) - frappe.throw(msg) - def book_unaccrued_interest(self): - if self.payment_type == 'Loan Closure': - total_interest_paid = 0 - for payment in self.repayment_details: - total_interest_paid += payment.paid_interest_amount + precision = cint(frappe.db.get_default("currency_precision")) or 2 + if self.total_interest_paid > self.interest_payable: + if not self.is_term_loan: + # get last loan interest accrual date + last_accrual_date = frappe.get_value('Loan Interest Accrual', {'loan': self.against_loan}, 'MAX(posting_date)') - if total_interest_paid < self.interest_payable: - if not self.is_term_loan: - process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date, - loan=self.against_loan) + # get posting date upto which interest has to be accrued + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), 2) - lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual': - process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1) + no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, + precision)/per_day_interest, 0) - self.append('repayment_details', { - 'loan_interest_accrual': lia.name, - 'paid_interest_amount': lia.interest_amount, - 'paid_principal_amount': lia.payable_principal_amount - }) + 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) + + 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 + }) def update_paid_amount(self): precision = cint(frappe.db.get_default("currency_precision")) or 2 @@ -108,12 +113,6 @@ class LoanRepayment(AccountsController): WHERE name = %s""", (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual)) - if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision): - if loan.is_secured_loan: - frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") - else: - frappe.db.set_value("Loan", self.against_loan, "status", "Closed") - frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, loan.total_principal_paid + self.principal_amount_paid, self.against_loan)) @@ -137,15 +136,17 @@ class LoanRepayment(AccountsController): if loan.status == "Loan Closure Requested": frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") - def allocate_amounts(self, paid_entries): + 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 total_interest_paid = 0 interest_paid = self.amount_paid - self.penalty_amount - if self.amount_paid - self.penalty_amount > 0 and paid_entries: + if self.amount_paid - self.penalty_amount > 0: interest_paid = self.amount_paid - self.penalty_amount - for lia, amounts in iteritems(paid_entries): + for lia, amounts in iteritems(repayment_details.get('pending_accrual_entries', [])): if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid: interest_amount = amounts['interest_amount'] paid_principal = amounts['payable_principal_amount'] @@ -169,9 +170,24 @@ class LoanRepayment(AccountsController): 'paid_principal_amount': paid_principal }) - if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable: - unaccrued_interest = self.interest_payable - total_interest_paid - interest_paid -= unaccrued_interest + if repayment_details['unaccrued_interest'] and interest_paid: + # 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']: + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), precision) + interest_paid -= repayment_details['unaccrued_interest'] + total_interest_paid += repayment_details['unaccrued_interest'] + else: + # get no of days for which interest can be paid + per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, + self.rate_of_interest, self.posting_date), precision) + + no_of_days = cint(interest_paid/per_day_interest) + total_interest_paid += no_of_days * per_day_interest + interest_paid -= no_of_days * per_day_interest + + self.total_interest_paid = total_interest_paid if interest_paid: self.principal_amount_paid += interest_paid @@ -289,7 +305,7 @@ def get_accrued_interest_entries(against_loan): # 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, payment_type): +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) @@ -332,15 +348,16 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): else: pending_principal_amount = against_loan_doc.disbursed_amount - if payment_type == "Loan Closure": - if due_date: - pending_days = date_diff(posting_date, due_date) + 1 - else: - pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 + unaccrued_interest = 0 + if due_date: + pending_days = date_diff(posting_date, due_date) + 1 + else: + pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 - payable_principal_amount = pending_principal_amount - per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 - total_pending_interest += (pending_days * per_day_interest) + if pending_days > 0: + payable_principal_amount = flt(pending_principal_amount, precision) + per_day_interest = get_per_day_interest(payable_principal_amount, loan_type_details.rate_of_interest, posting_date) + unaccrued_interest += (pending_days * flt(per_day_interest, precision)) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) @@ -348,6 +365,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): amounts["penalty_amount"] = flt(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"] = unaccrued_interest if final_due_date: amounts["due_date"] = final_due_date @@ -355,7 +373,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type): return amounts @frappe.whitelist() -def calculate_amounts(against_loan, posting_date, payment_type): +def calculate_amounts(against_loan, posting_date, payment_type=''): amounts = { 'penalty_amount': 0.0, @@ -363,10 +381,14 @@ def calculate_amounts(against_loan, posting_date, payment_type): '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, payment_type) + amounts = get_amounts(amounts, against_loan, posting_date) + + if payment_type == 'Loan Closure': + amounts['payable_amount'] += amounts['unaccrued_interest'] return amounts From a034311b1b397b26217c47b16e20ae8581ac1b0d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 11 Oct 2020 20:52:02 +0530 Subject: [PATCH 05/40] fix: Acrual type --- .../doctype/loan_interest_accrual/loan_interest_accrual.json | 2 +- .../process_loan_interest_accrual.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index a0abc5b57e..893609e0c7 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -169,7 +169,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-10 03:12:58.204501", + "modified": "2020-10-11 11:17:44.704694", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 1e05a42de7..bb781b1d56 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -53,13 +53,13 @@ "fieldname": "accrual_type", "fieldtype": "Select", "label": "Accrual Type", - "options": "Regular\nRepayment\nAccrual" + "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-08 12:20:11.124769", + "modified": "2020-10-11 11:19:00.531046", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", From 8c13fded17c6a52c09ac5a37bf636b91b2076e8d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Oct 2020 09:23:04 +0530 Subject: [PATCH 06/40] fix: Add unaccrued interest in interest amount for loan closure --- erpnext/loan_management/doctype/loan_repayment/loan_repayment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c8344d4667..940f82ee34 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -389,6 +389,7 @@ def calculate_amounts(against_loan, posting_date, payment_type=''): if payment_type == 'Loan Closure': amounts['payable_amount'] += amounts['unaccrued_interest'] + amounts['interest_amount'] += amounts['unaccrued_interest'] return amounts From 66967c6d450f5d83cf6d999ecb4b5516fd71a050 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 13 Oct 2020 18:10:51 +0530 Subject: [PATCH 07/40] fix: Loan Security unpledge on loan cancel --- erpnext/loan_management/doctype/loan/loan.js | 3 +++ erpnext/loan_management/doctype/loan/loan.py | 9 ++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 682b574fea..0dc3bf8563 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -11,6 +11,9 @@ frappe.ui.form.on('Loan', { } }, 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": { diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index 1fb0805d38..2d705fc296 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -138,9 +138,12 @@ class Loan(AccountsController): }) def unlink_loan_security_pledge(self): - frappe.db.sql("""UPDATE `tabLoan Security Pledge` SET - loan = '', status = 'Unpledged' - where name = %s """, (self.loan_security_pledge)) + 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)) def update_total_amount_paid(doc): total_amount_paid = 0 From c0e24735e362a64228c5e75421dc04ad78a2ef4d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 17 Oct 2020 22:31:36 +0530 Subject: [PATCH 08/40] feat: Add loan write off doctype --- .../doctype/loan_write_off/__init__.py | 0 .../doctype/loan_write_off/loan_write_off.js | 18 +++ .../loan_write_off/loan_write_off.json | 141 ++++++++++++++++++ .../doctype/loan_write_off/loan_write_off.py | 79 ++++++++++ .../loan_write_off/test_loan_write_off.py | 10 ++ 5 files changed, 248 insertions(+) create mode 100644 erpnext/loan_management/doctype/loan_write_off/__init__.py create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.js create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.json create mode 100644 erpnext/loan_management/doctype/loan_write_off/loan_write_off.py create mode 100644 erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py diff --git a/erpnext/loan_management/doctype/loan_write_off/__init__.py b/erpnext/loan_management/doctype/loan_write_off/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js new file mode 100644 index 0000000000..cc5cd0d3a0 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js @@ -0,0 +1,18 @@ +// Copyright (c) 2020, 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 Write Off', { + refresh: function(frm) { + frm.set_query('write_off_account', function(){ + return { + filters: { + 'company': frm.doc.company, + 'root_type': 'Expense', + 'is_group': 0 + } + } + }); + } +}); diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json new file mode 100644 index 0000000000..64623c4b3a --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json @@ -0,0 +1,141 @@ +{ + "actions": [], + "autoname": "LM-WO-.#####", + "creation": "2020-10-16 11:09:14.495066", + "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", + "write_off_account", + "column_break_11", + "write_off_amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "loan", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Loan", + "options": "Loan", + "reqd": 1 + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 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 + }, + { + "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 + }, + { + "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": "Write Off Details" + }, + { + "fieldname": "write_off_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Write Off Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "write_off_amount", + "fieldtype": "Currency", + "label": "Write Off Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Loan Write Off", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2020-10-17 08:30:54.859362", + "modified_by": "Administrator", + "module": "Loan Management", + "name": "Loan Write Off", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py new file mode 100644 index 0000000000..22fbe1ac57 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, erpnext +from frappe import _ +from frappe.utils import getdate +from frappe.model.document import Document +from erpnext.controllers.accounts_controller import AccountsController +from erpnext.accounts.general_ledger import make_gl_entries + +class LoanWriteOff(AccountsController): + def validate(self): + self.set_missing_values() + + def set_missing_values(self): + if not self.cost_center: + self.cost_center = erpnext.get_default_cost_center(self.company) + + 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'] + self.make_gl_entries(cancel=1) + + def update_outstanding_amount(self, cancel=0): + written_off_amount = frappe.db.get_value('Loan', self.loan, 'written_off_amount') + + if cancel: + written_off_amount -= self.write_off_amount + else: + written_off_amount += self.write_off_amount + + frappe.db.set_value('Loan', self.loan, 'written_off_amount', written_off_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.write_off_account, + "against": loan_details.loan_account, + "debit": self.write_off_amount, + "debit_in_account_currency": self.write_off_amount, + "against_voucher_type": "Loan", + "against_voucher": self.loan, + "remarks": _("Against Loan:") + self.loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "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.write_off_account, + "credit": self.write_off_amount, + "credit_in_account_currency": self.write_off_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) + + diff --git a/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py new file mode 100644 index 0000000000..9f6700e274 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_write_off/test_loan_write_off.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLoanWriteOff(unittest.TestCase): + pass From 9945ccc0cc76b08fe4f9e09fae134f066010a10b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Oct 2020 22:25:24 +0530 Subject: [PATCH 09/40] 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() { From 8cd8dbe15d736d33d04106def46c586bf39712b3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 18 Oct 2020 22:26:09 +0530 Subject: [PATCH 10/40] fix: Add write off test --- .../loan_management/doctype/loan/test_loan.py | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 7b653652ea..c38541196f 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,7 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall -from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure +from erpnext.loan_management.doctype.loan.loan import unpledge_security, request_loan_closure, make_loan_write_off from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount @@ -496,6 +496,96 @@ class TestLoan(unittest.TestCase): self.assertEquals(calculated_penalty_amount, penalty_amount) + def test_loan_write_off_limit(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + no_of_days += 6 + + 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) + + # repay 50 less so that it can be automatically written off + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + flt(loan.loan_amount + accrued_interest_amount - 50)) + + repayment_entry.submit() + + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50) + + request_loan_closure(loan.name) + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + def test_loan_amount_write_off(self): + pledge = [{ + "loan_security": "Test Security 1", + "qty": 4000.00 + }] + + loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) + create_pledge(loan_application) + + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + no_of_days += 6 + + 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) + + # repay 100 less so that it can be automatically written off + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + flt(loan.loan_amount + accrued_interest_amount - 100)) + + repayment_entry.submit() + + amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)']) + + self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) + self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100) + + we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) + we.submit() + + amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): @@ -579,7 +669,8 @@ def create_loan_type(loan_name, maximum_loan_amount, rate_of_interest, penalty_i "interest_income_account": interest_income_account, "penalty_income_account": penalty_income_account, "repayment_method": repayment_method, - "repayment_periods": repayment_periods + "repayment_periods": repayment_periods, + "write_off_amount": 100 }).insert() loan_type.submit() From 2f65ab5355a86e3422b9fe81142d7aa84d24ccc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 10:40:59 +0530 Subject: [PATCH 11/40] fix: Validatiion for loan write off amountt --- .../doctype/loan_write_off/loan_write_off.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 22fbe1ac57..823e6a904f 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -5,19 +5,28 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import getdate -from frappe.model.document import Document +from frappe.utils import getdate, flt from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries class LoanWriteOff(AccountsController): def validate(self): self.set_missing_values() + self.validate_write_off_amount() def set_missing_values(self): if not self.cost_center: self.cost_center = erpnext.get_default_cost_center(self.company) + def validate_write_off_amount(self): + 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) - flt(written_off_amount) + + if self.write_off_amount > pending_principal_amount: + frappe.throw(_("Write off amount cannot be greater than pending principal amount")) + def on_submit(self): self.update_outstanding_amount() self.make_gl_entries() From 13cbda110d46bb171dc0162dd8e3bafb8f754f66 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 16:11:52 +0530 Subject: [PATCH 12/40] fix: Test Case --- .../doctype/loan_interest_accrual/test_loan_interest_accrual.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 4b85b21869..5495d1c70d 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -57,4 +57,4 @@ class TestLoanInterestAccrual(unittest.TestCase): loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name}) - self.assertEquals(flt(loan_interest_accural.interest_amount, 2), flt(accrued_interest_amount, 2)) + self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0)) From 8f9bab79a7128671191111cb4dfea95c5e588c5e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 19:03:13 +0530 Subject: [PATCH 13/40] fix: Update no copy fields --- erpnext/loan_management/doctype/loan/loan.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 312e9affb9..8a3b2fa029 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -76,6 +76,7 @@ "fieldname": "loan_application", "fieldtype": "Link", "label": "Loan Application", + "no_copy": 1, "options": "Loan Application" }, { @@ -149,7 +150,8 @@ "depends_on": "eval:doc.status==\"Disbursed\"", "fieldname": "disbursement_date", "fieldtype": "Date", - "label": "Disbursement Date" + "label": "Disbursement Date", + "no_copy": 1 }, { "depends_on": "is_term_loan", @@ -253,6 +255,7 @@ "fieldname": "total_payment", "fieldtype": "Currency", "label": "Total Payable Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -266,6 +269,7 @@ "fieldname": "total_interest_payable", "fieldtype": "Currency", "label": "Total Interest Payable", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -274,6 +278,7 @@ "fieldname": "total_amount_paid", "fieldtype": "Currency", "label": "Total Amount Paid", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -314,6 +319,7 @@ "fieldname": "total_principal_paid", "fieldtype": "Currency", "label": "Total Principal Paid", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -321,6 +327,7 @@ "fieldname": "disbursed_amount", "fieldtype": "Currency", "label": "Disbursed Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -329,6 +336,7 @@ "fieldname": "maximum_loan_amount", "fieldtype": "Currency", "label": "Maximum Loan Amount", + "no_copy": 1, "options": "Company:company:default_currency", "read_only": 1 }, @@ -336,13 +344,14 @@ "fieldname": "written_off_amount", "fieldtype": "Currency", "label": "Written Off Amount", + "no_copy": 1, "options": "Company:company:default_currency" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 10:35:44.361836", + "modified": "2020-10-21 09:12:26.809228", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", From 3f177bffac645cb9218e4799835806b41487cdb9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 21 Oct 2020 22:15:18 +0530 Subject: [PATCH 14/40] fix: Add test for loan top up --- .../loan_management/doctype/loan/test_loan.py | 10 +++-- .../test_loan_disbursement.py | 44 ++++++++++++++++++- .../test_loan_interest_accrual.py | 2 +- .../doctype/loan_repayment/loan_repayment.py | 8 ++-- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index c38541196f..1634697939 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -320,7 +320,7 @@ class TestLoan(unittest.TestCase): amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) self.assertTrue(amounts['pending_principal_amount'] < 0) - self.assertTrue(amounts['payable_principal_amount'] < 0) + self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) def test_disbursal_check_with_shortfall(self): @@ -706,7 +706,7 @@ def create_loan_security(): "haircut": 50.00, }).insert(ignore_permissions=True) -def create_loan_security_pledge(applicant, pledges, loan_application): +def create_loan_security_pledge(applicant, pledges, loan_application=None, loan=None): lsp = frappe.new_doc("Loan Security Pledge") lsp.applicant_type = 'Customer' @@ -714,11 +714,13 @@ def create_loan_security_pledge(applicant, pledges, loan_application): lsp.company = "_Test Company" lsp.loan_application = loan_application + if loan: + lsp.loan = loan + for pledge in pledges: lsp.append('securities', { "loan_security": pledge['loan_security'], - "qty": pledge['qty'], - "haircut": pledge['haircut'] + "qty": pledge['qty'] }) lsp.save() diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index 3ade5c549b..aaaeea8c4e 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -8,9 +8,10 @@ from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_la from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_repayment_entry, create_loan_application, make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_security_price) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year, get_per_day_interest from erpnext.selling.doctype.customer.test_customer import get_customer_dict from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge +from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts class TestLoanDisbursement(unittest.TestCase): @@ -68,3 +69,44 @@ class TestLoanDisbursement(unittest.TestCase): # After repayment loan disbursement entry should go through make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) + 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.assertEquals(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.assertEquals(amounts['pending_principal_amount'], 1500000) + self.assertEquals(amounts['interest_amount'], flt(interest + previous_interest, 2)) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py index 5495d1c70d..46a6440553 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest from frappe.utils import (nowdate, add_days, get_datetime, get_first_day, get_last_day, date_diff, flt, add_to_date) -from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_pledge, create_loan_security_price, +from erpnext.loan_management.doctype.loan.test_loan import (create_loan_type, create_loan_security_price, make_loan_disbursement_entry, create_loan_accounts, create_loan_security_type, create_loan_security, create_demand_loan, create_loan_application) from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index de5ba8fcd5..6b3fba41c8 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -357,8 +357,8 @@ def get_amounts(amounts, against_loan, posting_date): pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 if pending_days > 0: - payable_principal_amount = flt(pending_principal_amount, precision) - per_day_interest = get_per_day_interest(payable_principal_amount, loan_type_details.rate_of_interest, posting_date) + 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 * flt(per_day_interest, precision)) amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) @@ -389,9 +389,11 @@ def calculate_amounts(against_loan, posting_date, payment_type=''): amounts = get_amounts(amounts, against_loan, posting_date) + # update values for closure if payment_type == 'Loan Closure': - amounts['payable_amount'] += amounts['unaccrued_interest'] + amounts['payable_principal_amount'] = amounts['pending_principal_amount'] amounts['interest_amount'] += amounts['unaccrued_interest'] + amounts['payable_amount'] = amounts['payable_principal_amount'] + amounts['interest_amount'] return amounts From 2c97244bada37ce793c466ed9bfef75910f5fda7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 22 Oct 2020 09:34:27 +0530 Subject: [PATCH 15/40] fix: Test Cases --- .../doctype/loan_security_pledge/loan_security_pledge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index 2bb6fd84e5..cbc8376aa5 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -78,7 +78,7 @@ class LoanSecurityPledge(Document): self.maximum_loan_value = maximum_loan_value def update_loan(loan, maximum_value_against_pledge): - maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_value']) + maximum_loan_value = frappe.db.get_value('Loan', {'name': loan}, ['maximum_loan_amount']) - frappe.db.sql(""" UPDATE `tabLoan` SET maximum_loan_value=%s, is_secured_loan=1 + 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)) From cd2c90451e38716dce9db3e802c60c971a001381 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 22 Oct 2020 21:50:06 +0530 Subject: [PATCH 16/40] fix: Unaccrued interest after disbursal --- erpnext/loan_management/doctype/loan/loan.json | 5 ++--- .../loan_interest_accrual/loan_interest_accrual.py | 8 ++++---- .../doctype/loan_repayment/loan_repayment.py | 7 ++++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 8a3b2fa029..b613d22827 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -295,8 +295,7 @@ "default": "0", "fieldname": "is_secured_loan", "fieldtype": "Check", - "label": "Is Secured Loan", - "read_only": 1 + "label": "Is Secured Loan" }, { "default": "0", @@ -351,7 +350,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-21 09:12:26.809228", + "modified": "2020-10-22 11:03:43.697394", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", 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 e31b844953..1fc41f9ea5 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 @@ -210,21 +210,21 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accural_date_in_current_month(loan) + last_interest_accrual_date = get_last_accural_date(loan.name) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accural_date_in_current_month(loan): +def get_last_accural_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s""", (loan.name)) + WHERE loan = %s""", (loan)) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day return add_days(last_posting_date[0][0], 1) else: - return loan.disbursement_date + return frappe.db.get_value('Loan', loan, 'disbursement_date') def days_in_year(year): days = 365 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 6b3fba41c8..12d81d3a24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,7 +14,7 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries 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 -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accural_date class LoanRepayment(AccountsController): @@ -76,14 +76,15 @@ class LoanRepayment(AccountsController): if self.total_interest_paid > self.interest_payable: if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = frappe.get_value('Loan Interest Accrual', {'loan': self.against_loan}, 'MAX(posting_date)') + last_accrual_date = get_last_accural_date(self.against_loan) # get posting date upto which interest has to be accrued per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, self.rate_of_interest, self.posting_date), 2) no_of_days = flt(flt(self.total_interest_paid - self.interest_payable, - precision)/per_day_interest, 0) + precision)/per_day_interest, 0) - 1 + posting_date = add_days(last_accrual_date, no_of_days) From 2b3f8e0c3b5bab732ec452675a5b21fc72f5404a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 23 Oct 2020 19:02:24 +0530 Subject: [PATCH 17/40] fix: Cancel repayment accrual interest entry on payment cancellation --- .../doctype/loan_repayment/loan_repayment.py | 15 +++++++++++++-- .../loan_repayment_detail.json | 14 ++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 12d81d3a24..b973cd69e6 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -24,8 +24,10 @@ class LoanRepayment(AccountsController): self.validate_amount() self.allocate_amounts(amounts) - def on_submit(self): + def before_submit(self): self.book_unaccrued_interest() + + def on_submit(self): self.update_paid_amount() self.make_gl_entries() @@ -99,7 +101,8 @@ class LoanRepayment(AccountsController): 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 + 'paid_principal_amount': 0.0, + 'accrual_type': 'Repayment' }) def update_paid_amount(self): @@ -123,6 +126,8 @@ class LoanRepayment(AccountsController): def mark_as_unpaid(self): loan = frappe.get_doc("Loan", self.against_loan) + no_of_repayments = len(self.repayment_details) + for payment in self.repayment_details: frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` SET paid_principal_amount = `paid_principal_amount` - %s, @@ -130,6 +135,12 @@ class LoanRepayment(AccountsController): 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 WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, loan.total_principal_paid - self.principal_amount_paid, self.against_loan)) diff --git a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json index cff1dbb1d2..4b9b191e26 100644 --- a/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json +++ b/erpnext/loan_management/doctype/loan_repayment_detail/loan_repayment_detail.json @@ -7,7 +7,8 @@ "field_order": [ "loan_interest_accrual", "paid_principal_amount", - "paid_interest_amount" + "paid_interest_amount", + "accrual_type" ], "fields": [ { @@ -27,11 +28,20 @@ "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-04-15 21:50:03.837019", + "modified": "2020-10-23 08:09:18.267030", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment Detail", From d63fbd79f4f434e3a6d925832d114c9f2b930442 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 14:20:55 +0530 Subject: [PATCH 18/40] fix: Unaccrued interest from last accrual date instead of disbursement date --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index b973cd69e6..c1e83d9305 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -310,6 +310,7 @@ def get_accrued_interest_entries(against_loan): payable_principal_amount - paid_principal_amount > 0) AND docstatus = 1 + ORDER BY posting_date """, (against_loan), as_dict=1) return unpaid_accrued_entries @@ -366,7 +367,8 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - pending_days = date_diff(posting_date, against_loan_doc.disbursement_date) + 1 + last_accrual_date = get_last_accural_date(against_loan_doc.name) + pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: principal_amount = flt(pending_principal_amount, precision) From 5b4742fd25043532cd9338ace81998c233c9a91d Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 16:56:05 +0530 Subject: [PATCH 19/40] fix: Permission fixes for some doctypes --- .../doctype/loan_type/loan_type.json | 4 +++- .../doctype/loan_write_off/loan_write_off.json | 18 +++++++++++++++++- .../process_loan_interest_accrual.json | 6 +++++- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 5d9232d711..18a97315f0 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -154,13 +154,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 11:41:17.907683", + "modified": "2020-10-26 07:13:55.029811", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -170,6 +171,7 @@ "report": 1, "role": "Loan Manager", "share": 1, + "submit": 1, "write": 1 }, { diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json index 64623c4b3a..4617a62f5b 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.json @@ -116,13 +116,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-17 08:30:54.859362", + "modified": "2020-10-26 07:13:43.663924", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Write Off", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -132,6 +133,21 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, + "write": 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 } ], diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index bb781b1d56..c1296f759f 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -59,13 +59,14 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-11 11:19:00.531046", + "modified": "2020-10-26 07:14:31.491249", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", "owner": "Administrator", "permissions": [ { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -75,9 +76,11 @@ "report": 1, "role": "System Manager", "share": 1, + "submit": 1, "write": 1 }, { + "cancel": 1, "create": 1, "delete": 1, "email": 1, @@ -87,6 +90,7 @@ "report": 1, "role": "Loan Manager", "share": 1, + "submit": 1, "write": 1 } ], From 3f971b23716205351ee45960ee448462ca392a6e Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 16:59:57 +0530 Subject: [PATCH 20/40] fix: copying po no when mapping doc --- .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/controllers/selling_controller.py | 40 +++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 4b598877d9..af6c6968dc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1401,6 +1401,7 @@ def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): target.ignore_pricing_rule = 1 target.run_method("set_missing_values") + target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") def update_item(source_doc, target_doc, source_parent): diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 58861715c2..0fbd914fdf 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -371,13 +371,39 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"): - ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order" - sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) - if sales_orders: - po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if po_nos and po_nos[0].get('po_no'): - self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) + self.po_no = '' + if self.doctype == 'Sales Invoice' and hasattr(self, "items"): + self.set_pos_for_sales_invoice() + if self.doctype == 'Delivery Note' and hasattr(self, "items"): + self.set_pos_for_delivery_note() + + def set_pos_for_sales_invoice(self): + ref_fieldname1 = "sales_order" + ref_fieldname2 = "delivery_note" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if delivery_notes: + dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) + if dn_po_nos and dn_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + + def set_pos_for_delivery_note(self): + ref_fieldname1 = "against_sales_order" + ref_fieldname2 = "against_sales_invoice" + sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) + sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) + if sales_orders: + so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) + if so_po_nos and so_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) + if sales_invoices: + si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) + if si_po_nos and si_po_nos[0].get('po_no'): + self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From a630de56e9d38aa5ed253fb349aa70957e8d8cd4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 18:53:56 +0530 Subject: [PATCH 21/40] fix: Loan disbursement amount validation check --- .../loan_disbursement/loan_disbursement.py | 107 ++++++++++-------- .../doctype/loan_security/loan_security.json | 4 +- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index bfdc8b403e..f58b989a20 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -17,6 +17,7 @@ class LoanDisbursement(AccountsController): def validate(self): self.set_missing_values() + self.validate_disbursal_amount() def on_submit(self): self.set_status_and_amounts() @@ -40,57 +41,21 @@ class LoanDisbursement(AccountsController): if not self.bank_account and self.applicant_type == "Customer": self.bank_account = frappe.db.get_value("Customer", self.applicant, "default_bank_account") - def set_status_and_amounts(self, cancel=0): + 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 = 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" + disbursed_amount, status, total_payment = self.get_values_on_cancel(loan_details) else: - disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount - total_payment = loan_details.total_payment - - 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)) - - if loan_details.status == "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" + 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, @@ -99,6 +64,54 @@ class LoanDisbursement(AccountsController): "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" + total_payment = loan_details.loan_amount + 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 == "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" + total_payment = disbursed_amount + + return disbursed_amount, status, total_payment + def make_gl_entries(self, cancel=0, adv_adj=0): gle_map = [] loan_details = frappe.get_doc("Loan", self.against_loan) @@ -155,7 +168,8 @@ def get_total_pledged_security_value(loan): pledged_securities = get_pledged_security_qty(loan) for security, qty in pledged_securities.items(): - security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100 + after_haircut_percentage = 100 - hair_cut_map.get(security) + security_value += (loan_security_price_map.get(security) * qty * after_haircut_percentage)/100 return security_value @@ -173,7 +187,8 @@ def get_disbursal_amount(loan): pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \ - flt(loan_details.total_principal_paid) else: - pending_principal_amount = flt(loan_details.disbursed_amount) + pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \ + - flt(loan_details.total_principal_paid) security_value = 0.0 if loan_details.is_secured_loan: diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json index 1d0bb30910..c698601ea4 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.json +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -25,6 +25,7 @@ }, { "fetch_from": "loan_security_type.haircut", + "fetch_if_empty": 1, "fieldname": "haircut", "fieldtype": "Percent", "label": "Haircut %" @@ -64,8 +65,9 @@ "reqd": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-04-29 13:21:26.043492", + "modified": "2020-10-26 07:34:48.601766", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", From 13f4b3fc17f785176a006aea8592dcee588da5dd Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Oct 2020 19:01:36 +0530 Subject: [PATCH 22/40] fix: Translation syntax --- .../loan_security_unpledge/loan_security_unpledge.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 d0d25e8897..5d4447bf2b 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 @@ -51,9 +51,11 @@ class LoanSecurityUnpledge(Document): for security in self.securities: pledged_qty = pledge_qty_map.get(security.loan_security, 0) if security.qty > pledged_qty: - frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}. - You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom, - frappe.bold(security.loan_security), frappe.bold(self.loan))) + 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 += "
" + msg += _("You are trying to unpledge more.") + frappe.throw(msg, title=_("Loan Security Unpledge Error")) qty_after_unpledge = pledged_qty - security.qty ltv_ratio = ltv_ratio_map.get(security.loan_security_type) From 0a6e1b35030e3fc3574d568531c02fbd6f36dd1c Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 26 Oct 2020 21:23:31 +0530 Subject: [PATCH 23/40] fix: refactor --- erpnext/controllers/selling_controller.py | 40 +++++++++-------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 0fbd914fdf..e5405b2e43 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -371,39 +371,29 @@ class SellingController(StockController): self.make_sl_entries(sl_entries) def set_po_nos(self): - self.po_no = '' if self.doctype == 'Sales Invoice' and hasattr(self, "items"): self.set_pos_for_sales_invoice() if self.doctype == 'Delivery Note' and hasattr(self, "items"): self.set_pos_for_delivery_note() def set_pos_for_sales_invoice(self): - ref_fieldname1 = "sales_order" - ref_fieldname2 = "delivery_note" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - delivery_notes = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if delivery_notes: - dn_po_nos = frappe.get_all('Delivery Note', 'po_no', filters = {'name': ('in', delivery_notes)}) - if dn_po_nos and dn_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in dn_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'sales_order', po_nos) + self.get_po_nos('Delivery Note', 'delivery_note', po_nos) + self.po_no = ', '.join(list(set(po_nos))) def set_pos_for_delivery_note(self): - ref_fieldname1 = "against_sales_order" - ref_fieldname2 = "against_sales_invoice" - sales_orders = list(set([d.get(ref_fieldname1) for d in self.items if d.get(ref_fieldname1)])) - sales_invoices = list(set([d.get(ref_fieldname2) for d in self.items if d.get(ref_fieldname2)])) - if sales_orders: - so_po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) - if so_po_nos and so_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in so_po_nos if d.po_no]))) - if sales_invoices: - si_po_nos = frappe.get_all('Sales Invoice', 'po_no', filters = {'name': ('in', sales_invoices)}) - if si_po_nos and si_po_nos[0].get('po_no'): - self.po_no += ', '.join(list(set([d.po_no for d in si_po_nos if d.po_no]))) + po_nos = [] + self.get_po_nos('Sales Order', 'against_sales_order', po_nos) + self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) + self.po_no = ', '.join(list(set(po_nos))) + + def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): + doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) + if doc_list: + po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) + if po_no_list and po_no_list[0].get('po_no'): + po_nos += [d.po_no for d in po_no_list if d.po_no] def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From c9a6135d6cddc9480db89b26ccf38628571d937e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 28 Oct 2020 13:38:15 +0530 Subject: [PATCH 24/40] fix: Interest accrual after loan topup --- erpnext/loan_management/doctype/loan/loan.json | 5 +++-- .../doctype/loan_disbursement/loan_disbursement.py | 8 +++++--- .../doctype/loan_disbursement/test_loan_disbursement.py | 6 ++++++ .../loan_interest_accrual/loan_interest_accrual.py | 2 +- .../doctype/loan_repayment/loan_repayment.py | 7 ++++--- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index b613d22827..23996afd21 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -344,13 +344,14 @@ "fieldtype": "Currency", "label": "Written Off Amount", "no_copy": 1, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-22 11:03:43.697394", + "modified": "2020-10-27 23:37:02.785940", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index f58b989a20..949e1412e2 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -77,7 +77,7 @@ class LoanDisbursement(AccountsController): if disbursed_amount == 0: status = "Sanctioned" - total_payment = loan_details.loan_amount + elif disbursed_amount >= loan_details.loan_amount: status = "Disbursed" else: @@ -89,7 +89,7 @@ class LoanDisbursement(AccountsController): disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount total_payment = loan_details.total_payment - if loan_details.status == "Disbursed" and not loan_details.is_term_loan: + 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") @@ -108,7 +108,6 @@ class LoanDisbursement(AccountsController): status = "Disbursed" else: status = "Partially Disbursed" - total_payment = disbursed_amount return disbursed_amount, status, total_payment @@ -199,6 +198,9 @@ def get_disbursal_amount(loan): 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 diff --git a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py index aaaeea8c4e..a8753877a6 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/test_loan_disbursement.py @@ -69,6 +69,12 @@ class TestLoanDisbursement(unittest.TestCase): # 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", 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 1fc41f9ea5..d0b957de56 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 @@ -218,7 +218,7 @@ def get_no_of_days_for_interest_accural(loan, posting_date): def get_last_accural_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` - WHERE loan = %s""", (loan)) + WHERE loan = %s and docstatus = 1""", (loan)) if last_posting_date[0][0]: # interest for last interest accrual date is already booked, so add 1 day diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c1e83d9305..63d388daf5 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -301,7 +301,8 @@ def get_accrued_interest_entries(against_loan): 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 + payable_principal_amount - paid_principal_amount as payable_principal_amount, + accrual_type FROM `tabLoan Interest Accrual` WHERE @@ -342,7 +343,7 @@ def get_amounts(amounts, against_loan, posting_date): no_of_late_days = date_diff(posting_date, add_days(due_date, loan_type_details.grace_period_in_days)) - if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary): + if no_of_late_days > 0 and (not 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)/365 total_pending_interest += entry.interest_amount @@ -353,7 +354,7 @@ def get_amounts(amounts, against_loan, posting_date): 'payable_principal_amount': flt(entry.payable_principal_amount, precision) }) - if not final_due_date: + if due_date and not final_due_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'): From 846ff323e2ef5853b3fe1af614b9589de93df6e2 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 3 Nov 2020 17:35:44 +0530 Subject: [PATCH 25/40] feat: sales order status filter for production plan --- .../doctype/production_plan/production_plan.json | 10 +++++++++- .../doctype/production_plan/production_plan.py | 6 ++++-- .../doctype/production_plan/test_production_plan.py | 3 ++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 850d5aeff8..63df5f379d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -19,6 +19,7 @@ "column_break2", "from_date", "to_date", + "sales_order_status", "sales_orders_detail", "get_sales_orders", "sales_orders", @@ -301,13 +302,20 @@ "label": "Warehouses", "options": "Production Plan Material Request Warehouse", "read_only": 1 + }, + { + "depends_on": "eval: doc.get_items_from == \"Sales Order\"", + "fieldname": "sales_order_status", + "fieldtype": "Select", + "label": "Sales Order Status", + "options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 13:00:54.335319", + "modified": "2020-11-03 15:19:18.270529", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a314a15c23..3833e86d27 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -571,6 +571,8 @@ def get_sales_orders(self): so_filter += " and so.customer = %(customer)s" if self.project: so_filter += " and so.project = %(project)s" + if self.sales_order_status: + so_filter += "and so.status = %(sales_order_status)s" if self.item_code: item_filter += " and so_item.item_code = %(item)s" @@ -594,8 +596,8 @@ def get_sales_orders(self): "customer": self.customer, "project": self.project, "item": self.item_code, - "company": self.company - + "company": self.company, + "sales_order_status": self.sales_order_status }, as_dict=1) return open_so diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index fa9d080cca..27335aa204 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -137,7 +137,8 @@ class TestProductionPlan(unittest.TestCase): 'from_date': so.transaction_date, 'to_date': so.transaction_date, 'customer': so.customer, - 'item_code': item + 'item_code': item, + 'sales_order_status': so.status }) sales_orders = get_sales_orders(pln) or {} sales_orders = [d.get('name') for d in sales_orders if d.get('name') == sales_order] From cdc17bb9f3c6b2f1eab818ed04ed587da58013b0 Mon Sep 17 00:00:00 2001 From: Afshan Date: Wed, 4 Nov 2020 19:28:55 +0530 Subject: [PATCH 26/40] fix: added code for testing --- .../doctype/sales_order/test_sales_order.py | 1 + .../delivery_note/test_delivery_note.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index a33d401b57..643e7cf38b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1064,6 +1064,7 @@ def make_sales_order(**args): so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" + so.po_no = args.po_no or '12345' if args.selling_price_list: so.selling_price_list = args.selling_price_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 0168613415..9566af7b38 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -442,9 +442,15 @@ class TestDeliveryNote(unittest.TestCase): self.assertEqual(dn.status, "To Bill") self.assertEqual(dn.per_billed, 0) + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, so.po_no) + si = make_sales_invoice(dn.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + dn.load_from_db() self.assertEqual(dn.get("items")[0].billed_amt, 200) self.assertEqual(dn.per_billed, 100) @@ -461,16 +467,25 @@ class TestDeliveryNote(unittest.TestCase): si.insert() si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) dn1 = make_delivery_note(so.name) dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn1.po_no) + dn2 = make_delivery_note(so.name) dn2.get("items")[0].qty = 3 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, dn2.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -492,9 +507,15 @@ class TestDeliveryNote(unittest.TestCase): dn1.get("items")[0].qty = 2 dn1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, so.po_no) + si1 = make_sales_invoice(dn1.name) si1.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn1.po_no, si1.po_no) + dn1.load_from_db() self.assertEqual(dn1.per_billed, 100) @@ -502,10 +523,16 @@ class TestDeliveryNote(unittest.TestCase): si2.get("items")[0].qty = 4 si2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(si2.po_no, so.po_no) + dn2 = make_delivery_note(so.name) dn2.get("items")[0].qty = 5 dn2.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn2.po_no, so.po_no) + dn1.load_from_db() self.assertEqual(dn1.get("items")[0].billed_amt, 200) self.assertEqual(dn1.per_billed, 100) @@ -525,9 +552,15 @@ class TestDeliveryNote(unittest.TestCase): si = make_sales_invoice(so.name) si.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(so.po_no, si.po_no) + dn = make_delivery_note(si.name) dn.submit() + # Testing if Customer's Purchase Order No was rightly copied + self.assertEqual(dn.po_no, si.po_no) + self.assertEqual(dn.get("items")[0].billed_amt, 1000) self.assertEqual(dn.per_billed, 100) self.assertEqual(dn.status, "Completed") From 49cc57e76eb61bec1d05c2e6b4f7f15b12008204 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:14:07 +0530 Subject: [PATCH 27/40] fix: Negative amount check for amounts --- erpnext/loan_management/doctype/loan/loan.json | 3 ++- .../doctype/loan_disbursement/loan_disbursement.json | 3 ++- .../doctype/loan_repayment/loan_repayment.json | 3 ++- erpnext/loan_management/doctype/pledge/pledge.json | 7 +++++-- .../doctype/proposed_pledge/proposed_pledge.json | 8 ++++++-- erpnext/loan_management/doctype/unpledge/unpledge.json | 4 +++- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 23996afd21..e8ecf015c3 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -136,6 +136,7 @@ "fieldname": "loan_amount", "fieldtype": "Currency", "label": "Loan Amount", + "non_negative": 1, "options": "Company:company:default_currency" }, { @@ -351,7 +352,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-27 23:37:02.785940", + "modified": "2020-11-05 10:04:00.762975", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json index 89f671bcc0..cd5df4d3cd 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.json @@ -41,6 +41,7 @@ "fieldname": "disbursed_amount", "fieldtype": "Currency", "label": "Disbursed Amount", + "non_negative": 1, "options": "Company:company:default_currency", "reqd": 1 }, @@ -130,7 +131,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-16 10:04:26.229216", + "modified": "2020-11-06 10:04:30.882322", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Disbursement", diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 60b20369dc..2b5df4be24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -108,6 +108,7 @@ "fieldname": "amount_paid", "fieldtype": "Currency", "label": "Amount Paid", + "non_negative": 1, "options": "Company:company:default_currency", "reqd": 1 }, @@ -230,7 +231,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-10 03:49:01.827593", + "modified": "2020-11-05 10:06:58.792841", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json index f22a21e3be..801e3a3117 100644 --- a/erpnext/loan_management/doctype/pledge/pledge.json +++ b/erpnext/loan_management/doctype/pledge/pledge.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-09-09 17:06:16.756573", "doctype": "DocType", "editable_grid": 1, @@ -49,7 +50,8 @@ "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Quantity" + "label": "Quantity", + "non_negative": 1 }, { "fieldname": "loan_security_price", @@ -86,7 +88,8 @@ } ], "istable": 1, - "modified": "2019-12-03 10:59:58.001421", + "links": [], + "modified": "2020-11-05 10:07:15.424937", "modified_by": "Administrator", "module": "Loan Management", "name": "Pledge", diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json index aee7c2ced5..3e7e778a25 100644 --- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json +++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-08-29 22:29:37.628178", "doctype": "DocType", "editable_grid": 1, @@ -39,7 +40,8 @@ "fieldname": "qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Quantity" + "label": "Quantity", + "non_negative": 1 }, { "fieldname": "loan_security", @@ -56,8 +58,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-12-02 10:23:11.498308", + "links": [], + "modified": "2020-11-05 10:07:37.542344", "modified_by": "Administrator", "module": "Loan Management", "name": "Proposed Pledge", diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json index ee192d7377..00356685eb 100644 --- a/erpnext/loan_management/doctype/unpledge/unpledge.json +++ b/erpnext/loan_management/doctype/unpledge/unpledge.json @@ -52,6 +52,7 @@ "fieldtype": "Float", "in_list_view": 1, "label": "Quantity", + "non_negative": 1, "reqd": 1 }, { @@ -62,9 +63,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-05-06 10:50:18.448552", + "modified": "2020-11-05 10:07:28.106961", "modified_by": "Administrator", "module": "Loan Management", "name": "Unpledge", From a2bff7fbfcbdcda76143dab00468655232105fd7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:14:29 +0530 Subject: [PATCH 28/40] fix: Penalty amount calculation fix --- .../loan_management/doctype/loan/test_loan.py | 60 +++++++++---------- .../doctype/loan_repayment/loan_repayment.py | 4 +- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 1634697939..10a7b1143d 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -142,19 +142,19 @@ class TestLoan(unittest.TestCase): 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) + accrued_interest_amount = flt((loan.loan_amount * loan.rate_of_interest * no_of_days) + / (days_in_year(get_datetime(first_date).year) * 100), 2) make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111118.68) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), 111119) repayment_entry.save() repayment_entry.submit() - penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year)) - self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) + penalty_amount = (accrued_interest_amount * 5 * 25) / 100 + self.assertEquals(flt(repayment_entry.penalty_amount,0), flt(penalty_amount, 0)) amounts = frappe.db.get_all('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount']) @@ -162,8 +162,8 @@ class TestLoan(unittest.TestCase): total_interest_paid = amounts[0]['paid_interest_amount'] + amounts[1]['paid_interest_amount'] self.assertEquals(amounts[1]['paid_interest_amount'], repayment_entry.interest_payable) - self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid - - penalty_amount - total_interest_paid, 2)) + self.assertEquals(flt(loan.total_principal_paid, 0), flt(repayment_entry.amount_paid - + penalty_amount - total_interest_paid, 0)) def test_loan_closure(self): pledge = [{ @@ -184,10 +184,10 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - # Adding 6 since repayment is made 5 days late after due date + # Adding 5 since repayment is made 5 days late after due date # and since payment type is loan closure so interest should be considered for those - # 6 days as well though in grace period - no_of_days += 6 + # 5 days as well though in grace period + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -195,7 +195,7 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() @@ -292,7 +292,7 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -300,7 +300,7 @@ class TestLoan(unittest.TestCase): make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() request_loan_closure(loan.name) @@ -318,7 +318,7 @@ class TestLoan(unittest.TestCase): self.assertEqual(loan.status, 'Closed') self.assertEquals(sum(pledged_qty.values()), 0) - amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertTrue(amounts['pending_principal_amount'] < 0) self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEqual(amounts['interest_amount'], 0) @@ -392,7 +392,7 @@ class TestLoan(unittest.TestCase): no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -400,9 +400,9 @@ class TestLoan(unittest.TestCase): 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, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount)) repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', @@ -412,7 +412,7 @@ class TestLoan(unittest.TestCase): loan.load_from_db() self.assertEquals(loan.status, "Loan Closure Requested") - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertTrue(amounts['pending_principal_amount'] < 0.0) def test_partial_unaccrued_interest_payment(self): @@ -443,9 +443,9 @@ class TestLoan(unittest.TestCase): 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, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), paid_amount) repayment_entry.submit() @@ -480,15 +480,15 @@ class TestLoan(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 1)) paid_amount = amounts['interest_amount']/2 - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), paid_amount) repayment_entry.submit() # 30 days - grace period - penalty_days = 30 - 5 + penalty_days = 30 - 4 penalty_applicable_amount = flt(amounts['interest_amount']/2, 2) - penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days)/365, 2) + penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2) process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30') calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', @@ -514,7 +514,7 @@ class TestLoan(unittest.TestCase): last_date = '2019-10-30' no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -523,7 +523,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) # repay 50 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount - 50)) repayment_entry.submit() @@ -533,7 +533,7 @@ class TestLoan(unittest.TestCase): self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 50) request_loan_closure(loan.name) @@ -558,7 +558,7 @@ class TestLoan(unittest.TestCase): last_date = '2019-10-30' no_of_days = date_diff(last_date, first_date) + 1 - no_of_days += 6 + no_of_days += 5 accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ / (days_in_year(get_datetime(first_date).year) * 100) @@ -567,7 +567,7 @@ class TestLoan(unittest.TestCase): process_loan_interest_accrual_for_demand_loans(posting_date = last_date) # repay 100 less so that it can be automatically written off - repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6), + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), flt(loan.loan_amount + accrued_interest_amount - 100)) repayment_entry.submit() @@ -577,13 +577,13 @@ class TestLoan(unittest.TestCase): self.assertEquals(flt(amount, 0),flt(accrued_interest_amount, 0)) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 100) we = make_loan_write_off(loan.name, amount=amounts['pending_principal_amount']) we.submit() - amounts = calculate_amounts(loan.name, add_days(last_date, 6)) + amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 63d388daf5..7216c8b813 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -341,10 +341,10 @@ def get_amounts(amounts, against_loan, posting_date): due_date = add_days(entry.posting_date, 1) no_of_late_days = date_diff(posting_date, - add_days(due_date, loan_type_details.grace_period_in_days)) + add_days(due_date, loan_type_details.grace_period_in_days)) + 1 if no_of_late_days > 0 and (not 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)/365 + 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 From 1145b3796c50308c529955a9654c3afdd27e6470 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 5 Nov 2020 21:21:40 +0530 Subject: [PATCH 29/40] fix: Remove accrual type from process --- .../loan_interest_accrual/loan_interest_accrual.py | 5 +++-- .../process_loan_interest_accrual.json | 9 +-------- .../process_loan_interest_accrual.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) 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 d0b957de56..70d1453df4 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 @@ -135,7 +135,7 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte 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): +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) @@ -154,7 +154,8 @@ def make_accrual_interest_entry_for_term_loans(posting_date, process_loan_intere 'payable_principal': loan.principal_amount, 'process_loan_interest': process_loan_interest, 'repayment_schedule_name': loan.payment_entry, - 'posting_date': posting_date + 'posting_date': posting_date, + 'accrual_type': accrual_type }) make_loan_interest_accrual_entry(args) diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index c1296f759f..4c354e6721 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,7 +10,6 @@ "loan_type", "loan", "process_type", - "accrual_type", "amended_from" ], "fields": [ @@ -48,18 +47,12 @@ "hidden": 1, "label": "Process Type", "read_only": 1 - }, - { - "fieldname": "accrual_type", - "fieldtype": "Select", - "label": "Accrual Type", - "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-26 07:14:31.491249", + "modified": "2020-11-05 10:49:35.657728", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index 1eeb18b7ea..f0522657eb 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,7 +20,7 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) + open_loans = open_loans, loan_type = self.loan_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, From 3e69756e154a5b3cc1ca467f1c8d9bf7e77a2e60 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 Nov 2020 14:25:09 +0530 Subject: [PATCH 30/40] fix: Add better remarks for Loan GL entries --- .../doctype/loan_disbursement/loan_disbursement.py | 4 ++-- .../doctype/loan_interest_accrual/loan_interest_accrual.py | 6 ++++-- .../doctype/loan_repayment/loan_repayment.py | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 949e1412e2..bda439fb7c 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -123,7 +123,7 @@ class LoanDisbursement(AccountsController): "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": "Against Loan:" + self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -139,7 +139,7 @@ class LoanDisbursement(AccountsController): "credit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": "Against Loan:" + self.against_loan, + "remarks": _("Disbursement against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, 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 70d1453df4..22ff6663d3 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 @@ -49,7 +49,8 @@ class LoanInterestAccrual(AccountsController): "debit_in_account_currency": self.interest_amount, "against_voucher_type": "Loan", "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, + "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( + get_last_accural_date(self.loan), self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -65,7 +66,8 @@ class LoanInterestAccrual(AccountsController): "credit_in_account_currency": self.interest_amount, "against_voucher_type": "Loan", "against_voucher": self.loan, - "remarks": _("Against Loan:") + self.loan, + "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( + get_last_accural_date(self.loan), self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 7216c8b813..e478cb8d43 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -217,7 +217,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -233,7 +233,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, From 3899079bb9b0ef733e5b1100359afd7d94cfba5a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 6 Nov 2020 17:39:54 +0530 Subject: [PATCH 31/40] fix: Loan seurity unpledge msg improvement --- .../loan_security_shortfall.py | 2 +- .../loan_security_unpledge/loan_security_unpledge.py | 12 ++++++++++-- .../process_loan_interest_accrual.json | 9 ++++++++- .../process_loan_interest_accrual.py | 4 ++-- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index 0f42bde3c4..8ec0bfb62c 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -22,7 +22,7 @@ def update_shortfall_status(loan, security_value): if security_value >= loan_security_shortfall.shortfall_amount: frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, { "status": "Completed", - "shortfall_value": loan_security_shortfall.shortfall_amount}) + "shortfall_amount": loan_security_shortfall.shortfall_amount}) else: frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, "shortfall_amount", loan_security_shortfall.shortfall_amount - security_value) 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 5d4447bf2b..c29f325bfc 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 @@ -67,10 +67,18 @@ class LoanSecurityUnpledge(Document): security_value += qty_after_unpledge * current_price if not security_value and flt(pending_principal_amount, 2) > 0: - frappe.throw("Cannot Unpledge, loan to value ratio is breaching") + self._throw(security_value, pending_principal_amount, ltv_ratio) if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio: - frappe.throw("Cannot Unpledge, loan to value ratio is breaching") + 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 += '
' + msg += _("Pending principal amount is {0}").format(frappe.bold(flt(pending_principal_amount, 2))) + msg += '
' + 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() diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 4c354e6721..b78c3ba5d8 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -10,6 +10,7 @@ "loan_type", "loan", "process_type", + "accrual_type", "amended_from" ], "fields": [ @@ -47,12 +48,18 @@ "hidden": 1, "label": "Process Type", "read_only": 1 + }, + { + "fieldname": "accrual_type", + "fieldtype": "Select", + "label": "Accrual Type", + "options": "Regular\nRepayment\nDisbursement" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-05 10:49:35.657728", + "modified": "2020-11-06 04:43:56.581670", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py index f0522657eb..11333dc2aa 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.py @@ -20,11 +20,11 @@ class ProcessLoanInterestAccrual(Document): if (not self.loan or not loan_doc.is_term_loan) and self.process_type != 'Term Loans': make_accrual_interest_entry_for_demand_loans(self.posting_date, self.name, - open_loans = open_loans, loan_type = self.loan_type) + open_loans = open_loans, loan_type = self.loan_type, accrual_type=self.accrual_type) if (not self.loan or loan_doc.is_term_loan) and self.process_type != 'Demand Loans': make_accrual_interest_entry_for_term_loans(self.posting_date, self.name, term_loan=self.loan, - loan_type=self.loan_type) + loan_type=self.loan_type, accrual_type=self.accrual_type) def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=None, loan=None, accrual_type="Regular"): From 2ad015450ed74a558e693b31ab0b3d27eb82752b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Nov 2020 00:14:40 +0530 Subject: [PATCH 32/40] fix: Remarks fix --- erpnext/loan_management/doctype/loan/loan.js | 3 ++- .../loan_interest_accrual/loan_interest_accrual.json | 10 +++++++++- .../loan_interest_accrual/loan_interest_accrual.py | 11 +++++++---- .../doctype/loan_repayment/loan_repayment.py | 7 +++---- .../process_loan_interest_accrual.json | 6 ++++-- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 8d101b862a..28af3a9c41 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -7,7 +7,8 @@ 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 Security Unpledge': function() { frm.trigger('create_loan_security_unpledge') }, + 'Loan Write Off': function() { frm.trigger('make_loan_write_off_entry') } } }, onload: function (frm) { diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index 893609e0c7..d6bf08ac51 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -27,6 +27,7 @@ "section_break_15", "process_loan_interest_accrual", "repayment_schedule_name", + "last_accrual_date", "amended_from" ], "fields": [ @@ -163,13 +164,20 @@ "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 } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-11 11:17:44.704694", + "modified": "2020-11-06 13:22:40.197916", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", 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 22ff6663d3..d642400cdc 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 @@ -22,6 +22,9 @@ class LoanInterestAccrual(AccountsController): 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) + def on_submit(self): self.make_gl_entries() @@ -50,7 +53,7 @@ class LoanInterestAccrual(AccountsController): "against_voucher_type": "Loan", "against_voucher": self.loan, "remarks": _("Interest accrued from {0} to {1} against loan: {2}").format( - get_last_accural_date(self.loan), self.posting_date, self.loan), + self.last_accrual_date, self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -67,7 +70,7 @@ class LoanInterestAccrual(AccountsController): "against_voucher_type": "Loan", "against_voucher": self.loan, "remarks": ("Interest accrued from {0} to {1} against loan: {2}").format( - get_last_accural_date(self.loan), self.posting_date, self.loan), + self.last_accrual_date, self.posting_date, self.loan), "cost_center": erpnext.get_default_cost_center(self.company), "posting_date": self.posting_date }) @@ -213,13 +216,13 @@ def make_loan_interest_accrual_entry(args): def get_no_of_days_for_interest_accural(loan, posting_date): - last_interest_accrual_date = get_last_accural_date(loan.name) + last_interest_accrual_date = get_last_accrual_date(loan.name) no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 return no_of_days -def get_last_accural_date(loan): +def get_last_accrual_date(loan): last_posting_date = frappe.db.sql(""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` WHERE loan = %s and docstatus = 1""", (loan)) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index e478cb8d43..bb91abd628 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -14,7 +14,7 @@ from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries 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 -from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accural_date +from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import get_per_day_interest, get_last_accrual_date class LoanRepayment(AccountsController): @@ -78,7 +78,7 @@ class LoanRepayment(AccountsController): if self.total_interest_paid > self.interest_payable: if not self.is_term_loan: # get last loan interest accrual date - last_accrual_date = get_last_accural_date(self.against_loan) + last_accrual_date = get_last_accrual_date(self.against_loan) # get posting date upto which interest has to be accrued per_day_interest = flt(get_per_day_interest(self.pending_principal_amount, @@ -87,7 +87,6 @@ class LoanRepayment(AccountsController): 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 @@ -368,7 +367,7 @@ def get_amounts(amounts, against_loan, posting_date): if due_date: pending_days = date_diff(posting_date, due_date) + 1 else: - last_accrual_date = get_last_accural_date(against_loan_doc.name) + last_accrual_date = get_last_accrual_date(against_loan_doc.name) pending_days = date_diff(posting_date, last_accrual_date) + 1 if pending_days > 0: diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index b78c3ba5d8..828df2e35f 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -52,14 +52,16 @@ { "fieldname": "accrual_type", "fieldtype": "Select", + "hidden": 1, "label": "Accrual Type", - "options": "Regular\nRepayment\nDisbursement" + "options": "Regular\nRepayment\nDisbursement", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-06 04:43:56.581670", + "modified": "2020-11-06 13:28:51.478909", "modified_by": "Administrator", "module": "Loan Management", "name": "Process Loan Interest Accrual", From dd94587ef807f6d5e9797a8ddf1db9ff4d9a14ff Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 7 Nov 2020 17:08:30 +0530 Subject: [PATCH 33/40] fix: Loan write off precision issue --- .../loan_interest_accrual.json | 3 ++- .../doctype/loan_write_off/loan_write_off.js | 18 ++++++++++++++++++ .../doctype/loan_write_off/loan_write_off.py | 6 ++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index d6bf08ac51..f157f0df8f 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -142,6 +142,7 @@ "read_only": 1 }, { + "depends_on": "eval:doc.is_term_loan", "fieldname": "paid_principal_amount", "fieldtype": "Currency", "label": "Paid Principal Amount", @@ -177,7 +178,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-06 13:22:40.197916", + "modified": "2020-11-07 05:49:25.448875", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Interest Accrual", diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js index cc5cd0d3a0..4e3319c208 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.js @@ -4,6 +4,12 @@ {% include 'erpnext/loan_management/loan_common.js' %}; frappe.ui.form.on('Loan Write Off', { + loan: function(frm) { + frm.trigger('show_pending_principal_amount'); + }, + onload: function(frm) { + frm.trigger('show_pending_principal_amount'); + }, refresh: function(frm) { frm.set_query('write_off_account', function(){ return { @@ -14,5 +20,17 @@ frappe.ui.form.on('Loan Write Off', { } } }); + }, + show_pending_principal_amount: function(frm) { + if (frm.doc.loan && frm.doc.docstatus === 0) { + frappe.db.get_value('Loan', frm.doc.loan, ['total_payment', 'total_interest_payable', + 'total_principal_paid', 'written_off_amount'], function(values) { + frm.set_df_property('write_off_amount', 'description', + "Pending principal amount is " + cstr(flt(values.total_payment - values.total_interest_payable + - values.total_principal_paid - values.written_off_amount, 2))); + frm.refresh_field('write_off_amount'); + }); + + } } }); diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 823e6a904f..6e402edcd2 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext from frappe import _ -from frappe.utils import getdate, flt +from frappe.utils import getdate, flt, cint from erpnext.controllers.accounts_controller import AccountsController from erpnext.accounts.general_ledger import make_gl_entries @@ -19,10 +19,12 @@ class LoanWriteOff(AccountsController): self.cost_center = erpnext.get_default_cost_center(self.company) def validate_write_off_amount(self): + precision = cint(frappe.db.get_default("currency_precision")) or 2 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) - flt(written_off_amount) + pending_principal_amount = flt(flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount), + precision) if self.write_off_amount > pending_principal_amount: frappe.throw(_("Write off amount cannot be greater than pending principal amount")) From d53abf194e900aa36d96d9793a889923fbd1901c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 9 Nov 2020 18:34:38 +0530 Subject: [PATCH 34/40] fix: Party for loan ledger entries --- .../loan_management/doctype/loan/loan_list.js | 16 ++++++++++++++++ .../loan_disbursement/loan_disbursement.py | 2 -- .../loan_interest_accrual.py | 2 -- .../doctype/loan_repayment/loan_repayment.py | 4 ---- .../doctype/loan_write_off/loan_write_off.py | 2 -- 5 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 erpnext/loan_management/doctype/loan/loan_list.js diff --git a/erpnext/loan_management/doctype/loan/loan_list.js b/erpnext/loan_management/doctype/loan/loan_list.js new file mode 100644 index 0000000000..6591b72996 --- /dev/null +++ b/erpnext/loan_management/doctype/loan/loan_list.js @@ -0,0 +1,16 @@ +// 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]; + }, +}; diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index bda439fb7c..233862bcfe 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -141,8 +141,6 @@ class LoanDisbursement(AccountsController): "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 }) ) 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 d642400cdc..d17f5af490 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 @@ -62,8 +62,6 @@ class LoanInterestAccrual(AccountsController): gle_map.append( self.get_gl_dict({ "account": self.interest_income_account, - "party_type": self.applicant_type, - "party": self.applicant, "against": self.loan_account, "credit": self.interest_amount, "credit_in_account_currency": self.interest_amount, diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index bb91abd628..a8887d7b24 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -234,8 +234,6 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": _("Repayment against loan:") + self.against_loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) @@ -251,8 +249,6 @@ class LoanRepayment(AccountsController): "against_voucher": self.against_loan, "remarks": _("Against Loan:") + self.against_loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) diff --git a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py index 6e402edcd2..54a3f2cbb1 100644 --- a/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py +++ b/erpnext/loan_management/doctype/loan_write_off/loan_write_off.py @@ -63,8 +63,6 @@ class LoanWriteOff(AccountsController): "against_voucher": self.loan, "remarks": _("Against Loan:") + self.loan, "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, "posting_date": getdate(self.posting_date) }) ) From 1924019531a6ed01d1140b83bd9fc094c114f047 Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 10 Nov 2020 13:19:21 +0530 Subject: [PATCH 35/40] fix: refactor --- erpnext/controllers/selling_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e5405b2e43..2a5617c168 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -386,14 +386,12 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set((x.strip() for x in ','.join(po_nos).split(','))))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) if doc_list: - po_no_list = frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) - if po_no_list and po_no_list[0].get('po_no'): - po_nos += [d.po_no for d in po_no_list if d.po_no] + po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')] def set_gross_profit(self): if self.doctype in ["Sales Order", "Quotation"]: From 1e564bc02d4ce292a913865bb7ab434e2042476d Mon Sep 17 00:00:00 2001 From: Afshan Date: Tue, 10 Nov 2020 13:23:59 +0530 Subject: [PATCH 36/40] fix: refactor --- erpnext/controllers/selling_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 2a5617c168..7504746e07 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -380,13 +380,13 @@ class SellingController(StockController): po_nos = [] self.get_po_nos('Sales Order', 'sales_order', po_nos) self.get_po_nos('Delivery Note', 'delivery_note', po_nos) - self.po_no = ', '.join(list(set(po_nos))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def set_pos_for_delivery_note(self): po_nos = [] self.get_po_nos('Sales Order', 'against_sales_order', po_nos) self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos) - self.po_no = ', '.join(list(set((x.strip() for x in ','.join(po_nos).split(','))))) + self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(',')))) def get_po_nos(self, ref_doctype, ref_fieldname, po_nos): doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) From 101bad3ea1d53371a5e1f24559feddb6c70988b5 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 10 Nov 2020 17:53:50 +0530 Subject: [PATCH 37/40] fix: Remarks for penalty GL Entry --- .../doctype/loan_repayment/loan_repayment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index a8887d7b24..415ba993c7 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -216,7 +216,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against loan:") + self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "party_type": self.applicant_type, "party": self.applicant, @@ -232,7 +232,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.penalty_amount, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Repayment against loan:") + self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) @@ -247,7 +247,7 @@ class LoanRepayment(AccountsController): "debit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against Loan: ") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) @@ -263,7 +263,7 @@ class LoanRepayment(AccountsController): "credit_in_account_currency": self.amount_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": _("Against Loan:") + self.against_loan, + "remarks": _("Repayment against Loan: ") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) From d08c91673af3c21f77b402e6d1ee53bf9548344a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Tue, 10 Nov 2020 18:04:30 +0530 Subject: [PATCH 38/40] fix: removed unnecessary filter options --- .../doctype/production_plan/production_plan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 63df5f379d..7daf7069f3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -308,14 +308,14 @@ "fieldname": "sales_order_status", "fieldtype": "Select", "label": "Sales Order Status", - "options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed" + "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver" } ], "icon": "fa fa-calendar", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-03 15:19:18.270529", + "modified": "2020-11-10 18:01:54.991970", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", From a91b68c8689073296815033d12a94063977de20b Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 11 Nov 2020 16:34:43 +0530 Subject: [PATCH 39/40] feat: add communication channel to communication medium (#23793) Co-authored-by: Saqib Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .../communication_medium/communication_medium.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/communication/doctype/communication_medium/communication_medium.json b/erpnext/communication/doctype/communication_medium/communication_medium.json index f009b38877..1e1fe3bf49 100644 --- a/erpnext/communication/doctype/communication_medium/communication_medium.json +++ b/erpnext/communication/doctype/communication_medium/communication_medium.json @@ -1,12 +1,14 @@ { + "actions": [], "autoname": "Prompt", "creation": "2019-06-05 11:48:30.572795", "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "communication_channel", "communication_medium_type", - "catch_all", "column_break_3", + "catch_all", "provider", "disabled", "timeslots_section", @@ -54,9 +56,16 @@ "fieldtype": "Table", "label": "Timeslots", "options": "Communication Medium Timeslot" + }, + { + "fieldname": "communication_channel", + "fieldtype": "Select", + "label": "Communication Channel", + "options": "\nExotel" } ], - "modified": "2019-06-05 11:49:30.769006", + "links": [], + "modified": "2020-10-27 16:22:08.068542", "modified_by": "Administrator", "module": "Communication", "name": "Communication Medium", From 0ea0a7495c5de280cfb97cb8de31012d0aef7081 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Thu, 12 Nov 2020 11:10:59 +0530 Subject: [PATCH 40/40] fix: making company address read-only in delivery note (#23890) --- erpnext/stock/doctype/delivery_note/delivery_note.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ea385c8b2a..3c5129b1ab 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -413,7 +413,8 @@ { "fieldname": "company_address_display", "fieldtype": "Small Text", - "label": "Company Address" + "label": "Company Address", + "read_only": 1 }, { "collapsible": 1, @@ -1255,7 +1256,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-08-03 23:18:47.739997", + "modified": "2020-11-11 14:57:16.388139", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note",