Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Calculate monthly repayment precisely to the fraction of a currency unit, turning rounding up into an optional feature. #36

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -106,6 +106,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 @@ -114,13 +119,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 @@ -244,7 +242,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 @@ -268,13 +266,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")
}
});
});
19 changes: 16 additions & 3 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 @@ -178,13 +179,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 @@ -193,7 +204,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
15 changes: 11 additions & 4 deletions lending/loan_management/doctype/loan/loan.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@

class Loan(AccountsController):
def validate(self):
self.set_loan_amount()
if self.docstatus.is_draft():
self.set_missing_fields()
self.set_loan_amount()

self.validate_loan_amount()
self.set_missing_fields()
self.validate_cost_center()
self.validate_accounts()
self.check_sanctioned_amount_limit()
Expand All @@ -31,8 +33,11 @@ def validate(self):
if self.is_term_loan and not self.is_new():
self.update_draft_schedule()

if not self.is_term_loan or (self.is_term_loan and not self.is_new()):
self.calculate_totals()
if self.docstatus.is_draft():
if self.is_term_loan and not self.is_new():
self.update_draft_schedule()
if not self.is_term_loan or (self.is_term_loan and not self.is_new()):
self.calculate_totals()

def after_insert(self):
if self.is_term_loan:
Expand Down Expand Up @@ -129,6 +134,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 @@ -150,6 +156,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,25 +25,23 @@

class LoanApplication(Document):
def validate(self):
self.set_pledge_amount()
self.set_loan_amount()
if self.docstatus.is_draft():
self.set_pledge_amount()
self.set_loan_amount()

self.validate_loan_type()
self.validate_loan_amount()

if self.is_term_loan:
self.validate_repayment_method()

self.validate_loan_type()
if self.docstatus.is_draft():
self.get_repayment_details()

self.get_repayment_details()
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.repayment_amount:
frappe.throw(_("Please enter repayment Amount"))
if self.repayment_amount > self.loan_amount:
frappe.throw(_("Monthly Repayment Amount cannot be greater than Loan Amount"))

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 @@ -18,7 +18,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 @@ -72,6 +72,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 @@ -184,4 +191,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 @@ -167,13 +168,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