Skip to content

Commit 93f29e7

Browse files
committed
[FIX] point_of_sale: prevent duplicate preparation ticket
Before this change, when a device sent an order in preparation via the ticket printer, this could result in the same order being printed by multiple devices, as the order was not synchronized after it was sent. The error is a bit tricky, because if the user had installed a preparation screen, the order was sent to the preparation screen via syncAllOrders. In this case, the order was correctly synchronized and the other devices were informed of the changes. This commit adds two things. - We check the server before sending the order to preparation to make sure it has not already been sent. - Even when the user does not have a preparation display, the order will be synchronized after being sent to a printer. closes odoo#220716 X-original-commit: 9240148 Related: odoo/enterprise#91093 Signed-off-by: Adrien Guilliams (adgu) <adgu@odoo.com> Signed-off-by: David Monnom (moda) <moda@odoo.com>
1 parent 90292e6 commit 93f29e7

6 files changed

Lines changed: 94 additions & 5 deletions

File tree

addons/point_of_sale/models/pos_order.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,34 @@ def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
355355
('to_invoice', 'To Invoice'),
356356
], string='Invoice Status', compute='_compute_invoice_status')
357357

358+
def get_preparation_change(self):
359+
self.ensure_one()
360+
return {
361+
'last_order_preparation_change': self.last_order_preparation_change,
362+
}
363+
364+
def _ensure_to_keep_last_preparation_change(self, vals):
365+
for record in self:
366+
if record.last_order_preparation_change:
367+
change = json.loads(record.last_order_preparation_change)
368+
if not change.get('metadata'):
369+
return
370+
371+
local_change = json.loads(vals.get('last_order_preparation_change', '{}'))
372+
if not local_change.get('metadata'):
373+
vals['last_order_preparation_change'] = record.last_order_preparation_change
374+
return
375+
376+
server_date = fields.Datetime.from_string(change['metadata'].get('serverDate'))
377+
local_date = fields.Datetime.from_string(local_change['metadata'].get('serverDate'))
378+
379+
if server_date > local_date:
380+
_logger.warning("Preparation changes were outdated, probably linked to a synching issue.")
381+
vals['last_order_preparation_change'] = record.last_order_preparation_change
382+
else:
383+
local_change['metadata']['serverDate'] = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S')
384+
vals['last_order_preparation_change'] = json.dumps(local_change)
385+
358386
@api.depends('account_move')
359387
def _compute_invoice_status(self):
360388
for order in self:
@@ -1120,6 +1148,7 @@ def sync_from_ui(self, orders):
11201148

11211149
existing_order = self._get_open_order(order)
11221150
if existing_order and existing_order.state == 'draft':
1151+
existing_order._ensure_to_keep_last_preparation_change(order)
11231152
order_ids.append(self._process_order(order, existing_order))
11241153
_logger.info("PoS synchronisation #%d order %s updated pos.order #%d", sync_token, order_log_name, order_ids[-1])
11251154
elif not existing_order:
@@ -1128,6 +1157,7 @@ def sync_from_ui(self, orders):
11281157
else:
11291158
# In theory, this situation is unintended
11301159
# In practice it can happen when "Tip later" option is used
1160+
existing_order._ensure_to_keep_last_preparation_change(order)
11311161
order_ids.append(existing_order.id)
11321162
_logger.info("PoS synchronisation #%d order %s sync ignored for existing PoS order %s (state: %s)", sync_token, order_log_name, existing_order, existing_order.state)
11331163

