From e4bbda6ff5c8945a844411ed4651f4707961850b Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Wed, 8 May 2019 12:24:08 +0530 Subject: [PATCH 1/9] feat(HR): Added Employee CheckIn Log > Employee CheckIn Log can be used to store IN/OUT Logs of employees from biometric/RFID devices. >added biometric_id in the Employee Doctype --- erpnext/hr/doctype/attendance/attendance.json | 401 +- erpnext/hr/doctype/employee/employee.json | 3748 +++-------------- .../doctype/employee_checkin_log/__init__.py | 0 .../employee_checkin_log.js | 8 + .../employee_checkin_log.json | 126 + .../employee_checkin_log.py | 42 + .../test_employee_checkin_log.py | 21 + 7 files changed, 927 insertions(+), 3419 deletions(-) create mode 100644 erpnext/hr/doctype/employee_checkin_log/__init__.py create mode 100644 erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js create mode 100644 erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json create mode 100644 erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py create mode 100644 erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 517fcd7ad7..7e1d5ed962 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -1,540 +1,197 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, "allow_import": 1, - "allow_rename": 0, "autoname": "naming_series:", - "beta": 0, "creation": "2013-01-10 16:34:13", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", - "editable_grid": 0, + "field_order": [ + "attendance_details", + "naming_series", + "employee", + "employee_name", + "status", + "leave_type", + "leave_application", + "column_break0", + "attendance_date", + "company", + "department", + "attendance_request", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "attendance_details", "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": "", - "length": 0, - "no_copy": 0, "oldfieldtype": "Section Break", - "options": "Simple", - "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 + "options": "Simple" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", "fieldname": "naming_series", "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": "Series", - "length": 0, "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", "options": "HR-ATT-.YYYY.-", - "permlevel": 0, - "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": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "employee", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, "in_standard_filter": 1, "label": "Employee", - "length": 0, - "no_copy": 0, "oldfieldname": "employee", "oldfieldtype": "Link", "options": "Employee", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "employee.employee_name", "fieldname": "employee_name", "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Employee Name", - "length": 0, - "no_copy": 0, "oldfieldname": "employee_name", - "oldfieldtype": "Data", - "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 + "oldfieldtype": "Data" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Present", "fieldname": "status", "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": 1, "label": "Status", - "length": 0, "no_copy": 1, "oldfieldname": "status", "oldfieldtype": "Select", "options": "\nPresent\nAbsent\nOn Leave\nHalf Day", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:doc.status==\"On Leave\"", "fieldname": "leave_type", "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": 1, "label": "Leave Type", - "length": 0, - "no_copy": 0, "oldfieldname": "leave_type", "oldfieldtype": "Link", - "options": "Leave Type", - "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 + "options": "Leave Type" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "leave_application", "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 Application", - "length": 0, - "no_copy": 0, "options": "Leave Application", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break0", "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, "oldfieldtype": "Column Break", - "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, "width": "50%" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "attendance_date", "fieldtype": "Date", - "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": "Attendance Date", - "length": 0, - "no_copy": 0, "oldfieldname": "attendance_date", "oldfieldtype": "Date", - "permlevel": 0, - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "company", "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": "Company", - "length": 0, - "no_copy": 0, "oldfieldname": "company", "oldfieldtype": "Link", "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fetch_from": "employee.department", "fieldname": "department", "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": "Department", - "length": 0, - "no_copy": 0, "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "attendance_request", "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": "Attendance Request", - "length": 0, - "no_copy": 0, "options": "Attendance Request", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amended_from", "fieldtype": "Link", - "hidden": 0, "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Amended From", - "length": 0, "no_copy": 1, "options": "Attendance", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "read_only": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "icon": "fa fa-ok", "idx": 1, - "image_view": 0, - "in_create": 0, "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-06-05 12:00:14.043535", + "modified": "2019-06-05 19:37:30.410071", "modified_by": "Administrator", "module": "HR", "name": "Attendance", "owner": "ashwini@webnotestech.com", "permissions": [ { - "amend": 0, "cancel": 1, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 }, { - "amend": 0, "cancel": 1, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, "import": 1, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 }, { - "amend": 0, "cancel": 1, "create": 1, "delete": 1, "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, "submit": 1, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, "search_fields": "employee,employee_name,attendance_date,status", - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "title_field": "employee_name" } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index f6948c1a91..6cd4710cd6 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -1,3157 +1,811 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 1, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-03-07 09:04:18", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, + "allow_events_in_timeline": 1, + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2013-03-07 09:04:18", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "field_order": [ + "basic_information", + "employee", + "naming_series", + "salutation", + "first_name", + "middle_name", + "last_name", + "employee_name", + "biometric_id", + "image", + "column_break1", + "company", + "status", + "employee_number", + "employment_type", + "gender", + "date_of_birth", + "date_of_joining", + "emergency_contact_details", + "emergency_phone_number", + "person_to_be_contacted", + "relation", + "erpnext_user", + "user_id", + "create_user", + "create_user_permission", + "employment_details", + "job_applicant", + "scheduled_confirmation_date", + "final_confirmation_date", + "col_break_22", + "contract_end_date", + "notice_number_of_days", + "date_of_retirement", + "job_profile", + "department", + "designation", + "reports_to", + "column_break_31", + "grade", + "branch", + "organization_profile", + "leave_policy", + "holiday_list", + "salary_information", + "salary_mode", + "bank_name", + "bank_ac_no", + "health_insurance_section", + "health_insurance_provider", + "health_insurance_no", + "contact_details", + "cell_number", + "prefered_contact_email", + "prefered_email", + "company_email", + "personal_email", + "unsubscribed", + "column_break4", + "permanent_accommodation_type", + "permanent_address", + "current_accommodation_type", + "current_address", + "sb53", + "bio", + "personal_details", + "passport_number", + "date_of_issue", + "valid_upto", + "place_of_issue", + "column_break6", + "marital_status", + "blood_group", + "family_background", + "health_details", + "educational_qualification", + "education", + "previous_work_experience", + "external_work_history", + "history_in_company", + "internal_work_history", + "exit", + "resignation_letter_date", + "relieving_date", + "reason_for_leaving", + "leave_encashed", + "encashment_date", + "exit_interview_details", + "held_on", + "reason_for_resignation", + "new_workplace", + "feedback", + "lft", + "rgt", + "old_parent" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "basic_information", - "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": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "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": "basic_information", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "fieldtype": "Data", - "hidden": 1, - "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", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee", + "fieldtype": "Data", + "hidden": 1, + "label": "Employee", + "no_copy": 1, + "print_hide": 1, + "report_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "naming_series", - "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": "Series", - "length": 0, - "no_copy": 1, - "oldfieldname": "naming_series", - "oldfieldtype": "Select", - "options": "HR-EMP-", - "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": 1, - "translatable": 0, - "unique": 0 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "oldfieldname": "naming_series", + "oldfieldtype": "Select", + "options": "HR-EMP-", + "set_only_once": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "salutation", - "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": "Salutation", - "length": 0, - "no_copy": 0, - "oldfieldname": "salutation", - "oldfieldtype": "Select", - "options": "Salutation", - "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": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "oldfieldname": "salutation", + "oldfieldtype": "Select", + "options": "Salutation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "first_name", - "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": "First Name", - "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": "first_name", + "fieldtype": "Data", + "label": "First Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "middle_name", - "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": "Middle Name", - "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 - }, + "allow_in_quick_entry": 1, + "fieldname": "middle_name", + "fieldtype": "Data", + "label": "Middle Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "last_name", - "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": "Last Name", - "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 - }, + "allow_in_quick_entry": 1, + "fieldname": "last_name", + "fieldtype": "Data", + "label": "Last Name" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Full Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "employee_name", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "employee_name", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Full Name", + "oldfieldname": "employee_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 1, - "options": "", - "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": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "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, - "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, + "allow_in_quick_entry": 1, + "fieldname": "column_break1", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Active", - "fieldname": "status", - "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": "Status", - "length": 0, - "no_copy": 0, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "\nActive\nLeft", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Active", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "\nActive\nLeft", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee_number", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee Number", - "length": 0, - "no_copy": 0, - "oldfieldname": "employee_number", - "oldfieldtype": "Data", - "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_number", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Employee Number", + "oldfieldname": "employee_number", + "oldfieldtype": "Data" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employment_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employment Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "employment_type", - "oldfieldtype": "Link", - "options": "Employment Type", - "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": "employment_type", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Employment Type", + "oldfieldname": "employment_type", + "oldfieldtype": "Link", + "options": "Employment Type" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gender", - "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": "Gender", - "length": 0, - "no_copy": 0, - "oldfieldname": "gender", - "oldfieldtype": "Select", - "options": "Gender", - "permlevel": 0, - "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": "gender", + "fieldtype": "Link", + "label": "Gender", + "oldfieldname": "gender", + "oldfieldtype": "Select", + "options": "Gender", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "You can enter any date manually", - "fieldname": "date_of_birth", - "fieldtype": "Date", - "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": "Date of Birth", - "length": 0, - "no_copy": 0, - "oldfieldname": "date_of_birth", - "oldfieldtype": "Date", - "permlevel": 0, - "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 - }, + "description": "You can enter any date manually", + "fieldname": "date_of_birth", + "fieldtype": "Date", + "label": "Date of Birth", + "oldfieldname": "date_of_birth", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_joining", - "fieldtype": "Date", - "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": "Date of Joining", - "length": 0, - "no_copy": 0, - "oldfieldname": "date_of_joining", - "oldfieldtype": "Date", - "permlevel": 0, - "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": "date_of_joining", + "fieldtype": "Date", + "label": "Date of Joining", + "oldfieldname": "date_of_joining", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "emergency_contact_details", - "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": "Emergency Contact", - "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 - }, + "allow_in_quick_entry": 1, + "collapsible": 1, + "fieldname": "emergency_contact_details", + "fieldtype": "Section Break", + "label": "Emergency Contact" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "emergency_phone_number", - "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": "Emergency Phone", - "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 - }, + "bold": 1, + "fieldname": "emergency_phone_number", + "fieldtype": "Data", + "label": "Emergency Phone" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "person_to_be_contacted", - "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": "Emergency Contact", - "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 - }, + "bold": 1, + "fieldname": "person_to_be_contacted", + "fieldtype": "Data", + "label": "Emergency Contact" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "relation", - "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": "Relation", - "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": "relation", + "fieldtype": "Data", + "label": "Relation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "erpnext_user", - "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": "ERPNext User", - "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 - }, + "collapsible": 1, + "fieldname": "erpnext_user", + "fieldtype": "Section Break", + "label": "ERPNext User" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "System User (login) ID. If set, it will become default for all HR forms.", - "fieldname": "user_id", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "User ID", - "length": 0, - "no_copy": 0, - "options": "User", - "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": "System User (login) ID. If set, it will become default for all HR forms.", + "fieldname": "user_id", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "User ID", + "options": "User" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:(!doc.user_id)", - "fieldname": "create_user", - "fieldtype": "Button", - "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": "Create User", - "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.user_id)", + "fieldname": "create_user", + "fieldtype": "Button", + "label": "Create User" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "1", - "depends_on": "user_id", - "description": "This will restrict user access to other employee records", - "fieldname": "create_user_permission", - "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": "Create User Permission", - "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", + "depends_on": "user_id", + "description": "This will restrict user access to other employee records", + "fieldname": "create_user_permission", + "fieldtype": "Check", + "label": "Create User Permission" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 1, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "employment_details", - "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": "Joining Details", - "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 - }, + "allow_in_quick_entry": 1, + "collapsible": 1, + "fieldname": "employment_details", + "fieldtype": "Section Break", + "label": "Joining Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "job_applicant", - "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": "Job Applicant", - "length": 0, - "no_copy": 0, - "options": "Job Applicant", - "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": "job_applicant", + "fieldtype": "Link", + "label": "Job Applicant", + "options": "Job Applicant" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "scheduled_confirmation_date", - "fieldtype": "Date", - "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": "Offer Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "scheduled_confirmation_date", - "oldfieldtype": "Date", - "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": "scheduled_confirmation_date", + "fieldtype": "Date", + "label": "Offer Date", + "oldfieldname": "scheduled_confirmation_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "final_confirmation_date", - "fieldtype": "Date", - "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": "Confirmation Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "final_confirmation_date", - "oldfieldtype": "Date", - "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": "final_confirmation_date", + "fieldtype": "Date", + "label": "Confirmation Date", + "oldfieldname": "final_confirmation_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break_22", - "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, - "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": "col_break_22", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contract_end_date", - "fieldtype": "Date", - "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": "Contract End Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "contract_end_date", - "oldfieldtype": "Date", - "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": "contract_end_date", + "fieldtype": "Date", + "label": "Contract End Date", + "oldfieldname": "contract_end_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "notice_number_of_days", - "fieldtype": "Int", - "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": "Notice (days)", - "length": 0, - "no_copy": 0, - "oldfieldname": "notice_number_of_days", - "oldfieldtype": "Int", - "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": "notice_number_of_days", + "fieldtype": "Int", + "label": "Notice (days)", + "oldfieldname": "notice_number_of_days", + "oldfieldtype": "Int" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_retirement", - "fieldtype": "Date", - "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": "Date Of Retirement", - "length": 0, - "no_copy": 0, - "oldfieldname": "date_of_retirement", - "oldfieldtype": "Date", - "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": "date_of_retirement", + "fieldtype": "Date", + "label": "Date Of Retirement", + "oldfieldname": "date_of_retirement", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "job_profile", - "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": "Department and Grade", - "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 - }, + "collapsible": 1, + "fieldname": "job_profile", + "fieldtype": "Section Break", + "label": "Department and Grade" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "department", - "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": 1, - "label": "Department", - "length": 0, - "no_copy": 0, - "oldfieldname": "department", - "oldfieldtype": "Link", - "options": "Department", - "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": "department", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Department", + "oldfieldname": "department", + "oldfieldtype": "Link", + "options": "Department" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "designation", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Designation", - "length": 0, - "no_copy": 0, - "oldfieldname": "designation", - "oldfieldtype": "Link", - "options": "Designation", - "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": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "designation", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "label": "Designation", + "oldfieldname": "designation", + "oldfieldtype": "Link", + "options": "Designation", + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reports_to", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reports to", - "length": 0, - "no_copy": 0, - "oldfieldname": "reports_to", - "oldfieldtype": "Link", - "options": "Employee", - "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": "reports_to", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Reports to", + "oldfieldname": "reports_to", + "oldfieldtype": "Link", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_31", - "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_31", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grade", - "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": "Grade", - "length": 0, - "no_copy": 0, - "options": "Employee Grade", - "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": "grade", + "fieldtype": "Link", + "label": "Grade", + "options": "Employee Grade" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "branch", - "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": "Branch", - "length": 0, - "no_copy": 0, - "oldfieldname": "branch", - "oldfieldtype": "Link", - "options": "Branch", - "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": "branch", + "fieldtype": "Link", + "label": "Branch", + "oldfieldname": "branch", + "oldfieldtype": "Link", + "options": "Branch" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "organization_profile", - "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 Details", - "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 - }, + "collapsible": 1, + "fieldname": "organization_profile", + "fieldtype": "Section Break", + "label": "Leave Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "leave_policy", - "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 Policy", - "length": 0, - "no_copy": 0, - "options": "Leave Policy", - "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_policy", + "fieldtype": "Link", + "label": "Leave Policy", + "options": "Leave Policy" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Applicable Holiday List", - "fieldname": "holiday_list", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "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, - "oldfieldname": "holiday_list", - "oldfieldtype": "Link", - "options": "Holiday List", - "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": "Applicable Holiday List", + "fieldname": "holiday_list", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Holiday List", + "oldfieldname": "holiday_list", + "oldfieldtype": "Link", + "options": "Holiday List" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "salary_information", - "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": "Salary Details", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "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, + "collapsible": 1, + "fieldname": "salary_information", + "fieldtype": "Section Break", + "label": "Salary Details", + "oldfieldtype": "Section Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "salary_mode", - "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": "Salary Mode", - "length": 0, - "no_copy": 0, - "oldfieldname": "salary_mode", - "oldfieldtype": "Select", - "options": "\nBank\nCash\nCheque", - "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": "salary_mode", + "fieldtype": "Select", + "label": "Salary Mode", + "oldfieldname": "salary_mode", + "oldfieldtype": "Select", + "options": "\nBank\nCash\nCheque" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.salary_mode == 'Bank'", - "fieldname": "bank_name", - "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": "Bank Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "bank_name", - "oldfieldtype": "Link", - "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 - }, + "depends_on": "eval:doc.salary_mode == 'Bank'", + "fieldname": "bank_name", + "fieldtype": "Data", + "label": "Bank Name", + "oldfieldname": "bank_name", + "oldfieldtype": "Link" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.salary_mode == 'Bank'", - "fieldname": "bank_ac_no", - "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": "Bank A/C No.", - "length": 0, - "no_copy": 0, - "oldfieldname": "bank_ac_no", - "oldfieldtype": "Data", - "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 - }, + "depends_on": "eval:doc.salary_mode == 'Bank'", + "fieldname": "bank_ac_no", + "fieldtype": "Data", + "label": "Bank A/C No.", + "oldfieldname": "bank_ac_no", + "oldfieldtype": "Data" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "health_insurance_section", - "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": "Health Insurance", - "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 - }, + "collapsible": 1, + "fieldname": "health_insurance_section", + "fieldtype": "Section Break", + "label": "Health Insurance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "health_insurance_provider", - "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": "Health Insurance Provider", - "length": 0, - "no_copy": 0, - "options": "Employee Health Insurance", - "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": "health_insurance_provider", + "fieldtype": "Link", + "label": "Health Insurance Provider", + "options": "Employee Health Insurance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.health_insurance_provider", - "fieldname": "health_insurance_no", - "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": "Health Insurance No", - "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.health_insurance_provider", + "fieldname": "health_insurance_no", + "fieldtype": "Data", + "label": "Health Insurance No" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "contact_details", - "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": "Contact Details", - "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 - }, + "collapsible": 1, + "fieldname": "contact_details", + "fieldtype": "Section Break", + "label": "Contact Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "cell_number", - "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": "Mobile", - "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": "cell_number", + "fieldtype": "Data", + "label": "Mobile" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "prefered_contact_email", - "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": "Prefered Contact Email", - "length": 0, - "no_copy": 0, - "options": "\nCompany Email\nPersonal Email\nUser ID", - "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": "prefered_contact_email", + "fieldtype": "Select", + "label": "Prefered Contact Email", + "options": "\nCompany Email\nPersonal Email\nUser ID" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "prefered_email", - "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": "Prefered Email", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "prefered_email", + "fieldtype": "Data", + "label": "Prefered Email", + "options": "Email", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Provide Email Address registered in company", - "fieldname": "company_email", - "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": "Company Email", - "length": 0, - "no_copy": 0, - "oldfieldname": "company_email", - "oldfieldtype": "Data", - "options": "Email", - "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": "Provide Email Address registered in company", + "fieldname": "company_email", + "fieldtype": "Data", + "label": "Company Email", + "oldfieldname": "company_email", + "oldfieldtype": "Data", + "options": "Email" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "personal_email", - "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": "Personal Email", - "length": 0, - "no_copy": 0, - "options": "Email", - "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": "personal_email", + "fieldtype": "Data", + "label": "Personal Email", + "options": "Email" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "unsubscribed", - "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": "Unsubscribed", - "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": "unsubscribed", + "fieldtype": "Check", + "label": "Unsubscribed" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break4", - "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, - "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_break4", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "permanent_accommodation_type", - "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": "Permanent Address Is", - "length": 0, - "no_copy": 0, - "options": "\nRented\nOwned", - "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": "permanent_accommodation_type", + "fieldtype": "Select", + "label": "Permanent Address Is", + "options": "\nRented\nOwned" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "permanent_address", - "fieldtype": "Small Text", - "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": "Permanent Address", - "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": "permanent_address", + "fieldtype": "Small Text", + "label": "Permanent Address" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_accommodation_type", - "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": "Current Address Is", - "length": 0, - "no_copy": 0, - "options": "\nRented\nOwned", - "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": "current_accommodation_type", + "fieldtype": "Select", + "label": "Current Address Is", + "options": "\nRented\nOwned" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_address", - "fieldtype": "Small Text", - "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": "Current Address", - "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": "current_address", + "fieldtype": "Small Text", + "label": "Current Address" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "sb53", - "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": "Personal Bio", - "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 - }, + "collapsible": 1, + "fieldname": "sb53", + "fieldtype": "Section Break", + "label": "Personal Bio" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Short biography for website and other publications.", - "fieldname": "bio", - "fieldtype": "Text Editor", - "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": "Bio / Cover Letter", - "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": "Short biography for website and other publications.", + "fieldname": "bio", + "fieldtype": "Text Editor", + "label": "Bio / Cover Letter" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "personal_details", - "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": "Personal Details", - "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 - }, + "collapsible": 1, + "fieldname": "personal_details", + "fieldtype": "Section Break", + "label": "Personal Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "passport_number", - "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": "Passport Number", - "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": "passport_number", + "fieldtype": "Data", + "label": "Passport Number" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date_of_issue", - "fieldtype": "Date", - "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": "Date of Issue", - "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": "date_of_issue", + "fieldtype": "Date", + "label": "Date of Issue" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "valid_upto", - "fieldtype": "Date", - "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": "Valid Upto", - "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": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "place_of_issue", - "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": "Place of Issue", - "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": "place_of_issue", + "fieldtype": "Data", + "label": "Place of Issue" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break6", - "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, - "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_break6", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "marital_status", - "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": "Marital Status", - "length": 0, - "no_copy": 0, - "options": "\nSingle\nMarried\nDivorced\nWidowed", - "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": "marital_status", + "fieldtype": "Select", + "label": "Marital Status", + "options": "\nSingle\nMarried\nDivorced\nWidowed" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "blood_group", - "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": "Blood Group", - "length": 0, - "no_copy": 0, - "options": "\nA+\nA-\nB+\nB-\nAB+\nAB-\nO+\nO-", - "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": "blood_group", + "fieldtype": "Select", + "label": "Blood Group", + "options": "\nA+\nA-\nB+\nB-\nAB+\nAB-\nO+\nO-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Here you can maintain family details like name and occupation of parent, spouse and children", - "fieldname": "family_background", - "fieldtype": "Small Text", - "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": "Family Background", - "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": "Here you can maintain family details like name and occupation of parent, spouse and children", + "fieldname": "family_background", + "fieldtype": "Small Text", + "label": "Family Background" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Here you can maintain height, weight, allergies, medical concerns etc", - "fieldname": "health_details", - "fieldtype": "Small Text", - "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": "Health Details", - "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": "Here you can maintain height, weight, allergies, medical concerns etc", + "fieldname": "health_details", + "fieldtype": "Small Text", + "label": "Health Details" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "educational_qualification", - "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": "Educational Qualification", - "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 - }, + "collapsible": 1, + "fieldname": "educational_qualification", + "fieldtype": "Section Break", + "label": "Educational Qualification" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "education", - "fieldtype": "Table", - "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": "Education", - "length": 0, - "no_copy": 0, - "options": "Employee Education", - "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": "education", + "fieldtype": "Table", + "label": "Education", + "options": "Employee Education" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "previous_work_experience", - "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": "Previous Work Experience", - "length": 0, - "no_copy": 0, - "options": "Simple", - "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 - }, + "collapsible": 1, + "fieldname": "previous_work_experience", + "fieldtype": "Section Break", + "label": "Previous Work Experience", + "options": "Simple" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "external_work_history", - "fieldtype": "Table", - "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": "External Work History", - "length": 0, - "no_copy": 0, - "options": "Employee External Work History", - "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": "external_work_history", + "fieldtype": "Table", + "label": "External Work History", + "options": "Employee External Work History" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "history_in_company", - "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": "History In Company", - "length": 0, - "no_copy": 0, - "options": "Simple", - "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 - }, + "collapsible": 1, + "fieldname": "history_in_company", + "fieldtype": "Section Break", + "label": "History In Company", + "options": "Simple" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "internal_work_history", - "fieldtype": "Table", - "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": "Internal Work History", - "length": 0, - "no_copy": 0, - "options": "Employee Internal Work History", - "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": "internal_work_history", + "fieldtype": "Table", + "label": "Internal Work History", + "options": "Employee Internal Work History" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "exit", - "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": "Exit", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "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 - }, + "collapsible": 1, + "fieldname": "exit", + "fieldtype": "Section Break", + "label": "Exit", + "oldfieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "resignation_letter_date", - "fieldtype": "Date", - "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": "Resignation Letter Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "resignation_letter_date", - "oldfieldtype": "Date", - "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": "resignation_letter_date", + "fieldtype": "Date", + "label": "Resignation Letter Date", + "oldfieldname": "resignation_letter_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "relieving_date", - "fieldtype": "Date", - "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": "Relieving Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "relieving_date", - "oldfieldtype": "Date", - "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": "relieving_date", + "fieldtype": "Date", + "label": "Relieving Date", + "oldfieldname": "relieving_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reason_for_leaving", - "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": "Reason for Leaving", - "length": 0, - "no_copy": 0, - "oldfieldname": "reason_for_leaving", - "oldfieldtype": "Data", - "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": "reason_for_leaving", + "fieldtype": "Data", + "label": "Reason for Leaving", + "oldfieldname": "reason_for_leaving", + "oldfieldtype": "Data" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "leave_encashed", - "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": "Leave Encashed?", - "length": 0, - "no_copy": 0, - "oldfieldname": "leave_encashed", - "oldfieldtype": "Select", - "options": "\nYes\nNo", - "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": "leave_encashed", + "fieldtype": "Select", + "label": "Leave Encashed?", + "oldfieldname": "leave_encashed", + "oldfieldtype": "Select", + "options": "\nYes\nNo" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "encashment_date", - "fieldtype": "Date", - "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": "Encashment Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "encashment_date", - "oldfieldtype": "Date", - "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": "encashment_date", + "fieldtype": "Date", + "label": "Encashment Date", + "oldfieldname": "encashment_date", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "exit_interview_details", - "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, - "label": "Exit Interview Details", - "length": 0, - "no_copy": 0, - "oldfieldname": "col_brk6", - "oldfieldtype": "Column Break", - "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": "exit_interview_details", + "fieldtype": "Column Break", + "label": "Exit Interview Details", + "oldfieldname": "col_brk6", + "oldfieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "held_on", - "fieldtype": "Date", - "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": "Held On", - "length": 0, - "no_copy": 0, - "oldfieldname": "held_on", - "oldfieldtype": "Date", - "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": "held_on", + "fieldtype": "Date", + "label": "Held On", + "oldfieldname": "held_on", + "oldfieldtype": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reason_for_resignation", - "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": "Reason for Resignation", - "length": 0, - "no_copy": 0, - "oldfieldname": "reason_for_resignation", - "oldfieldtype": "Select", - "options": "\nBetter Prospects\nHealth Concerns", - "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": "reason_for_resignation", + "fieldtype": "Select", + "label": "Reason for Resignation", + "oldfieldname": "reason_for_resignation", + "oldfieldtype": "Select", + "options": "\nBetter Prospects\nHealth Concerns" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "new_workplace", - "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": "New Workplace", - "length": 0, - "no_copy": 0, - "oldfieldname": "new_workplace", - "oldfieldtype": "Data", - "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": "new_workplace", + "fieldtype": "Data", + "label": "New Workplace", + "oldfieldname": "new_workplace", + "oldfieldtype": "Data" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "feedback", - "fieldtype": "Small Text", - "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": "Feedback", - "length": 0, - "no_copy": 0, - "oldfieldname": "feedback", - "oldfieldtype": "Text", - "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": "feedback", + "fieldtype": "Small Text", + "label": "Feedback", + "oldfieldname": "feedback", + "oldfieldtype": "Text" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lft", - "fieldtype": "Int", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "lft", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "lft", + "fieldtype": "Int", + "hidden": 1, + "label": "lft", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rgt", - "fieldtype": "Int", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "rgt", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "rgt", + "fieldtype": "Int", + "hidden": 1, + "label": "rgt", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "old_parent", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Old Parent", - "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": "old_parent", + "fieldtype": "Data", + "hidden": 1, + "ignore_user_permissions": 1, + "label": "Old Parent" + }, + { + "fieldname": "biometric_id", + "fieldtype": "Data", + "label": "Biometric/RF tag ID ", + "no_copy": 1, + "unique": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-user", - "idx": 24, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-24 15:36:58.234710", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee", - "name_case": "Title Case", - "owner": "Administrator", + ], + "icon": "fa fa-user", + "idx": 24, + "image_field": "image", + "modified": "2019-05-08 10:53:50.897464", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee", + "name_case": "Title Case", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee" + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "set_user_permissions": 1, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "set_user_permissions": 1, + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "employee_name", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "search_fields": "employee_name", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_checkin_log/__init__.py b/erpnext/hr/doctype/employee_checkin_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js new file mode 100644 index 0000000000..eb2a21b7c9 --- /dev/null +++ b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Checkin Log', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json new file mode 100644 index 0000000000..363c502012 --- /dev/null +++ b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json @@ -0,0 +1,126 @@ +{ + "allow_import": 1, + "autoname": "EMP-CHECKIN-.MM.-.YYYY.-.######", + "creation": "2019-04-25 10:17:11.225671", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "log_type", + "column_break_4", + "time", + "device_id", + "attendance_marked", + "entry_grace_period_consequence", + "exit_grace_period_consequence" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "Now", + "fieldname": "time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Time", + "reqd": 1 + }, + { + "fieldname": "device_id", + "fieldtype": "Data", + "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, + "label": "Entry Grace Period Consequence" + }, + { + "fieldname": "exit_grace_period_consequence", + "fieldtype": "Check", + "hidden": 1, + "label": "Exit Grace Period Consequence" + }, + { + "fieldname": "log_type", + "fieldtype": "Select", + "label": "Log Type", + "options": "\nIN\nOUT" + } + ], + "modified": "2019-05-08 11:07:56.885960", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Checkin Log", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py new file mode 100644 index 0000000000..c7dec0512e --- /dev/null +++ b/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import now +from frappe.model.document import Document +from frappe import _ + +class EmployeeCheckinLog(Document): + pass + + +@frappe.whitelist() +def add_employee_checkin_log_based_on_biometric_id(biometric_id, timestamp, device_id=None, log_type=None): + """Finds the relevant Employee using the biometric_id and creates a Employee Checkin Log. + + :param biometric_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 Check-in if available (IN/OUT). + """ + + if not biometric_id or not timestamp: + frappe.throw(_("'biometric_id' and 'timestamp' are required.")) + + employee = frappe.db.get_values("Employee", {"biometric_id": biometric_id},["name","employee_name","biometric_id"],as_dict=True) + if len(employee) != 0: + employee = employee[0] + else: + frappe.throw(_("No Employee found for the given 'biometric_id'.")) + + doc = frappe.new_doc("Employee Checkin Log") + doc.employee = employee.name + doc.employee_name = employee.employee_name + doc.time = timestamp + doc.device_id = device_id + doc.log_type = log_type + doc.save() + + return doc diff --git a/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py b/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py new file mode 100644 index 0000000000..f7ae775161 --- /dev/null +++ b/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +from erpnext.hr.doctype.employee_checkin_log.employee_checkin_log import add_employee_checkin_log_based_on_biometric_id + +import frappe +import unittest + +class TestEmployeeCheckinLog(unittest.TestCase): + def test_add_employee_checkin_log_based_on_biometric_id(self): + employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) + employee.biometric_id = '12349' + employee.save() + + checkin_log = add_employee_checkin_log_based_on_biometric_id('12349', '2019-05-08 10:48:08.000000', 'mumbai_first_floor', 'IN') + self.assertTrue(checkin_log.employee == employee.name) + self.assertTrue(checkin_log.time == '2019-05-08 10:48:08.000000') + self.assertTrue(checkin_log.device_id == 'mumbai_first_floor') + self.assertTrue(checkin_log.log_type == 'IN') From a4d0ba222b3cce9232cf00f22a09ee7dd143b44c Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Wed, 8 May 2019 14:38:26 +0530 Subject: [PATCH 2/9] fix(HR): changes as per PR review > renamed 'Employee Checkin Log' DocType to 'Employee_attendance_log' DocType > renamed biometric_id to biometric_rf_if --- erpnext/hr/doctype/employee/employee.json | 6 +++--- .../__init__.py | 0 .../employee_attendance_log.js} | 2 +- .../employee_attendance_log.json} | 6 +++--- .../employee_attendance_log.py} | 20 +++++++++--------- .../test_employee_attendance_log.py | 21 +++++++++++++++++++ .../test_employee_checkin_log.py | 21 ------------------- 7 files changed, 38 insertions(+), 38 deletions(-) rename erpnext/hr/doctype/{employee_checkin_log => employee_attendance_log}/__init__.py (100%) rename erpnext/hr/doctype/{employee_checkin_log/employee_checkin_log.js => employee_attendance_log/employee_attendance_log.js} (77%) rename erpnext/hr/doctype/{employee_checkin_log/employee_checkin_log.json => employee_attendance_log/employee_attendance_log.json} (94%) rename erpnext/hr/doctype/{employee_checkin_log/employee_checkin_log.py => employee_attendance_log/employee_attendance_log.py} (50%) create mode 100644 erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py delete mode 100644 erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 6cd4710cd6..d1384188e0 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -16,7 +16,7 @@ "middle_name", "last_name", "employee_name", - "biometric_id", + "biometric_rf_id", "image", "column_break1", "company", @@ -749,7 +749,7 @@ "label": "Old Parent" }, { - "fieldname": "biometric_id", + "fieldname": "biometric_rf_id", "fieldtype": "Data", "label": "Biometric/RF tag ID ", "no_copy": 1, @@ -759,7 +759,7 @@ "icon": "fa fa-user", "idx": 24, "image_field": "image", - "modified": "2019-05-08 10:53:50.897464", + "modified": "2019-05-08 14:32:06.443825", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee_checkin_log/__init__.py b/erpnext/hr/doctype/employee_attendance_log/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_checkin_log/__init__.py rename to erpnext/hr/doctype/employee_attendance_log/__init__.py diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js similarity index 77% rename from erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js rename to erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js index eb2a21b7c9..55021f6735 100644 --- a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.js +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js @@ -1,7 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Employee Checkin Log', { +frappe.ui.form.on('Employee Attendance Log', { // refresh: function(frm) { // } diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json similarity index 94% rename from erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json rename to erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json index 363c502012..ccdb83d889 100644 --- a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.json +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json @@ -1,6 +1,6 @@ { "allow_import": 1, - "autoname": "EMP-CHECKIN-.MM.-.YYYY.-.######", + "autoname": "EMP-ATT-LOG-.MM.-.YYYY.-.######", "creation": "2019-04-25 10:17:11.225671", "doctype": "DocType", "engine": "InnoDB", @@ -74,10 +74,10 @@ "options": "\nIN\nOUT" } ], - "modified": "2019-05-08 11:07:56.885960", + "modified": "2019-05-08 14:10:22.468252", "modified_by": "Administrator", "module": "HR", - "name": "Employee Checkin Log", + "name": "Employee Attendance Log", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py similarity index 50% rename from erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py rename to erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py index c7dec0512e..6603b1c5a4 100644 --- a/erpnext/hr/doctype/employee_checkin_log/employee_checkin_log.py +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py @@ -8,30 +8,30 @@ from frappe.utils import now from frappe.model.document import Document from frappe import _ -class EmployeeCheckinLog(Document): +class EmployeeAttendanceLog(Document): pass @frappe.whitelist() -def add_employee_checkin_log_based_on_biometric_id(biometric_id, timestamp, device_id=None, log_type=None): - """Finds the relevant Employee using the biometric_id and creates a Employee Checkin Log. +def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, log_type=None): + """Finds the relevant Employee using the biometric_rf_id and creates a Employee Attendance Log. - :param biometric_id: The Biometric/RF tag ID as set up in Employee DocType. + :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 Check-in if available (IN/OUT). + :param log_type(optional): Direction of the Punch if available (IN/OUT). """ - if not biometric_id or not timestamp: - frappe.throw(_("'biometric_id' and 'timestamp' are required.")) + if not biometric_rf_id or not timestamp: + frappe.throw(_("'biometric_rf_id' and 'timestamp' are required.")) - employee = frappe.db.get_values("Employee", {"biometric_id": biometric_id},["name","employee_name","biometric_id"],as_dict=True) + 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 = employee[0] else: - frappe.throw(_("No Employee found for the given 'biometric_id'.")) + frappe.throw(_("No Employee found for the given 'biometric_rf_id'.")) - doc = frappe.new_doc("Employee Checkin Log") + doc = frappe.new_doc("Employee Attendance Log") doc.employee = employee.name doc.employee_name = employee.employee_name doc.time = timestamp 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 new file mode 100644 index 0000000000..45c1353d51 --- /dev/null +++ b/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# 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 +import unittest + +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.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') diff --git a/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py b/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py deleted file mode 100644 index f7ae775161..0000000000 --- a/erpnext/hr/doctype/employee_checkin_log/test_employee_checkin_log.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -from erpnext.hr.doctype.employee_checkin_log.employee_checkin_log import add_employee_checkin_log_based_on_biometric_id - -import frappe -import unittest - -class TestEmployeeCheckinLog(unittest.TestCase): - def test_add_employee_checkin_log_based_on_biometric_id(self): - employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) - employee.biometric_id = '12349' - employee.save() - - checkin_log = add_employee_checkin_log_based_on_biometric_id('12349', '2019-05-08 10:48:08.000000', 'mumbai_first_floor', 'IN') - self.assertTrue(checkin_log.employee == employee.name) - self.assertTrue(checkin_log.time == '2019-05-08 10:48:08.000000') - self.assertTrue(checkin_log.device_id == 'mumbai_first_floor') - self.assertTrue(checkin_log.log_type == 'IN') From ceea328542d47c511ffcab80ad9ef12f14d861a4 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Sun, 12 May 2019 19:41:34 +0530 Subject: [PATCH 3/9] fix(HR): validations and bug fixes for Attendance Log --- erpnext/config/hr.py | 7 +++++++ .../employee_attendance_log/employee_attendance_log.py | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index 0f009b826f..a91e078c4d 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -34,6 +34,13 @@ def get_data(): "name": "Upload Attendance", "hide_count": True, "dependencies": ["Employee"] + }, + { + "type": "doctype", + "name": "Employee Attendance Log", + "hide_count": True, + "onboard": 1, + "dependencies": ["Employee"] }, ] }, 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 6603b1c5a4..956d9c3895 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py @@ -9,7 +9,9 @@ from frappe.model.document import Document from frappe import _ class EmployeeAttendanceLog(Document): - pass + 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.whitelist() @@ -37,6 +39,7 @@ def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, doc.time = timestamp doc.device_id = device_id doc.log_type = log_type - doc.save() - + doc.insert() + frappe.db.commit() + return doc From 23cedfd6c8edcdcbcd86156ca73e9d8f01765887 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Wed, 29 May 2019 14:27:08 +0530 Subject: [PATCH 4/9] feat(HR): Auto Attendance >Marking attendance based on Employee Attendance Log --- erpnext/hooks.py | 3 +- erpnext/hr/doctype/attendance/attendance.json | 9 + erpnext/hr/doctype/attendance/attendance.py | 14 + .../employee_attendance_log.json | 27 +- .../employee_attendance_log.py | 54 +- .../test_employee_attendance_log.py | 60 +- .../hr/doctype/holiday_list/holiday_list.py | 23 + .../doctype/holiday_list/test_holiday_list.py | 37 +- .../hr/doctype/hr_settings/hr_settings.json | 817 ++++-------------- erpnext/hr/doctype/hr_settings/hr_settings.py | 240 ++++- .../doctype/hr_settings/test_hr_settings.py | 69 +- .../shift_assignment/shift_assignment.py | 101 ++- erpnext/hr/doctype/shift_type/shift_type.json | 395 +++++---- 13 files changed, 1000 insertions(+), 849 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9502006265..acb44772f8 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -241,7 +241,8 @@ scheduler_events = { "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 7e1d5ed962..2a6d00544b 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 321fca7155..9bb8495e6e 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -88,3 +88,17 @@ def add_attendance(events, start, end, conditions=None): } 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 ccdb83d889..260eebdb94 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" ], @@ -48,13 +49,6 @@ "fieldtype": "Data", "label": "Location / Device ID" }, - { - "fieldname": "attendance_marked", - "fieldtype": "Link", - "label": "Attendance Marked", - "options": "Attendance", - "read_only": 1 - }, { "fieldname": "entry_grace_period_consequence", "fieldtype": "Check", @@ -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 956d9c3895..3932f8294b 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 @@ from frappe import _ 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 @@ def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, :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 @@ def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, 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 45c1353d51..fdc63d6678 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 e475e6e95b..453320fb27 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -84,3 +84,26 @@ def get_events(start, end, filters=None): 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 653ef2f40d..33a24d14f7 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 @@ from __future__ import unicode_literals 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 5502ce81e3..ac1d23ef41 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": "Example: SAL-{first_name}-{date_of_birth.year}
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": "Example: SAL-{first_name}-{date_of_birth.year}
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 78095b3086..afd90f7b58 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 @@ class HRSettings(Document): 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 2d5b18b6fb..367c83ac7c 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 @@ from __future__ import unicode_literals 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 48d4e570d8..c90894e6dd 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -6,7 +6,9 @@ from __future__ import unicode_literals 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 @@ def add_assignments(events, start, end, conditions=None): } 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 d5af2e46fc..1ee30b2d5d 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 From 66e459b35d882fe36e282651c5e3fbf4d45c5735 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Thu, 30 May 2019 16:12:34 +0530 Subject: [PATCH 5/9] fix(auto attendance): changes as requested in review > removed unused field from shift assignment > renamed 'biometric_rf_id' to 'attendance_device_id' > added more test cases > few other minor changes after demo --- .../hr/doctype/attendance/test_attendance.py | 11 +- erpnext/hr/doctype/employee/employee.json | 9 +- erpnext/hr/doctype/employee/employee.py | 4 +- .../employee_attendance_log.py | 16 +- .../test_employee_attendance_log.py | 10 +- .../hr/doctype/holiday_list/holiday_list.py | 23 - .../doctype/holiday_list/test_holiday_list.py | 48 +- erpnext/hr/doctype/hr_settings/hr_settings.py | 15 +- .../shift_assignment/shift_assignment.json | 521 ++++-------------- .../shift_assignment/shift_assignment.py | 6 +- erpnext/hr/doctype/shift_type/shift_type.json | 56 +- 11 files changed, 202 insertions(+), 517 deletions(-) diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py index 7fe020a18e..35d1126dc1 100644 --- a/erpnext/hr/doctype/attendance/test_attendance.py +++ b/erpnext/hr/doctype/attendance/test_attendance.py @@ -4,8 +4,17 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import nowdate test_records = frappe.get_test_records('Attendance') class TestAttendance(unittest.TestCase): - pass + def test_mark_absent(self): + from erpnext.hr.doctype.employee.test_employee import make_employee + employee = make_employee("test_mark_absent@example.com") + date = nowdate() + frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date}) + from erpnext.hr.doctype.attendance.attendance import mark_absent + attendance = mark_absent(employee, date) + fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'}) + self.assertEqual(attendance, fetch_attendance) diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index d1384188e0..188d460fb3 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -16,7 +16,7 @@ "middle_name", "last_name", "employee_name", - "biometric_rf_id", + "attendance_device_id", "image", "column_break1", "company", @@ -511,6 +511,7 @@ "options": "Email" }, { + "default": "0", "fieldname": "unsubscribed", "fieldtype": "Check", "label": "Unsubscribed" @@ -749,9 +750,9 @@ "label": "Old Parent" }, { - "fieldname": "biometric_rf_id", + "fieldname": "attendance_device_id", "fieldtype": "Data", - "label": "Biometric/RF tag ID ", + "label": "Attendance Device ID (Biometric/RF tag ID)", "no_copy": 1, "unique": 1 } @@ -759,7 +760,7 @@ "icon": "fa fa-user", "idx": 24, "image_field": "image", - "modified": "2019-05-08 14:32:06.443825", + "modified": "2019-05-29 17:33:11.988538", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 48957e5819..63ba57385a 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -319,12 +319,12 @@ def get_holiday_list_for_employee(employee, raise_exception=True): return holiday_list -def is_holiday(employee, date=None): +def is_holiday(employee, date=None, raise_exception=True): '''Returns True if given Employee has an holiday on the given date :param employee: Employee `name` :param date: Date to check. Will check for today if None''' - holiday_list = get_holiday_list_for_employee(employee) + holiday_list = get_holiday_list_for_employee(employee, raise_exception) if not date: date = today() 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 3932f8294b..b25f9b9aac 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py @@ -15,23 +15,24 @@ class EmployeeAttendanceLog(Document): @frappe.whitelist() -def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, log_type=None): - """Finds the relevant Employee using the biometric_rf_id and creates a Employee Attendance Log. +def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, employee_fieldname='attendance_device_id'): + """Finds the relevant Employee using the employee field value and creates a Employee Attendance Log. - :param biometric_rf_id: The Biometric/RF tag ID as set up in Employee DocType. + :param employee_field_value: The value to look for in employee field. :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 employee_fieldname: (Default: attendance_device_id)Name of the field in Employee DocType based on which employee lookup will happen. """ - if not biometric_rf_id or not timestamp: - frappe.throw(_("'biometric_rf_id' and 'timestamp' are required.")) + if not employee_field_value or not timestamp: + frappe.throw(_("'employee_field_value' 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) + employee = frappe.db.get_values("Employee", {employee_fieldname: employee_field_value}, ["name", "employee_name", employee_fieldname], as_dict=True) if employee: employee = employee[0] else: - frappe.throw(_("No Employee found for the given 'biometric_rf_id':{}.").format(biometric_rf_id)) + frappe.throw(_("No Employee found for the given employee field value. '{}': {}").format(employee_fieldname,employee_field_value)) doc = frappe.new_doc("Employee Attendance Log") doc.employee = employee.name @@ -40,7 +41,6 @@ def add_log_based_on_biometric_rf_id(biometric_rf_id, timestamp, device_id=None, doc.device_id = device_id doc.log_type = log_type doc.insert() - frappe.db.commit() return doc 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 fdc63d6678..d1fcf0c55f 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 @@ -8,18 +8,18 @@ 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_attendance_log.employee_attendance_log import add_log_based_on_employee_field, 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 = make_employee("test_add_log_based_on_biometric_rf_id@example.com") + def test_add_log_based_on_employee_field(self): + employee = make_employee("test_add_log_based_on_employee_field@example.com") employee = frappe.get_doc("Employee", employee) - employee.biometric_rf_id = '3344' + employee.attendance_device_id = '3344' employee.save() time_now = now_datetime().__str__()[:-7] - employee_attendance_log = add_log_based_on_biometric_rf_id('3344', time_now, 'mumbai_first_floor', 'IN') + employee_attendance_log = add_log_based_on_employee_field('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') diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 453320fb27..e475e6e95b 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -84,26 +84,3 @@ def get_events(start, end, filters=None): 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 33a24d14f7..2d2cc0b848 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -6,39 +6,27 @@ 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): - 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 test_holiday_list(self): + today_date = getdate() + test_holiday_dates = [today_date-timedelta(days=5), today_date-timedelta(days=4)] + holiday_list = make_holiday_list("test_is_holiday", + holiday_dates=[ + {'holiday_date': test_holiday_dates[0], 'description': 'test holiday'}, + {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'} + ]) + fetched_holiday_list = frappe.get_value('Holiday List', holiday_list.name) + self.assertEqual(holiday_list.name, fetched_holiday_list) 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) + frappe.delete_doc_if_exists("Holiday List", name, force=1) + doc = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": name, + "from_date" : from_date, + "to_date" : to_date, + "holidays" : holiday_dates + }).insert() return doc diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index afd90f7b58..9511ff1717 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -12,7 +12,7 @@ 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.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.attendance.attendance import mark_absent class HRSettings(Document): @@ -79,9 +79,9 @@ def process_single_employee_logs(logs, hr_settings=None): 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) + mark_absent_for_dates_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): +def mark_absent_for_dates_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. @@ -100,7 +100,7 @@ def mark_absent_for_days_with_no_attendance(employee, employee_last_sync, hr_set 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)): + for date in get_filtered_date_list(employee, "All Dates", start_date, employee_last_sync.date(), True, get_holiday_list_for_employee(employee, False)): mark_absent(employee, date) return else: @@ -111,7 +111,7 @@ def mark_absent_for_days_with_no_attendance(employee, employee_last_sync, hr_set 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)): + for date in get_filtered_date_list(employee, "All Dates", start_date, employee_last_sync.date(), True, get_holiday_list_for_employee(employee, False)): mark_absent(employee, date) else: for date in get_filtered_date_list(employee, "Assigned Shifts", start_date, end_date): @@ -155,9 +155,12 @@ def get_filtered_date_list(employee, base_dates_set, start_date, end_date, filte 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. + 1. These logs belongs to an single shift, single employee and is not in a holiday date. 2. Logs are in chronological order """ + if shift_details.shift_type.enable_auto_attendance: + mark_attendance_and_link_log(logs, 'Skip', None) + return 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) diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.json b/erpnext/hr/doctype/shift_assignment/shift_assignment.json index 437e4d7eef..d4cd1c4c93 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.json +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.json @@ -1,429 +1,132 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "HR-SHA-.YY.-.MM.-.#####", - "beta": 0, - "creation": "2018-04-13 16:25:04.562730", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "allow_import": 1, + "autoname": "HR-SHA-.YY.-.MM.-.#####", + "creation": "2018-04-13 16:25:04.562730", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "shift_type", + "column_break_3", + "company", + "date", + "shift_request", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "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": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "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": "Employee Name", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shift_type", - "fieldtype": "Link", - "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": "Shift Type", - "length": 0, - "no_copy": 0, - "options": "Shift Type", - "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": "shift_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Shift Type", + "options": "Shift Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "present", - "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": "Present", - "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_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "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": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "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": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "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": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "date", - "fieldtype": "Date", - "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": "Date", - "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": "shift_request", + "fieldtype": "Link", + "label": "Shift Request", + "options": "Shift Request", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shift_request", - "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": "Shift Request", - "length": 0, - "no_copy": 0, - "options": "Shift Request", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Shift Assignment", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Shift Assignment", + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:41.155464", - "modified_by": "Administrator", - "module": "HR", - "name": "Shift Assignment", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "modified": "2019-05-30 15:40:54.418427", + "modified_by": "Administrator", + "module": "HR", + "name": "Shift Assignment", + "owner": "Administrator", "permissions": [ { - "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": 1, - "cancel": 1, - "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": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, "write": 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": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "employee_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "employee_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index c90894e6dd..ac8c8f2b89 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document 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 erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, is_holiday from datetime import timedelta, datetime class OverlapError(frappe.ValidationError): pass @@ -97,8 +97,8 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals 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): + holiday_list_name = get_holiday_list_for_employee(employee, False) + if holiday_list_name and is_holiday(holiday_list_name, for_date, False): shift_type_name = None if not shift_type_name and next_shift_direction: diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 1ee30b2d5d..0c295c625e 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -7,10 +7,10 @@ "field_order": [ "start_time", "end_time", - "disable_auto_attendance_for_this_shift", "column_break_3", "holiday_list", - "auto_attendance_configurations_section", + "enable_auto_attendance", + "auto_attendance_settings_section", "determine_check_in_and_check_out", "working_hours_calculation_based_on", "begin_check_in_before_shift_start_time", @@ -18,7 +18,7 @@ "column_break_10", "working_hours_threshold_for_half_day", "working_hours_threshold_for_absent", - "grace_period_configuration_auto_attendance_section", + "grace_period_settings_auto_attendance_section", "enable_entry_grace_period", "late_entry_grace_period", "consequence_after", @@ -86,33 +86,14 @@ "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", + "default": "60", "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" - }, - { + "default": "0", "fieldname": "enable_entry_grace_period", "fieldtype": "Check", "label": "Enable Entry Grace Period" @@ -144,11 +125,13 @@ "fieldtype": "Column Break" }, { + "default": "0", "fieldname": "enable_exit_grace_period", "fieldtype": "Check", "label": "Enable Exit Grace Period" }, { + "default": "0", "depends_on": "enable_exit_grace_period", "fieldname": "enable_different_consequence_for_early_exit", "fieldtype": "Check", @@ -177,13 +160,34 @@ "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)", + "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", "fieldtype": "Int", "label": "Allow check-out after shift end time (in minutes)" + }, + { + "depends_on": "enable_auto_attendance", + "fieldname": "auto_attendance_settings_section", + "fieldtype": "Section Break", + "label": "Auto Attendance Settings" + }, + { + "depends_on": "enable_auto_attendance", + "fieldname": "grace_period_settings_auto_attendance_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Grace Period Settings For Auto Attendance" + }, + { + "default": "0", + "description": "Mark attendance based on 'Employee Attendance Log' for Employees assigned to this shift.", + "fieldname": "enable_auto_attendance", + "fieldtype": "Check", + "label": "Enable Auto Attendance" } ], - "modified": "2019-05-16 18:57:00.150899", + "modified": "2019-05-30 15:31:35.594990", "modified_by": "Administrator", "module": "HR", "name": "Shift Type", From e0c517638349e845602bd7c1cc7f46a962d301e2 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Mon, 10 Jun 2019 10:24:27 +0530 Subject: [PATCH 6/9] refactor(HR): Auto Attendance > moved all auto attendance settings from HR Settings to shift > added shift in attendance and Employee Attendance Log > reordered and cleaned up fields in HR Settings and Employee DocType --- erpnext/hooks.py | 2 +- erpnext/hr/doctype/attendance/attendance.json | 7 + erpnext/hr/doctype/attendance/attendance.py | 7 +- .../daily_work_summary_group.py | 12 +- erpnext/hr/doctype/employee/employee.json | 30 ++- .../employee_attendance_log.json | 46 +++- .../employee_attendance_log.py | 87 ++++++- .../test_employee_attendance_log.py | 38 ++- .../hr/doctype/holiday_list/holiday_list.py | 12 +- .../doctype/holiday_list/test_holiday_list.py | 2 +- .../hr/doctype/hr_settings/hr_settings.json | 72 ++---- erpnext/hr/doctype/hr_settings/hr_settings.py | 242 ------------------ .../doctype/hr_settings/test_hr_settings.py | 66 +---- .../shift_assignment/shift_assignment.py | 38 ++- erpnext/hr/doctype/shift_type/shift_type.json | 16 +- erpnext/hr/doctype/shift_type/shift_type.py | 109 +++++++- erpnext/projects/doctype/project/project.py | 4 +- .../projects/doctype/timesheet/timesheet.py | 3 - 18 files changed, 380 insertions(+), 413 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index acb44772f8..bd64023dc9 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -242,7 +242,7 @@ scheduler_events = { "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.hr.doctype.hr_settings.hr_settings.make_attendance_from_employee_attendance_log" + "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts" ], "daily": [ "erpnext.stock.reorder_item.reorder_item", diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index 2a6d00544b..eb38147a98 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -17,6 +17,7 @@ "attendance_date", "company", "department", + "shift", "attendance_request", "amended_from" ], @@ -146,6 +147,12 @@ "label": "Working Hours", "precision": "1", "read_only": 1 + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Shift Type" } ], "icon": "fa fa-ok", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 9bb8495e6e..b8081128ff 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -89,15 +89,16 @@ def add_attendance(events, start, end, conditions=None): if e not in events: events.append(e) -def mark_absent(employee, attendance_date): +def mark_absent(employee, attendance_date, shift=None): employee_doc = frappe.get_doc('Employee', employee) - if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date}): + if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): doc_dict = { 'doctype': 'Attendance', 'employee': employee, 'attendance_date': attendance_date, 'status': 'Absent', - 'company': employee_doc.company + 'company': employee_doc.company, + 'shift': shift } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() diff --git a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py index 67070bfa53..ece331aa71 100644 --- a/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py +++ b/erpnext/hr/doctype/daily_work_summary_group/daily_work_summary_group.py @@ -8,6 +8,7 @@ from frappe.model.document import Document import frappe.utils from frappe import _ from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_user_emails_from_group +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday class DailyWorkSummaryGroup(Document): def validate(self): @@ -23,7 +24,7 @@ def trigger_emails(): for d in groups: group_doc = frappe.get_doc("Daily Work Summary Group", d) if (is_current_hour(group_doc.send_emails_at) - and not is_holiday_today(group_doc.holiday_list) + and not is_holiday(group_doc.holiday_list) and group_doc.enabled): emails = get_user_emails_from_group(group_doc) # find emails relating to a company @@ -38,15 +39,6 @@ def is_current_hour(hour): return frappe.utils.nowtime().split(':')[0] == hour.split(':')[0] -def is_holiday_today(holiday_list): - date = frappe.utils.today() - if holiday_list: - return frappe.get_all('Holiday List', - dict(name=holiday_list, holiday_date=date)) and True or False - else: - return False - - def send_summary(): '''Send summary to everyone''' for d in frappe.get_all('Daily Work Summary', dict(status='Open')): diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 188d460fb3..5202218ed3 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -16,7 +16,6 @@ "middle_name", "last_name", "employee_name", - "attendance_device_id", "image", "column_break1", "company", @@ -49,9 +48,12 @@ "column_break_31", "grade", "branch", - "organization_profile", + "attendance_and_leave_details", "leave_policy", + "attendance_device_id", + "column_break_44", "holiday_list", + "default_shift", "salary_information", "salary_mode", "bank_name", @@ -399,12 +401,6 @@ "oldfieldtype": "Link", "options": "Branch" }, - { - "collapsible": 1, - "fieldname": "organization_profile", - "fieldtype": "Section Break", - "label": "Leave Details" - }, { "fieldname": "leave_policy", "fieldtype": "Link", @@ -755,12 +751,28 @@ "label": "Attendance Device ID (Biometric/RF tag ID)", "no_copy": 1, "unique": 1 + }, + { + "collapsible": 1, + "fieldname": "attendance_and_leave_details", + "fieldtype": "Section Break", + "label": "Attendance and Leave Details" + }, + { + "fieldname": "column_break_44", + "fieldtype": "Column Break" + }, + { + "fieldname": "default_shift", + "fieldtype": "Link", + "label": "Default Shift", + "options": "Shift Type" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", - "modified": "2019-05-29 17:33:11.988538", + "modified": "2019-06-01 16:05:55.132180", "modified_by": "Administrator", "module": "HR", "name": "Employee", 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 260eebdb94..f3f4861c17 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json @@ -8,13 +8,18 @@ "employee", "employee_name", "log_type", + "shift", "column_break_4", "time", "device_id", "skip_auto_attendance", "attendance", "entry_grace_period_consequence", - "exit_grace_period_consequence" + "exit_grace_period_consequence", + "shift_start", + "shift_end", + "shift_actual_start", + "shift_actual_end" ], "fields": [ { @@ -50,12 +55,14 @@ "label": "Location / Device ID" }, { + "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, @@ -68,10 +75,10 @@ "options": "\nIN\nOUT" }, { + "default": "0", "fieldname": "skip_auto_attendance", "fieldtype": "Check", - "label": "Skip Auto Attendance", - "read_only": 1 + "label": "Skip Auto Attendance" }, { "fieldname": "attendance", @@ -79,9 +86,40 @@ "label": "Attendance Marked", "options": "Attendance", "read_only": 1 + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Shift Type", + "read_only": 1 + }, + { + "fieldname": "shift_start", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Shift Start" + }, + { + "fieldname": "shift_end", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Shift End" + }, + { + "fieldname": "shift_actual_start", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Shift Actual Start" + }, + { + "fieldname": "shift_actual_end", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Shift Actual End" } ], - "modified": "2019-05-24 13:40:01.287808", + "modified": "2019-06-06 23:09:37.766717", "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 b25f9b9aac..7ad135674d 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py +++ b/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py @@ -4,24 +4,48 @@ from __future__ import unicode_literals import frappe -from frappe.utils import now +from frappe.utils import now, cint, get_datetime from frappe.model.document import Document from frappe import _ +from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift + 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.')) + self.validate_duplicate_log() + self.fetch_shift() + def validate_duplicate_log(self): + doc = frappe.db.exists('Employee Attendance Log', { + 'employee': self.employee, + 'time': self.time, + 'name': ['!=', self.name]}) + if doc: + doc_link = frappe.get_desk_link('Employee Attendance Log', doc) + frappe.throw(_('This employee already has a log with the same timestamp.{0}') + .format("
" + doc_link)) + + def fetch_shift(self): + shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True) + if shift_actual_timings[0] and shift_actual_timings[1]: + if not self.attendance: + self.shift = shift_actual_timings[2].shift_type.name + self.shift_actual_start = shift_actual_timings[0] + self.shift_actual_end = shift_actual_timings[1] + self.shift_start = shift_actual_timings[2].start_datetime + self.shift_end = shift_actual_timings[2].end_datetime + else: + self.shift = None @frappe.whitelist() -def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, employee_fieldname='attendance_device_id'): +def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'): """Finds the relevant Employee using the employee field value and creates a Employee Attendance Log. :param employee_field_value: The value to look for in employee field. :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 skip_auto_attendance: (optional)Skip auto attendance field will be set for this log(0/1). :param employee_fieldname: (Default: attendance_device_id)Name of the field in Employee DocType based on which employee lookup will happen. """ @@ -40,12 +64,13 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N doc.time = timestamp doc.device_id = device_id doc.log_type = log_type + if cint(skip_auto_attendance) == 1: doc.skip_auto_attendance = '1' doc.insert() - + return doc -def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, company=None): +def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, shift=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. @@ -63,14 +88,15 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki 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}): + if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): doc_dict = { 'doctype': 'Attendance', 'employee': employee, 'attendance_date': attendance_date, 'status': attendance_status, 'working_hours': working_hours, - 'company': employee_doc.company + 'company': employee_doc.company, + 'shift': shift } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() @@ -85,3 +111,48 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki return None else: frappe.throw(_('{} is an invalid Attendance Status.').format(attendance_status)) + + +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 taken as IN, Last log always taken as 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) 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 d1fcf0c55f..2e4868b386 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 @@ -8,7 +8,7 @@ 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_employee_field, mark_attendance_and_link_log +from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import add_log_based_on_employee_field, mark_attendance_and_link_log, calculate_working_hours from erpnext.hr.doctype.employee.test_employee import make_employee class TestEmployeeAttendanceLog(unittest.TestCase): @@ -44,6 +44,42 @@ class TestEmployeeAttendanceLog(unittest.TestCase): 'employee':employee, 'attendance_date':now_date}) self.assertEqual(attendance_count, 1) + 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] + + 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) 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))] diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index e475e6e95b..8c7b6f723f 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe import json -from frappe.utils import cint, getdate, formatdate +from frappe.utils import cint, getdate, formatdate, today from frappe import throw, _ from frappe.model.document import Document @@ -84,3 +84,13 @@ def get_events(start, end, filters=None): fields=['name', '`tabHoliday`.holiday_date', '`tabHoliday`.description', '`tabHoliday List`.color'], filters = filters, update={"allDay": 1}) + + +def is_holiday(holiday_list, date=today()): + """Returns true if the given date is a holiday in the given holiday list + """ + if holiday_list: + return bool(frappe.get_all('Holiday List', + dict(name=holiday_list, holiday_date=date))) + else: + return False diff --git a/erpnext/hr/doctype/holiday_list/test_holiday_list.py b/erpnext/hr/doctype/holiday_list/test_holiday_list.py index 2d2cc0b848..64bed6637b 100644 --- a/erpnext/hr/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/test_holiday_list.py @@ -12,7 +12,7 @@ class TestHolidayList(unittest.TestCase): def test_holiday_list(self): today_date = getdate() test_holiday_dates = [today_date-timedelta(days=5), today_date-timedelta(days=4)] - holiday_list = make_holiday_list("test_is_holiday", + holiday_list = make_holiday_list("test_holiday_list", holiday_dates=[ {'holiday_date': test_holiday_dates[0], 'description': 'test holiday'}, {'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'} diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index ac1d23ef41..3f5a2ab333 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -7,28 +7,22 @@ "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", + "max_working_hours_against_timesheet", + "column_break_11", "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" + "leave_approval_notification_template", + "leave_status_notification_template", + "column_break_18", + "leave_approver_mandatory_in_leave_application", + "show_leaves_of_all_department_members_in_calendar" ], "fields": [ { @@ -67,16 +61,12 @@ "fieldtype": "Column Break" }, { + "default": "0", "description": "Don't send Employee Birthday Reminders", "fieldname": "stop_birthday_reminders", "fieldtype": "Check", "label": "Stop Birthday Reminders" }, - { - "fieldname": "maintain_bill_work_hours_same", - "fieldtype": "Check", - "label": "Maintain Billing Hours and Working Hours Same on Timesheet" - }, { "default": "1", "fieldname": "leave_approver_mandatory_in_leave_application", @@ -95,6 +85,7 @@ "label": "Payroll Settings" }, { + "default": "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", @@ -108,6 +99,7 @@ "label": "Email Salary Slip to Employee" }, { + "default": "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", @@ -133,58 +125,24 @@ "label": "Leave Settings" }, { + "default": "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", + "fieldname": "column_break_11", "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" + "fieldname": "column_break_18", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-05-22 12:50:40.189766", + "modified": "2019-05-31 16:18:50.245872", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index 9511ff1717..2ee1b7bdda 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -5,15 +5,8 @@ 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.employee.employee import get_holiday_list_for_employee -from erpnext.hr.doctype.attendance.attendance import mark_absent class HRSettings(Document): def validate(self): @@ -29,238 +22,3 @@ class HRSettings(Document): 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")) - - -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_dates_with_no_attendance(last_log.employee, employee_last_sync, hr_settings) - -def mark_absent_for_dates_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_for_employee(employee, False)): - 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_for_employee(employee, False)): - 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 date. - 2. Logs are in chronological order - """ - if shift_details.shift_type.enable_auto_attendance: - mark_attendance_and_link_log(logs, 'Skip', None) - return - 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 367c83ac7c..b0b07b0c0b 100644 --- a/erpnext/hr/doctype/hr_settings/test_hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/test_hr_settings.py @@ -10,68 +10,4 @@ from frappe.utils import now_datetime from datetime import timedelta class TestHRSettings(unittest.TestCase): - 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) - - - + pass diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index ac8c8f2b89..5b732c4b99 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -7,7 +7,8 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate -from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, is_holiday +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from datetime import timedelta, datetime class OverlapError(frappe.ValidationError): pass @@ -90,7 +91,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals :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') + default_shift = frappe.db.get_value('Employee', employee, '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 @@ -98,7 +99,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals 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_for_employee(employee, False) - if holiday_list_name and is_holiday(holiday_list_name, for_date, False): + 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: @@ -114,7 +115,7 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals break else: direction = '<' if next_shift_direction == 'reverse' else '>' - dates = frappe.db.get_list('Shift Assignment', + dates = frappe.db.get_all('Shift Assignment', 'date', {'employee':employee, 'date':(direction, for_date), 'docstatus': '1'}, as_list=True, @@ -177,3 +178,32 @@ def get_shift_details(shift_type_name, for_date=nowdate()): 'actual_start': actual_start, 'actual_end': actual_end }) + + +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) + 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 diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 0c295c625e..7610ebef5e 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -18,6 +18,8 @@ "column_break_10", "working_hours_threshold_for_half_day", "working_hours_threshold_for_absent", + "process_attendance_after", + "last_sync_of_attendance_log", "grace_period_settings_auto_attendance_section", "enable_entry_grace_period", "late_entry_grace_period", @@ -185,9 +187,21 @@ "fieldname": "enable_auto_attendance", "fieldtype": "Check", "label": "Enable Auto Attendance" + }, + { + "description": "Attendance will be marked automatically only after this date.", + "fieldname": "process_attendance_after", + "fieldtype": "Date", + "label": "Process Attendance After" + }, + { + "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" } ], - "modified": "2019-05-30 15:31:35.594990", + "modified": "2019-05-31 16:02:44.272036", "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 88ae243dda..0baffe0efb 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -3,8 +3,115 @@ # For license information, please see license.txt from __future__ import unicode_literals +import itertools +from datetime import timedelta + import frappe from frappe.model.document import Document +from frappe.utils import cint, getdate +from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift +from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import mark_attendance_and_link_log, calculate_working_hours +from erpnext.hr.doctype.attendance.attendance import mark_absent +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class ShiftType(Document): - pass \ No newline at end of file + def process_auto_attendance(self): + if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_attendance_log: + return + filters = { + 'skip_auto_attendance':'0', + 'attendance':('is', 'not set'), + 'time':('>=', self.process_attendance_after), + 'shift_actual_start': ('<', self.last_sync_of_attendance_log), + 'shift': self.name + } + logs = frappe.db.get_list('Employee Attendance Log', 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) + for employee in self.get_assigned_employee(self.process_attendance_after, True): + self.mark_absent_for_dates_with_no_attendance(employee) + + def get_attendance(self, logs): + """Return attendance_status, working_hours 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 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) + if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: + return 'Absent', total_working_hours + 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 + + 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. + The Absent is marked starting from 'process_attendance_after' or employee creation date. + """ + date_of_joining, relieving_date, employee_creation = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date", "creation"]) + if not date_of_joining: + date_of_joining = employee_creation.date() + start_date = max(self.process_attendance_after, date_of_joining) + actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, self.last_sync_of_attendance_log, True) + last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else self.last_sync_of_attendance_log + prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse') + if prev_shift: + end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date() + else: + return + holiday_list_name = self.holiday_list + if not holiday_list_name: + holiday_list_name = get_holiday_list_for_employee(employee, False) + for date in get_filtered_date_list(employee, start_date, end_date, holiday_list=holiday_list_name): + shift_details = get_employee_shift(employee, date, True) + if shift_details and shift_details.shift_type.name == self.name: + mark_absent(employee, date) + + def get_assigned_employee(self, from_date=None, consider_default_shift=False): + filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'} + if not from_date: + del filters['date'] + assigned_employees = frappe.get_all('Shift Assignment', 'employee', filters, as_list=True) + assigned_employees = [x[0] for x in assigned_employees] + + if consider_default_shift: + filters = {'default_shift': self.name} + default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True) + default_shift_employees = [x[0] for x in default_shift_employees] + return list(set(assigned_employees+default_shift_employees)) + return assigned_employees + +def process_auto_attendance_for_all_shifts(): + shift_list = frappe.get_all('Shift Type', 'name', {'enable_auto_attendance':'1'}, as_list=True) + for shift in shift_list: + doc = frappe.get_doc('Shift Type', shift[0]) + doc.process_auto_attendance() + +def get_filtered_date_list(employee, start_date, end_date, filter_attendance=True, holiday_list=None): + """Returns a list of dates after removing the dates with attendance and holidays + """ + 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""" + 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] diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 74e70a9276..75847d5167 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -11,7 +11,7 @@ from frappe.utils import (flt, getdate, get_url, now, from erpnext.controllers.queries import get_filters_cond from frappe.desk.reportview import get_match_cond from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email -from erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group import is_holiday_today +from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from frappe.model.document import Document class Project(Document): @@ -530,7 +530,7 @@ def get_projects_for_collect_progress(frequency, fields): def send_project_update_email_to_users(project): doc = frappe.get_doc('Project', project) - if is_holiday_today(doc.holiday_list) or not doc.users: return + if is_holiday(doc.holiday_list) or not doc.users: return project_update = frappe.get_doc({ "doctype" : "Project Update", diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index 8ff64a792d..df9a6baf38 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -19,9 +19,6 @@ class OverlapError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass class Timesheet(Document): - def onload(self): - self.get("__onload").maintain_bill_work_hours_same = frappe.db.get_single_value('HR Settings', 'maintain_bill_work_hours_same') - def validate(self): self.set_employee_name() self.set_status() From 32197355add09ff4626dc704bc02a42f003d8b85 Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Mon, 10 Jun 2019 13:04:44 +0530 Subject: [PATCH 7/9] rename(HR): Employee Attendance Log DocType to Employee Checkin --- erpnext/config/hr.py | 2 +- .../__init__.py | 0 .../employee_checkin.js} | 2 +- .../employee_checkin.json} | 58 +++++++++---------- .../employee_checkin.py} | 27 ++++----- .../test_employee_checkin.py} | 36 ++++++------ erpnext/hr/doctype/shift_type/shift_type.json | 14 ++--- erpnext/hr/doctype/shift_type/shift_type.py | 12 ++-- 8 files changed, 76 insertions(+), 75 deletions(-) rename erpnext/hr/doctype/{employee_attendance_log => employee_checkin}/__init__.py (100%) rename erpnext/hr/doctype/{employee_attendance_log/employee_attendance_log.js => employee_checkin/employee_checkin.js} (77%) rename erpnext/hr/doctype/{employee_attendance_log/employee_attendance_log.json => employee_checkin/employee_checkin.json} (94%) rename erpnext/hr/doctype/{employee_attendance_log/employee_attendance_log.py => employee_checkin/employee_checkin.py} (91%) rename erpnext/hr/doctype/{employee_attendance_log/test_employee_attendance_log.py => employee_checkin/test_employee_checkin.py} (70%) diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py index a91e078c4d..607a5fd135 100644 --- a/erpnext/config/hr.py +++ b/erpnext/config/hr.py @@ -37,7 +37,7 @@ def get_data(): }, { "type": "doctype", - "name": "Employee Attendance Log", + "name": "Employee Checkin", "hide_count": True, "onboard": 1, "dependencies": ["Employee"] diff --git a/erpnext/hr/doctype/employee_attendance_log/__init__.py b/erpnext/hr/doctype/employee_checkin/__init__.py similarity index 100% rename from erpnext/hr/doctype/employee_attendance_log/__init__.py rename to erpnext/hr/doctype/employee_checkin/__init__.py diff --git a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js b/erpnext/hr/doctype/employee_checkin/employee_checkin.js similarity index 77% rename from erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js rename to erpnext/hr/doctype/employee_checkin/employee_checkin.js index 55021f6735..f11cc9b252 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.js +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.js @@ -1,7 +1,7 @@ // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Employee Attendance Log', { +frappe.ui.form.on('Employee Checkin', { // refresh: function(frm) { // } diff --git a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json similarity index 94% rename from erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json rename to erpnext/hr/doctype/employee_checkin/employee_checkin.json index f3f4861c17..d340527538 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.json +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json @@ -1,7 +1,7 @@ { "allow_import": 1, - "autoname": "ATT-LOG-.MM.-.YYYY.-.######", - "creation": "2019-04-25 10:17:11.225671", + "autoname": "ATT-CKIN-.MM.-.YYYY.-.######", + "creation": "2019-06-10 11:56:34.536413", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -37,6 +37,19 @@ "label": "Employee Name", "read_only": 1 }, + { + "fieldname": "log_type", + "fieldtype": "Select", + "label": "Log Type", + "options": "\nIN\nOUT" + }, + { + "fieldname": "shift", + "fieldtype": "Link", + "label": "Shift", + "options": "Shift Type", + "read_only": 1 + }, { "fieldname": "column_break_4", "fieldtype": "Column Break" @@ -54,26 +67,6 @@ "fieldtype": "Data", "label": "Location / Device ID" }, - { - "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": "log_type", - "fieldtype": "Select", - "label": "Log Type", - "options": "\nIN\nOUT" - }, { "default": "0", "fieldname": "skip_auto_attendance", @@ -88,11 +81,18 @@ "read_only": 1 }, { - "fieldname": "shift", - "fieldtype": "Link", - "label": "Shift", - "options": "Shift Type", - "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", @@ -119,10 +119,10 @@ "label": "Shift Actual End" } ], - "modified": "2019-06-06 23:09:37.766717", + "modified": "2019-06-10 11:56:34.536413", "modified_by": "Administrator", "module": "HR", - "name": "Employee Attendance Log", + "name": "Employee Checkin", "owner": "Administrator", "permissions": [ { diff --git a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py similarity index 91% rename from erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py rename to erpnext/hr/doctype/employee_checkin/employee_checkin.py index 7ad135674d..997897b554 100644 --- a/erpnext/hr/doctype/employee_attendance_log/employee_attendance_log.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -10,18 +10,18 @@ from frappe import _ from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift -class EmployeeAttendanceLog(Document): +class EmployeeCheckin(Document): def validate(self): self.validate_duplicate_log() self.fetch_shift() def validate_duplicate_log(self): - doc = frappe.db.exists('Employee Attendance Log', { + doc = frappe.db.exists('Employee Checkin', { 'employee': self.employee, 'time': self.time, 'name': ['!=', self.name]}) if doc: - doc_link = frappe.get_desk_link('Employee Attendance Log', doc) + doc_link = frappe.get_desk_link('Employee Checkin', doc) frappe.throw(_('This employee already has a log with the same timestamp.{0}') .format("
" + doc_link)) @@ -39,7 +39,7 @@ class EmployeeAttendanceLog(Document): @frappe.whitelist() def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=None, log_type=None, skip_auto_attendance=0, employee_fieldname='attendance_device_id'): - """Finds the relevant Employee using the employee field value and creates a Employee Attendance Log. + """Finds the relevant Employee using the employee field value and creates a Employee Checkin. :param employee_field_value: The value to look for in employee field. :param timestamp: The timestamp of the Log. Currently expected in the following format as string: '2019-05-08 10:48:08.000000' @@ -58,7 +58,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N else: frappe.throw(_("No Employee found for the given employee field value. '{}': {}").format(employee_fieldname,employee_field_value)) - doc = frappe.new_doc("Employee Attendance Log") + doc = frappe.new_doc("Employee Checkin") doc.employee = employee.name doc.employee_name = employee.employee_name doc.time = timestamp @@ -71,10 +71,10 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, shift=None): - """Creates an attendance and links the attendance to the Employee Attendance Log. + """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. - :param logs: The List of 'Employee Attendance Log'. + :param logs: The List of 'Employee Checkin'. :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. @@ -82,7 +82,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki log_names = [x.name for x in logs] employee = logs[0].employee if attendance_status == 'Skip': - frappe.db.sql("""update `tabEmployee Attendance Log` + frappe.db.sql("""update `tabEmployee Checkin` set skip_auto_attendance = %s where name in %s""", ('1', log_names)) return None @@ -100,12 +100,12 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki } attendance = frappe.get_doc(doc_dict).insert() attendance.submit() - frappe.db.sql("""update `tabEmployee Attendance Log` + frappe.db.sql("""update `tabEmployee Checkin` set attendance = %s where name in %s""", (attendance.name, log_names)) return attendance else: - frappe.db.sql("""update `tabEmployee Attendance Log` + frappe.db.sql("""update `tabEmployee Checkin` set skip_auto_attendance = %s where name in %s""", ('1', log_names)) return None @@ -117,8 +117,8 @@ 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 logs: The List of 'Employee Checkin'. + :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' """ total_hours = 0 @@ -131,7 +131,7 @@ def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): 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': + elif check_in_out_type == 'Strictly based on Log Type in Employee Checkin': 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')] @@ -156,3 +156,4 @@ def time_diff_in_hours(start, end): def find_index_in_dict(dict_list, key, value): return next((index for (index, d) in enumerate(dict_list) if d[key] == value), None) + diff --git a/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py similarity index 70% rename from erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py rename to erpnext/hr/doctype/employee_checkin/test_employee_checkin.py index 2e4868b386..424d1a3c1b 100644 --- a/erpnext/hr/doctype/employee_attendance_log/test_employee_attendance_log.py +++ b/erpnext/hr/doctype/employee_checkin/test_employee_checkin.py @@ -8,10 +8,10 @@ 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_employee_field, mark_attendance_and_link_log, calculate_working_hours +from erpnext.hr.doctype.employee_checkin.employee_checkin import add_log_based_on_employee_field, mark_attendance_and_link_log, calculate_working_hours from erpnext.hr.doctype.employee.test_employee import make_employee -class TestEmployeeAttendanceLog(unittest.TestCase): +class TestEmployeeCheckin(unittest.TestCase): def test_add_log_based_on_employee_field(self): employee = make_employee("test_add_log_based_on_employee_field@example.com") employee = frappe.get_doc("Employee", employee) @@ -19,26 +19,26 @@ class TestEmployeeAttendanceLog(unittest.TestCase): employee.save() time_now = now_datetime().__str__()[:-7] - employee_attendance_log = add_log_based_on_employee_field('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') + employee_checkin = add_log_based_on_employee_field('3344', time_now, 'mumbai_first_floor', 'IN') + self.assertEqual(employee_checkin.employee, employee.name) + self.assertEqual(employee_checkin.time, time_now) + self.assertEqual(employee_checkin.device_id, 'mumbai_first_floor') + self.assertEqual(employee_checkin.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) + logs = make_n_checkins(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}) + logs_count = frappe.db.count('Employee Checkin', {'name':['in', log_names], 'skip_auto_attendance':1}) self.assertEqual(logs_count, 3) - logs = make_n_attendance_logs(employee, 4, 2) + logs = make_n_checkins(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}) + logs_count = frappe.db.count('Employee Checkin', {'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}) @@ -46,7 +46,7 @@ class TestEmployeeAttendanceLog(unittest.TestCase): 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'] + 'Strictly based on Log Type in Employee Checkin'] working_hours_calc_type = ['First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'] logs_type_1 = [ @@ -81,19 +81,19 @@ class TestEmployeeAttendanceLog(unittest.TestCase): working_hours = calculate_working_hours(logs_type_2,check_in_out_type[1],working_hours_calc_type[1]) self.assertEqual(working_hours, 4.5) -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))] +def make_n_checkins(employee, n, hours_to_reverse=1): + logs = [make_checkin(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))) + logs.append(make_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i))) return logs -def make_attendance_log(employee, time=now_datetime()): +def make_checkin(employee, time=now_datetime()): log = frappe.get_doc({ - "doctype": "Employee Attendance Log", + "doctype": "Employee Checkin", "employee" : employee, "time" : time, "device_id" : "device1", "log_type" : "IN" }).insert() - return log \ No newline at end of file + return log diff --git a/erpnext/hr/doctype/shift_type/shift_type.json b/erpnext/hr/doctype/shift_type/shift_type.json index 7610ebef5e..86039deebd 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.json +++ b/erpnext/hr/doctype/shift_type/shift_type.json @@ -19,7 +19,7 @@ "working_hours_threshold_for_half_day", "working_hours_threshold_for_absent", "process_attendance_after", - "last_sync_of_attendance_log", + "last_sync_of_checkin", "grace_period_settings_auto_attendance_section", "enable_entry_grace_period", "late_entry_grace_period", @@ -65,7 +65,7 @@ "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" + "options": "Alternating entries as IN and OUT during the same shift\nStrictly based on Log Type in Employee Checkin" }, { "fieldname": "working_hours_calculation_based_on", @@ -183,7 +183,7 @@ }, { "default": "0", - "description": "Mark attendance based on 'Employee Attendance Log' for Employees assigned to this shift.", + "description": "Mark attendance based on 'Employee Checkin' for Employees assigned to this shift.", "fieldname": "enable_auto_attendance", "fieldtype": "Check", "label": "Enable Auto Attendance" @@ -195,13 +195,13 @@ "label": "Process Attendance After" }, { - "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", + "description": "Last Known Successful Sync of Employee Checkin. 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_checkin", "fieldtype": "Datetime", - "label": "Last Sync of Attendance Log" + "label": "Last Sync of Checkin" } ], - "modified": "2019-05-31 16:02:44.272036", + "modified": "2019-06-10 06:02:44.272036", "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 0baffe0efb..b77d224cfb 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -10,22 +10,22 @@ import frappe from frappe.model.document import Document from frappe.utils import cint, getdate from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift -from erpnext.hr.doctype.employee_attendance_log.employee_attendance_log import mark_attendance_and_link_log, calculate_working_hours +from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours from erpnext.hr.doctype.attendance.attendance import mark_absent from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class ShiftType(Document): def process_auto_attendance(self): - if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_attendance_log: + if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin: return filters = { 'skip_auto_attendance':'0', 'attendance':('is', 'not set'), 'time':('>=', self.process_attendance_after), - 'shift_actual_start': ('<', self.last_sync_of_attendance_log), + 'shift_actual_start': ('<', self.last_sync_of_checkin), 'shift': self.name } - logs = frappe.db.get_list('Employee Attendance Log', fields="*", filters=filters, order_by="employee,time") + 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) @@ -54,8 +54,8 @@ class ShiftType(Document): if not date_of_joining: date_of_joining = employee_creation.date() start_date = max(self.process_attendance_after, date_of_joining) - actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, self.last_sync_of_attendance_log, True) - last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else self.last_sync_of_attendance_log + actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, self.last_sync_of_checkin, True) + last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else self.last_sync_of_checkin prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse') if prev_shift: end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date() From 82256bc58b5673a6ffaf9d67f313f91c145553bc Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Tue, 11 Jun 2019 14:25:34 +0530 Subject: [PATCH 8/9] fix(hr): Auto Attendance > fixing minor bug in shift calculation > adding 'Mark Auto Attendance' button in shift and related fixes > changed employee checkin naming series > added aditional validation for employee checkin > added/updated dashboard for Attendance, Employee, Shift Type --- .../doctype/attendance/attendance_dashboard.py | 13 +++++++++++++ .../hr/doctype/employee/employee_dashboard.py | 4 ++-- .../employee_checkin/employee_checkin.json | 4 ++-- .../doctype/employee_checkin/employee_checkin.py | 2 ++ .../doctype/shift_assignment/shift_assignment.py | 3 ++- erpnext/hr/doctype/shift_type/shift_type.js | 12 +++++++++++- erpnext/hr/doctype/shift_type/shift_type.py | 13 +++++++------ .../doctype/shift_type/shift_type_dashboard.py | 16 ++++++++-------- 8 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 erpnext/hr/doctype/attendance/attendance_dashboard.py diff --git a/erpnext/hr/doctype/attendance/attendance_dashboard.py b/erpnext/hr/doctype/attendance/attendance_dashboard.py new file mode 100644 index 0000000000..5dd9403674 --- /dev/null +++ b/erpnext/hr/doctype/attendance/attendance_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'attendance', + 'transactions': [ + { + 'label': '', + 'items': ['Employee Checkin'] + } + ] + } diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index e3cc33d142..162b697ac8 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -9,7 +9,7 @@ def get_data(): 'transactions': [ { 'label': _('Leave and Attendance'), - 'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation'] + 'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation', 'Employee Checkin'] }, { 'label': _('Lifecycle'), @@ -40,4 +40,4 @@ def get_data(): 'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map'] }, ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json index d340527538..15ec7c0b1b 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json @@ -1,6 +1,6 @@ { "allow_import": 1, - "autoname": "ATT-CKIN-.MM.-.YYYY.-.######", + "autoname": "EMP-CKIN-.MM.-.YYYY.-.######", "creation": "2019-06-10 11:56:34.536413", "doctype": "DocType", "engine": "InnoDB", @@ -119,7 +119,7 @@ "label": "Shift Actual End" } ], - "modified": "2019-06-10 11:56:34.536413", + "modified": "2019-06-10 15:33:22.731697", "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 997897b554..b0e15d96ed 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -28,6 +28,8 @@ class EmployeeCheckin(Document): def fetch_shift(self): shift_actual_timings = get_actual_start_end_datetime_of_shift(self.employee, get_datetime(self.time), True) if shift_actual_timings[0] and shift_actual_timings[1]: + if shift_actual_timings[2].shift_type.determine_check_in_and_check_out == 'Strictly based on Log Type in Employee Checkin' and not self.log_type and not self.skip_auto_attendance: + frappe.throw(_('Log Type is required for check-ins falling in the shift: {0}.').format(shift_actual_timings[2].shift_type.name)) if not self.attendance: self.shift = shift_actual_timings[2].shift_type.name self.shift_actual_start = shift_actual_timings[0] diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 5b732c4b99..40c78cdf07 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -115,11 +115,12 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals break else: direction = '<' if next_shift_direction == 'reverse' else '>' + sort_order = 'desc' if next_shift_direction == 'reverse' else 'asc' dates = frappe.db.get_all('Shift Assignment', 'date', {'employee':employee, 'date':(direction, for_date), 'docstatus': '1'}, as_list=True, - limit=MAX_DAYS) + limit=MAX_DAYS, order_by="date "+sort_order) for date in dates: shift_details = get_employee_shift(employee, date[0], consider_default_shift, None) if shift_details: diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js index feae889ecd..e633545630 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.js +++ b/erpnext/hr/doctype/shift_type/shift_type.js @@ -3,6 +3,16 @@ frappe.ui.form.on('Shift Type', { refresh: function(frm) { - + frm.add_custom_button( + 'Mark Auto Attendance', + () => frm.call({ + doc: frm.doc, + method: 'process_auto_attendance', + freeze: true, + callback: () => { + frappe.msgprint(__("Attendance has been marked as per employee check-ins")); + } + }) + ); } }); diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index b77d224cfb..eaf6b1e2d3 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -8,7 +8,7 @@ from datetime import timedelta import frappe from frappe.model.document import Document -from frappe.utils import cint, getdate +from frappe.utils import cint, getdate, get_datetime from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift, get_employee_shift from erpnext.hr.doctype.employee_checkin.employee_checkin import mark_attendance_and_link_log, calculate_working_hours from erpnext.hr.doctype.attendance.attendance import mark_absent @@ -53,9 +53,9 @@ class ShiftType(Document): date_of_joining, relieving_date, employee_creation = frappe.db.get_value("Employee", employee, ["date_of_joining", "relieving_date", "creation"]) if not date_of_joining: date_of_joining = employee_creation.date() - start_date = max(self.process_attendance_after, date_of_joining) - actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, self.last_sync_of_checkin, True) - last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else self.last_sync_of_checkin + start_date = max(getdate(self.process_attendance_after), date_of_joining) + actual_shift_datetime = get_actual_start_end_datetime_of_shift(employee, get_datetime(self.last_sync_of_checkin), True) + last_shift_time = actual_shift_datetime[0] if actual_shift_datetime[0] else get_datetime(self.last_sync_of_checkin) prev_shift = get_employee_shift(employee, last_shift_time.date()-timedelta(days=1), True, 'reverse') if prev_shift: end_date = min(prev_shift.start_datetime.date(), relieving_date) if relieving_date else prev_shift.start_datetime.date() @@ -64,10 +64,11 @@ class ShiftType(Document): holiday_list_name = self.holiday_list if not holiday_list_name: holiday_list_name = get_holiday_list_for_employee(employee, False) - for date in get_filtered_date_list(employee, start_date, end_date, holiday_list=holiday_list_name): + dates = get_filtered_date_list(employee, start_date, end_date, holiday_list=holiday_list_name) + for date in dates: shift_details = get_employee_shift(employee, date, True) if shift_details and shift_details.shift_type.name == self.name: - mark_absent(employee, date) + mark_absent(employee, date, self.name) def get_assigned_employee(self, from_date=None, consider_default_shift=False): filters = {'date':('>=', from_date), 'shift_type': self.name, 'docstatus': '1'} diff --git a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py index 91dfbad7b1..0a49ddaa9a 100644 --- a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py +++ b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py @@ -2,11 +2,11 @@ from __future__ import unicode_literals from frappe import _ def get_data(): - return { - 'fieldname': 'shift_type', - 'transactions': [ - { - 'items': ['Shift Request', 'Shift Assignment'] - } - ], - } \ No newline at end of file + return { + 'fieldname': 'shift', + 'transactions': [ + { + 'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment'] + } + ] + } From eadc14447773503facd4c8bdb01a3fbc230e6afa Mon Sep 17 00:00:00 2001 From: karthikeyan5 Date: Wed, 19 Jun 2019 08:53:04 +0530 Subject: [PATCH 9/9] fix(HR): fixing bug introduced during merge conflict resolution. > while merging with https://github.com/frappe/erpnext/pull/17837 --- erpnext/hr/doctype/shift_type/shift_type_dashboard.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py index 0a49ddaa9a..aedd190bcb 100644 --- a/erpnext/hr/doctype/shift_type/shift_type_dashboard.py +++ b/erpnext/hr/doctype/shift_type/shift_type_dashboard.py @@ -4,6 +4,10 @@ from frappe import _ def get_data(): return { 'fieldname': 'shift', + 'non_standard_fieldnames': { + 'Shift Request': 'shift_type', + 'Shift Assignment': 'shift_type' + }, 'transactions': [ { 'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment']