[REF] point_of_sale, pos_restaurant: cleaning of the methods and flow

Cleaning of the principal methods used in the pos_restaurant flow. This make the code a lot more readable and maintainable as we strive for the single responsibility principle.

A few notable changes:
 - [REF] `point_of_sale.assets` in `pos_restaurant` now uses glob syntax (`**`) for the js files, no need to add them in the manifest anymore.
 - [MOV] Moved some methods to `pos_restaurant` (because it was only used there) from the `point_of_sale`
 - [REF] Only the created, removed and updated orders of a table are being sent to the server once we reach the Floor Screen. Before, all the orders of a table were being sent to the server as the newest state. This led to some synchronization problem when two users wanted to modify each a different order of the same table, the user leaving the table last had **all** his orders states (even the ones he didn't modify) pushed to the server.
 - [IMP] After a table transfer while being in offline mode, the transferred order is saved locally (if not yet synchronized) and going back to the previous table after getting the connection back will no longer show the transferred order after the orders fetching from the server.
 - [FIX] When an order is selected (normal Point of Sale and in a table in `pos_restaurant`), the selected order is not changed in the Ticket Screen when deleting an order unless the deleted order is the current selected order.
 - [FIX] Same as above but while settling all the tips from the `Ticket Screen`
 - [FIX] Errors are now properly handled in the `Floor Screen` depending on the context.
 - [REF] Kitchen order printing flow has been revamped and improved. This also fixes a bug where we would print a cancelled receipt changes for a skipped order line when adding a product which was already printed before (add 1 product => print => add same product + skip => print would give a cancelled changes for this order line).

closes odoo/odoo#85956

Related: odoo/enterprise#25051
Related: odoo/upgrade#3375
Signed-off-by: Masereel Pierre <pim@odoo.com>
This commit is contained in:
Jacky (trj)
2022-05-16 13:56:38 +02:00
parent b246def6d5
commit 537d067569
47 changed files with 1091 additions and 1212 deletions
-16
View File
@@ -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."""
@@ -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) {
@@ -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);
}
@@ -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() {
+2 -8
View File
@@ -519,14 +519,8 @@ var PosDB = core.Class.extend({
* @return {array<object>} 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.
+29 -98
View File
@@ -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<number>} 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<array<number>>} 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();
+18 -5
View File
@@ -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 };
});
@@ -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: () => {},
},
];
}
@@ -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 {
@@ -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
+2 -27
View File
@@ -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': [
+19 -3
View File
@@ -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`."""
@@ -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;
}
@@ -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() {
@@ -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 });
}
}
@@ -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;
}
@@ -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';
@@ -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;
@@ -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;
@@ -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,
};
}
@@ -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);
}
}
}
@@ -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');
}
}
@@ -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();
@@ -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);
@@ -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);
@@ -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', {
@@ -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);
}
@@ -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);
});
@@ -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);
});
@@ -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);
});
@@ -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);
});
@@ -3,7 +3,7 @@
<t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('search-bar-portal')]" position="before">
<BackToFloorButton mobileSearchBarIsShown="state.mobileSearchBarIsShown"/>
<div class="back-to-floor-portal"/>
</xpath>
</t>
@@ -10,19 +10,10 @@
>
<i class="fa fa-angle-double-left" role="img" aria-label="Back to floor" title="Back to floor" />
<t t-if="env.isMobile">
<span class="table-name">
<span t-esc="table.name" />
</span>
<span class="table-name" t-esc="table.name"/>
</t>
<t t-else="">
<span> </span>
<span t-esc="floor.name" />
<span> </span>
<span class="table-name">
<span>( </span>
<span t-esc="table.name" />
<span> )</span>
</span>
<span t-esc="floor.name" /><span class="table-name">(<t t-esc="table.name" />)</span>
</t>
</span>
<span t-else=""></span>
@@ -5,11 +5,6 @@
<div class="receipt-screen screen">
<div class="screen-content">
<div class="top-content">
<span class="button back" t-on-click="confirm">
<i class="fa fa-angle-double-left"></i>
<span> </span>
<span>Back</span>
</span>
<div class="top-content-center">
<h1>Bill Printing</h1>
</div>
@@ -3,19 +3,19 @@
<t t-name="EditBar" owl="1">
<div class="edit-bar" t-attf-style="top:{{props.floorMapScrollTop}}px;">
<span class="edit-button" t-on-click.stop="() => this.trigger('create-table')">
<span class="edit-button" t-on-click.stop="props.createTable">
<i class="fa fa-plus" role="img" aria-label="Add" title="Add"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="() => this.trigger('duplicate-table')">
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.duplicateTable">
<i class="fa fa-files-o" role="img" aria-label="Duplicate" title="Duplicate"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="() => this.trigger('rename-table')">
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.renameTable">
<i class="fa fa-font" role="img" aria-label="Rename" title="Rename"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="() => this.trigger('change-seats-num')">
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeSeatsNum">
<i class="fa fa-user" role="img" aria-label="Seats" title="Seats"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="() => this.trigger('change-shape')">
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeShape">
<span t-if="!props.selectedTable or props.selectedTable.shape == 'square'" class="button-option square">
<i class="fa fa-square-o" role="img" aria-label="Square Shape" title="Square Shape"></i>
</span>
@@ -30,31 +30,31 @@
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
<i class="fa fa-times" />
</div>
<span class="color tl" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="() => this.trigger('set-table-color', '#EB6D6D')" />
<span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="() => this.trigger('set-table-color', '#35D374')" />
<span class="color tr" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => this.trigger('set-table-color', '#6C6DEC')" />
<span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => this.trigger('set-table-color', '#EBBF6D')" />
<span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => this.trigger('set-table-color', '#EBEC6D')" />
<span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => this.trigger('set-table-color', '#AC6DAD')" />
<span class="color bl" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => this.trigger('set-table-color', '#6C6D6D')" />
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => this.trigger('set-table-color', '#ACADAD')" />
<span class="color br" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => this.trigger('set-table-color', '#4ED2BE')" />
<span class="color tl" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setTableColor('#EB6D6D')" />
<span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setTableColor('#35D374')" />
<span class="color tr" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setTableColor('#6C6DEC')" />
<span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setTableColor('#EBBF6D')" />
<span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setTableColor('#EBEC6D')" />
<span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setTableColor('#AC6DAD')" />
<span class="color bl" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setTableColor('#6C6D6D')" />
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setTableColor('#ACADAD')" />
<span class="color br" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setTableColor('#4ED2BE')" />
</div>
<div t-if="state.isColorPicker and !props.selectedTable" class="color-picker bg-picker">
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
<i class="fa fa-times" />
</div>
<span class="color tl" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(244, 149, 149)')" />
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(130, 233, 171)')" />
<span class="color tr" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(136, 137, 242)')" />
<span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(255, 214, 136)')" />
<span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(254, 255, 154)')" />
<span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(209, 171, 210)')" />
<span class="color bl" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(75, 75, 75)')" />
<span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(210, 210, 210)')" />
<span class="color br" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => this.trigger('set-floor-color', 'rgb(127, 221, 236)')" />
<span class="color tl" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setFloorColor('rgb(244, 149, 149)')" />
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setFloorColor('rgb(130, 233, 171)')" />
<span class="color tr" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setFloorColor('rgb(136, 137, 242)')" />
<span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setFloorColor('rgb(255, 214, 136)')" />
<span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setFloorColor('rgb(254, 255, 154)')" />
<span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setFloorColor('rgb(209, 171, 210)')" />
<span class="color bl" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setFloorColor('rgb(75, 75, 75)')" />
<span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setFloorColor('rgb(210, 210, 210)')" />
<span class="color br" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setFloorColor('rgb(127, 221, 236)')" />
</div>
<span class="edit-button trash" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="() => this.trigger('delete-table')">
<span class="edit-button trash" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.deleteTable">
<i class="fa fa-trash" role="img" aria-label="Delete" title="Delete"></i>
</span>
</div>
@@ -15,7 +15,7 @@
</t>
<div
t-on-click="() => this.trigger('deselect-table')"
t-on-click="_onDeselectTable"
t-on-touchstart="_onPinchStart"
t-on-touchmove="_onPinchMove"
t-on-touchend="_onPinchEnd"
@@ -29,15 +29,19 @@
</div>
<div t-else="" class="tables">
<t t-foreach="activeTables" t-as="table" t-key="table.id">
<TableWidget t-if="table.id !== state.selectedTableId" table="table" />
<EditableTable t-else="" table="table" />
<TableWidget t-if="table.id !== state.selectedTableId" onClick.bind="onSelectTable" table="table" />
<EditableTable t-else="" table="table" onSaveTable.bind="onSaveTable" />
</t>
</div>
<span t-if="env.pos.user.role == 'manager'" class="edit-button editing" t-att-class="{ active: state.isEditMode }" t-on-click.stop="toggleEditMode"
t-attf-style="top:{{state.floorMapScrollTop}}px;">
<i class="fa fa-pencil" role="img" aria-label="Edit" title="Edit"></i>
</span>
<EditBar t-if="state.isEditMode" selectedTable="selectedTable" floorMapScrollTop="state.floorMapScrollTop"/>
<EditBar t-if="state.isEditMode" selectedTable="selectedTable" floorMapScrollTop="state.floorMapScrollTop"
createTable.bind="createTable" duplicateTable.bind="duplicateTable" renameTable.bind="renameTable"
changeSeatsNum.bind="changeSeatsNum" changeShape.bind="changeShape" setTableColor.bind="setTableColor"
setFloorColor.bind="setFloorColor" deleteTable.bind="deleteTable"
/>
</div>
</div>
</div>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="TableWidget" owl="1">
<div t-if="!props.isSelected" class="table" t-on-click.stop="() => this.trigger('select-table', props.table)">
<div class="table" t-on-click.stop="() => props.onClick(props.table)">
<span class="table-cover" t-att-class="{ full: fill >= 1 }"></span>
<span t-att-class="orderCountClass" t-att-hidden="orderCount === 0">
<t t-esc="orderCount" />
@@ -2,6 +2,12 @@
<templates id="template" xml:space="preserve">
<t t-name="PaymentScreen" t-inherit="point_of_sale.PaymentScreen" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('payment-screen')]" position="inside">
<t t-portal="'.pos .back-to-floor-portal'" position="before">
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
</t>
</xpath>
<xpath expr="//div[hasclass('button') and hasclass('next')]" position="attributes">
<attribute name="t-att-hidden">env.pos.config.set_tip_after_payment and !currentOrder.is_paid()</attribute>
</xpath>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="SubmitOrderButton" owl="1">
<span class="control-button" t-att-class="addedClasses">
<span class="control-button" t-att-class="addedClasses" t-on-click="_onClick">
<i class="fa fa-cutlery"></i>
<span> </span>
<span>Order</span>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('product-screen')]" position="inside">
<t t-portal="'.pos .back-to-floor-portal'" position="before">
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
</t>
</xpath>
</t>
</templates>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ReceiptScreen" t-inherit="point_of_sale.ReceiptScreen" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('receipt-screen')]" position="inside">
<t t-portal="'.pos .back-to-floor-portal'" position="before">
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"
onClick.bind="onBackToFloorButtonClick"
/>
</t>
</xpath>
</t>
</templates>
@@ -55,6 +55,9 @@
</div>
</div>
</div>
<t t-portal="'.pos .back-to-floor-portal'" position="before">
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
</t>
</div>
</t>
@@ -1,21 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="NameWrapped">
<t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line">
<div style="text-align: right">
<span t-esc="wrapped_line"/>
</div>
</t>
</t>
<t t-name="OrderChangeReceipt">
<div class="pos-receipt">
<div class="pos-receipt-order-data"><t t-esc="changes.name" /></div>
<t t-if="changes.floor || changes.table">
<t t-if="changes.floor_name || changes.table_name">
<br />
<div class="pos-receipt-title">
<t t-esc="changes.floor" /> / <t t-esc="changes.table"/>
<t t-esc="changes.floor_name" /> / <t t-esc="changes.table_name"/>
</div>
</t>
<br />
@@ -30,10 +22,9 @@
<br />
<t t-foreach="changes.cancelled" t-as="change">
<div class="multiprint-flex">
<t t-esc="change.qty"/>
<span t-esc="change.name_wrapped[0]"/>
<span class="product-quantity" t-esc="change.quantity"/>
<span class="product-name" t-esc="change.name"/>
</div>
<t t-call="NameWrapped"/>
<t t-if="change.note">
<div>
NOTE
@@ -56,10 +47,9 @@
<br />
<t t-foreach="changes.new" t-as="change">
<div class="multiprint-flex">
<t t-esc="change.qty"/>
<span t-esc="change.name_wrapped[0]"/>
<span class="product-quantity" t-esc="change.quantity"/>
<span class="product-name" t-esc="change.name"/>
</div>
<t t-call="NameWrapped"/>
<t t-if="change.note">
<div>
NOTE
@@ -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();
@@ -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');
@@ -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`,
},
];
}
@@ -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: () => {},
},
];