Merge pull request #21305 from deepeshgarg007/partial_loan_repayment_fixes

fix: Loan Repayment code clean up and other fixes
This commit is contained in:
Deepesh Garg 2020-04-17 11:16:38 +05:30 committed by GitHub
commit 3c6795a52d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 191 additions and 173 deletions

View File

@ -776,22 +776,16 @@ class SalarySlip(TransactionBase):
for payment in self.get('loans'):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
if payment.total_payment > total_amount:
frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment),
frappe.bold(total_amount), frappe.bold(payment.loan)))
if payment.interest_amount > amounts['interest_amount']:
frappe.throw(_("""Row {0}: Paid Interest amount {1} is greater than pending interest amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.interest_amount),
frappe.bold(amounts['interest_amount']), frappe.bold(payment.loan)))
if payment.principal_amount > amounts['payable_principal_amount']:
frappe.throw(_("""Row {0}: Paid Principal amount {1} is greater than pending principal amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.principal_amount),
frappe.bold(amounts['payable_principal_amount']), frappe.bold(payment.loan)))
payment.total_payment = payment.interest_amount + payment.principal_amount
self.total_interest_amount += payment.interest_amount
self.total_principal_amount += payment.principal_amount
self.total_loan_repayment = self.total_interest_amount + self.total_principal_amount
self.total_loan_repayment += payment.total_payment
def get_loan_details(self):

View File

