diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 833b6e32c1c..9b359da4db7 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -5,6 +5,7 @@ import werkzeug.utils from odoo import http from odoo.http import request +from odoo.osv.expression import AND _logger = logging.getLogger(__name__) @@ -12,18 +13,31 @@ _logger = logging.getLogger(__name__) class PosController(http.Controller): @http.route('/pos/web', type='http', auth='user') - def pos_web(self, **k): - # if user not logged in, log him in - pos_sessions = request.env['pos.session'].sudo().search([ - ('state', '=', 'opened'), - ('user_id', '=', request.session.uid), - ('rescue', '=', False)]) - if not pos_sessions: + def pos_web(self, config_id=False, **k): + """Open a pos session for the given config. + + The right pos session will be selected to open, if non is open yet a new session will be created. + + :param debug: The debug mode to load the session in. + :type debug: str. + :param config_id: id of the config that has to be loaded. + :type config_id: str. + :returns: object -- The rendered pos session. + """ + domain = [ + ('state', '=', 'opened'), + ('user_id', '=', request.session.uid), + ('rescue', '=', False) + ] + if config_id: + AND([domain,['config_id', '=', int(config_id)]]) + pos_session = request.env['pos.session'].sudo().search(domain, limit=1) + if not pos_session: return werkzeug.utils.redirect('/web#action=point_of_sale.action_client_pos_menu') - pos_sessions.login() + pos_session.login() # The POS only work in one company, so we enforce the one of the session in the context session_info = request.env['ir.http'].session_info() - session_info['user_context']['allowed_company_ids'] = pos_sessions.company_id.ids + session_info['user_context']['allowed_company_ids'] = pos_session.company_id.ids context = { 'session_info': session_info } diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py index 0ba6baa7d1e..f247a2fecbb 100644 --- a/addons/point_of_sale/models/pos_config.py +++ b/addons/point_of_sale/models/pos_config.py @@ -226,10 +226,13 @@ class PosConfig(models.Model): else: pos_config.currency_id = pos_config.company_id.currency_id.id - @api.depends('session_ids') + @api.depends('session_ids', 'session_ids.state') def _compute_current_session(self): + """If there is an open session, store it to current_session_id / current_session_State. + """ for pos_config in self: - session = pos_config.session_ids.filtered(lambda s: s.state != 'closed' and not s.rescue) + session = pos_config.session_ids.filtered(lambda s: s.user_id.id == self.env.uid and \ + not s.state == 'closed' and not s.rescue) # sessions ordered by id desc pos_config.current_session_id = session and session[0].id or False pos_config.current_session_state = session and session[0].state or False @@ -467,13 +470,19 @@ class PosConfig(models.Model): # Methods to open the POS def open_ui(self): - """ open the pos interface """ + """Open the pos interface with config_id as an extra argument. + + In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id + on opening a session. It is also possible to login to sessions created by other users. + + :returns: dict + """ self.ensure_one() # check all constraints, raises if any is not met self._validate_fields(set(self._fields) - {"cash_control"}) return { 'type': 'ir.actions.act_url', - 'url': '/pos/web/', + 'url': '/pos/web?config_id=%d' % self.id, 'target': 'self', } @@ -495,7 +504,6 @@ class PosConfig(models.Model): }) if self.current_session_id.state == 'opened': return self.open_ui() - return self._open_session(self.current_session_id.id) return self._open_session(self.current_session_id.id) def open_existing_session_cb(self): diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 49ffd4ecb68..de36a06468d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -48,6 +48,7 @@ class PosOrder(models.Model): 'amount_tax': ui_order['amount_tax'], 'amount_return': ui_order['amount_return'], 'company_id': self.env['pos.session'].browse(ui_order['pos_session_id']).company_id.id, + 'to_invoice': ui_order['to_invoice'] if "to_invoice" in ui_order else False, } @api.model @@ -95,22 +96,77 @@ class PosOrder(models.Model): return new_session + @api.model - def _process_order(self, pos_order): - pos_session = self.env['pos.session'].browse(pos_order['pos_session_id']) + def _process_order(self, order, draft, existing_order): + """Create or update an pos.order from a given dictionary. + + :param pos_order: dictionary representing the order. + :type pos_order: dict. + :param draft: Indicate that the pos_order is not validated yet. + :type draft: bool. + :param existing_order: order to be updated or False. + :type existing_order: pos.order. + :returns number pos_order id + """ + to_invoice = order['to_invoice'] if not draft else False + order = order['data'] + pos_session = self.env['pos.session'].browse(order['pos_session_id']) if pos_session.state == 'closing_control' or pos_session.state == 'closed': - pos_order['pos_session_id'] = self._get_valid_session(pos_order).id - order = self.create(self._order_fields(pos_order)) + order['pos_session_id'] = self._get_valid_session(order).id + + pos_order = False + if not existing_order: + pos_order = self.create(self._order_fields(order)) + else: + pos_order = existing_order + pos_order.lines.unlink() + order['user_id'] = pos_order.user_id.id + pos_order.write(self._order_fields(order)) + + self._process_payment_lines(order, pos_order, pos_session, draft) + + if not draft: + try: + pos_order.action_pos_order_paid() + except psycopg2.DatabaseError: + # do not hide transactional errors, the order(s) won't be saved! + raise + except Exception as e: + _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) + + if to_invoice: + pos_order.action_pos_order_invoice() + pos_order.account_move.sudo().with_context(force_company=self.env.user.company_id.id).post() + + return pos_order.id + + + def _process_payment_lines(self, pos_order, order, pos_session, draft): + """Create account.bank.statement.lines from the dictionary given to the parent function. + + If the payment_line is an updated version of an existing one, the existing payment_line will first be + removed before making a new one. + :param pos_order: dictionary representing the order. + :type pos_order: dict. + :param order: Order object the payment lines should belong to. + :type order: pos.order + :param pos_session: PoS session the order was created in. + :type pos_session: pos.session + :param draft: Indicate that the pos_order is not validated yet. + :type draft: bool. + """ prec_acc = order.pricelist_id.currency_id.decimal_places + + order_bank_statement_lines= self.env['pos.payment'].search([('pos_order_id', '=', order.id)]) + order_bank_statement_lines.unlink() for payments in pos_order['statement_ids']: if not float_is_zero(payments[2]['amount'], precision_digits=prec_acc): order.add_payment(self._payment_fields(order, payments[2])) - if pos_session.sequence_number <= pos_order['sequence_number']: - pos_session.write({'sequence_number': pos_order['sequence_number'] + 1}) - pos_session.refresh() + order.amount_paid = sum(order.payment_ids.mapped('amount')) - if not float_is_zero(pos_order['amount_return'], prec_acc): + if not draft and not float_is_zero(pos_order['amount_return'], prec_acc): cash_payment_method = pos_session.payment_method_ids.filtered('is_cash_count')[:1] if not cash_payment_method: raise UserError(_("No cash statement found for this session. Unable to record returned cash.")) @@ -122,7 +178,6 @@ class PosOrder(models.Model): 'payment_method_id': cash_payment_method.id, } order.add_payment(return_payment_vals) - return order def _prepare_invoice_line(self, order_line): return { @@ -195,18 +250,16 @@ class PosOrder(models.Model): ) payment_ids = fields.One2many('pos.payment', 'pos_order_id', string='Payments', readonly=True) session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False) + to_invoice = fields.Boolean('To invoice') is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced') - @api.depends('payment_ids', 'payment_ids.amount') - def _compute_amount_paid(self): - for order in self: - order.amount_paid = sum(order.payment_ids.mapped('amount')) @api.depends('account_move') def _compute_is_invoiced(self): for order in self: order.is_invoiced = bool(order.account_move) + @api.depends('date_order', 'company_id', 'currency_id', 'company_id.currency_id') def _compute_currency_rate(self): for order in self: @@ -343,34 +396,27 @@ class PosOrder(models.Model): return self.write({'state': 'cancel'}) @api.model - def create_from_ui(self, orders): - # Keep only new orders - submitted_references = [o['data']['name'] for o in orders] - pos_order = self.search([('pos_reference', 'in', submitted_references)]) - existing_orders = pos_order.read(['pos_reference']) - existing_references = set([o['pos_reference'] for o in existing_orders]) - orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] + def create_from_ui(self, orders, draft=False): + """ Create and update Orders from the frontend PoS application. + + Create new orders and update orders that are in draft status. If an order already exists with a status + diferent from 'draft'it will be discareded, otherwise it will be saved to the database. If saved with + 'draft' status the order can be overwritten later by this function. + + :param orders: dictionary with the orders to be created. + :type orders: dict. + :param draft: Indicate if the orders are ment to be finalised or temporarily saved. + :type draft: bool. + :Returns: list -- list of db-ids for the created and updated orders. + """ order_ids = [] + for order in orders: + existing_order = False + if 'server_id' in order['data']: + existing_order = self.env['pos.order'].search([('id', '=', order['data']['server_id'])], limit=1) + order_ids.append(self._process_order(order, draft, existing_order)) - for tmp_order in orders_to_save: - to_invoice = tmp_order['to_invoice'] - order = tmp_order['data'] - - pos_order = self._process_order(order) - order_ids.append(pos_order.id) - - try: - pos_order.action_pos_order_paid() - except psycopg2.DatabaseError: - # do not hide transactional errors, the order(s) won't be saved! - raise - except Exception as e: - _logger.error('Could not fully process the POS Order: %s', tools.ustr(e)) - - if to_invoice: - pos_order.action_pos_order_invoice() - pos_order.account_move.sudo().with_context(force_company=self.env.user.company_id.id).post() - return order_ids + return self.env['pos.order'].search_read(domain = [('id', 'in', order_ids)], fields = ['id', 'pos_reference']) def create_picking(self): """Create a picking for each order and validate it.""" @@ -516,7 +562,7 @@ class PosOrder(models.Model): """Create a new payment for the order""" self.ensure_one() self.env['pos.payment'].create(data) - self.amount_paid = sum(payment.amount for payment in self.payment_ids) + self.amount_paid = sum(self.payment_ids.mapped('amount')) def refund(self): """Create a copy of order for refund order""" @@ -558,6 +604,19 @@ class PosOrder(models.Model): 'target': 'current', } + @api.model + def remove_from_ui(self, server_ids): + """ Remove orders from the frontend PoS application + + Remove orders from the server by id. + :param server_ids: list of the id's of orders to remove from the server. + :type server_ids: list. + :returns: list -- list of db-ids for the removed orders. + """ + orders = self.search([('id', 'in', server_ids),('state', '=', 'draft')]) + orders.write({'state': 'cancel'}) + orders.sudo().unlink() + return orders.ids class PosOrderLine(models.Model): _name = "pos.order.line" @@ -642,6 +701,15 @@ class PosOrderLine(models.Model): values['name'] = self.env['ir.sequence'].next_by_code('pos.order.line') return super(PosOrderLine, self).create(values) + @api.model + def write(self, values): + if values.get('pack_lot_line_ids'): + for pl in values.get('pack_lot_ids'): + if pl[2].get('server_id'): + pl[2]['id'] = pl[2]['server_id'] + del pl[2]['server_id'] + return super().write(values) + @api.onchange('price_unit', 'tax_ids', 'qty', 'discount', 'product_id') def _onchange_amount_line_all(self): for line in self: diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 0f200cf971d..0ff8e78550b 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -153,16 +153,6 @@ class PosSession(models.Model): session.cash_register_id = statement.id break # stop iteration after finding the cash journal - @api.constrains('user_id', 'state') - def _check_unicity(self): - # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user - if self.search_count([ - ('state', 'not in', ('closed', 'closing_control')), - ('user_id', '=', self.user_id.id), - ('rescue', '=', False) - ]) > 1: - raise ValidationError(_("You cannot create two active sessions with the same responsible.")) - @api.constrains('config_id') def _check_pos_config(self): if self.search_count([ @@ -754,15 +744,19 @@ class PosSession(models.Model): } def open_frontend_cb(self): + """Open the pos interface with config_id as an extra argument. + + In vanilla PoS each user can only have one active session, therefore it was not needed to pass the config_id + on opening a session. It is also possible to login to sessions created by other users. + + :returns: dict + """ if not self.ids: return {} - for session in self.filtered(lambda s: s.user_id.id != self.env.uid): - raise UserError(_("You cannot use the session of another user. This session is owned by %s. " - "Please first close this one to use this point of sale.") % session.user_id.name) return { 'type': 'ir.actions.act_url', 'target': 'self', - 'url': '/pos/web/', + 'url': '/pos/web?config_id=%d' % self.config_id.id, } def open_cashbox_pos(self): diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index 436b54cea1c..c148f8ed7b4 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -248,7 +248,7 @@ var DebugWidget = PosBaseWidget.extend({ 'body': _t('This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'), confirm: function(){ self.pos.db.remove_all_orders(); - self.pos.set({synch: { state:'connected', pending: 0 }}); + self.pos.set_synch('connected', 0); }, }); }); diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 96083b0a247..cb4bd43b83f 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -488,6 +488,54 @@ var PosDB = core.Class.extend({ } return orders; }, + /** + * Return the orders with requested ids if they are unpaid. + * @param {array} ids order_ids. + * @return {array} list of orders. + */ + get_unpaid_orders_to_sync: function(ids){ + var saved = this.load('unpaid_orders',[]); + var orders = []; + saved.forEach(function(o) { + if (ids.includes(o.id) && (o.data.server_id || o.data.lines.length)){ + orders.push(o); + } + }); + return orders; + }, + /** + * Add a given order to the orders to be removed from the server. + * + * If an order is removed from a table it also has to be removed from the server to prevent it from reapearing + * after syncing. This function will add the server_id of the order to a list of orders still to be removed. + * @param {object} order object. + */ + set_order_to_remove_from_server: function(order){ + if (order.server_id !== undefined) { + var to_remove = this.load('unpaid_orders_to_remove',[]); + to_remove.push(order.server_id); + this.save('unpaid_orders_to_remove', to_remove); + } + }, + /** + * Get a list of server_ids of orders to be removed. + * @return {array} list of server_ids. + */ + get_ids_to_remove_from_server: function(){ + return this.load('unpaid_orders_to_remove',[]); + }, + /** + * Remove server_ids from the list of orders to be removed. + * @param {array} ids + */ + set_ids_removed_from_server: function(ids){ + var to_remove = this.load('unpaid_orders_to_remove',[]); + + to_remove = _.filter(to_remove, function(id){ + return !ids.includes(id); + }); + this.save('unpaid_orders_to_remove', to_remove); + }, set_cashier: function(cashier) { // Always update if the user is the same as before this.save('cashier', cashier || null); diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index d50ad71faea..0253e4aece6 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -25,6 +25,7 @@ var Gui = core.Class.extend({ this.startup_screen = null; this.current_popup = null; this.current_screen = null; + this.show_sync_errors = true; this.chrome.ready.then(function(){ self.close_other_tabs(); @@ -165,6 +166,15 @@ var Gui = core.Class.extend({ this.current_popup = this.popup_instances[name]; return this.current_popup.show(options); }, + show_sync_error_popup: function() { + if (this.show_sync_errors) { + this.show_popup('error-sync',{ + 'title':_t('Changes could not be saved'), + 'body': _t('You must be connected to the internet to save your changes.\n\n' + + 'Orders that where not synced before will be synced next time you close an order while connected to the internet or when you close the session.'), + }); + } + }, // close the current popup. close_popup: function() { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 7a3fa82e252..a2da7016a4a 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -67,6 +67,10 @@ exports.PosModel = Backbone.Model.extend({ this.order_sequence = 1; window.posmodel = this; + // Extract the config id from the url. + var given_config = new RegExp('[\?&]config_id=([^&#]*)').exec(window.location.href); + this.config_id = given_config && given_config[1] && parseInt(given_config[1]) || false; + // these dynamic attributes can be watched for change by other models or widgets this.set({ 'synch': { state:'connected', pending:0 }, @@ -251,15 +255,23 @@ exports.PosModel = Backbone.Model.extend({ },{ model: 'pos.session', fields: ['id', 'name', 'user_id', 'config_id', 'start_at', 'stop_at', 'sequence_number', 'login_number', 'payment_method_ids'], - domain: function(self){ return [['state','=','opened'],['user_id','=',session.uid]]; }, - loaded: function(self,pos_sessions,tmp){ + domain: function(self){ + var domain = [ + ['state','=','opened'], + ['rescue', '=', false], + ]; + if (self.config_id) domain.push(['config_id', '=', self.config_id]); + return domain; + }, + loaded: function(self, pos_sessions, tmp){ self.pos_session = pos_sessions[0]; + self.config_id = self.config_id || self.pos_session && self.pos_session.config_id[0]; tmp.payment_method_ids = pos_sessions[0].payment_method_ids; }, },{ model: 'pos.config', fields: [], - domain: function(self){ return [['id','=', self.pos_session.config_id[0]]]; }, + domain: function(self){ return [['id','=', self.config_id]]; }, loaded: function(self,configs){ self.config = configs[0]; self.config.use_proxy = self.config.iface_payment_terminal || @@ -860,6 +872,7 @@ exports.PosModel = Backbone.Model.extend({ transfer.then(function(order_server_id){ // generate the pdf and download it if (order_server_id.length) { + order_server_id[0] = order_server_id[0]["id"]; self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{ active_ids:order_server_id, }}).then(function () { @@ -889,27 +902,24 @@ exports.PosModel = Backbone.Model.extend({ // wrapper around the _save_to_server that updates the synch status widget _flush_orders: function(orders, options) { var self = this; - this.set('synch',{ state: 'connecting', pending: orders.length}); + this.set_synch('connecting', orders.length); - return self._save_to_server(orders, options).then(function (server_ids) { - var pending = self.db.get_orders().length; - - self.set('synch', { - state: pending ? 'connecting' : 'connected', - pending: pending - }); - - return server_ids; + return this._save_to_server(orders, options).then(function (server_ids) { + self.set_synch('connected'); + return _.pluck(server_ids, 'id'); }).catch(function(){ - var pending = self.db.get_orders().length; - if (self.get('failed')) { - self.set('synch', { state: 'error', pending: pending }); - } else { - self.set('synch', { state: 'disconnected', pending: pending }); - } + self.set_synch(self.get('failed') ? 'error' : 'disconnected'); }); }, + set_synch: function(state, pending) { + if (['connected', 'connecting', 'error', 'disconnected'].indexOf(state) === -1) { + console.error(state, ' is not a known connection state.'); + } + pending = pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length; + this.set('synch', { state: state, pending: pending }); + }, + // send an array of orders to the server // available options: // - timeout: timeout for the rpc call in ms @@ -936,6 +946,7 @@ exports.PosModel = Backbone.Model.extend({ order.to_invoice = options.to_invoice || false; return order; })]; + args.push(options.draft || false); return rpc.query({ model: 'pos.order', method: 'create_from_ui', @@ -969,6 +980,49 @@ exports.PosModel = Backbone.Model.extend({ self.set('failed',error); } console.error('Failed to send orders:', orders); + self.gui.show_sync_error_popup(); + throw reason; + }); + }, + + /** + * Remove orders with given ids from the database. + * @param {array} server_ids ids of the orders to be removed. + * @param {dict} options. + * @param {number} options.timeout optional timeout parameter for the rpc call. + * @return {Promise>} returns a promise of the ids successfully removed. + */ + _remove_from_server: function (server_ids, options) { + options = options || {}; + if (!server_ids || !server_ids.length) { + return Promise.resolve([]); + } + + var self = this; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * server_ids.length; + + return rpc.query({ + model: 'pos.order', + method: 'remove_from_ui', + args: [server_ids], + kwargs: {context: session.user_context}, + }, { + timeout: timeout, + shadow: true, + }) + .then(function (ids) { + self.db.set_ids_removed_from_server(server_ids); + return server_ids; + }).catch(function (reason){ + var error = reason.message; + if(error.code === 200 ){ // Business Logic Error, not a connection problem + //if warning do not need to display traceback!! + if (error.data.exception_type == 'warning') { + delete error.data.debug; + } + } + self.gui.show_sync_error_popup(); + console.error('Failed to remove orders:', server_ids); }); }, @@ -1334,7 +1388,7 @@ exports.Orderline = Backbone.Model.extend({ this.price = json.price_unit; this.set_discount(json.discount); this.set_quantity(json.qty, 'do not recompute unit price'); - this.id = json.id; + this.id = json.id ? json.id : orderline_id++; orderline_id = Math.max(this.id+1,orderline_id); var pack_lot_lines = json.pack_lot_ids; for (var i = 0; i < pack_lot_lines.length; i++) { @@ -2152,6 +2206,8 @@ exports.Order = Backbone.Model.extend({ this.uid = json.uid; this.name = _t("Order ") + this.uid; this.validation_date = json.creation_date; + this.server_id = json.server_id ? json.server_id : false; + this.user_id = json.user_id; if (json.fiscal_position_id) { var fiscal_position = _.find(this.pos.fiscal_positions, function (fp) { @@ -2213,7 +2269,7 @@ exports.Order = Backbone.Model.extend({ this.paymentlines.each(_.bind( function(item) { return paymentLines.push([0, 0, item.export_as_JSON()]); }, this)); - return { + var json = { name: this.get_name(), amount_paid: this.get_total_paid() - this.get_change(), amount_total: this.get_total_with_tax(), @@ -2229,8 +2285,13 @@ exports.Order = Backbone.Model.extend({ uid: this.uid, sequence_number: this.sequence_number, creation_date: this.validation_date || this.creation_date, // todo: rename creation_date in master - fiscal_position_id: this.fiscal_position ? this.fiscal_position.id : false + fiscal_position_id: this.fiscal_position ? this.fiscal_position.id : false, + server_id: this.server_id ? this.server_id : false, }; + if (!this.is_paid && this.user_id) { + json.user_id = this.user_id; + } + return json; }, export_for_printing: function(){ var orderlines = []; diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index e27ffc75dd6..89d9b8d6a7e 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -98,6 +98,21 @@ var ErrorPopupWidget = PopupWidget.extend({ gui.define_popup({name:'error', widget: ErrorPopupWidget}); +var SyncErrorPopupWidget = ErrorPopupWidget.extend({ + template:'SyncErrorPopupWidget', + show: function(opts) { + var self = this; + this._super(opts); + + this.$('.stop_showing_sync_errors').off('click').click(function(){ + self.gui.show_sync_errors = false; + self.click_confirm(); + }); + } +}); +gui.define_popup({name:'error-sync', widget: SyncErrorPopupWidget}); + + var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ template:'ErrorTracebackPopupWidget', show: function(opts) { diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 3d4c2462b94..26fe124b55e 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -1052,6 +1052,23 @@ + + + +