summaryrefslogtreecommitdiff
path: root/addons/pos_restaurant/static/src/js/Screens
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/static/src/js/Screens
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/static/src/js/Screens')
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/BillScreen.js31
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js19
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js60
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js315
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js73
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js28
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js17
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js48
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js42
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js38
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js33
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js70
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js44
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js30
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js46
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js28
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js197
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js25
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TicketScreen.js158
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TipScreen.js156
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;
+});