Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 146 additions & 1 deletion employee_self_service/background_jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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():
Expand Down Expand Up @@ -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)

Original file line number Diff line number Diff line change
@@ -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) {

// },
// });
Original file line number Diff line number Diff line change
@@ -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": []
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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) {

// },
// });
Loading
Loading