Merge pull request #18523 from Vigneshsekar/attendance_grace_period
feat(Auto Attendance): Add grace period
This commit is contained in:
commit
d5e5e22adb
@ -4,6 +4,7 @@
|
||||
"creation": "2013-01-10 16:34:13",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"attendance_details",
|
||||
"naming_series",
|
||||
@ -19,7 +20,9 @@
|
||||
"department",
|
||||
"shift",
|
||||
"attendance_request",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"late_entry",
|
||||
"early_exit"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -153,12 +156,24 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Shift",
|
||||
"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",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-06-05 19:37:30.410071",
|
||||
"modified": "2019-07-29 20:35:40.845422",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Attendance",
|
||||
|
@ -14,8 +14,6 @@
|
||||
"device_id",
|
||||
"skip_auto_attendance",
|
||||
"attendance",
|
||||
"entry_grace_period_consequence",
|
||||
"exit_grace_period_consequence",
|
||||
"shift_start",
|
||||
"shift_end",
|
||||
"shift_actual_start",
|
||||
@ -80,20 +78,6 @@
|
||||
"options": "Attendance",
|
||||
"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",
|
||||
"fieldtype": "Datetime",
|
||||
@ -119,7 +103,7 @@
|
||||
"label": "Shift Actual End"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-10 15:33:22.731697",
|
||||
"modified": "2019-07-23 23:47:33.975263",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Checkin",
|
||||
|
@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
|
||||
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.
|
||||
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,
|
||||
'working_hours': working_hours,
|
||||
'company': employee_doc.company,
|
||||
'shift': shift
|
||||
'shift': shift,
|
||||
'late_entry': late_entry,
|
||||
'early_exit': early_exit
|
||||
}
|
||||
attendance = frappe.get_doc(doc_dict).insert()
|
||||
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'
|
||||
"""
|
||||
total_hours = 0
|
||||
in_time = out_time = None
|
||||
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':
|
||||
# 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':
|
||||
logs = logs[:]
|
||||
while len(logs) >= 2:
|
||||
total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
|
||||
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')]
|
||||
last_out_log = logs[len(logs)-1-find_index_in_dict(reversed(logs), 'log_type', 'OUT')]
|
||||
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':
|
||||
in_log = out_log = None
|
||||
for log in logs:
|
||||
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)
|
||||
in_log = out_log = None
|
||||
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:
|
||||
out_log = log if log.log_type == 'OUT' else None
|
||||
if in_log and out_log:
|
||||
out_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):
|
||||
return round((end-start).total_seconds() / 3600, 1)
|
||||
|
@ -70,16 +70,16 @@ class TestEmployeeCheckin(unittest.TestCase):
|
||||
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])
|
||||
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])
|
||||
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])
|
||||
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])
|
||||
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):
|
||||
logs = [make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]
|
||||
|
@ -23,14 +23,9 @@
|
||||
"grace_period_settings_auto_attendance_section",
|
||||
"enable_entry_grace_period",
|
||||
"late_entry_grace_period",
|
||||
"consequence_after",
|
||||
"consequence",
|
||||
"column_break_18",
|
||||
"enable_exit_grace_period",
|
||||
"enable_different_consequence_for_early_exit",
|
||||
"early_exit_grace_period",
|
||||
"early_exit_consequence_after",
|
||||
"early_exit_consequence"
|
||||
"early_exit_grace_period"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -107,21 +102,6 @@
|
||||
"fieldtype": "Int",
|
||||
"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",
|
||||
"fieldtype": "Column Break"
|
||||
@ -132,13 +112,6 @@
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
"description": "The time before the shift end time when check-out is considered as early (in minutes).",
|
||||
@ -146,21 +119,6 @@
|
||||
"fieldtype": "Int",
|
||||
"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",
|
||||
"description": "Time after the end of shift during which check-out is considered for attendance.",
|
||||
@ -178,7 +136,6 @@
|
||||
"depends_on": "enable_auto_attendance",
|
||||
"fieldname": "grace_period_settings_auto_attendance_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Grace Period Settings For Auto Attendance"
|
||||
},
|
||||
{
|
||||
@ -201,7 +158,7 @@
|
||||
"label": "Last Sync of Checkin"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-10 06:02:44.272036",
|
||||
"modified": "2019-07-30 01:05:24.660666",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Shift Type",
|
||||
|
@ -28,8 +28,8 @@ class ShiftType(Document):
|
||||
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'])):
|
||||
single_shift_logs = list(group)
|
||||
attendance_status, working_hours = self.get_attendance(single_shift_logs)
|
||||
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name)
|
||||
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, late_entry, early_exit, self.name)
|
||||
for employee in self.get_assigned_employee(self.process_attendance_after, True):
|
||||
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.
|
||||
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:
|
||||
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:
|
||||
return 'Half Day', total_working_hours
|
||||
return 'Present', total_working_hours
|
||||
return 'Half Day', total_working_hours, late_entry, early_exit
|
||||
return 'Present', total_working_hours, late_entry, early_exit
|
||||
|
||||
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.
|
||||
|
@ -25,6 +25,7 @@ def execute(filters=None):
|
||||
leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True)
|
||||
leave_list = [d[0] for d in leave_types]
|
||||
columns.extend(leave_list)
|
||||
columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"])
|
||||
|
||||
for emp in sorted(att_map):
|
||||
emp_det = emp_map.get(emp)
|
||||
@ -66,6 +67,10 @@ def execute(filters=None):
|
||||
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)
|
||||
|
||||
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 = {}
|
||||
for d in leave_details:
|
||||
if d.status == "Half Day":
|
||||
@ -81,6 +86,7 @@ def execute(filters=None):
|
||||
else:
|
||||
row.append("0.0")
|
||||
|
||||
row.extend([time_default_counts[0][0],time_default_counts[0][1]])
|
||||
data.append(row)
|
||||
return columns, data
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user