From 3cd35c0def8e5da30a2f948e8eb06bd09a632fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Mon, 18 Sep 2023 00:56:08 +0200 Subject: [PATCH] fix: Calculate monthly repayment precisely to the fraction of a currency, turning rounding up into an optional feature. --- lending/loan_management/doctype/loan/loan.js | 25 +++++----------- .../loan_management/doctype/loan/loan.json | 21 ++++++++++--- lending/loan_management/doctype/loan/loan.py | 2 ++ .../loan_application/loan_application.js | 19 +++--------- .../loan_application/loan_application.json | 30 +++++++++++++------ .../loan_application/loan_application.py | 17 +++++------ .../loan_repayment_schedule.json | 11 +++++-- .../loan_repayment_schedule.py | 18 +++++------ 8 files changed, 77 insertions(+), 66 deletions(-) diff --git a/lending/loan_management/doctype/loan/loan.js b/lending/loan_management/doctype/loan/loan.js index 800cf2c9..eac21293 100644 --- a/lending/loan_management/doctype/loan/loan.js +++ b/lending/loan_management/doctype/loan/loan.js @@ -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"); @@ -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: { @@ -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 => { @@ -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") - } -}); \ No newline at end of file +}); diff --git a/lending/loan_management/doctype/loan/loan.json b/lending/loan_management/doctype/loan/loan.json index 63bc6302..8af9912c 100644 --- a/lending/loan_management/doctype/loan/loan.json +++ b/lending/loan_management/doctype/loan/loan.json @@ -30,6 +30,7 @@ "maximum_loan_amount", "repayment_method", "repayment_periods", + "repayment_round_up", "monthly_repayment_amount", "repayment_start_date", "is_term_loan", @@ -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", @@ -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, @@ -511,4 +524,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/lending/loan_management/doctype/loan/loan.py b/lending/loan_management/doctype/loan/loan.py index 88d10804..5b350f39 100644 --- a/lending/loan_management/doctype/loan/loan.py +++ b/lending/loan_management/doctype/loan/loan.py @@ -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, @@ -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, diff --git a/lending/loan_management/doctype/loan_application/loan_application.js b/lending/loan_management/doctype/loan_application/loan_application.js index ffef3e7a..4adedf72 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.js +++ b/lending/loan_management/doctype/loan_application/loan_application.js @@ -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 { @@ -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") { @@ -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); }, diff --git a/lending/loan_management/doctype/loan_application/loan_application.json b/lending/loan_management/doctype/loan_application/loan_application.json index f91fa072..a801795a 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.json +++ b/lending/loan_management/doctype/loan_application/loan_application.json @@ -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", @@ -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" ], @@ -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", @@ -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", @@ -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", @@ -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 diff --git a/lending/loan_management/doctype/loan_application/loan_application.py b/lending/loan_management/doctype/loan_application/loan_application.py index ca678d3b..97316df2 100644 --- a/lending/loan_management/doctype/loan_application/loan_application.py +++ b/lending/loan_management/doctype/loan_application/loan_application.py @@ -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() @@ -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): @@ -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) @@ -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 diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json index 5b9977c2..6bf13892 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.json @@ -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", @@ -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", @@ -176,4 +183,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py index 4bf307b3..a6355c95 100644 --- a/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py +++ b/lending/loan_management/doctype/loan_repayment_schedule/loan_repayment_schedule.py @@ -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": @@ -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