[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) <rhe@odoo.com>
This commit is contained in:
David Monnom (moda)
2023-05-31 14:23:24 +02:00
parent 1c3ba9c3f6
commit 3db548c76f
9 changed files with 170 additions and 69 deletions
+5
View File
@@ -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
@@ -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);
@@ -21,6 +21,11 @@ QUnit.module("Chrome", {
start() {
return { bus: new EventBus() };
},
})
.add("bus_service", {
start() {
return { addChannel: () => {}, addEventListener: () => {} };
},
});
for (const service of [
@@ -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:
+21
View File
@@ -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.
@@ -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]
@@ -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
);
}
}
@@ -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";
@@ -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;
}
}
},
});