@ -149,13 +149,19 @@ class TestLoan(unittest.TestCase):
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db()
self.assertEquals(amounts[0], repayment_entry.interest_payable)
self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid -
penalty_amount - amounts[0], 2))
def test_loan_closure_repayment(self):
pledges = []
@ -189,15 +195,19 @@ 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, 5),
"Loan Closure", 13315.0681)
repayment_entry.save()
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
repayment_entry.amount_paid = repayment_entry.payable_amount
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3))
unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \
/ (days_in_year(get_datetime(first_date).year) * 100)
self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3),
flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
repayment_entry.submit()
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
@ -227,57 +237,15 @@ 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(get_last_day(nowdate()), 5),
"Regular Payment", 89768.7534247)
"Regular Payment", 89768.75)
repayment_entry.save()
repayment_entry.submit()
repayment_entry.load_from_db()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
self.assertEquals(repayment_entry.interest_payable, 11250.00)
self.assertEquals(repayment_entry.payable_principal_amount, 78303.00)
def test_partial_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
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
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime().year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 15))
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 30))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
lia1 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 1}, 'name')
lia2 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 0}, 'name')
self.assertTrue(lia1)
self.assertTrue(lia2)
self.assertEquals(amounts[0], 11250.00)
self.assertEquals(amounts[1], 78303.00)
def test_security_shortfall(self):
pledges = []
@ -294,7 +262,7 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 100
frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
where loan_security='Test Security 2'""")
create_process_loan_security_shortfall()

View File

@ -15,12 +15,13 @@
"company",
"posting_date",
"is_term_loan",
"is_paid",
"section_break_7",
"pending_principal_amount",
"payable_principal_amount",
"paid_principal_amount",
"column_break_14",
"interest_amount",
"paid_interest_amount",
"section_break_15",
"process_loan_interest_accrual",
"repayment_schedule_name",
@ -101,13 +102,6 @@
"label": "Company",
"options": "Company"
},
{
"default": "0",
"fieldname": "is_paid",
"fieldtype": "Check",
"label": "Is Paid",
"read_only": 1
},
{
"default": "0",
"fetch_from": "loan.is_term_loan",
@ -143,12 +137,24 @@
"hidden": 1,
"label": "Repayment Schedule Name",
"read_only": 1
},
{
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
}
],
"in_create": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-04-10 18:31:02.369857",
"modified": "2020-04-16 11:24:23.258404",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",

View File

@ -30,9 +30,8 @@
"reference_number",
"column_break_21",
"reference_date",
"paid_accrual_entries",
"partial_paid_entry",
"principal_amount_paid",
"repayment_details",
"amended_from"
],
"fields": [
@ -155,13 +154,6 @@
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "paid_accrual_entries",
"fieldtype": "Text",
"hidden": 1,
"label": "Paid Accrual Entries",
"read_only": 1
},
{
"default": "0",
"fetch_from": "against_loan.is_term_loan",
@ -197,13 +189,6 @@
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "partial_paid_entry",
"fieldtype": "Text",
"hidden": 1,
"label": "Partial Paid Entry",
"read_only": 1
},
{
"default": "0.0",
"fieldname": "principal_amount_paid",
@ -225,11 +210,18 @@
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{
"fieldname": "repayment_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Repayment Details",
"options": "Loan Repayment Detail"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-02-26 06:18:54.934538",
"modified": "2020-04-16 18:14:45.166754",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment",
@ -264,7 +256,6 @@
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1

View File

@ -19,11 +19,11 @@ class LoanRepayment(AccountsController):
def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
self.set_missing_values(amounts)
def before_submit(self):
self.mark_as_paid()
self.validate_amount()
self.allocate_amounts(amounts['pending_accrual_entries'])
def on_submit(self):
self.update_paid_amount()
self.make_gl_entries()
def on_cancel(self):
@ -38,32 +38,25 @@ class LoanRepayment(AccountsController):
self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.interest_payable:
self.interest_payable = amounts['interest_amount']
self.interest_payable = flt(amounts['interest_amount'], 2)
if not self.penalty_amount:
self.penalty_amount = amounts['penalty_amount']
self.penalty_amount = flt(amounts['penalty_amount'], 2)
if not self.pending_principal_amount:
self.pending_principal_amount = amounts['pending_principal_amount']
self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2)
if not self.payable_principal_amount and self.is_term_loan:
self.payable_principal_amount = amounts['payable_principal_amount']
self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2)
if not self.payable_amount:
self.payable_amount = amounts['payable_amount']
if amounts.get('paid_accrual_entries'):
self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries'))
self.payable_amount = flt(amounts['payable_amount'], 2)
if amounts.get('due_date'):
self.due_date = amounts.get('due_date')
def mark_as_paid(self):
paid_entries = []
paid_amount = self.amount_paid
interest_paid = paid_amount
if not paid_amount:
def validate_amount(self):
if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero"))
if self.amount_paid < self.penalty_amount:
@ -74,37 +67,15 @@ class LoanRepayment(AccountsController):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg)
def update_paid_amount(self):
loan = frappe.get_doc("Loan", self.against_loan)
if self.paid_accrual_entries:
paid_accrual_entries = json.loads(self.paid_accrual_entries)
if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries:
interest_paid = paid_amount - self.penalty_amount
for lia, interest_amount in iteritems(paid_accrual_entries):
if interest_amount <= interest_paid:
paid_entries.append(lia)
interest_paid -= interest_amount
elif interest_paid:
self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount})
frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount",
interest_amount - interest_paid)
interest_paid = 0
if paid_entries:
self.paid_accrual_entries = frappe.as_json(paid_entries)
else:
self.paid_accrual_entries = ""
if interest_paid:
self.principal_amount_paid = interest_paid
if paid_entries:
frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
SET is_paid = 1 where name in (%s)""" #nosec
% ", ".join(['%s']*len(paid_entries)), tuple(paid_entries))
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2):
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
@ -116,21 +87,14 @@ class LoanRepayment(AccountsController):
update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self):
loan = frappe.get_doc("Loan", self.against_loan)
if self.paid_accrual_entries:
paid_accrual_entries = json.loads(self.paid_accrual_entries)
if self.paid_accrual_entries:
frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
SET is_paid = 0 where name in (%s)""" #nosec
% ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries))
if self.partial_paid_entry:
partial_paid_entry = json.loads(self.partial_paid_entry)
frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount",
partial_paid_entry["interest_amount"])
for payment in self.repayment_details:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` - %s,
paid_interest_amount = `paid_interest_amount` - %s
WHERE name = %s""",
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
@ -139,6 +103,38 @@ 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):
self.set('repayment_details', [])
self.principal_amount_paid = 0
if self.amount_paid - self.penalty_amount > 0 and paid_entries:
interest_paid = self.amount_paid - self.penalty_amount
for lia, amounts in iteritems(paid_entries):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount']
self.principal_amount_paid += paid_principal
interest_paid -= (interest_amount + paid_principal)
elif interest_paid:
if interest_paid >= amounts['interest_amount']:
interest_amount = amounts['interest_amount']
paid_principal = interest_paid - interest_amount
self.principal_amount_paid += paid_principal
interest_paid = 0
else:
interest_amount = interest_paid
interest_paid = 0
paid_principal=0
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount,
'paid_principal_amount': paid_principal
})
if interest_paid:
self.principal_amount_paid += interest_paid
def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan)
@ -223,7 +219,7 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
"posting_date": posting_date,
"applicant": applicant,
"penalty_amount": penalty_amount,
"interst_payable": interest_payable,
"interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid,
"loan_type": loan_type
@ -232,15 +228,22 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
return lr
def get_accrued_interest_entries(against_loan):
accrued_interest_entries = frappe.get_all("Loan Interest Accrual",
fields=["name", "interest_amount", "posting_date", "payable_principal_amount"],
filters = {
"loan": against_loan,
"is_paid": 0,
"docstatus": 1
}, order_by="posting_date")
return accrued_interest_entries
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
FROM
`tabLoan Interest Accrual`
WHERE
loan = %s
AND (interest_amount - paid_interest_amount > 0 OR
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
""", (against_loan), as_dict=1)
return unpaid_accrued_entries
# 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
@ -273,8 +276,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
total_pending_interest += entry.interest_amount
payable_principal_amount += entry.payable_principal_amount
pending_accrual_entries.setdefault(entry.name,
flt(entry.interest_amount) + flt(entry.payable_principal_amount))
pending_accrual_entries.setdefault(entry.name, {
'interest_amount': flt(entry.interest_amount),
'payable_principal_amount': flt(entry.payable_principal_amount)
})
final_due_date = due_date
@ -291,7 +296,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
amounts["interest_amount"] = total_pending_interest
amounts["penalty_amount"] = penalty_amount
amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount
amounts["paid_accrual_entries"] = pending_accrual_entries
amounts["pending_accrual_entries"] = pending_accrual_entries
if final_due_date:
amounts["due_date"] = final_due_date

View File

@ -0,0 +1,43 @@
{
"actions": [],
"creation": "2020-04-15 18:31:54.026923",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_interest_accrual",
"paid_principal_amount",
"paid_interest_amount"
],
"fields": [
{
"fieldname": "loan_interest_accrual",
"fieldtype": "Link",
"label": "Loan Interest Accrual",
"options": "Loan Interest Accrual"
},
{
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
}
],
"istable": 1,
"links": [],
"modified": "2020-04-15 21:50:03.837019",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- 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
from frappe.model.document import Document
class LoanRepaymentDetail(Document):
pass

View File

@ -28,7 +28,6 @@
{
"fieldname": "loan_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Account",
"options": "Account",
"read_only": 1
@ -50,21 +49,23 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Principal Amount",
"options": "Company:company:default_currency"
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "interest_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Interest Amount",
"options": "Company:company:default_currency"
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_payment",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Payment",
"options": "Company:company:default_currency",
"read_only": 1
"options": "Company:company:default_currency"
},
{
"fieldname": "loan_repayment_entry",
@ -84,7 +85,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-04-09 20:01:53.546364",
"modified": "2020-04-16 13:17:04.798335",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Salary Slip Loan",

View File

@ -287,7 +287,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
if (in_list(['serial_no', 'batch_no'], field)) {
args[field] = value;
}
// add to cur_frm
const item = this.frm.add_child('items', args);
frappe.flags.hide_serial_batch_dialog = true;