Merge branch 'develop' into fix_by_voucher_order_develop

This commit is contained in:
Don-Leopardo 2019-06-24 11:29:28 -03:00 committed by GitHub
commit fb2a05cb3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 2049 additions and 4693 deletions

View File

@ -34,6 +34,13 @@ def get_data():
"name": "Upload Attendance",
"hide_count": True,
"dependencies": ["Employee"]
},
{
"type": "doctype",
"name": "Employee Checkin",
"hide_count": True,
"onboard": 1,
"dependencies": ["Employee"]
},
]
},

View File

@ -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.shift_type.shift_type.process_auto_attendance_for_all_shifts"
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",

View File

@ -1,540 +1,213 @@
{
"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",
"working_hours",
"status",
"leave_type",
"leave_application",
"column_break0",
"attendance_date",
"company",
"department",
"shift",
"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
},
{
"depends_on": "working_hours",
"fieldname": "working_hours",
"fieldtype": "Float",
"label": "Working Hours",
"precision": "1",
"read_only": 1
},
{
"fieldname": "shift",
"fieldtype": "Link",
"label": "Shift",
"options": "Shift Type"
}
],
"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"
}

View File

@ -88,3 +88,18 @@ def add_attendance(events, start, end, conditions=None):
}
if e not in events:
events.append(e)
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, 'docstatus':('!=', '2')}):
doc_dict = {
'doctype': 'Attendance',
'employee': employee,
'attendance_date': attendance_date,
'status': 'Absent',
'company': employee_doc.company,
'shift': shift
}
attendance = frappe.get_doc(doc_dict).insert()
attendance.submit()
return attendance.name

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'attendance',
'transactions': [
{
'label': '',
'items': ['Employee Checkin']
}
]
}

View File

@ -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)

View File

@ -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')):

File diff suppressed because it is too large Load Diff

View File

@ -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()

View File

@ -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'),

View File

@ -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', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,171 @@
{
"allow_import": 1,
"autoname": "EMP-CKIN-.MM.-.YYYY.-.######",
"creation": "2019-06-10 11:56:34.536413",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"log_type",
"shift",
"column_break_4",
"time",
"device_id",
"skip_auto_attendance",
"attendance",
"entry_grace_period_consequence",
"exit_grace_period_consequence",
"shift_start",
"shift_end",
"shift_actual_start",
"shift_actual_end"
],
"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": "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"
},
{
"default": "Now",
"fieldname": "time",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Time",
"reqd": 1
},
{
"fieldname": "device_id",
"fieldtype": "Data",
"label": "Location / Device ID"
},
{
"default": "0",
"fieldname": "skip_auto_attendance",
"fieldtype": "Check",
"label": "Skip Auto Attendance"
},
{
"fieldname": "attendance",
"fieldtype": "Link",
"label": "Attendance Marked",
"options": "Attendance",
"read_only": 1
},
{
"default": "0",
"fieldname": "entry_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Entry Grace Period Consequence"
},
{
"default": "0",
"fieldname": "exit_grace_period_consequence",
"fieldtype": "Check",
"hidden": 1,
"label": "Exit Grace Period Consequence"
},
{
"fieldname": "shift_start",
"fieldtype": "Datetime",
"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-06-10 15:33:22.731697",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Checkin",
"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
}

View File

