diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index 949b6472b1e..27fd8ae6962 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from random import choice +from string import digits import base64 import logging @@ -198,6 +200,16 @@ class Employee(models.Model): # misc notes = fields.Text('Notes') color = fields.Integer('Color Index', default=0) + barcode = fields.Char(string="Badge ID", help="ID used for employee identification.", copy=False) + pin = fields.Char(string="PIN", help="PIN used to Check In/Out in Kiosk Mode (if enabled in Configuration).", copy=False) + + _sql_constraints = [('barcode_uniq', 'unique (barcode)', "The Badge ID must be unique, this one is already assigned to another employee.")] + + @api.constrains('pin') + def _verify_pin(self): + for employee in self: + if employee.pin and not employee.pin.isdigit(): + raise ValidationError(_("The PIN must be a sequence of digits.")) @api.onchange('job_id') def _onchange_job_id(self): @@ -273,6 +285,10 @@ class Employee(models.Model): resources = self.mapped('resource_id') super(Employee, self).unlink() return resources.unlink() + + @api.multi + def generate_random_barcode(self): + for i in self: i.barcode = "".join(choice(digits) for i in range(8)) @api.depends('address_home_id.parent_id') def _compute_is_address_home_a_company(self): diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 89c2cd4a2b8..ca5eecab771 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -139,6 +139,11 @@ + + + + - - - - - - + + diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index 6d58943fc26..5428fcaa521 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -37,7 +37,6 @@ 'views/res_config_settings_views.xml', 'views/digest_views.xml', 'views/res_partner_view.xml', - 'views/res_users_views.xml', 'views/report_statement.xml', 'views/report_userlabel.xml', 'views/report_saledetails.xml', diff --git a/addons/point_of_sale/data/point_of_sale_demo.xml b/addons/point_of_sale/data/point_of_sale_demo.xml index eb792a82102..7bc0fda4a77 100644 --- a/addons/point_of_sale/data/point_of_sale_demo.xml +++ b/addons/point_of_sale/data/point_of_sale_demo.xml @@ -10,9 +10,6 @@ 0420800000008 0421800000005 - - 0410100000006 - 0410100000006 diff --git a/addons/point_of_sale/models/__init__.py b/addons/point_of_sale/models/__init__.py index 89b3974897d..2934be43e10 100644 --- a/addons/point_of_sale/models/__init__.py +++ b/addons/point_of_sale/models/__init__.py @@ -12,5 +12,4 @@ from . import pos_order from . import pos_session from . import product from . import res_partner -from . import res_users from . import res_config_settings diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 00608a966d3..99007a7abd9 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -174,6 +174,7 @@ class PosConfig(models.Model): module_pos_reprint = fields.Boolean(string="Reprint Receipt") is_posbox = fields.Boolean("PosBox") is_header_or_footer = fields.Boolean("Header & Footer") + module_pos_hr = fields.Boolean(help="Show employee login screen") def _compute_is_installed_account_accountant(self): account_accountant = self.env['ir.module.module'].sudo().search([('name', '=', 'account_accountant'), ('state', '=', 'installed')]) diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 49fe4586442..2d17216eadc 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -524,7 +524,7 @@ class PosOrder(models.Model): company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) date_order = fields.Datetime(string='Order Date', readonly=True, index=True, default=fields.Datetime.now) user_id = fields.Many2one( - comodel_name='res.users', string='Salesperson', + comodel_name='res.users', string='User', help="Person who uses the cash register. It can be a reliever, a student or an interim employee.", default=lambda self: self.env.uid, states={'done': [('readonly', True)], 'invoiced': [('readonly', True)]}, diff --git a/addons/point_of_sale/models/res_users.py b/addons/point_of_sale/models/res_users.py deleted file mode 100644 index 6fb09e8a44e..00000000000 --- a/addons/point_of_sale/models/res_users.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, _ -from odoo.exceptions import UserError - - -class ResUsers(models.Model): - _inherit = 'res.users' - - pos_security_pin = fields.Char(string='Security PIN', size=32, help='A Security PIN used to protect sensible functionality in the Point of Sale') - - @api.constrains('pos_security_pin') - def _check_pin(self): - if self.pos_security_pin and not self.pos_security_pin.isdigit(): - raise UserError(_("Security PIN can only contain digits")) diff --git a/addons/point_of_sale/report/pos_order_report.py b/addons/point_of_sale/report/pos_order_report.py index 25bd27d9bcf..4de7412ac0b 100644 --- a/addons/point_of_sale/report/pos_order_report.py +++ b/addons/point_of_sale/report/pos_order_report.py @@ -19,7 +19,7 @@ class PosOrderReport(models.Model): [('draft', 'New'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced'), ('cancel', 'Cancelled')], string='Status') - user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) + user_id = fields.Many2one('res.users', string='User', readonly=True) price_total = fields.Float(string='Total Price', readonly=True) price_sub_total = fields.Float(string='Subtotal w/o discount', readonly=True) total_discount = fields.Float(string='Total Discount', readonly=True) diff --git a/addons/point_of_sale/static/img/barcode.png b/addons/point_of_sale/static/img/barcode.png new file mode 100644 index 00000000000..2e9d2f4bc04 Binary files /dev/null and b/addons/point_of_sale/static/img/barcode.png differ diff --git a/addons/point_of_sale/static/img/login-bg-overlay.svg b/addons/point_of_sale/static/img/login-bg-overlay.svg new file mode 100644 index 00000000000..b9fe251bc42 --- /dev/null +++ b/addons/point_of_sale/static/img/login-bg-overlay.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index 59ec2f0b801..5127fe75c71 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -2149,22 +2149,6 @@ td { font-family: 'Lato'; font-family: Lato; } - -.pos .modal-dialog .popup-textinput{ - display: flex; - flex-direction: column; -} - -.pos .modal-dialog .popup-textinput .title, -.pos .modal-dialog .popup-textinput .footer{ - flex: none; -} - -.pos .modal-dialog .popup-textinput textarea{ - resize: none; - flex: auto; - margin: 0 20px 80px; -} .pos .modal-dialog .popup-confirm{ height: 250px; } @@ -2172,19 +2156,6 @@ td { overflow: auto; height: 120px; } -.pos .modal-dialog .popup-textinput{ - display: flex; - flex-direction: column; -} -.pos .modal-dialog .popup-textinput .title, -.pos .modal-dialog .popup-textinput .footer{ - flex: none; -} -.pos .modal-dialog .popup-textinput textarea{ - resize: none; - flex: auto; - margin: 0 20px 80px; -} .pos .popup .title { background: rgba(255,255,255,0.5); margin: 0; @@ -2471,3 +2442,82 @@ td { .pos .not-supported-browser img{ border-collapse: separate; } + +/* ********* Login Screen ********* */ + +.pos .login-overlay{ + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height:100%; + z-index:1000; + background: linear-gradient(to right bottom, #77717e, #c9a8a9); +} +.pos .login-overlay:before { + content: ''; + background-image: url(../../img/login-bg-overlay.svg); + background-color: rgba(0, 0, 0, 0.3); + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + width: 100%; + height:100%; +} +.pos .screen-login{ + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + margin: auto; + width:550px; + height:300px; + text-align:center; + font-size:20px; + font-weight:bold; + background-color: #F0EEEE; + border-radius: 3px; + z-index:1200; + font-family: 'Lato'; +} +.pos .login-title{ + height: 55%; + vertical-align: middle; + line-height: 5; + font-size: larger; +} +.pos .login-body{ + height:33%; +} +.pos .login-element{ + float: left; + width: 45%; + height: 60%; +} +.pos .login-barcode-img{ + width: 80px; + height: 55px; + background: white; + border: 0px; +} +.pos .login-barcode-text{ + color: #999999; + font-size: 13px; + padding-top: 0.2em; +} +.pos .login-or{ + font-size: 15px; + font-style: italic; + float: left; + width: 10%; + height: 100%; + line-height: 5; +} +.pos .login-button{ + font-size: initial; + height: 100%; + color: #555555; + border-radius: 5px; +} diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index a7ef2d69b86..d0d74c272f2 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -80,8 +80,7 @@ var OrderSelectorWidget = PosBaseWidget.extend({ /* ------- The User Name Widget ------- */ -// Displays the current cashier's name and allows -// to switch between cashiers. +// Displays the current cashier's name var UsernameWidget = PosBaseWidget.extend({ template: 'UsernameWidget', @@ -89,25 +88,6 @@ var UsernameWidget = PosBaseWidget.extend({ options = options || {}; this._super(parent,options); }, - renderElement: function(){ - var self = this; - this._super(); - - this.$el.click(function(){ - self.click_username(); - }); - }, - click_username: function(){ - var self = this; - this.gui.select_user({ - 'security': true, - 'current_user': this.pos.get_cashier(), - 'title': _t('Change Cashier'), - }).then(function(user){ - self.pos.set_cashier(user); - self.renderElement(); - }); - }, get_name: function(){ var user = this.pos.get_cashier(); if(user){ @@ -130,7 +110,9 @@ var HeaderButtonWidget = PosBaseWidget.extend({ options = options || {}; this._super(parent, options); this.action = options.action; - this.label = options.label; + this.label = options.label; + this.button_class = options.button_class; + }, renderElement: function(){ var self = this; @@ -141,8 +123,8 @@ var HeaderButtonWidget = PosBaseWidget.extend({ }); } }, - show: function(){ this.$el.removeClass('oe_hidden'); }, - hide: function(){ this.$el.addClass('oe_hidden'); }, + show: function() { this.$el.removeClass('oe_hidden'); }, + hide: function() { this.$el.addClass('oe_hidden'); }, }); /* --------- The Debug Widget --------- */ @@ -510,7 +492,6 @@ var ClientScreenWidget = PosBaseWidget.extend({ this.$el.click(function(){ self.pos.render_html_for_customer_facing_display().then(function(rendered_html) { self.pos.proxy.take_ownership_over_client_screen(rendered_html).then( - function(data) { if (typeof data === 'string') { data = JSON.parse(data); @@ -587,7 +568,6 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { this.widget = {}; // contains references to subwidgets instances this.cleanup_dom(); - this.pos.ready.done(function(){ self.build_chrome(); self.build_widgets(); @@ -824,8 +804,9 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { 'widget': HeaderButtonWidget, 'append': '.pos-rightheader', 'args': { - label: _lt('Close'), + label: _t('Close'), action: function(){ + this.$el.addClass('close_button'); var self = this; if (!this.confirmed) { this.$el.addClass('confirm'); @@ -856,30 +837,33 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, { }, ], - // This method instantiates all the screens, widgets, etc. - build_widgets: function() { - var classe; - - for (var i = 0; i < this.widgets.length; i++) { - var def = this.widgets[i]; - if ( !def.condition || def.condition.call(this) ) { - var args = typeof def.args === 'function' ? def.args(this) : def.args; - var w = new def.widget(this, args || {}); - if (def.replace) { - w.replace(this.$(def.replace)); - } else if (def.append) { - w.appendTo(this.$(def.append)); - } else if (def.prepend) { - w.prependTo(this.$(def.prepend)); + load_widgets: function(widgets) { + for (var i = 0; i < widgets.length; i++) { + var widget = widgets[i]; + if ( !widget.condition || widget.condition.call(this) ) { + var args = typeof widget.args === 'function' ? widget.args(this) : widget.args; + var w = new widget.widget(this, args || {}); + if (widget.replace) { + w.replace(this.$(widget.replace)); + } else if (widget.append) { + w.appendTo(this.$(widget.append)); + } else if (widget.prepend) { + w.prependTo(this.$(widget.prepend)); } else { w.appendTo(this.$el); } - this.widget[def.name] = w; + this.widget[widget.name] = w; } } + }, + + // This method instantiates all the screens, widgets, etc. + build_widgets: function() { + this.load_widgets(this.widgets); this.screens = {}; - for (i = 0; i < this.gui.screen_classes.length; i++) { + var classe; + for (var i = 0; i < this.gui.screen_classes.length; i++) { classe = this.gui.screen_classes[i]; if (!classe.condition || classe.condition.call(this)) { var screen = new classe.widget(this,{}); diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index 7abb931d68e..a0626326b62 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -28,18 +28,22 @@ var Gui = core.Class.extend({ this.chrome.ready.then(function(){ self.close_other_tabs(); - var order = self.pos.get_order(); - if (order) { - self.show_saved_screen(order); - } else { - self.show_screen(self.startup_screen); - } + self._show_first_screen(); self.pos.bind('change:selectedOrder', function(){ self.show_saved_screen(self.pos.get_order()); }); }); }, + _show_first_screen: function() { + var order = this.pos.get_order(); + if (order) { + this.show_saved_screen(order); + } else { + this.show_screen(this.startup_screen); + } + }, + /* ---- Gui: SCREEN MANIPULATION ---- */ // register a screen widget to the gui, @@ -226,66 +230,8 @@ var Gui = core.Class.extend({ // - only_managers: restricts the list to managers // - current_user: password will not be asked if this // user is selected. - // - title: The title of the user selection list. - select_user: function(options){ - options = options || {}; - var self = this; - var def = new $.Deferred(); - - var list = []; - for (var i = 0; i < this.pos.users.length; i++) { - var user = this.pos.users[i]; - if (!options.only_managers || user.role === 'manager') { - list.push({ - 'label': user.name, - 'item': user, - }); - } - } - - this.show_popup('selection',{ - title: options.title || _t('Select User'), - list: list, - confirm: function(user){ def.resolve(user); }, - cancel: function(){ def.reject(); }, - is_selected: function(user){ return user === self.pos.get_cashier(); }, - }); - - return def.then(function(user){ - if (options.security && user !== options.current_user && user.pos_security_pin) { - return self.ask_password(user.pos_security_pin).then(function(){ - return user; - }); - } else { - return user; - } - }); - }, - - // 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; - }, + // - title: The title of the employee selection list. + // checks if the current user (or the user provided) has manager // access rights. If not, a popup is shown allowing the user to @@ -298,7 +244,7 @@ var Gui = core.Class.extend({ if (user.role === 'manager') { return new $.Deferred().resolve(user); } else { - return this.select_user({ + return this.select_employee({ security: true, only_managers: true, title: _t('Login as a Manager'), diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 8f82fa043da..d0855c6fe18 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -55,6 +55,8 @@ exports.PosModel = Backbone.Model.extend({ this.company = null; this.user = null; this.users = []; + this.employee = {name: null, id: null, barcode: null, user_id:null, pin:null}; + this.employees = []; this.partners = []; this.cashregisters = []; this.taxes = []; @@ -89,7 +91,7 @@ exports.PosModel = Backbone.Model.extend({ // We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched, // Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched. - // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. + // when all the data has loaded, we compute some stuff, and declare the Pos ready to be used. this.ready = this.load_server_data().then(function(){ return self.after_load_server_data(); }); @@ -162,9 +164,16 @@ exports.PosModel = Backbone.Model.extend({ },{ model: 'res.users', - fields: ['name','company_id'], + fields: ['name','company_id', 'id'], ids: function(self){ return [session.uid]; }, - loaded: function(self,users){ self.user = users[0]; }, + loaded: function(self,users){ + self.user = users[0]; + self.user.role = 'manager'; + self.employee.name = self.user.name; + self.employee.user_id = [self.user.id, self.user.name]; + self.employees = [self.employee]; + self.set_cashier(self.employee); + }, },{ model: 'res.company', fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id', 'tax_calculation_rounding_method'], @@ -262,39 +271,6 @@ exports.PosModel = Backbone.Model.extend({ self.pos_session.sequence_number = Math.max(self.pos_session.sequence_number, orders[i].data.sequence_number+1); } }, - },{ - model: 'res.users', - fields: ['name','pos_security_pin','groups_id','barcode'], - 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. - var pos_users = []; - var current_cashier = self.get_cashier(); - for (var i = 0; i < users.length; i++) { - var user = users[i]; - for (var j = 0; j < user.groups_id.length; j++) { - var group_id = user.groups_id[j]; - if (group_id === self.config.group_pos_manager_id[0]) { - user.role = 'manager'; - break; - } else if (group_id === self.config.group_pos_user_id[0]) { - user.role = 'cashier'; - } - } - if (user.role) { - pos_users.push(user); - } - // replace the current user with its updated version - if (user.id === self.user.id) { - self.user = user; - } - if (user.id === current_cashier.id) { - self.set_cashier(user); - } - } - self.users = pos_users; - }, },{ model: 'stock.location', fields: [], @@ -653,13 +629,13 @@ exports.PosModel = Backbone.Model.extend({ get_cashier: function(){ // reset the cashier to the current user if session is new if (this.db.load('pos_session_id') !== this.pos_session.id) { - this.set_cashier(this.user); + this.set_cashier(this.employee); } - return this.db.get_cashier() || this.get('cashier') || this.user; + return this.db.get_cashier() || this.get('cashier') || this.employee; }, // changes the current cashier - set_cashier: function(user){ - this.set('cashier', user); + set_cashier: function(employee){ + this.set('cashier', employee); this.db.set_cashier(this.get('cashier')); }, //creates a new empty order and sets it as the current order @@ -2007,6 +1983,7 @@ exports.Order = Backbone.Model.extend({ this.orderlines = new OrderlineCollection(); this.paymentlines = new PaymentlineCollection(); this.pos_session_id = this.pos.pos_session.id; + this.employee = this.pos.employee; this.finalized = false; // if true, cannot be modified. this.set_pricelist(this.pos.default_pricelist); @@ -2131,7 +2108,8 @@ exports.Order = Backbone.Model.extend({ pos_session_id: this.pos_session_id, pricelist_id: this.pricelist ? this.pricelist.id : false, partner_id: this.get_client() ? this.get_client().id : false, - user_id: this.pos.get_cashier().id, + user_id: this.pos.user.id, + employee_id: this.pos.get_cashier().id, uid: this.uid, sequence_number: this.sequence_number, creation_date: this.validation_date || this.creation_date, // todo: rename creation_date in master diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index c5873ddd324..4bf06abdc6b 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -68,32 +68,6 @@ var ScreenWidget = PosBaseWidget.extend({ this.barcode_error_action(code); } }, - - // what happens when a cashier id barcode is scanned. - // the default behavior is the following : - // - if there's a user 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 users = this.pos.users; - for(var i = 0, len = users.length; i < len; i++){ - if(users[i].barcode === code.code){ - if (users[i].id !== this.pos.get_cashier().id && users[i].pos_security_pin) { - return this.gui.ask_password(users[i].pos_security_pin).then(function(){ - self.pos.set_cashier(users[i]); - self.chrome.widget.username.renderElement(); - return true; - }); - } else { - this.pos.set_cashier(users[i]); - this.chrome.widget.username.renderElement(); - return true; - } - } - } - this.barcode_error_action(code); - return false; - }, // what happens when a client id barcode is scanned. // the default behavior is the following : @@ -139,7 +113,6 @@ var ScreenWidget = PosBaseWidget.extend({ } this.pos.barcode_reader.set_action_callback({ - 'cashier': _.bind(self.barcode_cashier_action, self), 'product': _.bind(self.barcode_product_action, self), 'weight': _.bind(self.barcode_product_action, self), 'price': _.bind(self.barcode_product_action, self), diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index f8626cab1bd..78dfa8cdca2 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -359,7 +359,7 @@ odoo.define('point_of_sale.tour.acceptance', function (require) { trigger: ".header-button", }, { content: "confirm closing the frontend", - trigger: ".header-button.confirm", + trigger: ".header-button", run: function() {}, //it's a check, }]); diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 95ecfc9874f..b094c035b4d 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -6,11 +6,13 @@
-
+
+ +
@@ -109,7 +111,12 @@
- + + + + + +
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 5669bc6ce85..8dd29cb7bfe 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -41,6 +41,14 @@