From 118190f3b3ecde11d7bcb46301af0e016d6ea19f Mon Sep 17 00:00:00 2001 From: Gert Pellin Date: Wed, 26 Jun 2019 06:11:18 +0000 Subject: [PATCH] [IMP] point_of_sale, pos_restaurant: multi-user sessions PURPOSE ========= When working in restaurants, there are often more than one waiter working on the same floor. But Odoo doesn't support that. There can only be one person working on one session for the same tables. The purpose of this task to allow several people to work on the same session (while synchronizing sessions). SPECIFICATIONS =============== - MultiUser is active by default. - Allow multiple waiters to login on one PoS Config. - All waiters login to the same session, the waiter that does the paiment is the responsible for the order (same as now). - Synchonize the information across the multiple instances running on same floors. Syncronisation will take place at switch of table. (close tables automaticly if no activity, to force sync) - Syncro cannot be done if no internet : how do we manage it ? We should inform the user the internet connection has been lost and that synchro cannot be done. What impacts? To check Useful links https://github.com/it-projects-llc/pos-addons https://apps.odoo.com/apps/modules/11.0/pos_bus_restaurant/ Related PR: odoo#32789 TASK-ID: 1891130 closes odoo/odoo#33973 Signed-off-by: pimodoo --- addons/point_of_sale/controllers/main.py | 32 ++- addons/point_of_sale/models/pos_config.py | 18 +- addons/point_of_sale/models/pos_order.py | 148 +++++++--- addons/point_of_sale/models/pos_session.py | 22 +- addons/point_of_sale/static/src/js/chrome.js | 2 +- addons/point_of_sale/static/src/js/db.js | 48 ++++ addons/point_of_sale/static/src/js/gui.js | 10 + addons/point_of_sale/static/src/js/models.js | 105 +++++-- addons/point_of_sale/static/src/js/popups.js | 15 + addons/point_of_sale/static/src/xml/pos.xml | 22 +- .../static/tests/tours/point_of_sale.js | 8 +- addons/point_of_sale/tests/test_frontend.py | 4 +- .../tests/test_point_of_sale_flow.py | 4 - .../views/point_of_sale_dashboard.xml | 3 + addons/pos_hr/static/src/js/screens.js | 2 +- addons/pos_restaurant/models/pos_config.py | 8 + addons/pos_restaurant/models/pos_order.py | 148 ++++++++++ addons/pos_restaurant/static/src/js/floors.js | 262 +++++++++++++++--- .../static/src/js/tours/pos_restaurant.js | 228 +++++++++++++++ addons/pos_restaurant/tests/__init__.py | 4 + addons/pos_restaurant/tests/test_frontend.py | 76 +++++ .../views/pos_restaurant_templates.xml | 7 + 22 files changed, 1030 insertions(+), 146 deletions(-) create mode 100644 addons/pos_restaurant/static/src/js/tours/pos_restaurant.js create mode 100644 addons/pos_restaurant/tests/__init__.py create mode 100644 addons/pos_restaurant/tests/test_frontend.py 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 @@ + + + +