diff --git a/addons/pos_hr/__manifest__.py b/addons/pos_hr/__manifest__.py
index c92bbe1a071..6ad2705c0d1 100644
--- a/addons/pos_hr/__manifest__.py
+++ b/addons/pos_hr/__manifest__.py
@@ -21,5 +21,10 @@ The actual till still requires one user but an unlimited number of employees can
],
'installable': True,
'auto_install': True,
- 'qweb': ['static/src/xml/pos.xml'],
+ 'qweb': [
+ 'static/src/xml/HeaderLockButton.xml',
+ 'static/src/xml/Chrome.xml',
+ 'static/src/xml/CashierName.xml',
+ 'static/src/xml/LoginScreen.xml',
+ ],
}
diff --git a/addons/pos_hr/static/src/css/pos.css b/addons/pos_hr/static/src/css/pos.css
index e50be169538..84b66fe9554 100644
--- a/addons/pos_hr/static/src/css/pos.css
+++ b/addons/pos_hr/static/src/css/pos.css
@@ -79,3 +79,14 @@
color: #555555;
border-radius: 5px;
}
+
+.pos .pos-rightheader .header-button.lock-button {
+ font-size: 20px;
+ color: green;
+ transition: all 200ms ease-in-out;
+ width: 18px;
+}
+
+.pos .pos-rightheader .header-button.lock-button:hover {
+ color: red;
+}
diff --git a/addons/pos_hr/static/src/js/CashierName.js b/addons/pos_hr/static/src/js/CashierName.js
new file mode 100644
index 00000000000..7628372032f
--- /dev/null
+++ b/addons/pos_hr/static/src/js/CashierName.js
@@ -0,0 +1,59 @@
+odoo.define('pos_hr.CashierName', function (require) {
+ 'use strict';
+
+ const CashierName = require('point_of_sale.CashierName');
+ const Registries = require('point_of_sale.Registries');
+ const useSelectEmployee = require('pos_hr.useSelectEmployee');
+ const { useBarcodeReader } = require('point_of_sale.custom_hooks');
+
+ const PosHrCashierName = (CashierName) =>
+ class extends CashierName {
+ constructor() {
+ super(...arguments);
+ const { selectEmployee, askPin } = useSelectEmployee();
+ this.askPin = askPin;
+ this.selectEmployee = selectEmployee;
+ useBarcodeReader({ cashier: this._onCashierScan });
+ }
+ mounted() {
+ this.env.pos.on('change:cashier', this.render, this);
+ }
+ willUnmount() {
+ this.env.pos.off('change:cashier', null, this);
+ }
+ async selectCashier() {
+ if (!this.env.pos.config.module_pos_hr) return;
+
+ const list = this.env.pos.employees
+ .filter((employee) => employee.id !== this.env.pos.get_cashier().id)
+ .map((employee) => {
+ return {
+ id: employee.id,
+ item: employee,
+ label: employee.name,
+ isSelected: false,
+ };
+ });
+
+ const employee = await this.selectEmployee(list);
+ if (employee) {
+ this.env.pos.set_cashier(employee);
+ }
+ }
+ async _onCashierScan(code) {
+ const employee = this.env.pos.employees.find(
+ (emp) => emp.barcode === Sha1.hash(code.code)
+ );
+
+ if (!employee || employee === this.env.pos.get_cashier()) return;
+
+ if (!employee.pin || (await this.askPin(employee))) {
+ this.env.pos.set_cashier(employee);
+ }
+ }
+ };
+
+ Registries.Component.extend(CashierName, PosHrCashierName);
+
+ return CashierName;
+});
diff --git a/addons/pos_hr/static/src/js/Chrome.js b/addons/pos_hr/static/src/js/Chrome.js
new file mode 100644
index 00000000000..f4d4ac1282b
--- /dev/null
+++ b/addons/pos_hr/static/src/js/Chrome.js
@@ -0,0 +1,22 @@
+odoo.define('pos_hr.chrome', function (require) {
+ 'use strict';
+
+ const Chrome = require('point_of_sale.Chrome');
+ const Registries = require('point_of_sale.Registries');
+
+ const PosHrChrome = (Chrome) =>
+ class extends Chrome {
+ async start() {
+ await super.start();
+ this.env.pos.on('change:cashier', this.render, this);
+ if (this.env.pos.config.module_pos_hr) this.showTempScreen('LoginScreen');
+ }
+ get headerButtonIsShown() {
+ return this.env.pos.get('cashier').role == 'manager';
+ }
+ };
+
+ Registries.Component.extend(Chrome, PosHrChrome);
+
+ return Chrome;
+});
diff --git a/addons/pos_hr/static/src/js/HeaderLockButton.js b/addons/pos_hr/static/src/js/HeaderLockButton.js
new file mode 100644
index 00000000000..3ad0ee9f02b
--- /dev/null
+++ b/addons/pos_hr/static/src/js/HeaderLockButton.js
@@ -0,0 +1,25 @@
+odoo.define('point_of_sale.HeaderLockButton', function(require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { useState } = owl;
+
+ class HeaderLockButton extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.state = useState({ isUnlockIcon: true, title: 'Unlocked' });
+ }
+ async showLoginScreen() {
+ await this.showTempScreen('LoginScreen');
+ }
+ onMouseOver(isMouseOver) {
+ this.state.isUnlockIcon = !isMouseOver;
+ this.state.title = isMouseOver ? 'Lock' : 'Unlocked';
+ }
+ }
+
+ Registries.Component.add(HeaderLockButton);
+
+ return HeaderLockButton;
+});
diff --git a/addons/pos_hr/static/src/js/LoginScreen.js b/addons/pos_hr/static/src/js/LoginScreen.js
new file mode 100644
index 00000000000..bf93abeb0af
--- /dev/null
+++ b/addons/pos_hr/static/src/js/LoginScreen.js
@@ -0,0 +1,74 @@
+odoo.define('pos_hr.LoginScreen', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const useSelectEmployee = require('pos_hr.useSelectEmployee');
+ const { useBarcodeReader } = require('point_of_sale.custom_hooks');
+
+ class LoginScreen extends PosComponent {
+ constructor() {
+ super(...arguments);
+ const { selectEmployee, askPin } = useSelectEmployee();
+ this.selectEmployee = selectEmployee;
+ this.askPin = askPin;
+ useBarcodeReader(
+ {
+ cashier: this._barcodeCashierAction,
+ },
+ true
+ );
+ }
+ back() {
+ this.props.resolve({ confirmed: false, payload: false });
+ this.trigger('close-temp-screen');
+ }
+ confirm() {
+ this.props.resolve({ confirmed: true, payload: true });
+ this.trigger('close-temp-screen');
+ }
+ get shopName() {
+ return this.env.pos.config.name;
+ }
+ closeSession() {
+ this.trigger('close-pos');
+ }
+ async selectCashier() {
+ const list = this.env.pos.employees.map((employee) => {
+ return {
+ id: employee.id,
+ item: employee,
+ label: employee.name,
+ isSelected: false,
+ };
+ });
+
+ const employee = await this.selectEmployee(list);
+ if (employee) {
+ this.env.pos.set_cashier(employee);
+ this.back();
+ }
+ }
+ async _barcodeCashierAction(code) {
+ let theEmployee;
+ for (let employee of this.env.pos.employees) {
+ if (employee.barcode === Sha1.hash(code.code)) {
+ theEmployee = employee;
+ break;
+ }
+ }
+
+ if (!theEmployee) return;
+
+ if (!theEmployee.pin || (await this.askPin(theEmployee))) {
+ this.env.pos.set_cashier(theEmployee);
+ this.back();
+ }
+ }
+ }
+ LoginScreen.template = 'LoginScreen';
+
+ Registries.Component.add(LoginScreen);
+
+ return LoginScreen;
+});
diff --git a/addons/pos_hr/static/src/js/chrome.js b/addons/pos_hr/static/src/js/chrome.js
deleted file mode 100644
index 576b9a1d037..00000000000
--- a/addons/pos_hr/static/src/js/chrome.js
+++ /dev/null
@@ -1,124 +0,0 @@
-odoo.define('pos_hr.chrome', function (require) {
- "use strict";
-
- var core = require('web.core');
- var chrome = require('point_of_sale.chrome');
- var _t = core._t;
- var _lt = core._lt;
- var QWeb = core.qweb;
-
-
- /* ------- The User Name Widget ------- */
-
- // Displays the current cashier's name and allows
- // to switch between cashiers.
-
- chrome.UsernameWidget.include({
- renderElement: function(){
- var self = this;
- this._super();
- this.$el.click(function(){
- self.click_username();
- });
- },
- click_username: function(){
- if(!this.pos.config.module_pos_hr) { return; }
- var self = this;
- this.gui.select_employee({
- 'security': true,
- 'current_employee': this.pos.get_cashier(),
- 'title': _t('Change Cashier'),
- }).then(function(employee){
- self.pos.set_cashier(employee);
- self.chrome.widget.username.renderElement();
- self.renderElement();
- });
- },
- });
-
- var HeaderCloseButtonWidget = chrome.HeaderButtonWidget.extend({
- start: function(){
- if (this.pos.config.module_pos_hr) {
- var self = this;
- this.pos.bind('change:cashier', this._show_hide_close_button , this);
- }
- return this._super();
- },
- _show_hide_close_button: function(){
- if (this.pos.get('cashier').role == 'manager') {
- this.show();
- } else {
- this.hide();
- }
- }
- });
-
- var HeaderLockButtonWidget = chrome.HeaderButtonWidget.extend({
- init: function(parent, options) {
- this._super(parent, options);
- this.icon = 'fa-unlock';
- this.icon_color = 'green';
- this.icon_mouseover = 'fa-lock';
- this.icon_color_mouseover = 'red';
- },
-
- start: function(){
- if (this.pos.config.module_pos_hr) {
- this.show();
- } else {
- this.hide();
- }
- return this._super();
- },
-
- renderElement: function() {
- var self = this;
- this._super();
- this.iconElement = this.$el.find("i");
- this.$el.css('font-size','20px');
- this.iconElement.addClass(this.icon);
- this.$el.css('color',this.icon_color);
- this.$el.mouseover(function(){
- self.iconElement.addClass(self.icon_mouseover).removeClass(self.icon);
- $(this).css('color', self.icon_color_mouseover);
- }).mouseleave(function(){
- self.iconElement.addClass(self.icon).removeClass(self.icon_mouseover);
- $(this).css('color', self.icon_color);
- });
- },
- });
- chrome.Chrome.include({
- lock_button_widget: {
- 'name': 'lock_button',
- 'widget': HeaderLockButtonWidget,
- 'append': '.pos-rightheader',
- 'args': {
- label: _lt('Lock'),
- action: function() {
- this.chrome.return_to_login_screen();
- }
- }
- },
- build_widgets: function() {
- var self = this;
- this.widgets.some(function(widget, index){
- if (widget.name === 'close_button'){
- widget.widget = HeaderCloseButtonWidget;
- self.widgets.splice(index, 0, self.lock_button_widget);
- return true;
- }
- return false;
- });
- this._super();
- },
- return_to_login_screen: function() {
- this.gui.show_screen('login');
- },
-
- });
-
- return {
- HeaderLockButtonWidget: HeaderLockButtonWidget,
- HeaderCloseButtonWidget: HeaderCloseButtonWidget,
- };
-});
diff --git a/addons/pos_hr/static/src/js/gui.js b/addons/pos_hr/static/src/js/gui.js
deleted file mode 100644
index 45c20b7551e..00000000000
--- a/addons/pos_hr/static/src/js/gui.js
+++ /dev/null
@@ -1,74 +0,0 @@
-odoo.define('pos_hr.gui', function (require) {
- "use strict";
-
-var core = require('web.core');
-var gui = require('point_of_sale.gui');
-var _t = core._t;
-
-gui.Gui.include({
- _show_first_screen: function () {
- if (this.pos.config.module_pos_hr) {
- this.show_screen('login');
- } else {
- this._super();
- }
- },
- select_employee: function(options) {
- options = options || {};
- var self = this;
-
- var list = [];
- this.pos.employees.forEach(function(employee) {
- if (!options.only_managers || employee.role === 'manager') {
- list.push({
- 'label': employee.name,
- 'item': employee,
- });
- }
- });
-
- var prom = new Promise(function (resolve, reject) {
- self.show_popup('selection', {
- title: options.title || _t('Select User'),
- list: list,
- confirm: resolve,
- cancel: reject,
- is_selected: function (employee) {
- return employee === self.pos.get_cashier();
- },
- });
- });
-
- return prom.then(function (employee) {
- return self.ask_password(employee.pin).then(function(){
- return employee;
- });
- });
- },
- // Ask for a password, and checks if it this
- // the same as specified by the function call.
- // Returns a promise that resolves on success,
- // fails on failure.
- ask_password: function(password) {
- var self = this;
- var prom = new Promise(function (resolve, reject) {
- if (password) {
- self.show_popup('password',{
- 'title': _t('Password ?'),
- confirm: function (pw) {
- if (Sha1.hash(pw) !== password) {
- self.show_popup('error', _t('Incorrect Password'));
- reject();
- } else {
- resolve();
- }
- },
- });
- } else {
- resolve();
- }
- });
- return prom;
- },
-});
-});
\ No newline at end of file
diff --git a/addons/pos_hr/static/src/js/models.js b/addons/pos_hr/static/src/js/models.js
index 83dbd2cfc3a..549ace40902 100644
--- a/addons/pos_hr/static/src/js/models.js
+++ b/addons/pos_hr/static/src/js/models.js
@@ -2,7 +2,6 @@ odoo.define('pos_hr.employees', function (require) {
"use strict";
var models = require('point_of_sale.models');
-var rpc = require('web.rpc');
models.load_models([{
model: 'hr.employee',
@@ -39,7 +38,7 @@ models.PosModel = models.PosModel.extend({
var self = this;
return posmodel_super.load_server_data.apply(this, arguments).then(function () {
var employee_ids = _.map(self.employees, function(employee){return employee.id;});
- var records = rpc.query({
+ var records = self.rpc({
model: 'hr.employee',
method: 'get_barcodes_and_pin_hashed',
args: [employee_ids],
diff --git a/addons/pos_hr/static/src/js/screens.js b/addons/pos_hr/static/src/js/screens.js
deleted file mode 100644
index 402bcbd7496..00000000000
--- a/addons/pos_hr/static/src/js/screens.js
+++ /dev/null
@@ -1,105 +0,0 @@
-odoo.define('pos_hr.screens', function (require) {
- "use strict";
-
-var core = require('web.core');
-var gui = require('point_of_sale.gui');
-var ScreenWidget = require('point_of_sale.screens').ScreenWidget;
-
-var _t = core._t;
-
-ScreenWidget.include({
-
- // what happens when a cashier id barcode is scanned.
- // the default behavior is the following :
- // - if there's an employee with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true
- // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup...
- barcode_cashier_action: function(code){
- var self = this;
- var employees = this.pos.employees;
- var prom;
- for(var i = 0, len = employees.length; i < len; i++){
- if(employees[i].barcode === Sha1.hash(code.code)){
- if (employees[i].id !== this.pos.get_cashier().id && employees[i].pin) {
- prom = this.gui.ask_password(employees[i].pin).then(function(){
- self.pos.set_cashier(employees[i]);
- self.chrome.widget.username.renderElement();
- return true;
- });
- } else {
- this.pos.set_cashier(employees[i]);
- this.chrome.widget.username.renderElement();
- prom = Promise.resolve(true);
- }
- break;
- }
- }
- if (!prom){
- this.barcode_error_action(code);
- return Promise.resolve(false);
- }
- else {
- return prom
- }
- },
- show: function() {
- this._super();
- this.pos.barcode_reader.set_action_callback('cashier', _.bind(this.barcode_cashier_action, this));
- },
-});
-
-/*--------------------------------------*\
- | THE LOGIN SCREEN |
-\*======================================*/
-
-// The login screen enables employees to log in to the PoS
-// at startup or after it was locked, with either barcode, pin, or both.
-
-var LoginScreenWidget = ScreenWidget.extend({
- template: 'LoginScreenWidget',
-
- /**
- * @override
- */
- show: function() {
- var self = this;
- this.$('.select-employee').click(function() {
- self.gui.select_employee({
- 'security': true,
- 'current_employee': self.pos.get_cashier(),
- 'title':_t('Change Cashier'),})
- .then(function(employee){
- self.pos.set_cashier(employee);
- self.chrome.widget.username.renderElement();
- self.unlock_screen();
- });
- });
-
- this.$('.close-session').click(function() {
- self.gui.close();
- });
-
- this._super();
- },
-
- /**
- * @override
- */
- barcode_cashier_action: function(code) {
- var self = this;
- return this._super(code).then(function () {
- self.unlock_screen();
- });
- },
-
- unlock_screen: function() {
- var screen = (this.gui.pos.get_order() ? this.gui.pos.get_order().get_screen_data('previous-screen') : this.gui.startup_screen) || this.gui.startup_screen;
- this.gui.show_screen(screen);
- }
-});
-
-gui.define_screen({name:'login', widget: LoginScreenWidget});
-
-return {
- LoginScreenWidget: LoginScreenWidget
-};
-});
diff --git a/addons/pos_hr/static/src/js/useSelectEmployee.js b/addons/pos_hr/static/src/js/useSelectEmployee.js
new file mode 100644
index 00000000000..ef8e58a5e9f
--- /dev/null
+++ b/addons/pos_hr/static/src/js/useSelectEmployee.js
@@ -0,0 +1,46 @@
+odoo.define('pos_hr.useSelectEmployee', function (require) {
+ 'use strict';
+
+ const { Component } = owl;
+
+ function useSelectEmployee() {
+ const current = Component.current;
+
+ async function askPin(employee) {
+ const { confirmed, payload: inputPin } = await this.showPopup('NumberPopup', {
+ isPassword: true,
+ title: this.env._t('Password ?'),
+ startingValue: null,
+ });
+
+ if (!confirmed) return false;
+
+ if (employee.pin === Sha1.hash(inputPin)) {
+ return employee;
+ } else {
+ await this.showPopup('ErrorPopup', {
+ title: this.env._t('Incorrect Password'),
+ });
+ return false;
+ }
+ }
+
+ async function selectEmployee(selectionList) {
+ const { confirmed, payload: employee } = await this.showPopup('SelectionPopup', {
+ title: this.env._t('Change Cashier'),
+ list: selectionList,
+ });
+
+ if (!confirmed) return false;
+
+ if (!employee.pin) {
+ return employee;
+ }
+
+ return await askPin.call(current, employee);
+ }
+ return { askPin: askPin.bind(current), selectEmployee: selectEmployee.bind(current) };
+ }
+
+ return useSelectEmployee;
+});
diff --git a/addons/pos_hr/static/src/xml/CashierName.xml b/addons/pos_hr/static/src/xml/CashierName.xml
new file mode 100644
index 00000000000..9caddada2e6
--- /dev/null
+++ b/addons/pos_hr/static/src/xml/CashierName.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+ selectCashier
+
+
+
+
diff --git a/addons/pos_hr/static/src/xml/Chrome.xml b/addons/pos_hr/static/src/xml/Chrome.xml
new file mode 100644
index 00000000000..430ed75dcdb
--- /dev/null
+++ b/addons/pos_hr/static/src/xml/Chrome.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/static/src/xml/HeaderLockButton.xml b/addons/pos_hr/static/src/xml/HeaderLockButton.xml
new file mode 100644
index 00000000000..d24a9f749ef
--- /dev/null
+++ b/addons/pos_hr/static/src/xml/HeaderLockButton.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/static/src/xml/pos.xml b/addons/pos_hr/static/src/xml/LoginScreen.xml
similarity index 55%
rename from addons/pos_hr/static/src/xml/pos.xml
rename to addons/pos_hr/static/src/xml/LoginScreen.xml
index 4e1f9128942..5d96f2b1538 100644
--- a/addons/pos_hr/static/src/xml/pos.xml
+++ b/addons/pos_hr/static/src/xml/LoginScreen.xml
@@ -1,24 +1,28 @@
-
+
-
Log in to
+
Log in to
+
+
-
+
Scan your badge
or
-
+
-
+
diff --git a/addons/pos_hr/static/tests/tours/PosHrTour.js b/addons/pos_hr/static/tests/tours/PosHrTour.js
new file mode 100644
index 00000000000..dea8b4a2baf
--- /dev/null
+++ b/addons/pos_hr/static/tests/tours/PosHrTour.js
@@ -0,0 +1,43 @@
+odoo.define('point_of_sale.tour.PosHr', function (require) {
+ 'use strict';
+
+ const { PosHr } = require('pos_hr.tour.PosHrTourMethods');
+ const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
+ const { ErrorPopup } = require('point_of_sale.tour.ErrorPopupTourMethods');
+ const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods');
+ const { SelectionPopup } = require('point_of_sale.tour.SelectionPopupTourMethods');
+ const { getSteps, startSteps } = require('point_of_sale.tour.utils');
+ const Tour = require('web_tour.tour');
+
+ startSteps();
+
+ PosHr.check.loginScreenIsShown();
+ PosHr.do.clickLoginButton();
+ SelectionPopup.check.isShown();
+ SelectionPopup.check.hasSelectionItem('Pos Employee1');
+ SelectionPopup.check.hasSelectionItem('Pos Employee2');
+ SelectionPopup.check.hasSelectionItem('Mitchell Admin');
+ SelectionPopup.do.clickItem('Pos Employee1');
+ NumberPopup.check.isShown();
+ NumberPopup.do.pressNumpad('2 5 8 1');
+ NumberPopup.check.inputShownIs('••••');
+ NumberPopup.do.clickConfirm();
+ ErrorPopup.check.isShown();
+ ErrorPopup.do.clickConfirm();
+ PosHr.do.clickLoginButton();
+ SelectionPopup.do.clickItem('Pos Employee1');
+ NumberPopup.check.isShown();
+ NumberPopup.do.pressNumpad('2 5 8 0');
+ NumberPopup.check.inputShownIs('••••');
+ NumberPopup.do.clickConfirm();
+ ProductScreen.check.isShown();
+ PosHr.check.cashierNameIs('Pos Employee1');
+ PosHr.do.clickCashierName();
+ SelectionPopup.do.clickItem('Mitchell Admin');
+ PosHr.check.cashierNameIs('Mitchell Admin');
+ PosHr.do.clickLockButton();
+ PosHr.exec.login('Pos Employee2', '1234');
+ ProductScreen.check.isShown();
+
+ Tour.register('PosHrTour', { test: true, url: '/pos/web' }, getSteps());
+});
diff --git a/addons/pos_hr/static/tests/tours/PosHrTourMethods.js b/addons/pos_hr/static/tests/tours/PosHrTourMethods.js
new file mode 100644
index 00000000000..391fc1421b6
--- /dev/null
+++ b/addons/pos_hr/static/tests/tours/PosHrTourMethods.js
@@ -0,0 +1,67 @@
+odoo.define('pos_hr.tour.PosHrTourMethods', function (require) {
+ 'use strict';
+
+ const { createTourMethods } = require('point_of_sale.tour.utils');
+ const { SelectionPopup } = require('point_of_sale.tour.SelectionPopupTourMethods');
+ const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods');
+
+ class Do {
+ clickLoginButton() {
+ return [
+ {
+ content: 'click login button',
+ trigger: '.login-overlay .login-button.select-employee',
+ },
+ ];
+ }
+ clickLockButton() {
+ return [
+ {
+ content: 'click lock button',
+ trigger: '.header-button .lock-button',
+ },
+ ];
+ }
+ clickCashierName() {
+ return [
+ {
+ content: 'click cashier name',
+ trigger: '.oe_status .username',
+ }
+ ]
+ }
+ }
+ class Check {
+ loginScreenIsShown() {
+ return [
+ {
+ content: 'login screen is shown',
+ trigger: '.login-overlay .screen-login .login-body',
+ run: () => {},
+ },
+ ];
+ }
+ cashierNameIs(name) {
+ return [
+ {
+ content: `logged cashier is '${name}'`,
+ trigger: `.pos .oe_status .username:contains("${name}")`,
+ run: () => {},
+ },
+ ];
+ }
+ }
+ class Execute {
+ login(name, pin) {
+ const res = this._do.clickLoginButton();
+ res.push(...SelectionPopup._do.clickItem(name));
+ if (pin) {
+ res.push(...NumberPopup._do.pressNumpad(pin.split('').join(' ')));
+ res.push(...NumberPopup._do.clickConfirm());
+ }
+ return res;
+ }
+ }
+
+ return { Do, Check, Execute, PosHr: createTourMethods('PosHr', Do, Check, Execute) };
+});
diff --git a/addons/pos_hr/tests/__init__.py b/addons/pos_hr/tests/__init__.py
new file mode 100644
index 00000000000..6f4bae0c7a2
--- /dev/null
+++ b/addons/pos_hr/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_frontend
diff --git a/addons/pos_hr/tests/test_frontend.py b/addons/pos_hr/tests/test_frontend.py
new file mode 100644
index 00000000000..a28f057e327
--- /dev/null
+++ b/addons/pos_hr/tests/test_frontend.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import tools
+from odoo.api import Environment
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
+from datetime import date, timedelta
+
+from odoo.tests import Form, tagged, new_test_user
+from odoo.addons.point_of_sale.tests.test_frontend import TestPointOfSaleHttpCommon
+
+
+class TestPosHrHttpCommon(TestPointOfSaleHttpCommon):
+ def setUp(self):
+ super().setUp()
+ self.main_pos_config.write({"module_pos_hr": True})
+
+ # Admin employee
+ self.env.ref("hr.employee_admin").write(
+ {"name": "Mitchell Admin", "pin": False}
+ )
+
+ # User employee
+ emp1 = self.env.ref("hr.employee_han")
+ emp1_user = new_test_user(
+ self.env,
+ login="emp1_user",
+ groups="base.group_user",
+ name="Pos Employee1",
+ email="emp1_user@pos.com",
+ )
+ emp1.write({"name": "Pos Employee1", "pin": "2580", "user_id": emp1_user.id})
+
+ # Non-user employee
+ emp2 = self.env.ref("hr.employee_jve")
+ emp2.write({"name": "Pos Employee2", "pin": "1234"})
+
+ with Form(self.main_pos_config) as config:
+ config.employee_ids.add(emp1)
+ config.employee_ids.add(emp2)
+
+
+@tagged("post_install", "-at_install")
+class TestUi(TestPosHrHttpCommon):
+ def test_01_pos_hr_tour(self):
+ # open a session, the /pos/web controller will redirect to it
+ self.main_pos_config.open_session_cb(check_coa=False)
+
+ self.start_tour(
+ "/pos/web?config_id=%d" % self.main_pos_config.id,
+ "PosHrTour",
+ login="admin",
+ step_delay=50,
+ )
diff --git a/addons/pos_hr/views/point_of_sale.xml b/addons/pos_hr/views/point_of_sale.xml
index 4362faf76ee..4c768bc1d11 100644
--- a/addons/pos_hr/views/point_of_sale.xml
+++ b/addons/pos_hr/views/point_of_sale.xml
@@ -5,9 +5,18 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+