feat(HR): Auto Attendance
>Marking attendance based on Employee Attendance Log
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9502006..acb4477 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -241,7 +241,8 @@
"erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details",
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
"erpnext.projects.doctype.project.project.hourly_reminder",
- "erpnext.projects.doctype.project.project.collect_project_status"
+ "erpnext.projects.doctype.project.project.collect_project_status",
+ "erpnext.hr.doctype.hr_settings.hr_settings.make_attendance_from_employee_attendance_log"
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",
diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json
index 7e1d5ed..2a6d005 100644
--- a/erpnext/hr/doctype/attendance/attendance.json
+++ b/erpnext/hr/doctype/attendance/attendance.json
@@ -9,6 +9,7 @@
"naming_series",
"employee",
"employee_name",
+ "working_hours",
"status",
"leave_type",
"leave_application",
@@ -137,6 +138,14 @@
"options": "Attendance",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "working_hours",
+ "fieldname": "working_hours",
+ "fieldtype": "Float",
+ "label": "Working Hours",
+ "precision": "1",
+ "read_only": 1
}
],
"icon": "fa fa-ok",
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 321fca7..9bb8495 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -88,3 +88,17 @@
}
if e not in events:
events.append(e)
+
+def mark_absent(employee, attendance_date):
+ employee_doc = frappe.get_doc('Employee', employee)
+ if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date}):
+ doc_dict = {
+ 'doctype': 'Attendance',
+ 'employee': employee,
+ 'attendance_date': attendance_date,
+ 'status': 'Absent',
+ 'company': employee_doc.company
+ }
+ attendance = frappe.get_doc(doc_dict).insert()
+ attendance.submit()
+ return attendance.name
diff --git a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json
index ccdb83d..260eebd 100644
--- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json
+++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json
@@ -1,6 +1,6 @@
{
"allow_import": 1,
- "autoname": "EMP-ATT-LOG-.MM.-.YYYY.-.######",
+ "autoname": "ATT-LOG-.MM.-.YYYY.-.######",
"creation": "2019-04-25 10:17:11.225671",
"doctype": "DocType",
"engine": "InnoDB",
@@ -11,7 +11,8 @@
"column_break_4",
"time",
"device_id",
- "attendance_marked",
+ "skip_auto_attendance",
+ "attendance",
"entry_grace_period_consequence",
"exit_grace_period_consequence"
],
@@ -49,13 +50,6 @@
"label": "Location / Device ID"
},
{
- "fieldname": "attendance_marked",
- "fieldtype": "Link",
- "label": "Attendance Marked",
- "options": "Attendance",
- "read_only": 1
- },
- {
"fieldname": "entry_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
@@ -72,9 +66,22 @@
"fieldtype": "Select",
"label": "Log Type",
"options": "\nIN\nOUT"
+ },
+ {
+ "fieldname": "skip_auto_attendance",
+ "fieldtype": "Check",
+ "label": "Skip Auto Attendance",
+ "read_only": 1
+ },
+ {
+ "fieldname": "attendance",
+ "fieldtype": "Link",
+ "label": "Attendance Marked",
+ "options": "Attendance",
+ "read_only": 1
}
],
- "modified": "2019-05-08 14:10:22.468252",
+ "modified": "2019-05-24 13:40:01.287808",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Attendance Log",
diff --git a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py
index 956d9c3..3932f82 100644
--- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py
+++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py
@@ -11,7 +11,7 @@
class EmployeeAttendanceLog(Document):
def validate(self):
if frappe.db.exists('Employee Attendance Log', {'employee': self.employee, 'time': self.time}):
- frappe.throw('This log already exists for this employee.')
+ frappe.throw(_('This log already exists for this employee.'))
@frappe.whitelist()
@@ -20,18 +20,18 @@
:param biometric_rf_id: The Biometric/RF tag ID as set up in Employee DocType.
:param timestamp: The timestamp of the Log. Currently expected in the following format as string: '2019-05-08 10:48:08.000000'
- :param device_id(optional): Location / Device ID. A short string is expected.
- :param log_type(optional): Direction of the Punch if available (IN/OUT).
+ :param device_id: (optional)Location / Device ID. A short string is expected.
+ :param log_type: (optional)Direction of the Punch if available (IN/OUT).
"""
if not biometric_rf_id or not timestamp:
frappe.throw(_("'biometric_rf_id' and 'timestamp' are required."))
- employee = frappe.db.get_values("Employee", {"biometric_rf_id": biometric_rf_id},["name","employee_name","biometric_rf_id"],as_dict=True)
- if len(employee) != 0:
+ employee = frappe.db.get_values("Employee", {"biometric_rf_id": biometric_rf_id}, ["name", "employee_name", "biometric_rf_id"], as_dict=True)
+ if employee:
employee = employee[0]
else:
- frappe.throw(_("No Employee found for the given 'biometric_rf_id'."))
+ frappe.throw(_("No Employee found for the given 'biometric_rf_id':{}.").format(biometric_rf_id))
doc = frappe.new_doc("Employee Attendance Log")
doc.employee = employee.name
@@ -43,3 +43,45 @@
frappe.db.commit()
return doc
+
+
+def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, company=None):
+ """Creates an attendance and links the attendance to the Employee Attendance Log.
+ Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
+
+ :param logs: The List of 'Employee Attendance Log'.
+ :param attendance_status: Attendance status to be marked. One of: (Present, Absent, Half Day, Skip). Note: 'On Leave' is not supported by this function.
+ :param attendance_date: Date of the attendance to be created.
+ :param working_hours: (optional)Number of working hours for the given date.
+ """
+ log_names = [x.name for x in logs]
+ employee = logs[0].employee
+ if attendance_status == 'Skip':
+ frappe.db.sql("""update `tabEmployee Attendance Log`
+ set skip_auto_attendance = %s
+ where name in %s""", ('1', log_names))
+ return None
+ elif attendance_status in ('Present', 'Absent', 'Half Day'):
+ employee_doc = frappe.get_doc('Employee', employee)
+ if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date}):
+ doc_dict = {
+ 'doctype': 'Attendance',
+ 'employee': employee,
+ 'attendance_date': attendance_date,
+ 'status': attendance_status,
+ 'working_hours': working_hours,
+ 'company': employee_doc.company
+ }
+ attendance = frappe.get_doc(doc_dict).insert()
+ attendance.submit()
+ frappe.db.sql("""update `tabEmployee Attendance Log`
+ set attendance = %s
+ where name in %s""", (attendance.name, log_names))
+ return attendance
+ else:
+ frappe.db.sql("""update `tabEmployee Attendance Log`
+ set skip_auto_attendance = %s
+ where name in %s""", ('1', log_names))
+ return None
+ else:
+ frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status))
diff --git a/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py b/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py
index 45c1353..fdc63d6 100644
--- a/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py
+++ b/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py
@@ -3,19 +3,61 @@
# See license.txt
from __future__ import unicode_literals
-from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import add_log_based_on_biometric_rf_id
-
import frappe
+from frappe.utils import now_datetime, nowdate, to_timedelta
import unittest
+from datetime import timedelta
+
+from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import add_log_based_on_biometric_rf_id, mark_attendance_and_link_log
+from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeAttendanceLog(unittest.TestCase):
def test_add_log_based_on_biometric_rf_id(self):
- employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
- employee.biometric_rf_id = '12349'
+ employee = make_employee("test_add_log_based_on_biometric_rf_id@example.com")
+ employee = frappe.get_doc("Employee", employee)
+ employee.biometric_rf_id = '3344'
employee.save()
- employee_attendance_log = add_log_based_on_biometric_rf_id('12349', '2019-05-08 10:48:08.000000', 'mumbai_first_floor', 'IN')
- self.assertTrue(employee_attendance_log.employee == employee.name)
- self.assertTrue(employee_attendance_log.time == '2019-05-08 10:48:08.000000')
- self.assertTrue(employee_attendance_log.device_id == 'mumbai_first_floor')
- self.assertTrue(employee_attendance_log.log_type == 'IN')
+ time_now = now_datetime().__str__()[:-7]
+ employee_attendance_log = add_log_based_on_biometric_rf_id('3344', time_now, 'mumbai_first_floor', 'IN')
+ self.assertEqual(employee_attendance_log.employee, employee.name)
+ self.assertEqual(employee_attendance_log.time, time_now)
+ self.assertEqual(employee_attendance_log.device_id, 'mumbai_first_floor')
+ self.assertEqual(employee_attendance_log.log_type, 'IN')
+
+ def test_mark_attendance_and_link_log(self):
+ employee = make_employee("test_mark_attendance_and_link_log@example.com")
+ logs = make_n_attendance_logs(employee, 3)
+ mark_attendance_and_link_log(logs, 'Skip', nowdate())
+ log_names = [log.name for log in logs]
+ logs_count = frappe.db.count('Employee Attendance Log', {'name':['in', log_names], 'skip_auto_attendance':1})
+ self.assertEqual(logs_count, 3)
+
+ logs = make_n_attendance_logs(employee, 4, 2)
+ now_date = nowdate()
+ frappe.db.delete('Attendance', {'employee':employee})
+ attendance = mark_attendance_and_link_log(logs, 'Present', now_date, 8.2)
+ log_names = [log.name for log in logs]
+ logs_count = frappe.db.count('Employee Attendance Log', {'name':['in', log_names], 'attendance':attendance.name})
+ self.assertEqual(logs_count, 4)
+ attendance_count = frappe.db.count('Attendance', {'status':'Present', 'working_hours':8.2,
+ 'employee':employee, 'attendance_date':now_date})
+ self.assertEqual(attendance_count, 1)
+
+
+def make_n_attendance_logs(employee, n, hours_to_reverse=1):
+ logs = [make_attendance_log(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n+1))]
+ for i in range(n-1):
+ logs.append(make_attendance_log(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i)))
+ return logs
+
+
+def make_attendance_log(employee, time=now_datetime()):
+ log = frappe.get_doc({
+ "doctype": "Employee Attendance Log",
+ "employee" : employee,
+ "time" : time,
+ "device_id" : "device1",
+ "log_type" : "IN"
+ }).insert()
+ return log
\ No newline at end of file
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py
index e475e6e..453320f 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.py
@@ -84,3 +84,26 @@
fields=['name', '`tabHoliday`.holiday_date', '`tabHoliday`.description', '`tabHoliday List`.color'],
filters = filters,
update={"allDay": 1})
+
+def get_holiday_list(employee):
+ employee_holiday = frappe.db.get_all('Employee', fields=['name', 'holiday_list', 'company'], filters={'name':employee})
+ if not employee_holiday:
+ frappe.throw(_("Employee not found."))
+ if employee_holiday[0].holiday_list:
+ return employee_holiday[0].holiday_list
+ else:
+ company_holiday = frappe.db.get_all('Company', fields=['name', 'default_holiday_list'], filters={'name':employee_holiday[0].company})
+ if company_holiday[0].default_holiday_list:
+ return company_holiday[0].default_holiday_list
+ return None
+
+def is_holiday(holiday_list, for_date):
+ """Returns true if the given date is a holiday in the given holiday list
+ """
+ holiday = frappe.get_value('Holiday', {
+ 'parent': holiday_list,
+ 'parentfield': 'holidays',
+ 'parenttype': 'Holiday List',
+ 'holiday_date': for_date
+ }, 'name')
+ return bool(holiday)
\ No newline at end of file
diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
index 653ef2f..33a24d1 100644
--- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py
@@ -4,6 +4,41 @@
import frappe
import unittest
+from frappe.utils import getdate
+from datetime import timedelta
+from erpnext.hr.doctype.employee.test_employee import make_employee
+
class TestHolidayList(unittest.TestCase):
- pass
\ No newline at end of file
+ def test_get_holiday_list(self):
+ holiday_list = make_holiday_list("test_get_holiday_list")
+ employee = make_employee("test_get_holiday_list@example.com")
+ employee = frappe.get_doc("Employee", employee)
+ employee.holiday_list = None
+ employee.save()
+ company = frappe.get_doc("Company", employee.company)
+ company_default_holiday_list = company.default_holiday_list
+
+ from erpnext.hr.doctype.holiday_list.holiday_list import get_holiday_list
+ holiday_list_name = get_holiday_list(employee.name)
+ self.assertEqual(holiday_list_name, company_default_holiday_list)
+
+ employee.holiday_list = holiday_list.name
+ employee.save()
+ holiday_list_name = get_holiday_list(employee.name)
+ self.assertEqual(holiday_list_name, holiday_list.name)
+
+
+def make_holiday_list(name, from_date=getdate()-timedelta(days=10), to_date=getdate(), holiday_dates=None):
+ if not frappe.db.get_value("Holiday List", name):
+ doc = frappe.get_doc({
+ "doctype": "Holiday List",
+ "holiday_list_name": name,
+ "from_date" : from_date,
+ "to_date" : to_date
+ }).insert()
+ doc.holidays = holiday_dates
+ doc.save()
+ else:
+ doc = frappe.get_doc("Holiday List", name)
+ return doc
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 5502ce8..ac1d23e 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -1,665 +1,204 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2013-08-02 13:45:23",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Other",
- "editable_grid": 1,
+ "creation": "2013-08-02 13:45:23",
+ "doctype": "DocType",
+ "document_type": "Other",
+ "editable_grid": 1,
+ "field_order": [
+ "employee_settings",
+ "retirement_age",
+ "emp_created_by",
+ "leave_approval_notification_template",
+ "leave_status_notification_template",
+ "default_shift",
+ "column_break_4",
+ "stop_birthday_reminders",
+ "maintain_bill_work_hours_same",
+ "leave_approver_mandatory_in_leave_application",
+ "expense_approver_mandatory_in_expense_claim",
+ "payroll_settings",
+ "include_holidays_in_total_working_days",
+ "email_salary_slip_to_employee",
+ "encrypt_salary_slips_in_emails",
+ "password_policy",
+ "max_working_hours_against_timesheet",
+ "leave_settings",
+ "show_leaves_of_all_department_members_in_calendar",
+ "auto_attendance_section",
+ "disable_auto_attendance",
+ "attendance_for_employee_without_shift",
+ "column_break_23",
+ "process_attendance_after",
+ "last_sync_of_attendance_log"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "employee_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Employee Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "employee_settings",
+ "fieldtype": "Section Break",
+ "label": "Employee Settings"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "description": "Enter retirement age in years",
- "fetch_if_empty": 0,
- "fieldname": "retirement_age",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Retirement Age",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "description": "Enter retirement age in years",
+ "fieldname": "retirement_age",
+ "fieldtype": "Data",
+ "label": "Retirement Age"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Naming Series",
- "description": "Employee record is created using selected field. ",
- "fetch_if_empty": 0,
- "fieldname": "emp_created_by",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Employee Records to be created by",
- "length": 0,
- "no_copy": 0,
- "options": "Naming Series\nEmployee Number\nFull Name",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "Naming Series",
+ "description": "Employee record is created using selected field. ",
+ "fieldname": "emp_created_by",
+ "fieldtype": "Select",
+ "label": "Employee Records to be created by",
+ "options": "Naming Series\nEmployee Number\nFull Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fetch_if_empty": 0,
- "fieldname": "leave_approval_notification_template",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Leave Approval Notification Template",
- "length": 0,
- "no_copy": 0,
- "options": "Email Template",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "leave_approval_notification_template",
+ "fieldtype": "Link",
+ "label": "Leave Approval Notification Template",
+ "options": "Email Template"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "leave_status_notification_template",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Leave Status Notification Template",
- "length": 0,
- "no_copy": 0,
- "options": "Email Template",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "leave_status_notification_template",
+ "fieldtype": "Link",
+ "label": "Leave Status Notification Template",
+ "options": "Email Template"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Don't send Employee Birthday Reminders",
- "fetch_if_empty": 0,
- "fieldname": "stop_birthday_reminders",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Stop Birthday Reminders",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "description": "Don't send Employee Birthday Reminders",
+ "fieldname": "stop_birthday_reminders",
+ "fieldtype": "Check",
+ "label": "Stop Birthday Reminders"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "maintain_bill_work_hours_same",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Maintain Billing Hours and Working Hours Same on Timesheet",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "maintain_bill_work_hours_same",
+ "fieldtype": "Check",
+ "label": "Maintain Billing Hours and Working Hours Same on Timesheet"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fetch_if_empty": 0,
- "fieldname": "leave_approver_mandatory_in_leave_application",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Leave Approver Mandatory In Leave Application",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "1",
+ "fieldname": "leave_approver_mandatory_in_leave_application",
+ "fieldtype": "Check",
+ "label": "Leave Approver Mandatory In Leave Application"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "fetch_if_empty": 0,
- "fieldname": "expense_approver_mandatory_in_expense_claim",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Expense Approver Mandatory In Expense Claim",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "1",
+ "fieldname": "expense_approver_mandatory_in_expense_claim",
+ "fieldtype": "Check",
+ "label": "Expense Approver Mandatory In Expense Claim"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
- "fieldname": "payroll_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payroll Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "payroll_settings",
+ "fieldtype": "Section Break",
+ "label": "Payroll Settings"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day",
- "fetch_if_empty": 0,
- "fieldname": "include_holidays_in_total_working_days",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Include holidays in Total no. of Working Days",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day",
+ "fieldname": "include_holidays_in_total_working_days",
+ "fieldtype": "Check",
+ "label": "Include holidays in Total no. of Working Days"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "1",
- "description": "Emails salary slip to employee based on preferred email selected in Employee",
- "fetch_if_empty": 0,
- "fieldname": "email_salary_slip_to_employee",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email Salary Slip to Employee",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "1",
+ "description": "Emails salary slip to employee based on preferred email selected in Employee",
+ "fieldname": "email_salary_slip_to_employee",
+ "fieldtype": "Check",
+ "label": "Email Salary Slip to Employee"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval: doc.email_salary_slip_to_employee == 1;",
- "description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",
- "fetch_if_empty": 0,
- "fieldname": "encrypt_salary_slips_in_emails",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Encrypt Salary Slips in Emails",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval: doc.email_salary_slip_to_employee == 1;",
+ "description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",
+ "fieldname": "encrypt_salary_slips_in_emails",
+ "fieldtype": "Check",
+ "label": "Encrypt Salary Slips in Emails"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
- "description": "<b>Example:</b> SAL-{first_name}-{date_of_birth.year} <br>This will generate a password like SAL-Jane-1972",
- "fetch_if_empty": 0,
- "fieldname": "password_policy",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Password Policy",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
+ "description": "<b>Example:</b> SAL-{first_name}-{date_of_birth.year} <br>This will generate a password like SAL-Jane-1972",
+ "fieldname": "password_policy",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Password Policy"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "max_working_hours_against_timesheet",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Max working hours against Timesheet",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "max_working_hours_against_timesheet",
+ "fieldtype": "Float",
+ "label": "Max working hours against Timesheet"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "leave_settings",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Leave Settings",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "leave_settings",
+ "fieldtype": "Section Break",
+ "label": "Leave Settings"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "show_leaves_of_all_department_members_in_calendar",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Show Leaves Of All Department Members In Calendar",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "show_leaves_of_all_department_members_in_calendar",
+ "fieldtype": "Check",
+ "label": "Show Leaves Of All Department Members In Calendar"
+ },
+ {
+ "description": "Attendance automatically marked based Employee Attendance Log",
+ "fieldname": "auto_attendance_section",
+ "fieldtype": "Section Break",
+ "label": "Auto Attendance"
+ },
+ {
+ "fieldname": "default_shift",
+ "fieldtype": "Link",
+ "label": "Default Shift",
+ "options": "Shift Type"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance",
+ "fieldname": "attendance_for_employee_without_shift",
+ "fieldtype": "Select",
+ "label": "Attendance for Employee Without Shift",
+ "options": "Skip\nBased on Default Shift\nAt least one Employee Attendance Log per day as present"
+ },
+ {
+ "fieldname": "disable_auto_attendance",
+ "fieldtype": "Check",
+ "label": "Disable Auto Attendance"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance",
+ "fieldname": "column_break_23",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance",
+ "description": "Attendance will be marked automatically only after this date.",
+ "fieldname": "process_attendance_after",
+ "fieldtype": "Date",
+ "label": "Process Attendance After"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance",
+ "description": "Last Known Successful Sync of Employee Attendance Log. Reset this only if you are sure that all Logs are synced from all the locations. Please don't modify this if you are unsure.",
+ "fieldname": "last_sync_of_attendance_log",
+ "fieldtype": "Datetime",
+ "label": "Last Sync of Attendance Log"
}
- ],
- "has_web_view": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-cog",
- "idx": 1,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-04-25 15:08:12.983571",
- "modified_by": "shivam@example.com",
- "module": "HR",
- "name": "HR Settings",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-cog",
+ "idx": 1,
+ "issingle": 1,
+ "modified": "2019-05-22 12:50:40.189766",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "HR Settings",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 0,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index 78095b3..afd90f7 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.py
@@ -5,9 +5,15 @@
from __future__ import unicode_literals
import frappe
+from frappe.utils import get_datetime, getdate
+from datetime import timedelta
from frappe import _
from frappe.model.document import Document
+from erpnext.hr.doctype.shift_assignment.shift_assignment import get_employee_shift_timings, get_employee_shift
+from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import mark_attendance_and_link_log
+from erpnext.hr.doctype.holiday_list.holiday_list import get_holiday_list
+from erpnext.hr.doctype.attendance.attendance import mark_absent
class HRSettings(Document):
def validate(self):
@@ -22,4 +28,236 @@
def validate_password_policy(self):
if self.email_salary_slip_to_employee and self.encrypt_salary_slips_in_emails:
if not self.password_policy:
- frappe.throw(_("Password policy for Salary Slips is not set"))
\ No newline at end of file
+ frappe.throw(_("Password policy for Salary Slips is not set"))
+
+
+def make_attendance_from_employee_attendance_log():
+ hr_settings = frappe.db.get_singles_dict("HR Settings")
+ if hr_settings.disable_auto_attendance == '1' or not hr_settings.process_attendance_after:
+ return
+
+ frappe.flags.hr_settings_for_auto_attendance = hr_settings
+ filters = {'skip_auto_attendance':'0', 'attendance_marked':('is', 'not set'), 'time':('>=', hr_settings.process_attendance_after)}
+
+ logs = frappe.db.get_all('Employee Attendance Log', fields="*", filters=filters, order_by="employee,time")
+ single_employee_logs = []
+ for log in logs:
+ if not len(single_employee_logs) or (len(single_employee_logs) and single_employee_logs[0].employee == log.employee):
+ single_employee_logs.append(log)
+ else:
+ process_single_employee_logs(single_employee_logs, hr_settings)
+ single_employee_logs = [log]
+ process_single_employee_logs(single_employee_logs, hr_settings)
+
+def process_single_employee_logs(logs, hr_settings=None):
+ """Takes logs of a single employee in chronological order and tries to mark attendance for that employee.
+ """
+ last_log = logs[-1]
+ if not hr_settings:
+ hr_settings = frappe.db.get_singles_dict("HR Settings")
+ consider_default_shift = bool(hr_settings.attendance_for_employee_without_shift == 'Based on Default Shift')
+ employee_last_sync = get_employee_attendance_log_last_sync(last_log.employee, hr_settings, last_log)
+ while logs:
+ actual_shift_start, actual_shift_end, shift_details = get_actual_start_end_datetime_of_shift(logs[0].employee, logs[0].time, consider_default_shift)
+ if actual_shift_end and actual_shift_end >= employee_last_sync:
+ break # skip processing employee if last_sync timestamp is in the middle of a shift
+ if not actual_shift_start and not actual_shift_end: # when the log does not belong to any 'actual' shift timings
+ if not shift_details: # employee does not have any future shifts assigned
+ if hr_settings.attendance_for_employee_without_shift == 'At least one Employee Attendance Log per day as present':
+ single_day_logs = [logs.pop(0)]
+ while logs and logs[0].time.date() == single_day_logs[0].time.date():
+ single_day_logs.append(logs.pop(0))
+ mark_attendance_and_link_log(single_day_logs, 'Present', single_day_logs[0].time.date())
+ continue
+ else:
+ mark_attendance_and_link_log(logs, 'Skip', None) # skipping attendance for all logs
+ break
+ else:
+ mark_attendance_and_link_log([logs.pop(0)], 'Skip', None) # skipping single log
+ continue
+ single_shift_logs = [logs.pop(0)]
+ while logs and logs[0].time <= actual_shift_end:
+ single_shift_logs.append(logs.pop(0))
+ process_single_employee_shift_logs(single_shift_logs, shift_details)
+ mark_absent_for_days_with_no_attendance(last_log.employee, employee_last_sync, hr_settings)
+
+def mark_absent_for_days_with_no_attendance(employee, employee_last_sync, hr_settings=None):
+ """Marks Absents for the given employee on working days which have no attendance marked.
+ The Absent is marked starting from one shift before the employee_last_sync
+ going back to 'hr_settings.process_attendance_after' or employee creation date.
+ """
+ if not hr_settings:
+ hr_settings = frappe.db.get_singles_dict("HR Settings")
+ consider_default_shift = bool(hr_settings.attendance_for_employee_without_shift == 'Based on Default Shift')
+ employee_date_of_joining = frappe.db.get_value('Employee', employee, 'date_of_joining')
+ if not employee_date_of_joining:
+ employee_date_of_joining = frappe.db.get_value('Employee', employee, 'creation').date()
+ start_date = max(getdate(hr_settings.process_attendance_after), employee_date_of_joining)
+
+ actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, employee_last_sync, consider_default_shift)
+ last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else employee_last_sync
+ prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), consider_default_shift, 'reverse')
+ if prev_shift:
+ end_date = prev_shift.start_datetime.date()
+ elif hr_settings.attendance_for_employee_without_shift == 'At least one Employee Attendance Log per day as present':
+ for date in get_filtered_date_list(employee, "All Dates", start_date, employee_last_sync.date(), True, get_holiday_list(employee)):
+ mark_absent(employee, date)
+ return
+ else:
+ return
+
+ if consider_default_shift:
+ for date in get_filtered_date_list(employee, "All Dates", start_date, end_date):
+ if get_employee_shift(employee, date, consider_default_shift):
+ mark_absent(employee, date)
+ elif hr_settings.attendance_for_employee_without_shift == 'At least one Employee Attendance Log per day as present':
+ for date in get_filtered_date_list(employee, "All Dates", start_date, employee_last_sync.date(), True, get_holiday_list(employee)):
+ mark_absent(employee, date)
+ else:
+ for date in get_filtered_date_list(employee, "Assigned Shifts", start_date, end_date):
+ if get_employee_shift(employee, date, consider_default_shift):
+ mark_absent(employee, date)
+
+
+def get_filtered_date_list(employee, base_dates_set, start_date, end_date, filter_attendance=True, holiday_list=None):
+ """
+ :param base_dates_set: One of: "All Dates", "Assigned Shifts"
+ """
+ if base_dates_set == "All Dates":
+ base_dates_query = """select adddate(%(start_date)s, t2.i*100 + t1.i*10 + t0.i) selected_date from
+ (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
+ (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
+ (select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2"""
+ else:
+ base_dates_query = "select date as selected_date from `tabShift Assignment` where docstatus = '1' and employee = %(employee)s and date >= %(start_date)s"
+
+ condition_query = ''
+ if filter_attendance:
+ condition_query += """and a.selected_date not in (
+ select attendance_date from `tabAttendance`
+ where docstatus = '1' and employee = %(employee)s
+ and attendance_date between %(start_date)s and %(end_date)s)"""
+ if holiday_list:
+ condition_query += """and a.selected_date not in (
+ select holiday_date from `tabHoliday` where parenttype = 'Holiday List' and
+ parentfield = 'holidays' and parent = %(holiday_list)s
+ and holiday_date between %(start_date)s and %(end_date)s)"""
+
+ dates = frappe.db.sql("""select * from
+ ({base_dates_query}) as a
+ where a.selected_date <= %(end_date)s {condition_query}
+ """.format(base_dates_query=base_dates_query,condition_query=condition_query),
+ {"employee":employee, "start_date":start_date, "end_date":end_date, "holiday_list":holiday_list},as_list=True)
+
+ return [getdate(date[0]) for date in dates]
+
+
+def process_single_employee_shift_logs(logs, shift_details):
+ """Mark Attendance for a set of logs belonging to a single shift.
+ Assumtion:
+ 1. These logs belongs to an single shift, single employee and is not in a holiday shift.
+ 2. Logs are in chronological order
+ """
+ check_in_out_type = shift_details.shift_type.determine_check_in_and_check_out
+ working_hours_calc_type = shift_details.shift_type.working_hours_calculation_based_on
+ total_working_hours = calculate_working_hours(logs, check_in_out_type, working_hours_calc_type)
+ if shift_details.working_hours_threshold_for_absent and total_working_hours < shift_details.working_hours_threshold_for_absent:
+ mark_attendance_and_link_log(logs, 'Absent', shift_details.start_datetime.date(), total_working_hours)
+ return
+ if shift_details.working_hours_threshold_for_half_day and total_working_hours < shift_details.working_hours_threshold_for_half_day:
+ mark_attendance_and_link_log(logs, 'Half Day', shift_details.start_datetime.date(), total_working_hours)
+ return
+ mark_attendance_and_link_log(logs, 'Present', shift_details.start_datetime.date(), total_working_hours)
+
+
+def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
+ """Given a set of logs in chronological order calculates the total working hours based on the parameters.
+ Zero is returned for all invalid cases.
+
+ :param logs: The List of 'Employee Attendance Log'.
+ :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Attendance Log'
+ :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'
+ """
+ total_hours = 0
+ if check_in_out_type == 'Alternating entries as IN and OUT during the same shift':
+ if working_hours_calc_type == 'First Check-in and Last Check-out':
+ # assumption in this case: First log always IN, Last log always OUT
+ total_hours = time_diff_in_hours(logs[0].time, logs[-1].time)
+ elif working_hours_calc_type == 'Every Valid Check-in and Check-out':
+ while len(logs) >= 2:
+ total_hours += time_diff_in_hours(logs[0].time, logs[1].time)
+ del logs[:2]
+
+ elif check_in_out_type == 'Strictly based on Log Type in Employee Attendance Log':
+ if working_hours_calc_type == 'First Check-in and Last Check-out':
+ 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)
+ 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:
+ total_hours += time_diff_in_hours(in_log.time, out_log.time)
+ in_log = out_log = None
+ if not in_log:
+ in_log = log if log.log_type == 'IN' else None
+ elif not out_log:
+ out_log = log if log.log_type == 'OUT' else None
+ if in_log and out_log:
+ total_hours += time_diff_in_hours(in_log.time, out_log.time)
+ return total_hours
+
+
+def time_diff_in_hours(start, end):
+ return round((end-start).total_seconds() / 3600, 1)
+
+def find_index_in_dict(dict_list, key, value):
+ return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None)
+
+def get_actual_start_end_datetime_of_shift(employee, for_datetime, consider_default_shift=False):
+ """Takes a datetime and returns the 'actual' start datetime and end datetime of the shift in which the timestamp belongs.
+ Here 'actual' means - taking in to account the "begin_check_in_before_shift_start_time" and "allow_check_out_after_shift_end_time".
+ None is returned if the timestamp is outside any actual shift timings.
+ Shift Details is also returned(current/upcoming i.e. if timestamp not in any actual shift then details of next shift returned)
+ """
+ actual_shift_start = actual_shift_end = shift_details = None
+ shift_timings_as_per_timestamp = get_employee_shift_timings(employee, for_datetime, consider_default_shift)
+ prev_shift, curr_shift, next_shift = shift_timings_as_per_timestamp
+ timestamp_list = []
+ for shift in shift_timings_as_per_timestamp:
+ if shift:
+ timestamp_list.extend([shift.actual_start, shift.actual_end])
+ else:
+ timestamp_list.extend([None, None])
+ timestamp_index = None
+ for index, timestamp in enumerate(timestamp_list):
+ if timestamp and for_datetime <= timestamp:
+ timestamp_index = index
+ break
+ if timestamp_index and timestamp_index%2 == 1:
+ shift_details = shift_timings_as_per_timestamp[int((timestamp_index-1)/2)]
+ actual_shift_start = shift_details.actual_start
+ actual_shift_end = shift_details.actual_end
+ elif timestamp_index:
+ shift_details = shift_timings_as_per_timestamp[int(timestamp_index/2)]
+
+ return actual_shift_start, actual_shift_end, shift_details
+
+
+def get_employee_attendance_log_last_sync(employee, hr_settings=None, last_log=None):
+ """This functions returns a last sync timestamp for the given employee.
+ """
+ # when using inside auto attendance function 'last_log', 'hr_setting' is passed along
+ if last_log:
+ last_log_time = [last_log]
+ else:
+ last_log_time = frappe.db.get_all('Employee Attendance Log', fields="time", filters={'employee':employee}, limit=1, order_by='time desc')
+ if not hr_settings:
+ hr_settings = frappe.db.get_singles_dict("HR Settings")
+
+ if last_log_time and hr_settings.last_sync_of_attendance_log:
+ return max(last_log_time[0].time, get_datetime(hr_settings.last_sync_of_attendance_log))
+ elif last_log_time:
+ return last_log_time[0].time
+ return get_datetime(hr_settings.last_sync_of_attendance_log) if hr_settings.last_sync_of_attendance_log else None
diff --git a/erpnext/hr/doctype/hr_settings/test_hr_settings.py b/erpnext/hr/doctype/hr_settings/test_hr_settings.py
index 2d5b18b..367c83a 100644
--- a/erpnext/hr/doctype/hr_settings/test_hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/test_hr_settings.py
@@ -5,6 +5,73 @@
import frappe
import unittest
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from frappe.utils import now_datetime
+from datetime import timedelta
class TestHRSettings(unittest.TestCase):
- pass
+ def test_get_employee_attendance_log_last_sync(self):
+ doc = frappe.get_doc("HR Settings")
+ doc.last_sync_of_attendance_log = None
+ doc.save()
+ hr_settings = frappe.db.get_singles_dict("HR Settings")
+ doc.last_sync_of_attendance_log = now_datetime()
+ doc.save()
+
+ from erpnext.hr.doctype.hr_settings.hr_settings import get_employee_attendance_log_last_sync
+ employee = make_employee("test_attendance_log_last_sync@example.com")
+
+ frappe.db.delete('Employee Attendance Log',{'employee':'EMP-00001'})
+ employee_last_sync = get_employee_attendance_log_last_sync(employee, hr_settings)
+ self.assertEqual(employee_last_sync, None)
+
+ employee_last_sync = get_employee_attendance_log_last_sync(employee)
+ self.assertEqual(employee_last_sync, doc.last_sync_of_attendance_log)
+
+ from erpnext.hr.doctype.employee_attendance_log.test_employee_attendance_log import make_attendance_log
+ time_now = now_datetime()
+ make_attendance_log(employee, time_now)
+ employee_last_sync = get_employee_attendance_log_last_sync(employee)
+ self.assertEqual(employee_last_sync, time_now)
+
+ def test_calculate_working_hours(self):
+ check_in_out_type = ['Alternating entries as IN and OUT during the same shift',
+ 'Strictly based on Log Type in Employee Attendance Log']
+ working_hours_calc_type = ['First Check-in and Last Check-out',
+ 'Every Valid Check-in and Check-out']
+ logs_type_1 = [
+ {'time':now_datetime()-timedelta(minutes=390)},
+ {'time':now_datetime()-timedelta(minutes=300)},
+ {'time':now_datetime()-timedelta(minutes=270)},
+ {'time':now_datetime()-timedelta(minutes=90)},
+ {'time':now_datetime()-timedelta(minutes=0)}
+ ]
+ logs_type_2 = [
+ {'time':now_datetime()-timedelta(minutes=390),'log_type':'OUT'},
+ {'time':now_datetime()-timedelta(minutes=360),'log_type':'IN'},
+ {'time':now_datetime()-timedelta(minutes=300),'log_type':'OUT'},
+ {'time':now_datetime()-timedelta(minutes=290),'log_type':'IN'},
+ {'time':now_datetime()-timedelta(minutes=260),'log_type':'OUT'},
+ {'time':now_datetime()-timedelta(minutes=240),'log_type':'IN'},
+ {'time':now_datetime()-timedelta(minutes=150),'log_type':'IN'},
+ {'time':now_datetime()-timedelta(minutes=60),'log_type':'OUT'}
+ ]
+ logs_type_1 = [frappe._dict(x) for x in logs_type_1]
+ logs_type_2 = [frappe._dict(x) for x in logs_type_2]
+
+ from erpnext.hr.doctype.hr_settings.hr_settings import calculate_working_hours
+
+ working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[0])
+ self.assertEqual(working_hours, 6.5)
+
+ working_hours = calculate_working_hours(logs_type_1,check_in_out_type[0],working_hours_calc_type[1])
+ self.assertEqual(working_hours, 4.5)
+
+ working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[0])
+ self.assertEqual(working_hours, 5)
+
+ working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1])
+ self.assertEqual(working_hours, 4.5)
+
+
+
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 48d4e57..c90894e 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -6,7 +6,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate
+from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
+from erpnext.hr.doctype.holiday_list.holiday_list import get_holiday_list, is_holiday
+from datetime import timedelta, datetime
class OverlapError(frappe.ValidationError): pass
@@ -78,3 +80,100 @@
}
if e not in events:
events.append(e)
+
+
+def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):
+ """Returns a Shift Type for the given employee on the given date. (excluding the holidays)
+
+ :param employee: Employee for which shift is required.
+ :param for_date: Date on which shift are required
+ :param consider_default_shift: If set to true, default shift is taken when no shift assignment is found.
+ :param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date.
+ """
+ default_shift = frappe.db.get_value('HR Settings', None, 'default_shift')
+ shift_type_name = frappe.db.get_value('Shift Assignment', {'employee':employee, 'date': for_date, 'docstatus': '1'}, 'shift_type')
+ if not shift_type_name and consider_default_shift:
+ shift_type_name = default_shift
+ if shift_type_name:
+ holiday_list_name = frappe.db.get_value('Shift Type', shift_type_name, 'holiday_list')
+ if not holiday_list_name:
+ holiday_list_name = get_holiday_list(employee)
+ if holiday_list_name and is_holiday(holiday_list_name, for_date):
+ shift_type_name = None
+
+ if not shift_type_name and next_shift_direction:
+ MAX_DAYS = 366
+ if consider_default_shift and default_shift:
+ direction = -1 if next_shift_direction == 'reverse' else +1
+ for i in range(MAX_DAYS):
+ date = for_date+timedelta(days=direction*(i+1))
+ shift_details = get_employee_shift(employee, date, consider_default_shift, None)
+ if shift_details:
+ shift_type_name = shift_details.shift_type.name
+ for_date = date
+ break
+ else:
+ direction = '<' if next_shift_direction == 'reverse' else '>'
+ dates = frappe.db.get_list('Shift Assignment',
+ 'date',
+ {'employee':employee, 'date':(direction, for_date), 'docstatus': '1'},
+ as_list=True,
+ limit=MAX_DAYS)
+ for date in dates:
+ shift_details = get_employee_shift(employee, date[0], consider_default_shift, None)
+ if shift_details:
+ shift_type_name = shift_details.shift_type.name
+ for_date = date[0]
+ break
+
+ return get_shift_details(shift_type_name, for_date)
+
+
+def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False):
+ """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee
+ """
+ # write and verify a test case for midnight shift.
+ prev_shift = curr_shift = next_shift = None
+ curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward')
+ if curr_shift:
+ next_shift = get_employee_shift(employee, curr_shift.start_datetime.date()+timedelta(days=1), consider_default_shift, 'forward')
+ prev_shift = get_employee_shift(employee, for_timestamp.date()+timedelta(days=-1), consider_default_shift, 'reverse')
+
+ if curr_shift:
+ if prev_shift:
+ curr_shift.actual_start = prev_shift.end_datetime if curr_shift.actual_start < prev_shift.end_datetime else curr_shift.actual_start
+ prev_shift.actual_end = curr_shift.actual_start if prev_shift.actual_end > curr_shift.actual_start else prev_shift.actual_end
+ if next_shift:
+ next_shift.actual_start = curr_shift.end_datetime if next_shift.actual_start < curr_shift.end_datetime else next_shift.actual_start
+ curr_shift.actual_end = next_shift.actual_start if curr_shift.actual_end > next_shift.actual_start else curr_shift.actual_end
+ return prev_shift, curr_shift, next_shift
+
+
+def get_shift_details(shift_type_name, for_date=nowdate()):
+ """Returns Shift Details which contain some additional information as described below.
+ 'shift_details' contains the following keys:
+ 'shift_type' - Object of DocType Shift Type,
+ 'start_datetime' - Date and Time of shift start on given date,
+ 'end_datetime' - Date and Time of shift end on given date,
+ 'actual_start' - datetime of shift start after adding 'begin_check_in_before_shift_start_time',
+ 'actual_end' - datetime of shift end after adding 'allow_check_out_after_shift_end_time'(None is returned if this is zero)
+
+ :param shift_type_name: shift type name for which shift_details is required.
+ :param for_date: Date on which shift_details are required
+ """
+ if not shift_type_name:
+ return None
+ shift_type = frappe.get_doc('Shift Type', shift_type_name)
+ start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
+ for_date = for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
+ end_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.end_time
+ actual_start = start_datetime - timedelta(minutes=shift_type.begin_check_in_before_shift_start_time)
+ actual_end = end_datetime + timedelta(minutes=shift_type.allow_check_out_after_shift_end_time)
+
+ return frappe._dict({
+ 'shift_type': shift_type,
+ 'start_datetime': start_datetime,
+ 'end_datetime': end_datetime,
+ 'actual_start': actual_start,
+ 'actual_end': actual_end
+ })
diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json
index d5af2e4..1ee30b2 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.json
+++ b/erpnext/hr/doctype/shift_type/shift_type.json
@@ -1,194 +1,229 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "prompt",
- "beta": 0,
- "creation": "2018-04-13 16:22:52.954783",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "autoname": "prompt",
+ "creation": "2018-04-13 16:22:52.954783",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "start_time",
+ "end_time",
+ "disable_auto_attendance_for_this_shift",
+ "column_break_3",
+ "holiday_list",
+ "auto_attendance_configurations_section",
+ "determine_check_in_and_check_out",
+ "working_hours_calculation_based_on",
+ "begin_check_in_before_shift_start_time",
+ "allow_check_out_after_shift_end_time",
+ "column_break_10",
+ "working_hours_threshold_for_half_day",
+ "working_hours_threshold_for_absent",
+ "grace_period_configuration_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"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "start_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Start Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "start_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "Start Time",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "end_time",
- "fieldtype": "Time",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "End Time",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "end_time",
+ "fieldtype": "Time",
+ "in_list_view": 1,
+ "label": "End Time",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "holiday_list",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Holiday List",
- "length": 0,
- "no_copy": 0,
- "options": "Holiday List",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "holiday_list",
+ "fieldtype": "Link",
+ "label": "Holiday List",
+ "options": "Holiday List"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "determine_check_in_and_check_out",
+ "fieldtype": "Select",
+ "label": "Determine Check-in and Check-out",
+ "options": "Alternating entries as IN and OUT during the same shift\nStrictly based on Log Type in Employee Attendance Log"
+ },
+ {
+ "fieldname": "working_hours_calculation_based_on",
+ "fieldtype": "Select",
+ "label": "Working Hours Calculation Based On",
+ "options": "First Check-in and Last Check-out\nEvery Valid Check-in and Check-out"
+ },
+ {
+ "description": "Working hours below which Half Day is marked. (Zero to disable)",
+ "fieldname": "working_hours_threshold_for_half_day",
+ "fieldtype": "Float",
+ "label": "Working Hours Threshold for Half Day",
+ "precision": "1"
+ },
+ {
+ "description": "Working hours below which Absent is marked. (Zero to disable)",
+ "fieldname": "working_hours_threshold_for_absent",
+ "fieldtype": "Float",
+ "label": "Working Hours Threshold for Absent",
+ "precision": "1"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance_for_this_shift",
+ "fieldname": "auto_attendance_configurations_section",
+ "fieldtype": "Section Break",
+ "label": "Auto Attendance Configurations"
+ },
+ {
+ "default": "45",
+ "description": "The time before the shift start time during which Employee Check-in is considered for attendance.",
+ "fieldname": "begin_check_in_before_shift_start_time",
+ "fieldtype": "Int",
+ "label": "Begin check-in before shift start time (in minutes)"
+ },
+ {
+ "default": "1",
+ "description": "Don't mark attendance based on Employee Attendance Log.",
+ "fieldname": "disable_auto_attendance_for_this_shift",
+ "fieldtype": "Check",
+ "label": "Disable Auto Attendance for this shift"
+ },
+ {
+ "depends_on": "eval:!doc.disable_auto_attendance_for_this_shift",
+ "fieldname": "grace_period_configuration_auto_attendance_section",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Grace Period Configuration For Auto Attendance"
+ },
+ {
+ "fieldname": "enable_entry_grace_period",
+ "fieldtype": "Check",
+ "label": "Enable Entry Grace Period"
+ },
+ {
+ "depends_on": "enable_entry_grace_period",
+ "description": "The time after the shift start time when check-in is considered as late (in minutes).",
+ "fieldname": "late_entry_grace_period",
+ "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"
+ },
+ {
+ "fieldname": "enable_exit_grace_period",
+ "fieldtype": "Check",
+ "label": "Enable Exit Grace Period"
+ },
+ {
+ "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",
+ "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"
+ },
+ {
+ "description": "Time after the end of shift during which check-out is considered for attendance. (Zero to allow till next shift begins)",
+ "fieldname": "allow_check_out_after_shift_end_time",
+ "fieldtype": "Int",
+ "label": "Allow check-out after shift end time (in minutes)"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-04-13 17:48:00.309273",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Shift Type",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "modified": "2019-05-16 18:57:00.150899",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Shift Type",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Employee",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
- },
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file