Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: Auto Attendance processing #517

Merged
merged 6 commits into from
May 16, 2023
Merged
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
10 changes: 7 additions & 3 deletions hrms/hr/doctype/employee_checkin/employee_checkin.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.employee_name",
Expand All @@ -48,7 +49,8 @@
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type",
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "column_break_4",
Expand Down Expand Up @@ -107,10 +109,11 @@
}
],
"links": [],
"modified": "2020-07-08 11:02:32.660986",
"modified": "2023-05-12 14:52:22.660264",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
Expand Down Expand Up @@ -203,6 +206,7 @@
],
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "employee_name",
"track_changes": 1
}
4 changes: 2 additions & 2 deletions hrms/hr/doctype/employee_checkin/employee_checkin.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def mark_attendance_and_link_log(
return None

elif attendance_status in ("Present", "Absent", "Half Day"):
employee_doc = frappe.get_doc("Employee", employee)
company = frappe.db.get_value("Employee", employee, "company", cache=True)
duplicate = get_duplicate_attendance_record(employee, attendance_date, shift)
overlapping = get_overlapping_shift_attendance(employee, attendance_date, shift)

Expand All @@ -147,7 +147,7 @@ def mark_attendance_and_link_log(
"attendance_date": attendance_date,
"status": attendance_status,
"working_hours": working_hours,
"company": employee_doc.company,
"company": company,
"shift": shift,
"late_entry": late_entry,
"early_exit": early_exit,
Expand Down
15 changes: 13 additions & 2 deletions hrms/hr/doctype/shift_assignment/shift_assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def get_employee_shift(
shift_details = get_shift_for_timestamp(employee, for_timestamp)

# if shift assignment is not found, consider default shift
default_shift = frappe.db.get_value("Employee", employee, "default_shift")
default_shift = frappe.db.get_value("Employee", employee, "default_shift", cache=True)
if not shift_details and consider_default_shift:
shift_details = get_shift_details(default_shift, for_timestamp)

Expand Down Expand Up @@ -462,7 +462,18 @@ def get_shift_details(shift_type_name: str, for_timestamp: datetime = None) -> D
if for_timestamp is None:
for_timestamp = now_datetime()

shift_type = frappe.get_doc("Shift Type", shift_type_name)
shift_type = frappe.get_cached_value(
"Shift Type",
shift_type_name,
[
"name",
"start_time",
"end_time",
"begin_check_in_before_shift_start_time",
"allow_check_out_after_shift_end_time",
],
as_dict=1,
)
shift_actual_start = shift_type.start_time - timedelta(
minutes=shift_type.begin_check_in_before_shift_start_time
)
Expand Down
66 changes: 39 additions & 27 deletions hrms/hr/doctype/shift_type/shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,7 @@ def process_auto_attendance(self):
):
return

filters = {
"skip_auto_attendance": 0,
"attendance": ("is", "not set"),
"time": (">=", self.process_attendance_after),
"shift_actual_end": ("<", self.last_sync_of_checkin),
"shift": self.name,
}
logs = frappe.db.get_list(
"Employee Checkin", fields="*", filters=filters, order_by="employee,time"
)
logs = self.get_employee_checkins()

for key, group in itertools.groupby(logs, key=lambda x: (x["employee"], x["shift_start"])):
single_shift_logs = list(group)
Expand Down Expand Up @@ -73,6 +64,31 @@ def process_auto_attendance(self):
for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee)

def get_employee_checkins(self) -> list[dict]:
return frappe.get_all(
"Employee Checkin",
fields=[
"name",
"employee",
"log_type",
"time",
"shift",
"shift_start",
"shift_end",
"shift_actual_start",
"shift_actual_end",
"device_id",
],
filters={
"skip_auto_attendance": 0,
"attendance": ("is", "not set"),
"time": (">=", self.process_attendance_after),
"shift_actual_end": ("<", self.last_sync_of_checkin),
"shift": self.name,
},
order_by="employee,time",
)

def get_attendance(self, logs):
"""Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time
for a set of logs belonging to a single shift.
Expand Down Expand Up @@ -151,7 +167,7 @@ def get_start_and_end_dates(self, employee):
return: start date = max of `process_attendance_after` and DOJ
return: end date = min of shift before `last_sync_of_checkin` and Relieving Date
"""
date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
date_of_joining, relieving_date, employee_creation = frappe.get_cached_value(
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
)

Expand Down Expand Up @@ -181,33 +197,29 @@ def get_start_and_end_dates(self, employee):
return start_date, end_date

def get_assigned_employee(self, from_date=None, consider_default_shift=False):
filters = {"shift_type": self.name, "docstatus": "1"}
filters = {"shift_type": self.name, "docstatus": "1", "status": "Active"}
if from_date:
filters["start_date"] = (">", from_date)
filters["start_date"] = (">=", from_date)

assigned_employees = frappe.get_all("Shift Assignment", filters=filters, pluck="employee")

if consider_default_shift:
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
default_shift_employees = self.get_employees_with_default_shift(from_date)
default_shift_employees = self.get_employees_with_default_shift(filters)

return list(set(assigned_employees + default_shift_employees))
return assigned_employees

def get_employees_with_default_shift(self, from_date=None):
def get_employees_with_default_shift(self, filters: dict) -> list:
default_shift_employees = frappe.get_all(
"Employee", filters={"default_shift": self.name, "status": ("!=", "Inactive")}, pluck="name"
"Employee", filters={"default_shift": self.name, "status": "Active"}, pluck="name"
)

# exclude employees from default shift list if any other valid shift assignment exists
filters = {
"docstatus": "1",
"status": "Active",
"shift_type": ["!=", self.name],
}
if not default_shift_employees:
return []

if from_date:
filters["start_date"] = (">", from_date)
# exclude employees from default shift list if any other valid shift assignment exists
del filters["shift_type"]
filters["employee"] = ("in", default_shift_employees)

active_shift_assignments = frappe.get_all(
"Shift Assignment",
Expand Down Expand Up @@ -235,7 +247,7 @@ def should_mark_attendance(self, employee: str, attendance_date: str) -> bool:


def process_auto_attendance_for_all_shifts():
shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
shift_list = frappe.get_all("Shift Type", filters={"enable_auto_attendance": "1"}, pluck="name")
for shift in shift_list:
doc = frappe.get_doc("Shift Type", shift[0])
doc = frappe.get_cached_doc("Shift Type", shift)
doc.process_auto_attendance()
10 changes: 7 additions & 3 deletions hrms/hr/doctype/shift_type/test_shift_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def test_skip_marking_absent_for_a_fallback_default_shift(self):

default_shift = setup_shift_type()
employee = make_employee(
"test_employee_checkin@example.com", company="_Test Company", shift_type=default_shift.name
"test_employee_checkin@example.com", company="_Test Company", default_shift=default_shift.name
)

assigned_shift = setup_shift_type(shift_type="Test Absent with no Attendance")
Expand All @@ -355,11 +355,15 @@ def test_skip_marking_absent_for_a_fallback_default_shift(self):
log_out = make_checkin(employee, timestamp)

default_shift.process_auto_attendance()
attendance = frappe.db.get_value("Attendance", {"employee": employee}, "status")
attendance = frappe.db.get_value(
"Attendance", {"employee": employee, "shift": default_shift.name}, "status"
)
self.assertIsNone(attendance)

assigned_shift.process_auto_attendance()
attendance = frappe.db.get_value("Attendance", {"employee": employee}, "status")
attendance = frappe.db.get_value(
"Attendance", {"employee": employee, "shift": assigned_shift.name}, "status"
)
self.assertEqual(attendance, "Present")

def test_get_start_and_end_dates(self):
Expand Down