diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 600e04e02a8..33af3087e86 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -810,22 +810,6 @@ class PosOrder(models.Model): mail = self.env['mail.mail'].sudo().create(mail_values) mail.send() - @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'}) - # TODO Looks like delete cascade is a better solution. - orders.mapped('payment_ids').sudo().unlink() - orders.sudo().unlink() - return orders.ids - @api.model def search_paid_order_ids(self, config_id, domain, limit, offset): """Search for 'paid' orders that satisfy the given domain, limit and offset.""" diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 819f28d9952..97544d76154 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -233,6 +233,9 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { // Always show the next screen regardless of error since pos has to // continue working even offline. this.showScreen(this.nextScreen); + // Remove the order from the local storage so that when we refresh the page, the order + // won't be there + this.env.pos.db.remove_unpaid_order(this.currentOrder); // Ask the user to sync the remaining unsynced orders. if (!hasError && syncOrderResult && this.env.pos.db.get_orders().length) { diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 298e3ef7c4c..3fd4fcf5cd6 100644 --- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -38,6 +38,9 @@ odoo.define('point_of_sale.ReceiptScreen', function (require) { }, 0); }); } + _addNewOrder() { + this.env.pos.add_new_order(); + } async onSendEmail() { if (!is_email(this.orderUiState.inputEmail)) { this.orderUiState.emailSuccessful = false; @@ -89,7 +92,8 @@ odoo.define('point_of_sale.ReceiptScreen', function (require) { } } orderDone() { - this.currentOrder.finalize(); + this.env.pos.removeOrder(this.currentOrder); + this._addNewOrder(); const { name, props } = this.nextScreen; this.showScreen(name, props); } diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js index 8db5625f5e7..99ca55b6ac3 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js @@ -99,6 +99,11 @@ odoo.define('point_of_sale.TicketScreen', function (require) { this.env.pos.add_new_order(); this.showScreen('ProductScreen'); } + _selectNextOrder(currentOrder) { + const currentOrderIndex = this._getOrderList().indexOf(currentOrder); + const orderList = this._getOrderList(); + this.env.pos.set_order(orderList[currentOrderIndex+1] || orderList[currentOrderIndex-1]); + } async _onDeleteOrder({ detail: order }) { const screen = order.get_screen_data(); if (['ProductScreen', 'PaymentScreen'].includes(screen.name) && order.get_orderlines().length > 0) { @@ -112,7 +117,10 @@ odoo.define('point_of_sale.TicketScreen', function (require) { if (!confirmed) return; } if (order && (await this._onBeforeDeleteOrder(order))) { - order.destroy({ reason: 'abandon' }); + if (order === this.env.pos.get_order()) { + this._selectNextOrder(order); + } + this.env.pos.removeOrder(order); } } async _onNextPage() { diff --git a/addons/point_of_sale/static/src/js/db.js b/addons/point_of_sale/static/src/js/db.js index 4d24a40bf60..a14276129b7 100644 --- a/addons/point_of_sale/static/src/js/db.js +++ b/addons/point_of_sale/static/src/js/db.js @@ -519,14 +519,8 @@ var PosDB = core.Class.extend({ * @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)){ - orders.push(o); - } - }); - return orders; + const savedOrders = this.load('unpaid_orders',[]); + return savedOrders.filter(order => ids.includes(order.id)); }, /** * Add a given order to the orders to be removed from the server. diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index b09c6b3ff25..f2e71291bdc 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -353,21 +353,14 @@ class PosGlobalState extends PosModel { this.selectedCategoryId = categoryId; } - // this is called when an order is removed from the order collection. It ensures that there is always an existing - // order and a valid selected order - on_removed_order(removed_order,index,reason){ - var order_list = this.get_order_list(); - if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ - // when we intentionally remove an unfinished order, and there is another existing one - this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true }); - }else{ - // when the order was automatically removed after completion, - // or when we intentionally delete the only concurrent order - this.add_new_order({ silent: true }); - } - // Remove the link between the refund orderlines when deleting an order - // that contains a refund. - for (const line of removed_order.get_orderlines()) { + /** + * Remove the order passed in params from the list of orders + * @param order + */ + removeOrder(order) { + this.orders.remove(order); + this.db.remove_unpaid_order(order); + for (const line of order.get_orderlines()) { if (line.refunded_orderline_id) { delete this.toRefundLines[line.refunded_orderline_id]; } @@ -387,22 +380,25 @@ class PosGlobalState extends PosModel { cashierHasPriceControlRights() { return !this.config.restrict_price_control || this.get_cashier().role == 'manager'; } - createAutomaticallySavedOrder(json) { + _onReactiveOrderUpdated(order) { + order.save_to_db(); + } + createReactiveOrder(json) { const options = {pos:this}; if (json) { options.json = json; } let order = Order.create({}, options); - const batchedSaveToDb = batched(() => { - order.save_to_db(); + const batchedCallback = batched(() => { + this._onReactiveOrderUpdated(order) }); - order = reactive(order, batchedSaveToDb); + order = reactive(order, batchedCallback); order.save_to_db(); return order; } // creates a new empty order and sets it as the current order - add_new_order(options){ - var order = this.createAutomaticallySavedOrder(); + add_new_order(){ + const order = this.createReactiveOrder(); this.orders.add(order); this.selectedOrder = order; return order; @@ -422,13 +418,13 @@ class PosGlobalState extends PosModel { for (var i = 0; i < jsons.length; i++) { var json = jsons[i]; if (json.pos_session_id === this.pos_session.id) { - orders.push(this.createAutomaticallySavedOrder(json)); + orders.push(this.createReactiveOrder(json)); } } for (var i = 0; i < jsons.length; i++) { var json = jsons[i]; if (json.pos_session_id !== this.pos_session.id && (json.lines.length > 0 || json.statement_ids.length > 0)) { - orders.push(this.createAutomaticallySavedOrder(json)); + orders.push(this.createReactiveOrder(json)); } else if (json.pos_session_id !== this.pos_session.id) { this.db.remove_unpaid_order(jsons[i]); } @@ -523,14 +519,6 @@ class PosGlobalState extends PosModel { return this.orders; } - //removes the current order - delete_current_order(){ - var order = this.get_order(); - if (order) { - order.destroy({'reason':'abandon'}); - } - } - _convert_product_img_to_base64 (product, url) { return new Promise(function (resolve, reject) { var img = new Image(); @@ -660,21 +648,16 @@ class PosGlobalState extends PosModel { }); } - // wrapper around the _save_to_server that updates the synch status widget + // Send validated orders to the backend. // Resolves to the backend ids of the synced orders. _flush_orders(orders, options) { var self = this; - this.set_synch('connecting', orders.length); return this._save_to_server(orders, options).then(function (server_ids) { - self.set_synch('connected'); for (let i = 0; i < server_ids.length; i++) { self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = server_ids[i].id; } return server_ids; - }).catch(function(error){ - self.set_synch(self.failed ? 'error' : 'disconnected'); - throw error; }).finally(function() { self._after_flush_orders(orders); }); @@ -721,7 +704,7 @@ class PosGlobalState extends PosModel { if (!orders || !orders.length) { return Promise.resolve([]); } - + this.set_synch('connecting', orders.length); options = options || {}; var self = this; @@ -753,6 +736,7 @@ class PosGlobalState extends PosModel { self.db.remove_order(order_id); }); self.failed = false; + self.set_synch('connected'); return server_ids; }).catch(function (error){ console.warn('Failed to send orders:', orders); @@ -760,61 +744,15 @@ class PosGlobalState extends PosModel { // Hide error if already shown before ... if ((!self.failed || options.show_error) && !options.to_invoice) { self.failed = error; + self.set_synch('error'); throw error; } } + self.set_synch('disconnected'); throw error; }); } - /** - * 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 (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 this.env.services.rpc({ - model: 'pos.order', - method: 'remove_from_ui', - args: [server_ids], - kwargs: {context: this.env.session.user_context}, - }, { - timeout: timeout, - shadow: true, - }) - .then(function (data) { - return self._post_remove_from_server(server_ids, data) - }).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; - } - } - // important to throw error here and let the rendering component handle the - // error - console.warn('Failed to remove orders:', server_ids); - throw error; - }); - } - - // to override - _post_remove_from_server(server_ids, data) { - this.db.set_ids_removed_from_server(server_ids); - return server_ids; - } - // Exports the paid orders (the ones waiting for internet connection) export_paid_orders() { return JSON.stringify({ @@ -883,7 +821,7 @@ class PosGlobalState extends PosModel { } else if (existing_uids[order.uid]) { report.unpaid_skipped_existing += 1; } else { - orders.push(this.createAutomaticallySavedOrder(order)); + orders.push(this.createReactiveOrder(order)); } } @@ -910,7 +848,7 @@ class PosGlobalState extends PosModel { for (var i = 0; i < jsons.length; i++) { var json = jsons[i]; if (json.pos_session_id === this.pos_session.id) { - orders.push(this.createAutomaticallySavedOrder(json)); + orders.push(this.createReactiveOrder(json)); } else { not_loaded_count += 1; } @@ -2552,8 +2490,9 @@ class Order extends PosModel { add_product(product, options){ if(this._printed){ - this.destroy(); - return this.pos.get_order().add_product(product, options); + // when adding product with a barcode while being in receipt screen + this.pos.removeOrder(this); + return this.pos.add_new_order().add_product(product, options); } this.assert_editable(); options = options || {}; @@ -2996,14 +2935,6 @@ class Order extends PosModel { return sum + orderLine.get_total_cost(); }), 0) } - finalize(){ - this.destroy(); - } - destroy(options){ - const deletedIndex = this.pos.orders.remove(this); - this.pos.db.remove_unpaid_order(this); - this.pos.on_removed_order(this, deletedIndex, options && options.reason); - } /* ---- Invoice --- */ set_to_invoice(to_invoice) { this.assert_editable(); diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js index e419029f9bf..a8eca669471 100644 --- a/addons/point_of_sale/static/src/js/utils.js +++ b/addons/point_of_sale/static/src/js/utils.js @@ -76,14 +76,27 @@ odoo.define('point_of_sale.utils', function (require) { await Promise.resolve(); if (!called) { called = true; + // so that only the first call to the batched function calls the original callback. + // Schedule this before calling the callback so that calls to the batched function + // within the callback will proceed only after resetting called to false, and have + // a chance to execute the callback again + Promise.resolve().then(() => called = false); callback(); - // wait for all calls in this microtick to fall through before resetting "called" - // so that only the first call to the batched function calls the original callback - await Promise.resolve(); - called = false; } }; } - return { getFileAsText, nextFrame, identifyError, isConnectionError, batched }; + /* + * comes from o_spreadsheet.js + * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript + * */ + function uuidv4() { + // mainly for jest and other browsers that do not have the crypto functionality + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + let r = (Math.random() * 16) | 0, v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + }; + + return { getFileAsText, nextFrame, identifyError, isConnectionError, batched, uuidv4 }; }); diff --git a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js index 040f686b42a..297042bf397 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js @@ -143,6 +143,7 @@ odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) { return [ { trigger: '.ticket-screen:not(:has(.order-empty))', + run: () => {}, }, ]; } @@ -166,13 +167,23 @@ odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) { return [ { trigger: `.ticket-screen .to-refund-highlight:contains("${text}")`, - }, + run: () => {}, + }, ]; } refundedNoteContains(text) { return [ { trigger: `.ticket-screen .refund-note:contains("${text}")`, + run: () => {}, + }, + ]; + } + tipContains(amount) { + return [ + { + trigger: `.ticket-screen .tip-cell:contains("${amount}")`, + run: () => {}, }, ]; } diff --git a/addons/pos_epson_printer_restaurant/static/src/js/multiprint.js b/addons/pos_epson_printer_restaurant/static/src/js/models.js similarity index 83% rename from addons/pos_epson_printer_restaurant/static/src/js/multiprint.js rename to addons/pos_epson_printer_restaurant/static/src/js/models.js index e26d4701309..b4ad75b471e 100644 --- a/addons/pos_epson_printer_restaurant/static/src/js/multiprint.js +++ b/addons/pos_epson_printer_restaurant/static/src/js/models.js @@ -1,4 +1,4 @@ -odoo.define('pos_epson_printer_restaurant.multiprint', function (require) { +odoo.define('pos_epson_printer_restaurant.models', function (require) { "use strict"; var { PosGlobalState } = require('point_of_sale.models'); @@ -7,8 +7,8 @@ const Registries = require('point_of_sale.Registries'); // The override of create_printer needs to happen after its declaration in // pos_restaurant. We need to make sure that this code is executed after the -// multiprint file in pos_restaurant. -require('pos_restaurant.multiprint'); +// models file in pos_restaurant. +require('pos_restaurant.models'); const PosEpsonResPosGlobalState = (PosGlobalState) => class PosEpsonResPosGlobalState extends PosGlobalState { diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index 258b364b88c..b74f3286a09 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -37,6 +37,7 @@ export class PosLoyaltyCard { } const PosLoyaltyGlobalState = (PosGlobalState) => class PosLoyaltyGlobalState extends PosGlobalState { + //@override async _processData(loadedData) { await super._processData(loadedData); this.programs = loadedData['loyalty.program'] || []; //TODO: rename to `loyaltyPrograms` etc diff --git a/addons/pos_restaurant/__manifest__.py b/addons/pos_restaurant/__manifest__.py index 07417d673f2..ec3ed174a7d 100644 --- a/addons/pos_restaurant/__manifest__.py +++ b/addons/pos_restaurant/__manifest__.py @@ -30,33 +30,8 @@ This module adds several features to the Point of Sale that are specific to rest 'installable': True, 'assets': { 'point_of_sale.assets': [ - 'pos_restaurant/static/lib/js/jquery.ui.touch-punch.js', - 'pos_restaurant/static/src/js/multiprint.js', - 'pos_restaurant/static/src/js/floors.js', - 'pos_restaurant/static/src/js/notes.js', - 'pos_restaurant/static/src/js/payment.js', - 'pos_restaurant/static/src/js/Resizeable.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js', - 'pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js', - 'pos_restaurant/static/src/js/Screens/BillScreen.js', - 'pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js', - 'pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js', - 'pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js', - 'pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js', - 'pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js', - 'pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js', - 'pos_restaurant/static/src/js/Screens/TicketScreen.js', - 'pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js', - 'pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js', - 'pos_restaurant/static/src/js/Chrome.js', - 'pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js', - 'pos_restaurant/static/src/js/Screens/PaymentScreen.js', - 'pos_restaurant/static/src/js/Screens/TipScreen.js', + 'pos_restaurant/static/lib/**/*.js', + 'pos_restaurant/static/src/js/**/*.js', ('after', 'point_of_sale/static/src/css/pos.css', 'pos_restaurant/static/src/css/restaurant.css'), ], 'web.assets_backend': [ diff --git a/addons/pos_restaurant/models/pos_order.py b/addons/pos_restaurant/models/pos_order.py index 789bfcf8bec..dffd7e830a7 100644 --- a/addons/pos_restaurant/models/pos_order.py +++ b/addons/pos_restaurant/models/pos_order.py @@ -11,8 +11,8 @@ class PosOrderLine(models.Model): _inherit = 'pos.order.line' note = fields.Char('Internal Note added by the waiter.') + uuid = fields.Char(string='Uuid', readonly=True, copy=False) mp_skip = fields.Boolean('Skip line when sending ticket to kitchen printers.') - mp_dirty = fields.Boolean() class PosOrder(models.Model): @@ -20,7 +20,7 @@ class PosOrder(models.Model): table_id = fields.Many2one('restaurant.table', string='Table', help='The table where this order was served', index='btree_not_null') customer_count = fields.Integer(string='Guests', help='The amount of customers that have been served by this order.') - multiprint_resume = fields.Char() + multiprint_resume = fields.Char(string='Multiprint Resume', help="Last printed state of the order") def _get_pack_lot_lines(self, order_lines): """Add pack_lot_lines to the order_lines. @@ -57,8 +57,8 @@ class PosOrder(models.Model): 'order_id', 'qty', 'note', + 'uuid', 'mp_skip', - 'mp_dirty', 'full_product_name', 'customer_note', 'price_extra', @@ -193,6 +193,22 @@ class PosOrder(models.Model): return table_orders + @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'}) + # TODO Looks like delete cascade is a better solution. + orders.mapped('payment_ids').sudo().unlink() + orders.sudo().unlink() + return orders.ids + def set_tip(self, tip_line_vals): """Set tip to `self` based on values in `tip_line_vals`.""" diff --git a/addons/pos_restaurant/static/src/css/restaurant.css b/addons/pos_restaurant/static/src/css/restaurant.css index 5f7cc549522..0eea59fd128 100644 --- a/addons/pos_restaurant/static/src/css/restaurant.css +++ b/addons/pos_restaurant/static/src/css/restaurant.css @@ -293,7 +293,7 @@ padding-right: 16px; } .pos .order-button.floor-button .table-name { - font-weight: normal; + margin-left: 5px; } .pos .order-button.floor-button .fa{ font-size: 24px; @@ -575,5 +575,9 @@ .multiprint-flex { display: flex; - justify-content: space-between; + margin-bottom: 10px; +} + +.multiprint-flex .product-quantity { + margin-right: 50px; } diff --git a/addons/pos_restaurant/static/src/js/Chrome.js b/addons/pos_restaurant/static/src/js/Chrome.js index 58141ec9a70..5b9162d6837 100644 --- a/addons/pos_restaurant/static/src/js/Chrome.js +++ b/addons/pos_restaurant/static/src/js/Chrome.js @@ -57,6 +57,12 @@ odoo.define('pos_restaurant.chrome', function (require) { this.trigger('close-temp-screen'); } const table = this.env.pos.table; + const order = this.env.pos.get_order(); + if (order && order.get_screen_data().name === 'ReceiptScreen') { + // When the order is finalized, we can safely remove it from the memory + // We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state + this.env.pos.removeOrder(order); + } this.showScreen('FloorScreen', { floor: table ? table.floor : null }); } _shouldResetIdleTimer() { diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js index 58c8566d10c..7b60ba00d61 100644 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js @@ -4,18 +4,25 @@ odoo.define('pos_restaurant.BackToFloorButton', function (require) { const PosComponent = require('point_of_sale.PosComponent'); const Registries = require('point_of_sale.Registries'); + /** + * Props: { + * onClick: callback + * } + */ class BackToFloorButton extends PosComponent { get table() { - return (this.env.pos && this.env.pos.table) || null; + return this.env.pos.table; } get floor() { - const table = this.table; - return table ? table.floor : null; + return this.table ? this.table.floor : null; } get hasTable() { - return this.table !== null; + return this.table != null; } backToFloorScreen() { + if (this.props.onClick) { + this.props.onClick(); + } this.showScreen('FloorScreen', { floor: this.floor }); } } diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js index 47476e6dcb9..31c6f5ba346 100644 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js @@ -12,8 +12,8 @@ odoo.define('pos_restaurant.TicketButton', function (require) { */ get count() { if (!this.env.pos || !this.env.pos.config) return 0; - if (this.env.pos.config.iface_floorplan && !this.env.pos.table) { - return this.env.pos.orders.length; + if (this.env.pos.config.iface_floorplan && this.env.pos.table) { + return this.env.pos.getTableOrders(this.env.pos.table.id).length; } else { return super.count; } diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js index 0b2e8898989..a80336468db 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js @@ -43,14 +43,14 @@ odoo.define('pos_restaurant.EditableTable', function(require) { table.height = size.height; table.position_v = loc.top; table.position_h = loc.left; - this.trigger('save-table', this.props.table); + this.props.onSaveTable(this.props.table); } _onDragEnd(event) { const { loc } = event.detail; const table = this.props.table; table.position_v = loc.top; table.position_h = loc.left; - this.trigger('save-table', this.props.table); + this.props.onSaveTable(this.props.table); } } EditableTable.template = 'EditableTable'; diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js index dc1ddc445a0..d930f9cb888 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -2,9 +2,9 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { 'use strict'; const PosComponent = require('point_of_sale.PosComponent'); - const { useListener } = require("@web/core/utils/hooks"); const Registries = require('point_of_sale.Registries'); const { debounce } = require("@web/core/utils/timing"); + const { isConnectionError } = require('point_of_sale.utils'); const { onPatched, onMounted, onWillUnmount, useRef, useState } = owl; @@ -15,19 +15,6 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { */ setup() { super.setup(); - this._setTableColor = debounce(this._setTableColor, 70); - this._setFloorColor = debounce(this._setFloorColor, 70); - useListener('select-table', this._onSelectTable); - useListener('deselect-table', this._onDeselectTable); - useListener('save-table', this._onSaveTable); - useListener('create-table', this._createTable); - useListener('duplicate-table', this._duplicateTable); - useListener('rename-table', this._renameTable); - useListener('change-seats-num', this._changeSeatsNum); - useListener('change-shape', this._changeShape); - useListener('set-table-color', this._setTableColor); - useListener('set-floor-color', this._setFloorColor); - useListener('delete-table', this._deleteTable); const floor = this.props.floor ? this.props.floor : this.env.pos.floors[0]; this.state = useState({ selectedFloorId: floor.id, @@ -47,7 +34,7 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { } onMounted() { if (this.env.pos.table) { - this.env.pos.set_table(null); + this.env.pos.unsetTable(); } this.env.posbus.trigger('start-cash-control'); this.floorMapRef.el.style.background = this.state.floorBackground; @@ -59,6 +46,124 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { onWillUnmount() { clearInterval(this.tableLongpolling); } + _computePinchHypo(ev, callbackFunction) { + const touches = ev.touches; + // If two pointers are down, check for pinch gestures + if (touches.length === 2) { + const deltaX = touches[0].pageX - touches[1].pageX; + const deltaY = touches[0].pageY - touches[1].pageY; + callbackFunction(Math.hypot(deltaX, deltaY)) + } + } + _onPinchStart(ev) { + ev.currentTarget.style.setProperty('touch-action', 'none'); + this._computePinchHypo(ev, this.startPinch.bind(this)); + } + _onPinchEnd(ev) { + ev.currentTarget.style.removeProperty('touch-action'); + } + _onPinchMove(ev) { + debounce(this._computePinchHypo, 10, true)(ev, this.movePinch.bind(this)); + } + _onDeselectTable() { + this.state.selectedTableId = null; + } + async _createTableHelper(copyTable) { + let newTable; + if (copyTable) { + newTable = Object.assign({}, copyTable); + newTable.position_h += 10; + newTable.position_v += 10; + } else { + newTable = { + position_v: 100, + position_h: 100, + width: 75, + height: 75, + shape: 'square', + seats: 1, + }; + } + newTable.name = this._getNewTableName(newTable.name); + delete newTable.id; + newTable.floor_id = [this.activeFloor.id, '']; + newTable.floor = this.activeFloor; + try { + await this._save(newTable); + this.activeTables.push(newTable); + return newTable; + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to create table because you are offline.'), + }); + return; + } else { + throw error; + } + } + } + _getNewTableName(name) { + if (name) { + const num = Number((name.match(/\d+/g) || [])[0] || 0); + const str = name.replace(/\d+/g, ''); + const n = { num: num, str: str }; + n.num += 1; + this._lastName = n; + } else if (this._lastName) { + this._lastName.num += 1; + } else { + this._lastName = { num: 1, str: 'T' }; + } + return '' + this._lastName.str + this._lastName.num; + } + async _save(table) { + const tableCopy = { ...table }; + delete tableCopy.floor; + const tableId = await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [tableCopy], + }); + table.id = tableId; + this.env.pos.tables_by_id[tableId] = table; + } + async _tableLongpolling() { + if (this.state.isEditMode) { + return; + } + try { + const result = await this.rpc({ + model: 'pos.config', + method: 'get_tables_order_count', + args: [this.env.pos.config.id], + }); + result.forEach((table) => { + const table_obj = this.env.pos.tables_by_id[table.id]; + const unsynced_orders = this.env.pos + .getTableOrders(table_obj.id) + .filter( + (o) => + o.server_id === undefined && + (o.orderlines.length !== 0 || o.paymentlines.length !== 0) && + // do not count the orders that are already finalized + !o.finalized + ).length; + table_obj.order_count = table.orders + unsynced_orders; + }); + this.render(); + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to get orders count'), + }); + } else { + throw error; + } + } + } get activeFloor() { return this.env.pos.floors_by_id[this.state.selectedFloorId]; } @@ -106,20 +211,60 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { this.state.isEditMode = !this.state.isEditMode; this.state.selectedTableId = null; } - async _createTable() { + async onSelectTable(table) { + if (this.state.isEditMode) { + this.state.selectedTableId = table.id; + } else { + try { + if (this.env.pos.orderToTransfer) { + await this.env.pos.transferTable(table); + } else { + await this.env.pos.setTable(table); + } + } catch (error) { + if (isConnectionError(error)) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to fetch orders'), + }); + } else { + throw error; + } + } + const order = this.env.pos.get_order(); + this.showScreen(order.get_screen_data().name); + } + } + async onSaveTable(table) { + await this._save(table); + } + async createTable() { const newTable = await this._createTableHelper(); if (newTable) { this.state.selectedTableId = newTable.id; } } - async _duplicateTable() { + async duplicateTable() { if (!this.selectedTable) return; const newTable = await this._createTableHelper(this.selectedTable); if (newTable) { this.state.selectedTableId = newTable.id; } } - async _changeSeatsNum() { + async renameTable() { + const selectedTable = this.selectedTable; + if (!selectedTable) return; + const { confirmed, payload: newName } = await this.showPopup('TextInputPopup', { + startingValue: selectedTable.name, + title: this.env._t('Table Name ?'), + }); + if (!confirmed) return; + if (newName !== selectedTable.name) { + selectedTable.name = newName; + await this._save(selectedTable); + } + } + async changeSeatsNum() { const selectedTable = this.selectedTable if (!selectedTable) return; const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { @@ -135,31 +280,18 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { await this._save(selectedTable); } } - async _changeShape() { + async changeShape() { if (!this.selectedTable) return; this.selectedTable.shape = this.selectedTable.shape === 'square' ? 'round' : 'square'; this.render(); await this._save(this.selectedTable); } - async _renameTable() { - const selectedTable = this.selectedTable; - if (!selectedTable) return; - const { confirmed, payload: newName } = await this.showPopup('TextInputPopup', { - startingValue: selectedTable.name, - title: this.env._t('Table Name ?'), - }); - if (!confirmed) return; - if (newName !== selectedTable.name) { - selectedTable.name = newName; - await this._save(selectedTable); - } - } - async _setTableColor({ detail: color }) { + async setTableColor(color) { this.selectedTable.color = color; this.render(); await this._save(this.selectedTable); } - async _setFloorColor({ detail: color }) { + async setFloorColor(color) { this.state.floorBackground = color; this.activeFloor.background_color = color; try { @@ -169,7 +301,7 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { args: [[this.activeFloor.id], { background_color: color }], }); } catch (error) { - if (error.message.code < 0) { + if (isConnectionError(error)) { await this.showPopup('OfflineErrorPopup', { title: this.env._t('Offline'), body: this.env._t('Unable to change background color'), @@ -179,7 +311,7 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { } } } - async _deleteTable() { + async deleteTable() { if (!this.selectedTable) return; const { confirmed } = await this.showPopup('ConfirmPopup', { title: this.env._t('Are you sure ?'), @@ -206,7 +338,7 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { this.state.selectedTableId = null; } } catch (error) { - if (error.message.code < 0) { + if (isConnectionError(error)) { await this.showPopup('OfflineErrorPopup', { title: this.env._t('Offline'), body: this.env._t('Unable to delete table'), @@ -216,142 +348,6 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { } } } - _computePinchHypo(ev, callbackFunction) { - const touches = ev.touches; - // If two pointers are down, check for pinch gestures - if (touches.length === 2) { - const deltaX = touches[0].pageX - touches[1].pageX; - const deltaY = touches[0].pageY - touches[1].pageY; - callbackFunction(Math.hypot(deltaX, deltaY)) - } - } - _onPinchStart(ev) { - ev.currentTarget.style.setProperty('touch-action', 'none'); - this._computePinchHypo(ev, this.startPinch.bind(this)); - } - _onPinchEnd(ev) { - ev.currentTarget.style.removeProperty('touch-action'); - } - _onPinchMove(ev) { - debounce(this._computePinchHypo, 10, true)(ev, this.movePinch.bind(this)); - } - _onSelectTable(event) { - const table = event.detail; - if (this.state.isEditMode) { - this.state.selectedTableId = table.id; - } else { - this.env.pos.set_table(table).then(() => { - const order = this.env.pos.get_order(); - if (order) { - const { name: screenName } = order.get_screen_data(); - this.showScreen(screenName); - } - }); - } - } - _onDeselectTable() { - this.state.selectedTableId = null; - } - async _createTableHelper(copyTable) { - let newTable; - if (copyTable) { - newTable = Object.assign({}, copyTable); - newTable.position_h += 10; - newTable.position_v += 10; - } else { - newTable = { - position_v: 100, - position_h: 100, - width: 75, - height: 75, - shape: 'square', - seats: 1, - }; - } - newTable.name = this._getNewTableName(newTable.name); - delete newTable.id; - newTable.floor_id = [this.activeFloor.id, '']; - newTable.floor = this.activeFloor; - try { - await this._save(newTable); - this.activeTables.push(newTable); - return newTable; - } catch (error) { - if (error.message.code < 0) { - await this.showPopup('ErrorPopup', { - title: this.env._t('Offline'), - body: this.env._t('Unable to create table because you are offline.'), - }); - return; - } else { - throw error; - } - } - } - _getNewTableName(name) { - if (name) { - const num = Number((name.match(/\d+/g) || [])[0] || 0); - const str = name.replace(/\d+/g, ''); - const n = { num: num, str: str }; - n.num += 1; - this._lastName = n; - } else if (this._lastName) { - this._lastName.num += 1; - } else { - this._lastName = { num: 1, str: 'T' }; - } - return '' + this._lastName.str + this._lastName.num; - } - async _save(table) { - const tableCopy = { ...table }; - delete tableCopy.floor; - const tableId = await this.rpc({ - model: 'restaurant.table', - method: 'create_from_ui', - args: [tableCopy], - }); - table.id = tableId; - this.env.pos.tables_by_id[tableId] = table; - } - async _onSaveTable(event) { - const table = event.detail; - await this._save(table); - } - async _tableLongpolling() { - if (this.state.isEditMode) { - return; - } - try { - const result = await this.rpc({ - model: 'pos.config', - method: 'get_tables_order_count', - args: [this.env.pos.config.id], - }); - result.forEach((table) => { - const table_obj = this.env.pos.tables_by_id[table.id]; - const unsynced_orders = this.env.pos - .get_table_orders(table_obj) - .filter( - (o) => - o.server_id === undefined && - (o.orderlines.length !== 0 || o.paymentlines.length !== 0) && - // do not count the orders that are already finalized - !o.finalized - ).length; - table_obj.order_count = table.orders + unsynced_orders; - }); - this.render(); - } catch (error) { - if (error.message.code < 0) { - await this.showPopup('OfflineErrorPopup', { - title: this.env._t('Offline'), - body: this.env._t('Unable to get orders count'), - }); - } else { - throw error; - } - } - } } FloorScreen.template = 'FloorScreen'; FloorScreen.hideOrderSelector = true; diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js index 698e8cf5972..5061e1a1158 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js @@ -4,6 +4,12 @@ odoo.define('pos_restaurant.TableWidget', function(require) { const PosComponent = require('point_of_sale.PosComponent'); const Registries = require('point_of_sale.Registries'); + /** + * props: { + * onClick: callback, + * table: table object, + * } + */ class TableWidget extends PosComponent { setup() { owl.onMounted(this.onMounted); @@ -33,7 +39,7 @@ odoo.define('pos_restaurant.TableWidget', function(require) { Object.assign(tableCover.style, { height: `${Math.ceil(this.fill * 100)}%` }); } get fill() { - const customerCount = this.env.pos.get_customer_count(this.props.table); + const customerCount = this.env.pos.getCustomerCount(this.props.table.id); return Math.min(1, Math.max(0, customerCount / this.props.table.seats)); } get orderCount() { @@ -41,22 +47,23 @@ odoo.define('pos_restaurant.TableWidget', function(require) { return table.order_count !== undefined ? table.order_count : this.env.pos - .get_table_orders(table) + .getTableOrders(table.id) .filter(o => o.orderlines.length !== 0 || o.paymentlines.length !== 0).length; } get orderCountClass() { - const notifications = this._getNotifications(); - return { - 'order-count': true, - 'notify-printing': notifications.printing, - 'notify-skipped': notifications.skipped, - }; + const countClass = { 'order-count': true } + if (this.env.pos.config.iface_printers) { + const notifications = this._getNotifications(); + countClass['notify-printing'] = notifications.printing; + countClass['notify-skipped'] = notifications.skipped; + } + return countClass; } get customerCountDisplay() { - return `${this.env.pos.get_customer_count(this.props.table)}/${this.props.table.seats}`; + return `${this.env.pos.getCustomerCount(this.props.table.id)}/${this.props.table.seats}`; } _getNotifications() { - const orders = this.env.pos.get_table_orders(this.props.table); + const orders = this.env.pos.getTableOrders(this.props.table.id); let hasChangesCount = 0; let hasSkippedCount = 0; diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js index 3a09241151b..d014c110c7e 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js @@ -3,7 +3,6 @@ odoo.define('pos_restaurant.SubmitOrderButton', function(require) { const PosComponent = require('point_of_sale.PosComponent'); const ProductScreen = require('point_of_sale.ProductScreen'); - const { useListener } = require("@web/core/utils/hooks"); const Registries = require('point_of_sale.Registries'); /** @@ -15,19 +14,26 @@ odoo.define('pos_restaurant.SubmitOrderButton', function(require) { class SubmitOrderButton extends PosComponent { setup() { super.setup(); - useListener('click', this.onClick); + this.clicked = false; //mutex, we don't want to be able to spam the printers } - async onClick() { - const order = this.env.pos.get_order(); - if (order.hasChangesToPrint()) { - const isPrintSuccessful = await order.printChanges(); - if (isPrintSuccessful) { - order.saveChanges(); - } else { - await this.showPopup('ErrorPopup', { - title: this.env._t('Printing failed'), - body: this.env._t('Failed in printing the changes in the order'), - }); + async _onClick() { + if (!this.clicked) { + try { + this.clicked = true; + const order = this.env.pos.get_order(); + if (order.hasChangesToPrint()) { + const isPrintSuccessful = await order.printChanges(); + if (isPrintSuccessful) { + order.updatePrintedResume(); + } else { + this.showPopup('ErrorPopup', { + title: this.env._t('Printing failed'), + body: this.env._t('Failed in printing the changes in the order'), + }); + } + } + } finally { + this.clicked = false; } } } @@ -36,10 +42,10 @@ odoo.define('pos_restaurant.SubmitOrderButton', function(require) { } get addedClasses() { if (!this.currentOrder) return {}; - const changes = this.currentOrder.hasChangesToPrint(); - const skipped = changes ? false : this.currentOrder.hasSkippedChanges(); + const hasChanges = this.currentOrder.hasChangesToPrint(); + const skipped = hasChanges ? false : this.currentOrder.hasSkippedChanges(); return { - highlight: changes, + highlight: hasChanges, altlight: skipped, }; } diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js index df36c3f5bd9..962db9d6545 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js @@ -15,7 +15,7 @@ odoo.define('pos_restaurant.TableGuestsButton', function(require) { return this.env.pos.get_order(); } get nGuests() { - return this.currentOrder ? this.currentOrder.get_customer_count() : 0; + return this.currentOrder ? this.currentOrder.getCustomerCount() : 0; } async onClick() { const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { @@ -26,7 +26,7 @@ odoo.define('pos_restaurant.TableGuestsButton', function(require) { }); if (confirmed) { - this.env.pos.get_order().set_customer_count(parseInt(inputNumber, 10) || 1); + this.env.pos.get_order().setCustomerCount(parseInt(inputNumber, 10) || 1); } } } diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js index 67211231dfb..d62a322ed3a 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js @@ -12,7 +12,7 @@ odoo.define('pos_restaurant.TransferOrderButton', function(require) { useListener('click', this.onClick); } async onClick() { - this.env.pos.transfer_order_to_different_table(); + this.env.pos.setCurrentOrderToTransfer(); this.showScreen('FloorScreen'); } } diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js index 2204405cd2d..9e33a060ada 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js @@ -26,7 +26,7 @@ odoo.define('pos_restaurant.Orderline', function(require) { */ selectLine() { const line = this.props.line; // the orderline - if (this.env.pos.get_order().selected_orderline !== line) { + if (this.env.pos.get_order().selected_orderline.id !== line.id) { this.mp_dbclk_time = new Date().getTime(); } else if (!this.mp_dbclk_time) { this.mp_dbclk_time = new Date().getTime(); diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 689c3bf40ca..c8b7799b732 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -6,20 +6,25 @@ odoo.define('pos_restaurant.ReceiptScreen', function(require) { const PosResReceiptScreen = ReceiptScreen => class extends ReceiptScreen { - /** - * @override - */ + //@override + _addNewOrder() { + if (!this.env.pos.config.iface_floorplan) { + super._addNewOrder(); + } + } + //@override get nextScreen() { - if ( - this.env.pos.config.module_pos_restaurant && - this.env.pos.config.iface_floorplan - ) { + if (this.env.pos.config.iface_floorplan) { const table = this.env.pos.table; return { name: 'FloorScreen', props: { floor: table ? table.floor : null } }; } else { return super.nextScreen; } } + onBackToFloorButtonClick() { + // If we're here and the order is paid, we can remove it from the orders + this.env.pos.removeOrder(this.currentOrder); + } }; Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js index df1a0456fa4..813fad1e248 100644 --- a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -63,15 +63,14 @@ odoo.define('pos_restaurant.SplitBillScreen', function(require) { // order and for the new one. This is not entirely correct // but avoids flooding the kitchen with unnecessary orders. // Not sure what to do in this case. - - if (this.newOrder.saveChanges) { - this.currentOrder.saveChanges(); - this.newOrder.saveChanges(); + if (this.env.pos.config.iface_printers) { + this.currentOrder.updatePrintedResume(); + this.newOrder.updatePrintedResume(); } - this.newOrder.set_customer_count(1); - const newCustomerCount = this.currentOrder.get_customer_count() - 1; - this.currentOrder.set_customer_count(newCustomerCount || 1); + this.newOrder.setCustomerCount(1); + const newCustomerCount = this.currentOrder.getCustomerCount() - 1; + this.currentOrder.setCustomerCount(newCustomerCount || 1); this.currentOrder.set_screen_data({ name: 'ProductScreen' }); this.env.pos.orders.add(this.newOrder); diff --git a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js index 7c9a1a28cee..95511757101 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js @@ -31,28 +31,24 @@ odoo.define('pos_restaurant.TicketScreen', function (require) { }); } getTable(order) { - return `${order.table.floor.name} (${order.table.name})`; + return `${order.getTable().floor.name} (${order.getTable().name})`; } get _searchFields() { if (!this.env.pos.config.iface_floorplan) { return super._searchFields; } return Object.assign({}, super._searchFields, { - Table: (order) => `${order.table.floor.name} (${order.table.name})`, + Table: (order) => `${order.getTable().floor.name} (${order.getTable().name})`, }); } - _setOrder(order) { - if (!this.env.pos.config.iface_floorplan || order === this.env.pos.get_order()) { + async _setOrder(order) { + if (!this.env.pos.config.iface_floorplan || this.env.pos.table) { super._setOrder(order); - } else if (order !== this.env.pos.get_order()) { - // Only call set_table if the order is not the same as the current order. - // This is to prevent syncing to the server because syncing is only intended - // when going back to the floorscreen or opening a table. - this.env.pos.set_table(order.table, order).then(() => { - const order = this.env.pos.get_order(); - const { name: screenName } = order.get_screen_data(); - this.showScreen(screenName); - }); + } else { + // we came from the FloorScreen + const orderTable = order.getTable(); + await this.env.pos.setTable(orderTable, order.uid); + this.close(); } } shouldShowNewOrderButton() { @@ -60,10 +56,9 @@ odoo.define('pos_restaurant.TicketScreen', function (require) { } _getOrderList() { if (this.env.pos.table) { - return super._getOrderList(); - } else { - return this.env.pos.orders; + return this.env.pos.getTableOrders(this.env.pos.table.id); } + return super._getOrderList(); } async settleTips() { // set tip in each order @@ -78,11 +73,23 @@ odoo.define('pos_restaurant.TicketScreen', function (require) { } } } + //@override + _selectNextOrder(currentOrder) { + if (this.env.pos.config.iface_floorplan && this.env.pos.table) { + return super._selectNextOrder(...arguments); + } + } + //@override async _onDeleteOrder() { await super._onDeleteOrder(...arguments); - const orderlist = this.env.pos.table ? this.env.pos.get_order_list() : this.env.pos.orders; - if (orderlist.length == 0) { - this.showScreen('FloorScreen'); + if (this.env.pos.config.iface_floorplan) { + if (!this.env.pos.table) { + this.env.pos._removeOrdersFromServer(); + } + const orderList = this.env.pos.table ? this.env.pos.getTableOrders(this.env.pos.table.id) : this.env.pos.orders; + if (orderList.length == 0) { + this.showScreen('FloorScreen'); + } } } async setTip(order, serverId, amount) { @@ -107,7 +114,10 @@ odoo.define('pos_restaurant.TicketScreen', function (require) { args: [serverId, tip_line.export_as_JSON()], }); } - order.finalize(); + if (order === this.env.pos.get_order()) { + this._selectNextOrder(order); + } + this.env.pos.removeOrder(order); return true; } catch (_error) { const { confirmed } = await this.showPopup('ConfirmPopup', { diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js index 61ec91ef335..f7fdfff66ae 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -92,7 +92,10 @@ odoo.define('pos_restaurant.TipScreen', function (require) { this.goNextScreen(); } goNextScreen() { - this.env.pos.get_order().finalize(); + this.env.pos.removeOrder(this.currentOrder); + if (!this.env.pos.config.iface_floorplan) { + this.env.pos.add_new_order(); + } const { name, props } = this.nextScreen; this.showScreen(name, props); } diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js deleted file mode 100644 index cb3853c003f..00000000000 --- a/addons/pos_restaurant/static/src/js/floors.js +++ /dev/null @@ -1,393 +0,0 @@ -odoo.define('pos_restaurant.floors', function (require) { -"use strict"; - -var { PosGlobalState, Order, Payment } = require('point_of_sale.models'); -const { Gui } = require('point_of_sale.Gui'); -const Registries = require('point_of_sale.Registries'); - - -// New orders are now associated with the current table, if any. -const PosRestaurantOrder = (Order) => class PosRestaurantOrder extends Order { - constructor(obj, options) { - super(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan && !this.table && !options.json) { - this.table = this.pos.table; - } - this.customer_count = this.customer_count || 1; - } - } - export_as_JSON() { - var json = super.export_as_JSON(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - json.table = this.table ? this.table.name : undefined; - json.table_id = this.table ? this.table.id : false; - json.floor = this.table ? this.table.floor.name : false; - json.floor_id = this.table ? this.table.floor.id : false; - } - json.customer_count = this.customer_count; - } - return json; - } - init_from_JSON(json) { - super.init_from_JSON(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - this.table = this.pos.tables_by_id[json.table_id]; - this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined; - } - this.customer_count = json.customer_count; - } - } - export_for_printing() { - var json = super.export_for_printing(...arguments); - if (this.pos.config.module_pos_restaurant) { - if (this.pos.config.iface_floorplan) { - json.table = this.table ? this.table.name : undefined; - json.floor = this.table ? this.table.floor.name : undefined; - } - json.customer_count = this.get_customer_count(); - } - return json; - } - get_customer_count(){ - return this.customer_count; - } - set_customer_count(count) { - this.customer_count = Math.max(count,0); - } -} -Registries.Model.extend(Order, PosRestaurantOrder); - -// We need to change the way the regular UI sees the orders, it -// needs to only see the orders associated with the current table, -// and when an order is validated, it needs to go back to the floor map. -// -// And when we change the table, we must create an order for that table -// if there is none. -const PosRestaurantPosGlobalState = (PosGlobalState) => class PosRestaurantPosGlobalState extends PosGlobalState { - async _processData(loadedData) { - await super._processData(...arguments); - if (this.config.is_table_management) { - this.floors = loadedData['restaurant.floor']; - this.loadRestaurantFloor(); - } - } - - loadRestaurantFloor() { - // we do this in the front end due to the circular/recursive reference needed - // Ignore floorplan features if no floor specified. - this.config.iface_floorplan = !!(this.floors && this.floors.length > 0); - if (this.config.iface_floorplan) { - this.floors_by_id = {}; - this.tables_by_id = {}; - for (let floor of this.floors) { - this.floors_by_id[floor.id] = floor; - for (let table of floor.tables) { - this.tables_by_id[table.id] = table; - table.floor = floor; - } - } - } - } - - async after_load_server_data() { - var res = await super.after_load_server_data(...arguments); - if (this.config.iface_floorplan) { - this.table = null; - } - return res; - } - - transfer_order_to_different_table () { - this.order_to_transfer_to_different_table = this.get_order(); - - // go to 'floors' screen, this will set the order to null and - // eventually this will cause the gui to go to its - // default_screen, which is 'floors' - this.set_table(null); - } - - remove_from_server_and_set_sync_state(ids_to_remove){ - var self = this; - this.set_synch('connecting', ids_to_remove.length); - return self._remove_from_server(ids_to_remove) - .then(function(server_ids) { - self.set_synch('connected'); - }).catch(function(reason){ - self.set_synch('error'); - throw reason; - }); - } - - /** - * Request the orders of the table with given id. - * @param {number} table_id. - * @param {dict} options. - * @param {number} options.timeout optional timeout parameter for the rpc call. - * @return {Promise} - */ - _get_from_server (table_id, options) { - options = options || {}; - var timeout = typeof options.timeout === 'number' ? options.timeout : 7500; - return this.env.services.rpc({ - model: 'pos.order', - method: 'get_table_draft_orders', - args: [table_id], - kwargs: {context: this.env.session.user_context}, - }, { - timeout: timeout, - shadow: false, - }) - } - - transfer_order_to_table(table) { - this.order_to_transfer_to_different_table.table = table; - this.order_to_transfer_to_different_table.save_to_db(); - } - - push_order_for_transfer(order_ids, table_orders) { - order_ids.push(this.order_to_transfer_to_different_table.uid); - table_orders.push(this.order_to_transfer_to_different_table); - } - - clean_table_transfer(table) { - if (this.order_to_transfer_to_different_table && table) { - this.order_to_transfer_to_different_table = null; - return this.set_table(table); - } - } - - sync_from_server(table, table_orders, order_ids) { - var self = this; - var ids_to_remove = this.db.get_ids_to_remove_from_server(); - var orders_to_sync = this.db.get_unpaid_orders_to_sync(order_ids); - if (orders_to_sync.length) { - this.set_synch('connecting', orders_to_sync.length); - return this._save_to_server(orders_to_sync, {'draft': true}).then(function (server_ids) { - server_ids.forEach(server_id => self.update_table_order(server_id, table_orders)); - if (!ids_to_remove.length) { - self.set_synch('connected'); - } else { - self.remove_from_server_and_set_sync_state(ids_to_remove); - } - }).catch(function(reason){ - self.set_synch('error'); - }).finally(function(){ - return self.clean_table_transfer(table); - }); - } else { - if (ids_to_remove.length) { - self.remove_from_server_and_set_sync_state(ids_to_remove); - } - return self.clean_table_transfer(table); - } - } - - update_table_order(server_id, table_orders) { - const order = table_orders.find(o => o.name === server_id.pos_reference); - if (order) { - order.server_id = server_id.id; - order.save_to_db(); - } - return order; - } - - /** - * @param {models.Order} order order to set - */ - set_order_on_table(order) { - var orders = this.get_order_list(); - if (orders.length) { - order = order ? orders.find((o) => o.uid === order.uid) : null; - if (order) { - this.set_order(order); - } else { - // do not mindlessly set the first order in the list. - orders = orders.filter(order => !order.finalized); - if (orders.length) { - this.set_order(orders[0]); - } else { - this.add_new_order(); - } - } - } else { - this.add_new_order(); // or create a new order with the current table - } - } - - sync_to_server(table, order) { - var self = this; - var ids_to_remove = this.db.get_ids_to_remove_from_server(); - - this.set_synch('connecting', 1); - return this._get_from_server(table.id).then(function (server_orders) { - var orders = self.get_order_list(); - orders.forEach(function(order){ - // We don't remove the validated orders because we still want to see them - // in the ticket screen. Orders in 'ReceiptScreen' or 'TipScreen' are validated - // orders. - if (order.server_id && !order.finalized){ - self.orders.remove(order); - order.destroy(); - } - }); - server_orders.forEach(function(server_order){ - var new_order = self.createAutomaticallySavedOrder(server_order); - self.orders.add(new_order); - }) - if (!ids_to_remove.length) { - self.set_synch('connected'); - } else { - self.remove_from_server_and_set_sync_state(ids_to_remove); - } - }).catch(function(reason){ - self.set_synch('error'); - }).finally(function(){ - self.set_order_on_table(order); - }); - } - - get_order_with_uid() { - var order_ids = []; - this.get_order_list().forEach(function(o){ - order_ids.push(o.uid); - }); - - return order_ids; - } - - /** - * Changes the current table. - * - * Switch table and make sure all nececery syncing tasks are done. - * @param {object} table. - * @param {models.Order|undefined} order if provided, set to this order - */ - set_table(table, order) { - let res = Promise.resolve(); - if(!table){ - res = this.sync_from_server(table, this.get_order_list(), this.get_order_with_uid()); - this.set_order(null); - this.table = null; - } else if (this.order_to_transfer_to_different_table) { - var order_ids = this.get_order_with_uid(); - - this.transfer_order_to_table(table); - this.push_order_for_transfer(order_ids, this.get_order_list()); - - res = this.sync_from_server(table, this.get_order_list(), order_ids); - } else { - this.table = table; - res = this.sync_to_server(table, order); - } - return res; - } - - // if we have tables, we do not load a default order, as the default order will be - // set when the user selects a table. - set_start_order() { - if (!this.config.iface_floorplan) { - super.set_start_order(...arguments); - } - } - - // we need to prevent the creation of orders when there is no - // table selected. - add_new_order() { - if (this.config.iface_floorplan) { - if (this.table) { - return super.add_new_order(...arguments); - } else { - Gui.showPopup('ConfirmPopup', { - title: 'Unable to create order', - body: 'Orders cannot be created when there is no active table in restaurant mode', - }); - return undefined; - } - } else { - return super.add_new_order(...arguments); - } - } - - - // get the list of unpaid orders (associated to the current table) - get_order_list() { - var orders = super.get_order_list(...arguments); - if (!(this.config && this.config.iface_floorplan)) { - return orders; - } else if (!this.table) { - return []; - } else { - var t_orders = []; - for (var i = 0; i < orders.length; i++) { - if ( orders[i].table === this.table) { - t_orders.push(orders[i]); - } - } - return t_orders; - } - } - - // get the list of orders associated to a table. FIXME: should be O(1) - get_table_orders(table) { - var orders = this.get_order_list(); - var t_orders = []; - for (var i = 0; i < orders.length; i++) { - if (orders[i].table === table) { - t_orders.push(orders[i]); - } - } - return t_orders; - } - - // get customer count at table - get_customer_count(table) { - var orders = this.get_table_orders(table).filter(order => !order.finalized); - var count = 0; - for (var i = 0; i < orders.length; i++) { - count += orders[i].get_customer_count(); - } - return count; - } - - // When we validate an order we go back to the floor plan. - // When we cancel an order and there is multiple orders - // on the table, stay on the table. - on_removed_order(removed_order,index,reason){ - if (this.config.iface_floorplan) { - var order_list = this.get_order_list(); - if (reason === 'abandon') { - this.db.set_order_to_remove_from_server(removed_order); - } - if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ - this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true }); - } else if (order_list.length === 0) { - this.table ? this.set_order(null) : this.set_table(null); - } - } else { - super.on_removed_order(...arguments); - } - } -} -Registries.Model.extend(PosGlobalState, PosRestaurantPosGlobalState); - - -const PosRestaurantPayment = (Payment) => class PosRestaurantPayment extends Payment { - /** - * Override this method to be able to show the 'Adjust Authorisation' button - * on a validated payment_line and to show the tip screen which allow - * tipping even after payment. By default, this returns true for all - * non-cash payment. - */ - canBeAdjusted() { - if (this.payment_method.payment_terminal) { - return this.payment_method.payment_terminal.canBeAdjusted(this.cid); - } - return !this.payment_method.is_cash_count; - } -} -Registries.Model.extend(Payment, PosRestaurantPayment); - -}); diff --git a/addons/pos_restaurant/static/src/js/models.js b/addons/pos_restaurant/static/src/js/models.js new file mode 100644 index 00000000000..d4692c6db16 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/models.js @@ -0,0 +1,618 @@ +odoo.define('pos_restaurant.models', function (require) { +"use strict"; + +const { PosGlobalState, Order, Orderline, Payment } = require('point_of_sale.models'); +const Registries = require('point_of_sale.Registries'); +const { uuidv4 } = require('point_of_sale.utils'); +const core = require('web.core'); +const Printer = require('point_of_sale.Printer').Printer; +const { batched } = require('point_of_sale.utils') +const QWeb = core.qweb; + +const TIMEOUT = 7500; + +const PosRestaurantPosGlobalState = (PosGlobalState) => class PosRestaurantPosGlobalState extends PosGlobalState { + constructor(obj) { + super(obj); + this.orderToTransfer = null; // table transfer feature + this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing + } + //@override + async _processData(loadedData) { + await super._processData(...arguments); + if (this.config.is_table_management) { + this.floors = loadedData['restaurant.floor']; + this.loadRestaurantFloor(); + } + if (this.config.module_pos_restaurant) { + this._loadRestaurantPrinter(loadedData['restaurant.printer']); + } + } + //@override + _onReactiveOrderUpdated(order) { + super._onReactiveOrderUpdated(...arguments) + if (this.config.iface_floorplan) { + this.ordersToUpdateSet.add(order); + } + } + //@override + removeOrder(order, removeFromServer=true) { + super.removeOrder(...arguments); + if (this.config.iface_floorplan && removeFromServer) { + if (this.ordersToUpdateSet.has(order)) { + this.ordersToUpdateSet.delete(order) + } + if (order.server_id && !order.finalized) { + this.db.set_order_to_remove_from_server(order); + } + } + } + //@override + async after_load_server_data() { + var res = await super.after_load_server_data(...arguments); + if (this.config.iface_floorplan) { + this.table = null; + } + return res; + } + //@override + // if we have tables, we do not load a default order, as the default order will be + // set when the user selects a table. + set_start_order() { + if (!this.config.iface_floorplan) { + super.set_start_order(...arguments); + } + } + //@override + add_new_order() { + const order = super.add_new_order(); + this.ordersToUpdateSet.add(order); + return order; + } + //@override + createReactiveOrder(json) { + let reactiveOrder = super.createReactiveOrder(...arguments); + if (this.config.iface_printers) { + const updateOrderChanges = () => { + if (reactiveOrder.get_screen_data().name === 'ProductScreen') { + reactiveOrder.updateChangesToPrint(); + } + } + reactiveOrder = owl.reactive(reactiveOrder, batched(updateOrderChanges)); + reactiveOrder.updateChangesToPrint(); + } + return reactiveOrder; + } + _loadRestaurantPrinter(printers) { + this.unwatched.printers = []; + // list of product categories that belong to one or more order printer + this.printers_category_ids_set = new Set(); + for (let printerConfig of printers) { + let printer = this.create_printer(printerConfig); + printer.config = printerConfig; + this.unwatched.printers.push(printer); + for (let id of printer.config.product_categories_ids) { + this.printers_category_ids_set.add(id); + } + } + this.config.iface_printers = !!this.unwatched.printers.length; + } + async _getTableOrdersFromServer(tableId) { + this.set_synch('connecting', 1); + try { + const orders = await this.env.services.rpc({ + model: 'pos.order', + method: 'get_table_draft_orders', + args: [tableId], + }, { + timeout: TIMEOUT, + shadow: true, + }); + this.set_synch('connected'); + return orders; + } catch (error) { + this.set_synch('error'); + throw error; + } + } + /** + * Sync orders that got updated to the back end + * @param tableId ID of the table we want to sync + */ + async _syncTableOrdersToServer() { + await this._pushOrdersToServer(); + await this._removeOrdersFromServer(); + // This need to be called here otherwise _onReactiveOrderUpdated() will be called after the set is being cleared + this.ordersToUpdateSet.clear(); + } + /** + * Send the orders to be saved to the back end + * @throw error + */ + async _pushOrdersToServer() { + const ordersUidsToSync = [...this.ordersToUpdateSet].map(order => order.uid); + const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync); + const ordersResponse = await this._save_to_server(ordersToSync, {'draft': true}); + const tableOrders = [...this.ordersToUpdateSet].map(order => order); + ordersResponse.forEach(orderResponseData => this._updateTableOrder(orderResponseData, tableOrders)); + } + // created this hook for modularity + _updateTableOrder(ordersResponseData, tableOrders) { + const order = tableOrders.find(order => order.name === ordersResponseData.pos_reference); + order.server_id = ordersResponseData.id; + return order; + } + /** + * Remove the deleted orders from the backend. + * @throw error + */ + async _removeOrdersFromServer() { + const removedOrdersIds = this.db.get_ids_to_remove_from_server(); + if (removedOrdersIds.length === 0) { + return; + } + + const timeout = TIMEOUT * removedOrdersIds.length; + this.set_synch('connecting', removedOrdersIds.length); + try { + const removeOrdersResponseData = await this.env.services.rpc({ + model: 'pos.order', + method: 'remove_from_ui', + args: [removedOrdersIds], + }, { + timeout: timeout, + shadow: true, + }); + this.set_synch('connected'); + this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData); + } catch (reason) { + let 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; + } + } + // important to throw error here and let the rendering component handle the error + console.warn('Failed to remove orders:', removedOrdersIds); + throw error; + } + } + // to override + _postRemoveFromServer(serverIds, data) { + this.db.set_ids_removed_from_server(serverIds); + } + /** + * Replace all the orders of a table by orders fetched from the backend + * @param tableId ID of the table + * @throws error + */ + async _syncTableOrdersFromServer(tableId) { + await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time + const ordersJsons = await this._getTableOrdersFromServer(tableId); + const tableOrders = this.getTableOrders(tableId); + + tableOrders.forEach(order => { + // We don't remove the validated orders because we still want to see them in the ticket screen. + // Orders in 'ReceiptScreen' or 'TipScreen' are validated orders. + if (order.server_id && !order.finalized){ + this.removeOrder(order, false); + } + }); + ordersJsons.forEach(json => { + // Because of the offline feature, some draft orders fetched from the backend will appear + // to belong in different table, but in fact they are already moved. + const potentiallyTransferredOrder = [...this.ordersToUpdateSet].find(order => order.uid === json.uid) + if (!potentiallyTransferredOrder || potentiallyTransferredOrder.tableId === tableId) { + const order = this.createReactiveOrder(json); + this.orders.add(order); + } + }); + } + loadRestaurantFloor() { + // we do this in the front end due to the circular/recursive reference needed + // Ignore floorplan features if no floor specified. + this.config.iface_floorplan = !!(this.floors && this.floors.length > 0); + if (this.config.iface_floorplan) { + this.floors_by_id = {}; + this.tables_by_id = {}; + for (let floor of this.floors) { + this.floors_by_id[floor.id] = floor; + for (let table of floor.tables) { + this.tables_by_id[table.id] = table; + table.floor = floor; + } + } + } + } + async setTable(table, orderUid=null) { + this.table = table; + try { + await this._syncTableOrdersFromServer(table.id); + } catch (error) { + throw error; + } finally { + const currentOrder = this.getTableOrders(table.id).find(order => orderUid ? order.uid === orderUid : !order.finalized); + if (currentOrder) { + this.set_order(currentOrder); + } else { + this.add_new_order(); + } + } + } + getTableOrders(tableId) { + return this.get_order_list().filter(order => order.tableId === tableId); + } + unsetTable() { + this._syncTableOrdersToServer(); + this.table = null; + this.set_order(null); + } + setCurrentOrderToTransfer() { + this.orderToTransfer = this.selectedOrder; + } + async transferTable(table) { + this.table = table; + try { + await this._syncTableOrdersFromServer(table.id); + } catch (error) { + throw error; + } finally { + this.orderToTransfer.tableId = table.id; + this.set_order(this.orderToTransfer); + this.orderToTransfer = null; + } + } + getCustomerCount(tableId) { + const tableOrders = this.getTableOrders(tableId).filter(order => !order.finalized); + return tableOrders.reduce((count, order) => count + order.getCustomerCount(), 0); + } + create_printer(config) { + var url = config.proxy_ip || ''; + if(url.indexOf('//') < 0) { + url = window.location.protocol + '//' + url; + } + if(url.indexOf(':', url.indexOf('//') + 2) < 0 && window.location.protocol !== 'https:') { + url = url + ':8069'; + } + return new Printer(url, this); + } +} +Registries.Model.extend(PosGlobalState, PosRestaurantPosGlobalState); + +// New orders are now associated with the current table, if any. +const PosRestaurantOrder = (Order) => class PosRestaurantOrder extends Order { + constructor(obj, options) { + super(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan && !this.tableId && !options.json) { + this.tableId = this.pos.table.id; + } + this.customerCount = this.customerCount || 1; + } + if (this.pos.config.iface_printers) { + // printedResume will store the previous state of the orderlines (when there were no skip), it will + // store all the orderlines even if the product are not printable. This way, when we add a new category in + // the printers, the already added products of the newly added category are not printed. + this.printedResume = owl.markRaw(this.printedResume || {}); // we don't wanna track it and re-render + // no need to store this in the backend, we can just compute it once the order is fetched from clicking a table + if (!this.printingChanges) { + this._resetPrintingChanges(); + } + } + } + //@override + export_as_JSON() { + const json = super.export_as_JSON(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + json.table_id = this.tableId + } + json.customer_count = this.customerCount; + } + if (this.pos.config.iface_printers) { + json.multiprint_resume = JSON.stringify(this.printedResume); + // so that it can be stored in local storage and be used when loading the pos in the floorscreen + json.printing_changes = JSON.stringify(this.printingChanges); + } + return json; + } + //@override + init_from_JSON(json) { + super.init_from_JSON(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + this.tableId = json.table_id; + } + this.customerCount = json.customer_count; + } + if (this.pos.config.iface_printers) { + this.printedResume = json.multiprint_resume && JSON.parse(json.multiprint_resume); + this.printingChanges = json.printing_changes && JSON.parse(json.printing_changes); + } + } + //@override + export_for_printing() { + const json = super.export_for_printing(...arguments); + if (this.pos.config.module_pos_restaurant) { + if (this.pos.config.iface_floorplan) { + json.table = this.getTable().name; + } + json.customer_count = this.getCustomerCount(); + } + return json; + } + _resetPrintingChanges() { + this.printingChanges = { new:[], cancelled:[] }; + } + /** + * @returns {{ [productKey: string]: { product_id: number, name: string, note: string, quantity: number } }} + */ + _computePrintChanges() { + const changes = {}; + + // If there's a new orderline, we add it otherwise we add the change if there's one + this.orderlines.forEach(line => { + if (!line.mp_skip) { + const productId = line.get_product().id; + const note = line.get_note(); + const productKey = `${productId} - ${line.get_full_product_name()} - ${note}`; + const lineKey = `${line.uuid} - ${note}`; + const quantityDiff = line.get_quantity() - (this.printedResume[lineKey] ? this.printedResume[lineKey]['quantity'] : 0); + if (quantityDiff) { + if (!changes[productKey]) { + changes[productKey] = { + product_id: productId, + name: line.get_full_product_name(), + note: note, + quantity: quantityDiff, + } + } else { + changes[productKey]['quantity'] += quantityDiff; + } + line.set_dirty(true); + } else { + line.set_dirty(false); + } + } + }) + + // If there's an orderline that's not present anymore, we consider it as removed (even if note changed) + for (const [lineKey, lineResume] of Object.entries(this.printedResume)) { + if (!this._getPrintedLine(lineKey)) { + const productKey = `${lineResume['product_id']} - ${lineResume['name']} - ${lineResume['note']}`; + if (!changes[productKey]) { + changes[productKey] = { + product_id: lineResume['product_id'], + name: lineResume['name'], + note: lineResume['note'], + quantity: -lineResume['quantity'], + } + } else { + changes[productKey]['quantity'] -= lineResume['quantity']; + } + } + } + + return changes; + } + _getPrintingCategoriesChanges(categories) { + return { + new: this.printingChanges['new'].filter(change => this.pos.db.is_product_in_category(categories, change['product_id'])), + cancelled: this.printingChanges['cancelled'].filter(change => this.pos.db.is_product_in_category(categories, change['product_id'])), + } + } + _getPrintedLine(lineKey) { + return this.orderlines.find(line => line.uuid === this.printedResume[lineKey]['line_uuid'] && + line.note === this.printedResume[lineKey]['note']); + } + getCustomerCount(){ + return this.customerCount; + } + setCustomerCount(count) { + this.customerCount = Math.max(count,0); + } + getTable() { + if (this.pos.config.iface_floorplan) { + return this.pos.tables_by_id[this.tableId]; + } + return null; + } + updatePrintedResume(){ + // we first remove the removed orderlines + for (const lineKey in this.printedResume) { + if (!this._getPrintedLine(lineKey)) { + delete this.printedResume[lineKey]; + } + } + // we then update the added orderline or product quantity change + this.orderlines.forEach(line => { + if (!line.mp_skip) { + const note = line.get_note(); + const lineKey = `${line.uuid} - ${note}`; + if (this.printedResume[lineKey]) { + this.printedResume[lineKey]['quantity'] = line.get_quantity(); + } else { + this.printedResume[lineKey] = { + line_uuid: line.uuid, + product_id: line.get_product().id, + name: line.get_full_product_name(), + note: note, + quantity: line.get_quantity() + } + } + line.set_dirty(false); + } + }); + this._resetPrintingChanges(); + } + updateChangesToPrint() { + const changes = this._computePrintChanges(); // it's possible to have a change's quantity of 0 + // we thoroughly parse the changes we just computed to properly separate them into two + const toAdd = []; + const toRemove = []; + + for (const lineChange of Object.values(changes)) { + if (lineChange['quantity'] > 0) { + toAdd.push(lineChange); + } else if (lineChange['quantity'] < 0) { + lineChange['quantity'] *= -1; // we change the sign because that's how it is + toRemove.push(lineChange); + } + } + + this.printingChanges = { new: toAdd, cancelled: toRemove }; + } + hasChangesToPrint(){ + for (const printer of this.pos.unwatched.printers) { + const changes = this._getPrintingCategoriesChanges(printer.config.product_categories_ids); + if (changes['new'].length > 0 || changes['cancelled'].length > 0) { + return true; + } + } + return false; + } + hasSkippedChanges() { + var orderlines = this.get_orderlines(); + for (var i = 0; i < orderlines.length; i++) { + if (orderlines[i].mp_skip) { + return true; + } + } + return false; + } + async printChanges(){ + let isPrintSuccessful = true; + const d = new Date(); + let hours = '' + d.getHours(); + hours = hours.length < 2 ? ('0' + hours) : hours; + let minutes = '' + d.getMinutes(); + minutes = minutes.length < 2 ? ('0' + minutes) : minutes; + + + for (const printer of this.pos.unwatched.printers) { + const changes = this._getPrintingCategoriesChanges(printer.config.product_categories_ids); + if (changes['new'].length > 0 || changes['cancelled'].length > 0) { + const printingChanges = { + new: changes['new'], + cancelled: changes['cancelled'], + table_name: this.pos.config.iface_floorplan ? this.getTable().name : false, + floor_name: this.pos.config.iface_floorplan ? this.getTable().floor.name : false, + name: this.name || 'unknown order', + time: { + hours, + minutes, + }, + }; + const receipt = QWeb.render('OrderChangeReceipt', { changes: printingChanges }); + const result = await printer.print_receipt(receipt); + if (!result.successful) { + isPrintSuccessful = false; + } + } + } + return isPrintSuccessful; + } +} +Registries.Model.extend(Order, PosRestaurantOrder); + + +const PosRestaurantOrderline = (Orderline) => class PosRestaurantOrderline extends Orderline { + constructor() { + super(...arguments); + this.note = this.note || ""; + if (this.pos.config.iface_printers) { + this.uuid = this.uuid || uuidv4(); + // mp dirty is true if this orderline has changed since the last kitchen print + this.mp_dirty = false + if (!this.mp_skip) { + // mp_skip is true if the cashier want this orderline + // not to be sent to the kitchen + this.mp_skip = false; + } + } + } + //@override + can_be_merged_with(orderline) { + if (orderline.get_note() !== this.get_note()) { + return false; + } else { + return (!this.mp_skip) && (!orderline.mp_skip) && super.can_be_merged_with(...arguments); + } + } + //@override + clone(){ + const orderline = super.clone(...arguments); + orderline.note = this.note; + return orderline; + } + //@override + export_as_JSON(){ + const json = super.export_as_JSON(...arguments); + json.note = this.note; + if (this.pos.config.iface_printers) { + json.uuid = this.uuid; + json.mp_skip = this.mp_skip; + } + return json; + } + //@override + init_from_JSON(json){ + super.init_from_JSON(...arguments); + this.note = json.note; + if (this.pos.config.iface_printers) { + this.uuid = json.uuid; + this.mp_skip = json.mp_skip; + } + } + set_note(note){ + this.note = note; + } + get_note(){ + return this.note; + } + set_skip(skip) { + if (this.mp_dirty && skip && !this.mp_skip) { + this.mp_skip = true; + } + if (this.mp_skip && !skip) { + this.mp_skip = false; + } + } + set_dirty(dirty) { + if (this.printable()) { + this.mp_dirty = dirty; + } + } + get_line_diff_hash(){ + if (this.get_note()) { + return this.id + '|' + this.get_note(); + } else { + return '' + this.id; + } + } + // can this orderline be potentially printed ? + printable() { + return this.pos.db.is_product_in_category(this.pos.printers_category_ids_set, this.get_product().id); + } +} +Registries.Model.extend(Orderline, PosRestaurantOrderline); + +const PosRestaurantPayment = (Payment) => class PosRestaurantPayment extends Payment { + /** + * Override this method to be able to show the 'Adjust Authorisation' button + * on a validated payment_line and to show the tip screen which allow + * tipping even after payment. By default, this returns true for all + * non-cash payment. + */ + canBeAdjusted() { + if (this.payment_method.payment_terminal) { + return this.payment_method.payment_terminal.canBeAdjusted(this.cid); + } + return !this.payment_method.is_cash_count; + } +} +Registries.Model.extend(Payment, PosRestaurantPayment); + +}); diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js deleted file mode 100644 index dad0e95408b..00000000000 --- a/addons/pos_restaurant/static/src/js/multiprint.js +++ /dev/null @@ -1,298 +0,0 @@ -odoo.define('pos_restaurant.multiprint', function (require) { -"use strict"; - -var { PosGlobalState, Orderline, Order } = require('point_of_sale.models'); -var core = require('web.core'); -var Printer = require('point_of_sale.Printer').Printer; -const Registries = require('point_of_sale.Registries'); - -var QWeb = core.qweb; - - -const PosResMultiprintPosGlobalState = (PosGlobalState) => class PosResMultiprintPosGlobalState extends PosGlobalState { - async _processData(loadedData) { - await super._processData(...arguments); - if (this.config.module_pos_restaurant) { - this._loadRestaurantPrinter(loadedData['restaurant.printer']); - } - } - _loadRestaurantPrinter(printers) { - this.unwatched.printers = []; - // list of product categories that belong to one or more order printer - this.printers_category_ids_set = new Set(); - for (let printerConfig of printers) { - let printer = this.create_printer(printerConfig); - printer.config = printerConfig; - this.unwatched.printers.push(printer); - for (let id of printer.config.product_categories_ids) { - this.printers_category_ids_set.add(id); - } - } - this.config.iface_printers = !!this.unwatched.printers.length; - - } - create_printer(config) { - var url = config.proxy_ip || ''; - if(url.indexOf('//') < 0) { - url = window.location.protocol + '//' + url; - } - if(url.indexOf(':', url.indexOf('//') + 2) < 0 && window.location.protocol !== 'https:') { - url = url + ':8069'; - } - return new Printer(url, this); - } -} -Registries.Model.extend(PosGlobalState, PosResMultiprintPosGlobalState); - - -const PosResMultiprintOrderline = (Orderline) => class PosResMultiprintOrderline extends Orderline { - constructor() { - super(...arguments); - if (!this.pos.config.iface_printers) { - return; - } - if (typeof this.mp_dirty === 'undefined') { - // mp dirty is true if this orderline has changed - // since the last kitchen print - // it's left undefined if the orderline does not - // need to be printed to a printer. - - this.mp_dirty = this.printable() || undefined; - } - if (!this.mp_skip) { - // mp_skip is true if the cashier want this orderline - // not to be sent to the kitchen - this.mp_skip = false; - } - } - // can this orderline be potentially printed ? - printable() { - return this.pos.db.is_product_in_category(this.pos.printers_category_ids_set, this.get_product().id); - } - init_from_JSON(json) { - super.init_from_JSON(...arguments); - this.mp_dirty = json.mp_dirty; - this.mp_skip = json.mp_skip; - } - export_as_JSON() { - var json = super.export_as_JSON(...arguments); - json.mp_dirty = this.mp_dirty; - json.mp_skip = this.mp_skip; - return json; - } - set_quantity(quantity) { - if (this.pos.config.iface_printers && quantity !== this.quantity && this.printable()) { - this.mp_dirty = true; - } - return super.set_quantity(...arguments); - } - can_be_merged_with(orderline) { - return (!this.mp_skip) && - (!orderline.mp_skip) && - super.can_be_merged_with(...arguments); - } - set_skip(skip) { - if (this.mp_dirty && skip && !this.mp_skip) { - this.mp_skip = true; - } - if (this.mp_skip && !skip) { - this.mp_dirty = true; - this.mp_skip = false; - } - } - set_dirty(dirty) { - if (this.mp_dirty !== dirty) { - this.mp_dirty = dirty; - } - } - get_line_diff_hash(){ - if (this.get_note()) { - return this.id + '|' + this.get_note(); - } else { - return '' + this.id; - } - } -} -Registries.Model.extend(Orderline, PosResMultiprintOrderline); - - -const PosResMultiprintOrder = (Order) => class PosResMultiprintOrder extends Order { - build_line_resume(){ - var resume = {}; - this.orderlines.forEach(function(line){ - if (line.mp_skip) { - return; - } - var qty = Number(line.get_quantity()); - var note = line.get_note(); - var product_id = line.get_product().id; - var product_name = line.get_full_product_name(); - var p_key = product_id + " - " + product_name; - var product_resume = p_key in resume ? resume[p_key] : { - pid: product_id, - product_name_wrapped: line.generate_wrapped_product_name(), - qties: {}, - }; - if (note in product_resume['qties']) product_resume['qties'][note] += qty; - else product_resume['qties'][note] = qty; - resume[p_key] = product_resume; - }); - return resume; - } - saveChanges(){ - this.saved_resume = this.build_line_resume(); - this.orderlines.forEach(function(line){ - line.set_dirty(false); - }); - } - computeChanges(categories){ - var current_res = this.build_line_resume(); - var old_res = this.saved_resume || {}; - var json = this.export_as_JSON(); - var add = []; - var rem = []; - var p_key, note; - - for (p_key in current_res) { - for (note in current_res[p_key]['qties']) { - var curr = current_res[p_key]; - var old = old_res[p_key] || {}; - var pid = curr.pid; - var found = p_key in old_res && note in old_res[p_key]['qties']; - - if (!found) { - add.push({ - 'id': pid, - 'name': this.pos.db.get_product_by_id(pid).display_name, - 'name_wrapped': curr.product_name_wrapped, - 'note': note, - 'qty': curr['qties'][note], - }); - } else if (old['qties'][note] < curr['qties'][note]) { - add.push({ - 'id': pid, - 'name': this.pos.db.get_product_by_id(pid).display_name, - 'name_wrapped': curr.product_name_wrapped, - 'note': note, - 'qty': curr['qties'][note] - old['qties'][note], - }); - } else if (old['qties'][note] > curr['qties'][note]) { - rem.push({ - 'id': pid, - 'name': this.pos.db.get_product_by_id(pid).display_name, - 'name_wrapped': curr.product_name_wrapped, - 'note': note, - 'qty': old['qties'][note] - curr['qties'][note], - }); - } - } - } - - for (p_key in old_res) { - for (note in old_res[p_key]['qties']) { - var found = p_key in current_res && note in current_res[p_key]['qties']; - if (!found) { - var old = old_res[p_key]; - var pid = old.pid; - rem.push({ - 'id': pid, - 'name': this.pos.db.get_product_by_id(pid).display_name, - 'name_wrapped': old.product_name_wrapped, - 'note': note, - 'qty': old['qties'][note], - }); - } - } - } - - if(categories && categories.length > 0){ - // filter the added and removed orders to only contains - // products that belong to one of the categories supplied as a parameter - - var self = this; - - var _add = []; - var _rem = []; - - for(var i = 0; i < add.length; i++){ - if(self.pos.db.is_product_in_category(categories,add[i].id)){ - _add.push(add[i]); - } - } - add = _add; - - for(var i = 0; i < rem.length; i++){ - if(self.pos.db.is_product_in_category(categories,rem[i].id)){ - _rem.push(rem[i]); - } - } - rem = _rem; - } - - var d = new Date(); - var hours = '' + d.getHours(); - hours = hours.length < 2 ? ('0' + hours) : hours; - var minutes = '' + d.getMinutes(); - minutes = minutes.length < 2 ? ('0' + minutes) : minutes; - - return { - 'new': add, - 'cancelled': rem, - 'table': json.table || false, - 'floor': json.floor || false, - 'name': json.name || 'unknown order', - 'time': { - 'hours': hours, - 'minutes': minutes, - }, - }; - - } - async printChanges(){ - var printers = this.pos.unwatched.printers; - let isPrintSuccessful = true; - for(var i = 0; i < printers.length; i++){ - var changes = this.computeChanges(printers[i].config.product_categories_ids); - if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ - var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this}); - const result = await printers[i].print_receipt(receipt); - if (!result.successful) { - isPrintSuccessful = false; - } - } - } - return isPrintSuccessful; - } - hasChangesToPrint(){ - var printers = this.pos.unwatched.printers; - for(var i = 0; i < printers.length; i++){ - var changes = this.computeChanges(printers[i].config.product_categories_ids); - if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ - return true; - } - } - return false; - } - hasSkippedChanges() { - var orderlines = this.get_orderlines(); - for (var i = 0; i < orderlines.length; i++) { - if (orderlines[i].mp_skip) { - return true; - } - } - return false; - } - export_as_JSON(){ - var json = super.export_as_JSON(...arguments); - json.multiprint_resume = JSON.stringify(this.saved_resume); - return json; - } - init_from_JSON(json){ - super.init_from_JSON(...arguments); - this.saved_resume = json.multiprint_resume && JSON.parse(json.multiprint_resume); - } -} -Registries.Model.extend(Order, PosResMultiprintOrder); - - -}); diff --git a/addons/pos_restaurant/static/src/js/notes.js b/addons/pos_restaurant/static/src/js/notes.js deleted file mode 100644 index 33466e4de1e..00000000000 --- a/addons/pos_restaurant/static/src/js/notes.js +++ /dev/null @@ -1,43 +0,0 @@ -odoo.define('pos_restaurant.notes', function (require) { -"use strict"; - -var { Orderline } = require('point_of_sale.models'); -const Registries = require('point_of_sale.Registries'); - - -const PosResNotesOrderline = (Orderline) => class PosResNotesOrderline extends Orderline { - constructor() { - super(...arguments); - this.note = this.note || ""; - } - set_note(note){ - this.note = note; - } - get_note(note){ - return this.note; - } - can_be_merged_with(orderline) { - if (orderline.get_note() !== this.get_note()) { - return false; - } else { - return super.can_be_merged_with(...arguments); - } - } - clone(){ - var orderline = super.clone(...arguments); - orderline.note = this.note; - return orderline; - } - export_as_JSON(){ - var json = super.export_as_JSON(...arguments); - json.note = this.note; - return json; - } - init_from_JSON(json){ - super.init_from_JSON(...arguments); - this.note = json.note; - } -} -Registries.Model.extend(Orderline, PosResNotesOrderline); - -}); diff --git a/addons/pos_restaurant/static/src/xml/Chrome.xml b/addons/pos_restaurant/static/src/xml/Chrome.xml index 9b25e8aa21a..1a30bc58374 100644 --- a/addons/pos_restaurant/static/src/xml/Chrome.xml +++ b/addons/pos_restaurant/static/src/xml/Chrome.xml @@ -3,7 +3,7 @@ - +
diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml index 2861ec1bc64..013d7c38c4b 100644 --- a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml +++ b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml @@ -10,19 +10,10 @@ > - - - + - - - - - ( - - ) - + () diff --git a/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml index fd6ecc2825e..14743a187b1 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml @@ -5,11 +5,6 @@
- - - - Back -

