From 3cc33afd4ced88ea1f668b8cdd989181f1e776c5 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 28 May 2024 13:31:21 +0530 Subject: [PATCH 01/15] feat: multiple account support for fee category --- .../doctype/fee_category/fee_category.json | 11 +++- .../doctype/fee_category/fee_category.py | 7 ++- .../doctype/fee_category/test_fee_category.py | 21 +++++++- .../doctype/fee_category_default/__init__.py | 0 .../fee_category_default.json | 52 +++++++++++++++++++ .../fee_category_default.py | 9 ++++ 6 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 education/education/doctype/fee_category_default/__init__.py create mode 100644 education/education/doctype/fee_category_default/fee_category_default.json create mode 100644 education/education/doctype/fee_category_default/fee_category_default.py diff --git a/education/education/doctype/fee_category/fee_category.json b/education/education/doctype/fee_category/fee_category.json index f40fc323..9639cadd 100644 --- a/education/education/doctype/fee_category/fee_category.json +++ b/education/education/doctype/fee_category/fee_category.json @@ -10,7 +10,8 @@ "field_order": [ "category_name", "description", - "item" + "item", + "item_defaults" ], "fields": [ { @@ -39,11 +40,17 @@ "label": "Item", "options": "Item", "read_only": 1 + }, + { + "fieldname": "item_defaults", + "fieldtype": "Table", + "label": "Accounting Defaults", + "options": "Fee Category Default" } ], "icon": "fa fa-flag", "links": [], - "modified": "2024-01-07 14:03:30.856221", + "modified": "2024-05-28 12:32:35.849279", "modified_by": "Administrator", "module": "Education", "name": "Fee Category", diff --git a/education/education/doctype/fee_category/fee_category.py b/education/education/doctype/fee_category/fee_category.py index f3c946b6..975df071 100644 --- a/education/education/doctype/fee_category/fee_category.py +++ b/education/education/doctype/fee_category/fee_category.py @@ -3,9 +3,14 @@ import frappe from frappe.model.document import Document +from frappe import _, bold +from erpnext.stock.doctype.item.item import Item -class FeeCategory(Document): +class FeeCategory(Item): + def validate(self): + super().validate_item_defaults() + def after_insert(self): # create an item item_name = create_item(self) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index f2b4b7f3..d7c38798 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -1,8 +1,27 @@ # Copyright (c) 2015, Frappe Technologies and Contributors # See license.txt +import frappe from frappe.tests.utils import FrappeTestCase +from education.education.test_utils import create_fee_category class TestFeeCategory(FrappeTestCase): - pass + def setUp(self): + create_fee_category("Tuition Fee") + + def tearDown(self): + frappe.db.rollback() + + def test_item_created(self): + fee_category = frappe.get_doc("Fee Category", "Tuition Fee") + item = fee_category.get("item") + self.assertTrue(frappe.db.exists("Item", item)) + + item = frappe.get_doc("Item", item) + self.assertEqual(item.item_group, "Fee Component") + + # def test_category_defaults(self): + # fee_category = frappe.get_doc("Fee Category", "Tuition Fee") + # self.assertTrue(fee_category.get('item_defaults')) + # fieldnames = ["company","income_account","selling_cost_center"] diff --git a/education/education/doctype/fee_category_default/__init__.py b/education/education/doctype/fee_category_default/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/education/education/doctype/fee_category_default/fee_category_default.json b/education/education/doctype/fee_category_default/fee_category_default.json new file mode 100644 index 00000000..09ffd997 --- /dev/null +++ b/education/education/doctype/fee_category_default/fee_category_default.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-05-28 11:36:10.928607", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "income_account", + "selling_cost_center" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "selling_cost_center", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Selling Cost Center", + "link_filters": "[[\"Cost Center\",\"is_group\",\"=\",0],[\"Cost Center\",\"company\",\"=\",\"eval: doc.company\"]]", + "options": "Cost Center" + }, + { + "fieldname": "income_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Income Account", + "link_filters": "[[\"Account\",\"company\",\"=\",\"eval: doc.company\"]]", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-05-28 12:17:22.020803", + "modified_by": "Administrator", + "module": "Education", + "name": "Fee Category Default", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/education/education/doctype/fee_category_default/fee_category_default.py b/education/education/doctype/fee_category_default/fee_category_default.py new file mode 100644 index 00000000..14f75e24 --- /dev/null +++ b/education/education/doctype/fee_category_default/fee_category_default.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FeeCategoryDefault(Document): + pass From 02f46d045c3329a1fdaa66fb476757f21fa877d6 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 28 May 2024 19:48:29 +0530 Subject: [PATCH 02/15] fix: update fee_category defaults --- .../doctype/fee_category/fee_category.py | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/education/education/doctype/fee_category/fee_category.py b/education/education/doctype/fee_category/fee_category.py index 975df071..76688d22 100644 --- a/education/education/doctype/fee_category/fee_category.py +++ b/education/education/doctype/fee_category/fee_category.py @@ -7,9 +7,9 @@ from erpnext.stock.doctype.item.item import Item -class FeeCategory(Item): +class FeeCategory(Document): def validate(self): - super().validate_item_defaults() + self.update_defaults_from_item_group() def after_insert(self): # create an item @@ -26,6 +26,25 @@ def on_trash(self): # delete item frappe.delete_doc("Item", self.name, force=1) + def update_defaults_from_item_group(self): + """Get defaults from Item Group""" + item_group = "Fee Component" + if self.item_defaults: + return + + item_defaults = frappe.db.get_values( + "Item Default", + {"parent": item_group}, + [ + "company", + "selling_cost_center", + "income_account", + ], + as_dict=1, + ) + if item_defaults: + update_item_defaults(self, item_defaults) + def create_item(doc, use_name_field=True): name_field = doc.name if use_name_field else doc.fees_category @@ -39,7 +58,7 @@ def create_item(doc, use_name_field=True): item.is_sales_item = 1 item.is_service_item = 1 item.is_stock_item = 0 - + update_item_defaults(item, doc.item_defaults) item.insert() return item.name @@ -48,5 +67,20 @@ def update_item(doc): item = frappe.get_doc("Item", doc.name) item.item_name = doc.name item.description = doc.description + item.item_defaults = [] + update_item_defaults(item, doc.item_defaults) item.save() return item.name + + +def update_item_defaults(item, item_defaults): + for item_default in item_defaults: + item.append( + "item_defaults", + { + "company": item_default.company, + "selling_cost_center": item_default.selling_cost_center, + "income_account": item_default.income_account, + "default_warehouse": "", + }, + ) From 05c9af5027f8041b2e5777ac64bdefb2ef30b3db Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 28 May 2024 20:07:31 +0530 Subject: [PATCH 03/15] test: add tests for checking defaults --- .../doctype/fee_category/test_fee_category.py | 82 +++++++++++++++++-- education/education/test_utils.py | 8 ++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index d7c38798..4c10439d 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -3,17 +3,19 @@ import frappe from frappe.tests.utils import FrappeTestCase -from education.education.test_utils import create_fee_category +from education.education.test_utils import create_fee_category, create_company class TestFeeCategory(FrappeTestCase): def setUp(self): create_fee_category("Tuition Fee") + create_company("Dunder Mifflin Paper Co") def tearDown(self): frappe.db.rollback() def test_item_created(self): + companies = frappe.db.get_all("Company", fields=["name"]) fee_category = frappe.get_doc("Fee Category", "Tuition Fee") item = fee_category.get("item") self.assertTrue(frappe.db.exists("Item", item)) @@ -21,7 +23,77 @@ def test_item_created(self): item = frappe.get_doc("Item", item) self.assertEqual(item.item_group, "Fee Component") - # def test_category_defaults(self): - # fee_category = frappe.get_doc("Fee Category", "Tuition Fee") - # self.assertTrue(fee_category.get('item_defaults')) - # fieldnames = ["company","income_account","selling_cost_center"] + def test_item_defaults_from_item_group(self): + """ + When creating a Fee Category, if there are no item defaults, then get defaults from Item Group + """ + defaults = frappe.get_all( + "Company", + filters={"name": "_Test Company"}, + fields=["default_income_account", "cost_center"], + )[0] + + item_group = frappe.get_doc("Item Group", "Fee Component") + item_group.append( + "item_group_defaults", + { + "company": "_Test Company", + "income_account": defaults.default_income_account, + "selling_cost_center": defaults.cost_center, + }, + ) + item_group.save() + + fee_category = frappe.get_doc("Fee Category", "Tuition Fee") + fee_category.description = "Test" + fee_category.save() + + fee_category = frappe.get_doc("Fee Category", "Tuition Fee") + fee_category_defaults = fee_category.get("item_defaults")[0] + + self.assertEqual(fee_category_defaults.company, "_Test Company") + self.assertEqual( + fee_category_defaults.income_account, defaults.default_income_account + ) + self.assertEqual(fee_category_defaults.selling_cost_center, defaults.cost_center) + + def test_fee_component_defaults_same_as_item_defaults(self): + """ + When creating a Fee Category, if the defaults are set in Fee Category those should be saved in the Item Defaults aswell + """ + defaults = frappe.get_all( + "Company", + filters={"name": "_Test Company"}, + fields=["default_income_account", "cost_center"], + )[0] + fee_category_name = "Test Fee Category" + fee_category = create_fee_category(fee_category_name) + fee_category.append( + "item_defaults", + { + "company": "_Test Company", + "income_account": defaults.default_income_account, + "selling_cost_center": defaults.cost_center, + }, + ) + fee_category.save() + + item = frappe.get_doc("Item", fee_category_name) + item_defaults = item.get("item_defaults")[0] + + self.assertEqual(item_defaults.company, "_Test Company") + self.assertEqual(item_defaults.income_account, defaults.default_income_account) + self.assertEqual(item_defaults.selling_cost_center, defaults.cost_center) + + def test_fee_component_default_update(self): + # test while updating, update defaults and in item also have same defaults + """ + After updating the fee component defaults, the item defaults should also be updated + """ + pass + + def test_fee_component_duplicate_default(self): + """ + When setting defaults if there are 2 defaults for the same company, then throw an error + """ + pass diff --git a/education/education/test_utils.py b/education/education/test_utils.py index 59fbe472..577da752 100644 --- a/education/education/test_utils.py +++ b/education/education/test_utils.py @@ -95,6 +95,7 @@ def create_fee_category(category_name=DEFAULT_FEES_CATEGORY): fee_category = frappe.new_doc("Fee Category") fee_category.category_name = category_name fee_category.save() + return fee_category def create_program(name=DEFAULT_PROGRAM_NAME): @@ -256,3 +257,10 @@ def create_grading_scale(grading_scale_name="_Test Grading Scale"): grading_scale.save() grading_scale.submit() + + +def create_company(company_name): + company = frappe.get_doc( + {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} + ) + company.insert(ignore_if_duplicate=True) From 975ad9da7ba8b3a8df77aabfebafdc48640be402 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 28 May 2024 20:30:54 +0530 Subject: [PATCH 04/15] test: add tests for validating duplicate defaults --- .../doctype/fee_category/test_fee_category.py | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index 4c10439d..08cc7ba3 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -27,11 +27,7 @@ def test_item_defaults_from_item_group(self): """ When creating a Fee Category, if there are no item defaults, then get defaults from Item Group """ - defaults = frappe.get_all( - "Company", - filters={"name": "_Test Company"}, - fields=["default_income_account", "cost_center"], - )[0] + defaults = get_defaults() item_group = frappe.get_doc("Item Group", "Fee Component") item_group.append( @@ -61,11 +57,8 @@ def test_fee_component_defaults_same_as_item_defaults(self): """ When creating a Fee Category, if the defaults are set in Fee Category those should be saved in the Item Defaults aswell """ - defaults = frappe.get_all( - "Company", - filters={"name": "_Test Company"}, - fields=["default_income_account", "cost_center"], - )[0] + defaults = get_defaults() + fee_category_name = "Test Fee Category" fee_category = create_fee_category(fee_category_name) fee_category.append( @@ -85,15 +78,36 @@ def test_fee_component_defaults_same_as_item_defaults(self): self.assertEqual(item_defaults.income_account, defaults.default_income_account) self.assertEqual(item_defaults.selling_cost_center, defaults.cost_center) - def test_fee_component_default_update(self): - # test while updating, update defaults and in item also have same defaults - """ - After updating the fee component defaults, the item defaults should also be updated - """ - pass - def test_fee_component_duplicate_default(self): """ When setting defaults if there are 2 defaults for the same company, then throw an error """ - pass + fee_component = frappe.get_doc("Fee Category", "Tuition Fee") + # get any income account + income_account = frappe.get_all("Account", fields=["name"], limit=1)[0]["name"] + defaults = get_defaults() + default_array = [ + { + "company": "_Test Company", + "income_account": income_account, + "selling_cost_center": defaults.cost_center, + }, + { + "company": "_Test Company", + "income_account": income_account, + "selling_cost_center": defaults.cost_center, + }, + ] + for default in default_array: + fee_component.append("item_defaults", default) + self.assertRaises(frappe.ValidationError, fee_component.save) + + +def get_defaults(company_name="_Test Company"): + defaults = frappe.get_all( + "Company", + filters={"name": company_name}, + fields=["default_income_account", "cost_center"], + limit=1, + )[0] + return defaults From 798b843d3d11a6344f43dab11a8962d55ffce2f7 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Wed, 29 May 2024 12:43:12 +0530 Subject: [PATCH 05/15] fix: add validations in Fee Schedule DocType --- .../doctype/fee_category/fee_category.py | 2 - .../doctype/fee_schedule/fee_schedule.py | 1 + .../doctype/fee_structure/fee_structure.js | 470 +++++++++--------- .../doctype/fee_structure/fee_structure.py | 21 +- education/education/test_utils.py | 10 +- 5 files changed, 270 insertions(+), 234 deletions(-) diff --git a/education/education/doctype/fee_category/fee_category.py b/education/education/doctype/fee_category/fee_category.py index 76688d22..6e525312 100644 --- a/education/education/doctype/fee_category/fee_category.py +++ b/education/education/doctype/fee_category/fee_category.py @@ -3,8 +3,6 @@ import frappe from frappe.model.document import Document -from frappe import _, bold -from erpnext.stock.doctype.item.item import Item class FeeCategory(Document): diff --git a/education/education/doctype/fee_schedule/fee_schedule.py b/education/education/doctype/fee_schedule/fee_schedule.py index 6919f8c2..b6962bf3 100644 --- a/education/education/doctype/fee_schedule/fee_schedule.py +++ b/education/education/doctype/fee_schedule/fee_schedule.py @@ -185,6 +185,7 @@ def create_sales_invoice(fee_schedule, student_id, create_sales_order=False): for item in sales_invoice_doc.items: item.qty = 1 + item.cost_center = "" sales_invoice_doc.save() if frappe.db.get_single_value("Education Settings", "auto_submit_sales_invoice"): diff --git a/education/education/doctype/fee_structure/fee_structure.js b/education/education/doctype/fee_structure/fee_structure.js index 4d70f565..d34d298b 100644 --- a/education/education/doctype/fee_structure/fee_structure.js +++ b/education/education/doctype/fee_structure/fee_structure.js @@ -3,232 +3,250 @@ frappe.provide("erpnext.accounts.dimensions"); -frappe.ui.form.on('Fee Structure', { - - company: function(frm) { - erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); - }, - - onload: function(frm) { - frm.set_query('academic_term', function() { - return { - 'filters': { - 'academic_year': frm.doc.academic_year - } - }; - }); - - frm.set_query('receivable_account', function(doc) { - return { - filters: { - 'account_type': 'Receivable', - 'is_group': 0, - 'company': doc.company - } - }; - }); - frm.set_query('income_account', function(doc) { - return { - filters: { - 'account_type': 'Income Account', - 'is_group': 0, - 'company': doc.company - } - }; - }); - - erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); - }, - - refresh: function(frm) { - if (frm.doc.docstatus === 1) { - frm.add_custom_button(__('Create Fee Schedule'), function() { - frm.doc.academic_term - ? frm.events.make_term_wise_fee_schedule(frm) - : frm.events.open_fee_schedule_modal(frm); - }); - } - }, - - make_term_wise_fee_schedule: function(frm) { - frappe.model.open_mapped_doc({ - method: 'education.education.doctype.fee_structure.fee_structure.make_term_wise_fee_schedule', - frm: frm - }); - }, - - - make_fee_schedule: function(frm) { - let {distribution} = frm.dialog.get_values(); - let total_amount_from_dialog = distribution.reduce((accumulated_value,current_value) => accumulated_value + current_value.amount,0) - if(!(frm.doc.total_amount === total_amount_from_dialog)) { - frappe.throw(__("Total amount in the table should be equal to the total amount from fee structure")) - return; - } - frappe.call({ - method: 'education.education.doctype.fee_structure.fee_structure.make_fee_schedule', - args: { - "source_name":frm.doc.name, - "dialog_values": frm.dialog.get_values(), - "per_component_amount":frm. per_component_amount - }, - freeze: true, - callback: function(r) { - if (r.message) { - frappe.msgprint(__("{0} Fee Schedule(s) Create", [r.message])); - frm.dialog.hide(); - } - } - }) - }, - get_amount_distribution_based_on_fee_plan:function(frm) { - let dialog = frm.dialog - let fee_plan = dialog.get_value('fee_plan'); - - // remove existing data in table when fee plan is changed - dialog.fields_dict.distribution.df.data = []; - dialog.refresh(); - - frappe.call({ - method: 'education.education.doctype.fee_structure.fee_structure.get_amount_distribution_based_on_fee_plan', - args: { - "fee_plan": fee_plan, - "total_amount": frm.doc.total_amount, - "components": frm.doc.components, - "academic_year": frm.doc.academic_year, - }, - callback: function(r) { - if (!r.message) return; - - let dialog_grid = dialog.fields_dict.distribution.grid; - let distribution = r.message.distribution; - frm.per_component_amount = r.message.per_component_amount - - fee_plan === "Term-Wise" - ? dialog_grid.docfields[0].hidden = false - : dialog_grid.docfields[0].hidden = true; - - distribution.forEach((month,idx) => { - dialog_grid.reset_grid(); - dialog.fields_dict['distribution'].grid.add_new_row(); - dialog.get_value("distribution")[idx] = { - term: month.term, - due_date: month.due_date, - amount: month.amount - }; - }) - dialog.refresh(); - - } - }); - }, - - open_fee_schedule_modal: function(frm) { - - let distribution_table_fields = [ - { - "fieldname": "term", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Term", - "read_only": 1, - 'hidden':1, - }, - { - "fieldname": "due_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "Due Date" - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Amount" - } - ] - - let dialog_fields = [ - { - label:"Select Fee Plan", - fieldname:"fee_plan", - fieldtype:"Select", - reqd: 1, - options:["Monthly","Quarterly","Semi-Annually","Annually","Term-Wise"], - change: () => frm.events.get_amount_distribution_based_on_fee_plan(frm) - }, - { - fieldname: "distribution", - label: "Distribution", - fieldtype: "Table", - in_place_edit: false, - data: [], - cannot_add_rows: true, - reqd: 1, - fields: distribution_table_fields, - // not selectable and do not show edit icon - }, - { - label:"Select Student Groups", - fieldname:"student_groups", - fieldtype:"Table", - in_place_edit: false, - reqd: 1, - data: [], - fields: [ - { - "fieldname": "student_group", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Student Group", - "options": "Student Group", - get_query: () => { - return { - filters: { - program: frm.doc.program, - academic_year: frm.doc.academic_year, - academic_term: frm.doc.academic_term, - student_category: frm.doc.student_category - } - } - } - }, - ], - } - ] - - frm.per_component_amount = []; - - frm.dialog = new frappe.ui.Dialog({ - title: "Create Fee Schedule", - fields: dialog_fields, - primary_action: function() { - // validate whether total amount from dialog is equal to total amount from fee structure - frm.events.make_fee_schedule(frm); - }, - primary_action_label: __("Create"), - }) - frm.dialog.show(); - } +frappe.ui.form.on("Fee Structure", { + company: function (frm) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + }, + + onload: function (frm) { + frm.set_query("academic_term", function () { + return { + filters: { + academic_year: frm.doc.academic_year, + }, + }; + }); + + frm.set_query("receivable_account", function (doc) { + return { + filters: { + account_type: "Receivable", + is_group: 0, + company: doc.company, + }, + }; + }); + frm.set_query("income_account", function (doc) { + return { + filters: { + account_type: "Income Account", + is_group: 0, + company: doc.company, + }, + }; + }); + + erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); + }, + + refresh: function (frm) { + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__("Create Fee Schedule"), function () { + frm.doc.academic_term + ? frm.events.make_term_wise_fee_schedule(frm) + : frm.events.open_fee_schedule_modal(frm); + }); + } + }, + + make_term_wise_fee_schedule: function (frm) { + frappe.model.open_mapped_doc({ + method: + "education.education.doctype.fee_structure.fee_structure.make_term_wise_fee_schedule", + frm: frm, + }); + }, + + make_fee_schedule: function (frm) { + let { distribution, student_groups } = frm.dialog.get_values(); + student_groups.forEach((student_group) => { + if (!student_group.student_group) { + frappe.throw(__("Student Group is mandatory")); + return; + } + }); + + let total_amount_from_dialog = distribution.reduce( + (accumulated_value, current_value) => + accumulated_value + current_value.amount, + 0 + ); + if (!(frm.doc.total_amount === total_amount_from_dialog)) { + frappe.throw( + __( + "Total amount in the table should be equal to the total amount from fee structure" + ) + ); + return; + } + frappe.call({ + method: + "education.education.doctype.fee_structure.fee_structure.make_fee_schedule", + args: { + source_name: frm.doc.name, + dialog_values: frm.dialog.get_values(), + per_component_amount: frm.per_component_amount, + }, + freeze: true, + callback: function (r) { + if (r.message) { + frappe.msgprint(__("{0} Fee Schedule(s) Create", [r.message])); + frm.dialog.hide(); + } + }, + }); + }, + get_amount_distribution_based_on_fee_plan: function (frm) { + let dialog = frm.dialog; + let fee_plan = dialog.get_value("fee_plan"); + + // remove existing data in table when fee plan is changed + dialog.fields_dict.distribution.df.data = []; + dialog.refresh(); + + frappe.call({ + method: + "education.education.doctype.fee_structure.fee_structure.get_amount_distribution_based_on_fee_plan", + args: { + fee_plan: fee_plan, + total_amount: frm.doc.total_amount, + components: frm.doc.components, + academic_year: frm.doc.academic_year, + }, + callback: function (r) { + if (!r.message) return; + + let dialog_grid = dialog.fields_dict.distribution.grid; + let distribution = r.message.distribution; + frm.per_component_amount = r.message.per_component_amount; + + fee_plan === "Term-Wise" + ? (dialog_grid.docfields[0].hidden = false) + : (dialog_grid.docfields[0].hidden = true); + + distribution.forEach((month, idx) => { + dialog_grid.reset_grid(); + dialog.fields_dict["distribution"].grid.add_new_row(); + dialog.get_value("distribution")[idx] = { + term: month.term, + due_date: month.due_date, + amount: month.amount, + }; + }); + dialog.refresh(); + }, + }); + }, + + open_fee_schedule_modal: function (frm) { + let distribution_table_fields = [ + { + fieldname: "term", + fieldtype: "Link", + in_list_view: 1, + label: "Term", + read_only: 1, + hidden: 1, + }, + { + fieldname: "due_date", + fieldtype: "Date", + in_list_view: 1, + label: "Due Date", + }, + { + fieldname: "amount", + fieldtype: "Float", + in_list_view: 1, + label: "Amount", + }, + ]; + + let dialog_fields = [ + { + label: "Select Fee Plan", + fieldname: "fee_plan", + fieldtype: "Select", + reqd: 1, + options: [ + "Monthly", + "Quarterly", + "Semi-Annually", + "Annually", + "Term-Wise", + ], + change: () => frm.events.get_amount_distribution_based_on_fee_plan(frm), + }, + { + fieldname: "distribution", + label: "Distribution", + fieldtype: "Table", + in_place_edit: false, + data: [], + cannot_add_rows: true, + reqd: 1, + fields: distribution_table_fields, + // not selectable and do not show edit icon + }, + { + label: "Select Student Groups", + fieldname: "student_groups", + fieldtype: "Table", + in_place_edit: false, + reqd: 1, + data: [], + fields: [ + { + fieldname: "student_group", + fieldtype: "Link", + in_list_view: 1, + label: "Student Group", + options: "Student Group", + get_query: () => { + return { + filters: { + program: frm.doc.program, + academic_year: frm.doc.academic_year, + academic_term: frm.doc.academic_term, + student_category: frm.doc.student_category, + }, + }; + }, + }, + ], + }, + ]; + + frm.per_component_amount = []; + + frm.dialog = new frappe.ui.Dialog({ + title: "Create Fee Schedule", + fields: dialog_fields, + primary_action: function () { + // validate whether total amount from dialog is equal to total amount from fee structure + frm.events.make_fee_schedule(frm); + }, + primary_action_label: __("Create"), + }); + frm.dialog.show(); + }, }); -frappe.ui.form.on('Fee Component', { - amount: function(frm,cdt,cdn) { - let d = locals[cdt][cdn]; - d.total = d.amount; - refresh_field('components'); - if (d.discount) { - d.total = d.amount - (d.amount * (d.discount / 100) ); - refresh_field('components'); - } - - }, - discount: function(frm,cdt,cdn) { - let d = locals[cdt][cdn]; - if (d.discount < 100) { - d.total = d.amount - (d.amount * (d.discount / 100) ); - } - refresh_field('components'); - }, - +frappe.ui.form.on("Fee Component", { + amount: function (frm, cdt, cdn) { + let d = locals[cdt][cdn]; + d.total = d.amount; + refresh_field("components"); + if (d.discount) { + d.total = d.amount - d.amount * (d.discount / 100); + refresh_field("components"); + } + }, + discount: function (frm, cdt, cdn) { + let d = locals[cdt][cdn]; + if (d.discount < 100) { + d.total = d.amount - d.amount * (d.discount / 100); + } + refresh_field("components"); + }, }); diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index 92ae3a6e..cfc211e4 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -16,6 +16,21 @@ class FeeStructure(Document): def validate(self): self.calculate_total() self.validate_discount() + self.validate_fee_component() + + def validate_fee_component(self): + for component in self.components: + fee_component = frappe.get_doc("Fee Category", component.fees_category) + company_defaults = [] + for default in fee_component.item_defaults: + company_defaults.append(default.company) + + if self.company not in company_defaults: + frappe.throw( + _( + "The company of the Fee Category {0} and the Fee Structure should be the same." + ).format(frappe.bold(fee_component.name)) + ) def calculate_total(self): """Calculates total amount.""" @@ -164,7 +179,11 @@ def make_fee_schedule( for component in doc.components: component.total = per_component_amount.get(component.fees_category) discount = flt(component.discount) / 100 - component.amount = flt((component.total) / (1 - discount)) + + if discount == 1: + component.amount = component.total + else: + component.amount = flt((component.total) / flt(1 - discount)) amount_per_month += component.total # amount_per_month will be the total amount for each Fee Structure doc.total_amount = amount_per_month diff --git a/education/education/test_utils.py b/education/education/test_utils.py index 577da752..bd1e6ec2 100644 --- a/education/education/test_utils.py +++ b/education/education/test_utils.py @@ -117,14 +117,14 @@ def create_fee_structure( fee_structure.academic_year = academic_year or DEFAULT_ACADEMIC_YEAR fee_structure.academic_term = academic_term or DEFAULT_ACADEMIC_TERM fee_structure.program = program or DEFAULT_PROGRAM_NAME - for i in components: + for c in components: fee_structure.append( "components", { - "fees_category": i.get("fees_category"), - "amount": i.get("amount"), - "discount": i.get("discount"), - "total": i.get("total"), + "fees_category": c.get("fees_category"), + "amount": c.get("amount"), + "discount": c.get("discount"), + "total": c.get("total"), }, ) fee_structure.save() From 588e66534eb3d7488650d9d8f69525345ef4b410 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Wed, 29 May 2024 16:56:40 +0530 Subject: [PATCH 06/15] test: add test for custom accounting defaults in fee category --- .../doctype/fee_category/test_fee_category.py | 16 +- .../doctype/fee_schedule/fee_schedule.json | 730 +++++++++--------- .../doctype/fee_schedule/test_fee_schedule.py | 66 +- .../doctype/fee_structure/fee_structure.js | 9 +- .../doctype/fee_structure/fee_structure.json | 12 +- .../doctype/fee_structure/fee_structure.py | 15 - education/education/test_utils.py | 20 +- 7 files changed, 450 insertions(+), 418 deletions(-) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index 08cc7ba3..104f74e1 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -3,7 +3,11 @@ import frappe from frappe.tests.utils import FrappeTestCase -from education.education.test_utils import create_fee_category, create_company +from education.education.test_utils import ( + create_fee_category, + create_company, + get_defaults, +) class TestFeeCategory(FrappeTestCase): @@ -101,13 +105,3 @@ def test_fee_component_duplicate_default(self): for default in default_array: fee_component.append("item_defaults", default) self.assertRaises(frappe.ValidationError, fee_component.save) - - -def get_defaults(company_name="_Test Company"): - defaults = frappe.get_all( - "Company", - filters={"name": company_name}, - fields=["default_income_account", "cost_center"], - limit=1, - )[0] - return defaults diff --git a/education/education/doctype/fee_schedule/fee_schedule.json b/education/education/doctype/fee_schedule/fee_schedule.json index 349fbba5..e5ea76d2 100644 --- a/education/education/doctype/fee_schedule/fee_schedule.json +++ b/education/education/doctype/fee_schedule/fee_schedule.json @@ -1,370 +1,362 @@ { - "actions": [], - "allow_import": 1, - "autoname": "naming_series:", - "creation": "2017-07-18 15:21:21.527136", - "doctype": "DocType", - "document_type": "Document", - "engine": "InnoDB", - "field_order": [ - "fee_structure", - "posting_date", - "due_date", - "naming_series", - "send_email", - "column_break_4", - "student_category", - "program", - "academic_year", - "academic_term", - "section_break_10", - "currency", - "student_groups", - "section_break_14", - "components", - "section_break_16", - "column_break_18", - "total_amount", - "grand_total", - "grand_total_in_words", - "edit_printing_settings", - "letter_head", - "column_break_32", - "select_print_heading", - "account", - "receivable_account", - "income_account", - "column_break_39", - "company", - "amended_from", - "accounting_dimensions_section", - "cost_center", - "dimension_col_break", - "section_break_31", - "error_log", - "status" - ], - "fields": [ - { - "fieldname": "fee_structure", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "label": "Fee Structure", - "options": "Fee Structure", - "reqd": 1 - }, - { - "fieldname": "due_date", - "fieldtype": "Date", - "label": "Due Date", - "reqd": 1 - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "no_copy": 1, - "options": "EDU-FSH-.YYYY.-" - }, - { - "fieldname": "status", - "fieldtype": "Select", - "hide_days": 1, - "hide_seconds": 1, - "in_standard_filter": 1, - "label": "Status", - "length": 30, - "no_copy": 1, - "options": "Draft\nCancelled\nInvoice Pending\nOrder Pending\nIn Process\nInvoice Created\nOrder Created\nFailed", - "print_hide": 1, - "read_only": 1 - }, - { - "default": "0", - "fieldname": "send_email", - "fieldtype": "Check", - "label": "Send Payment Request Email" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "student_category", - "fieldtype": "Link", - "label": "Student Category", - "options": "Student Category", - "read_only": 1 - }, - { - "fieldname": "program", - "fieldtype": "Link", - "label": "Program", - "options": "Program", - "read_only": 1 - }, - { - "fieldname": "academic_year", - "fieldtype": "Link", - "label": "Academic Year", - "options": "Academic Year", - "reqd": 1 - }, - { - "fieldname": "academic_term", - "fieldtype": "Link", - "label": "Academic Term", - "options": "Academic Term" - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "hidden": 1, - "label": "Currency", - "options": "Currency", - "read_only": 1 - }, - { - "fieldname": "student_groups", - "fieldtype": "Table", - "options": "Fee Schedule Student Group", - "reqd": 1 - }, - { - "fieldname": "section_break_14", - "fieldtype": "Section Break", - "label": "Fee Breakup for each student", - "read_only": 1 - }, - { - "fieldname": "components", - "fieldtype": "Table", - "label": "Components", - "options": "Fee Component", - "read_only": 1 - }, - { - "fieldname": "section_break_16", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "total_amount", - "fieldtype": "Currency", - "label": "Total Amount per Student", - "read_only": 1 - }, - { - "fieldname": "grand_total", - "fieldtype": "Currency", - "label": "Grand Total", - "read_only": 1 - }, - { - "fieldname": "grand_total_in_words", - "fieldtype": "Data", - "label": "In Words", - "length": 240, - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "edit_printing_settings", - "fieldtype": "Section Break", - "label": "Printing Settings" - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "fieldname": "column_break_32", - "fieldtype": "Column Break" - }, - { - "allow_on_submit": 1, - "fieldname": "select_print_heading", - "fieldtype": "Link", - "label": "Print Heading", - "no_copy": 1, - "options": "Print Heading", - "print_hide": 1, - "report_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "account", - "fieldtype": "Section Break", - "label": "Accounting" - }, - { - "fetch_from": "fee_structure.receivable_account", - "fieldname": "receivable_account", - "fieldtype": "Link", - "label": "Receivable Account", - "options": "Account" - }, - { - "fetch_from": "fee_structure.income_account", - "fieldname": "income_account", - "fieldtype": "Link", - "label": "Income Account", - "options": "Account" - }, - { - "fieldname": "column_break_39", - "fieldtype": "Column Break" - }, - { - "fetch_from": "fee_structure.cost_center", - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Institution", - "options": "Company" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Fee Schedule", - "print_hide": 1, - "read_only": 1 - }, - { - "collapsible": 1, - "depends_on": "error_log", - "fieldname": "section_break_31", - "fieldtype": "Section Break", - "label": "Error Log" - }, - { - "fieldname": "error_log", - "fieldtype": "Small Text", - "label": "Error Log" - }, - { - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, - { - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "reqd": 1 - } - ], - "is_submittable": 1, - "links": [ - { - "link_doctype": "Sales Invoice", - "link_fieldname": "fee_schedule" - }, - { - "link_doctype": "Sales Order", - "link_fieldname": "fee_schedule" - } - ], - "modified": "2024-03-27 22:48:15.293321", - "modified_by": "Administrator", - "module": "Education", - "name": "Fee Schedule", - "naming_rule": "By \"Naming Series\" field", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "import": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [ - { - "color": "Orange", - "title": "Invoice Pending" - }, - { - "color": "Orange", - "title": "Order Pending" - }, - { - "color": "Orange", - "title": "In Process" - }, - { - "color": "Green", - "title": "Order Created" - }, - { - "color": "Green", - "title": "Invoice Created" - }, - { - "color": "Red", - "title": "Fee Creation Failed" - } - ] -} + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2017-07-18 15:21:21.527136", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "fee_structure", + "posting_date", + "due_date", + "naming_series", + "send_email", + "column_break_4", + "student_category", + "program", + "academic_year", + "academic_term", + "section_break_10", + "currency", + "student_groups", + "section_break_14", + "components", + "section_break_16", + "column_break_18", + "total_amount", + "grand_total", + "grand_total_in_words", + "edit_printing_settings", + "letter_head", + "column_break_32", + "select_print_heading", + "account", + "receivable_account", + "column_break_39", + "company", + "amended_from", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "section_break_31", + "error_log", + "status" + ], + "fields": [ + { + "fieldname": "fee_structure", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Fee Structure", + "options": "Fee Structure", + "reqd": 1 + }, + { + "fieldname": "due_date", + "fieldtype": "Date", + "label": "Due Date", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "no_copy": 1, + "options": "EDU-FSH-.YYYY.-" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, + "in_standard_filter": 1, + "label": "Status", + "length": 30, + "no_copy": 1, + "options": "Draft\nCancelled\nInvoice Pending\nOrder Pending\nIn Process\nInvoice Created\nOrder Created\nFailed", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Payment Request Email" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "student_category", + "fieldtype": "Link", + "label": "Student Category", + "options": "Student Category", + "read_only": 1 + }, + { + "fieldname": "program", + "fieldtype": "Link", + "label": "Program", + "options": "Program", + "read_only": 1 + }, + { + "fieldname": "academic_year", + "fieldtype": "Link", + "label": "Academic Year", + "options": "Academic Year", + "reqd": 1 + }, + { + "fieldname": "academic_term", + "fieldtype": "Link", + "label": "Academic Term", + "options": "Academic Term" + }, + { + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "student_groups", + "fieldtype": "Table", + "options": "Fee Schedule Student Group", + "reqd": 1 + }, + { + "fieldname": "section_break_14", + "fieldtype": "Section Break", + "label": "Fee Breakup (each student)", + "read_only": 1 + }, + { + "fieldname": "components", + "fieldtype": "Table", + "label": "Components", + "options": "Fee Component", + "read_only": 1 + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "total_amount", + "fieldtype": "Currency", + "label": "Total Amount per Student", + "read_only": 1 + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "label": "Grand Total", + "read_only": 1 + }, + { + "fieldname": "grand_total_in_words", + "fieldtype": "Data", + "label": "In Words", + "length": 240, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "edit_printing_settings", + "fieldtype": "Section Break", + "label": "Printing Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "options": "Letter Head", + "print_hide": 1 + }, + { + "fieldname": "column_break_32", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "account", + "fieldtype": "Section Break", + "label": "Accounting" + }, + { + "fetch_from": "fee_structure.receivable_account", + "fieldname": "receivable_account", + "fieldtype": "Link", + "label": "Receivable Account", + "options": "Account" + }, + { + "fieldname": "column_break_39", + "fieldtype": "Column Break" + }, + { + "fetch_from": "fee_structure.cost_center", + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Institution", + "options": "Company" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Fee Schedule", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "depends_on": "error_log", + "fieldname": "section_break_31", + "fieldtype": "Section Break", + "label": "Error Log" + }, + { + "fieldname": "error_log", + "fieldtype": "Small Text", + "label": "Error Log" + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "reqd": 1 + } + ], + "is_submittable": 1, + "links": [ + { + "link_doctype": "Sales Invoice", + "link_fieldname": "fee_schedule" + }, + { + "link_doctype": "Sales Order", + "link_fieldname": "fee_schedule" + } + ], + "modified": "2024-05-29 13:48:34.950263", + "modified_by": "Administrator", + "module": "Education", + "name": "Fee Schedule", + "naming_rule": "By \"Naming Series\" field", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Academics User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [ + { + "color": "Orange", + "title": "Invoice Pending" + }, + { + "color": "Orange", + "title": "Order Pending" + }, + { + "color": "Orange", + "title": "In Process" + }, + { + "color": "Green", + "title": "Order Created" + }, + { + "color": "Green", + "title": "Invoice Created" + }, + { + "color": "Red", + "title": "Fee Creation Failed" + } + ] +} \ No newline at end of file diff --git a/education/education/doctype/fee_schedule/test_fee_schedule.py b/education/education/doctype/fee_schedule/test_fee_schedule.py index 7204e8bf..cdb7c116 100644 --- a/education/education/doctype/fee_schedule/test_fee_schedule.py +++ b/education/education/doctype/fee_schedule/test_fee_schedule.py @@ -5,6 +5,9 @@ from frappe.tests.utils import FrappeTestCase from education.education.doctype.fee_schedule.fee_schedule import generate_fees +# get_defaults from test_utils +from education.education.test_utils import get_defaults + from education.education.test_utils import ( create_academic_year, create_academic_term, @@ -55,8 +58,7 @@ def test_fee_schedule(self): self.assertEqual(fee_schedule.grand_total, total_students * fee_schedule.total_amount) def test_sales_invoice_creation_flow(self): - due_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) - fee_schedule = create_fee_schedule(submit=1, due_date=due_date) + fee_schedule = create_fee_schedule(submit=1) # sales_invoice_posting_date_fee_schedule set it as 1 self.assertEqual(fee_schedule.status, "Invoice Pending") self.assertNotEqual(fee_schedule.status, "Order Pending") @@ -64,19 +66,30 @@ def test_sales_invoice_creation_flow(self): sales_invoices = frappe.get_all( "Sales Invoice", filters={"fee_schedule": fee_schedule.name} ) - sales_order = frappe.get_all( + sales_orders = frappe.get_all( "Sales Order", filters={"fee_schedule": fee_schedule.name} ) + + # Check if the income account and cost center are set correctly + items = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": sales_invoices[0].name}, + fields=["item_code", "income_account", "cost_center"], + ) + company_defaults = get_defaults() + for item in items: + self.assertEqual(item.income_account, company_defaults.get("default_income_account")) + self.assertEqual(item.cost_center, company_defaults.get("cost_center")) + self.assertEqual(len(sales_invoices), 1) - self.assertEqual(len(sales_order), 0) + self.assertEqual(len(sales_orders), 0) fee_schedule_status = frappe.db.get_value("Fee Schedule", fee_schedule.name, "status") self.assertEqual(fee_schedule_status, "Invoice Created") def test_sales_order_creation_flow(self): # create_so from education settings set to 1 frappe.db.set_value("Education Settings", "Education Settings", "create_so", 1) - due_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) - fee_schedule = create_fee_schedule(submit=1, due_date=due_date) + fee_schedule = create_fee_schedule(submit=1) self.assertEqual(fee_schedule.status, "Order Pending") self.assertNotEqual(fee_schedule.status, "Invoice Pending") @@ -91,3 +104,44 @@ def test_sales_order_creation_flow(self): self.assertEqual(len(sales_invoices), 0) fee_schedule_status = frappe.db.get_value("Fee Schedule", fee_schedule.name, "status") self.assertEqual(fee_schedule_status, "Order Created") + + def test_sales_invoice_flow_with_custom_component_defaults(self): + """ + If defaults are set for fee components, then invoices items should have those defaults of income account and cost center. + """ + company_defaults = get_defaults() + income_account = frappe.get_all( + "Account", fields=["name"], filters={"is_group": 0}, limit=2 + )[1]["name"] + + fee_component = "Tuition Fee" + fee_category = create_fee_category(fee_component) + fee_category.append( + "item_defaults", + { + "company": "_Test Company", + "income_account": income_account, + "selling_cost_center": company_defaults.get("cost_center"), + }, + ) + fee_category.save() + + fee_components = [{"fees_category": fee_component, "amount": 2000, "discount": 0}] + fee_structure = create_fee_structure(components=fee_components, submit=1) + fee_schedule = create_fee_schedule(submit=1, fee_structure=fee_structure.name) + self.assertEqual(fee_schedule.status, "Invoice Pending") + + generate_fees(fee_schedule.name) + sales_invoice = frappe.get_all( + "Sales Invoice", filters={"fee_schedule": fee_schedule.name} + ) + + items = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": sales_invoice[0].name}, + fields=["item_code", "income_account", "cost_center"], + ) + for item in items: + # Invoice Item should have the income account and cost center set in the fee category + self.assertEqual(item.income_account, income_account) + self.assertEqual(item.cost_center, company_defaults.get("cost_center")) diff --git a/education/education/doctype/fee_structure/fee_structure.js b/education/education/doctype/fee_structure/fee_structure.js index d34d298b..e2a61011 100644 --- a/education/education/doctype/fee_structure/fee_structure.js +++ b/education/education/doctype/fee_structure/fee_structure.js @@ -2,8 +2,15 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts.dimensions"); - frappe.ui.form.on("Fee Structure", { + setup: function (frm) { + frm.add_fetch( + "company", + "default_receivable_account", + "receivable_account" + ); + frm.add_fetch("company", "cost_center", "cost_center"); + }, company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, diff --git a/education/education/doctype/fee_structure/fee_structure.json b/education/education/doctype/fee_structure/fee_structure.json index a59a62a5..d62f072c 100644 --- a/education/education/doctype/fee_structure/fee_structure.json +++ b/education/education/doctype/fee_structure/fee_structure.json @@ -21,7 +21,6 @@ "total_amount", "accounts", "receivable_account", - "income_account", "column_break_16", "company", "amended_from", @@ -111,26 +110,17 @@ "label": "Accounts" }, { - "fetch_from": "company.default_receivable_account", "fieldname": "receivable_account", "fieldtype": "Link", "label": "Receivable Account", "options": "Account", "reqd": 1 }, - { - "fetch_from": "company.default_income_account", - "fieldname": "income_account", - "fieldtype": "Link", - "label": "Income Account", - "options": "Account" - }, { "fieldname": "column_break_16", "fieldtype": "Column Break" }, { - "fetch_from": "company.cost_center", "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -169,7 +159,7 @@ "link_fieldname": "fee_structure" } ], - "modified": "2024-01-23 15:05:32.737437", + "modified": "2024-05-29 14:22:54.268892", "modified_by": "Administrator", "module": "Education", "name": "Fee Structure", diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index cfc211e4..ae615d01 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -16,21 +16,6 @@ class FeeStructure(Document): def validate(self): self.calculate_total() self.validate_discount() - self.validate_fee_component() - - def validate_fee_component(self): - for component in self.components: - fee_component = frappe.get_doc("Fee Category", component.fees_category) - company_defaults = [] - for default in fee_component.item_defaults: - company_defaults.append(default.company) - - if self.company not in company_defaults: - frappe.throw( - _( - "The company of the Fee Category {0} and the Fee Structure should be the same." - ).format(frappe.bold(fee_component.name)) - ) def calculate_total(self): """Calculates total amount.""" diff --git a/education/education/test_utils.py b/education/education/test_utils.py index bd1e6ec2..ba96ffcb 100644 --- a/education/education/test_utils.py +++ b/education/education/test_utils.py @@ -194,15 +194,15 @@ def create_student_group( def create_fee_schedule( - academic_year=DEFAULT_ACADEMIC_YEAR, due_date="2023-04-01", submit=False + academic_year=DEFAULT_ACADEMIC_YEAR, submit=False, fee_structure=None ): - fee_structure = frappe.db.get_value( + due_date = frappe.utils.add_days(frappe.utils.nowdate(), 2) + fee_structure_name = fee_structure or frappe.db.get_value( "Fee Structure", {"academic_year": academic_year}, "name" ) - fee_schedule = frappe.new_doc("Fee Schedule") - fee_schedule.fee_structure = fee_structure - fee_schedule = get_fee_structure(fee_schedule) + fee_schedule.fee_structure = fee_structure_name + fee_schedule = get_fee_structure(fee_structure_name) fee_schedule.due_date = due_date student_groups = frappe.db.get_list( @@ -264,3 +264,13 @@ def create_company(company_name): {"doctype": "Company", "company_name": company_name, "default_currency": "INR"} ) company.insert(ignore_if_duplicate=True) + + +def get_defaults(company_name="_Test Company"): + defaults = frappe.get_all( + "Company", + filters={"name": company_name}, + fields=["default_income_account", "cost_center"], + limit=1, + )[0] + return defaults From 778a1fd59c2dec6998aa97f533f38ab229ca3f3e Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Wed, 29 May 2024 18:09:12 +0530 Subject: [PATCH 07/15] fix: fetch from receivable and cost center fields --- education/education/api.py | 1 - .../education/doctype/fee_structure/fee_structure.js | 8 -------- .../education/doctype/fee_structure/fee_structure.json | 6 +++++- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/education/education/api.py b/education/education/api.py index 8e52d911..cdafedcd 100644 --- a/education/education/api.py +++ b/education/education/api.py @@ -753,7 +753,6 @@ def get_school_abbr_logo(): @frappe.whitelist() def get_student_attendance(student, student_group): - print(student, student_group, "student,student_group") return frappe.db.get_list( "Student Attendance", filters={"student": student, "student_group": student_group, "docstatus": 1}, diff --git a/education/education/doctype/fee_structure/fee_structure.js b/education/education/doctype/fee_structure/fee_structure.js index e2a61011..8fe98962 100644 --- a/education/education/doctype/fee_structure/fee_structure.js +++ b/education/education/doctype/fee_structure/fee_structure.js @@ -3,14 +3,6 @@ frappe.provide("erpnext.accounts.dimensions"); frappe.ui.form.on("Fee Structure", { - setup: function (frm) { - frm.add_fetch( - "company", - "default_receivable_account", - "receivable_account" - ); - frm.add_fetch("company", "cost_center", "cost_center"); - }, company: function (frm) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); }, diff --git a/education/education/doctype/fee_structure/fee_structure.json b/education/education/doctype/fee_structure/fee_structure.json index d62f072c..f0aeb593 100644 --- a/education/education/doctype/fee_structure/fee_structure.json +++ b/education/education/doctype/fee_structure/fee_structure.json @@ -110,6 +110,8 @@ "label": "Accounts" }, { + "fetch_from": "company.default_receivable_account", + "fetch_if_empty": 1, "fieldname": "receivable_account", "fieldtype": "Link", "label": "Receivable Account", @@ -121,6 +123,8 @@ "fieldtype": "Column Break" }, { + "fetch_from": "company.cost_center", + "fetch_if_empty": 1, "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", @@ -159,7 +163,7 @@ "link_fieldname": "fee_structure" } ], - "modified": "2024-05-29 14:22:54.268892", + "modified": "2024-05-29 18:07:13.773830", "modified_by": "Administrator", "module": "Education", "name": "Fee Structure", From b4ef13a1a27bcc3ff83d5a6da3ea632d631a094b Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Thu, 30 May 2024 14:15:14 +0530 Subject: [PATCH 08/15] chore(test): code cleanup --- .../doctype/fee_category/test_fee_category.py | 14 ++++++++++---- .../fee_category_default/fee_category_default.json | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index 104f74e1..820b774e 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -19,13 +19,19 @@ def tearDown(self): frappe.db.rollback() def test_item_created(self): + """ + Test to check if the item master is created when a Fee Category is created. + """ companies = frappe.db.get_all("Company", fields=["name"]) - fee_category = frappe.get_doc("Fee Category", "Tuition Fee") - item = fee_category.get("item") + item = frappe.db.get_value( + "Fee Category", filters={"name": "Tuition Fee"}, fieldname="item" + ) self.assertTrue(frappe.db.exists("Item", item)) - item = frappe.get_doc("Item", item) - self.assertEqual(item.item_group, "Fee Component") + item_group = frappe.db.get_value( + "Item", filters={"name": item}, fieldname="item_group" + ) + self.assertEqual(item_group, "Fee Component") def test_item_defaults_from_item_group(self): """ diff --git a/education/education/doctype/fee_category_default/fee_category_default.json b/education/education/doctype/fee_category_default/fee_category_default.json index 09ffd997..6564a02c 100644 --- a/education/education/doctype/fee_category_default/fee_category_default.json +++ b/education/education/doctype/fee_category_default/fee_category_default.json @@ -24,7 +24,7 @@ "fieldname": "selling_cost_center", "fieldtype": "Link", "in_list_view": 1, - "label": "Default Selling Cost Center", + "label": "Default Cost Center", "link_filters": "[[\"Cost Center\",\"is_group\",\"=\",0],[\"Cost Center\",\"company\",\"=\",\"eval: doc.company\"]]", "options": "Cost Center" }, @@ -40,7 +40,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-05-28 12:17:22.020803", + "modified": "2024-05-30 12:54:29.285807", "modified_by": "Administrator", "module": "Education", "name": "Fee Category Default", From 00f68d7cfaeba5f3448611a37c9b5d28b60865c5 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sun, 2 Jun 2024 12:40:42 +0530 Subject: [PATCH 09/15] chore: code cleanup --- frontend/src/components/ProfileModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/ProfileModal.vue b/frontend/src/components/ProfileModal.vue index 2e89a1d4..c76d2c0f 100644 --- a/frontend/src/components/ProfileModal.vue +++ b/frontend/src/components/ProfileModal.vue @@ -76,7 +76,7 @@ const { getStudentInfo } = studentStore() const showProfileDialog = inject('showProfileDialog') const studentInfo = getStudentInfo().value -console.log(studentInfo) + const infoFormat = [ { section: 'section 1', From d4141848db5cb5f8abec63fb05eb554a6658c152 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Fri, 28 Jun 2024 13:08:10 +0530 Subject: [PATCH 10/15] fix: add validation to check defaults --- .../fee_category_default.json | 4 ++-- .../doctype/fee_structure/fee_structure.json | 3 ++- .../doctype/fee_structure/fee_structure.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/education/education/doctype/fee_category_default/fee_category_default.json b/education/education/doctype/fee_category_default/fee_category_default.json index 6564a02c..ffd39182 100644 --- a/education/education/doctype/fee_category_default/fee_category_default.json +++ b/education/education/doctype/fee_category_default/fee_category_default.json @@ -33,14 +33,14 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Default Income Account", - "link_filters": "[[\"Account\",\"company\",\"=\",\"eval: doc.company\"]]", + "link_filters": "[[\"Account\",\"company\",\"=\",\"eval: doc.company\"],[\"Account\",\"is_group\",\"=\",0],[\"Account\",\"root_type\",\"=\",\"Income\"]]", "options": "Account" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2024-05-30 12:54:29.285807", + "modified": "2024-06-28 13:05:14.403852", "modified_by": "Administrator", "module": "Education", "name": "Fee Category Default", diff --git a/education/education/doctype/fee_structure/fee_structure.json b/education/education/doctype/fee_structure/fee_structure.json index f0aeb593..654ec3d6 100644 --- a/education/education/doctype/fee_structure/fee_structure.json +++ b/education/education/doctype/fee_structure/fee_structure.json @@ -131,6 +131,7 @@ "options": "Cost Center" }, { + "description": "Ledger Entries will be created against the company mentioned here.", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -163,7 +164,7 @@ "link_fieldname": "fee_structure" } ], - "modified": "2024-05-29 18:07:13.773830", + "modified": "2024-06-21 16:05:34.011284", "modified_by": "Administrator", "module": "Education", "name": "Fee Structure", diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index ae615d01..19751df0 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -16,6 +16,7 @@ class FeeStructure(Document): def validate(self): self.calculate_total() self.validate_discount() + self.validate_component_defaults() def calculate_total(self): """Calculates total amount.""" @@ -31,6 +32,22 @@ def validate_discount(self): _("Discount cannot be greater than 100% in row {0}").format(component.idx) ) + def validate_component_defaults(self): + company = self.company + for fees_category in self.components: + fee_category = fees_category.fees_category + fee_category_default_income_account = frappe.db.get_value( + "Fee Category Default", + {"parent": fee_category, "company": company}, + "income_account", + ) + if not fee_category_default_income_account: + frappe.msgprint( + _("Accounting Defaults not set in row {0} for component {1} ").format( + frappe.bold(fees_category.idx), frappe.bold(fee_category) + ) + ) + def before_submit(self): for component in self.components: # create item for each component if it doesn't exist From 96fae8608b8c1875d6a71982147b23e9fcd5469d Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sat, 29 Jun 2024 14:28:45 +0530 Subject: [PATCH 11/15] fix: correct way to update item defaults --- .../doctype/fee_category/fee_category.py | 61 ++++++++++++++++--- .../doctype/fee_structure/fee_structure.py | 2 +- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/education/education/doctype/fee_category/fee_category.py b/education/education/doctype/fee_category/fee_category.py index 6e525312..59cee89f 100644 --- a/education/education/doctype/fee_category/fee_category.py +++ b/education/education/doctype/fee_category/fee_category.py @@ -3,6 +3,7 @@ import frappe from frappe.model.document import Document +from frappe import _ class FeeCategory(Document): @@ -42,6 +43,7 @@ def update_defaults_from_item_group(self): ) if item_defaults: update_item_defaults(self, item_defaults) + frappe.msgprint(_('Defaults fetched from "Fee Component" Item Group '), alert=True) def create_item(doc, use_name_field=True): @@ -61,18 +63,61 @@ def create_item(doc, use_name_field=True): return item.name -def update_item(doc): - item = frappe.get_doc("Item", doc.name) - item.item_name = doc.name - item.description = doc.description - item.item_defaults = [] - update_item_defaults(item, doc.item_defaults) +def update_item(fee_category): + item = frappe.get_doc("Item", fee_category.name) + item.item_name = fee_category.name + item.description = fee_category.description + + item_defaults = frappe.get_all( + "Item Default", + {"parent": fee_category.item}, + ["company", "selling_cost_center", "income_account"], + ) + item_default_companies = [d.company for d in item_defaults] + fee_category_companies = [d.company for d in fee_category.item_defaults] + for fee_category_default in fee_category.item_defaults: + if fee_category_default.company not in item_default_companies: + add_item_defaults(item, fee_category_default) + else: + update_defaults(item, fee_category_default) + + remove_item_defaults(item, fee_category_companies) item.save() return item.name -def update_item_defaults(item, item_defaults): - for item_default in item_defaults: +def add_item_defaults(item, fee_category_defaults): + item.append( + "item_defaults", + { + "company": fee_category_defaults.company, + "selling_cost_center": fee_category_defaults.selling_cost_center, + "income_account": fee_category_defaults.income_account, + "default_warehouse": "", + }, + ) + + +def update_defaults(item, fee_category_default): + for d in item.item_defaults: + if d.company == fee_category_default.company: + d.selling_cost_center = fee_category_default.selling_cost_center + d.income_account = fee_category_default.income_account + break + + +def remove_item_defaults(item, fee_category_companies): + items_to_remove = [] + for d in item.item_defaults: + if d.company not in fee_category_companies: + items_to_remove.append(d.idx) + + for idx in items_to_remove: + item.item_defaults.pop(idx - 1) + + +def update_item_defaults(item, defaults): + for item_default in defaults: item.append( "item_defaults", { diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index 19751df0..6fc949cd 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -43,7 +43,7 @@ def validate_component_defaults(self): ) if not fee_category_default_income_account: frappe.msgprint( - _("Accounting Defaults not set in row {0} for component {1} ").format( + _("Accounting Defaults are not set in row {0} for component {1} ").format( frappe.bold(fees_category.idx), frappe.bold(fee_category) ) ) From bba646e3570cb5da6028fff424388fa68f96a1f9 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sat, 29 Jun 2024 14:29:54 +0530 Subject: [PATCH 12/15] chore: remove dead code --- .../education/doctype/fee_structure/fee_structure.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/education/education/doctype/fee_structure/fee_structure.js b/education/education/doctype/fee_structure/fee_structure.js index 8fe98962..ce56cb06 100644 --- a/education/education/doctype/fee_structure/fee_structure.js +++ b/education/education/doctype/fee_structure/fee_structure.js @@ -25,15 +25,6 @@ frappe.ui.form.on("Fee Structure", { }, }; }); - frm.set_query("income_account", function (doc) { - return { - filters: { - account_type: "Income Account", - is_group: 0, - company: doc.company, - }, - }; - }); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); }, From a6e8529214352f2c5d1127b9069ebfab892ff206 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sat, 29 Jun 2024 14:42:32 +0530 Subject: [PATCH 13/15] fix: add validation for duplicate companies in fee category --- education/education/doctype/fee_category/fee_category.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/education/education/doctype/fee_category/fee_category.py b/education/education/doctype/fee_category/fee_category.py index 59cee89f..1df0f4ed 100644 --- a/education/education/doctype/fee_category/fee_category.py +++ b/education/education/doctype/fee_category/fee_category.py @@ -9,6 +9,7 @@ class FeeCategory(Document): def validate(self): self.update_defaults_from_item_group() + self.validate_duplicate_item_defaults() def after_insert(self): # create an item @@ -45,6 +46,12 @@ def update_defaults_from_item_group(self): update_item_defaults(self, item_defaults) frappe.msgprint(_('Defaults fetched from "Fee Component" Item Group '), alert=True) + def validate_duplicate_item_defaults(self): + """Validate duplicate item defaults""" + companies = [d.company for d in self.item_defaults] + if len(companies) != len(set(companies)): + frappe.throw(_("Cannot set multiple Item Defaults for a company.")) + def create_item(doc, use_name_field=True): name_field = doc.name if use_name_field else doc.fees_category From 240b3687b2e8e2dac486979e6e7518c0c58076f6 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sat, 29 Jun 2024 15:29:45 +0530 Subject: [PATCH 14/15] fix: improve discount calculation --- education/education/doctype/fee_structure/fee_structure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index 6fc949cd..6f217bb4 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -180,12 +180,12 @@ def make_fee_schedule( for component in doc.components: component.total = per_component_amount.get(component.fees_category) - discount = flt(component.discount) / 100 - if discount == 1: + if component.discount == 100: component.amount = component.total else: - component.amount = flt((component.total) / flt(1 - discount)) + component.amount = flt((component.total) / flt(100 - component.discount)) * 100 + # com amount_per_month += component.total # amount_per_month will be the total amount for each Fee Structure doc.total_amount = amount_per_month From 6c037e690bf6887d7a93e454c3beb396d39596d6 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sat, 29 Jun 2024 15:30:17 +0530 Subject: [PATCH 15/15] chore: code cleanup --- education/education/doctype/fee_structure/fee_structure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/education/education/doctype/fee_structure/fee_structure.py b/education/education/doctype/fee_structure/fee_structure.py index 6f217bb4..92267d0d 100644 --- a/education/education/doctype/fee_structure/fee_structure.py +++ b/education/education/doctype/fee_structure/fee_structure.py @@ -185,7 +185,7 @@ def make_fee_schedule( component.amount = component.total else: component.amount = flt((component.total) / flt(100 - component.discount)) * 100 - # com + amount_per_month += component.total # amount_per_month will be the total amount for each Fee Structure doc.total_amount = amount_per_month