diff --git a/hr_attendance_work_entry/__init__.py b/hr_attendance_work_entry/__init__.py new file mode 100644 index 00000000..26c033a8 --- /dev/null +++ b/hr_attendance_work_entry/__init__.py @@ -0,0 +1,15 @@ +from . import models + + +def attn_payroll_pre_init_hook(cr): + """ + This module installs a Work Entry Type with code "ATTN" + If you have undergone a migration (either for this module + or even your own Payslip Work Entry lines with code "ATTN") + then the uniqueness constraint will prevent this module + from installing. + """ + cr.execute("UPDATE hr_work_entry_type " + "SET code = 'ATTN-PRE-INSTALL' " + "WHERE code = 'ATTN';" + ) diff --git a/hr_attendance_work_entry/__manifest__.py b/hr_attendance_work_entry/__manifest__.py new file mode 100755 index 00000000..9c4259ae --- /dev/null +++ b/hr_attendance_work_entry/__manifest__.py @@ -0,0 +1,33 @@ +{ + 'name': 'Attendance Work Entry Type', + 'description': 'Set work types on attendance records.', + 'version': '16.0.1.0.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Human Resources', + 'depends': [ + 'hr_attendance', + 'hr_work_entry', + ], + 'data': [ + 'data/hr_attendance_work_entry_data.xml', + 'views/attendance_views.xml', + 'views/employee_views.xml', + 'views/work_entry_views.xml', + ], + 'demo': [ + 'data/hr_attendance_work_entry_demo.xml', + ], + 'assets': { + 'web.assets_qweb': [ + 'hr_attendance_work_entry/static/src/xml/hr_attendance.xml', + ], + 'web.assets_backend': [ + 'hr_attendance_work_entry/static/src/js/kiosk_confirm.js', + 'hr_attendance_work_entry/static/src/js/my_attendances.js', + 'hr_attendance_work_entry/static/src/scss/hr_attendances.scss', + ], + }, + 'pre_init_hook': 'attn_payroll_pre_init_hook', +} diff --git a/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml b/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml new file mode 100644 index 00000000..ab99a1ca --- /dev/null +++ b/hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml @@ -0,0 +1,17 @@ + + + + + Attendance + ATTN + + checked_in + fa-sign-in + + + + + Work Calendar + + + \ No newline at end of file diff --git a/hr_attendance_work_entry/data/hr_attendance_work_entry_demo.xml b/hr_attendance_work_entry/data/hr_attendance_work_entry_demo.xml new file mode 100644 index 00000000..e88356b2 --- /dev/null +++ b/hr_attendance_work_entry/data/hr_attendance_work_entry_demo.xml @@ -0,0 +1,20 @@ + + + + + Break + ATTN_BREAK + + break + fa-hourglass-1 + + + + Lunch + ATTN_LUNCH + + lunch + fa-coffee + + + \ No newline at end of file diff --git a/hr_attendance_work_entry/i18n/es.po b/hr_attendance_work_entry/i18n/es.po new file mode 100644 index 00000000..eebfbc6a --- /dev/null +++ b/hr_attendance_work_entry/i18n/es.po @@ -0,0 +1,266 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_attendance_work_entry +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-10-30 01:04+0000\n" +"PO-Revision-Date: 2021-10-30 01:04+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_attendance_work_entry +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_employees_view_kanban_inherit +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_kanban_view_employees_inherit +msgid "" +"" +msgstr "" +"" + +#. module: hr_attendance_work_entry +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_employees_view_kanban_inherit +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_kanban_view_employees_inherit +msgid "" +"" +msgstr "" +"" +msgstr "" +"" + +#. module: hr_attendance_work_entry +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_employees_view_kanban_inherit +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_kanban_view_employees_inherit +msgid "" +"" +msgstr "" +"" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields,field_description:hr_attendance_work_entry.field_hr_work_entry_type__allow_attendance +msgid "Allow in Attendance" +msgstr "Permitido en Asistencia" + +#. module: hr_attendance_work_entry +#: model:hr.work.entry.type,name:hr_attendance_work_entry.work_input_attendance +#: model:ir.model,name:hr_attendance_work_entry.model_hr_attendance +#: model_terms:ir.ui.view,arch_db:hr_attendance_work_entry.hr_work_entry_type_view_form_inherit +msgid "Attendance" +msgstr "Asistencia" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields,field_description:hr_attendance_work_entry.field_hr_work_entry_type__attendance_icon +msgid "Attendance Icon" +msgstr "Icono de Asistencia" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields,field_description:hr_attendance_work_entry.field_hr_work_entry_type__attendance_state +msgid "Attendance State" +msgstr "Estado de Asistencia" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields,field_description:hr_attendance_work_entry.field_hr_employee__attendance_state +msgid "Attendance Status" +msgstr "Estado de Asistencia" + +#. module: hr_attendance_work_entry +#: code:addons/hr_attendance_work_entry/models/work_entry.py:0 +#: model:ir.model.fields.selection,name:hr_attendance_work_entry.selection__hr_employee__attendance_state__break +#: model:ir.model.fields.selection,name:hr_attendance_work_entry.selection__hr_work_entry_type__attendance_state__break +#, python-format +msgid "Break" +msgstr "Descanso" + +#. module: hr_attendance_work_entry +#: code:addons/hr_attendance_work_entry/models/employee.py:0 +#, python-format +msgid "" +"Cannot perform check out on %(empl_name)s, could not find corresponding " +"check in. Your attendances have probably been modified manually by human " +"resources." +msgstr "" +"No se pudo realizar un registro de salida para %(empl_name)s, ya que no se " +"encontro un registro de entrada correspondiente. Sus asistencias han " +"probablemente sido modificado manualmente for recursos humanos" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields.selection,name:hr_attendance_work_entry.selection__hr_work_entry_type__attendance_state__checked_in +msgid "Checked in" +msgstr "Registro de Entrada" + +#. module: hr_attendance_work_entry +#: model:ir.model,name:hr_attendance_work_entry.model_hr_employee +msgid "Employee" +msgstr "Empleado" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Error: could not find corresponding employee." +msgstr "Error: No se pudo encontrar el empleado correspondiente" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Go back" +msgstr "Volver" + +#. module: hr_attendance_work_entry +#: model:ir.model,name:hr_attendance_work_entry.model_hr_work_entry_type +msgid "HR Work Entry Type" +msgstr "Tipo de entrada de trabajo de RRHH" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields.selection,name:hr_attendance_work_entry.selection__hr_employee__attendance_state__lunch +#: model:ir.model.fields.selection,name:hr_attendance_work_entry.selection__hr_work_entry_type__attendance_state__lunch +msgid "Lunch" +msgstr "Almuerzo" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Or, continue working as:" +msgstr "O, Sigue trabajando como:" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Please enter your PIN to" +msgstr "Por favor ingrese su PIN para" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Please return to the main menu." +msgstr "Por favor regrese al menú principal" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Sign Out" +msgstr "Cerrar Sesión" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Sign in" +msgstr "Iniciar Sesión" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Sign out" +msgstr "Cerrar Sesión" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Stop" +msgstr "Detener" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Today's work hours:" +msgstr "Horas de trabajo de hoy:" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Want to check out?" +msgstr "¿Desea registrar una salida?" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "" +"Warning : Your user should be linked to an employee to use attendance. " +"Please contact your administrator." +msgstr "" +"Advertencia : Su usuario debe estar vinculado a un empleado para usarla en asistencias." +"Por favor contacte su administrador" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "Welcome!" +msgstr "¡Bienvenida!" + +#. module: hr_attendance_work_entry +#: model:ir.ui.menu,name:hr_attendance_work_entry.hr_work_entry_type_menu +msgid "Work Entry Types" +msgstr "Tipos de entradas de trabajo" + +#. module: hr_attendance_work_entry +#: model:ir.model.fields,field_description:hr_attendance_work_entry.field_hr_attendance__work_type_id +msgid "Work Type" +msgstr "Tipo de trabajo" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "check in" +msgstr "el Registro de entrada" + +#. module: hr_attendance_work_entry +#. openerp-web +#: code:addons/hr_attendance_work_entry/static/src/xml/hr_attendance.xml:0 +#, python-format +msgid "check out" +msgstr "el Registro de salida" diff --git a/hr_attendance_work_entry/models/__init__.py b/hr_attendance_work_entry/models/__init__.py new file mode 100644 index 00000000..1e37c49d --- /dev/null +++ b/hr_attendance_work_entry/models/__init__.py @@ -0,0 +1,3 @@ +from . import attendance +from . import employee +from . import work_entry diff --git a/hr_attendance_work_entry/models/attendance.py b/hr_attendance_work_entry/models/attendance.py new file mode 100644 index 00000000..cd0d35b5 --- /dev/null +++ b/hr_attendance_work_entry/models/attendance.py @@ -0,0 +1,14 @@ +from odoo import api, fields, models + + +class HrAttendance(models.Model): + _inherit = 'hr.attendance' + + work_type_id = fields.Many2one('hr.work.entry.type', string='Work Type', + default=lambda self: self.env.ref('hr_attendance_work_entry.work_input_attendance', + raise_if_not_found=False)) + + @api.model + def gather_attendance_work_types(self): + work_types = self.env['hr.work.entry.type'].sudo().search([('allow_attendance', '=', True)]) + return work_types.read(['id', 'name', 'attendance_icon']) diff --git a/hr_attendance_work_entry/models/employee.py b/hr_attendance_work_entry/models/employee.py new file mode 100644 index 00000000..e57a422b --- /dev/null +++ b/hr_attendance_work_entry/models/employee.py @@ -0,0 +1,61 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + attendance_state = fields.Selection(selection_add=[('break', 'Break'), ('lunch', 'Lunch')]) + + @api.depends('last_attendance_id.work_type_id', 'last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id') + def _compute_attendance_state(self): + for employee in self: + att = employee.last_attendance_id.sudo() + if not att or att.check_out: + employee.attendance_state = 'checked_out' + elif employee.last_attendance_id.work_type_id.attendance_state: + employee.attendance_state = employee.last_attendance_id.work_type_id.attendance_state + else: + employee.attendance_state = 'checked_in' + + def attendance_manual(self, next_action, entered_pin=None, work_type_id=None): + self = self.with_context(work_type_id=work_type_id) + if not entered_pin: + # fix for pin mode with specific argument order for work_type_id + entered_pin = None + return super(HrEmployee, self).attendance_manual(next_action, entered_pin=entered_pin) + + 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 + """ + self.ensure_one() + action_date = fields.Datetime.now() + work_type_id = self._context.get('work_type_id', False) + + if self.attendance_state == 'checked_out': + vals = { + 'employee_id': self.id, + 'check_in': action_date, + } + if work_type_id: + # if we don't have a work_type_id, we want the default + vals['work_type_id'] = work_type_id + return self.env['hr.attendance'].create(vals) + attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) + if attendance and work_type_id: + # work_type_id is the "next" attendance type + attendance.check_out = action_date + vals = { + 'employee_id': self.id, + 'check_in': action_date, + 'work_type_id': work_type_id, + } + return self.env['hr.attendance'].create(vals) + if attendance: + attendance.check_out = action_date + else: + raise 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.sudo().name, }) + return attendance diff --git a/hr_attendance_work_entry/models/work_entry.py b/hr_attendance_work_entry/models/work_entry.py new file mode 100644 index 00000000..73618223 --- /dev/null +++ b/hr_attendance_work_entry/models/work_entry.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class HrWorkEntryType(models.Model): + _inherit = 'hr.work.entry.type' + _order = 'sequence, id' + + allow_attendance = fields.Boolean(string='Allow in Attendance') + attendance_icon = fields.Char(string='Attendance Icon', default='fa-sign-in') + attendance_state = fields.Selection([ + # ('checked_out', "Checked out"), # reserved for detecting new punch in + ('checked_in', "Checked in"), + ('break', 'Break'), + ('lunch', 'Lunch'), + ], string='Attendance State', default='checked_in') diff --git a/hr_attendance_work_entry/static/src/js/kiosk_confirm.js b/hr_attendance_work_entry/static/src/js/kiosk_confirm.js new file mode 100644 index 00000000..3c43b36d --- /dev/null +++ b/hr_attendance_work_entry/static/src/js/kiosk_confirm.js @@ -0,0 +1,69 @@ +odoo.define('hr_attendance_work_entry.kiosk_confirm', function (require) { +"use strict"; + +var KioskConfirm = require('hr_attendance.kiosk_confirm'); +var KioskConfirmTyped = KioskConfirm.extend({ + events: _.extend({}, KioskConfirm.prototype.events, { + "click .o_hr_attendance_punch_type": _.debounce(function(e) { + var work_entry_type = $(e.target).data('work-entry-type'); + this.update_attendance(work_entry_type); + }, 200, true), + "click .o_hr_attendance_pin_pad_button_work": _.debounce(function(e) { + var work_entry_type = $(e.target).data('work-entry-type'); + this.update_attendance_pin(work_entry_type); + }, 200, true), + }), + willStart: function () { + var self = this; + + var def = this._rpc({ + model: 'hr.attendance', + method: 'gather_attendance_work_types', + args: []}) + .then(function (res) { + self.work_types = res; + }); + + return Promise.all([def, this._super.apply(this, arguments)]); + }, + update_attendance: function (type) { + var self = this; + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[self.employee_id], this.next_action, false, type], + }) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + } + }); + }, + update_attendance_pin: function (type) { + var self = this; + this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled"); + this.$('.o_hr_attendance_pin_pad_button_work').attr("disabled", "disabled"); + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[this.employee_id], this.next_action, this.$('.o_hr_attendance_PINbox').val(), type], + }) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + self.$('.o_hr_attendance_PINbox').val(''); + setTimeout( function() { + self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); + self.$('.o_hr_attendance_pin_pad_button_work').removeAttr("disabled"); + }, 500); + } + }); + }, +}); + +return KioskConfirmTyped; +}); \ No newline at end of file diff --git a/hr_attendance_work_entry/static/src/js/my_attendances.js b/hr_attendance_work_entry/static/src/js/my_attendances.js new file mode 100644 index 00000000..a72d4d6e --- /dev/null +++ b/hr_attendance_work_entry/static/src/js/my_attendances.js @@ -0,0 +1,47 @@ +odoo.define('hr_attendance_work_entry.my_attendances', function (require) { +"use strict"; + +var MyAttendances = require('hr_attendance.my_attendances'); + +var MyTypedAttendances = MyAttendances.extend({ + events: _.extend({}, MyAttendances.prototype.events, { + "click .o_hr_attendance_punch_type": _.debounce(function(e) { + var work_entry_type = $(e.target).data('work-entry-type'); + this.update_attendance(work_entry_type); + }, 200, true), + }), + + willStart: function () { + var self = this; + + var def = this._rpc({ + model: 'hr.attendance', + method: 'gather_attendance_work_types', + args: []}) + .then(function (res) { + self.work_types = res; + }); + + return Promise.all([def, this._super.apply(this, arguments)]); + }, + + update_attendance: function (type) { + var self = this; + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[self.employee.id], 'hr_attendance.hr_attendance_action_my_attendances', false, type], + }) + .then(function(result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + } + }); + }, +}); + +return MyTypedAttendances; + +}); \ No newline at end of file diff --git a/hr_attendance_work_entry/static/src/scss/hr_attendances.scss b/hr_attendance_work_entry/static/src/scss/hr_attendances.scss new file mode 100644 index 00000000..308412de --- /dev/null +++ b/hr_attendance_work_entry/static/src/scss/hr_attendances.scss @@ -0,0 +1,31 @@ +.o_hr_attendance_sign_in_out_icon { + cursor: pointer; + border-radius: .1em; + box-shadow: inset 0 -3px 0 fade-out(black, 0.7); + + &.btn-secondary:hover { + color: $o-brand-primary; + } +} + +#oe_hr_attendance_status { + color: $o-brand-secondary; + + &.oe_hr_attendance_status_blue { + color: theme-color('info'); + } + + &.oe_hr_attendance_status_orange { + color: theme-color('warning'); + } +} + +.o_hr_attendance_kiosk_mode p.o_hr_attendance_continue { + margin-bottom: 0; + text-align: center; + font-weight: bold; +} + +.o_hr_attendance_pin_pad_button_work { + font-size: 0.9em; +} diff --git a/hr_attendance_work_entry/static/src/xml/hr_attendance.xml b/hr_attendance_work_entry/static/src/xml/hr_attendance.xml new file mode 100644 index 00000000..e356c106 --- /dev/null +++ b/hr_attendance_work_entry/static/src/xml/hr_attendance.xml @@ -0,0 +1,134 @@ + + + + + +
+ + + +
+ +
+

+

Welcome! + Want to check out?

+

Today's work hours:

+
+
+ +
+ + + +

+ Or, continue working as: +

+ + + + + + + +
+ +
+
+
+ + Warning : Your user should be linked to an employee to use attendance. Please contact your administrator. + +
+ +
+
+ + + +
+ + +
+ Go back + +
+ +
+ +
+

+

Welcome! Want to check out?

+

Today's work hours:

+ +
+ + + +

+ Or, continue working as: +

+ + + + + + + +
+
+ +

Please enter your PIN to check outcheck in

+
+
+
+
+
+
+ +
+ +
+ +
+ Sign Out +
+
+ +
+ +
+ + + +
+
+
+ + + +