addons/point_of_sale/static/src/app/models/pos_order.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class PosOrder extends Base {
3434
if (!vals.last_order_preparation_change) {
3535
this.last_order_preparation_change = {
3636
lines: {},
37+
metadata: {},
3738
general_customer_note: "",
3839
internal_note: "",
3940
sittingMode: 0,
@@ -340,6 +341,9 @@ export class PosOrder extends Base {
340341
this.last_order_preparation_change.general_customer_note = this.general_customer_note;
341342
this.last_order_preparation_change.internal_note = this.internal_note;
342343
this.last_order_preparation_change.sittingMode = this.preset_id?.id || 0;
344+
this.last_order_preparation_change.metadata = {
345+
serverDate: serializeDateTime(DateTime.now()),
346+
};
343347
}
344348

345349
isEmpty() {

addons/point_of_sale/static/src/app/screens/receipt_screen/receipt_screen.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class ReceiptScreen extends Component {
3535
this.currentOrder.uiState.locked = true;
3636

3737
if (!this.pos.config.module_pos_restaurant) {
38-
this.pos.sendOrderInPreparation(order, { orderDone: true });
38+
this.pos.checkPreparationStateAndSentOrderInPreparation(order, { orderDone: true });
3939
}
4040
});
4141
}

addons/point_of_sale/static/src/app/services/pos_store.js

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ export class PosStore extends WithLazyGetterTrap {
507507
const orderPresetDate = DateTime.fromISO(order.preset_time);
508508
const isSame = DateTime.now().hasSame(orderPresetDate, "day");
509509
if (!order.preset_time || isSame) {
510-
await this.sendOrderInPreparation(order, {
510+
await this.checkPreparationStateAndSentOrderInPreparation(order, {
511511
cancelled: true,
512512
orderDone: true,
513513
});
@@ -1660,8 +1660,41 @@ export class PosStore extends WithLazyGetterTrap {
16601660
changesToOrder(order, skipped = false, orderPreparationCategories, cancelled = false) {
16611661
return changesToOrder(order, skipped, orderPreparationCategories, cancelled);
16621662
}
1663+
async checkPreparationStateAndSentOrderInPreparation(order, cancelled = false) {
1664+
if (typeof order.id !== "number") {
1665+
return this.sendOrderInPreparation(order, cancelled);
1666+
}
1667+
1668+
const data = await this.data.call("pos.order", "get_preparation_change", [order.id]);
1669+
const rawchange = data.last_order_preparation_change || "{}";
1670+
const lastChanges = JSON.parse(rawchange);
1671+
const lastServerDate = DateTime.fromSQL(lastChanges.metadata?.serverDate).toUTC();
1672+
const lastLocalDate = DateTime.fromSQL(
1673+
order.last_order_preparation_change?.metadata?.serverDate
1674+
).toUTC();
1675+
1676+
if (lastServerDate.isValid && lastServerDate.ts != lastLocalDate.ts) {
1677+
this.dialog.add(AlertDialog, {
1678+
title: _t("Order Outdated"),
1679+
body: _t(
1680+
"The order has been modified on another device. If you have modified existing " +
1681+
"order lines, check that your changes have not been overwritten.\n\n" +
1682+
"The order will be sent to the server with the last changes made on this device."
1683+
),
1684+
});
1685+
1686+
// Update before syncing otherwise it will overwrite the last change
1687+
order.last_order_preparation_change = lastChanges;
1688+
await this.syncAllOrders({ orders: [order] });
1689+
return;
1690+
}
1691+
1692+
return this.sendOrderInPreparation(order, cancelled);
1693+
}
16631694
// Now the printer should work in PoS without restaurant
16641695
async sendOrderInPreparation(order, opts = {}) {
1696+
let isPrinted = false;
1697+
16651698
if (this.config.printerCategories.size && !opts.byPassPrint) {
16661699
try {
16671700
let reprint = false;
@@ -1688,19 +1721,25 @@ export class PosStore extends WithLazyGetterTrap {
16881721
if (reprint && opts.orderDone) {
16891722
return;
16901723
}
1691-
await this.printChanges(order, orderChange, reprint);
1724+
isPrinted = await this.printChanges(order, orderChange, reprint);
16921725
} catch (e) {
16931726
console.info("Failed in printing the changes in the order", e);
16941727
}
16951728
}
16961729
order.updateLastOrderChange();
1730+
// Ensure that other devices are aware of the changes
1731+
// Otherwise several devices can print the same changes
1732+
// We need to check if a preparation display is configured to avoid unnecessary sync
1733+
if (isPrinted && !this.config["<-pos_preparation_display.display.pos_config_ids"]?.length) {
1734+
await this.syncAllOrders({ orders: [order] });
1735+
}
16971736
}
16981737
async sendOrderInPreparationUpdateLastChange(o, cancelled = false) {
16991738
if (this.data.network.offline) {
17001739
this.data.network.warningTriggered = false;
17011740
throw new ConnectionLostError();
17021741
}
1703-
await this.sendOrderInPreparation(o, { cancelled });
1742+
await this.checkPreparationStateAndSentOrderInPreparation(o, { cancelled });
17041743
}
17051744

17061745
getStrNotes(note) {
@@ -1794,6 +1833,7 @@ export class PosStore extends WithLazyGetterTrap {
17941833
}
17951834

17961835
async printChanges(order, orderChange, reprint = false) {
1836+
let isPrinted = false;
17971837
const unsuccedPrints = [];
17981838

17991839
for (const printer of this.unwatched.printers) {
@@ -1812,6 +1852,8 @@ export class PosStore extends WithLazyGetterTrap {
18121852
const result = await this.printOrderChanges(data, printer);
18131853
if (!result.successful) {
18141854
unsuccedPrints.push(printer.config.name);
1855+
} else {
1856+
isPrinted = true;
18151857
}
18161858
}
18171859
}
@@ -1824,6 +1866,8 @@ export class PosStore extends WithLazyGetterTrap {
18241866
body: _t("Failed in printing %s changes of the order", failedReceipts),
18251867
});
18261868
}
1869+
1870+
return isPrinted;
18271871
}
18281872

18291873
async prepareReceiptGroupedData(data) {

addons/point_of_sale/static/tests/unit/data/pos_order.data.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { models } from "@web/../tests/web_test_helpers";
33
export class PosOrder extends models.ServerModel {
44
_name = "pos.order";
55

6+
get_preparation_change(id) {
7+
const read = this.read([id]);
8+
const changes = read[0]?.last_order_preparation_change || "{}";
9+
return {
10+
last_order_preparation_change: changes,
11+
};
12+
}
13+
614
_load_pos_data_fields() {
715
return [];
816
}

addons/pos_restaurant/static/src/app/services/pos_store.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -882,7 +882,10 @@ patch(PosStore.prototype, {
882882
const order = course.order_id;
883883
course.fired = true;
884884
order.deselectCourse();
885-
await this.sendOrderInPreparation(order, { firedCourseId: course.id, byPassPrint: true });
885+
await this.checkPreparationStateAndSentOrderInPreparation(order, {
886+
firedCourseId: course.id,
887+
byPassPrint: true,
888+
});
886889
await this.printCourseTicket(course);
887890
return true;
888891
},

0 commit comments

Comments
 (0)