Bill Printing

diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml index 9a5152ed4ce..76aff3d9f5d 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml @@ -3,19 +3,19 @@
- + - + - + - + - + @@ -30,31 +30,31 @@ - - - - - - - - - + + + + + + + + +
- - - - - - - - - + + + + + + + + +
- +
diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml index e185748d869..2b67a5f7b3c 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml @@ -15,7 +15,7 @@
- - + +
- +
diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml index 46aaeb724a3..12838995a82 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml @@ -2,7 +2,7 @@ -
+
diff --git a/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml index 7a460e5ed9b..cbdcc83d139 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -2,6 +2,12 @@ + + + + + + env.pos.config.set_tip_after_payment and !currentOrder.is_paid() diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml index e0229af81fc..2f1f4a18b53 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml @@ -2,7 +2,7 @@ - + Order diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml new file mode 100644 index 00000000000..aad8e48b26a --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml new file mode 100644 index 00000000000..f8bf56000b4 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml index 688bcbcd58f..212e2a2d7ac 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml @@ -55,6 +55,9 @@
+ + +
diff --git a/addons/pos_restaurant/static/src/xml/multiprint.xml b/addons/pos_restaurant/static/src/xml/multiprint.xml index e162e03d641..233cda8bcae 100644 --- a/addons/pos_restaurant/static/src/xml/multiprint.xml +++ b/addons/pos_restaurant/static/src/xml/multiprint.xml @@ -1,21 +1,13 @@ - - -
- -
-
-
-
- +
- / + /

