[REF] pos_hr: migration to OWL framework

This commit is contained in:
Joseph Caburnay
2020-05-12 12:48:58 +00:00
committed by Pierre Masereel
parent d8726778b6
commit 42b85f1115
20 changed files with 473 additions and 318 deletions
+6 -1
View File
@@ -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',
],
}
+11
View File
@@ -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;
}
@@ -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;
});
+22
View File
@@ -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;
});
@@ -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;
});
@@ -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;
});
-124
View File
@@ -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,
};
});
-74
View File
@@ -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;
},
});
});
+1 -2
View File
@@ -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],
-105
View File
@@ -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
};
});
@@ -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;
});
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="CashierName" t-inherit="point_of_sale.CashierName" t-inherit-mode="extension" owl="1">
<xpath expr="//div" position="attributes">
<attribute name="t-on-click">selectCashier</attribute>
</xpath>
</t>
</templates>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
<xpath expr="//HeaderButton" position="replace">
<HeaderLockButton t-if="env.pos.config.module_pos_hr" />
<HeaderButton t-if="headerButtonIsShown" />
</xpath>
</t>
</templates>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="HeaderLockButton" owl="1">
<div class="header-button lock-button" t-on-mouseover="onMouseOver(true)"
t-on-click="showLoginScreen" t-on-mouseout="onMouseOver(false)">
<span class="lock-button">
<i class="fa"
t-att-class="{ 'fa-unlock': state.isUnlockIcon, 'fa-lock': !state.isUnlockIcon }"
role="img" t-att-aria-label="state.title" t-att-title="state.title"></i>
</span>
</div>
</t>
</templates>
@@ -1,24 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="LoginScreenWidget">
<t t-name="LoginScreen" owl="1">
<div class="login-overlay">
<div class="screen-login">
<div class="login-title"><small>Log in to </small><t t-esc="widget.pos.config.name"/></div>
<div class="login-title"><small>Log in to </small>
<t t-esc="shopName" />
</div>
<div class="login-body">
<span class="login-element">
<img class="login-barcode-img" src="/point_of_sale/static/img/barcode.png"/>
<img class="login-barcode-img"
src="/point_of_sale/static/img/barcode.png" />
<div class="login-barcode-text">Scan your badge</div>
</span>
<span class="login-or">or</span>
<span class="login-element">
<button class="login-button select-employee">Select Cashier</button>
<button class="login-button select-employee"
t-on-click="selectCashier">Select Cashier</button>
</span>
</div>
<div class="login-footer">
<small>
<button class="login-button close-session">Close session</button>
</small>
</div>
<div class="login-footer">
<small>
<button class="login-button close-session" t-on-click="closeSession">Close session</button>
</small>
</div>
</div>
</div>
</t>
@@ -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());
});
@@ -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) };
});
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_frontend
+54
View File
@@ -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,
)
+12 -3
View File
@@ -5,9 +5,18 @@
<link rel="stylesheet" href="/pos_hr/static/src/css/pos.css"/>
<script type="text/javascript" src="/pos_hr/static/src/js/models.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/screens.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/gui.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/chrome.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/useSelectEmployee.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/Chrome.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/HeaderLockButton.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/CashierName.js"></script>
<script type="text/javascript" src="/pos_hr/static/src/js/LoginScreen.js"></script>
</xpath>
</template>
<template id="assets_tests" name="Pos Hr Assets Tests" inherit_id="web.assets_tests">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_hr/static/tests/tours/PosHrTourMethods.js"></script>
<script type="text/javascript" src="/pos_hr/static/tests/tours/PosHrTour.js"></script>
</xpath>
</template>
</odoo>