From 3db548c76f363c75124d69da53c90e97167a14fd Mon Sep 17 00:00:00 2001 From: "David Monnom (moda)" Date: Tue, 9 May 2023 06:08:46 +0000 Subject: [PATCH] [IMP] point_of_sale,pos_restaurant: add bus_service Before a longpolling every 5 seconds was set up to sync the number of orders per table in the pos_restaurant, which was not very userfriendly / efficient. Now, the longpolling has been replaced by a bus (websocket). As soon as an order is sent to the server by another PoS, the server dispatches the information to all other PoS using the corresponding floor plan. The bus_service was initialized in the pos_store of the point_of_sale, but is now used only in the pos_restaurant via override. This was done for obvious future use. closes odoo/odoo#121875 Signed-off-by: Heinz Robin (rhe) --- addons/point_of_sale/__manifest__.py | 5 ++ .../point_of_sale/static/src/app/pos_bus.js | 35 +++++++++++ .../static/tests/unit/pos_app_tests.js | 5 ++ addons/pos_restaurant/models/pos_config.py | 6 ++ addons/pos_restaurant/models/pos_order.py | 21 +++++++ .../src/app/floor_screen/floor_screen.js | 61 ++++++------------- .../static/src/app/floor_screen/table.js | 25 +++++--- .../static/src/app/navbar/BackButton.js | 21 ------- .../pos_restaurant/static/src/app/pos_bus.js | 60 ++++++++++++++++++ 9 files changed, 170 insertions(+), 69 deletions(-) create mode 100644 addons/point_of_sale/static/src/app/pos_bus.js create mode 100644 addons/pos_restaurant/static/src/app/pos_bus.js diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index cd0b09b7bfd..0295d6928f4 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -103,6 +103,11 @@ 'web/static/src/webclient/barcode/barcode_scanner.js', 'web/static/src/webclient/barcode/ZXingBarcodeDetector.js', 'web/static/src/webclient/barcode/crop_overlay.js', + # bus service + 'bus/static/src/services/bus_service.js', + 'bus/static/src/bus_parameters_service.js', + 'bus/static/src/multi_tab_service.js', + 'bus/static/src/workers/*', # report download utils 'web/static/src/webclient/actions/reports/utils.js', # libs diff --git a/addons/point_of_sale/static/src/app/pos_bus.js b/addons/point_of_sale/static/src/app/pos_bus.js new file mode 100644 index 00000000000..651fcf0f22e --- /dev/null +++ b/addons/point_of_sale/static/src/app/pos_bus.js @@ -0,0 +1,35 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; + +export class PosBus { + static serviceDependencies = ["pos", "orm", "bus_service"]; + + constructor(...args) { + this.setup(...args); + } + + setup(env, { pos, orm, bus_service }) { + this.pos = pos; + this.orm = orm; + + // initialize bus_service and listen on pos_config-`id` channel + bus_service.addChannel(`pos_config-${pos.globalState.config.id}`); + bus_service.addEventListener("notification", ({ detail }) => { + for (const message of detail) { + this.dispatch(message); + } + }); + } + + dispatch(message) {} +} + +export const posBusService = { + dependencies: PosBus.serviceDependencies, + async start(env, deps) { + return new PosBus(env, deps); + }, +}; + +registry.category("services").add("pos_bus", posBusService); diff --git a/addons/point_of_sale/static/tests/unit/pos_app_tests.js b/addons/point_of_sale/static/tests/unit/pos_app_tests.js index b8d51f384d8..4d071efdce7 100644 --- a/addons/point_of_sale/static/tests/unit/pos_app_tests.js +++ b/addons/point_of_sale/static/tests/unit/pos_app_tests.js @@ -21,6 +21,11 @@ QUnit.module("Chrome", { start() { return { bus: new EventBus() }; }, + }) + .add("bus_service", { + start() { + return { addChannel: () => {}, addEventListener: () => {} }; + }, }); for (const service of [ diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index c3bbbc17122..2e61fee03d1 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -28,6 +28,12 @@ class PosConfig(models.Model): skip_changes_map = defaultdict(lambda: 0) for line in linked_orderlines: + # For the moment, as this feature is not compatible with pos_self_order, + # we ignore last_order_preparation_change when it is set to false. + # In future, pos_self_order will send the various changes to the order. + if not line.order_id.last_order_preparation_change: + continue + last_order_preparation_change = json.loads(line.order_id.last_order_preparation_change) prep_change = {} for line_uuid in last_order_preparation_change: diff --git a/addons/pos_restaurant/models/pos_order.py b/addons/pos_restaurant/models/pos_order.py index 23e15484ec6..52c29c32bf2 100644 --- a/addons/pos_restaurant/models/pos_order.py +++ b/addons/pos_restaurant/models/pos_order.py @@ -54,6 +54,27 @@ class PosOrder(models.Model): """ return [('state', '=', 'draft'), ('table_id', 'in', table_ids)] + @api.model + def remove_from_ui(self, server_ids): + tables = self.env['pos.order'].search([('id', 'in', server_ids)]).table_id + order_ids = super().remove_from_ui(server_ids) + self.send_table_count_notification(tables) + return order_ids + + @api.model + def create_from_ui(self, orders, draft=False): + orders = super().create_from_ui(orders, draft) + order_ids = self.env['pos.order'].browse([order['id'] for order in orders]) + self.send_table_count_notification(order_ids.table_id) + return orders + + def send_table_count_notification(self, table_ids): + messages = [] + for config in self.env['pos.config'].search([('floor_ids', 'in', table_ids.floor_id.ids)]): + order_count = config.get_tables_order_count_and_printing_changes() + messages.append((f'pos_config-{config.id}', 'TABLE_ORDER_COUNT', order_count)) + self.env['bus.bus']._sendmany(messages) + @api.model def get_table_draft_orders(self, table_ids): """Generate an object of all draft orders for the given table. diff --git a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js index 025ca81deca..19b31a2ee5c 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js @@ -65,8 +65,27 @@ export class FloorScreen extends Component { this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; } async onWillStart() { + const table = this.pos.globalState.table; + if (table) { + const orders = this.pos.globalState.get_order_list(); + const tableOrders = orders.filter( + (order) => order.tableId === table.id && !order.finalized + ); + const qtyChange = tableOrders.reduce( + (acc, order) => { + const quantityChange = order.getOrderChanges(); + const quantitySkipped = order.getOrderChanges(true); + acc.changed += quantityChange.count; + acc.skipped += quantitySkipped.count; + return acc; + }, + { changed: 0, skipped: 0 } + ); + + table.changes_count = qtyChange.changed; + table.skip_changes = qtyChange.skipped; + } await this.pos.globalState.unsetTable(); - await this._tableLongpolling(); } onMounted() { this.pos.openCashControl(); @@ -78,9 +97,6 @@ export class FloorScreen extends Component { this.addFloorRef.el.style.display = "initial"; } this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; - // call _tableLongpolling once then set interval of 5sec. - - this.tableLongpolling = setInterval(this._tableLongpolling.bind(this), 5000); } onWillUnmount() { clearInterval(this.tableLongpolling); @@ -262,43 +278,6 @@ export class FloorScreen extends Component { async _renameFloor(floorId, newName) { await this.orm.call("restaurant.floor", "rename_floor", [floorId, newName]); } - async _tableLongpolling() { - const { globalState } = this.pos; - if (globalState.isEditMode) { - return; - } - const result = await this.orm.call( - "pos.config", - "get_tables_order_count_and_printing_changes", - [globalState.config.id] - ); - for (const table of result) { - if (!(table.id in globalState.tables_by_id)) { - //We enter this condition if, after loading our PoS, a table is deleted on a PoS and not on another. - //This will lead in receiving information on orders from a table that we do not have. - //If we do not have a table in the PoS but still receive information about it, we reload the tables - //from the server. - const result = await this.orm.call("pos.session", "get_pos_ui_restaurant_floor", [ - [odoo.pos_session_id], - ]); - if (globalState.config.module_pos_restaurant) { - globalState.floors = result; - globalState.loadRestaurantFloor(); - } - } - const table_obj = globalState.tables_by_id[table.id]; - const unsynced_orders = globalState.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 - ); - table_obj.order_count = table.orders + unsynced_orders.length; - table_obj.changes_count = table.changes; - table_obj.skip_changes = table.skip_changes; - } - } get activeFloor() { return this.state.selectedFloorId ? this.pos.globalState.floors_by_id[this.state.selectedFloorId] diff --git a/addons/pos_restaurant/static/src/app/floor_screen/table.js b/addons/pos_restaurant/static/src/app/floor_screen/table.js index 2b88eb53481..2d71cc21500 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/table.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/table.js @@ -35,10 +35,12 @@ export class Table extends Component { let boxShadow = "0px 3px rgba(0,0,0,0.07)"; if (!this.isOccupied()) { background = "transparent"; - const rgb = table.floor.background_color.substring(4, table.floor.background_color.length-1) - .replace(/ /g, '') - .split(','); - textColor = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2])/255 > 0.5 ? "black" : "white"; + const rgb = table.floor.background_color + .substring(4, table.floor.background_color.length - 1) + .replace(/ /g, "") + .split(","); + textColor = + (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255 > 0.5 ? "black" : "white"; border = "3px solid " + table.color; boxShadow = "none"; } @@ -90,13 +92,19 @@ export class Table extends Component { } get orderCount() { const table = this.props.table; - + const unsynced_orders = this.pos.globalState.getTableOrders(table.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 + ); if (table.changes_count > 0) { return table.changes_count; } else if (table.skip_changes > 0) { return table.skip_changes; } - return table.order_count; + return table.order_count + unsynced_orders.length; } get orderCountClass() { const countClass = { "order-count": true }; @@ -121,6 +129,9 @@ export class Table extends Component { return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {}; } isOccupied() { - return this.pos.globalState.getCustomerCount(this.props.table.id) > 0 || this.props.table.order_count > 0; + return ( + this.pos.globalState.getCustomerCount(this.props.table.id) > 0 || + this.props.table.order_count > 0 + ); } } diff --git a/addons/pos_restaurant/static/src/app/navbar/BackButton.js b/addons/pos_restaurant/static/src/app/navbar/BackButton.js index fdf0d556914..3f0013a3338 100644 --- a/addons/pos_restaurant/static/src/app/navbar/BackButton.js +++ b/addons/pos_restaurant/static/src/app/navbar/BackButton.js @@ -6,9 +6,6 @@ import { TipScreen } from "@pos_restaurant/js/Screens/TipScreen"; import { patch } from "@web/core/utils/patch"; patch(BackButton.prototype, "pos_restaurant.BackButton", { - get table() { - return this.pos.globalState.table; - }, get floor() { return this.table?.floor; }, @@ -27,24 +24,6 @@ patch(BackButton.prototype, "pos_restaurant.BackButton", { this.pos.globalState.mobile_pane == "right") || this.pos.mainScreen.component === TipScreen ) { - if (this.table) { - const orders = this.pos.globalState.get_order_list(); - const tableOrders = orders.filter((order) => order.tableId === this.table.id); - const qtyChange = tableOrders.reduce( - (acc, order) => { - const quantityChange = order.getOrderChanges(); - const quantitySkipped = order.getOrderChanges(true); - acc.changed += quantityChange.count; - acc.skipped += quantitySkipped.count; - return acc; - }, - { changed: 0, skipped: 0 } - ); - - this.table.changes_count = qtyChange.changed; - this.table.skip_changes = qtyChange.skipped; - } - this.pos.showScreen("FloorScreen", { floor: this.floor }); } else { this.pos.globalState.mobile_pane = "right"; diff --git a/addons/pos_restaurant/static/src/app/pos_bus.js b/addons/pos_restaurant/static/src/app/pos_bus.js new file mode 100644 index 00000000000..022d36415c8 --- /dev/null +++ b/addons/pos_restaurant/static/src/app/pos_bus.js @@ -0,0 +1,60 @@ +/** @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { PosBus } from "@point_of_sale/app/pos_bus"; + +patch(PosBus.prototype, "pos_restaurant.PosBus", { + // Override + setup() { + this._super(...arguments); + + if (this.pos.globalState.config.module_pos_restaurant) { + this.initTableOrderCount(); + } + }, + + async initTableOrderCount() { + const result = await this.orm.call( + "pos.config", + "get_tables_order_count_and_printing_changes", + [this.pos.globalState.config.id] + ); + + this.ws_syncTableCount(result); + }, + + // Override + dispatch(message) { + this._super(...arguments); + + if (message.type === "TABLE_ORDER_COUNT") { + this.ws_syncTableCount(message.payload); + } + }, + + // Sync the number of orders on each table with other PoS + // using the same floorplan. + async ws_syncTableCount(data) { + const missingTable = data.find((table) => !(table.id in this.pos.globalState.tables_by_id)); + + if (missingTable) { + const result = await this.orm.call("pos.session", "get_pos_ui_restaurant_floor", [ + [odoo.pos_session_id], + ]); + + if (this.pos.globalState.config.module_pos_restaurant) { + this.pos.globalState.floors = result; + this.pos.globalState.loadRestaurantFloor(); + } + } + + for (const table of data) { + const table_obj = this.pos.globalState.tables_by_id[table.id]; + if (table_obj) { + table_obj.order_count = table.orders; + table_obj.changes_count = table.changes; + table_obj.skip_changes = table.skip_changes; + } + } + }, +});