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_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..1df0f4ed 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 _ class FeeCategory(Document): + def validate(self): + self.update_defaults_from_item_group() + self.validate_duplicate_item_defaults() + def after_insert(self): # create an item item_name = create_item(self) @@ -21,6 +26,32 @@ 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) + 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 @@ -34,14 +65,72 @@ 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 -def update_item(doc): - item = frappe.get_doc("Item", doc.name) - item.item_name = doc.name - item.description = doc.description +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 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", + { + "company": item_default.company, + "selling_cost_center": item_default.selling_cost_center, + "income_account": item_default.income_account, + "default_warehouse": "", + }, + ) diff --git a/education/education/doctype/fee_category/test_fee_category.py b/education/education/doctype/fee_category/test_fee_category.py index f2b4b7f3..820b774e 100644 --- a/education/education/doctype/fee_category/test_fee_category.py +++ b/education/education/doctype/fee_category/test_fee_category.py @@ -1,8 +1,113 @@ # 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, + create_company, + get_defaults, +) class TestFeeCategory(FrappeTestCase): - pass + 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): + """ + Test to check if the item master is created when a Fee Category is created. + """ + companies = frappe.db.get_all("Company", fields=["name"]) + item = frappe.db.get_value( + "Fee Category", filters={"name": "Tuition Fee"}, fieldname="item" + ) + self.assertTrue(frappe.db.exists("Item", item)) + + 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): + """ + When creating a Fee Category, if there are no item defaults, then get defaults from Item Group + """ + defaults = get_defaults() + + 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 = get_defaults() + + 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_duplicate_default(self): + """ + When setting defaults if there are 2 defaults for the same company, then throw an error + """ + 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) 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..ffd39182 --- /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 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\"],[\"Account\",\"is_group\",\"=\",0],[\"Account\",\"root_type\",\"=\",\"Income\"]]", + "options": "Account" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-06-28 13:05:14.403852", + "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 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/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_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 4d70f565..ce56cb06 100644 --- a/education/education/doctype/fee_structure/fee_structure.js +++ b/education/education/doctype/fee_structure/fee_structure.js @@ -2,233 +2,241 @@ // For license information, please see license.txt 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, + }, + }; + }); + + 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.json b/education/education/doctype/fee_structure/fee_structure.json index a59a62a5..654ec3d6 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", @@ -112,31 +111,27 @@ }, { "fetch_from": "company.default_receivable_account", + "fetch_if_empty": 1, "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", + "fetch_if_empty": 1, "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" }, { + "description": "Ledger Entries will be created against the company mentioned here.", "fieldname": "company", "fieldtype": "Link", "label": "Company", @@ -169,7 +164,7 @@ "link_fieldname": "fee_structure" } ], - "modified": "2024-01-23 15:05:32.737437", + "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 92ae3a6e..92267d0d 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 are 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 @@ -163,8 +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 - component.amount = flt((component.total) / (1 - discount)) + + if component.discount == 100: + component.amount = component.total + else: + component.amount = flt((component.total) / flt(100 - component.discount)) * 100 + 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 59fbe472..ba96ffcb 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): @@ -116,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() @@ -193,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( @@ -256,3 +257,20 @@ 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) + + +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/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',