Merge pull request #18523 from Vigneshsekar/attendance_grace_period

feat(Auto Attendance): Add grace period
This commit is contained in:
Karthikeyan S 2019-08-18 13:25:48 +05:30 committed by GitHub
commit d5e5e22adb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 61 additions and 80 deletions

View File

@ -4,6 +4,7 @@
"creation": "2013-01-10 16:34:13", "creation": "2013-01-10 16:34:13",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"attendance_details", "attendance_details",
"naming_series", "naming_series",
@ -19,7 +20,9 @@
"department", "department",
"shift", "shift",
"attendance_request", "attendance_request",
"amended_from" "amended_from",
"late_entry",
"early_exit"
], ],
"fields": [ "fields": [
{ {
@ -153,12 +156,24 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Shift", "label": "Shift",
"options": "Shift Type" "options": "Shift Type"
},
{
"default": "0",
"fieldname": "late_entry",
"fieldtype": "Check",
"label": "Late Entry"
},
{
"default": "0",
"fieldname": "early_exit",
"fieldtype": "Check",
"label": "Early Exit"
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-06-05 19:37:30.410071", "modified": "2019-07-29 20:35:40.845422",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Attendance", "name": "Attendance",

View File

@ -14,8 +14,6 @@
"device_id", "device_id",
"skip_auto_attendance", "skip_auto_attendance",
"attendance", "attendance",
"entry_grace_period_consequence",
"exit_grace_period_consequence",
"shift_start", "shift_start",
"shift_end", "shift_end",
"shift_actual_start", "shift_actual_start",
@ -80,20 +78,6 @@
"options": "Attendance", "options": "Attendance",
"read_only": 1 "read_only": 1
}, },
{
"default": "0",
"fieldname": "entry_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Entry Grace Period Consequence"
},
{
"default": "0",
"fieldname": "exit_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Exit Grace Period Consequence"
},
{ {
"fieldname": "shift_start", "fieldname": "shift_start",
"fieldtype": "Datetime", "fieldtype": "Datetime",
@ -119,7 +103,7 @@
"label": "Shift Actual End" "label": "Shift Actual End"
} }
], ],
"modified": "2019-06-10 15:33:22.731697", "modified": "2019-07-23 23:47:33.975263",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Checkin", "name": "Employee Checkin",

View File

