From 87ac2897f322cf3a1a78e4d819c4091c067e78a6 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 18 Sep 2020 11:28:39 -0700 Subject: [PATCH 1/4] [NEW] hr_attendance_work_entry: for Odoo 13 --- hr_attendance_work_entry/__init__.py | 15 +++++ hr_attendance_work_entry/__manifest__.py | 17 ++++++ .../data/hr_attendance_work_entry_data.xml | 17 ++++++ hr_attendance_work_entry/models/__init__.py | 3 + hr_attendance_work_entry/models/attendance.py | 9 +++ hr_attendance_work_entry/models/employee.py | 58 +++++++++++++++++++ hr_attendance_work_entry/models/work_entry.py | 13 +++++ hr_attendance_work_entry/tests/__init__.py | 1 + .../tests/test_attendance_work_type.py | 47 +++++++++++++++ 9 files changed, 180 insertions(+) create mode 100644 hr_attendance_work_entry/__init__.py create mode 100755 hr_attendance_work_entry/__manifest__.py create mode 100644 hr_attendance_work_entry/data/hr_attendance_work_entry_data.xml create mode 100644 hr_attendance_work_entry/models/__init__.py create mode 100644 hr_attendance_work_entry/models/attendance.py create mode 100644 hr_attendance_work_entry/models/employee.py create mode 100644 hr_attendance_work_entry/models/work_entry.py create mode 100644 hr_attendance_work_entry/tests/__init__.py create mode 100644 hr_attendance_work_entry/tests/test_attendance_work_type.py 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..97d1d7ab --- /dev/null +++ b/hr_attendance_work_entry/__manifest__.py @@ -0,0 +1,17 @@ +{ + 'name': 'Attendance Work Entry Type', + 'description': 'Set work types on attendance records.', + 'version': '13.0.1.0.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Human Resources', + 'data': [ + 'data/hr_attendance_work_entry_data.xml', + ], + 'depends': [ + 'hr_attendance', + 'hr_work_entry', + ], + '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/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..273b1367 --- /dev/null +++ b/hr_attendance_work_entry/models/attendance.py @@ -0,0 +1,9 @@ +from odoo import 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)) diff --git a/hr_attendance_work_entry/models/employee.py b/hr_attendance_work_entry/models/employee.py new file mode 100644 index 00000000..d9caffc4 --- /dev/null +++ b/hr_attendance_work_entry/models/employee.py @@ -0,0 +1,58 @@ +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')]) + + @api.depends('last_attendance_id.work_type_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) + 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..b4088b0e --- /dev/null +++ b/hr_attendance_work_entry/models/work_entry.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class HrWorkEntryType(models.Model): + _inherit = 'hr.work.entry.type' + + 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'), + ], string='Attendance State', default='checked_in') diff --git a/hr_attendance_work_entry/tests/__init__.py b/hr_attendance_work_entry/tests/__init__.py new file mode 100644 index 00000000..170aaad8 --- /dev/null +++ b/hr_attendance_work_entry/tests/__init__.py @@ -0,0 +1 @@ +from . import test_attendance_work_type diff --git a/hr_attendance_work_entry/tests/test_attendance_work_type.py b/hr_attendance_work_entry/tests/test_attendance_work_type.py new file mode 100644 index 00000000..3b739d6d --- /dev/null +++ b/hr_attendance_work_entry/tests/test_attendance_work_type.py @@ -0,0 +1,47 @@ +from odoo.tests import common + + +class TestAttendanceWorkType(common.TransactionCase): + def setUp(self): + super().setUp() + self.employee = self.env.ref('hr.employee_hne') + self.default_work_type = self.env.ref('hr_attendance_work_entry.work_input_attendance') + + def test_01_work_type(self): + attendance = self.env['hr.attendance'].create({ + 'employee_id': self.employee.id, + 'check_in': '2020-01-06 10:00:00', # Monday + 'check_out': '2020-01-06 19:00:00', + }) + self.assertTrue(attendance.work_type_id) + self.assertEqual(attendance.work_type_id, self.default_work_type) + + def test_11_employee_clock_in(self): + self.assertEqual(self.employee.attendance_state, 'checked_out') + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, self.default_work_type) + self.assertEqual(self.employee.attendance_state, 'checked_in') + + # check out + self.employee._attendance_action_change() + self.assertEqual(self.employee.attendance_state, 'checked_out') + + def test_12_employee_clock_in_break(self): + # check in with non-standard work type + break_type = self.env['hr.work.entry.type'].create({ + 'name': 'Test Break', + 'code': 'TESTBREAK', + 'allow_attendance': True, + 'attendance_state': 'break', + }) + self.employee = self.employee.with_context(work_type_id=break_type.id) + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, break_type) + self.assertEqual(self.employee.attendance_state, 'break') + + # check back in immediately with default + self.employee = self.employee.with_context(work_type_id=self.default_work_type.id) + attendance = self.employee._attendance_action_change() + self.assertEqual(attendance.work_type_id, self.default_work_type) + self.assertEqual(attendance.work_type_id.attendance_state, 'checked_in') + self.assertEqual(self.employee.attendance_state, 'checked_in') From d52761f5bc3acd903682c1afec76c4bbdfe86ade Mon Sep 17 00:00:00 2001 From: Brett Spaulding Date: Fri, 18 Sep 2020 17:26:50 -0400 Subject: [PATCH 2/4] [IMP] hr_attendance_work_entry: Modified kiosk mode views and attendance views to allow for various punch types. --- hr_attendance_work_entry/__manifest__.py | 16 ++- .../data/hr_attendance_work_entry_demo.xml | 20 +++ hr_attendance_work_entry/models/attendance.py | 7 +- hr_attendance_work_entry/models/employee.py | 5 +- hr_attendance_work_entry/models/work_entry.py | 2 + .../static/src/js/kiosk_confirm.js | 71 ++++++++++ .../static/src/js/my_attendances.js | 50 +++++++ .../static/src/scss/hr_attendances.scss | 31 ++++ .../static/src/xml/hr_attendance.xml | 134 ++++++++++++++++++ .../views/attendance_views.xml | 15 ++ .../views/employee_views.xml | 58 ++++++++ hr_attendance_work_entry/views/web_assets.xml | 14 ++ .../views/work_entry_views.xml | 36 +++++ 13 files changed, 454 insertions(+), 5 deletions(-) create mode 100644 hr_attendance_work_entry/data/hr_attendance_work_entry_demo.xml create mode 100644 hr_attendance_work_entry/static/src/js/kiosk_confirm.js create mode 100644 hr_attendance_work_entry/static/src/js/my_attendances.js create mode 100644 hr_attendance_work_entry/static/src/scss/hr_attendances.scss create mode 100644 hr_attendance_work_entry/static/src/xml/hr_attendance.xml create mode 100644 hr_attendance_work_entry/views/attendance_views.xml create mode 100644 hr_attendance_work_entry/views/employee_views.xml create mode 100644 hr_attendance_work_entry/views/web_assets.xml create mode 100644 hr_attendance_work_entry/views/work_entry_views.xml diff --git a/hr_attendance_work_entry/__manifest__.py b/hr_attendance_work_entry/__manifest__.py index 97d1d7ab..61ab485f 100755 --- a/hr_attendance_work_entry/__manifest__.py +++ b/hr_attendance_work_entry/__manifest__.py @@ -6,12 +6,22 @@ 'author': 'Hibou Corp. ', 'license': 'AGPL-3', 'category': 'Human Resources', - 'data': [ - 'data/hr_attendance_work_entry_data.xml', - ], 'depends': [ 'hr_attendance', 'hr_work_entry', ], + 'data': [ + 'data/hr_attendance_work_entry_data.xml', + 'views/attendance_views.xml', + 'views/employee_views.xml', + 'views/web_assets.xml', + 'views/work_entry_views.xml', + ], + 'demo': [ + 'data/hr_attendance_work_entry_demo.xml', + ], + 'qweb': [ + 'static/src/xml/hr_attendance.xml', + ], 'pre_init_hook': 'attn_payroll_pre_init_hook', } 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/models/attendance.py b/hr_attendance_work_entry/models/attendance.py index 273b1367..cd0d35b5 100644 --- a/hr_attendance_work_entry/models/attendance.py +++ b/hr_attendance_work_entry/models/attendance.py @@ -1,4 +1,4 @@ -from odoo import fields, models +from odoo import api, fields, models class HrAttendance(models.Model): @@ -7,3 +7,8 @@ class HrAttendance(models.Model): 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 index d9caffc4..49edc1d6 100644 --- a/hr_attendance_work_entry/models/employee.py +++ b/hr_attendance_work_entry/models/employee.py @@ -5,7 +5,7 @@ from odoo.exceptions import UserError class HrEmployee(models.Model): _inherit = 'hr.employee' - attendance_state = fields.Selection(selection_add=[('break', 'Break')]) + attendance_state = fields.Selection(selection_add=[('break', 'Break'), ('lunch', 'Lunch')]) @api.depends('last_attendance_id.work_type_id') def _compute_attendance_state(self): @@ -20,6 +20,9 @@ class HrEmployee(models.Model): 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): diff --git a/hr_attendance_work_entry/models/work_entry.py b/hr_attendance_work_entry/models/work_entry.py index b4088b0e..73618223 100644 --- a/hr_attendance_work_entry/models/work_entry.py +++ b/hr_attendance_work_entry/models/work_entry.py @@ -3,6 +3,7 @@ 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') @@ -10,4 +11,5 @@ class HrWorkEntryType(models.Model): # ('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..8919f0e3 --- /dev/null +++ b/hr_attendance_work_entry/static/src/js/kiosk_confirm.js @@ -0,0 +1,71 @@ +odoo.define('hr_attendance_work_entry.kiosk_confirm', function (require) { +"use strict"; + +var core = require('web.core'); +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); + } + }); + }, +}); + +core.action_registry.add('hr_attendance_kiosk_confirm', KioskConfirmTyped); +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..d9dcf7ac --- /dev/null +++ b/hr_attendance_work_entry/static/src/js/my_attendances.js @@ -0,0 +1,50 @@ +odoo.define('hr_attendance_work_entry.my_attendances', function (require) { +"use strict"; + +var core = require('web.core'); +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); + } + }); + }, +}); + +core.action_registry.add('hr_attendance_my_attendances', MyTypedAttendances); + +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. + +
+ +
+
+ + + +