feat(Auto Attendance): Add grace period
Co-authored-by: Karthikeyan S <skarthikeyan1410@gmail.com>
diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json
index eb38147..bc89b36 100644
--- a/erpnext/hr/doctype/attendance/attendance.json
+++ b/erpnext/hr/doctype/attendance/attendance.json
@@ -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",
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json
index 15ec7c0..08fa4af 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json
@@ -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",
@@ -81,20 +79,6 @@
"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",
"hidden": 1,
@@ -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",
diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
index b0e15d9..d7d6706 100644
--- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py
@@ -72,7 +72,7 @@
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 @@
'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 @@
: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 @@
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 @@
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)
diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
index 424d1a3..9f12ef2 100644
--- a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
+++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py
@@ -70,16 +70,16 @@
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))]
diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json
index 86039de..61f3d2c 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.json
+++ b/erpnext/hr/doctype/shift_type/shift_type.json
@@ -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": [
{
@@ -108,21 +103,6 @@
"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"
},
@@ -133,13 +113,6 @@
"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).",
"fieldname": "early_exit_grace_period",
@@ -147,21 +120,6 @@
"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.",
"fieldname": "allow_check_out_after_shift_end_time",
@@ -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",
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index b98f445..8de92b2 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -28,8 +28,8 @@
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 @@
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.
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
index e9c7029..1e9c83b 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -25,6 +25,7 @@
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)
@@ -65,6 +66,10 @@
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:
@@ -80,7 +85,8 @@
row.append(leaves[d])
else:
row.append("0.0")
-
+
+ row.extend([time_default_counts[0][0],time_default_counts[0][1]])
data.append(row)
return columns, data