@@ -30,10 +22,9 @@
- - + +
-
NOTE @@ -56,10 +47,9 @@
- - + +
-
NOTE diff --git a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js index 7ae6f88ff74..1611d0bbd29 100644 --- a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js +++ b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js @@ -43,7 +43,7 @@ odoo.define('pos_restaurant.tour.ControlButtons', function (require) { // Test PrintBillButton ProductScreen.do.clickPrintBillButton(); BillScreen.check.isShown(); - BillScreen.do.clickBack(); + BillScreen.do.clickOk(); // Test GuestButton ProductScreen.do.clickGuestButton(); diff --git a/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js index e52acfa5591..2257ebda1c0 100644 --- a/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js @@ -104,6 +104,7 @@ odoo.define('pos_restaurant.tour.TipScreen', function (require) { // settle tips here Chrome.do.clickTicketButton(); TicketScreen.do.selectFilter('Tipping'); + TicketScreen.check.tipContains('1.00'); TicketScreen.do.settleTips(); TicketScreen.do.selectFilter('All active orders'); TicketScreen.check.nthRowContains(2, 'Ongoing'); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js index 9f831367aac..e13c515190f 100644 --- a/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js +++ b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js @@ -4,11 +4,11 @@ odoo.define('pos_restaurant.tour.BillScreenTourMethods', function (require) { const { createTourMethods } = require('point_of_sale.tour.utils'); class Do { - clickBack() { + clickOk() { return [ { content: `go back`, - trigger: `.receipt-screen .back`, + trigger: `.receipt-screen .button.next`, }, ]; } diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js index d60e39864cc..50591284adf 100644 --- a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js +++ b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js @@ -20,7 +20,7 @@ odoo.define('pos_restaurant.tour.ChromeTourMethods', function (require) { return [ { content: `back to floor text is '${floor} ( ${table} )'`, - trigger: `.floor-button span:contains("${floor}") ~ .table-name:contains("${table}")`, + trigger: `.floor-button span:contains("${floor}") ~ .table-name:contains("(${table})")`, run: () => {}, }, ];