diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_attendance/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_attendance/static')
| -rw-r--r-- | addons/hr_attendance/static/description/icon.png | bin | 0 -> 8577 bytes | |||
| -rw-r--r-- | addons/hr_attendance/static/description/icon.svg | 1 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/employee_kanban_view_handler.js | 35 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/greeting_message.js | 182 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/kiosk_confirm.js | 94 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/kiosk_mode.js | 85 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/my_attendances.js | 56 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/js/time_widget.js | 19 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/scss/hr_attendance.scss | 228 | ||||
| -rw-r--r-- | addons/hr_attendance/static/src/xml/attendance.xml | 145 | ||||
| -rw-r--r-- | addons/hr_attendance/static/tests/hr_attendance_tests.js | 189 |
11 files changed, 1034 insertions, 0 deletions
diff --git a/addons/hr_attendance/static/description/icon.png b/addons/hr_attendance/static/description/icon.png Binary files differnew file mode 100644 index 00000000..449fe487 --- /dev/null +++ b/addons/hr_attendance/static/description/icon.png diff --git a/addons/hr_attendance/static/description/icon.svg b/addons/hr_attendance/static/description/icon.svg new file mode 100644 index 00000000..bef191e7 --- /dev/null +++ b/addons/hr_attendance/static/description/icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#94B6C8"/><stop offset="100%" stop-color="#6A9EBA"/></linearGradient><path id="d" d="M28.278 17.235c5.193 0 9.404 4.271 9.404 9.54 0 5.27-4.21 9.541-9.404 9.541s-9.404-4.271-9.404-9.54c0-5.27 4.21-9.541 9.404-9.541zm6.018 19.492c0-.413-9.178 1.847-12.6-.65l-4.19 1.063c-2.512.638-4.275 2.927-4.275 5.554v2.209c0 1.58 1.264 2.862 2.822 2.862h15.466c-1.852-6.34 2.777-10.625 2.777-11.038zM45.5 33.794c-6.466 0-11.706 5.24-11.706 11.706 0 6.466 5.24 11.706 11.706 11.706 6.466 0 11.706-5.24 11.706-11.706 0-6.466-5.24-11.706-11.706-11.706zm2.695 16.525l-4.163-3.025a.57.57 0 0 1-.231-.458v-7.944c0-.312.255-.566.566-.566h2.266c.311 0 .566.254.566.566v6.5l2.997 2.18a.566.566 0 0 1 .123.793l-1.33 1.831a.57.57 0 0 1-.794.123z"/><path id="e" d="M28.278 15.235c5.193 0 9.404 4.271 9.404 9.54 0 5.27-4.21 9.541-9.404 9.541s-9.404-4.271-9.404-9.54c0-5.27 4.21-9.541 9.404-9.541zm6.018 19.492c0-.413-9.178 1.847-12.6-.65l-4.19 1.063c-2.512.638-4.275 2.927-4.275 5.554v2.209c0 1.58 1.264 2.862 2.822 2.862h15.466c-1.852-6.34 2.777-10.625 2.777-11.038zM45.5 31.794c-6.466 0-11.706 5.24-11.706 11.706 0 6.466 5.24 11.706 11.706 11.706 6.466 0 11.706-5.24 11.706-11.706 0-6.466-5.24-11.706-11.706-11.706zm2.695 16.525l-4.163-3.025a.57.57 0 0 1-.231-.458v-7.944c0-.312.255-.566.566-.566h2.266c.311 0 .566.254.566.566v6.5l2.997 2.18a.566.566 0 0 1 .123.793l-1.33 1.831a.57.57 0 0 1-.794.123z"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M42.488 69H4c-2 0-4-1-4-4V41.348l20.938-22.35 14.703 11.46-4.201 5.183-1.252 7.42 6.406-7.111L46 33l9.958 15.844L42.488 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>
\ No newline at end of file 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 00000000..02801f79 --- /dev/null +++ b/addons/hr_attendance/static/src/js/employee_kanban_view_handler.js @@ -0,0 +1,35 @@ + +odoo.define('hr_attendance.employee_kanban_view_handler', function(require) { +"use strict"; + +var KanbanRecord = require('web.KanbanRecord'); + +KanbanRecord.include({ + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _openRecord: function () { + if (this.modelName === 'hr.employee.public' && 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_kiosk_confirm', + employee_id: this.record.id.raw_value, + employee_name: this.record.name.raw_value, + employee_state: this.record.attendance_state.raw_value, + employee_hours_today: this.record.hours_today.raw_value, + }; + this.do_action(action); + } else { + this._super.apply(this, arguments); + } + } +}); + +}); diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js new file mode 100644 index 00000000..101f6dae --- /dev/null +++ b/addons/hr_attendance/static/src/js/greeting_message.js @@ -0,0 +1,182 @@ +odoo.define('hr_attendance.greeting_message', function (require) { +"use strict"; + +var AbstractAction = require('web.AbstractAction'); +var core = require('web.core'); +var time = require('web.time'); + +var _t = core._t; + + +var GreetingMessage = AbstractAction.extend({ + contentTemplate: 'HrAttendanceGreetingMessage', + + events: { + "click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); }, + }, + + init: function(parent, action) { + var self = this; + this._super.apply(this, arguments); + this.activeBarcode = true; + + // if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return + // to the (likely) appropriate menu, according to the user access rights + if(!action.attendance) { + this.activeBarcode = false; + this.getSession().user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) { + if(has_group) { + self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode'; + } else { + self.next_action = 'hr_attendance.hr_attendance_action_my_attendances'; + } + }); + return; + } + + this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances'; + // no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action) + if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') { + this.activeBarcode = false; + } + + this.attendance = action.attendance; + // We receive the check in/out times in UTC + // This widget only deals with display, which should be in browser's TimeZone + this.attendance.check_in = this.attendance.check_in && moment.utc(this.attendance.check_in).local(); + this.attendance.check_out = this.attendance.check_out && moment.utc(this.attendance.check_out).local(); + this.previous_attendance_change_date = action.previous_attendance_change_date && moment.utc(action.previous_attendance_change_date).local(); + + // check in/out times displayed in the greeting message template. + this.format_time = time.getLangTimeFormat(); + this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time); + this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time); + + if (action.hours_today) { + var duration = moment.duration(action.hours_today, "hours"); + this.hours_today = duration.hours() + ' hours, ' + duration.minutes() + ' minutes'; + } + + this.employee_name = action.employee_name; + this.attendanceBarcode = action.barcode; + }, + + start: function() { + if (this.attendance) { + this.attendance.check_out ? this.farewell_message() : this.welcome_message(); + } + if (this.activeBarcode) { + core.bus.on('barcode_scanned', this, this._onBarcodeScanned); + } + return this._super.apply(this, arguments); + }, + + welcome_message: function() { + var self = this; + var now = this.attendance.check_in.clone(); + this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + + if (now.hours() < 5) { + this.$('.o_hr_attendance_message_message').append(_t("Good night")); + } else if (now.hours() < 12) { + if (now.hours() < 8 && Math.random() < 0.3) { + if (Math.random() < 0.75) { + this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm")); + } else { + this.$('.o_hr_attendance_message_message').append(_t("First come, first served")); + } + } else { + this.$('.o_hr_attendance_message_message').append(_t("Good morning")); + } + } else if (now.hours() < 17){ + this.$('.o_hr_attendance_message_message').append(_t("Good afternoon")); + } else if (now.hours() < 23){ + this.$('.o_hr_attendance_message_message').append(_t("Good evening")); + } else { + this.$('.o_hr_attendance_message_message').append(_t("Good night")); + } + if(this.previous_attendance_change_date){ + var last_check_out_date = this.previous_attendance_change_date.clone(); + if(now - last_check_out_date > 24*7*60*60*1000){ + this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!")); + } else { + if(Math.random() < 0.02){ + this.$('.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 = this.attendance.check_out.clone(); + this.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 = this.previous_attendance_change_date.clone(); + if(now - last_check_in_date > 1000*60*60*12){ + this.$('.o_hr_attendance_warning_message').show().append(_t("<b>Warning! Last check in was over 12 hours ago.</b><br/>If this isn't right, please contact Human Resource staff")); + clearTimeout(this.return_to_main_menu); + this.activeBarcode = false; + } else if(now - last_check_in_date > 1000*60*60*8){ + this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!")); + } + } + + if (now.hours() < 12) { + this.$('.o_hr_attendance_message_message').append(_t("Have a good day!")); + } else if (now.hours() < 14) { + this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!")); + if (Math.random() < 0.05) { + this.$('.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) { + this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away")); + } + } else if (now.hours() < 17) { + this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon")); + } else { + if (now.hours() < 18 && Math.random() < 0.2) { + this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise")); + } else { + this.$('.o_hr_attendance_message_message').append(_t("Have a good evening")); + } + } + }, + + _onBarcodeScanned: function(barcode) { + var self = this; + if (this.attendanceBarcode !== barcode){ + if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created. + clearTimeout(this.return_to_main_menu); + } + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); + this._rpc({ + model: 'hr.employee', + method: 'attendance_scan', + args: [barcode, ], + }) + .then(function (result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + } + }, function () { + setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000); + }); + } + }, + + destroy: function () { + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); + clearTimeout(this.return_to_main_menu); + this._super.apply(this, arguments); + }, +}); + +core.action_registry.add('hr_attendance_greeting_message', GreetingMessage); + +return GreetingMessage; + +}); diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js new file mode 100644 index 00000000..8262b24c --- /dev/null +++ b/addons/hr_attendance/static/src/js/kiosk_confirm.js @@ -0,0 +1,94 @@ +odoo.define('hr_attendance.kiosk_confirm', function (require) { +"use strict"; + +var AbstractAction = require('web.AbstractAction'); +var core = require('web.core'); +var field_utils = require('web.field_utils'); +var QWeb = core.qweb; + + +var KioskConfirm = AbstractAction.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": _.debounce(function () { + var self = this; + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[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); + } + }); + }, 200, true), + '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': _.debounce(function() { + var self = this; + this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled"); + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[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); + self.$('.o_hr_attendance_PINbox').val(''); + setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500); + } + }); + }, 200, true), + }, + + init: function (parent, action) { + this._super.apply(this, arguments); + this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode'; + this.employee_id = action.employee_id; + this.employee_name = action.employee_name; + this.employee_state = action.employee_state; + this.employee_hours_today = field_utils.format.float_time(action.employee_hours_today); + }, + + start: function () { + var self = this; + this.getSession().user_has_group('hr_attendance.group_hr_attendance_use_pin').then(function(has_group){ + self.use_pin = has_group; + self.$el.html(QWeb.render("HrAttendanceKioskConfirm", {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', second:'2-digit'}));}, 500); + // First clock refresh before interval to avoid delay + this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'})); + }, + + destroy: function () { + clearInterval(this.clock_start); + this._super.apply(this, arguments); + }, +}); + +core.action_registry.add('hr_attendance_kiosk_confirm', KioskConfirm); + +return KioskConfirm; + +}); diff --git a/addons/hr_attendance/static/src/js/kiosk_mode.js b/addons/hr_attendance/static/src/js/kiosk_mode.js new file mode 100644 index 00000000..6ce0c026 --- /dev/null +++ b/addons/hr_attendance/static/src/js/kiosk_mode.js @@ -0,0 +1,85 @@ +odoo.define('hr_attendance.kiosk_mode', function (require) { +"use strict"; + +var AbstractAction = require('web.AbstractAction'); +var ajax = require('web.ajax'); +var core = require('web.core'); +var Session = require('web.session'); + +var QWeb = core.qweb; + + +var KioskMode = AbstractAction.extend({ + events: { + "click .o_hr_attendance_button_employees": function() { + this.do_action('hr_attendance.hr_employee_attendance_action_kanban', { + additional_context: {'no_group_by': true}, + }); + }, + }, + + start: function () { + var self = this; + core.bus.on('barcode_scanned', this, this._onBarcodeScanned); + self.session = Session; + var def = this._rpc({ + model: 'res.company', + method: 'search_read', + args: [[['id', '=', this.session.company_id]], ['name']], + }) + .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("HrAttendanceKioskMode", {widget: self})); + self.start_clock(); + }); + // Make a RPC call every day to keep the session alive + self._interval = window.setInterval(this._callServer.bind(this), (60*60*1000*24)); + return Promise.all([def, this._super.apply(this, arguments)]); + }, + + _onBarcodeScanned: function(barcode) { + var self = this; + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); + this._rpc({ + model: 'hr.employee', + method: 'attendance_scan', + args: [barcode, ], + }) + .then(function (result) { + if (result.action) { + self.do_action(result.action); + } else if (result.warning) { + self.do_warn(result.warning); + core.bus.on('barcode_scanned', self, self._onBarcodeScanned); + } + }, function () { + core.bus.on('barcode_scanned', self, self._onBarcodeScanned); + }); + }, + + 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', second:'2-digit'}));}, 500); + // First clock refresh before interval to avoid delay + this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'})); + }, + + destroy: function () { + core.bus.off('barcode_scanned', this, this._onBarcodeScanned); + clearInterval(this.clock_start); + clearInterval(this._interval); + this._super.apply(this, arguments); + }, + + _callServer: function () { + // Make a call to the database to avoid the auto close of the session + return ajax.rpc("/hr_attendance/kiosk_keepalive", {}); + }, + +}); + +core.action_registry.add('hr_attendance_kiosk_mode', KioskMode); + +return KioskMode; + +}); diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js new file mode 100644 index 00000000..91acb7f1 --- /dev/null +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -0,0 +1,56 @@ +odoo.define('hr_attendance.my_attendances', function (require) { +"use strict"; + +var AbstractAction = require('web.AbstractAction'); +var core = require('web.core'); +var field_utils = require('web.field_utils'); + + +var MyAttendances = AbstractAction.extend({ + contentTemplate: 'HrAttendanceMyMainMenu', + events: { + "click .o_hr_attendance_sign_in_out_icon": _.debounce(function() { + this.update_attendance(); + }, 200, true), + }, + + willStart: function () { + var self = this; + + var def = this._rpc({ + model: 'hr.employee', + method: 'search_read', + args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name', 'hours_today']], + }) + .then(function (res) { + self.employee = res.length && res[0]; + if (res.length) { + self.hours_today = field_utils.format.float_time(self.employee.hours_today); + } + }); + + return Promise.all([def, this._super.apply(this, arguments)]); + }, + + update_attendance: function () { + var self = this; + this._rpc({ + model: 'hr.employee', + method: 'attendance_manual', + args: [[self.employee.id], 'hr_attendance.hr_attendance_action_my_attendances'], + }) + .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', MyAttendances); + +return MyAttendances; + +}); diff --git a/addons/hr_attendance/static/src/js/time_widget.js b/addons/hr_attendance/static/src/js/time_widget.js new file mode 100644 index 00000000..eabfcfbb --- /dev/null +++ b/addons/hr_attendance/static/src/js/time_widget.js @@ -0,0 +1,19 @@ +odoo.define('hr_attendance.widget', function (require) { + "use strict"; + + var basic_fields = require('web.basic_fields'); + var field_registry = require('web.field_registry'); + + var RelativeTime = basic_fields.FieldDateTime.extend({ + _formatValue: function (val) { + if (!(val && val._isAMomentObject)) { + return; + } + return val.fromNow(true); + }, + }); + + field_registry.add('relative_time', RelativeTime); + + return RelativeTime; +});
\ No newline at end of file diff --git a/addons/hr_attendance/static/src/scss/hr_attendance.scss b/addons/hr_attendance/static/src/scss/hr_attendance.scss new file mode 100644 index 00000000..4bd2b65d --- /dev/null +++ b/addons/hr_attendance/static/src/scss/hr_attendance.scss @@ -0,0 +1,228 @@ + +.o_kanban_view #oe_hr_attendance_status { + margin-right: .3em; +} + +#oe_hr_attendance_status { + color: $o-brand-secondary; + &.oe_hr_attendance_status_green { + color: theme-color('success'); + } + &.oe_hr_attendance_status_orange { + color: theme-color('warning'); + } +} + +.o_hr_attendance_kiosk_backdrop { + @include o-position-absolute(0,0,0,0); + background-color: fade-out(black, 0.7); +} + +.o_hr_attendance_clock { + display: none; + position: relative; + width: 100%; + padding: 0.5em; + background-color: fade-out(black, 0.7); + font: normal 1.2em $font-family-monospace; + color: white; + + @include media-breakpoint-up(md) { + @include o-position-absolute(20px, 20px); + width: auto; + padding: 3px 10px 3px 10px; + border-radius: 3px; + font-size: 1.5em; + } +} + +.o_hr_attendance_kiosk_mode_container { + display: flex; + flex-flow: column nowrap; + justify-content: flex-start; + align-items: center; + @include o-position-absolute(0, 0, 0, 0); + + @include media-breakpoint-up(md) { + justify-content: center; + } +} + +.o_hr_attendance_kiosk_mode { + width: 100%; + text-align: center; + position: relative; + background-color: fade-out(white, 0.1); + &.o_barcode_main { + font-family: 'Lato', sans-serif; + width: 100%; + text-align: center; + background-color: #fff; + padding: 3em; + img { + width: 115px; + height: 60px; + } + .o_hr_attendance_kiosk_welcome_row img { + max-width: inherit; + } + @include media-breakpoint-down(sm) { + h2 { + margin-left: -10px; + margin-right: -10px; + } + } + } + + @include media-breakpoint-down(sm) { + overflow: hidden; + padding: 0 2em 2em; + flex: 1 0 auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + @include media-breakpoint-up(md) { + padding: 2em 4em; + width: auto; + max-width: 550px; + width: 100%; + border-radius: 0.2em; + font-size: 1.2em; + animation: fadeInDownSmall .3s; + } + + .o_hr_attendance_kiosk_welcome_row { + @include media-breakpoint-up(md) { + display: flex; + align-items: center; + } + + img { + max-width: 80px; + } + } + + .o_hr_attendance_sign_in_out_icon { + cursor: pointer; + margin: 0.1em 0 0.1em; + padding: 0.15em 0.3em; + border-radius: .1em; + box-shadow: inset 0 -3px 0 fade-out(black, 0.7); + + &.btn-secondary:hover { + color: $o-brand-primary; + } + } + + .o_hr_attendance_back_button { + .visible-xs{ + background: gray('200'); + margin: 0 -2em; + .fa { + @include o-position-absolute(0.75em); + margin-left: -1.5em + } + } + + .d-none.d-md-inline-block.btn-secondary { + transform: translate(-50%, -50%); + @include o-position-absolute(0, $left:0); + width: 2em; + height: 2em; + border-radius: 50%; + padding-left: 14px; + line-height: 1.5em; + text-align: left; + color: $body-color; + font-size: 1.6em; + z-index: 1; + box-shadow: inset 0 0 0 1px fade-out(black, 0.8); + } + } + + .btn-secondary{ + box-shadow: inset 0 0 0 1px fade-out(black, 0.9); + color: $headings-color; + } + + .o_hr_attendance_user_badge { + background: linear-gradient(to right bottom, #77717e, #c9a8a9); + + img { + width: 50px; + height: 50px; + background: white; + border: 1px solid #d7d7d7; + } + + @include media-breakpoint-down(sm) { + margin: 1em -2em 0; + background: transparent; + } + + @include media-breakpoint-up(md) { + margin: 0; + height: 90px; + border-radius: .2em .2em 0 0; + border-top: 1px solid fade-out(white, 0.8); + @include o-position-absolute(auto, 0, 100%, 0); + transform: translateY(3px); + + img { + width: 80px; + height: 80px; + transform: translateX(-50%)translateY(35%); + @include o-position-absolute($bottom: 0); + } + } + + + h1 { + margin-top: .4em; + } + } + + .o_hr_attendance_pin_pad [class*="col-"] { + padding: 4px; + + .o_hr_attendance_PINbox { + font-size: 2em; + border: none; + padding: 0 $input-btn-padding-x-lg; + background: fade-out(white, 0.6); + } + } + + .o_hr_attendance_random_message { + font-style: italic; + } + + .message_demo_barcodes { + font-size: 0.9em; + margin: 0; + } + + img.o_hr_attendance_kiosk_company_image { + overflow:hidden; // prevent margins colapsing with h1 + margin: 1rem 0 2rem; + width: 200px; + } + + p { + text-align: left; + margin: 3rem 0; + } + + > button { + font-size: 1.2em; + margin-bottom: 2rem; + width: 100%; + font-weight: 400; + + &:last-child { + margin-bottom: 0; + } + } +} diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml new file mode 100644 index 00000000..04800ed8 --- /dev/null +++ b/addons/hr_attendance/static/src/xml/attendance.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="utf-8"?> +<template xml:space="preserve"> + <t t-name="PresenceIndicator"> + <div id="oe_hr_attendance_status" class="fa fa-circle" role="img" aria-label="Available" title="Available"> + </div> + </t> + + <t t-name="HrAttendanceKioskMode"> + <div class="o_hr_attendance_kiosk_mode_container o_home_menu_background"> + <span class="o_hr_attendance_kiosk_backdrop"/> + <div class="o_hr_attendance_clock text-center"/> + <div class="o_hr_attendance_kiosk_mode o_barcode_main"> + <h2><small>Welcome to</small> <t t-esc="widget.company_name"/></h2> + <img t-attf-src="{{widget.company_image_url}}" alt="Company Logo" class="o_hr_attendance_kiosk_company_image"/> + <div class="row o_hr_attendance_kiosk_welcome_row"> + <div class="col-sm-5 mt16"> + <img class="img img-fluid d-block mx-auto" src="/barcodes/static/img/barcode.png"/> + <h5 class="mt8 mb0 text-muted">Scan your badge</h5> + </div> + <div class="col-sm-2 mt32"> + <h4 class="mt0 mb8"><i>or</i></h4> + </div> + <div class="col-sm-5 mt16"> + <button class="o_hr_attendance_button_employees btn btn-primary mb16"> + <div class="mb16 mt16">Identify Manually</div> + </button> + </div> + </div> + </div> + </div> + </t> + + <t t-name="HrAttendanceMyMainMenu"> + <div class="o_hr_attendance_kiosk_mode_container o_home_menu_background"> + <span class="o_hr_attendance_kiosk_backdrop"/> + <div class="o_hr_attendance_clock text-center"/> + <div class="o_hr_attendance_kiosk_mode"> + <t t-set="checked_in" t-value="widget.employee.attendance_state=='checked_in'"/> + <t t-if="widget.employee"> + <div class="o_hr_attendance_user_badge o_home_menu_background"> + <img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.employee.id}" t-att-title="widget.employee.name" t-att-alt="widget.employee.name"/> + </div> + <h1 class="mb8"><t t-esc="widget.employee.name"/></h1> + <h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3> + <h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.hours_today"/></h4> + <a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-out btn-warning" t-if="checked_in" aria-label="Sign out" title="Sign out"/> + <a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-in btn-secondary" t-if="!checked_in" aria-label="Sign in" title="Sign in"/> + <h3 class="mt0 mb0 text-muted">Click to <b t-if="checked_in">check out</b><b t-if="!checked_in">check in</b></h3> + </t> + <t t-else=""> + Warning : Your user should be linked to an employee to use attendance. Please contact your administrator. + </t> + </div> + </div> + </t> + + <t t-name="HrAttendanceKioskConfirm"> + <div class="o_hr_attendance_kiosk_mode_container o_home_menu_background"> + <span class="o_hr_attendance_kiosk_backdrop"/> + <div class="o_hr_attendance_clock text-center"/> + <div class="o_hr_attendance_kiosk_mode"> + <t t-set="checked_in" t-value="widget.employee_state=='checked_in'"/> + <div class="o_hr_attendance_back_button"> + <span class="btn btn-secondary btn-lg d-block d-md-none"><i class="fa fa-chevron-left mr8"/> Go back</span> + <span class="btn btn-secondary d-none d-md-inline-block"><i class="fa fa-chevron-left" role="img" aria-label="Go back" title="Go back"/></span> + </div> + <t t-if="widget.employee_id"> + <div class="o_hr_attendance_user_badge o_home_menu_background"> + <img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.employee_id}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/> + </div> + <h1 class="mb8"><t t-esc="widget.employee_name"/></h1> + <h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3> + <h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.employee_hours_today"/></h4> + <t t-if="!widget.use_pin"> + <a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-out btn-warning" t-if="checked_in" aria-label="Sign out" title="Sign out"/> + <a class="fa fa-7x o_hr_attendance_sign_in_out_icon fa-sign-in btn-secondary" t-if="!checked_in" aria-label="Sign in" title="Sign in"/> + <h3 class="mt0 mb0 text-muted">Click to <b t-if="checked_in">check out</b><b t-else="">check in</b></h3> + </t> + <t t-else=""> + <h3 class="mt0 mb0 text-muted">Please enter your PIN to <b t-if="checked_in">check out</b><b t-else="">check in</b></h3> + <div class="row"> + <div class="col-md-8 offset-md-2 o_hr_attendance_pin_pad"> + <div class="row" > + <div class="col-12 mb8 mt8"><input class="o_hr_attendance_PINbox text-center" type="password" disabled="true"/></div> + </div> + <div class="row"> + <t t-foreach="['1', '2', '3', '4', '5', '6', '7', '8', '9', ['C', 'btn-warning'], '0', ['ok', 'btn-primary']]" t-as="btn_name"> + <div class="col-4 mb4"> + <a t-attf-class="btn {{btn_name[1]? btn_name[1] : 'btn-secondary'}} btn-block btn-lg {{ 'o_hr_attendance_pin_pad_button_' + btn_name[0] }}"><t t-esc="btn_name[0]"/></a> + </div> + </t> + </div> + </div> + </div> + </t> + </t> + <div t-else="" class="alert alert-danger" role="alert"> + <b>Error: could not find corresponding employee.</b><br/>Please return to the main menu. + </div> + <a role="button" class="oe_attendance_sign_in_out" aria-label="Sign out" title="Sign out"/> + </div> + </div> + </t> + + <t t-name="HrAttendanceGreetingMessage"> + <div class="o_hr_attendance_kiosk_mode_container o_home_menu_background"> + <span class="o_hr_attendance_kiosk_backdrop"/> + <div class="o_hr_attendance_clock text-center"/> + <div class="o_hr_attendance_kiosk_mode"> + <t t-if="widget.attendance"> + <div class="o_hr_attendance_user_badge o_home_menu_background"> + <img class="img rounded-circle" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{widget.attendance.employee_id[0]}" t-att-title="widget.employee_name" t-att-alt="widget.employee_name"/> + </div> + <t t-if="widget.attendance.check_out"> + <h1 class="mb0">Goodbye <t t-esc="widget.employee_name"/>!</h1> + <h2 class="o_hr_attendance_message_message mt4 mb24"/> + <div class="alert alert-info h2 mt0" role="status"> + Checked out at <b><t t-esc="widget.attendance.check_out_time"/></b> + <br/><b><t t-esc="widget.hours_today"/></b> + </div> + <h3 class="o_hr_attendance_random_message mb24"/> + <div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/> + </t> + <t t-else=""> + <h1 class="mb0">Welcome <t t-esc="widget.employee_name"/>!</h1> + <h2 class="o_hr_attendance_message_message mt4 mb24"/> + <div class="alert alert-info h2 mt0" role="status"> + Checked in at <b><t t-esc="widget.attendance.check_in_time"/></b> + </div> + <h3 class="o_hr_attendance_random_message mb24"/> + <div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/> + </t> + <button class="o_hr_attendance_button_dismiss btn btn-primary btn-lg"> + <span class="text-capitalize" t-if="widget.attendance.check_out">Goodbye</span> + <span class="text-capitalize" t-else="">OK</span> + </button> + </t> + <t t-else=""> + <div class="alert alert-warning" role="alert">Invalid request, please return to the main menu.</div> + <button class="o_hr_attendance_button_dismiss btn btn-secondary btn-lg">Go back</button> + </t> + </div> + </div> + </t> +</template> diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js new file mode 100644 index 00000000..72e3ae93 --- /dev/null +++ b/addons/hr_attendance/static/tests/hr_attendance_tests.js @@ -0,0 +1,189 @@ +odoo.define('hr_attendance.tests', function (require) { +"use strict"; + +var testUtils = require('web.test_utils'); +var core = require('web.core'); + +var MyAttendances = require('hr_attendance.my_attendances'); +var KioskMode = require('hr_attendance.kiosk_mode'); +var GreetingMessage = require('hr_attendance.greeting_message'); + + +QUnit.module('HR Attendance', { + beforeEach: function () { + this.data = { + 'hr.employee': { + fields: { + name: {string: 'Name', type: 'char'}, + attendance_state: { + string: 'State', + type: 'selection', + selection: [['checked_in', "In"], ['checked_out', "Out"]], + default: 1, + }, + user_id: {string: 'user ID', type: 'integer'}, + barcode: {string:'barcode', type: 'integer'}, + hours_today: {string:'Hours today', type: 'float'}, + }, + records: [{ + id: 1, + name: "Employee A", + attendance_state: 'checked_out', + user_id: 1, + barcode: 1, + }, + { + id: 2, + name: "Employee B", + attendance_state: 'checked_out', + user_id: 2, + barcode: 2, + }], + }, + 'res.company': { + fields: { + name: {string: 'Name', type: 'char'}, + }, + records: [{ + id: 1, + name: "Company A", + }], + }, + }; + }, +}, function () { + QUnit.module('My attendances (client action)'); + + QUnit.test('simple rendering', async function (assert) { + assert.expect(1); + + var $target = $('#qunit-fixture'); + var clientAction = new MyAttendances(null, {}); + await testUtils.mock.addMockEnvironment(clientAction, { + data: this.data, + session: { + uid: 1, + }, + }); + await clientAction.appendTo($target); + + assert.strictEqual(clientAction.$('.o_hr_attendance_kiosk_mode h1').text(), 'Employee A', + "should have rendered the client action without crashing"); + + clientAction.destroy(); + }); + + QUnit.test('Attendance Kiosk Mode Test', async function (assert) { + assert.expect(2); + + var $target = $('#qunit-fixture'); + var self = this; + var rpcCount = 0; + var clientAction = new KioskMode(null, {}); + await testUtils.mock.addMockEnvironment(clientAction, { + data: this.data, + session: { + uid: 1, + company_id: 1, + }, + mockRPC: function(route, args) { + if (args.method === 'attendance_scan' && args.model === 'hr.employee') { + + rpcCount++; + return Promise.resolve(self.data['hr.employee'].records[0]); + } + return this._super(route, args); + }, + }); + await clientAction.appendTo($target); + core.bus.trigger('barcode_scanned', 1); + core.bus.trigger('barcode_scanned', 1); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + core.bus.trigger('barcode_scanned', 2); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + clientAction.destroy(); + }); + + QUnit.test('Attendance Greeting Message Test', async function (assert) { + assert.expect(10); + + var $target = $('#qunit-fixture'); + var self = this; + var rpcCount = 0; + + var clientActions = []; + async function createGreetingMessage (target, barcode){ + var action = { + attendance: { + check_in: "2018-09-20 13:41:13", + employee_id: [barcode], + }, + next_action: "hr_attendance.hr_attendance_action_kiosk_mode", + barcode: barcode, + }; + var clientAction = new GreetingMessage(null, action); + await testUtils.mock.addMockEnvironment(clientAction, { + data: self.data, + session: { + uid: 1, + company_id: 1, + }, + mockRPC: function(route, args) { + if (args.method === 'attendance_scan' && args.model === 'hr.employee') { + rpcCount++; + action.attendance.employee_id = [args.args[0], 'Employee']; + /* + if rpc have been made, a new instance is created to simulate the same behaviour + as functional flow. + */ + createGreetingMessage (target, args.args[0]); + return Promise.resolve({action: action}); + } + return this._super(route, args); + }, + }); + await clientAction.appendTo(target); + + clientActions.push(clientAction); + } + + // init - mock coming from kiosk + await createGreetingMessage ($target, 1); + await testUtils.nextMicrotaskTick(); + assert.strictEqual(clientActions.length, 1, 'Number of clientAction must = 1.'); + + core.bus.trigger('barcode_scanned', 1); + /* + As action is given when instantiate GreetingMessage, we simulate that we come from the KioskMode + So rescanning the same barcode won't lead to another RPC. + */ + assert.strictEqual(clientActions.length, 1, 'Number of clientActions must = 1.'); + assert.strictEqual(rpcCount, 0, 'RPC call should not have been done.'); + + core.bus.trigger('barcode_scanned', 2); + await testUtils.nextTick(); + assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + core.bus.trigger('barcode_scanned', 2); + await testUtils.nextMicrotaskTick(); + assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.'); + assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.'); + + core.bus.trigger('barcode_scanned', 1); + await testUtils.nextTick(); + assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); + core.bus.trigger('barcode_scanned', 1); + await testUtils.nextMicrotaskTick(); + assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.'); + assert.strictEqual(rpcCount, 2, 'RPC call should have been done only twice.'); + + _.each(clientActions.reverse(), function(clientAction) { + clientAction.destroy(); + }); + }); + +}); + +}); |
