Skip to content

Commit

Permalink
fix: Calculate monthly repayment precisely to the fraction of a curre…
Browse files Browse the repository at this point in the history
…ncy, turning rounding up into an optional feature.
  • Loading branch information
bosue committed Sep 18, 2023
1 parent 1566693 commit 3cd35c0
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 66 deletions.
25 changes: 7 additions & 18 deletions lending/loan_management/doctype/loan/loan.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ frappe.ui.form.on('Loan', {
frm.trigger("toggle_fields");
},

is_term_loan: function(frm) {
frm.doc.repayment_method = frm.doc.repayment_schedule_type = "";
frm.doc.monthly_repayment_amount = frm.doc.repayment_periods = "";
},

repayment_schedule_type: function(frm) {
if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") {
frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date");
Expand All @@ -115,13 +120,6 @@ frappe.ui.form.on('Loan', {
}
},

loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", frm.doc.is_term_loan);
},


make_loan_disbursement: function (frm) {
frappe.call({
args: {
Expand Down Expand Up @@ -245,7 +243,7 @@ frappe.ui.form.on('Loan', {
callback: function (r) {
if (!r.exc && r.message) {

let loan_fields = ["loan_type", "loan_amount", "repayment_method",
let loan_fields = ["loan_type", "loan_amount", "repayment_method", "repayment_round_up",
"monthly_repayment_amount", "repayment_periods", "rate_of_interest", "is_secured_loan"]

loan_fields.forEach(field => {
Expand All @@ -269,13 +267,4 @@ frappe.ui.form.on('Loan', {
});
}
},

repayment_method: function (frm) {
frm.trigger("toggle_fields")
},

toggle_fields: function (frm) {
frm.toggle_enable("monthly_repayment_amount", frm.doc.repayment_method == "Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method == "Repay Over Number of Periods")
}
});
});
21 changes: 17 additions & 4 deletions lending/loan_management/doctype/loan/loan.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"maximum_loan_amount",
"repayment_method",
"repayment_periods",
"repayment_round_up",
"monthly_repayment_amount",
"repayment_start_date",
"is_term_loan",
Expand Down Expand Up @@ -179,13 +180,23 @@
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
"mandatory_depends_on": "is_term_loan",
"options": "Repay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"depends_on": "is_term_loan",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
"label": "Repayment Period in Months",
"mandatory_depends_on": "eval: doc.repayment_method == 'Repay Over Number of Periods'",
"read_only_depends_on": "eval: doc.repayment_method != 'Repay Over Number of Periods'"
},
{
"default": "0",
"depends_on": "eval: doc.repayment_method == 'Repay Over Number of Periods'",
"fieldname": "repayment_round_up",
"fieldtype": "Check",
"label": "Round Up"
},
{
"depends_on": "is_term_loan",
Expand All @@ -194,7 +205,9 @@
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
"mandatory_depends_on": "eval: doc.repayment_method == 'Repay Fixed Amount per Period'",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.repayment_method != 'Repay Fixed Amount per Period'"
},
{
"collapsible": 1,
Expand Down Expand Up @@ -511,4 +524,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}
2 changes: 2 additions & 0 deletions lending/loan_management/doctype/loan/loan.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def make_draft_schedule(self):
"repayment_method": self.repayment_method,
"repayment_start_date": self.repayment_start_date,
"repayment_periods": self.repayment_periods,
"repayment_round_up": self.repayment_round_up,
"loan_amount": self.loan_amount,
"monthly_repayment_amount": self.monthly_repayment_amount,
"loan_type": self.loan_type,
Expand All @@ -135,6 +136,7 @@ def update_draft_schedule(self):
"repayment_periods": self.repayment_periods,
"repayment_method": self.repayment_method,
"repayment_start_date": self.repayment_start_date,
"repayment_round_up": self.repayment_round_up,
"posting_date": self.posting_date,
"loan_amount": self.loan_amount,
"monthly_repayment_amount": self.monthly_repayment_amount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ frappe.ui.form.on('Loan Application', {
}
},
refresh: function(frm) {
frm.trigger("toggle_fields");
frm.trigger("add_toolbar_buttons");
frm.set_query('loan_type', () => {
return {
Expand All @@ -22,18 +21,12 @@ frappe.ui.form.on('Loan Application', {
};
});
},
repayment_method: function(frm) {
is_term_loan: function(frm) {
frm.doc.repayment_method = "";
frm.doc.repayment_amount = frm.doc.repayment_periods = "";
frm.trigger("toggle_fields");
frm.trigger("toggle_required");
},
toggle_fields: function(frm) {
frm.toggle_enable("repayment_amount", frm.doc.repayment_method=="Repay Fixed Amount per Period")
frm.toggle_enable("repayment_periods", frm.doc.repayment_method=="Repay Over Number of Periods")
},
toggle_required: function(frm){
frm.toggle_reqd("repayment_amount", cint(frm.doc.repayment_method=='Repay Fixed Amount per Period'))
frm.toggle_reqd("repayment_periods", cint(frm.doc.repayment_method=='Repay Over Number of Periods'))
repayment_method: function(frm) {
frm.doc.repayment_amount = frm.doc.repayment_periods = "";
},
add_toolbar_buttons: function(frm) {
if (frm.doc.status == "Approved") {
Expand Down Expand Up @@ -85,10 +78,6 @@ frappe.ui.form.on('Loan Application', {
}
})
},
is_term_loan: function(frm) {
frm.set_df_property('repayment_method', 'hidden', 1 - frm.doc.is_term_loan);
frm.set_df_property('repayment_method', 'reqd', frm.doc.is_term_loan);
},
is_secured_loan: function(frm) {
frm.set_df_property('proposed_pledges', 'reqd', frm.doc.is_secured_loan);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "ACC-LOAP-.YYYY.-.#####",
"creation": "2019-08-29 17:46:49.201740",
"creation": "2023-09-18 18:58:25.087137",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
Expand All @@ -26,10 +26,11 @@
"maximum_loan_amount",
"repayment_info",
"repayment_method",
"total_payable_amount",
"column_break_11",
"repayment_periods",
"repayment_round_up",
"repayment_amount",
"column_break_11",
"total_payable_amount",
"total_payable_interest",
"amended_from"
],
Expand Down Expand Up @@ -127,7 +128,8 @@
"fieldname": "repayment_method",
"fieldtype": "Select",
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
"mandatory_depends_on": "eval: doc.is_term_loan == 1",
"options": "Repay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"fetch_from": "loan_type.rate_of_interest",
Expand All @@ -149,17 +151,26 @@
"fieldtype": "Column Break"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_amount",
"fieldtype": "Currency",
"label": "Monthly Repayment Amount",
"options": "Company:company:default_currency"
"mandatory_depends_on": "eval: doc.repayment_method == 'Repay Fixed Amount per Period'",
"options": "Company:company:default_currency",
"read_only_depends_on": "eval: doc.repayment_method != 'Repay Fixed Amount per Period'"
},
{
"default": "0",
"depends_on": "eval: doc.repayment_method == 'Repay Over Number of Periods'",
"fieldname": "repayment_round_up",
"fieldtype": "Check",
"label": "Round Up"
},
{
"depends_on": "repayment_method",
"fieldname": "repayment_periods",
"fieldtype": "Int",
"label": "Repayment Period in Months"
"label": "Repayment Period in Months",
"mandatory_depends_on": "eval: doc.repayment_method == 'Repay Over Number of Periods'",
"read_only_depends_on": "eval: doc.repayment_method != 'Repay Over Number of Periods'"
},
{
"fieldname": "total_payable_amount",
Expand Down Expand Up @@ -215,7 +226,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 18:24:40.119647",
"modified": "2023-09-18 19:35:16.307804",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Application",
Expand Down Expand Up @@ -276,6 +287,7 @@
"search_fields": "applicant_type, applicant, loan_type, loan_amount",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"timeline_field": "applicant",
"title_field": "applicant",
"track_changes": 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@

class LoanApplication(Document):
def validate(self):
if not self.docstatus.is_draft():
return

self.set_pledge_amount()
self.set_loan_amount()
self.validate_loan_amount()
Expand All @@ -38,13 +41,8 @@ def validate(self):
self.check_sanctioned_amount_limit()

def validate_repayment_method(self):
if self.repayment_method == "Repay Over Number of Periods" and not self.repayment_periods:
frappe.throw(_("Please enter Repayment Periods"))

if self.repayment_method == "Repay Fixed Amount per Period":
if not self.monthly_repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if self.monthly_repayment_amount > self.loan_amount:
if self.repayment_amount > self.loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))

def validate_loan_type(self):
Expand Down Expand Up @@ -106,9 +104,10 @@ def get_repayment_details(self):

if self.is_term_loan:
if self.repayment_method == "Repay Over Number of Periods":
self.repayment_amount = get_monthly_repayment_amount(
amount = get_monthly_repayment_amount(
self.loan_amount, self.rate_of_interest, self.repayment_periods
)
self.repayment_amount = math.ceil(amount) if self.repayment_round_up else amount

if self.repayment_method == "Repay Fixed Amount per Period":
monthly_interest_rate = flt(self.rate_of_interest) / (12 * 100)
Expand All @@ -133,8 +132,8 @@ def calculate_payable_amount(self):
self.total_payable_interest = 0

while balance_amount > 0:
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100))
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount)
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12 * 100), 2)
balance_amount = rounded(balance_amount + interest_amount - self.repayment_amount, 2)

self.total_payable_interest += interest_amount

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"loan_type",
"repayment_schedule_type",
"repayment_method",
"repayment_periods",
"repayment_round_up",
"monthly_repayment_amount",
"repayment_start_date",
"section_break_6rpg",
Expand Down Expand Up @@ -71,6 +71,13 @@
"label": "Repayment Method",
"options": "\nRepay Fixed Amount per Period\nRepay Over Number of Periods"
},
{
"default": "0",
"depends_on": "eval: doc.repayment_method == 'Repay Over Number of Periods'",
"fieldname": "repayment_round_up",
"fieldtype": "Check",
"label": "Round Up"
},
{
"fieldname": "monthly_repayment_amount",
"fieldtype": "Currency",
Expand Down Expand Up @@ -176,4 +183,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ def validate(self):

def set_missing_fields(self):
if self.repayment_method == "Repay Over Number of Periods":
self.monthly_repayment_amount = get_monthly_repayment_amount(
amount = get_monthly_repayment_amount(
self.loan_amount, self.rate_of_interest, self.repayment_periods
)
self.monthly_repayment_amount = math.ceil(amount) if self.repayment_round_up else amount

def set_repayment_period(self):
if self.repayment_method == "Repay Fixed Amount per Period":
Expand Down Expand Up @@ -163,13 +164,12 @@ def add_single_month(date):
return add_months(date, 1)


def get_monthly_repayment_amount(loan_amount, rate_of_interest, repayment_periods):
if rate_of_interest:
monthly_interest_rate = flt(rate_of_interest) / (12 * 100)
monthly_repayment_amount = math.ceil(
(loan_amount * monthly_interest_rate * (1 + monthly_interest_rate) ** repayment_periods)
/ ((1 + monthly_interest_rate) ** repayment_periods - 1)
def get_monthly_repayment_amount(loan_amount, yearly_intrate, periods):
if yearly_intrate:
monthly_intrate = yearly_intrate / (12 * 100)
annuity_factor = (monthly_intrate * (1 + monthly_intrate) ** periods) / (
(1 + monthly_intrate) ** periods - 1
)
return loan_amount * annuity_factor
else:
monthly_repayment_amount = math.ceil(flt(loan_amount) / repayment_periods)
return monthly_repayment_amount
return loan_amount / periods

0 comments on commit 3cd35c0

Please sign in to comment.