@ -0,0 +1,161 @@
# -*- 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, 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 EmployeeCheckin(Document):
def validate(self):
self.validate_duplicate_log()
self.fetch_shift()
def validate_duplicate_log(self):
doc = frappe.db.exists('Employee Checkin', {
'employee': self.employee,
'time': self.time,
'name': ['!=', self.name]})
if doc:
doc_link = frappe.get_desk_link('Employee Checkin', doc)
frappe.throw(_('This employee already has a log with the same timestamp.{0}')
.format("<Br>" + 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 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]
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, skip_auto_attendance=0, employee_fieldname='attendance_device_id'):
"""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'
: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.
"""
if not employee_field_value or not timestamp:
frappe.throw(_("'employee_field_value' and 'timestamp' are required."))
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 employee field value. '{}': {}").format(employee_fieldname,employee_field_value))
doc = frappe.new_doc("Employee Checkin")
doc.employee = employee.name
doc.employee_name = employee.employee_name
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, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
: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.
"""
log_names = [x.name for x in logs]
employee = logs[0].employee
if attendance_status == 'Skip':
frappe.db.sql("""update `tabEmployee Checkin`
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, 'docstatus':('!=', '2')}):
doc_dict = {
'doctype': 'Attendance',
'employee': employee,
'attendance_date': attendance_date,
'status': attendance_status,
'working_hours': working_hours,
'company': employee_doc.company,
'shift': shift
}
attendance = frappe.get_doc(doc_dict).insert()
attendance.submit()
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 Checkin`
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))
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 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
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 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')]
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)

View File

@ -0,0 +1,99 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import now_datetime, nowdate, to_timedelta
import unittest
from datetime import timedelta
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 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)
employee.attendance_device_id = '3344'
employee.save()
time_now = now_datetime().__str__()[:-7]
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_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 Checkin', {'name':['in', log_names], 'skip_auto_attendance':1})
self.assertEqual(logs_count, 3)
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 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})
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 Checkin']
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_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_checkin(employee, now_datetime() - timedelta(hours=hours_to_reverse, minutes=n-i)))
return logs
def make_checkin(employee, time=now_datetime()):
log = frappe.get_doc({
"doctype": "Employee Checkin",
"employee" : employee,
"time" : time,
"device_id" : "device1",
"log_type" : "IN"
}).insert()
return log

View File

@ -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

View File

@ -4,6 +4,29 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate
from datetime import timedelta
class TestHolidayList(unittest.TestCase):
pass
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_holiday_list",
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):
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

View File

@ -1,665 +1,162 @@
{
"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,
"field_order": [
"employee_settings",
"retirement_age",
"emp_created_by",
"column_break_4",
"stop_birthday_reminders",
"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",
"leave_settings",
"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": [
{
"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
"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
"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
"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
"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
"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
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "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
"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
},
{
"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
"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
"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
"label": "Payroll Settings"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "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
"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
"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,
"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.",
"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
"label": "Encrypt Salary Slips in Emails"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
"description": "<b>Example:</b> SAL-{first_name}-{date_of_birth.year} <br>This will generate a password like SAL-Jane-1972",
"fetch_if_empty": 0,
"fieldname": "password_policy",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Password Policy",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"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
"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
"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,
"default": "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
"label": "Show Leaves Of All Department Members In Calendar"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
}
],
"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",
"modified": "2019-05-31 16:18:50.245872",
"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,
"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"
}

View File

@ -6,7 +6,6 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class HRSettings(Document):

View File

@ -5,6 +5,9 @@ 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

View File

@ -1,381 +1,101 @@
{
"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",
"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
"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
"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
"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
"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
},
{
"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
"fieldtype": "Column Break"
},
{
"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
"reqd": 1
},
{
"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
"label": "Date"
},
{
"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
"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": 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
"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": "2019-05-30 15:40:54.418427",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Assignment",
"name_case": "",
"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
"share": 1
},
{
"amend": 1,
@ -384,46 +104,29 @@
"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,
"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,
"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
"track_changes": 1
}

View File

@ -6,7 +6,10 @@ 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.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
@ -78,3 +81,130 @@ 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('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
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_for_employee(employee, 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:
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 '>'
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, order_by="date "+sort_order)
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
})
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

View File

@ -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"));
}
})
);
}
});

View File

@ -1,194 +1,247 @@
{
"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",
"field_order": [
"start_time",
"end_time",
"column_break_3",
"holiday_list",
"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",
"allow_check_out_after_shift_end_time",
"column_break_10",
"working_hours_threshold_for_half_day",
"working_hours_threshold_for_absent",
"process_attendance_after",
"last_sync_of_checkin",
"grace_period_settings_auto_attendance_section",
"enable_entry_grace_period",
"late_entry_grace_period",
"consequence_after",
"consequence",
"column_break_18",
"enable_exit_grace_period",
"enable_different_consequence_for_early_exit",
"early_exit_grace_period",
"early_exit_consequence_after",
"early_exit_consequence"
],
"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
"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
"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
"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 Checkin"
},
{
"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"
},
{
"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": "0",
"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"
},
{
"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",
"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"
},
{
"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 Checkin' for Employees assigned to this shift.",
"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 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 Checkin"
}
],
"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": "2019-06-10 06:02:44.272036",
"modified_by": "Administrator",
"module": "HR",
"name": "Shift Type",
"name_case": "",
"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,
"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
"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,
"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
"track_changes": 1
}

View File

@ -3,8 +3,116 @@
# 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, 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
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class ShiftType(Document):
pass
def process_auto_attendance(self):
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_checkin),
'shift': self.name
}
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group)
attendance_status, working_hours = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, self.name)
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(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()
else:
return
holiday_list_name = self.holiday_list
if not holiday_list_name:
holiday_list_name = get_holiday_list_for_employee(employee, False)
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, self.name)
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]

View File

@ -2,11 +2,15 @@ from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'shift_type',
'transactions': [
{
'items': ['Shift Request', 'Shift Assignment']
}
],
}
return {
'fieldname': 'shift',
'non_standard_fieldnames': {
'Shift Request': 'shift_type',
'Shift Assignment': 'shift_type'
},
'transactions': [
{
'items': ['Attendance', 'Employee Checkin', 'Shift Request', 'Shift Assignment']
}
]
}

View File

@ -381,9 +381,12 @@ class BOM(WebsiteGenerator):
frappe.throw(_("Same item has been entered multiple times. {0}").format(duplicate_list))
def check_recursion(self):
def check_recursion(self, bom_list=[]):
""" Check whether recursion occurs in any bom"""
bom_list = self.traverse_tree()
if not bom_list:
bom_list = self.traverse_tree()
bom_nos = frappe.get_all('BOM Item', fields=["bom_no"],
filters={'parent': ('in', bom_list), 'parenttype': 'BOM'})
@ -405,7 +408,7 @@ class BOM(WebsiteGenerator):
bom_list = self.traverse_tree(bom_list)
for bom in bom_list:
bom_obj = frappe.get_doc("BOM", bom)
bom_obj.check_recursion()
bom_obj.check_recursion(bom_list=bom_list)
bom_obj.update_exploded_items()
return bom_list

View File

@ -55,7 +55,7 @@ class BOMUpdateTool(Document):
bom_list = []
data = frappe.db.sql(""" select distinct parent from `tabBOM Item`
where ifnull(bom_no, '') = %s and docstatus < 2 and parenttype='BOM'""", bom)
where bom_no = %s and docstatus < 2 and parenttype='BOM'""", bom)
for d in data:
bom_list.append(d[0])

View File

@ -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",

View File

@ -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()

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import flt, add_days, nowdate, add_months
from frappe.utils import flt, add_days, nowdate, add_months, getdate
import unittest
test_dependencies = ["Product Bundle"]
@ -18,7 +18,7 @@ class TestQuotation(unittest.TestCase):
self.assertTrue(quotation.payment_schedule)
def test_make_sales_order_terms_not_copied(self):
def test_make_sales_order_terms_copied(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
quotation = frappe.copy_doc(test_records[0])
@ -29,7 +29,7 @@ class TestQuotation(unittest.TestCase):
sales_order = make_sales_order(quotation.name)
self.assertFalse(sales_order.get('payment_schedule'))
self.assertTrue(sales_order.get('payment_schedule'))
def test_make_sales_order_with_different_currency(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
@ -109,10 +109,10 @@ class TestQuotation(unittest.TestCase):
sales_order.insert()
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
self.assertEqual(sales_order.payment_schedule[0].due_date, quotation.transaction_date)
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
self.assertEqual(
sales_order.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)
sales_order.payment_schedule[1].due_date, getdate(add_days(quotation.transaction_date, 30))
)
def test_valid_till(self):