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; + } + } + }, +});