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