diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index eb38147a98..bc89b368d3 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 15ec7c0b1b..08fa4afa5c 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", @@ -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", diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index b0e15d96ed..d7d6706140 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -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) diff --git a/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index 424d1a3c1b..9f12ef24e6 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 @@ 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))] diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 86039deebd..61f3d2c279 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": [ { @@ -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", diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index b98f445c0f..8de92b2761 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -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. 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 e9c702944d..1e9c83bf3e 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 @@ 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) @@ -65,6 +66,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: @@ -80,7 +85,8 @@ def execute(filters=None): 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