@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
return doc return doc
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, shift=None): def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin. """Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@ -98,7 +98,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
'status': attendance_status, 'status': attendance_status,
'working_hours': working_hours, 'working_hours': working_hours,
'company': employee_doc.company, 'company': employee_doc.company,
'shift': shift 'shift': shift,
'late_entry': late_entry,
'early_exit': early_exit
} }
attendance = frappe.get_doc(doc_dict).insert() attendance = frappe.get_doc(doc_dict).insert()
attendance.submit() attendance.submit()
@ -124,11 +126,16 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
""" """
total_hours = 0 total_hours = 0
in_time = out_time = None
if check_in_out_type == 'Alternating entries as IN and OUT during the same shift': if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
in_time = logs[0].time
if len(logs) >= 2:
out_time = logs[-1].time
if working_hours_calc_type == 'First Check-in and Last Check-out': if working_hours_calc_type == 'First Check-in and Last Check-out':
# assumption in this case: First log always taken as IN, Last log always taken as OUT # assumption in this case: First log always taken as IN, Last log always taken as OUT
total_hours = time_diff_in_hours(logs[0].time, logs[-1].time) total_hours = time_diff_in_hours(in_time, logs[-1].time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out': elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
logs = logs[:]
while len(logs) >= 2: while len(logs) >= 2:
total_hours += time_diff_in_hours(logs[0].time, logs[1].time) total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
del logs[:2] del logs[:2]
@ -138,11 +145,15 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
first_in_log = logs[find_index_in_dict(logs, 'log_type', 'IN')] first_in_log = logs[find_index_in_dict(logs, 'log_type', 'IN')]
last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')] last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')]
if first_in_log and last_out_log: if first_in_log and last_out_log:
total_hours = time_diff_in_hours(first_in_log.time, last_out_log.time) in_time, out_time = first_in_log.time, last_out_log.time
total_hours = time_diff_in_hours(in_time, out_time)
elif working_hours_calc_type == 'Every Valid Check-in and Check-out': elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
in_log = out_log = None in_log = out_log = None
for log in logs: for log in logs:
if in_log and out_log: if in_log and out_log:
if not in_time:
in_time = in_log.time
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time) total_hours += time_diff_in_hours(in_log.time, out_log.time)
in_log = out_log = None in_log = out_log = None
if not in_log: if not in_log:
@ -150,8 +161,9 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
elif not out_log: elif not out_log:
out_log = log if log.log_type == 'OUT' else None out_log = log if log.log_type == 'OUT' else None
if in_log and out_log: if in_log and out_log:
out_time = out_log.time
total_hours += time_diff_in_hours(in_log.time, out_log.time) total_hours += time_diff_in_hours(in_log.time, out_log.time)
return total_hours return total_hours, in_time, out_time
def time_diff_in_hours(start, end): def time_diff_in_hours(start, end):
return round((end-start).total_seconds() / 3600, 1) return round((end-start).total_seconds() / 3600, 1)

View File

@ -70,16 +70,16 @@ class TestEmployeeCheckin(unittest.TestCase):
logs_type_2 = [frappe._dict(x) for x in logs_type_2] logs_type_2 = [frappe._dict(x) for x in logs_type_2]
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0]) working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0])
self.assertEqual(working_hours, 6.5) self.assertEqual(working_hours, (6.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1]) working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1])
self.assertEqual(working_hours, 4.5) self.assertEqual(working_hours, (4.5, logs_type_1[0].time, logs_type_1[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0]) working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0])
self.assertEqual(working_hours, 5) self.assertEqual(working_hours, (5, logs_type_2[1].time, logs_type_2[-1].time))
working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1]) working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1])
self.assertEqual(working_hours, 4.5) self.assertEqual(working_hours, (4.5, logs_type_2[1].time, logs_type_2[-1].time))
def make_n_checkins(employee, n, hours_to_reverse=1): def make_n_checkins(employee, n, hours_to_reverse=1):
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))] logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]

View File

@ -23,14 +23,9 @@
"grace_period_settings_auto_attendance_section", "grace_period_settings_auto_attendance_section",
"enable_entry_grace_period", "enable_entry_grace_period",
"late_entry_grace_period", "late_entry_grace_period",
"consequence_after",
"consequence",
"column_break_18", "column_break_18",
"enable_exit_grace_period", "enable_exit_grace_period",
"enable_different_consequence_for_early_exit", "early_exit_grace_period"
"early_exit_grace_period",
"early_exit_consequence_after",
"early_exit_consequence"
], ],
"fields": [ "fields": [
{ {
@ -107,21 +102,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Late Entry Grace Period" "label": "Late Entry Grace Period"
}, },
{
"depends_on": "enable_entry_grace_period",
"description": "The number of occurrence after which the consequence is executed.",
"fieldname": "consequence_after",
"fieldtype": "Int",
"label": "Consequence after"
},
{
"default": "Half Day",
"depends_on": "enable_entry_grace_period",
"fieldname": "consequence",
"fieldtype": "Select",
"label": "Consequence",
"options": "Half Day\nAbsent"
},
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -132,13 +112,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Exit Grace Period" "label": "Enable Exit Grace Period"
}, },
{
"default": "0",
"depends_on": "enable_exit_grace_period",
"fieldname": "enable_different_consequence_for_early_exit",
"fieldtype": "Check",
"label": "Enable Different Consequence for Early Exit"
},
{ {
"depends_on": "eval:doc.enable_exit_grace_period", "depends_on": "eval:doc.enable_exit_grace_period",
"description": "The time before the shift end time when check-out is considered as early (in minutes).", "description": "The time before the shift end time when check-out is considered as early (in minutes).",
@ -146,21 +119,6 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Early Exit Grace Period" "label": "Early Exit Grace Period"
}, },
{
"depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
"description": "The number of occurrence after which the consequence is executed.",
"fieldname": "early_exit_consequence_after",
"fieldtype": "Int",
"label": "Early Exit Consequence after"
},
{
"default": "Half Day",
"depends_on": "eval:doc.enable_exit_grace_period && doc.enable_different_consequence_for_early_exit",
"fieldname": "early_exit_consequence",
"fieldtype": "Select",
"label": "Early Exit Consequence",
"options": "Half Day\nAbsent"
},
{ {
"default": "60", "default": "60",
"description": "Time after the end of shift during which check-out is considered for attendance.", "description": "Time after the end of shift during which check-out is considered for attendance.",
@ -178,7 +136,6 @@
"depends_on": "enable_auto_attendance", "depends_on": "enable_auto_attendance",
"fieldname": "grace_period_settings_auto_attendance_section", "fieldname": "grace_period_settings_auto_attendance_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 1,
"label": "Grace Period Settings For Auto Attendance" "label": "Grace Period Settings For Auto Attendance"
}, },
{ {
@ -201,7 +158,7 @@
"label": "Last Sync of Checkin" "label": "Last Sync of Checkin"
} }
], ],
"modified": "2019-06-10 06:02:44.272036", "modified": "2019-07-30 01:05:24.660666",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Shift Type", "name": "Shift Type",

View File

@ -28,8 +28,8 @@ class ShiftType(Document):
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group) single_shift_logs = list(group)
attendance_status, working_hours = self.get_attendance(single_shift_logs) attendance_status, working_hours, late_entry, early_exit = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name) mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True): for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee) self.mark_absent_for_dates_with_no_attendance(employee)
@ -39,12 +39,19 @@ class ShiftType(Document):
1. These logs belongs to an single shift, single employee and is not in a holiday date. 1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order 2. Logs are in chronological order
""" """
total_working_hours = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on) late_entry = early_exit = False
total_working_hours, in_time, out_time = calculate_working_hours(logs, self.determine_check_in_and_check_out, self.working_hours_calculation_based_on)
if cint(self.enable_entry_grace_period) and in_time and in_time > logs[0].shift_start + timedelta(minutes=cint(self.late_entry_grace_period)):
late_entry = True
if cint(self.enable_exit_grace_period) and out_time and out_time < logs[0].shift_end - timedelta(minutes=cint(self.early_exit_grace_period)):
early_exit = True
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
return 'Absent', total_working_hours return 'Absent', total_working_hours, late_entry, early_exit
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
return 'Half Day', total_working_hours return 'Half Day', total_working_hours, late_entry, early_exit
return 'Present', total_working_hours return 'Present', total_working_hours, late_entry, early_exit
def mark_absent_for_dates_with_no_attendance(self, employee): def mark_absent_for_dates_with_no_attendance(self, employee):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked. """Marks Absents for the given employee on working days in this shift which have no attendance marked.

View File

@ -25,6 +25,7 @@ def execute(filters=None):
leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
leave_list = [d[0] for d in leave_types] leave_list = [d[0] for d in leave_types]
columns.extend(leave_list) columns.extend(leave_list)
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
for emp in sorted(att_map): for emp in sorted(att_map):
emp_det = emp_map.get(emp) emp_det = emp_map.get(emp)
@ -65,6 +66,10 @@ def execute(filters=None):
leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\
where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1)
time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \
late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \
early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters)
leaves = {} leaves = {}
for d in leave_details: for d in leave_details:
@ -80,7 +85,8 @@ def execute(filters=None):
row.append(leaves[d]) row.append(leaves[d])
else: else:
row.append("0.0") row.append("0.0")
row.extend([time_default_counts[0][0],time_default_counts[0][1]])
data.append(row) data.append(row)
return columns, data return columns, data