diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml
index 73c388bbe09..19a3b91faec 100644
--- a/addons/point_of_sale/views/pos_order_report_view.xml
+++ b/addons/point_of_sale/views/pos_order_report_view.xml
@@ -30,17 +30,15 @@
-
-
-
+
diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml
index 95d046dfed4..18e6a3ec53e 100644
--- a/addons/point_of_sale/views/pos_order_view.xml
+++ b/addons/point_of_sale/views/pos_order_view.xml
@@ -27,6 +27,7 @@
+
@@ -88,7 +89,6 @@
-
@@ -127,7 +127,11 @@
@@ -306,7 +310,7 @@
-
+
diff --git a/addons/point_of_sale/views/res_users_views.xml b/addons/point_of_sale/views/res_users_views.xml
deleted file mode 100644
index c4a70ebd317..00000000000
--- a/addons/point_of_sale/views/res_users_views.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- res.users.form.view
- res.users
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/pos_hr/__init__.py b/addons/pos_hr/__init__.py
new file mode 100644
index 00000000000..7a6c1b0355b
--- /dev/null
+++ b/addons/pos_hr/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import models
+from . import report
diff --git a/addons/pos_hr/__manifest__.py b/addons/pos_hr/__manifest__.py
new file mode 100644
index 00000000000..a741ea0cd2c
--- /dev/null
+++ b/addons/pos_hr/__manifest__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': "pos_hr",
+ 'category': "Hidden",
+ 'summary': 'Link module between Point of Sale and HR',
+
+ 'description': """
+ This module adds the possibility to log in to the PoS with Employees using barcode, pin, or both.
+ The PoS still requires one user, but it can have an unlimited number of employees.
+ """,
+
+ 'depends': ['point_of_sale', 'hr'],
+
+ 'data': [
+ 'views/pos_config.xml',
+ 'views/point_of_sale.xml',
+ 'views/pos_order_view.xml',
+ 'views/pos_order_report_view.xml',
+ ],
+ 'installable': True,
+ 'auto-install': True,
+ 'qweb': ['static/src/xml/pos.xml'],
+}
diff --git a/addons/pos_hr/models/__init__.py b/addons/pos_hr/models/__init__.py
new file mode 100644
index 00000000000..5e5c6fc9b07
--- /dev/null
+++ b/addons/pos_hr/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import pos_config
+from . import pos_order
diff --git a/addons/pos_hr/models/pos_config.py b/addons/pos_hr/models/pos_config.py
new file mode 100644
index 00000000000..efa05ca6dcf
--- /dev/null
+++ b/addons/pos_hr/models/pos_config.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+from functools import partial
+
+from odoo import models, fields
+
+
+class PosConfig(models.Model):
+ _inherit = 'pos.config'
+
+ employee_ids = fields.Many2many(
+ 'hr.employee', string="Employees with access",
+ help='If left empty, all employees can log in to the PoS session')
diff --git a/addons/pos_hr/models/pos_order.py b/addons/pos_hr/models/pos_order.py
new file mode 100644
index 00000000000..cecbbb24a63
--- /dev/null
+++ b/addons/pos_hr/models/pos_order.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api
+
+
+class PosOrder(models.Model):
+ _inherit = "pos.order"
+
+ employee_id = fields.Many2one('hr.employee', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]})
+ cashier = fields.Char(string="Cashier", compute="_compute_cashier", store=True)
+
+ @api.model
+ def _order_fields(self, ui_order):
+ order_fields = super(PosOrder, self)._order_fields(ui_order)
+ order_fields['employee_id'] = ui_order.get('employee_id')
+ return order_fields
+
+ @api.depends('employee_id', 'user_id')
+ def _compute_cashier(self):
+ for order in self:
+ if order.employee_id:
+ order.cashier = order.employee_id.name
+ else:
+ order.cashier = order.user_id.name
diff --git a/addons/pos_hr/report/__init__.py b/addons/pos_hr/report/__init__.py
new file mode 100644
index 00000000000..1c73aa0920e
--- /dev/null
+++ b/addons/pos_hr/report/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import pos_order_report
diff --git a/addons/pos_hr/report/pos_order_report.py b/addons/pos_hr/report/pos_order_report.py
new file mode 100644
index 00000000000..45e44107f8d
--- /dev/null
+++ b/addons/pos_hr/report/pos_order_report.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+
+from functools import partial
+
+from odoo import models, fields
+
+
+class PosOrderReport(models.Model):
+ _inherit = "report.pos.order"
+ employee_id = fields.Many2one(
+ 'hr.employee', string='Employee', readonly=True)
+
+ def _select(self):
+ return super(PosOrderReport, self)._select() + ',s.employee_id AS employee_id'
+
+ def _group_by(self):
+ return super(PosOrderReport, self)._group_by() + ',s.employee_id'
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..26bbf31c859
--- /dev/null
+++ b/addons/pos_hr/static/src/js/chrome.js
@@ -0,0 +1,121 @@
+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.gui.show_screen('login');
+ }
+ }
+ },
+ 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 {
+ HeaderLockButtonWidget: HeaderLockButtonWidget,
+ HeaderCloseButtonWidget: HeaderCloseButtonWidget,
+ };
+});
diff --git a/addons/pos_hr/static/src/js/gui.js b/addons/pos_hr/static/src/js/gui.js
new file mode 100644
index 00000000000..60694041920
--- /dev/null
+++ b/addons/pos_hr/static/src/js/gui.js
@@ -0,0 +1,70 @@
+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 def = new $.Deferred();
+
+ var list = [];
+ this.pos.employees.forEach(function(employee) {
+ if (!options.only_managers || employee.role === 'manager') {
+ list.push({
+ 'label': employee.name,
+ 'item': employee,
+ });
+ }
+ });
+
+ this.show_popup('selection', {
+ title: options.title || _t('Select User'),
+ list: list,
+ confirm: function(employee){ def.resolve(employee); },
+ cancel: function(){ def.reject(); },
+ is_selected: function(employee){ return employee === self.pos.get_cashier(); },
+ });
+
+ return def.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 deferred that resolves on success,
+ // fails on failure.
+ ask_password: function(password) {
+ var self = this;
+ var ret = new $.Deferred();
+ if (password) {
+ this.show_popup('password',{
+ 'title': _t('Password ?'),
+ confirm: function(pw) {
+ if (pw !== password) {
+ self.show_popup('error',_t('Incorrect Password'));
+ ret.reject();
+ } else {
+ ret.resolve();
+ }
+ },
+ });
+ } else {
+ ret.resolve();
+ }
+ return ret;
+ },
+});
+});
\ 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
new file mode 100644
index 00000000000..30121cea357
--- /dev/null
+++ b/addons/pos_hr/static/src/js/models.js
@@ -0,0 +1,56 @@
+odoo.define('pos_hr.employees', function (require) {
+ "use strict";
+
+var models = require('point_of_sale.models');
+
+models.load_models([{
+ model: 'res.users',
+ fields: ['name','groups_id'],
+ domain: function(self){ return [['company_id','=',self.user.company_id[0]],'|', ['groups_id','=', self.config.group_pos_manager_id[0]],['groups_id','=', self.config.group_pos_user_id[0]]]; },
+ loaded: function(self,users){
+ // we attribute a role to the user, 'cashier' or 'manager', depending
+ // on the group the user belongs.
+ self.users = users;
+ self.users.forEach(function(user) {
+ user.role = 'cashier';
+ user.groups_id.some(function(group_id) {
+ if (group_id === self.config.group_pos_manager_id[0]) {
+ user.role = 'manager';
+ return true;
+ }
+ });
+ // replace the current user with its updated version
+ if (user.id === self.user.id) {
+ self.user = user;
+ self.employee = self.user;
+ }
+ });
+ },
+},{
+ model: 'hr.employee',
+ fields: ['name', 'id', 'barcode', 'pin', 'user_id'],
+ loaded: function(self, employees) {
+ if (self.config.module_pos_hr) {
+ if (self.config.employee_ids.length > 0) {
+ self.employees = employees.filter(function(employee) {
+ return self.config.employee_ids.includes(employee.id) || employee.user_id[0] === self.user.id;
+ });
+ } else {
+ self.employees = employees;
+ }
+ self.employees.forEach(function(employee) {
+ var hasUser = self.users.some(function(user) {
+ if (user.id === employee.user_id[0]) {
+ employee.role = user.role;
+ return true;
+ }
+ return false;
+ });
+ if (!hasUser) {
+ employee.role = 'cashier';
+ }
+ });
+ }
+ }
+}]);
+});
diff --git a/addons/pos_hr/static/src/js/screens.js b/addons/pos_hr/static/src/js/screens.js
new file mode 100644
index 00000000000..9b7fe1c4a9a
--- /dev/null
+++ b/addons/pos_hr/static/src/js/screens.js
@@ -0,0 +1,91 @@
+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;
+ for(var i = 0, len = employees.length; i < len; i++){
+ if(employees[i].barcode === code.code){
+ if (employees[i].id !== this.pos.get_cashier().id && employees[i].pin) {
+ return 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();
+ return true;
+ }
+ }
+ }
+ this.barcode_error_action(code);
+ return false;
+ },
+ 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._super();
+ },
+
+ /**
+ * @override
+ */
+ barcode_cashier_action: function(code) {
+ this._super(code);
+ this.unlock_screen();
+ },
+
+ unlock_screen: function() {
+ var screen = (this.gui.pos.get_order() ? this.gui.pos.get_order().get_screen_data('previous-screen') : this.gui.default_screen) || this.gui.default_screen;
+ this.gui.show_screen(screen);
+ }
+});
+
+gui.define_screen({name:'login', widget: LoginScreenWidget});
+
+return {
+ LoginScreenWidget: LoginScreenWidget
+};
+});
diff --git a/addons/pos_hr/static/src/xml/pos.xml b/addons/pos_hr/static/src/xml/pos.xml
new file mode 100644
index 00000000000..3c12a975f88
--- /dev/null
+++ b/addons/pos_hr/static/src/xml/pos.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
Log in to
+
+
+
+ Scan your badge
+
+
or
+
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/views/point_of_sale.xml b/addons/pos_hr/views/point_of_sale.xml
new file mode 100644
index 00000000000..3c67fa999ed
--- /dev/null
+++ b/addons/pos_hr/views/point_of_sale.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/views/pos_config.xml b/addons/pos_hr/views/pos_config.xml
new file mode 100644
index 00000000000..595278abe9c
--- /dev/null
+++ b/addons/pos_hr/views/pos_config.xml
@@ -0,0 +1,29 @@
+
+
+
+ pos.config.form.view.inherit
+ pos.config
+
+
+
+
+ Allowed Employees
+
+
+
+
+
+
+
+
+
+
+
+ Restrict price modification to managers
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/views/pos_order_report_view.xml b/addons/pos_hr/views/pos_order_report_view.xml
new file mode 100644
index 00000000000..64884146151
--- /dev/null
+++ b/addons/pos_hr/views/pos_order_report_view.xml
@@ -0,0 +1,14 @@
+
+
+
+ report.pos.order.search.inherit
+ report.pos.order
+
+
+
+
+
+
+
+
+
diff --git a/addons/pos_hr/views/pos_order_view.xml b/addons/pos_hr/views/pos_order_view.xml
new file mode 100644
index 00000000000..a4bff34d50f
--- /dev/null
+++ b/addons/pos_hr/views/pos_order_view.xml
@@ -0,0 +1,39 @@
+
+
+
+ pos.order.form.inherit
+ pos.order
+
+
+
+
+
+
+
+
+
+
+ pos.order.list.select.inherit
+ pos.order
+
+
+
+
+
+
+
+
+
+
+
+
+ pos.order.tree.inherit
+ pos.order
+
+
+
+
+
+
+
+
diff --git a/addons/pos_sale/views/pos_config_views.xml b/addons/pos_sale/views/pos_config_views.xml
index 1a8750163e3..a494cd6fa9b 100644
--- a/addons/pos_sale/views/pos_config_views.xml
+++ b/addons/pos_sale/views/pos_config_views.xml
@@ -22,5 +22,4 @@
-