diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/static/src/js/Screens | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/static/src/js/Screens')
20 files changed, 1458 insertions, 0 deletions
diff --git a/addons/pos_restaurant/static/src/js/Screens/BillScreen.js b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js new file mode 100644 index 00000000..8ecd7805 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js @@ -0,0 +1,31 @@ +odoo.define('pos_restaurant.BillScreen', function (require) { + 'use strict'; + + const ReceiptScreen = require('point_of_sale.ReceiptScreen'); + const Registries = require('point_of_sale.Registries'); + + const BillScreen = (ReceiptScreen) => { + class BillScreen extends ReceiptScreen { + confirm() { + this.props.resolve({ confirmed: true, payload: null }); + this.trigger('close-temp-screen'); + } + whenClosing() { + this.confirm(); + } + /** + * @override + */ + async printReceipt() { + await super.printReceipt(); + this.currentOrder._printed = false; + } + } + BillScreen.template = 'BillScreen'; + return BillScreen; + }; + + Registries.Component.addByExtending(BillScreen, ReceiptScreen); + + return BillScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js new file mode 100644 index 00000000..43b16d93 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js @@ -0,0 +1,19 @@ +odoo.define('pos_restaurant.EditBar', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + const { useState } = owl.hooks; + + class EditBar extends PosComponent { + constructor() { + super(...arguments); + this.state = useState({ isColorPicker: false }) + } + } + EditBar.template = 'EditBar'; + + Registries.Component.add(EditBar); + + return EditBar; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js new file mode 100644 index 00000000..4aeb781d --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js @@ -0,0 +1,60 @@ +odoo.define('pos_restaurant.EditableTable', function(require) { + 'use strict'; + + const { onPatched, onMounted } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class EditableTable extends PosComponent { + constructor() { + super(...arguments); + useListener('resize-end', this._onResizeEnd); + useListener('drag-end', this._onDragEnd); + onPatched(this._setElementStyle.bind(this)); + onMounted(this._setElementStyle.bind(this)); + } + _setElementStyle() { + const table = this.props.table; + function unit(val) { + return `${val}px`; + } + const style = { + width: unit(table.width), + height: unit(table.height), + 'line-height': unit(table.height), + top: unit(table.position_v), + left: unit(table.position_h), + 'border-radius': table.shape === 'round' ? unit(1000) : '3px', + }; + if (table.color) { + style.background = table.color; + } + if (table.height >= 150 && table.width >= 150) { + style['font-size'] = '32px'; + } + Object.assign(this.el.style, style); + } + _onResizeEnd(event) { + const { size, loc } = event.detail; + const table = this.props.table; + table.width = size.width; + table.height = size.height; + table.position_v = loc.top; + table.position_h = loc.left; + this.trigger('save-table', 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); + } + } + EditableTable.template = 'EditableTable'; + + Registries.Component.add(EditableTable); + + return EditableTable; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js new file mode 100644 index 00000000..7c46f22b --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -0,0 +1,315 @@ +odoo.define('pos_restaurant.FloorScreen', function (require) { + 'use strict'; + + const { debounce } = owl.utils; + const PosComponent = require('point_of_sale.PosComponent'); + const { useState, useRef } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class FloorScreen extends PosComponent { + /** + * @param {Object} props + * @param {Object} props.floor + */ + constructor() { + super(...arguments); + 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, + selectedTableId: null, + isEditMode: false, + floorBackground: floor.background_color, + floorMapScrollTop: 0, + }); + this.floorMapRef = useRef('floor-map-ref'); + } + patched() { + this.floorMapRef.el.style.background = this.state.floorBackground; + this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; + } + mounted() { + if (this.env.pos.table) { + this.env.pos.set_table(null); + } + this.floorMapRef.el.style.background = this.state.floorBackground; + this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; + // call _tableLongpolling once then set interval of 5sec. + this._tableLongpolling(); + this.tableLongpolling = setInterval(this._tableLongpolling.bind(this), 5000); + } + willUnmount() { + clearInterval(this.tableLongpolling); + } + get activeFloor() { + return this.env.pos.floors_by_id[this.state.selectedFloorId]; + } + get activeTables() { + return this.activeFloor.tables; + } + get isFloorEmpty() { + return this.activeTables.length === 0; + } + get selectedTable() { + return this.state.selectedTableId !== null + ? this.env.pos.tables_by_id[this.state.selectedTableId] + : false; + } + selectFloor(floor) { + this.state.selectedFloorId = floor.id; + this.state.floorBackground = this.activeFloor.background_color; + this.state.isEditMode = false; + this.state.selectedTableId = null; + } + toggleEditMode() { + this.state.isEditMode = !this.state.isEditMode; + this.state.selectedTableId = null; + } + async _createTable() { + const newTable = await this._createTableHelper(); + if (newTable) { + this.state.selectedTableId = newTable.id; + } + } + async _duplicateTable() { + if (!this.selectedTable) return; + const newTable = await this._createTableHelper(this.selectedTable); + if (newTable) { + this.state.selectedTableId = newTable.id; + } + } + async _changeSeatsNum() { + const selectedTable = this.selectedTable + if (!selectedTable) return; + const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { + startingValue: selectedTable.seats, + cheap: true, + title: this.env._t('Number of Seats ?'), + }); + if (!confirmed) return; + const newSeatsNum = parseInt(inputNumber, 10) || selectedTable.seats; + if (newSeatsNum !== selectedTable.seats) { + selectedTable.seats = newSeatsNum; + await this._save(selectedTable); + } + } + 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 }) { + this.selectedTable.color = color; + this.render(); + await this._save(this.selectedTable); + } + async _setFloorColor({ detail: color }) { + this.state.floorBackground = color; + this.activeFloor.background_color = color; + try { + await this.rpc({ + model: 'restaurant.floor', + method: 'write', + args: [[this.activeFloor.id], { background_color: color }], + }); + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to change background color'), + }); + } else { + throw error; + } + } + } + async _deleteTable() { + if (!this.selectedTable) return; + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: this.env._t('Are you sure ?'), + body: this.env._t('Removing a table cannot be undone'), + }); + if (!confirmed) return; + try { + const originalSelectedTableId = this.state.selectedTableId; + await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [{ active: false, id: originalSelectedTableId }], + }); + this.activeFloor.tables = this.activeTables.filter( + (table) => table.id !== originalSelectedTableId + ); + // Value of an object can change inside async function call. + // Which means that in this code block, the value of `state.selectedTableId` + // before the await call can be different after the finishing the await call. + // Since we wanted to disable the selected table after deletion, we should be + // setting the selectedTableId to null. However, we only do this if nothing + // else is selected during the rpc call. + if (this.state.selectedTableId === originalSelectedTableId) { + this.state.selectedTableId = null; + } + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to delete table'), + }); + } else { + throw error; + } + } + } + _onSelectTable(event) { + const table = event.detail; + if (this.state.isEditMode) { + this.state.selectedTableId = table.id; + } else { + this.env.pos.set_table(table); + } + } + _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 fields = this.env.pos.models.find((model) => model.model === 'restaurant.table') + .fields; + const serializeTable = {}; + for (let field of fields) { + if (typeof table[field] !== 'undefined') { + serializeTable[field] = table[field]; + } + } + serializeTable.id = table.id; + const tableId = await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [serializeTable], + }); + 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: 'Offline', + body: 'Unable to get orders count', + }); + } else { + throw error; + } + } + } + } + FloorScreen.template = 'FloorScreen'; + FloorScreen.hideOrderSelector = true; + + Registries.Component.add(FloorScreen); + + return FloorScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js new file mode 100644 index 00000000..48dfdad7 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js @@ -0,0 +1,73 @@ +odoo.define('pos_restaurant.TableWidget', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class TableWidget extends PosComponent { + mounted() { + const table = this.props.table; + function unit(val) { + return `${val}px`; + } + const style = { + width: unit(table.width), + height: unit(table.height), + 'line-height': unit(table.height), + top: unit(table.position_v), + left: unit(table.position_h), + 'border-radius': table.shape === 'round' ? unit(1000) : '3px', + }; + if (table.color) { + style.background = table.color; + } + if (table.height >= 150 && table.width >= 150) { + style['font-size'] = '32px'; + } + Object.assign(this.el.style, style); + + const tableCover = this.el.querySelector('.table-cover'); + Object.assign(tableCover.style, { height: `${Math.ceil(this.fill * 100)}%` }); + } + get fill() { + const customerCount = this.env.pos.get_customer_count(this.props.table); + return Math.min(1, Math.max(0, customerCount / this.props.table.seats)); + } + get orderCount() { + const table = this.props.table; + return table.order_count !== undefined + ? table.order_count + : this.env.pos + .get_table_orders(table) + .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, + }; + } + _getNotifications() { + const orders = this.env.pos.get_table_orders(this.props.table); + + let hasChangesCount = 0; + let hasSkippedCount = 0; + for (let i = 0; i < orders.length; i++) { + if (orders[i].hasChangesToPrint()) { + hasChangesCount++; + } else if (orders[i].hasSkippedChanges()) { + hasSkippedCount++; + } + } + + return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {}; + } + } + TableWidget.template = 'TableWidget'; + + Registries.Component.add(TableWidget); + + return TableWidget; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js new file mode 100644 index 00000000..8753be25 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js @@ -0,0 +1,28 @@ +odoo.define('pos_restaurant.OrderManagementScreen', function (require) { + 'use strict'; + + const OrderManagementScreen = require('point_of_sale.OrderManagementScreen'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderManagementScreen = (OrderManagementScreen) => + class extends OrderManagementScreen { + /** + * @override + */ + _setOrder(order) { + if (this.env.pos.config.module_pos_restaurant) { + const currentOrder = this.env.pos.get_order(); + this.env.pos.set_table(order.table, order); + if (currentOrder && currentOrder.uid === order.uid) { + this.close(); + } + } else { + super._setOrder(order); + } + } + }; + + Registries.Component.extend(OrderManagementScreen, PosResOrderManagementScreen); + + return OrderManagementScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js new file mode 100644 index 00000000..f5a32114 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js @@ -0,0 +1,17 @@ +odoo.define('pos_restaurant.OrderRow', function (require) { + 'use strict'; + + const OrderRow = require('point_of_sale.OrderRow'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderRow = (OrderRow) => + class extends OrderRow { + get table() { + return this.order.table ? this.order.table.name : ''; + } + }; + + Registries.Component.extend(OrderRow, PosResOrderRow); + + return OrderRow; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js new file mode 100644 index 00000000..472f7cad --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js @@ -0,0 +1,48 @@ +odoo.define('pos_restaurant.PosResPaymentScreen', function (require) { + 'use strict'; + + const PaymentScreen = require('point_of_sale.PaymentScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + const PosResPaymentScreen = (PaymentScreen) => + class extends PaymentScreen { + constructor() { + super(...arguments); + useListener('send-payment-adjust', this._sendPaymentAdjust); + } + + async _sendPaymentAdjust({ detail: line }) { + const previous_amount = line.get_amount(); + const amount_diff = line.order.get_total_with_tax() - line.order.get_total_paid(); + line.set_amount(previous_amount + amount_diff); + line.set_payment_status('waiting'); + + const payment_terminal = line.payment_method.payment_terminal; + const isAdjustSuccessful = await payment_terminal.send_payment_adjust(line.cid); + if (isAdjustSuccessful) { + line.set_payment_status('done'); + } else { + line.set_amount(previous_amount); + line.set_payment_status('done'); + } + } + + get nextScreen() { + const order = this.currentOrder; + if (!this.env.pos.config.set_tip_after_payment || order.is_tipped) { + return super.nextScreen; + } + // Take the first payment method as the main payment. + const mainPayment = order.get_paymentlines()[0]; + if (mainPayment.canBeAdjusted()) { + return 'TipScreen'; + } + return super.nextScreen; + } + }; + + Registries.Component.extend(PaymentScreen, PosResPaymentScreen); + + return PosResPaymentScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js new file mode 100644 index 00000000..58681810 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js @@ -0,0 +1,42 @@ +odoo.define('pos_restaurant.OrderlineNoteButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class OrderlineNoteButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get selectedOrderline() { + return this.env.pos.get_order().get_selected_orderline(); + } + async onClick() { + if (!this.selectedOrderline) return; + + const { confirmed, payload: inputNote } = await this.showPopup('TextAreaPopup', { + startingValue: this.selectedOrderline.get_note(), + title: this.env._t('Add Note'), + }); + + if (confirmed) { + this.selectedOrderline.set_note(inputNote); + } + } + } + OrderlineNoteButton.template = 'OrderlineNoteButton'; + + ProductScreen.addControlButton({ + component: OrderlineNoteButton, + condition: function() { + return this.env.pos.config.module_pos_restaurant; + }, + }); + + Registries.Component.add(OrderlineNoteButton); + + return OrderlineNoteButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js new file mode 100644 index 00000000..7563d9fc --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js @@ -0,0 +1,38 @@ +odoo.define('pos_restaurant.PrintBillButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class PrintBillButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + async onClick() { + const order = this.env.pos.get_order(); + if (order.get_orderlines().length > 0) { + await this.showTempScreen('BillScreen'); + } else { + await this.showPopup('ErrorPopup', { + title: this.env._t('Nothing to Print'), + body: this.env._t('There are no order lines'), + }); + } + } + } + PrintBillButton.template = 'PrintBillButton'; + + ProductScreen.addControlButton({ + component: PrintBillButton, + condition: function() { + return this.env.pos.config.iface_printbill; + }, + }); + + Registries.Component.add(PrintBillButton); + + return PrintBillButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js new file mode 100644 index 00000000..58ace925 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js @@ -0,0 +1,33 @@ +odoo.define('pos_restaurant.SplitBillButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class SplitBillButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + async onClick() { + const order = this.env.pos.get_order(); + if (order.get_orderlines().length > 0) { + this.showScreen('SplitBillScreen'); + } + } + } + SplitBillButton.template = 'SplitBillButton'; + + ProductScreen.addControlButton({ + component: SplitBillButton, + condition: function() { + return this.env.pos.config.iface_splitbill; + }, + }); + + Registries.Component.add(SplitBillButton); + + return SplitBillButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js new file mode 100644 index 00000000..948410e5 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js @@ -0,0 +1,70 @@ +odoo.define('pos_restaurant.SubmitOrderButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + /** + * IMPROVEMENT: Perhaps this class is quite complicated for its worth. + * This is because it needs to listen to changes to the current order. + * Also, the current order changes when the selectedOrder in pos is changed. + * After setting new current order, we update the listeners. + */ + class SubmitOrderButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + this._currentOrder = this.env.pos.get_order(); + this._currentOrder.orderlines.on('change', this.render, this); + this.env.pos.on('change:selectedOrder', this._updateCurrentOrder, this); + } + willUnmount() { + this._currentOrder.orderlines.off('change', null, this); + this.env.pos.off('change:selectedOrder', null, this); + } + 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: 'Printing failed', + body: 'Failed in printing the changes in the order', + }); + } + } + } + get addedClasses() { + if (!this._currentOrder) return {}; + const changes = this._currentOrder.hasChangesToPrint(); + const skipped = changes ? false : this._currentOrder.hasSkippedChanges(); + return { + highlight: changes, + altlight: skipped, + }; + } + _updateCurrentOrder(pos, newSelectedOrder) { + this._currentOrder.orderlines.off('change', null, this); + if (newSelectedOrder) { + this._currentOrder = newSelectedOrder; + this._currentOrder.orderlines.on('change', this.render, this); + } + } + } + SubmitOrderButton.template = 'SubmitOrderButton'; + + ProductScreen.addControlButton({ + component: SubmitOrderButton, + condition: function() { + return this.env.pos.printers.length; + }, + }); + + Registries.Component.add(SubmitOrderButton); + + return SubmitOrderButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js new file mode 100644 index 00000000..e015496d --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js @@ -0,0 +1,44 @@ +odoo.define('pos_restaurant.TableGuestsButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class TableGuestsButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get nGuests() { + return this.currentOrder ? this.currentOrder.get_customer_count() : 0; + } + async onClick() { + const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { + startingValue: this.nGuests, + cheap: true, + title: this.env._t('Guests ?'), + }); + + if (confirmed) { + this.env.pos.get_order().set_customer_count(parseInt(inputNumber, 10) || 1); + } + } + } + TableGuestsButton.template = 'TableGuestsButton'; + + ProductScreen.addControlButton({ + component: TableGuestsButton, + condition: function() { + return this.env.pos.config.module_pos_restaurant; + }, + }); + + Registries.Component.add(TableGuestsButton); + + return TableGuestsButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js new file mode 100644 index 00000000..ed72e2a7 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js @@ -0,0 +1,30 @@ +odoo.define('pos_restaurant.TransferOrderButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class TransferOrderButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + onClick() { + this.env.pos.transfer_order_to_different_table(); + } + } + TransferOrderButton.template = 'TransferOrderButton'; + + ProductScreen.addControlButton({ + component: TransferOrderButton, + condition: function() { + return this.env.pos.config.iface_floorplan; + }, + }); + + Registries.Component.add(TransferOrderButton); + + return TransferOrderButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js new file mode 100644 index 00000000..2204405c --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js @@ -0,0 +1,46 @@ +odoo.define('pos_restaurant.Orderline', function(require) { + 'use strict'; + + const Orderline = require('point_of_sale.Orderline'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderline = Orderline => + class extends Orderline { + /** + * @override + */ + get addedClasses() { + const res = super.addedClasses; + Object.assign(res, { + dirty: this.props.line.mp_dirty, + skip: this.props.line.mp_skip, + }); + return res; + } + /** + * @override + * if doubleclick, change mp_dirty to mp_skip + * + * IMPROVEMENT: Instead of handling both double click and click in single + * method, perhaps we can separate double click from single click. + */ + selectLine() { + const line = this.props.line; // the orderline + if (this.env.pos.get_order().selected_orderline !== line) { + this.mp_dbclk_time = new Date().getTime(); + } else if (!this.mp_dbclk_time) { + this.mp_dbclk_time = new Date().getTime(); + } else if (this.mp_dbclk_time + 500 > new Date().getTime()) { + line.set_skip(!line.mp_skip); + this.mp_dbclk_time = 0; + } else { + this.mp_dbclk_time = new Date().getTime(); + } + super.selectLine(); + } + }; + + Registries.Component.extend(Orderline, PosResOrderline); + + return Orderline; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js new file mode 100644 index 00000000..689c3bf4 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -0,0 +1,28 @@ +odoo.define('pos_restaurant.ReceiptScreen', function(require) { + 'use strict'; + + const ReceiptScreen = require('point_of_sale.ReceiptScreen'); + const Registries = require('point_of_sale.Registries'); + + const PosResReceiptScreen = ReceiptScreen => + class extends ReceiptScreen { + /** + * @override + */ + get nextScreen() { + if ( + this.env.pos.config.module_pos_restaurant && + 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; + } + } + }; + + Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); + + return ReceiptScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js new file mode 100644 index 00000000..f44af5bf --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -0,0 +1,197 @@ +odoo.define('pos_restaurant.SplitBillScreen', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const { useState } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const models = require('point_of_sale.models'); + const Registries = require('point_of_sale.Registries'); + + class SplitBillScreen extends PosComponent { + constructor() { + super(...arguments); + useListener('click-line', this.onClickLine); + this.splitlines = useState(this._initSplitLines(this.env.pos.get_order())); + this.newOrderLines = {}; + this.newOrder = new models.Order( + {}, + { + pos: this.env.pos, + temporary: true, + } + ); + this._isFinal = false; + } + mounted() { + this.env.pos.on('change:selectedOrder', this._resetState, this); + } + willUnmount() { + this.env.pos.off('change:selectedOrder', null, this); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get orderlines() { + return this.currentOrder.get_orderlines(); + } + onClickLine(event) { + const line = event.detail; + this._splitQuantity(line); + this._updateNewOrder(line); + } + back() { + this.showScreen('ProductScreen'); + } + proceed() { + if (_.isEmpty(this.splitlines)) + // Splitlines is empty + return; + + this._isFinal = true; + delete this.newOrder.temporary; + + if (this._isFullPayOrder()) { + this.showScreen('PaymentScreen'); + } else { + this._setQuantityOnCurrentOrder(); + + this.newOrder.set_screen_data({ name: 'PaymentScreen' }); + + // for the kitchen printer we assume that everything + // has already been sent to the kitchen before splitting + // the bill. So we save all changes both for the old + // 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(); + } + + this.newOrder.set_customer_count(1); + const newCustomerCount = this.currentOrder.get_customer_count() - 1; + this.currentOrder.set_customer_count(newCustomerCount || 1); + this.currentOrder.set_screen_data({ name: 'ProductScreen' }); + + this.env.pos.get('orders').add(this.newOrder); + this.env.pos.set('selectedOrder', this.newOrder); + } + } + /** + * @param {models.Order} order + * @returns {Object<{ quantity: number }>} splitlines + */ + _initSplitLines(order) { + const splitlines = {}; + for (let line of order.get_orderlines()) { + splitlines[line.id] = { product: line.get_product().id, quantity: 0 }; + } + return splitlines; + } + _splitQuantity(line) { + const split = this.splitlines[line.id]; + + let totalQuantity = 0; + + this.env.pos.get_order().get_orderlines().forEach(function(orderLine) { + if(orderLine.get_product().id === split.product) + totalQuantity += orderLine.get_quantity(); + }); + + if(line.get_quantity() > 0) { + if (!line.get_unit().is_pos_groupable) { + if (split.quantity !== line.get_quantity()) { + split.quantity = line.get_quantity(); + } else { + split.quantity = 0; + } + } else { + if (split.quantity < totalQuantity) { + split.quantity += line.get_unit().is_pos_groupable? 1: line.get_unit().rounding; + if (split.quantity > line.get_quantity()) { + split.quantity = line.get_quantity(); + } + } else { + split.quantity = 0; + } + } + } + } + _updateNewOrder(line) { + const split = this.splitlines[line.id]; + let orderline = this.newOrderLines[line.id]; + if (split.quantity) { + if (!orderline) { + orderline = line.clone(); + this.newOrder.add_orderline(orderline); + this.newOrderLines[line.id] = orderline; + } + orderline.set_quantity(split.quantity, 'do not recompute unit price'); + } else if (orderline) { + this.newOrder.remove_orderline(orderline); + this.newOrderLines[line.id] = null; + } + } + _isFullPayOrder() { + let order = this.env.pos.get_order(); + let full = true; + let splitlines = this.splitlines; + let groupedLines = _.groupBy(order.get_orderlines(), line => line.get_product().id); + + Object.keys(groupedLines).forEach(function (lineId) { + var maxQuantity = groupedLines[lineId].reduce(((quantity, line) => quantity + line.get_quantity()), 0); + Object.keys(splitlines).forEach(id => { + let split = splitlines[id]; + if(split.product === groupedLines[lineId][0].get_product().id) + maxQuantity -= split.quantity; + }); + if(maxQuantity !== 0) + full = false; + }); + + return full; + } + _setQuantityOnCurrentOrder() { + let order = this.env.pos.get_order(); + for (var id in this.splitlines) { + var split = this.splitlines[id]; + var line = this.currentOrder.get_orderline(parseInt(id)); + + if(!this.props.disallow) { + line.set_quantity( + line.get_quantity() - split.quantity, + 'do not recompute unit price' + ); + if (Math.abs(line.get_quantity()) < 0.00001) { + this.currentOrder.remove_orderline(line); + } + } else { + if(split.quantity) { + let decreaseLine = line.clone(); + decreaseLine.order = order; + decreaseLine.noDecrease = true; + decreaseLine.set_quantity(-split.quantity); + order.add_orderline(decreaseLine); + } + } + } + } + _resetState() { + if (this._isFinal) return; + + for (let id in this.splitlines) { + delete this.splitlines[id]; + } + for (let line of this.currentOrder.get_orderlines()) { + this.splitlines[line.id] = { quantity: 0 }; + } + this.newOrder.orderlines.reset(); + } + } + SplitBillScreen.template = 'SplitBillScreen'; + + Registries.Component.add(SplitBillScreen); + + return SplitBillScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js new file mode 100644 index 00000000..51c0b929 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js @@ -0,0 +1,25 @@ +odoo.define('pos_restaurant.SplitOrderline', function(require) { + 'use strict'; + + const { useListener } = require('web.custom_hooks'); + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class SplitOrderline extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get isSelected() { + return this.props.split.quantity !== 0; + } + onClick() { + this.trigger('click-line', this.props.line); + } + } + SplitOrderline.template = 'SplitOrderline'; + + Registries.Component.add(SplitOrderline); + + return SplitOrderline; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js new file mode 100644 index 00000000..cbea3d88 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js @@ -0,0 +1,158 @@ +odoo.define('pos_restaurant.TicketScreen', function (require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const TicketScreen = require('point_of_sale.TicketScreen'); + const Registries = require('point_of_sale.Registries'); + const { useAutofocus } = require('web.custom_hooks'); + const { posbus } = require('point_of_sale.utils'); + const { parse } = require('web.field_utils'); + const { useState, useContext } = owl.hooks; + + const PosResTicketScreen = (TicketScreen) => + class extends TicketScreen { + close() { + super.close(); + if (!this.env.pos.config.iface_floorplan) { + // Make sure the 'table-set' event is triggered + // to properly rerender the components that listens to it. + posbus.trigger('table-set'); + } + } + get filterOptions() { + const { Payment, Open, Tipping } = this.getOrderStates(); + var filterOptions = super.filterOptions; + if (this.env.pos.config.set_tip_after_payment) { + var idx = filterOptions.indexOf(Payment); + filterOptions[idx] = Open; + } + return [...filterOptions, Tipping]; + } + get _screenToStatusMap() { + const { Open, Tipping } = this.getOrderStates(); + return Object.assign(super._screenToStatusMap, { + PaymentScreen: this.env.pos.config.set_tip_after_payment ? Open : super._screenToStatusMap.PaymentScreen, + TipScreen: Tipping, + }); + } + getTable(order) { + return `${order.table.floor.name} (${order.table.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})`, + }); + } + _setOrder(order) { + if (!this.env.pos.config.iface_floorplan) { + 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); + } + } + get showNewTicketButton() { + return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.showNewTicketButton; + } + get orderList() { + if (this.env.pos.table) { + return super.orderList; + } else { + return this.env.pos.get('orders').models; + } + } + async settleTips() { + // set tip in each order + for (const order of this.filteredOrderList) { + const tipAmount = parse.float(order.uiState.TipScreen.state.inputTipAmount || '0'); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + if (!serverId) { + console.warn(`${order.name} is not yet sync. Sync it to server before setting a tip.`); + } else { + const result = await this.setTip(order, serverId, tipAmount); + if (!result) break; + } + } + } + async setTip(order, serverId, amount) { + try { + const paymentline = order.get_paymentlines()[0]; + if (paymentline.payment_method.payment_terminal) { + paymentline.amount += amount; + this.env.pos.set_order(order, {silent: true}); + await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid); + } + + if (!amount) { + await this.setNoTip(); + } else { + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + const tip_line = order.selected_orderline; + await this.rpc({ + method: 'set_tip', + model: 'pos.order', + args: [serverId, tip_line.export_as_JSON()], + }); + } + order.finalize(); + return true; + } catch (error) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: 'Failed to set tip', + body: `Failed to set tip to ${order.name}. Do you want to proceed on setting the tips of the remaining?`, + }); + return confirmed; + } + } + async setNoTip() { + await this.rpc({ + method: 'set_no_tip', + model: 'pos.order', + args: [serverId], + }); + } + getOrderStates() { + return Object.assign(super.getOrderStates(), { + Tipping: this.env._t('Tipping'), + Open: this.env._t('Open'), + }); + } + }; + + Registries.Component.extend(TicketScreen, PosResTicketScreen); + + class TipCell extends PosComponent { + constructor() { + super(...arguments); + this.state = useState({ isEditing: false }); + this.orderUiState = useContext(this.props.order.uiState.TipScreen); + useAutofocus({ selector: 'input' }); + } + get tipAmountStr() { + return this.env.pos.format_currency(parse.float(this.orderUiState.inputTipAmount || '0')); + } + onBlur() { + this.state.isEditing = false; + } + onKeydown(event) { + if (event.key === 'Enter') { + this.state.isEditing = false; + } + } + editTip() { + this.state.isEditing = true; + } + } + TipCell.template = 'TipCell'; + + Registries.Component.add(TipCell); + + return { TicketScreen, TipCell }; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js new file mode 100644 index 00000000..bb6e17a2 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -0,0 +1,156 @@ +odoo.define('pos_restaurant.TipScreen', function (require) { + 'use strict'; + + const Registries = require('point_of_sale.Registries'); + const PosComponent = require('point_of_sale.PosComponent'); + const { parse } = require('web.field_utils'); + const { useContext } = owl.hooks; + + class TipScreen extends PosComponent { + constructor() { + super(...arguments); + this.state = useContext(this.currentOrder.uiState.TipScreen); + this._totalAmount = this.currentOrder.get_total_with_tax(); + } + mounted () { + this.printTipReceipt(); + } + get overallAmountStr() { + const tipAmount = parse.float(this.state.inputTipAmount || '0'); + const original = this.env.pos.format_currency(this.totalAmount); + const tip = this.env.pos.format_currency(tipAmount); + const overall = this.env.pos.format_currency(this.totalAmount + tipAmount); + return `${original} + ${tip} tip = ${overall}`; + } + get totalAmount() { + return this._totalAmount; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get percentageTips() { + return [ + { percentage: '15%', amount: 0.15 * this.totalAmount }, + { percentage: '20%', amount: 0.2 * this.totalAmount }, + { percentage: '25%', amount: 0.25 * this.totalAmount }, + ]; + } + async validateTip() { + const amount = parse.float(this.state.inputTipAmount) || 0; + const order = this.env.pos.get_order(); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + + if (!serverId) { + this.showPopup('ErrorPopup', { + title: 'Unsynced order', + body: 'This order is not yet synced to server. Make sure it is synced then try again.', + }); + return; + } + + if (!amount) { + await this.rpc({ + method: 'set_no_tip', + model: 'pos.order', + args: [serverId], + }); + this.goNextScreen(); + return; + } + + if (amount > 0.25 * this.totalAmount) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: 'Are you sure?', + body: `${this.env.pos.format_currency( + amount + )} is more than 25% of the order's total amount. Are you sure of this tip amount?`, + }); + if (!confirmed) return; + } + + // set the tip by temporarily allowing order modification + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + + const paymentline = this.env.pos.get_order().get_paymentlines()[0]; + if (paymentline.payment_method.payment_terminal) { + paymentline.amount += amount; + await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid); + } + + // set_tip calls add_product which sets the new line as the selected_orderline + const tip_line = order.selected_orderline; + await this.rpc({ + method: 'set_tip', + model: 'pos.order', + args: [serverId, tip_line.export_as_JSON()], + }); + this.goNextScreen(); + } + goNextScreen() { + this.env.pos.get_order().finalize(); + const { name, props } = this.nextScreen; + this.showScreen(name, props); + } + get nextScreen() { + if (this.env.pos.config.module_pos_restaurant && this.env.pos.config.iface_floorplan) { + const table = this.env.pos.table; + return { name: 'FloorScreen', props: { floor: table ? table.floor : null } }; + } else { + return { name: 'ProductScreen' }; + } + } + async printTipReceipt() { + const receipts = [ + this.currentOrder.selected_paymentline.ticket, + this.currentOrder.selected_paymentline.cashier_receipt + ]; + + for (let i = 0; i < receipts.length; i++) { + const data = receipts[i]; + var receipt = this.env.qweb.renderToString('TipReceipt', { + receipt: this.currentOrder.getOrderReceiptEnv().receipt, + data: data, + total: this.env.pos.format_currency(this.totalAmount), + }); + + if (this.env.pos.proxy.printer) { + await this._printIoT(receipt); + } else { + await this._printWeb(receipt); + } + } + } + + async _printIoT(receipt) { + const printResult = await this.env.pos.proxy.printer.print_receipt(receipt); + if (!printResult.successful) { + await this.showPopup('ErrorPopup', { + title: printResult.message.title, + body: printResult.message.body, + }); + } + } + + async _printWeb(receipt) { + try { + $(this.el).find('.pos-receipt-container').html(receipt); + window.print(); + } catch (err) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Printing is not supported on some browsers'), + body: this.env._t( + 'Printing is not supported on some browsers due to no default printing protocol ' + + 'is available. It is possible to print your tickets by making use of an IoT Box.' + ), + }); + } + } + } + TipScreen.template = 'pos_restaurant.TipScreen'; + + Registries.Component.add(TipScreen); + + return TipScreen; +}); |
