diff --git a/addons/hr_attendance/__init__.py b/addons/hr_attendance/__init__.py index bd6cae5c16b..81fc81d29c9 100644 --- a/addons/hr_attendance/__init__.py +++ b/addons/hr_attendance/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import wizard -import report import models diff --git a/addons/hr_attendance/__openerp__.py b/addons/hr_attendance/__openerp__.py index c372ab60fef..6a8024282b8 100644 --- a/addons/hr_attendance/__openerp__.py +++ b/addons/hr_attendance/__openerp__.py @@ -4,34 +4,35 @@ { 'name': 'Attendances', - 'version': '1.1', + 'version': '2.0', 'category': 'Human Resources', + 'sequence': 81, + 'summary': 'Manage employee attendances', 'description': """ This module aims to manage employee's attendances. ================================================== Keeps account of the attendances of the employees on the basis of the -actions(Sign in/Sign out) performed by them. +actions(Check in/Check out) performed by them. """, 'website': 'https://www.odoo.com/page/employees', - 'depends': ['hr', 'report'], + 'depends': ['hr', 'report', 'barcodes'], 'data': [ - 'security/ir_rule.xml', + 'security/hr_attendance_security.xml', 'security/ir.model.access.csv', - 'hr_attendance_view.xml', - 'hr_attendance_report.xml', - 'wizard/hr_attendance_error_view.xml', - 'views/report_attendanceerrors.xml', - 'views/hr_attendance.xml', - 'hr_dashboard.xml', + 'views/web_asset_backend_template.xml', + 'views/hr_attendance_view.xml', + 'report/hr_employee_badge.xml', + 'views/hr_department_view.xml', + 'views/hr_employee_view.xml', + 'views/res_config_view.xml', ], - 'demo': ['hr_attendance_demo.xml'], - 'test': [ - 'test/attendance_process.yml', - 'test/hr_attendance_report.yml', + 'demo': [ + 'data/hr_attendance_demo.xml' ], 'installable': True, 'auto_install': False, - #web - 'qweb': ["static/src/xml/attendance.xml"], + 'qweb': [ + "static/src/xml/attendance.xml", + ], } diff --git a/addons/hr_attendance/data/hr_attendance_demo.xml b/addons/hr_attendance/data/hr_attendance_demo.xml new file mode 100644 index 00000000000..e1f444dfa62 --- /dev/null +++ b/addons/hr_attendance/data/hr_attendance_demo.xml @@ -0,0 +1,86 @@ + + + + + 0000 + 123 + + + + 0000 + 456 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_attendance/hr_attendance_demo.xml b/addons/hr_attendance/hr_attendance_demo.xml deleted file mode 100644 index 9f265113b83..00000000000 --- a/addons/hr_attendance/hr_attendance_demo.xml +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - - sign_in - - - - - - sign_out - - - - - diff --git a/addons/hr_attendance/hr_attendance_report.xml b/addons/hr_attendance/hr_attendance_report.xml deleted file mode 100644 index f98eb6c053b..00000000000 --- a/addons/hr_attendance/hr_attendance_report.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/addons/hr_attendance/hr_attendance_view.xml b/addons/hr_attendance/hr_attendance_view.xml deleted file mode 100644 index c99b18e4b1b..00000000000 --- a/addons/hr_attendance/hr_attendance_view.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - hr.attendance.form - hr.attendance - -
- - - - - - - - -
-
-
- - - hr.attendance.tree - hr.attendance - - - - - - - - - - - hr.attendance.tree - hr.attendance - - - - - - - - - - - - view_hr_attendance_filter - hr.attendance - - - - - - - - - - - - - - - - - - - - - hr.attendance.pivot - hr.attendance - - - - - - - - - - - hr.attendance.graph - hr.attendance - - - - - - - - - - - Attendances - hr.attendance - form - tree,form - {"search_default_today":1} - - The Time Tracking functionality aims to manage employee attendances from Sign in/Sign out actions. You can also link this feature to an attendance device using Odoo's web service features. - - - - Attendance Analysis - hr.attendance - form - pivot,graph - - - - - Attendance Analysis - hr.attendance - form - pivot,graph - { - 'search_default_department_id': [active_id], - 'default_department_id': active_id} - - - - - - - - - - - - hr.action.reason.form - hr.action.reason - -
- - - - -
-
-
- - - hr.action.reason.tree - hr.action.reason - - - - - - - - - - Attendance Reasons - ir.actions.act_window - hr.action.reason - form - tree,form - - - - - -
-
diff --git a/addons/hr_attendance/models/__init__.py b/addons/hr_attendance/models/__init__.py index eeef82d250c..f5ba8b43262 100644 --- a/addons/hr_attendance/models/__init__.py +++ b/addons/hr_attendance/models/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- import hr_attendance -import ir_http +import hr_employee +import res_config diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py index cd8bae0e1a1..f078b6037fd 100644 --- a/addons/hr_attendance/models/hr_attendance.py +++ b/addons/hr_attendance/models/hr_attendance.py @@ -1,169 +1,105 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import time from datetime import datetime -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError +from odoo import models, fields, api, exceptions, _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT -class hr_action_reason(osv.osv): - _name = "hr.action.reason" - _description = "Action Reason" - _columns = { - 'name': fields.char('Reason', required=True, help='Specifies the reason for Signing In/Signing Out.'), - 'action_type': fields.selection([('sign_in', 'Sign in'), ('sign_out', 'Sign out')], "Action Type"), - } - _defaults = { - 'action_type': 'sign_in', - } - - -def _employee_get(obj, cr, uid, context=None): - ids = obj.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context) - return ids and ids[0] or False - - -class hr_attendance(osv.osv): +class HrAttendance(models.Model): _name = "hr.attendance" _description = "Attendance" + _order = "check_in desc" - def _worked_hours_compute(self, cr, uid, ids, fieldnames, args, context=None): - """For each hr.attendance record of action sign-in: assign 0. - For each hr.attendance record of action sign-out: assign number of hours since last sign-in. - """ - res = {} - for obj in self.browse(cr, uid, ids, context=context): - if obj.action == 'sign_in': - res[obj.id] = 0 - elif obj.action == 'sign_out': - # Get the associated sign-in - last_signin_id = self.search(cr, uid, [ - ('employee_id', '=', obj.employee_id.id), - ('name', '<', obj.name), ('action', '=', 'sign_in') - ], limit=1, order='name DESC') - if last_signin_id: - last_signin = self.browse(cr, uid, last_signin_id, context=context)[0] + def _default_employee(self): + return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) - # Compute time elapsed between sign-in and sign-out - last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S') - signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S') - workedhours_datetime = (signout_datetime - last_signin_datetime) - res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60.0 - else: - res[obj.id] = False - return res + employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True, ondelete='cascade', index=True) + department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id") + check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True) + check_out = fields.Datetime(string="Check Out") + worked_hours = fields.Float(string='Worked Hours', compute='_compute_worked_hours', store=True, readonly=True) - _columns = { - 'name': fields.datetime('Date', required=True, select=1), - 'action': fields.selection([('sign_in', 'Sign In'), ('sign_out', 'Sign Out'), ('action','Action')], 'Action', required=True), - 'action_desc': fields.many2one("hr.action.reason", "Action Reason", domain="[('action_type', '=', action)]", help='Specifies the reason for Signing In/Signing Out in case of extra hours.'), - 'employee_id': fields.many2one('hr.employee', "Employee", required=True, select=True), - 'department_id': fields.many2one('hr.department', "Department", related="employee_id.department_id"), - 'worked_hours': fields.function(_worked_hours_compute, type='float', string='Worked Hours', store=True), - } - _defaults = { - 'name': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'), #please don't remove the lambda, if you remove it then the current time will not change - 'employee_id': _employee_get, - } - - def _altern_si_so(self, cr, uid, ids, context=None): - """ Alternance sign_in/sign_out check. - Previous (if exists) must be of opposite action. - Next (if exists) must be of opposite action. - """ - for att in self.browse(cr, uid, ids, context=context): - # search and browse for first previous and first next records - prev_att_ids = self.search(cr, uid, [('employee_id', '=', att.employee_id.id), ('name', '<', att.name), ('action', 'in', ('sign_in', 'sign_out'))], limit=1, order='name DESC') - next_add_ids = self.search(cr, uid, [('employee_id', '=', att.employee_id.id), ('name', '>', att.name), ('action', 'in', ('sign_in', 'sign_out'))], limit=1, order='name ASC') - prev_atts = self.browse(cr, uid, prev_att_ids, context=context) - next_atts = self.browse(cr, uid, next_add_ids, context=context) - # check for alternance, return False if at least one condition is not satisfied - if prev_atts and prev_atts[0].action == att.action: # previous exists and is same action - return False - if next_atts and next_atts[0].action == att.action: # next exists and is same action - return False - if (not prev_atts) and (not next_atts) and att.action != 'sign_in': # first attendance must be sign_in - return False - return True - - _constraints = [(_altern_si_so, 'Error ! Sign in (resp. Sign out) must follow Sign out (resp. Sign in)', ['action'])] - _order = 'name desc' - - -class hr_employee(osv.osv): - _inherit = "hr.employee" - _description = "Employee" - - def _state(self, cr, uid, ids, name, args, context=None): - result = {} - if not ids: - return result - for id in ids: - result[id] = 'absent' - cr.execute('SELECT hr_attendance.action, hr_attendance.employee_id \ - FROM ( \ - SELECT MAX(name) AS name, employee_id \ - FROM hr_attendance \ - WHERE action in (\'sign_in\', \'sign_out\') \ - GROUP BY employee_id \ - ) AS foo \ - LEFT JOIN hr_attendance \ - ON (hr_attendance.employee_id = foo.employee_id \ - AND hr_attendance.name = foo.name) \ - WHERE hr_attendance.employee_id IN %s',(tuple(ids),)) - for res in cr.fetchall(): - result[res[1]] = res[0] == 'sign_in' and 'present' or 'absent' + @api.multi + def name_get(self): + result = [] + for attendance in self: + if not attendance.check_out: + result.append((self.id, _("%(empl_name)s from %(check_in)s") % { + 'empl_name': self.employee_id.name_related, + 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))), + })) + else: + result.append((self.id, _("%(empl_name)s from %(check_in)s to %(check_out)s") % { + 'empl_name': self.employee_id.name_related, + 'check_in': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_in))), + 'check_out': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(self.check_out))), + })) return result - def _last_sign(self, cr, uid, ids, name, args, context=None): - result = {} - if not ids: - return result - for id in ids: - result[id] = False - cr.execute("""select max(name) as name - from hr_attendance - where action in ('sign_in', 'sign_out') and employee_id = %s""",(id,)) - for res in cr.fetchall(): - result[id] = res[0] - return result + @api.depends('check_in', 'check_out') + def _compute_worked_hours(self): + for attendance in self: + if attendance.check_out: + delta = datetime.strptime(attendance.check_out, DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime( + attendance.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + attendance.worked_hours = delta.total_seconds() / 3600.0 - def _attendance_access(self, cr, uid, ids, name, args, context=None): - # this function field use to hide attendance button to singin/singout from menu - visible = self.pool.get("res.users").has_group(cr, uid, "base.group_hr_attendance") - return dict([(x, visible) for x in ids]) + @api.constrains('check_in', 'check_out') + def _check_validity_check_in_check_out(self): + """ verifies if check_in is earlier than check_out. """ + for attendance in self: + if attendance.check_in and attendance.check_out: + if attendance.check_out < attendance.check_in: + raise exceptions.ValidationError(_('"Check Out" time cannot be earlier than "Check In" time.')) - _columns = { - 'state': fields.function(_state, type='selection', selection=[('absent', 'Absent'), ('present', 'Present')], string='Attendance'), - 'last_sign': fields.function(_last_sign, type='datetime', string='Last Sign'), - 'attendance_access': fields.function(_attendance_access, string='Attendance Access', type='boolean'), - } + @api.constrains('check_in', 'check_out', 'employee_id') + def _check_validity(self): + """ Verifies the validity of the attendance record compared to the others from the same employee. + For the same employee we must have : + * maximum 1 "open" attendance record (without check_out) + * no overlapping time slices with previous employee records + """ + for attendance in self: + # we take the latest attendance before our check_in time and check it doesn't overlap with ours + last_attendance_before_check_in = self.env['hr.attendance'].search([ + ('employee_id', '=', attendance.employee_id.id), + ('check_in', '<=', attendance.check_in), + ('id', '!=', attendance.id), + ], order='check_in desc', limit=1) + if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out >= attendance.check_in: + raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % { + 'empl_name': attendance.employee_id.name_related, + 'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(attendance.check_in))), + }) - def _action_check(self, cr, uid, emp_id, dt=False, context=None): - cr.execute('SELECT MAX(name) FROM hr_attendance WHERE employee_id=%s', (emp_id,)) - res = cr.fetchone() - return not (res and (res[0]>=(dt or time.strftime('%Y-%m-%d %H:%M:%S')))) + if not attendance.check_out: + # if our attendance is "open" (no check_out), we verify there is no other "open" attendance + no_check_out_attendances = self.env['hr.attendance'].search([ + ('employee_id', '=', attendance.employee_id.id), + ('check_out', '=', False), + ('id', '!=', attendance.id), + ]) + if no_check_out_attendances: + raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s") % { + 'empl_name': attendance.employee_id.name_related, + 'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(no_check_out_attendances.check_in))), + }) + else: + # we verify that the latest attendance with check_in time before our check_out time + # is the same as the one before our check_in time computed before, otherwise it overlaps + last_attendance_before_check_out = self.env['hr.attendance'].search([ + ('employee_id', '=', attendance.employee_id.id), + ('check_in', '<=', attendance.check_out), + ('id', '!=', attendance.id), + ], order='check_in desc', limit=1) + if last_attendance_before_check_out and last_attendance_before_check_in != last_attendance_before_check_out: + raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % { + 'empl_name': attendance.employee_id.name_related, + 'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(last_attendance_before_check_out.check_in))), + }) - def attendance_action_change(self, cr, uid, ids, context=None): - if context is None: - context = {} - action_date = context.get('action_date', False) - action = context.get('action', False) - hr_attendance = self.pool.get('hr.attendance') - warning_sign = {'sign_in': _('Sign In'), 'sign_out': _('Sign Out')} - for employee in self.browse(cr, uid, ids, context=context): - if not action: - if employee.state == 'present': action = 'sign_out' - if employee.state == 'absent': action = 'sign_in' - - if not self._action_check(cr, uid, employee.id, action_date, context): - raise UserError(_('You tried to %s with a date anterior to another event !\nTry to contact the HR Manager to correct attendances.') % (warning_sign[action],)) - - vals = {'action': action, 'employee_id': employee.id} - if action_date: - vals['name'] = action_date - hr_attendance.create(cr, uid, vals, context=context) - return True + @api.multi + def copy(self): + raise exceptions.UserError(_('You cannot duplicate an attendance.')) diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py new file mode 100644 index 00000000000..6a6e3bc9918 --- /dev/null +++ b/addons/hr_attendance/models/hr_employee.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from random import choice +from string import digits + +from odoo import models, fields, api, exceptions, _, SUPERUSER_ID + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + _description = "Employee" + + def _default_random_pin(self): + return ("".join(choice(digits) for i in range(4))) + + def _default_random_barcode(self): + barcode = None + while not barcode or self.env['hr.employee'].search([('barcode', '=', barcode)]): + barcode = "".join(choice(digits) for i in range(8)) + return barcode + + barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", default=_default_random_barcode, copy=False, groups='base.group_hr_user') + pin = fields.Char(string="PIN", default=_default_random_pin, help="PIN used for Check In/Out in Attendance.", copy=False, groups='base.group_hr_user') + + attendance_ids = fields.One2many('hr.attendance', 'employee_id', help='list of attendances for the employee') + last_attendance_id = fields.Many2one('hr.attendance', compute='_compute_last_attendance_id') + attendance_state = fields.Selection(string="Attendance", compute='_compute_attendance_state', selection=[('checked_out', "Checked out"), ('checked_in', "Checked in")]) + manual_attendance = fields.Boolean(string='Manual Attendance', compute='_compute_manual_attendance', inverse='_inverse_manual_attendance', + help='The employee will have access to the "My Attendances" menu to perform his check in and out from his session', default=False) + + _sql_constraints = [('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee.")] + + @api.multi + def _compute_manual_attendance(self): + for employee in self: + employee.manual_attendance = employee.user_id.has_group('base.group_hr_attendance') if employee.user_id else False + + @api.multi + def _inverse_manual_attendance(self): + manual_attendance_group = self.env['res.groups'].search([('name', '=', 'Manual Attendances')]) + for employee in self: + if employee.user_id: + if employee.manual_attendance: + manual_attendance_group.users = [(4, employee.user_id.id, 0)] + else: + manual_attendance_group.users = [(3, employee.user_id.id, 0)] + + @api.depends('attendance_ids') + def _compute_last_attendance_id(self): + for employee in self: + employee.last_attendance_id = employee.attendance_ids and employee.attendance_ids[0] or False + + @api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id') + def _compute_attendance_state(self): + """compute employee state, last check in and last check out + """ + for employee in self: + employee.attendance_state = employee.last_attendance_id and not employee.last_attendance_id.check_out and 'checked_in' or 'checked_out' + + @api.constrains('pin') + def _verify_pin(self): + for employee in self: + if employee.pin and not employee.pin.isdigit(): + raise exceptions.ValidationError(_("The PIN must be a sequence of digits.")) + + @api.model + def attendance_scan(self, barcode): + """ Receive a barcode scanned from the main menu and change the attendances of corresponding employee. + Returns either an action or a warning. + """ + employee = self.search([('barcode', '=', barcode)], limit=1) + return employee and employee.attendance_action('hr_attendance.hr_attendance_action_main_menu') or \ + {'warning': _('No employee corresponding to barcode %(barcode)s') % {'barcode': barcode}} + + @api.multi + def attendance_manual(self, next_action, entered_pin=None): + self.ensure_one() + if self.env['res.users'].browse(SUPERUSER_ID).has_group('base.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id): + if entered_pin != self.pin: + return {'warning': _('Wrong PIN')} + return self.attendance_action(next_action) + + @api.multi + def attendance_action(self, next_action): + """ Changes the attendance of the employee. + Returns an action to the check in/out message, + next_action defines which menu the check in/out message should return to. + """ + self.ensure_one() + action_message = self.env.ref('hr_attendance.hr_attendance_action_message').read()[0] + action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False + if action_message['previous_attendance_change_date']: + action_message['previous_attendance_change_date'] = \ + fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(action_message['previous_attendance_change_date']))) + action_message['employee_name'] = self.name + action_message['next_action'] = next_action + + if self.user_id: + modified_attendance = self.sudo(self.user_id.id).attendance_action_change() + else: + modified_attendance = self.sudo().attendance_action_change() + action_message['attendance'] = modified_attendance.read()[0] + return {'action': action_message} + + @api.multi + def attendance_action_change(self): + """ Check In/Check Out action + Check In: create a new attendance record + Check Out: modify check_out field of appropriate attendance record + """ + if len(self) > 1: + raise exceptions.UserError(_('Cannot perform check in or check out on multiple employees.')) + action_date = fields.Datetime.now() + + action = 'check_in' + if self.attendance_state == 'checked_in': + action = 'check_out' + + if action == 'check_in': + vals = { + 'employee_id': self.id, + 'check_in': action_date, + } + return self.env['hr.attendance'].create(vals) + else: + attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) + if attendance: + attendance.check_out = action_date + else: + raise exceptions.UserError(_('Cannot perform check out on %(empl_name)s, could not find corresponding check in. \ + Your attendances have probably been modified manually by human resources.') % {'empl_name': self.name, }) + return attendance + + def _init_column(self, cr, column_name, context=None): + """ Initialize the value of the given column for existing rows. + Overridden here because we need to have different default values + for barcode and pin for every employee. + """ + if column_name not in ["barcode", "pin"]: + super(HrEmployee, self)._init_column(cr, column_name, context=context) + else: + default_compute = self._defaults.get(column_name) + + query = 'SELECT id FROM "%s" WHERE "%s" is NULL' % ( + self._table, column_name) + cr.execute(query) + employee_ids = cr.fetchall() + + for employee_id in employee_ids: + default_value = default_compute(self, cr, SUPERUSER_ID, context) + + query = 'UPDATE "%s" SET "%s"=%%s WHERE id = %s' % ( + self._table, column_name, employee_id[0]) + cr.execute(query, (default_value,)) diff --git a/addons/hr_attendance/models/ir_http.py b/addons/hr_attendance/models/ir_http.py deleted file mode 100644 index af4ba6a9e5e..00000000000 --- a/addons/hr_attendance/models/ir_http.py +++ /dev/null @@ -1,9 +0,0 @@ -from openerp import models, service - -class Http(models.AbstractModel): - _inherit = 'ir.http' - - def session_info(self): - result = super(Http, self).session_info() - result['is_employee'] = self.env.user.has_group('base.group_user') - return result diff --git a/addons/hr_attendance/models/res_config.py b/addons/hr_attendance/models/res_config.py new file mode 100644 index 00000000000..f9bffadc61b --- /dev/null +++ b/addons/hr_attendance/models/res_config.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class BaseConfigSettings(models.TransientModel): + _inherit = 'base.config.settings' + + group_attendance_use_pin = fields.Selection([(0, 'Employees do not need to enter their PIN to check in manually in the "Company Screen".'), + (1, 'Employees must enter their PIN to check in manually in the "Company Screen".')], + string='Employee PIN', help='Enable or disable employee PIN identification at check in', implied_group="base.group_hr_attendance_use_pin") diff --git a/addons/hr_attendance/report/attendance_errors.py b/addons/hr_attendance/report/attendance_errors.py deleted file mode 100644 index f94c27ea156..00000000000 --- a/addons/hr_attendance/report/attendance_errors.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import datetime -import time -from openerp.osv import osv -from openerp.report import report_sxw - - -class attendance_print(report_sxw.rml_parse): - - def __init__(self, cr, uid, name, context): - super(attendance_print, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - 'lst': self._lst, - 'total': self._lst_total, - 'get_employees':self._get_employees, - }) - - def _get_employees(self, emp_ids): - emp_obj_list = self.pool.get('hr.employee').browse(self.cr, self.uid, emp_ids) - return emp_obj_list - - def _lst(self, employee_id, dt_from, dt_to, max, *args): - self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out')) - res = self.cr.dictfetchall() - for r in res: - if r['action'] == 'sign_out': - r['delay'] = -r['delay'] - temp = r['delay'].seconds - - r['delay'] = str(r['delay']).split('.')[0] - if abs(temp) < max*60: - r['delay2'] = r['delay'] - else: - r['delay2'] = '/' - return res - - def _lst_total(self, employee_id, dt_from, dt_to, max, *args): - self.cr.execute("select name as date, create_date, action, create_date-name as delay from hr_attendance where employee_id=%s and to_char(name,'YYYY-mm-dd')<=%s and to_char(name,'YYYY-mm-dd')>=%s and action IN (%s,%s) order by name", (employee_id, dt_to, dt_from, 'sign_in', 'sign_out')) - res = self.cr.dictfetchall() - if not res: - return ('/','/') - total2 = datetime.timedelta(seconds = 0, minutes = 0, hours = 0) - total = datetime.timedelta(seconds = 0, minutes = 0, hours = 0) - for r in res: - if r['action'] == 'sign_out': - r['delay'] = -r['delay'] - total += r['delay'] - if abs(r['delay'].seconds) < max*60: - total2 += r['delay'] - - result_dict = { - 'total': total and str(total).split('.')[0], - 'total2': total2 and str(total2).split('.')[0] - } - return [result_dict] - - -class report_hr_attendanceerrors(osv.AbstractModel): - _name = 'report.hr_attendance.report_attendanceerrors' - _inherit = 'report.abstract_report' - _template = 'hr_attendance.report_attendanceerrors' - _wrapped_report_class = attendance_print diff --git a/addons/hr_attendance/report/hr_employee_badge.xml b/addons/hr_attendance/report/hr_employee_badge.xml new file mode 100644 index 00000000000..387d8464716 --- /dev/null +++ b/addons/hr_attendance/report/hr_employee_badge.xml @@ -0,0 +1,44 @@ + + + + + + \ No newline at end of file diff --git a/addons/hr_attendance/security/hr_attendance_security.xml b/addons/hr_attendance/security/hr_attendance_security.xml new file mode 100644 index 00000000000..d033786fd5e --- /dev/null +++ b/addons/hr_attendance/security/hr_attendance_security.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + Manual Attendances + The user will gain access to the human resources attendance menu, enabling him to manage his own attendance. + + + + Enable PIN use + + The user will have to enter his PIN to check in and out manually at the company screen. + + + + + + attendance officer: full access + + [(1,'=',1)] + + + + + user: modify own attendance only + + [('employee_id.user_id','=',user.id)] + + + + + + + + + diff --git a/addons/hr_attendance/security/ir.model.access.csv b/addons/hr_attendance/security/ir.model.access.csv index ceb8acfe49e..9f9d88c44a5 100644 --- a/addons/hr_attendance/security/ir.model.access.csv +++ b/addons/hr_attendance/security/ir.model.access.csv @@ -1,5 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_hr_action_reason_user,hr.action.reason.user,model_hr_action_reason,base.group_hr_user,1,1,1,1 -access_hr_action_reason_employee,hr.action.reason.employee,model_hr_action_reason,base.group_user,1,1,1,1 access_hr_attendance_user,hr.attendance.user,model_hr_attendance,base.group_hr_user,1,1,1,1 -access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,1,1,1 +access_hr_attendance_system_user,hr.attendance.system.user,model_hr_attendance,base.group_user,1,1,1,0 \ No newline at end of file diff --git a/addons/hr_attendance/security/ir_rule.xml b/addons/hr_attendance/security/ir_rule.xml deleted file mode 100644 index 103dc14ba10..00000000000 --- a/addons/hr_attendance/security/ir_rule.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - Manager Attendance - - [(1,'=',1)] - - - - Employee Attendance - - [('employee_id.user_id','=',user.id)] - - - - - diff --git a/addons/hr_attendance/static/src/css/attendance.css b/addons/hr_attendance/static/src/css/attendance.css deleted file mode 100644 index 767cfbc061a..00000000000 --- a/addons/hr_attendance/static/src/css/attendance.css +++ /dev/null @@ -1,3 +0,0 @@ -.oe_systray #oe_attendance_sign_in_out_icon { - color: white; -} \ No newline at end of file diff --git a/addons/hr_attendance/static/src/js/attendance.js b/addons/hr_attendance/static/src/js/attendance.js deleted file mode 100644 index 355afc04c7f..00000000000 --- a/addons/hr_attendance/static/src/js/attendance.js +++ /dev/null @@ -1,85 +0,0 @@ -odoo.define('hr_attendance.hr_attendance', function(require) { -"use strict"; - -var core = require('web.core'); -var data = require('web.data'); -var formats = require('web.formats'); -var session = require('web.session'); -var SystrayMenu = require('web.SystrayMenu'); -var time = require('web.time'); -var Widget = require('web.Widget'); - -var _t = core._t; - -var AttendanceSlider = Widget.extend({ - template: 'AttendanceSlider', - init: function (parent) { - this._super(parent); - this.set({"signed_in": false}); - }, - start: function() { - var self = this; - var tmp = function() { - var $sign_in_out_icon = this.$('#oe_attendance_sign_in_out_icon'); - $sign_in_out_icon.toggleClass("fa-sign-in", ! this.get("signed_in")); - $sign_in_out_icon.toggleClass("fa-sign-out", this.get("signed_in")); - }; - this.on("change:signed_in", this, tmp); - _.bind(tmp, this)(); - this.$(".oe_attendance_sign_in_out").click(function(ev) { - ev.preventDefault(); - self.do_update_attendance(); - }); - this.$el.tooltip({ - title: function() { - var last_text = formats.format_value(self.last_sign, {type: "datetime"}); - var current_text = formats.format_value(new Date(), {type: "datetime"}); - var duration = self.last_sign ? moment(self.last_sign).fromNow() : "none"; - if (self.get("signed_in")) { - return _.str.sprintf(_t("Last sign in: %s,
%s.
Click to sign out."), last_text, duration); - } else { - return _.str.sprintf(_t("Click to Sign In at %s."), current_text); - } - }, - }); - return this.check_attendance(); - }, - do_update_attendance: function () { - var self = this; - var hr_employee = new data.DataSet(self, 'hr.employee'); - hr_employee.call('attendance_action_change', [ - [self.employee.id] - ]).done(function (result) { - self.last_sign = new Date(); - self.set({"signed_in": ! self.get("signed_in")}); - }); - }, - check_attendance: function () { - var self = this; - self.employee = false; - this.do_hide(); - var employee = new data.DataSetSearch(self, 'hr.employee', self.session.user_context, [ - ['user_id', '=', self.session.uid] - ]); - return employee.read_slice(['id', 'name', 'state', 'last_sign', 'attendance_access']).then(function (res) { - if (_.isEmpty(res) ) - return; - if (res[0].attendance_access === false){ - return; - } - self.do_show(); - self.employee = res[0]; - self.last_sign = time.str_to_datetime(self.employee.last_sign); - self.set({"signed_in": self.employee.state !== "absent"}); - }); - }, -}); - -session.is_bound.then(function () { - // Put the AttendanceSlider widget in the systray menu if the user is an employee - if (session.is_employee) { - SystrayMenu.Items.push(AttendanceSlider); - } -}); - -}); diff --git a/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js b/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js new file mode 100644 index 00000000000..b3810f05a2d --- /dev/null +++ b/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js @@ -0,0 +1,26 @@ + +odoo.define('hr_attendance.employee_kanban_view_handler', function(require) { +"use strict"; + +var KanbanRecord = require('web_kanban.Record'); + +KanbanRecord.include({ + on_card_clicked: function() { + if (this.model === 'hr.employee' && this.$el.parents('.o_hr_employee_attendance_kanban').length) { + // needed to diffentiate : check in/out kanban view of employees <-> standard employee kanban view + var action = { + type: 'ir.actions.client', + name: 'Confirm', + tag: 'hr_attendance_main_menu_confirm', + employee_id: this.record.id.raw_value, + employee_name: this.record.name.raw_value, + employee_state: this.record.attendance_state.raw_value, + }; + this.do_action(action); + } else { + this._super.apply(this, arguments); + } + } +}); + +}); diff --git a/addons/hr_attendance/static/src/js/main_menu.js b/addons/hr_attendance/static/src/js/main_menu.js new file mode 100644 index 00000000000..aa6d6682381 --- /dev/null +++ b/addons/hr_attendance/static/src/js/main_menu.js @@ -0,0 +1,71 @@ +odoo.define('hr_attendance.MainMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var Widget = require('web.Widget'); +var Session = require('web.session'); +var BarcodeHandlerMixin = require('barcodes.BarcodeHandlerMixin'); + +var QWeb = core.qweb; +var _t = core._t; + + +var MainMenu = Widget.extend(BarcodeHandlerMixin, { + events: { + "click .o_hr_attendance_button_employees": function(){ this.do_action('hr_attendance.hr_employee_attendance_action_kanban'); }, + }, + + init: function (parent, action) { + // Note: BarcodeHandlerMixin.init calls this._super.init, so there's no need to do it here. + // Yet, "_super" must be present in a function for the class mechanism to replace it with the actual parent method. + this._super; + BarcodeHandlerMixin.init.apply(this, arguments); + }, + + start: function () { + var self = this; + self.session = Session; + var res_company = new Model('res.company'); + res_company.query(['name']) + .filter([['id', '=', self.session.company_id]]) + .all() + .then(function (companies){ + self.company_name = companies[0].name; + self.company_image_url = self.session.url('/web/image', {model: 'res.company', id: self.session.company_id, field: 'logo',}) + self.$el.html(QWeb.render("HrAttendanceMainMenu", {widget: self})); + self.start_clock(); + }); + return self._super.apply(this, arguments); + }, + + on_barcode_scanned: function(barcode) { + var self = this; + var hr_employee = new Model('hr.employee'); + hr_employee.call('attendance_scan', [barcode, ]) + .then(function (result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + } + }); + }, + + start_clock: function() { + this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));}, 500); + // First clock refresh before interval to avoid delay + this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'})); + }, + + destroy: function () { + clearInterval(this.clock_start); + this._super.apply(this, arguments); + }, +}); + +core.action_registry.add('hr_attendance_main_menu', MainMenu); + +return MainMenu; + +}); diff --git a/addons/hr_attendance/static/src/js/main_menu_confirm.js b/addons/hr_attendance/static/src/js/main_menu_confirm.js new file mode 100644 index 00000000000..81ea12da883 --- /dev/null +++ b/addons/hr_attendance/static/src/js/main_menu_confirm.js @@ -0,0 +1,91 @@ +odoo.define('hr_attendance.MainMenuConfirm', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + + +var MainMenuConfirm = Widget.extend({ + events: { + "click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, + "click .o_hr_attendance_sign_in_out_icon": function () { + var self = this; + this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); + var hr_employee = new Model('hr.employee'); + hr_employee.call('attendance_manual', [[this.employee_id], this.next_action]) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + self.$('.o_hr_attendance_sign_in_out_icon').removeAttr("disabled"); + } + }); + }, + 'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); }, + 'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); }, + 'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); }, + 'click .o_hr_attendance_pin_pad_button_3': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 3); }, + 'click .o_hr_attendance_pin_pad_button_4': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 4); }, + 'click .o_hr_attendance_pin_pad_button_5': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 5); }, + 'click .o_hr_attendance_pin_pad_button_6': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 6); }, + 'click .o_hr_attendance_pin_pad_button_7': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 7); }, + 'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); }, + 'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); }, + 'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); }, + 'click .o_hr_attendance_pin_pad_button_ok': function() { + var self = this; + this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled"); + var hr_employee = new Model('hr.employee'); + hr_employee.call('attendance_manual', [[this.employee_id], this.next_action, this.$('.o_hr_attendance_PINbox').val()]) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500); + } + }); + }, + }, + + init: function (parent, action) { + this._super.apply(this, arguments); + this.next_action = { type: "ir.actions.client", name: "Attendances", tag: 'hr_attendance_main_menu', target: "fullscreen", params: {} }; + this.employee_id = action.employee_id; + this.employee_name = action.employee_name; + this.employee_state = action.employee_state; + var self = this; + }, + + start: function () { + var self = this; + self.session.user_has_group('base.group_hr_attendance_use_pin').then(function(has_group){ + self.use_pin = has_group; + self.$el.html(QWeb.render("HrAttendanceMainMenuConfirm", {widget: self})); + self.start_clock(); + }); + return self._super.apply(this, arguments); + }, + + start_clock: function () { + this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'}));}, 500); + // First clock refresh before interval to avoid delay + this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit'})); + }, + + destroy: function () { + clearInterval(this.clock_start); + this._super.apply(this, arguments); + }, +}); + +core.action_registry.add('hr_attendance_main_menu_confirm', MainMenuConfirm); + +return MainMenuConfirm; + +}); diff --git a/addons/hr_attendance/static/src/js/message.js b/addons/hr_attendance/static/src/js/message.js new file mode 100644 index 00000000000..1c8b9b4ce66 --- /dev/null +++ b/addons/hr_attendance/static/src/js/message.js @@ -0,0 +1,131 @@ +odoo.define('hr_attendance.Message', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var Widget = require('web.Widget'); + +var _t = core._t; + + +var Message = Widget.extend({ + template: 'HrAttendanceMessage', + + events: { + "click .o_hr_attendance_button_dismiss": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, + }, + + init: function(parent, action) { + this._super.apply(this, arguments); + var self = this; + var default_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_my_main_menu', params: {} }; + if(!action.attendance){ + self.session.user_has_group('base.group_hr_user').then(function(has_group){ + if(has_group){ + self.next_action = { type: "ir.actions.client", name: 'Attendances', tag: 'hr_attendance_main_menu', target: 'fullscreen', params: {} }; + } + }); + return; + } + this.next_action = action.next_action || default_action; + this.attendance = action.attendance; + this.attendance.check_in_time = (new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); + this.attendance.check_out_time = this.attendance.check_out && (new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000)).toTimeString().slice(0,8); + this.previous_attendance_change_date = action.previous_attendance_change_date; + this.employee_name = action.employee_name; + }, + + start: function() { + if (!this.attendance){ + return; + } + if (this.attendance.check_out) { + this.farewell_message(); + } else { + this.welcome_message(); + } + }, + + welcome_message: function() { + var self = this; + var now = new Date((new Date(this.attendance.check_in)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); + self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + + if(now.getHours()<5){ + self.$('.o_hr_attendance_message_message').append(_t("Good night")); + } else if(now.getHours() < 12){ + if(now.getHours() < 8 && Math.random() < .3){ + if(Math.random()<.75){ + self.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm")); + } else { + self.$('.o_hr_attendance_message_message').append(_t("First come, first served")); + } + + } else { + self.$('.o_hr_attendance_message_message').append(_t("Good morning")); + } + } else if(now.getHours()<17){ + self.$('.o_hr_attendance_message_message').append(_t("Good afternoon")); + } else if(now.getHours()<23){ + self.$('.o_hr_attendance_message_message').append(_t("Good evening")); + } else { + self.$('.o_hr_attendance_message_message').append(_t("Good night")); + } + if(this.previous_attendance_change_date){ + var last_check_out_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); + if(now.valueOf() - last_check_out_date.valueOf() > 1000*60*60*24*7){ + self.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!")); + } else { + if(Math.random() < .02){ + self.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!")); + } + } + } + }, + + farewell_message: function() { + var self = this; + var now = new Date((new Date(this.attendance.check_out)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); + self.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + + if(this.previous_attendance_change_date){ + var last_check_in_date = new Date((new Date(this.previous_attendance_change_date)).valueOf() - (new Date()).getTimezoneOffset()*60*1000); + if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*12){ + self.$('.o_hr_attendance_warning_message').append(_t("Warning! Last check in was over 12 hours ago.
If this isn't right, please contact Human Resources.")); + clearTimeout(self.return_to_main_menu); + } else if(now.valueOf() - last_check_in_date.valueOf() > 1000*60*60*8){ + self.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!")); + } + } + + if(now.getHours()<12){ + self.$('.o_hr_attendance_message_message').append(_t("Have a good day!")); + } else if(now.getHours()<14){ + self.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!")); + if(Math.random() < 0.05){ + self.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar")); + } else if(Math.random() < 0.06){ + self.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away")); + } + } else if(now.getHours()<17){ + self.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon")); + } else { + if(now.getHours()<18 && Math.random()<0.2){ + self.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise")); + } else { + self.$('.o_hr_attendance_message_message').append(_t("Have a good evening")); + } + } + }, + + destroy: function () { + clearTimeout(this.return_to_main_menu); + this._super.apply(this, arguments); + }, +}); + +core.action_registry.add('hr_attendance_message', Message); + +return Message; + +}); diff --git a/addons/hr_attendance/static/src/js/my_main_menu.js b/addons/hr_attendance/static/src/js/my_main_menu.js new file mode 100644 index 00000000000..3d7d4fb9ed2 --- /dev/null +++ b/addons/hr_attendance/static/src/js/my_main_menu.js @@ -0,0 +1,57 @@ +odoo.define('hr_attendance.MyMainMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var Widget = require('web.Widget'); + +var QWeb = core.qweb; +var _t = core._t; + + +var MyMainMenu = Widget.extend({ + events: { + "click .o_hr_attendance_sign_in_out_icon": function() { + this.$('.o_hr_attendance_sign_in_out_icon').attr("disabled", "disabled"); + this.update_attendance(); + }, + }, + + start: function () { + var self = this; + + var hr_employee = new Model('hr.employee'); + hr_employee.query(['attendance_state', 'name']) + .filter([['user_id', '=', self.session.uid]]) + .all() + .then(function (res) { + if (_.isEmpty(res) ) { + self.$('.o_hr_attendance_employee').append(_t("Error : Could not find employee linked to user")); + return; + } + self.employee = res[0]; + self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self})); + }); + + return this._super.apply(this, arguments); + }, + + update_attendance: function () { + var self = this; + var hr_employee = new Model('hr.employee'); + hr_employee.call('attendance_manual', [[self.employee.id], 'hr_attendance.hr_attendance_action_my_main_menu']) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + } + }); + }, +}); + +core.action_registry.add('hr_attendance_my_main_menu', MyMainMenu); + +return MyMainMenu; + +}); diff --git a/addons/hr_attendance/static/src/js/presence_widgets.js b/addons/hr_attendance/static/src/js/presence_widgets.js new file mode 100644 index 00000000000..c74f8d82249 --- /dev/null +++ b/addons/hr_attendance/static/src/js/presence_widgets.js @@ -0,0 +1,52 @@ +odoo.define('hr_attendance.presence_widgets', function (require) { +"use strict"; + +var core = require('web.core'); +var form_common = require('web.form_common'); +var kanban_widgets = require('web_kanban.widgets'); + +var QWeb = core.qweb; +var _t = core._t; + +var FormPresenceIndicator = form_common.AbstractField.extend({ + init: function() { + this._super.apply(this, arguments); + }, + start: function() { + this.display_field(); + // this.render_value(); -> gets called automatically in form_common.AbstractField + this.$el.tooltip({title: _t("employee presence
green: checked in
red: checked out"), trigger: 'hover'}); + return this._super(); + }, + render_value: function() { + this.$('.oe_hr_attendance_form_status').toggleClass("oe_hr_attendance_form_status_green", this.get_value() == 'checked_in'); + this.$('.oe_hr_attendance_form_status').toggleClass("oe_hr_attendance_form_status_red", this.get_value() == 'checked_out'); + }, + display_field: function() { + this.$el.html(QWeb.render("FormPresenceIndicator")); + }, +}); + +var KanbanPresenceIndicator = kanban_widgets.AbstractField.extend({ + init: function() { + this._super.apply(this, arguments); + }, + start: function() { + this.display_field(); + this.render_value(); // doesn't get called automatically in kanban_widgets.AbstractField + this.$el.tooltip({title: _t("employee presence
green: checked in
red: checked out"), trigger: 'hover'}); + return this._super(); + }, + render_value: function() { + this.$('.oe_kanban_status').toggleClass("oe_kanban_status_green", this.field.raw_value == 'checked_in'); + this.$('.oe_kanban_status').toggleClass("oe_kanban_status_red", this.field.raw_value == 'checked_out'); + }, + display_field: function() { + this.$el.html(QWeb.render("KanbanPresenceIndicator")); + }, +}); + +core.form_widget_registry.add('hr_attendance_form_presence_indicator', FormPresenceIndicator); +kanban_widgets.registry.add('hr_attendance_kanban_presence_indicator', KanbanPresenceIndicator); + +}); \ No newline at end of file diff --git a/addons/hr_attendance/static/src/less/hr_attendance.less b/addons/hr_attendance/static/src/less/hr_attendance.less new file mode 100644 index 00000000000..a384ffbcd42 --- /dev/null +++ b/addons/hr_attendance/static/src/less/hr_attendance.less @@ -0,0 +1,211 @@ +.o_hr_attendance_sign_in_out_icon { + font-size: 15em; + cursor: pointer; + margin: 20px 0px 20px 0px; + padding: 0px 15px 0px 15px; + border-radius: 10%; + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6); +} + +.oe_hr_attendance_form_status { + height: .6em; + width: .6em; + border-radius: 50%; + margin-right: .3em; + display: inline-block; + + background-color: @odoo-brand-secondary; + + &.oe_hr_attendance_form_status_green { + background-color: @brand-success; + } + &.oe_hr_attendance_form_status_red { + background-color: @brand-danger; + } +} + +.o_hr_attendance_back_button { + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6); + font-size: 3em; + border-radius: 50%; + height: 1.7em; + width: 1.7em; + position: absolute; + left: auto; + right: 0px; + top: 0px; + bottom: auto; + + @media (min-width: @screen-xs-max) { + transform: translate(50%, -50%); + } +} + +.o_hr_attendance_validation { + color: green; +} + +.o_hr_attendance_btn-round-corners { + border-radius: 10%; +} + +.o_hr_attendance_pin_pad_border { + margin: 10px 0px 10px 0px; +} + +.o_hr_attendance_PINbox { + background: white; + border: 1px solid #d5d5d5; + text-align: center; + margin: 5px 0px 5px 0px; +} + +.o_hr_attendance_main_menu_container { + .o-flex-display(); + .o-flex-flow(column, nowrap); + .o-justify-content(center); + .o-align-items(center); + + @media (min-width: @screen-xs-max) { + background: url("../../../../web_enterprise/static/src/img/application-switcher-bg.jpg") no-repeat center center fixed; + background-size: cover; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +} + +.o_hr_attendance_warning_message { + color: @brand-warning; +} +.o_hr_attendance_random_message { + font-style: italic; +} +.o_hr_attendance_main_menu { + width: 100%; + text-align: center; + position: relative; + background-color: #fff; + padding: 2em; + + h1 { + margin: 0 0 2rem 0; + } + + .message_demo_barcodes { + font-size: 0.9em; + margin: 0; + } + + img { + overflow:hidden; // prevent margins colapsing with h1 + margin-top: 3rem; + width: 200px; + } + + p { + text-align: left; + margin: 3rem 0; + } + + > button { + font-size: 1.2em; + margin-bottom: 2rem; + width: 100%; + } + + > button:last-child { + margin-bottom: 0; + } + + @media (min-width: @screen-xs-max) { + flex: 0 0 auto; + width: 550px; + border-radius: 10px; + background-color: rgba(255,255,255,0.8); + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6); + font-size: 1.2em; + padding: 3em; + } +} + +.o_hr_attendance_clock { + font-size: 1.5em; + text-align: center; + position: absolute; + top: 20px; + right: 20px; + + @media (min-width: @screen-xs-max) { + flex: 0 0 auto; + border-radius: 3px; + background-color: rgba(255,255,255,0.5); + padding: 3px 10px 3px 10px; + } +} + +.o_hr_attendance_message_container { + .o-flex-display(); + .o-flex-flow(column, nowrap); + .o-justify-content(center); + .o-align-items(center); + + @media (min-width: @screen-xs-max) { + background: url("../../../../web_enterprise/static/src/img/application-switcher-bg.jpg") no-repeat center center fixed; + background-size: cover; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } +} + +.o_hr_attendance_message { + width: 100%; + text-align: center; + background-color: #fff; + padding: 2em; + + h1 { + margin: 0 0 2rem 0; + } + + .message_demo_barcodes { + font-size: 0.9em; + margin: 0; + } + + img { + overflow:hidden; // prevent margins colapsing with h1 + margin-top: 3rem; + width: 200px; + } + + p { + text-align: left; + margin: 3rem 0; + } + + > button { + font-size: 1.2em; + margin-bottom: 2rem; + width: 100%; + } + + > button:last-child { + margin-bottom: 0; + } + + @media (min-width: @screen-xs-max) { + flex: 0 0 auto; + width: 550px; + border-radius: 10px; + background-color: rgba(255,255,255,0.8); + box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6); + font-size: 1.2em; + padding: 3em; + } +} \ No newline at end of file diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml index bed10677d2c..7d0ea6caae6 100644 --- a/addons/hr_attendance/static/src/xml/attendance.xml +++ b/addons/hr_attendance/static/src/xml/attendance.xml @@ -1,9 +1,132 @@ -