From 9c04a6e511c7034a30ad5373136cb982d402a859 Mon Sep 17 00:00:00 2001 From: Manav Mandli Date: Tue, 26 May 2026 14:11:17 +0000 Subject: [PATCH] feat: implement ess field staff and create new ess visit doctype --- .../background_jobs/__init__.py | 147 ++++++++- .../ess_field_staff_settings/__init__.py | 0 .../ess_field_staff_settings.js | 8 + .../ess_field_staff_settings.json | 80 +++++ .../ess_field_staff_settings.py | 9 + .../test_ess_field_staff_settings.py | 22 ++ .../doctype/ess_visit/__init__.py | 0 .../doctype/ess_visit/ess_visit.js | 8 + .../doctype/ess_visit/ess_visit.json | 190 ++++++++++++ .../doctype/ess_visit/ess_visit.py | 9 + .../doctype/ess_visit/test_ess_visit.py | 22 ++ .../ess_visit_schedule_rule/__init__.py | 0 .../ess_visit_schedule_rule.js | 8 + .../ess_visit_schedule_rule.json | 224 ++++++++++++++ .../ess_visit_schedule_rule.py | 97 ++++++ .../test_ess_visit_schedule_rule.py | 22 ++ .../doctype/visit/visit.json | 284 +++++++++--------- .../doctype/visit_proof/__init__.py | 0 .../doctype/visit_proof/visit_proof.json | 34 +++ .../doctype/visit_proof/visit_proof.py | 9 + .../visit_schedule_customer/__init__.py | 0 .../visit_schedule_customer.json | 33 ++ .../visit_schedule_customer.py | 9 + .../visit_schedule_customer_group/__init__.py | 0 .../visit_schedule_customer_group.json | 36 +++ .../visit_schedule_customer_group.py | 9 + .../visit_schedule_territory/__init__.py | 0 .../visit_schedule_territory.json | 36 +++ .../visit_schedule_territory.py | 9 + employee_self_service/mobile/v1/visit.py | 10 + 30 files changed, 1172 insertions(+), 143 deletions(-) create mode 100644 employee_self_service/employee_self_service/doctype/ess_field_staff_settings/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.js create mode 100644 employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.json create mode 100644 employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_field_staff_settings/test_ess_field_staff_settings.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.js create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.json create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit/test_ess_visit.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.js create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.json create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.py create mode 100644 employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/test_ess_visit_schedule_rule.py mode change 100755 => 100644 employee_self_service/employee_self_service/doctype/visit/visit.json create mode 100644 employee_self_service/employee_self_service/doctype/visit_proof/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.json create mode 100644 employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.json create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.json create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_territory/__init__.py create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.json create mode 100644 employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.py diff --git a/employee_self_service/background_jobs/__init__.py b/employee_self_service/background_jobs/__init__.py index ae41432..3d61726 100644 --- a/employee_self_service/background_jobs/__init__.py +++ b/employee_self_service/background_jobs/__init__.py @@ -7,8 +7,10 @@ now_datetime, time_diff, today, + get_weekday, + getdate ) - +from frappe import _ from employee_self_service.utils import ( get_employees_having_an_event_today, is_holiday, @@ -20,6 +22,7 @@ def process_daily_ess_jobs(): close_ess_poll() on_holiday_event() send_notification_on_event() + process_visit_schedule_rules() def close_ess_poll(): @@ -378,3 +381,145 @@ def get_assigned_employees(shift, date, checkedin_employees): ) return final_employees + + +def process_visit_schedule_rules(): + try: + settings = frappe.get_single("ESS Field Staff Settings") + create_todo = settings.visit_schedule_create_todo + + rules = frappe.get_all( + "ESS Visit Schedule Rule", + filters={"enabled": 1}, + pluck="name" + ) + + for rule in rules: + rule_doc = frappe.get_doc("ESS Visit Schedule Rule", rule) + if not should_process_rule(rule_doc): + continue + + customers = get_customers_from_rule(rule_doc) + for customer in customers: + if create_todo: + create_visit_todo(rule_doc, customer) + else: + create_visit_record(rule_doc, customer) + except Exception: + frappe.log_error(title="Process Visit Schedule Rules", message=frappe.get_traceback()) + +def should_process_rule(rule_doc): + current_date = getdate(today()) + schedule_type = rule_doc.schedule_type + + if schedule_type == "Daily": + weekday_map = { + 0: "monday", 1: "tuesday", 2: "wednesday", 3: "thursday", + 4: "friday", 5: "saturday", 6: "sunday" + } + weekday_field = weekday_map.get(current_date.weekday()) + return rule_doc.get(weekday_field) == 1 + + elif schedule_type == "Weekly": + return current_date.strftime("%A") == rule_doc.day_of_week + + elif schedule_type == "Fortnightly": + if current_date.strftime("%A") == rule_doc.day_of_week: + day_of_month = current_date.day + return (day_of_month <= 7) or (15 <= day_of_month <= 21) + + elif schedule_type == "Monthly": + expected_day = cint(rule_doc.date_of_month) + return cint(current_date.day) == expected_day + + elif schedule_type == "Quarterly": + quarter_months = { + 1: [1, 2, 3], + 2: [4, 5, 6], + 3: [7, 8, 9], + 4: [10, 11, 12] + } + expected_quarter = cint(rule_doc.quarter) + return ( + current_date.month in quarter_months.get(expected_quarter, []) + and cint(current_date.day) == cint(rule_doc.date_of_month) + ) + + elif schedule_type == "Yearly": + return cint(current_date.day) == cint(rule_doc.date_of_month) and cint(current_date.month) == cint(rule_doc.month_of_year) + + return False + + +def get_customers_from_rule(rule_doc): + customers = [] + + if rule_doc.based_on == "Customer": + customers = [row.customer for row in rule_doc.customers] + + elif rule_doc.based_on == "Customer Group": + customer_groups = [row.customer_group for row in rule_doc.customer_groups] + customers = frappe.get_all( + "Customer", + filters={"customer_group": ["in", customer_groups]}, + pluck="name" + ) + + elif rule_doc.based_on == "Territory": + territories = [row.territory for row in rule_doc.territories] + customers = frappe.get_all( + "Customer", + filters={"territory": ["in", territories]}, + pluck="name" + ) + + return customers + + +def create_visit_todo(rule_doc, customer): + if frappe.db.exists("ToDo", { + "reference_type": "ESS Visit Schedule Rule", + "reference_name": rule_doc.name, + "allocated_to": rule_doc.user, + "date": today(), + "description": ["like", f"%{customer}%"] + }): + return + + customer_name = frappe.db.get_value("Customer", customer, "customer_name") or customer + + todo = frappe.get_doc({ + "doctype": "ToDo", + "reference_type": "ESS Visit Schedule Rule", + "reference_name": rule_doc.name, + "allocated_to": rule_doc.user, + "date": today(), + "description": f"Visit scheduled for customer: {customer_name}", + "priority": "Medium", + "status": "Open" + }) + todo.insert(ignore_permissions=True) + +def create_visit_record(rule_doc, customer): + if frappe.db.exists("ESS Visit", { + "employee": rule_doc.employee, + "customer": customer, + "date": today(), + "auto_created_from_rule": 1 + }): + return + + visit = frappe.get_doc({ + "doctype": "ESS Visit", + "customer_type": "Existing", + "customer": customer, + "employee": rule_doc.employee, + "user": rule_doc.user, + "date": today(), + "visit_type": rule_doc.visit_type, + "status": "Pending", + "auto_created_from_rule": 1, + "description": f"Auto-scheduled visit from rule: {rule_doc.rule_name}" + }) + visit.insert(ignore_permissions=True) + diff --git a/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/__init__.py b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.js b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.js new file mode 100644 index 0000000..57c907b --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("ESS Field Staff Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.json b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.json new file mode 100644 index 0000000..356052b --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.json @@ -0,0 +1,80 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-26 14:54:19.656176", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "customer_geo_tagging_auto_based_on", + "manual_geo_tagging_allowed", + "visit_required_geofencing", + "allowed_radius", + "column_break_tvlp", + "visit_schedule_create_todo" + ], + "fields": [ + { + "default": "0", + "fieldname": "visit_schedule_create_todo", + "fieldtype": "Check", + "label": "Visit Schedule Create ToDo" + }, + { + "default": "Visit", + "fieldname": "customer_geo_tagging_auto_based_on", + "fieldtype": "Select", + "label": "Customer Geo Tagging Auto Based On", + "options": "Visit\nSales Order" + }, + { + "default": "1", + "fieldname": "manual_geo_tagging_allowed", + "fieldtype": "Check", + "label": "Manual Geo Tagging Allowed" + }, + { + "fieldname": "column_break_tvlp", + "fieldtype": "Column Break" + }, + { + "default": "100", + "depends_on": "eval:doc.visit_required_geofencing == 1", + "fieldname": "allowed_radius", + "fieldtype": "Int", + "label": "Allowed Radius (Meters)", + "mandatory_depends_on": "eval:doc.visit_required_geofencing == 1" + }, + { + "default": "0", + "fieldname": "visit_required_geofencing", + "fieldtype": "Check", + "label": "Visit Checkin/Checkout Required Geofencing" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2026-05-26 15:03:05.037126", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "ESS Field Staff Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.py b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.py new file mode 100644 index 0000000..fea7b50 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/ess_field_staff_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ESSFieldStaffSettings(Document): + pass diff --git a/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/test_ess_field_staff_settings.py b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/test_ess_field_staff_settings.py new file mode 100644 index 0000000..e84d1b5 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_field_staff_settings/test_ess_field_staff_settings.py @@ -0,0 +1,22 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestESSFieldStaffSettings(IntegrationTestCase): + """ + Integration tests for ESSFieldStaffSettings. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/employee_self_service/employee_self_service/doctype/ess_visit/__init__.py b/employee_self_service/employee_self_service/doctype/ess_visit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.js b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.js new file mode 100644 index 0000000..c9de9fa --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("ESS Visit", { +// refresh(frm) { + +// }, +// }); diff --git a/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.json b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.json new file mode 100644 index 0000000..fb0e74d --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.json @@ -0,0 +1,190 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "EVISIT.#####", + "creation": "2026-05-26 19:15:43.862478", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_type", + "customer", + "customer_name", + "auto_created_from_rule", + "column_break_ulu4v", + "status", + "visit_start_time", + "visit_stop_time", + "section_break_vxr9z", + "visit_type", + "column_break_okbrh", + "description", + "section_break_smmwi", + "visit_start_location", + "visit_stop_location", + "column_break_ssqu5", + "employee", + "user", + "user_remarks", + "customer_signature", + "section_break_visit_proof", + "visit_proof" + ], + "fields": [ + { + "default": "Existing", + "fieldname": "customer_type", + "fieldtype": "Select", + "label": "Customer Type", + "options": "Existing\nNew", + "reqd": 1 + }, + { + "depends_on": "eval:doc.customer_type == \"Existing\"", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "mandatory_depends_on": "eval:doc.customer_type == \"Existing\"", + "options": "Customer" + }, + { + "fetch_from": "customer.customer_name", + "fetch_if_empty": 1, + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "mandatory_depends_on": "eval: doc.customer_type == \"New\"" + }, + { + "default": "0", + "fieldname": "auto_created_from_rule", + "fieldtype": "Check", + "label": "Auto Created From Rule", + "read_only": 1 + }, + { + "fieldname": "column_break_ulu4v", + "fieldtype": "Column Break" + }, + { + "default": "Pending", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Pending\nIn Progress\nCompleted", + "reqd": 1 + }, + { + "fieldname": "visit_start_time", + "fieldtype": "Datetime", + "label": "Visit Start Time" + }, + { + "fieldname": "visit_stop_time", + "fieldtype": "Datetime", + "label": "Visit Stop Time" + }, + { + "fieldname": "section_break_vxr9z", + "fieldtype": "Section Break" + }, + { + "fieldname": "visit_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Visit Type", + "options": "Visit Type", + "reqd": 1 + }, + { + "fieldname": "column_break_okbrh", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "section_break_smmwi", + "fieldtype": "Section Break" + }, + { + "fieldname": "visit_start_location", + "fieldtype": "Geolocation", + "label": "Visit Start Location" + }, + { + "fieldname": "visit_stop_location", + "fieldtype": "Geolocation", + "label": "Visit Stop Location" + }, + { + "fieldname": "column_break_ssqu5", + "fieldtype": "Column Break" + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee" + }, + { + "fetch_from": "employee.user_id", + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "user_remarks", + "fieldtype": "Small Text", + "label": "User Remarks" + }, + { + "fieldname": "customer_signature", + "fieldtype": "Signature", + "label": "Customer Signature" + }, + { + "fieldname": "section_break_visit_proof", + "fieldtype": "Section Break", + "label": "Visit Proofs" + }, + { + "fieldname": "visit_proof", + "fieldtype": "Table", + "label": "Visit Proofs", + "options": "Visit Proof" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-26 19:18:52.856719", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "ESS Visit", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.py b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.py new file mode 100644 index 0000000..4f510b4 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit/ess_visit.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ESSVisit(Document): + pass diff --git a/employee_self_service/employee_self_service/doctype/ess_visit/test_ess_visit.py b/employee_self_service/employee_self_service/doctype/ess_visit/test_ess_visit.py new file mode 100644 index 0000000..4642b1e --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit/test_ess_visit.py @@ -0,0 +1,22 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestESSVisit(IntegrationTestCase): + """ + Integration tests for ESSVisit. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/__init__.py b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.js b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.js new file mode 100644 index 0000000..b83ef3e --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.js @@ -0,0 +1,8 @@ +// Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("ESS Visit Schedule Rule", { +// refresh(frm) { + +// }, +// }); diff --git a/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.json b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.json new file mode 100644 index 0000000..6885d14 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.json @@ -0,0 +1,224 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "VSR.#####", + "creation": "2026-05-26 15:20:29.572418", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "enabled", + "rule_name", + "employee", + "user", + "visit_type", + "based_on", + "schedule_type", + "column_break_hzau", + "customers", + "customer_groups", + "territories", + "daily_section", + "monday", + "tuesday", + "wednesday", + "thursday", + "column_break_ifwl", + "friday", + "saturday", + "sunday", + "section_break_wfbd", + "day_of_week", + "date_of_month", + "quarter", + "month_of_year" + ], + "fields": [ + { + "fieldname": "rule_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Rule Name", + "reqd": 1 + }, + { + "fieldname": "based_on", + "fieldtype": "Select", + "label": "Based On", + "options": "Customer\nCustomer Group\nTerritory", + "reqd": 1 + }, + { + "depends_on": "eval:doc.based_on == \"Customer\"", + "fieldname": "customers", + "fieldtype": "Table", + "label": "Customers", + "options": "Visit Schedule Customer" + }, + { + "depends_on": "eval: doc.based_on == \"Customer Group\"", + "fieldname": "customer_groups", + "fieldtype": "Table", + "label": "Customer Groups", + "options": "Visit Schedule Customer Group" + }, + { + "depends_on": "eval:doc.based_on == \"Territory\"", + "fieldname": "territories", + "fieldtype": "Table", + "label": "Territories", + "options": "Visit Schedule Territory" + }, + { + "fieldname": "column_break_hzau", + "fieldtype": "Column Break" + }, + { + "fieldname": "schedule_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Schedule Type", + "options": "Daily\nWeekly\nFortnightly\nMonthly\nQuarterly\nYearly", + "reqd": 1 + }, + { + "depends_on": "eval:doc.schedule_type == \"Daily\"", + "fieldname": "daily_section", + "fieldtype": "Section Break", + "label": "Daily" + }, + { + "default": "0", + "fieldname": "monday", + "fieldtype": "Check", + "label": "Monday" + }, + { + "default": "0", + "fieldname": "tuesday", + "fieldtype": "Check", + "label": "Tuesday" + }, + { + "default": "0", + "fieldname": "wednesday", + "fieldtype": "Check", + "label": "Wednesday" + }, + { + "default": "0", + "fieldname": "thursday", + "fieldtype": "Check", + "label": "Thursday" + }, + { + "fieldname": "column_break_ifwl", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "friday", + "fieldtype": "Check", + "label": "Friday" + }, + { + "default": "0", + "fieldname": "saturday", + "fieldtype": "Check", + "label": "Saturday" + }, + { + "default": "0", + "fieldname": "sunday", + "fieldtype": "Check", + "label": "Sunday" + }, + { + "depends_on": "eval:[\"Fortnightly\", \"Weekly\"].includes(doc.schedule_type)", + "fieldname": "day_of_week", + "fieldtype": "Select", + "label": "Day of Week/Fortnight", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday" + }, + { + "depends_on": "eval:[\"Monthly\", \"Quarterly\", \"Yearly\"].includes(doc.schedule_type)", + "description": "Days 29-31 will be skipped in months that don't have those dates (e.g., February, April)", + "fieldname": "date_of_month", + "fieldtype": "Int", + "label": "Date of Month" + }, + { + "fieldname": "section_break_wfbd", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.schedule_type == \"Quarterly\"", + "fieldname": "quarter", + "fieldtype": "Select", + "label": "Quarter", + "options": "1\n2\n3\n4" + }, + { + "depends_on": "eval:doc.schedule_type == \"Yearly\"", + "fieldname": "month_of_year", + "fieldtype": "Select", + "label": "Month of Year", + "options": "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12" + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.user_id", + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "visit_type", + "fieldtype": "Link", + "label": "Visit Type", + "options": "Visit Type", + "reqd": 1 + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2026-05-26 19:13:56.816990", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "ESS Visit Schedule Rule", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.py b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.py new file mode 100644 index 0000000..813f7fb --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/ess_visit_schedule_rule.py @@ -0,0 +1,97 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import cint + + +class ESSVisitScheduleRule(Document): + def validate(self): + self.validate_schedule_config() + self.validate_based_on() + self.validate_date_of_month() + + def validate_schedule_config(self): + if self.schedule_type == "Daily": + days_selected = any([ + self.monday, self.tuesday, self.wednesday, self.thursday, + self.friday, self.saturday, self.sunday + ]) + if not days_selected: + frappe.throw(_("Please select at least one day for Daily schedule")) + + elif self.schedule_type in ["Weekly", "Fortnightly"]: + if not self.day_of_week: + frappe.throw(_("Please select a day for {0} schedule").format(self.schedule_type)) + + elif self.schedule_type == "Monthly": + if not self.date_of_month: + frappe.throw(_("Please select date of month for Monthly schedule")) + + elif self.schedule_type == "Quarterly": + if not self.quarter: + frappe.throw(_("Please select a quarter for Quarterly schedule")) + if not self.date_of_month: + frappe.throw(_("Please select date of month for Quarterly schedule")) + + elif self.schedule_type == "Yearly": + if not self.month_of_year: + frappe.throw(_("Please select month of year for Yearly schedule")) + if not self.date_of_month: + frappe.throw(_("Please select date of month for Yearly schedule")) + + def validate_based_on(self): + if self.based_on == "Customer" and not self.customers: + frappe.throw(_("Please add at least one customer")) + elif self.based_on == "Customer Group" and not self.customer_groups: + frappe.throw(_("Please add at least one customer group")) + elif self.based_on == "Territory" and not self.territories: + frappe.throw(_("Please add at least one territory")) + + def validate_date_of_month(self): + if self.schedule_type in ["Monthly", "Quarterly", "Yearly"] and self.date_of_month: + day = cint(self.date_of_month) + + if day < 1 or day > 31: + frappe.throw(_("Date of month must be between 1 and 31")) + + if self.schedule_type == "Yearly" and self.month_of_year: + month = cint(self.month_of_year) + month_names = { + 1: "January", 2: "February", 3: "March", 4: "April", + 5: "May", 6: "June", 7: "July", 8: "August", + 9: "September", 10: "October", 11: "November", 12: "December" + } + months_30_days = [4, 6, 9, 11] + + if month == 2 and day > 29: + frappe.throw(_("February can have maximum 29 days. Please select a date between 1 and 29")) + elif month in months_30_days and day > 30: + frappe.throw(_("{0} can have maximum 30 days. Please select a date between 1 and 30").format(month_names.get(month))) + elif month == 2 and day == 29: + frappe.msgprint( + _("Day 29 will be skipped in February during non-leap years"), + indicator="orange", + alert=True + ) + else: + if day == 31: + frappe.msgprint( + _("Day 31 will be skipped in months with 30 days (April, June, September, November) and February"), + indicator="orange", + alert=True + ) + elif day == 30: + frappe.msgprint( + _("Day 30 will be skipped in February"), + indicator="orange", + alert=True + ) + elif day == 29: + frappe.msgprint( + _("Day 29 will be skipped in February during non-leap years"), + indicator="orange", + alert=True + ) diff --git a/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/test_ess_visit_schedule_rule.py b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/test_ess_visit_schedule_rule.py new file mode 100644 index 0000000..5f09408 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/ess_visit_schedule_rule/test_ess_visit_schedule_rule.py @@ -0,0 +1,22 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + + +class IntegrationTestESSVisitScheduleRule(IntegrationTestCase): + """ + Integration tests for ESSVisitScheduleRule. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/employee_self_service/employee_self_service/doctype/visit/visit.json b/employee_self_service/employee_self_service/doctype/visit/visit.json old mode 100755 new mode 100644 index f57efc9..e1117c5 --- a/employee_self_service/employee_self_service/doctype/visit/visit.json +++ b/employee_self_service/employee_self_service/doctype/visit/visit.json @@ -1,143 +1,143 @@ -{ - "actions": [], - "allow_rename": 1, - "autoname": "VISIT.#####", - "creation": "2023-04-21 20:30:24.865241", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "customer", - "customer_name", - "column_break_ulu4v", - "date", - "time", - "section_break_vxr9z", - "visit_type", - "column_break_okbrh", - "description", - "section_break_smmwi", - "location", - "column_break_ssqu5", - "employee", - "user", - "section_break_visit_proof", - "visit_proof" - ], - "fields": [ - { - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, - { - "fetch_from": "customer.customer_name", - "fetch_if_empty": 1, - "fieldname": "customer_name", - "fieldtype": "Read Only", - "label": "Customer Name" - }, - { - "fieldname": "column_break_ulu4v", - "fieldtype": "Column Break" - }, - { - "default": "Today", - "fieldname": "date", - "fieldtype": "Date", - "label": "Date" - }, - { - "fieldname": "time", - "fieldtype": "Time", - "label": "Time" - }, - { - "fieldname": "section_break_vxr9z", - "fieldtype": "Section Break" - }, - { - "fieldname": "visit_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Visit Type", - "options": "Visit Type", - "reqd": 1 - }, - { - "fieldname": "column_break_okbrh", - "fieldtype": "Column Break" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description", - "reqd": 1 - }, - { - "fieldname": "section_break_smmwi", - "fieldtype": "Section Break" - }, - { - "fieldname": "location", - "fieldtype": "Small Text", - "label": "Location" - }, - { - "fieldname": "column_break_ssqu5", - "fieldtype": "Column Break" - }, - { - "fieldname": "employee", - "fieldtype": "Link", - "label": "Employee", - "options": "Employee" - }, - { - "fetch_from": "employee.user_id", - "fieldname": "user", - "fieldtype": "Link", - "label": "User", - "options": "User" - }, - { - "fieldname": "section_break_visit_proof", - "fieldtype": "Section Break", - "label": "Visit Proof" - }, - { - "fieldname": "visit_proof", - "fieldtype": "Attach Image", - "label": "Visit Proof" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-09-28 12:40:37.494515", - "modified_by": "bhavesh@nesscale.com", - "module": "Employee Self Service", - "name": "Visit", - "naming_rule": "By fieldname", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "row_format": "Dynamic", - "sort_field": "modified", - "sort_order": "DESC", - "states": [] +{ + "actions": [], + "allow_rename": 1, + "autoname": "VISIT.#####", + "creation": "2023-04-21 20:30:24.865241", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "customer_name", + "column_break_ulu4v", + "date", + "time", + "section_break_vxr9z", + "visit_type", + "column_break_okbrh", + "description", + "section_break_smmwi", + "location", + "column_break_ssqu5", + "employee", + "user", + "section_break_visit_proof", + "visit_proof" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "fetch_from": "customer.customer_name", + "fetch_if_empty": 1, + "fieldname": "customer_name", + "fieldtype": "Read Only", + "label": "Customer Name" + }, + { + "fieldname": "column_break_ulu4v", + "fieldtype": "Column Break" + }, + { + "default": "Today", + "fieldname": "date", + "fieldtype": "Date", + "label": "Date" + }, + { + "fieldname": "time", + "fieldtype": "Time", + "label": "Time" + }, + { + "fieldname": "section_break_vxr9z", + "fieldtype": "Section Break" + }, + { + "fieldname": "visit_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Visit Type", + "options": "Visit Type", + "reqd": 1 + }, + { + "fieldname": "column_break_okbrh", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "fieldname": "section_break_smmwi", + "fieldtype": "Section Break" + }, + { + "fieldname": "location", + "fieldtype": "Small Text", + "label": "Location" + }, + { + "fieldname": "column_break_ssqu5", + "fieldtype": "Column Break" + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee" + }, + { + "fetch_from": "employee.user_id", + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User" + }, + { + "fieldname": "section_break_visit_proof", + "fieldtype": "Section Break", + "label": "Visit Proof" + }, + { + "fieldname": "visit_proof", + "fieldtype": "Attach Image", + "label": "Visit Proof" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-09-28 12:40:37.494515", + "modified_by": "bhavesh@nesscale.com", + "module": "Employee Self Service", + "name": "Visit", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/visit_proof/__init__.py b/employee_self_service/employee_self_service/doctype/visit_proof/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.json b/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.json new file mode 100644 index 0000000..edbfba5 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.json @@ -0,0 +1,34 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-26 15:38:42.594371", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "attachment" + ], + "fields": [ + { + "fieldname": "attachment", + "fieldtype": "Attach", + "in_list_view": 1, + "label": "Attachment" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-26 15:39:12.604763", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "Visit Proof", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.py b/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.py new file mode 100644 index 0000000..c9b2727 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_proof/visit_proof.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class VisitProof(Document): + pass diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer/__init__.py b/employee_self_service/employee_self_service/doctype/visit_schedule_customer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.json b/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.json new file mode 100644 index 0000000..deb8c68 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2026-05-26 15:23:39.273976", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "search_index": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2026-05-26 15:26:44.002809", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "Visit Schedule Customer", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.py b/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.py new file mode 100644 index 0000000..730fdc8 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_customer/visit_schedule_customer.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class VisitScheduleCustomer(Document): + pass diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/__init__.py b/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.json b/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.json new file mode 100644 index 0000000..dc78f82 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.json @@ -0,0 +1,36 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-26 15:27:09.040674", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_group" + ], + "fields": [ + { + "fieldname": "customer_group", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer Group", + "options": "Customer Group", + "search_index": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-26 15:27:28.982939", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "Visit Schedule Customer Group", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.py b/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.py new file mode 100644 index 0000000..d32c1ad --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_customer_group/visit_schedule_customer_group.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class VisitScheduleCustomerGroup(Document): + pass diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_territory/__init__.py b/employee_self_service/employee_self_service/doctype/visit_schedule_territory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.json b/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.json new file mode 100644 index 0000000..8766e41 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.json @@ -0,0 +1,36 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2026-05-26 15:27:49.896032", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "territory" + ], + "fields": [ + { + "fieldname": "territory", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Territory", + "options": "Territory", + "search_index": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2026-05-26 15:28:04.747824", + "modified_by": "Administrator", + "module": "Employee Self Service", + "name": "Visit Schedule Territory", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "rows_threshold_for_grid_search": 20, + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.py b/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.py new file mode 100644 index 0000000..7d45162 --- /dev/null +++ b/employee_self_service/employee_self_service/doctype/visit_schedule_territory/visit_schedule_territory.py @@ -0,0 +1,9 @@ +# Copyright (c) 2026, Nesscale Solutions Private Limited and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class VisitScheduleTerritory(Document): + pass diff --git a/employee_self_service/mobile/v1/visit.py b/employee_self_service/mobile/v1/visit.py index f96666b..f919351 100644 --- a/employee_self_service/mobile/v1/visit.py +++ b/employee_self_service/mobile/v1/visit.py @@ -131,3 +131,13 @@ def get_visit_type(): return gen_response(200, "Visit Type Get Successfully", visit_type) except Exception as e: return exception_handler(e) + +@frappe.whitelist() +@ess_validate(methods=["GET"]) +def get_ess_field_staff_settings(): + try: + ess_field_staff_settings = frappe.get_single("ESS Field Staff Settings") + return gen_response(200, "ESS Field Staff Settings Get Successfully", ess_field_staff_settings) + except Exception as e: + return exception_handler(e) +