From 76c6ccab5d0d518cfc8aacddb72f13f995e8922e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Sep 2022 10:13:17 +0530 Subject: [PATCH 01/12] feat: Repayment schedule types for term loans --- .../loan_management/doctype/loan/loan.json | 2 +- erpnext/loan_management/doctype/loan/loan.py | 86 ++++++++++++++----- .../doctype/loan_type/loan_type.json | 11 ++- .../quick_stock_balance.py | 1 + 4 files changed, 78 insertions(+), 22 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 47488f43ce..d245edc1ce 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -407,7 +407,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-07-12 11:50:31.957360", + "modified": "2022-09-29 11:50:31.957360", "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 d84eef6d8c..d55af703c0 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -7,7 +7,17 @@ import math import frappe from frappe import _ -from frappe.utils import add_months, flt, get_last_day, getdate, now_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + date_diff, + flt, + get_first_day, + get_last_day, + getdate, + now_datetime, + nowdate, +) import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_payment_entry @@ -107,30 +117,66 @@ class Loan(AccountsController): if not self.repayment_start_date: frappe.throw(_("Repayment Start Date is mandatory for term loans")) + schedule_type = frappe.db.get_value("Loan Type", self.loan_type, "repayment_schedule_type") self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount - while balance_amount > 0: - interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12 * 100)) - principal_amount = self.monthly_repayment_amount - interest_amount - balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) - if balance_amount < 0: - principal_amount += balance_amount - balance_amount = 0.0 - total_payment = principal_amount + interest_amount - self.append( - "repayment_schedule", - { - "payment_date": payment_date, - "principal_amount": principal_amount, - "interest_amount": interest_amount, - "total_payment": total_payment, - "balance_loan_amount": balance_amount, - }, + while balance_amount > 0: + interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts( + payment_date, balance_amount, schedule_type ) - next_payment_date = add_single_month(payment_date) - payment_date = next_payment_date + + if schedule_type == "Pro-rated calendar months": + next_payment_date = add_days(get_last_day(payment_date), 1) + payment_date = next_payment_date + + self.add_repayment_schedule_row( + payment_date, principal_amount, interest_amount, total_payment, balance_amount + ) + + if schedule_type == "Monthly as per repayment start date": + next_payment_date = add_single_month(payment_date) + payment_date = next_payment_date + + def get_amounts(self, payment_date, balance_amount, schedule_type): + first_day_of_month = get_first_day(payment_date) + + if schedule_type == "Monthly as per repayment start date": + days = 1 + months = 12 + else: + if first_day_of_month == payment_date: + days = 30 + months = 365 + else: + days = date_diff(get_last_day(payment_date), payment_date) + months = 365 + + interest_amount = flt(balance_amount * flt(self.rate_of_interest) * days / (months * 100)) + principal_amount = self.monthly_repayment_amount - interest_amount + balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount) + if balance_amount < 0: + principal_amount += balance_amount + balance_amount = 0.0 + + total_payment = principal_amount + interest_amount + + return interest_amount, principal_amount, balance_amount, total_payment + + def add_repayment_schedule_row( + self, payment_date, principal_amount, interest_amount, total_payment, balance_loan_amount + ): + self.append( + "repayment_schedule", + { + "payment_date": payment_date, + "principal_amount": principal_amount, + "interest_amount": interest_amount, + "total_payment": total_payment, + "balance_loan_amount": balance_loan_amount, + }, + ) def set_repayment_period(self): if self.repayment_method == "Repay Fixed Amount per Period": diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index 00337e4b4c..e1ed3caf8f 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -16,6 +16,7 @@ "company", "is_term_loan", "disabled", + "repayment_schedule_type", "description", "account_details_section", "mode_of_payment", @@ -157,12 +158,20 @@ "label": "Disbursement Account", "options": "Account", "reqd": 1 + }, + { + "depends_on": "is_term_loan", + "fieldname": "repayment_schedule_type", + "fieldtype": "Select", + "label": "Repayment Schedule Type", + "mandatory_depends_on": "is_term_loan", + "options": "\nMonthly as per repayment start date\nPro-rated calendar months" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-01-25 16:23:57.009349", + "modified": "2022-09-28 21:31:01.278941", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 846be0b9bd..403d874689 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -15,6 +15,7 @@ class QuickStockBalance(Document): @frappe.whitelist() def get_stock_item_details(warehouse, date, item=None, barcode=None): + print(warehouse, date, item, "########") out = {} if barcode: out["item"] = frappe.db.get_value( From 3466461eb3ec97394a5d1e29f0a5ae1df5987042 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Sep 2022 10:19:58 +0530 Subject: [PATCH 02/12] chore: Remove print statements --- erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py index 403d874689..846be0b9bd 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.py @@ -15,7 +15,6 @@ class QuickStockBalance(Document): @frappe.whitelist() def get_stock_item_details(warehouse, date, item=None, barcode=None): - print(warehouse, date, item, "########") out = {} if barcode: out["item"] = frappe.db.get_value( From 2ddee50f27277fa951a2b236b513f377afdc611a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Sep 2022 10:39:45 +0530 Subject: [PATCH 03/12] chore: Update labels as per repayment type --- erpnext/loan_management/doctype/loan/loan.js | 8 ++++++++ erpnext/loan_management/doctype/loan/loan.json | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index 38328e6967..c386ce3ac0 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -103,6 +103,14 @@ frappe.ui.form.on('Loan', { frm.trigger("toggle_fields"); }, + 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"); + } else { + frm.set_df_property("repayment_start_date", "label", "Repayment Start Date"); + } + }, + loan_type: function(frm) { frm.toggle_reqd("repayment_method", frm.doc.is_term_loan); frm.toggle_display("repayment_method", frm.doc.is_term_loan); diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index d245edc1ce..dc8b03e89d 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -18,6 +18,7 @@ "status", "section_break_8", "loan_type", + "repayment_schedule_type", "loan_amount", "rate_of_interest", "is_secured_loan", @@ -158,7 +159,8 @@ "depends_on": "is_term_loan", "fieldname": "repayment_start_date", "fieldtype": "Date", - "label": "Repayment Start Date" + "label": "Repayment Start Date", + "mandatory_depends_on": "is_term_loan" }, { "fieldname": "column_break_11", @@ -402,12 +404,20 @@ "fieldname": "is_npa", "fieldtype": "Check", "label": "Is NPA" + }, + { + "depends_on": "is_term_loan", + "fetch_from": "loan_type.repayment_schedule_type", + "fieldname": "repayment_schedule_type", + "fieldtype": "Data", + "label": "Repayment Schedule Type", + "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-09-29 11:50:31.957360", + "modified": "2022-09-30 10:36:47.902903", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan", From 679b5ed5514b1fc7ee2840326f073764333fe3b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Sep 2022 14:06:06 +0530 Subject: [PATCH 04/12] chore: Add patch to update repayment schedule type in loan documents --- erpnext/loan_management/doctype/loan/test_loan.py | 1 + erpnext/patches.txt | 1 + .../patches/v13_0/update_schedule_type_in_loans.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v13_0/update_schedule_type_in_loans.py diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index da05c8e90c..2608e42e08 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -1042,6 +1042,7 @@ def create_loan_type( "company": "_Test Company", "loan_name": loan_name, "is_term_loan": is_term_loan, + "repayment_schedule_type": "Monthly as per repayment start date", "maximum_loan_amount": maximum_loan_amount, "rate_of_interest": rate_of_interest, "penalty_interest_rate": penalty_interest_rate, diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2a0ca8c496..379b7fea90 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -315,3 +315,4 @@ erpnext.patches.v14_0.fix_crm_no_of_employees erpnext.patches.v14_0.create_accounting_dimensions_in_subcontracting_doctypes erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger +erpnext.patches.v13_0.update_schedule_type_in_loans diff --git a/erpnext/patches/v13_0/update_schedule_type_in_loans.py b/erpnext/patches/v13_0/update_schedule_type_in_loans.py new file mode 100644 index 0000000000..e5b5f64360 --- /dev/null +++ b/erpnext/patches/v13_0/update_schedule_type_in_loans.py @@ -0,0 +1,14 @@ +import frappe + + +def execute(): + loan = frappe.qb.DocType("Loan") + loan_type = frappe.qb.DocType("Loan Type") + + frappe.qb.update(loan_type).set( + loan_type.repayment_schedule_type, "Monthly as per repayment start date" + ).where(loan_type.is_term_loan == 1).run() + + frappe.qb.update(loan).set( + loan.repayment_schedule_type, "Monthly as per repayment start date" + ).where(loan.is_term_loan == 1).run() From bf7a51791aec5c1e1c522e5243e391a05b4bf1f7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 30 Sep 2022 15:29:07 +0530 Subject: [PATCH 05/12] chore: label post save --- erpnext/loan_management/doctype/loan/loan.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/loan_management/doctype/loan/loan.js b/erpnext/loan_management/doctype/loan/loan.js index c386ce3ac0..20e2b0b201 100644 --- a/erpnext/loan_management/doctype/loan/loan.js +++ b/erpnext/loan_management/doctype/loan/loan.js @@ -61,6 +61,10 @@ frappe.ui.form.on('Loan', { }, refresh: function (frm) { + if (frm.doc.repayment_schedule_type == "Pro-rated calendar months") { + frm.set_df_property("repayment_start_date", "label", "Interest Calculation Start Date"); + } + 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() { From 23f0bb45b01fbe7f1884bceb7a457969df79034e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 19 Oct 2022 12:26:56 +0200 Subject: [PATCH 06/12] fix: unset contact details --- erpnext/public/js/utils/party.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 58594b0a13..644adff1e2 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -242,20 +242,29 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { }); }; -erpnext.utils.get_contact_details = function(frm) { +erpnext.utils.get_contact_details = function (frm) { if (frm.updating_party_details) return; if (frm.doc["contact_person"]) { frappe.call({ method: "frappe.contacts.doctype.contact.contact.get_contact_details", - args: {contact: frm.doc.contact_person }, - callback: function(r) { - if (r.message) - frm.set_value(r.message); - } - }) + args: { contact: frm.doc.contact_person }, + callback: function (r) { + if (r.message) frm.set_value(r.message); + }, + }); + } else { + frm.set_value({ + contact_person: "", + contact_display: "", + contact_email: "", + contact_mobile: "", + contact_phone: "", + contact_designation: "", + contact_department: "", + }); } -} +}; erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) { if (!value) { From ef0cb17fafce696fff11685a5cf74e936a3f0508 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 22 Oct 2022 23:46:01 +0530 Subject: [PATCH 07/12] chore: Add repayment date on option --- erpnext/loan_management/doctype/loan/loan.py | 34 ++++-- .../loan_management/doctype/loan/test_loan.py | 104 +++++++++++++++++- .../doctype/loan_type/loan_type.json | 13 ++- 3 files changed, 138 insertions(+), 13 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index d55af703c0..0c9c97f60f 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -12,7 +12,6 @@ from frappe.utils import ( add_months, date_diff, flt, - get_first_day, get_last_day, getdate, now_datetime, @@ -117,36 +116,51 @@ class Loan(AccountsController): if not self.repayment_start_date: frappe.throw(_("Repayment Start Date is mandatory for term loans")) - schedule_type = frappe.db.get_value("Loan Type", self.loan_type, "repayment_schedule_type") + schedule_type_details = frappe.db.get_value( + "Loan Type", self.loan_type, ["repayment_schedule_type", "repayment_date_on"], as_dict=1 + ) + self.repayment_schedule = [] payment_date = self.repayment_start_date balance_amount = self.loan_amount while balance_amount > 0: interest_amount, principal_amount, balance_amount, total_payment = self.get_amounts( - payment_date, balance_amount, schedule_type + payment_date, + balance_amount, + schedule_type_details.repayment_schedule_type, + schedule_type_details.repayment_date_on, ) - if schedule_type == "Pro-rated calendar months": - next_payment_date = add_days(get_last_day(payment_date), 1) + if schedule_type_details.repayment_schedule_type == "Pro-rated calendar months": + next_payment_date = get_last_day(payment_date) + if schedule_type_details.repayment_date_on == "Start of the next month": + next_payment_date = add_days(next_payment_date, 1) + payment_date = next_payment_date self.add_repayment_schedule_row( payment_date, principal_amount, interest_amount, total_payment, balance_amount ) - if schedule_type == "Monthly as per repayment start date": + if ( + schedule_type_details.repayment_schedule_type == "Monthly as per repayment start date" + or schedule_type_details.repayment_date_on == "End of the current month" + ): next_payment_date = add_single_month(payment_date) payment_date = next_payment_date - def get_amounts(self, payment_date, balance_amount, schedule_type): - first_day_of_month = get_first_day(payment_date) - + def get_amounts(self, payment_date, balance_amount, schedule_type, repayment_date_on): if schedule_type == "Monthly as per repayment start date": days = 1 months = 12 else: - if first_day_of_month == payment_date: + expected_payment_date = get_last_day(payment_date) + if repayment_date_on == "Start of the next month": + expected_payment_date = add_days(expected_payment_date, 1) + + if expected_payment_date == payment_date: + # using 30 days for calculating interest for all full months days = 30 months = 365 else: diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 2608e42e08..0e61161601 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -4,7 +4,16 @@ import unittest import frappe -from frappe.utils import add_days, add_months, add_to_date, date_diff, flt, get_datetime, nowdate +from frappe.utils import ( + add_days, + add_months, + add_to_date, + date_diff, + flt, + format_date, + get_datetime, + nowdate, +) from erpnext.loan_management.doctype.loan.loan import ( make_loan_write_off, @@ -49,6 +58,50 @@ class TestLoan(unittest.TestCase): penalty_income_account="Penalty Income Account - _TC", ) + create_loan_type( + "Term Loan Type 1", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", + ) + + create_loan_type( + "Term Loan Type 2", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Pro-rated calendar months", + repayment_date_on="Start of the next month", + ) + + create_loan_type( + "Term Loan Type 3", + 12000, + 7.5, + is_term_loan=1, + mode_of_payment="Cash", + disbursement_account="Disbursement Account - _TC", + payment_account="Payment Account - _TC", + loan_account="Loan Account - _TC", + interest_income_account="Interest Income Account - _TC", + penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Pro-rated calendar months", + repayment_date_on="End of the current month", + ) + create_loan_type( "Stock Loan", 2000000, @@ -902,6 +955,45 @@ class TestLoan(unittest.TestCase): amounts = calculate_amounts(loan.name, add_days(last_date, 5)) self.assertEqual(flt(amounts["pending_principal_amount"], 0), 0) + def test_term_loan_schedule_types(self): + loan = create_loan( + self.applicant1, + "Term Loan Type 1", + 12000, + "Repay Over Number of Periods", + 12, + repayment_start_date="2022-10-17", + ) + + # Check for first, second and last installment date + self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "17-10-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "17-11-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "17-09-2023") + + loan.loan_type = "Term Loan Type 2" + loan.save() + + # Check for first, second and last installment date + self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "01-11-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "01-12-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "01-10-2023") + + loan.loan_type = "Term Loan Type 3" + loan.save() + + # Check for first, second and last installment date + self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "31-10-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "30-11-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "30-09-2023") + + loan.repayment_method = "Repay Fixed Amount per Period" + loan.monthly_repayment_amount = 1042 + loan.save() + + self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "31-10-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "30-11-2022") + self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "30-09-2023") + def create_loan_scenario_for_penalty(doc): pledge = [{"loan_security": "Test Security 1", "qty": 4000.00}] @@ -1033,6 +1125,8 @@ def create_loan_type( penalty_income_account=None, repayment_method=None, repayment_periods=None, + repayment_schedule_type=None, + repayment_date_on=None, ): if not frappe.db.exists("Loan Type", loan_name): @@ -1057,8 +1151,14 @@ def create_loan_type( "repayment_periods": repayment_periods, "write_off_amount": 100, } - ).insert() + ) + if loan_type.is_term_loan: + loan_type.repayment_schedule_type = repayment_schedule_type + if loan_type.repayment_schedule_type != "Monthly as per repayment start date": + loan_type.repayment_date_on = repayment_date_on + + loan_type.insert() loan_type.submit() diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json index e1ed3caf8f..5cc9464585 100644 --- a/erpnext/loan_management/doctype/loan_type/loan_type.json +++ b/erpnext/loan_management/doctype/loan_type/loan_type.json @@ -17,6 +17,7 @@ "is_term_loan", "disabled", "repayment_schedule_type", + "repayment_date_on", "description", "account_details_section", "mode_of_payment", @@ -161,17 +162,27 @@ }, { "depends_on": "is_term_loan", + "description": "The schedule type that will be used for generating the term loan schedules (will affect the payment date and monthly repayment amount)", "fieldname": "repayment_schedule_type", "fieldtype": "Select", "label": "Repayment Schedule Type", "mandatory_depends_on": "is_term_loan", "options": "\nMonthly as per repayment start date\nPro-rated calendar months" + }, + { + "depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"", + "description": "Select whether the repayment date should be the end of the current month or start of the upcoming month", + "fieldname": "repayment_date_on", + "fieldtype": "Select", + "label": "Repayment Date On", + "mandatory_depends_on": "eval:doc.repayment_schedule_type == \"Pro-rated calendar months\"", + "options": "\nStart of the next month\nEnd of the current month" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-09-28 21:31:01.278941", + "modified": "2022-10-22 17:43:03.954201", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Type", From e59b147a620c45d8727c8a0adf9ea1cd0d57df6f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 23 Oct 2022 18:51:51 +0530 Subject: [PATCH 08/12] chore: Update tests --- .../loan_management/doctype/loan/test_loan.py | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 0e61161601..388e65d9e5 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -56,6 +56,7 @@ class TestLoan(unittest.TestCase): loan_account="Loan Account - _TC", interest_income_account="Interest Income Account - _TC", penalty_income_account="Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", ) create_loan_type( @@ -115,6 +116,7 @@ class TestLoan(unittest.TestCase): "Loan Account - _TC", "Interest Income Account - _TC", "Penalty Income Account - _TC", + repayment_schedule_type="Monthly as per repayment start date", ) create_loan_type( @@ -966,33 +968,57 @@ class TestLoan(unittest.TestCase): ) # Check for first, second and last installment date - self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "17-10-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "17-11-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "17-09-2023") + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "17-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "17-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "17-09-2023" + ) loan.loan_type = "Term Loan Type 2" loan.save() # Check for first, second and last installment date - self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "01-11-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "01-12-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "01-10-2023") + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "01-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "01-12-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "01-10-2023" + ) loan.loan_type = "Term Loan Type 3" loan.save() # Check for first, second and last installment date - self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "31-10-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "30-11-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "30-09-2023") + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023" + ) loan.repayment_method = "Repay Fixed Amount per Period" loan.monthly_repayment_amount = 1042 loan.save() - self.assertEqual(format_date(loan.get("repayment_schedule")[0].payment_date), "31-10-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[1].payment_date), "30-11-2022") - self.assertEqual(format_date(loan.get("repayment_schedule")[-1].payment_date), "30-09-2023") + self.assertEqual( + format_date(loan.get("repayment_schedule")[0].payment_date, "dd-MM-yyyy"), "31-10-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[1].payment_date, "dd-MM-yyyy"), "30-11-2022" + ) + self.assertEqual( + format_date(loan.get("repayment_schedule")[-1].payment_date, "dd-MM-yyyy"), "30-09-2023" + ) def create_loan_scenario_for_penalty(doc): From 46d148defd59cbb1b9147e035aa6de41ee5fd099 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Oct 2022 15:48:34 +0530 Subject: [PATCH 09/12] fix: searchfield not working for cuctsomer, supplier as per customize form --- erpnext/controllers/queries.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 8eae0a0702..691a39aa8b 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -83,13 +83,11 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") - if cust_master_name == "Customer Name": - fields = ["name", "customer_group", "territory"] - else: - fields = ["name", "customer_name", "customer_group", "territory"] + fields = ["name"] + if cust_master_name != "Customer Name": + fields = ["customer_name"] fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) @@ -122,10 +120,9 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") - if supp_master_name == "Supplier Name": - fields = ["name", "supplier_group"] - else: - fields = ["name", "supplier_name", "supplier_group"] + fields = ["name"] + if supp_master_name != "Supplier Name": + fields = ["supplier_name"] fields = get_fields(doctype, fields) From 5f84993bae5df78e257cc2bfc41c123a1122a0b6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Oct 2022 16:10:47 +0530 Subject: [PATCH 10/12] test: added test case to validate seachfields for customer, supplier --- .../buying/doctype/supplier/test_supplier.py | 35 +++++++++++++++++++ erpnext/controllers/queries.py | 6 ++-- .../selling/doctype/customer/test_customer.py | 28 +++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index 55722686fe..e2dbf21be2 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from erpnext.accounts.party import get_due_date @@ -152,6 +153,40 @@ class TestSupplier(FrappeTestCase): # Rollback address.delete() + def test_serach_fields_for_supplier(self): + from erpnext.controllers.queries import supplier_query + + supplier_name = create_supplier(supplier_name="Test Supplier 1").name + + make_property_setter( + "Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype" + ) + + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertTrue("supplier_type" not in data[0]) + + make_property_setter( + "Supplier", + None, + "search_fields", + "supplier_group, supplier_type", + "Data", + for_doctype="Doctype", + ) + data = supplier_query( + "Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True + ) + + self.assertEqual(data[0].name, supplier_name) + self.assertEqual(data[0].supplier_group, "Services") + self.assertEqual(data[0].supplier_type, "Company") + self.assertTrue("supplier_type" in data[0]) + def create_supplier(**args): args = frappe._dict(args) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 691a39aa8b..3bdc017068 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -78,7 +78,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def customer_query(doctype, txt, searchfield, start, page_len, filters): +def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Customer" conditions = [] cust_master_name = frappe.defaults.get_user_default("cust_master_name") @@ -110,13 +110,14 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters): } ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) # searches for supplier @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def supplier_query(doctype, txt, searchfield, start, page_len, filters): +def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): doctype = "Supplier" supp_master_name = frappe.defaults.get_user_default("supp_master_name") @@ -142,6 +143,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters): **{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)} ), {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, + as_dict=as_dict, ) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 7dc3fab623..691adccd4d 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -3,6 +3,7 @@ import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.test_runner import make_test_records from frappe.tests.utils import FrappeTestCase from frappe.utils import flt @@ -341,6 +342,33 @@ class TestCustomer(FrappeTestCase): due_date = get_due_date("2017-01-22", "Customer", "_Test Customer") self.assertEqual(due_date, "2017-01-22") + def test_serach_fields_for_customer(self): + from erpnext.controllers.queries import customer_query + + make_property_setter( + "Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype" + ) + + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertTrue("territory" not in data[0]) + + make_property_setter( + "Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype" + ) + data = customer_query( + "Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True + ) + + self.assertEqual(data[0].name, "_Test Customer") + self.assertEqual(data[0].customer_group, "_Test Customer Group") + self.assertEqual(data[0].territory, "_Test Territory") + self.assertTrue("territory" in data[0]) + def get_customer_dict(customer_name): return { From 4cd65027c4a80acce8251c3d3b274c3963daad17 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Oct 2022 16:48:24 +0530 Subject: [PATCH 11/12] fix: Add condition for discount section collapse --- .../doctype/purchase_invoice_item/purchase_invoice_item.json | 4 +++- .../doctype/sales_invoice_item/sales_invoice_item.json | 3 ++- .../doctype/purchase_order_item/purchase_order_item.json | 3 ++- .../selling/doctype/sales_order_item/sales_order_item.json | 3 ++- .../stock/doctype/delivery_note_item/delivery_note_item.json | 3 ++- .../doctype/purchase_receipt_item/purchase_receipt_item.json | 3 ++- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 9de9036887..a8f6f80b6b 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -214,6 +214,7 @@ "reqd": 1 }, { + "default": "1", "depends_on": "eval:doc.uom != doc.stock_uom", "fieldname": "conversion_factor", "fieldtype": "Float", @@ -820,6 +821,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "section_break_26", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -871,7 +873,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:37:29.032732", + "modified": "2022-10-26 16:05:37.304788", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index a307a6c17c..7f1a1eccc4 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -247,6 +247,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -876,7 +877,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-10 20:57:38.340026", + "modified": "2022-10-26 11:38:36.119339", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 82e92e87bc..b8203bd128 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -777,6 +777,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -894,7 +895,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-09-07 11:12:38.634976", + "modified": "2022-10-26 16:47:41.364387", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 2cf836f9fc..ea0b25f41c 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -272,6 +272,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -842,7 +843,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-09-06 13:24:18.065312", + "modified": "2022-10-26 16:05:02.712705", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 0a5cbabab0..77c3253432 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -261,6 +261,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -814,7 +815,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:36:05.344847", + "modified": "2022-10-26 16:05:17.720768", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 772736e0ad..474ee92e26 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -919,6 +919,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin_section", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -1000,7 +1001,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-12 03:37:59.516609", + "modified": "2022-10-26 16:06:02.524435", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From a5a73ba85762e0940ea1c48f39b22f035923a860 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 26 Oct 2022 21:18:50 +0530 Subject: [PATCH 12/12] fix: Filter fixes in Accounts Payable report --- erpnext/accounts/report/accounts_payable/accounts_payable.js | 2 ++ .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 7cf14e6738..e1a30a4b77 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -51,6 +51,8 @@ frappe.query_reports["Accounts Payable"] = { } else { frappe.query_report.set_filter_value('tax_id', ""); } + + frappe.query_report.refresh(); } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 8557c03bd5..f2ee1eb10e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -748,7 +748,7 @@ class ReceivablePayableReport(object): self.add_accounting_dimensions_filters() - def get_cost_center_conditions(self, conditions): + def get_cost_center_conditions(self): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) cost_center_list = [ center.name