summaryrefslogtreecommitdiff
path: root/addons/pos_restaurant/static/src/js
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
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/static/src/js')
-rw-r--r--addons/pos_restaurant/static/src/js/Chrome.js99
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js34
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js33
-rw-r--r--addons/pos_restaurant/static/src/js/Resizeable.js330
-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
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js399
-rw-r--r--addons/pos_restaurant/static/src/js/multiprint.js315
-rw-r--r--addons/pos_restaurant/static/src/js/notes.js42
-rw-r--r--addons/pos_restaurant/static/src/js/payment.js24
28 files changed, 2734 insertions, 0 deletions
diff --git a/addons/pos_restaurant/static/src/js/Chrome.js b/addons/pos_restaurant/static/src/js/Chrome.js
new file mode 100644
index 00000000..949082f1
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/Chrome.js
@@ -0,0 +1,99 @@
+odoo.define('pos_restaurant.chrome', function (require) {
+ 'use strict';
+
+ const Chrome = require('point_of_sale.Chrome');
+ const Registries = require('point_of_sale.Registries');
+
+ const NON_IDLE_EVENTS = 'mousemove mousedown touchstart touchend touchmove click scroll keypress'.split(/\s+/);
+ let IDLE_TIMER_SETTER;
+
+ const PosResChrome = (Chrome) =>
+ class extends Chrome {
+ /**
+ * @override
+ */
+ async start() {
+ await super.start();
+ if (this.env.pos.config.iface_floorplan) {
+ this._setActivityListeners();
+ }
+ }
+ /**
+ * @override
+ * Do not set `FloorScreen` to the order.
+ */
+ _setScreenData(name) {
+ if (name === 'FloorScreen') return;
+ super._setScreenData(...arguments);
+ }
+ /**
+ * @override
+ * `FloorScreen` is the start screen if there are floors.
+ */
+ get startScreen() {
+ if (this.env.pos.config.iface_floorplan) {
+ const table = this.env.pos.table;
+ return { name: 'FloorScreen', props: { floor: table ? table.floor : null } };
+ } else {
+ return super.startScreen;
+ }
+ }
+ /**
+ * @override
+ * Order is set to null when table is selected. There is no saved
+ * screen for null order so show `FloorScreen` instead.
+ */
+ _showSavedScreen(pos, newSelectedOrder) {
+ if (!newSelectedOrder) {
+ this.showScreen('FloorScreen', { floor: pos.table ? pos.table.floor : null });
+ } else {
+ super._showSavedScreen(pos, newSelectedOrder);
+ }
+ }
+ _setActivityListeners() {
+ IDLE_TIMER_SETTER = this._setIdleTimer.bind(this);
+ for (const event of NON_IDLE_EVENTS) {
+ window.addEventListener(event, IDLE_TIMER_SETTER);
+ }
+ }
+ _setIdleTimer() {
+ if (this._shouldResetIdleTimer()) {
+ clearTimeout(this.idleTimer);
+ this.idleTimer = setTimeout(() => {
+ this._actionAfterIdle();
+ }, 60000);
+ }
+ }
+ _actionAfterIdle() {
+ if (this.tempScreen.isShown) {
+ this.trigger('close-temp-screen');
+ }
+ const table = this.env.pos.table;
+ this.showScreen('FloorScreen', { floor: table ? table.floor : null });
+ }
+ _shouldResetIdleTimer() {
+ return this.env.pos.config.iface_floorplan && this.mainScreen.name !== 'FloorScreen';
+ }
+ __showScreen() {
+ super.__showScreen(...arguments);
+ this._setIdleTimer();
+ }
+ /**
+ * @override
+ * Before closing pos, we remove the event listeners set on window
+ * for detecting activities outside FloorScreen.
+ */
+ async _closePos() {
+ if (IDLE_TIMER_SETTER) {
+ for (const event of NON_IDLE_EVENTS) {
+ window.removeEventListener(event, IDLE_TIMER_SETTER);
+ }
+ }
+ await super._closePos();
+ }
+ };
+
+ Registries.Component.extend(Chrome, PosResChrome);
+
+ return Chrome;
+});
diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js
new file mode 100644
index 00000000..c2b62350
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js
@@ -0,0 +1,34 @@
+odoo.define('pos_restaurant.BackToFloorButton', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { posbus } = require('point_of_sale.utils');
+
+ class BackToFloorButton extends PosComponent {
+ mounted() {
+ posbus.on('table-set', this, this.render);
+ }
+ willUnmount() {
+ posbus.on('table-set', this);
+ }
+ get table() {
+ return (this.env.pos && this.env.pos.table) || null;
+ }
+ get floor() {
+ const table = this.table;
+ return table ? table.floor : null;
+ }
+ get hasTable() {
+ return this.table !== null;
+ }
+ backToFloorScreen() {
+ this.showScreen('FloorScreen', { floor: this.floor });
+ }
+ }
+ BackToFloorButton.template = 'BackToFloorButton';
+
+ Registries.Component.add(BackToFloorButton);
+
+ return BackToFloorButton;
+});
diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js
new file mode 100644
index 00000000..1e66bf91
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js
@@ -0,0 +1,33 @@
+odoo.define('pos_restaurant.TicketButton', function (require) {
+ 'use strict';
+
+ const TicketButton = require('point_of_sale.TicketButton');
+ const Registries = require('point_of_sale.Registries');
+ const { posbus } = require('point_of_sale.utils');
+
+ const PosResTicketButton = (TicketButton) =>
+ class extends TicketButton {
+ mounted() {
+ posbus.on('table-set', this, this.render);
+ }
+ willUnmount() {
+ posbus.off('table-set', this);
+ }
+ /**
+ * If no table is set to pos, which means the current main screen
+ * is floor screen, then the order count should be based on all the orders.
+ */
+ get count() {
+ if (!this.env.pos || !this.env.pos.config) return 0;
+ if (this.env.pos.config.iface_floorplan && !this.env.pos.table) {
+ return this.env.pos.get('orders').models.length;
+ } else {
+ return super.count;
+ }
+ }
+ };
+
+ Registries.Component.extend(TicketButton, PosResTicketButton);
+
+ return TicketButton;
+});
diff --git a/addons/pos_restaurant/static/src/js/Resizeable.js b/addons/pos_restaurant/static/src/js/Resizeable.js
new file mode 100644
index 00000000..c651ae09
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/Resizeable.js
@@ -0,0 +1,330 @@
+odoo.define('pos_restaurant.Resizeable', function(require) {
+ 'use strict';
+
+ const { useExternalListener } = owl.hooks;
+ const { useListener } = require('web.custom_hooks');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ class Resizeable extends PosComponent {
+ constructor() {
+ super(...arguments);
+
+ useExternalListener(document, 'mousemove', this.resizeN);
+ useExternalListener(document, 'mouseup', this.endResizeN);
+ useListener('mousedown', '.resize-handle-n', this.startResizeN);
+
+ useExternalListener(document, 'mousemove', this.resizeS);
+ useExternalListener(document, 'mouseup', this.endResizeS);
+ useListener('mousedown', '.resize-handle-s', this.startResizeS);
+
+ useExternalListener(document, 'mousemove', this.resizeW);
+ useExternalListener(document, 'mouseup', this.endResizeW);
+ useListener('mousedown', '.resize-handle-w', this.startResizeW);
+
+ useExternalListener(document, 'mousemove', this.resizeE);
+ useExternalListener(document, 'mouseup', this.endResizeE);
+ useListener('mousedown', '.resize-handle-e', this.startResizeE);
+
+ useExternalListener(document, 'mousemove', this.resizeNW);
+ useExternalListener(document, 'mouseup', this.endResizeNW);
+ useListener('mousedown', '.resize-handle-nw', this.startResizeNW);
+
+ useExternalListener(document, 'mousemove', this.resizeNE);
+ useExternalListener(document, 'mouseup', this.endResizeNE);
+ useListener('mousedown', '.resize-handle-ne', this.startResizeNE);
+
+ useExternalListener(document, 'mousemove', this.resizeSW);
+ useExternalListener(document, 'mouseup', this.endResizeSW);
+ useListener('mousedown', '.resize-handle-sw', this.startResizeSW);
+
+ useExternalListener(document, 'mousemove', this.resizeSE);
+ useExternalListener(document, 'mouseup', this.endResizeSE);
+ useListener('mousedown', '.resize-handle-se', this.startResizeSE);
+
+ useExternalListener(document, 'touchmove', this.resizeN);
+ useExternalListener(document, 'touchend', this.endResizeN);
+ useListener('touchstart', '.resize-handle-n', this.startResizeN);
+
+ useExternalListener(document, 'touchmove', this.resizeS);
+ useExternalListener(document, 'touchend', this.endResizeS);
+ useListener('touchstart', '.resize-handle-s', this.startResizeS);
+
+ useExternalListener(document, 'touchmove', this.resizeW);
+ useExternalListener(document, 'touchend', this.endResizeW);
+ useListener('touchstart', '.resize-handle-w', this.startResizeW);
+
+ useExternalListener(document, 'touchmove', this.resizeE);
+ useExternalListener(document, 'touchend', this.endResizeE);
+ useListener('touchstart', '.resize-handle-e', this.startResizeE);
+
+ useExternalListener(document, 'touchmove', this.resizeNW);
+ useExternalListener(document, 'touchend', this.endResizeNW);
+ useListener('touchstart', '.resize-handle-nw', this.startResizeNW);
+
+ useExternalListener(document, 'touchmove', this.resizeNE);
+ useExternalListener(document, 'touchend', this.endResizeNE);
+ useListener('touchstart', '.resize-handle-ne', this.startResizeNE);
+
+ useExternalListener(document, 'touchmove', this.resizeSW);
+ useExternalListener(document, 'touchend', this.endResizeSW);
+ useListener('touchstart', '.resize-handle-sw', this.startResizeSW);
+
+ useExternalListener(document, 'touchmove', this.resizeSE);
+ useExternalListener(document, 'touchend', this.endResizeSE);
+ useListener('touchstart', '.resize-handle-se', this.startResizeSE);
+
+ this.size = { height: 0, width: 0 };
+ this.loc = { top: 0, left: 0 };
+ this.tempSize = {};
+ }
+ mounted() {
+ this.limitArea = this.props.limitArea
+ ? document.querySelector(this.props.limitArea)
+ : this.el.offsetParent;
+ this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect();
+ if (this.limitArea === this.el.offsetParent) {
+ this.limitLeft = 0;
+ this.limitTop = 0;
+ this.limitRight = this.limitAreaBoundingRect.width;
+ this.limitBottom = this.limitAreaBoundingRect.height;
+ } else {
+ this.limitLeft = -this.el.offsetParent.offsetLeft;
+ this.limitTop = -this.el.offsetParent.offsetTop;
+ this.limitRight =
+ this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft;
+ this.limitBottom =
+ this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop;
+ }
+ this.limitAreaWidth = this.limitAreaBoundingRect.width;
+ this.limitAreaHeight = this.limitAreaBoundingRect.height;
+ }
+ startResizeN(event) {
+ let realEvent;
+ if (event instanceof CustomEvent) {
+ realEvent = event.detail;
+ } else {
+ realEvent = event;
+ }
+ const { y } = this._getEventLoc(realEvent);
+ this.isResizingN = true;
+ this.startY = y;
+ this.size.height = this.el.offsetHeight;
+ this.loc.top = this.el.offsetTop;
+ event.stopPropagation();
+ }
+ resizeN(event) {
+ if (this.isResizingN) {
+ const { y: newY } = this._getEventLoc(event);
+ let dY = newY - this.startY;
+ if (dY < 0 && Math.abs(dY) > this.loc.top) {
+ dY = -this.loc.top;
+ } else if (dY > 0 && dY > this.size.height) {
+ dY = this.size.height;
+ }
+ this.el.style.height = `${this.size.height - dY}px`;
+ this.el.style.top = `${this.loc.top + dY}px`;
+ }
+ }
+ endResizeN() {
+ if (this.isResizingN && !this.isResizingE && !this.isResizingW && !this.isResizingS) {
+ this.isResizingN = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeS(event) {
+ let realEvent;
+ if (event instanceof CustomEvent) {
+ realEvent = event.detail;
+ } else {
+ realEvent = event;
+ }
+ const { y } = this._getEventLoc(realEvent);
+ this.isResizingS = true;
+ this.startY = y;
+ this.size.height = this.el.offsetHeight;
+ this.loc.top = this.el.offsetTop;
+ event.stopPropagation();
+ }
+ resizeS(event) {
+ if (this.isResizingS) {
+ const { y: newY } = this._getEventLoc(event);
+ let dY = newY - this.startY;
+ if (dY > 0 && dY > this.limitAreaHeight - (this.size.height + this.loc.top)) {
+ dY = this.limitAreaHeight - (this.size.height + this.loc.top);
+ } else if (dY < 0 && Math.abs(dY) > this.size.height) {
+ dY = -this.size.height;
+ }
+ this.el.style.height = `${this.size.height + dY}px`;
+ }
+ }
+ endResizeS() {
+ if (!this.isResizingN && !this.isResizingE && !this.isResizingW && this.isResizingS) {
+ this.isResizingS = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeW(event) {
+ let realEvent;
+ if (event instanceof CustomEvent) {
+ realEvent = event.detail;
+ } else {
+ realEvent = event;
+ }
+ const { x } = this._getEventLoc(realEvent);
+ this.isResizingW = true;
+ this.startX = x;
+ this.size.width = this.el.offsetWidth;
+ this.loc.left = this.el.offsetLeft;
+ event.stopPropagation();
+ }
+ resizeW(event) {
+ if (this.isResizingW) {
+ const { x: newX } = this._getEventLoc(event);
+ let dX = newX - this.startX;
+ if (dX > 0 && dX > this.size.width) {
+ dX = this.size.width;
+ } else if (dX < 0 && Math.abs(dX) > this.loc.left + Math.abs(this.limitLeft)) {
+ dX = -this.loc.left + this.limitLeft;
+ }
+ this.el.style.width = `${this.size.width - dX}px`;
+ this.el.style.left = `${this.loc.left + dX}px`;
+ }
+ }
+ endResizeW() {
+ if (!this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) {
+ this.isResizingW = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeE(event) {
+ let realEvent;
+ if (event instanceof CustomEvent) {
+ realEvent = event.detail;
+ } else {
+ realEvent = event;
+ }
+ const { x } = this._getEventLoc(realEvent);
+ this.isResizingE = true;
+ this.startX = x;
+ this.size.width = this.el.offsetWidth;
+ this.loc.left = this.el.offsetLeft;
+ event.stopPropagation();
+ }
+ resizeE(event) {
+ if (this.isResizingE) {
+ const { x: newX } = this._getEventLoc(event);
+ let dX = newX - this.startX;
+ if (
+ dX > 0 &&
+ dX >
+ this.limitAreaWidth -
+ (this.size.width + this.loc.left + Math.abs(this.limitLeft))
+ ) {
+ dX =
+ this.limitAreaWidth -
+ (this.size.width + this.loc.left + Math.abs(this.limitLeft));
+ } else if (dX < 0 && Math.abs(dX) > this.size.width) {
+ dX = -this.size.width;
+ }
+ this.el.style.width = `${this.size.width + dX}px`;
+ }
+ }
+ endResizeE() {
+ if (!this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) {
+ this.isResizingE = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeNW(event) {
+ this.startResizeN(event);
+ this.startResizeW(event);
+ }
+ resizeNW(event) {
+ this.resizeN(event);
+ this.resizeW(event);
+ }
+ endResizeNW() {
+ if (this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) {
+ this.isResizingN = false;
+ this.isResizingW = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeNE(event) {
+ this.startResizeN(event);
+ this.startResizeE(event);
+ }
+ resizeNE(event) {
+ this.resizeN(event);
+ this.resizeE(event);
+ }
+ endResizeNE() {
+ if (this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) {
+ this.isResizingN = false;
+ this.isResizingE = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeSE(event) {
+ this.startResizeS(event);
+ this.startResizeE(event);
+ }
+ resizeSE(event) {
+ this.resizeS(event);
+ this.resizeE(event);
+ }
+ endResizeSE() {
+ if (!this.isResizingN && this.isResizingE && !this.isResizingW && this.isResizingS) {
+ this.isResizingS = false;
+ this.isResizingE = false;
+ this._triggerResizeEnd();
+ }
+ }
+ startResizeSW(event) {
+ this.startResizeS(event);
+ this.startResizeW(event);
+ }
+ resizeSW(event) {
+ this.resizeS(event);
+ this.resizeW(event);
+ }
+ endResizeSW() {
+ if (!this.isResizingN && !this.isResizingE && this.isResizingW && this.isResizingS) {
+ this.isResizingS = false;
+ this.isResizingW = false;
+ this._triggerResizeEnd();
+ }
+ }
+ _getEventLoc(event) {
+ let coordX, coordY;
+ if (event.touches && event.touches[0]) {
+ coordX = event.touches[0].clientX;
+ coordY = event.touches[0].clientY;
+ } else {
+ coordX = event.clientX;
+ coordY = event.clientY;
+ }
+ return {
+ x: coordX,
+ y: coordY,
+ };
+ }
+ _triggerResizeEnd() {
+ const size = {
+ height: this.el.offsetHeight,
+ width: this.el.offsetWidth,
+ };
+ const loc = {
+ top: this.el.offsetTop,
+ left: this.el.offsetLeft,
+ };
+ this.trigger('resize-end', { size, loc });
+ }
+ }
+ Resizeable.template = 'Resizeable';
+
+ Registries.Component.add(Resizeable);
+
+ return Resizeable;
+});
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;
+});
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
new file mode 100644
index 00000000..3f5b739a
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -0,0 +1,399 @@
+odoo.define('pos_restaurant.floors', function (require) {
+"use strict";
+
+var models = require('point_of_sale.models');
+const { Gui } = require('point_of_sale.Gui');
+const { posbus } = require('point_of_sale.utils');
+
+// At POS Startup, load the floors, and add them to the pos model
+models.load_models({
+ model: 'restaurant.floor',
+ fields: ['name','background_color','table_ids','sequence'],
+ domain: function(self){ return [['pos_config_id','=',self.config.id]]; },
+ loaded: function(self,floors){
+ self.floors = floors;
+ self.floors_by_id = {};
+ for (var i = 0; i < floors.length; i++) {
+ floors[i].tables = [];
+ self.floors_by_id[floors[i].id] = floors[i];
+ }
+
+ // Make sure they display in the correct order
+ self.floors = self.floors.sort(function(a,b){ return a.sequence - b.sequence; });
+
+ // Ignore floorplan features if no floor specified.
+ self.config.iface_floorplan = !!self.floors.length;
+ },
+});
+
+// At POS Startup, after the floors are loaded, load the tables, and associate
+// them with their floor.
+models.load_models({
+ model: 'restaurant.table',
+ fields: ['name','width','height','position_h','position_v','shape','floor_id','color','seats'],
+ loaded: function(self,tables){
+ self.tables_by_id = {};
+ for (var i = 0; i < tables.length; i++) {
+ self.tables_by_id[tables[i].id] = tables[i];
+ var floor = self.floors_by_id[tables[i].floor_id[0]];
+ if (floor) {
+ floor.tables.push(tables[i]);
+ tables[i].floor = floor;
+ }
+ }
+ },
+});
+
+// New orders are now associated with the current table, if any.
+var _super_order = models.Order.prototype;
+models.Order = models.Order.extend({
+ initialize: function(attr,options) {
+ _super_order.initialize.apply(this,arguments);
+ if (!this.table && !options.json) {
+ this.table = this.pos.table;
+ }
+ this.customer_count = this.customer_count || 1;
+ this.save_to_db();
+ },
+ export_as_JSON: function() {
+ var json = _super_order.export_as_JSON.apply(this,arguments);
+ json.table = this.table ? this.table.name : undefined;
+ json.table_id = this.table ? this.table.id : false;
+ json.floor = this.table ? this.table.floor.name : false;
+ json.floor_id = this.table ? this.table.floor.id : false;
+ json.customer_count = this.customer_count;
+ return json;
+ },
+ init_from_JSON: function(json) {
+ _super_order.init_from_JSON.apply(this,arguments);
+ this.table = this.pos.tables_by_id[json.table_id];
+ this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined;
+ this.customer_count = json.customer_count || 1;
+ },
+ export_for_printing: function() {
+ var json = _super_order.export_for_printing.apply(this,arguments);
+ json.table = this.table ? this.table.name : undefined;
+ json.floor = this.table ? this.table.floor.name : undefined;
+ json.customer_count = this.get_customer_count();
+ return json;
+ },
+ get_customer_count: function(){
+ return this.customer_count;
+ },
+ set_customer_count: function(count) {
+ this.customer_count = Math.max(count,0);
+ this.trigger('change');
+ },
+});
+
+// We need to change the way the regular UI sees the orders, it
+// needs to only see the orders associated with the current table,
+// and when an order is validated, it needs to go back to the floor map.
+//
+// And when we change the table, we must create an order for that table
+// if there is none.
+var _super_posmodel = models.PosModel.prototype;
+models.PosModel = models.PosModel.extend({
+ after_load_server_data: async function() {
+ var res = await _super_posmodel.after_load_server_data.call(this);
+ if (this.config.iface_floorplan) {
+ this.table = null;
+ }
+ return res;
+ },
+
+ transfer_order_to_different_table: function () {
+ this.order_to_transfer_to_different_table = this.get_order();
+
+ // go to 'floors' screen, this will set the order to null and
+ // eventually this will cause the gui to go to its
+ // default_screen, which is 'floors'
+ this.set_table(null);
+ },
+
+ remove_from_server_and_set_sync_state: function(ids_to_remove){
+ var self = this;
+ this.set_synch('connecting', ids_to_remove.length);
+ return self._remove_from_server(ids_to_remove)
+ .then(function(server_ids) {
+ self.set_synch('connected');
+ }).catch(function(reason){
+ self.set_synch('error');
+ throw reason;
+ });
+ },
+
+ /**
+ * Request the orders of the table with given id.
+ * @param {number} table_id.
+ * @param {dict} options.
+ * @param {number} options.timeout optional timeout parameter for the rpc call.
+ * @return {Promise}
+ */
+ _get_from_server: function (table_id, options) {
+ options = options || {};
+ var timeout = typeof options.timeout === 'number' ? options.timeout : 7500;
+ return this.rpc({
+ model: 'pos.order',
+ method: 'get_table_draft_orders',
+ args: [table_id],
+ kwargs: {context: this.session.user_context},
+ }, {
+ timeout: timeout,
+ shadow: false,
+ })
+ },
+
+ transfer_order_to_table: function(table) {
+ this.order_to_transfer_to_different_table.table = table;
+ this.order_to_transfer_to_different_table.save_to_db();
+ },
+
+ push_order_for_transfer: function(order_ids, table_orders) {
+ order_ids.push(this.order_to_transfer_to_different_table.uid);
+ table_orders.push(this.order_to_transfer_to_different_table);
+ },
+
+ clean_table_transfer: function(table) {
+ if (this.order_to_transfer_to_different_table && table) {
+ this.order_to_transfer_to_different_table = null;
+ this.set_table(table);
+ }
+ },
+
+ sync_from_server: function(table, table_orders, order_ids) {
+ var self = this;
+ var ids_to_remove = this.db.get_ids_to_remove_from_server();
+ var orders_to_sync = this.db.get_unpaid_orders_to_sync(order_ids);
+ if (orders_to_sync.length) {
+ this.set_synch('connecting', orders_to_sync.length);
+ this._save_to_server(orders_to_sync, {'draft': true}).then(function (server_ids) {
+ server_ids.forEach(server_id => self.update_table_order(server_id, table_orders));
+ if (!ids_to_remove.length) {
+ self.set_synch('connected');
+ } else {
+ self.remove_from_server_and_set_sync_state(ids_to_remove);
+ }
+ }).catch(function(reason){
+ self.set_synch('error');
+ }).finally(function(){
+ self.clean_table_transfer(table);
+ });
+ } else {
+ if (ids_to_remove.length) {
+ self.remove_from_server_and_set_sync_state(ids_to_remove);
+ }
+ self.clean_table_transfer(table);
+ }
+ },
+
+ update_table_order: function(server_id, table_orders) {
+ const order = table_orders.find(o => o.name === server_id.pos_reference);
+ if (order) {
+ order.server_id = server_id.id;
+ order.save_to_db();
+ }
+ return order;
+ },
+
+ /**
+ * @param {models.Order} order order to set
+ */
+ set_order_on_table: function(order) {
+ var orders = this.get_order_list();
+ if (orders.length) {
+ order = order ? orders.find((o) => o.uid === order.uid) : null;
+ if (order) {
+ this.set_order(order);
+ } else {
+ // do not mindlessly set the first order in the list.
+ orders = orders.filter(order => !order.finalized);
+ if (orders.length) {
+ this.set_order(orders[0]);
+ } else {
+ this.add_new_order();
+ }
+ }
+ } else {
+ this.add_new_order(); // or create a new order with the current table
+ }
+ },
+
+ sync_to_server: function(table, order) {
+ var self = this;
+ var ids_to_remove = this.db.get_ids_to_remove_from_server();
+
+ this.set_synch('connecting', 1);
+ this._get_from_server(table.id).then(function (server_orders) {
+ var orders = self.get_order_list();
+ orders.forEach(function(order){
+ // We don't remove the validated orders because we still want to see them
+ // in the ticket screen. Orders in 'ReceiptScreen' or 'TipScreen' are validated
+ // orders.
+ if (order.server_id && !order.finalized){
+ self.get("orders").remove(order);
+ order.destroy();
+ }
+ });
+ server_orders.forEach(function(server_order){
+ if (server_order.lines.length){
+ var new_order = new models.Order({},{pos: self, json: server_order});
+ self.get("orders").add(new_order);
+ new_order.save_to_db();
+ }
+ })
+ if (!ids_to_remove.length) {
+ self.set_synch('connected');
+ } else {
+ self.remove_from_server_and_set_sync_state(ids_to_remove);
+ }
+ }).catch(function(reason){
+ self.set_synch('error');
+ }).finally(function(){
+ self.set_order_on_table(order);
+ });
+ },
+
+ get_order_with_uid: function() {
+ var order_ids = [];
+ this.get_order_list().forEach(function(o){
+ order_ids.push(o.uid);
+ });
+
+ return order_ids;
+ },
+
+ /**
+ * Changes the current table.
+ *
+ * Switch table and make sure all nececery syncing tasks are done.
+ * @param {object} table.
+ * @param {models.Order|undefined} order if provided, set to this order
+ */
+ set_table: function(table, order) {
+ if(!table){
+ this.sync_from_server(table, this.get_order_list(), this.get_order_with_uid());
+ this.set_order(null);
+ this.table = null;
+ } else if (this.order_to_transfer_to_different_table) {
+ var order_ids = this.get_order_with_uid();
+
+ this.transfer_order_to_table(table);
+ this.push_order_for_transfer(order_ids, this.get_order_list());
+
+ this.sync_from_server(table, this.get_order_list(), order_ids);
+ this.set_order(null);
+ } else {
+ this.table = table;
+ this.sync_to_server(table, order);
+ }
+ posbus.trigger('table-set');
+ },
+
+ // if we have tables, we do not load a default order, as the default order will be
+ // set when the user selects a table.
+ set_start_order: function() {
+ if (!this.config.iface_floorplan) {
+ _super_posmodel.set_start_order.apply(this,arguments);
+ }
+ },
+
+ // we need to prevent the creation of orders when there is no
+ // table selected.
+ add_new_order: function() {
+ if (this.config.iface_floorplan) {
+ if (this.table) {
+ return _super_posmodel.add_new_order.apply(this, arguments);
+ } else {
+ Gui.showPopup('ConfirmPopup', {
+ title: 'Unable to create order',
+ body: 'Orders cannot be created when there is no active table in restaurant mode',
+ });
+ return undefined;
+ }
+ } else {
+ return _super_posmodel.add_new_order.apply(this,arguments);
+ }
+ },
+
+
+ // get the list of unpaid orders (associated to the current table)
+ get_order_list: function() {
+ var orders = _super_posmodel.get_order_list.call(this);
+ if (!(this.config && this.config.iface_floorplan)) {
+ return orders;
+ } else if (!this.table) {
+ return [];
+ } else {
+ var t_orders = [];
+ for (var i = 0; i < orders.length; i++) {
+ if ( orders[i].table === this.table) {
+ t_orders.push(orders[i]);
+ }
+ }
+ return t_orders;
+ }
+ },
+
+ // get the list of orders associated to a table. FIXME: should be O(1)
+ get_table_orders: function(table) {
+ var orders = _super_posmodel.get_order_list.call(this);
+ var t_orders = [];
+ for (var i = 0; i < orders.length; i++) {
+ if (orders[i].table === table) {
+ t_orders.push(orders[i]);
+ }
+ }
+ return t_orders;
+ },
+
+ // get customer count at table
+ get_customer_count: function(table) {
+ var orders = this.get_table_orders(table).filter(order => !order.finalized);
+ var count = 0;
+ for (var i = 0; i < orders.length; i++) {
+ count += orders[i].get_customer_count();
+ }
+ return count;
+ },
+
+ // When we validate an order we go back to the floor plan.
+ // When we cancel an order and there is multiple orders
+ // on the table, stay on the table.
+ on_removed_order: function(removed_order,index,reason){
+ if (this.config.iface_floorplan) {
+ var order_list = this.get_order_list();
+ if (reason === 'abandon') {
+ this.db.set_order_to_remove_from_server(removed_order);
+ }
+ if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
+ this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true });
+ } else if (order_list.length === 0) {
+ this.table ? this.set_order(null) : this.set_table(null);
+ }
+ } else {
+ _super_posmodel.on_removed_order.apply(this,arguments);
+ }
+ },
+
+
+});
+
+
+var _super_paymentline = models.Paymentline.prototype;
+models.Paymentline = models.Paymentline.extend({
+ /**
+ * Override this method to be able to show the 'Adjust Authorisation' button
+ * on a validated payment_line and to show the tip screen which allow
+ * tipping even after payment. By default, this returns true for all
+ * non-cash payment.
+ */
+ canBeAdjusted: function() {
+ if (this.payment_method.payment_terminal) {
+ return this.payment_method.payment_terminal.canBeAdjusted(this.cid);
+ }
+ return !this.payment_method.is_cash_count;
+ },
+});
+
+});
diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js
new file mode 100644
index 00000000..709de43e
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/multiprint.js
@@ -0,0 +1,315 @@
+odoo.define('pos_restaurant.multiprint', function (require) {
+"use strict";
+
+var models = require('point_of_sale.models');
+var core = require('web.core');
+var Printer = require('point_of_sale.Printer').Printer;
+
+var QWeb = core.qweb;
+
+models.PosModel = models.PosModel.extend({
+ create_printer: function (config) {
+ var url = config.proxy_ip || '';
+ if(url.indexOf('//') < 0) {
+ url = window.location.protocol + '//' + url;
+ }
+ if(url.indexOf(':', url.indexOf('//') + 2) < 0 && window.location.protocol !== 'https:') {
+ url = url + ':8069';
+ }
+ return new Printer(url, this);
+ },
+});
+
+models.load_models({
+ model: 'restaurant.printer',
+ fields: ['name','proxy_ip','product_categories_ids', 'printer_type'],
+ domain: null,
+ loaded: function(self,printers){
+ var active_printers = {};
+ for (var i = 0; i < self.config.printer_ids.length; i++) {
+ active_printers[self.config.printer_ids[i]] = true;
+ }
+
+ self.printers = [];
+ self.printers_categories = {}; // list of product categories that belong to
+ // one or more order printer
+
+ for(var i = 0; i < printers.length; i++){
+ if(active_printers[printers[i].id]){
+ var printer = self.create_printer(printers[i]);
+ printer.config = printers[i];
+ self.printers.push(printer);
+
+ for (var j = 0; j < printer.config.product_categories_ids.length; j++) {
+ self.printers_categories[printer.config.product_categories_ids[j]] = true;
+ }
+ }
+ }
+ self.printers_categories = _.keys(self.printers_categories);
+ self.config.iface_printers = !!self.printers.length;
+ },
+});
+
+var _super_orderline = models.Orderline.prototype;
+
+models.Orderline = models.Orderline.extend({
+ initialize: function() {
+ _super_orderline.initialize.apply(this,arguments);
+ if (!this.pos.config.iface_printers) {
+ return;
+ }
+ if (typeof this.mp_dirty === 'undefined') {
+ // mp dirty is true if this orderline has changed
+ // since the last kitchen print
+ // it's left undefined if the orderline does not
+ // need to be printed to a printer.
+
+ this.mp_dirty = this.printable() || undefined;
+ }
+ if (!this.mp_skip) {
+ // mp_skip is true if the cashier want this orderline
+ // not to be sent to the kitchen
+ this.mp_skip = false;
+ }
+ },
+ // can this orderline be potentially printed ?
+ printable: function() {
+ return this.pos.db.is_product_in_category(this.pos.printers_categories, this.get_product().id);
+ },
+ init_from_JSON: function(json) {
+ _super_orderline.init_from_JSON.apply(this,arguments);
+ this.mp_dirty = json.mp_dirty;
+ this.mp_skip = json.mp_skip;
+ },
+ export_as_JSON: function() {
+ var json = _super_orderline.export_as_JSON.apply(this,arguments);
+ json.mp_dirty = this.mp_dirty;
+ json.mp_skip = this.mp_skip;
+ return json;
+ },
+ set_quantity: function(quantity) {
+ if (this.pos.config.iface_printers && quantity !== this.quantity && this.printable()) {
+ this.mp_dirty = true;
+ }
+ _super_orderline.set_quantity.apply(this,arguments);
+ },
+ can_be_merged_with: function(orderline) {
+ return (!this.mp_skip) &&
+ (!orderline.mp_skip) &&
+ _super_orderline.can_be_merged_with.apply(this,arguments);
+ },
+ set_skip: function(skip) {
+ if (this.mp_dirty && skip && !this.mp_skip) {
+ this.mp_skip = true;
+ this.trigger('change',this);
+ }
+ if (this.mp_skip && !skip) {
+ this.mp_dirty = true;
+ this.mp_skip = false;
+ this.trigger('change',this);
+ }
+ },
+ set_dirty: function(dirty) {
+ if (this.mp_dirty !== dirty) {
+ this.mp_dirty = dirty;
+ this.trigger('change', this);
+ }
+ },
+ get_line_diff_hash: function(){
+ if (this.get_note()) {
+ return this.id + '|' + this.get_note();
+ } else {
+ return '' + this.id;
+ }
+ },
+});
+
+var _super_order = models.Order.prototype;
+models.Order = models.Order.extend({
+ build_line_resume: function(){
+ var resume = {};
+ this.orderlines.each(function(line){
+ if (line.mp_skip) {
+ return;
+ }
+ var line_hash = line.get_line_diff_hash();
+ var qty = Number(line.get_quantity());
+ var note = line.get_note();
+ var product_id = line.get_product().id;
+
+ if (typeof resume[line_hash] === 'undefined') {
+ resume[line_hash] = {
+ qty: qty,
+ note: note,
+ product_id: product_id,
+ product_name_wrapped: line.generate_wrapped_product_name(),
+ };
+ } else {
+ resume[line_hash].qty += qty;
+ }
+
+ });
+ return resume;
+ },
+ saveChanges: function(){
+ this.saved_resume = this.build_line_resume();
+ this.orderlines.each(function(line){
+ line.set_dirty(false);
+ });
+ this.trigger('change',this);
+ },
+ computeChanges: function(categories){
+ var current_res = this.build_line_resume();
+ var old_res = this.saved_resume || {};
+ var json = this.export_as_JSON();
+ var add = [];
+ var rem = [];
+ var line_hash;
+
+ for ( line_hash in current_res) {
+ var curr = current_res[line_hash];
+ var old = {};
+ var found = false;
+ for(var id in old_res) {
+ if(old_res[id].product_id === curr.product_id){
+ found = true;
+ old = old_res[id];
+ break;
+ }
+ }
+
+ if (!found) {
+ add.push({
+ 'id': curr.product_id,
+ 'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
+ 'note': curr.note,
+ 'qty': curr.qty,
+ });
+ } else if (old.qty < curr.qty) {
+ add.push({
+ 'id': curr.product_id,
+ 'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
+ 'note': curr.note,
+ 'qty': curr.qty - old.qty,
+ });
+ } else if (old.qty > curr.qty) {
+ rem.push({
+ 'id': curr.product_id,
+ 'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
+ 'name_wrapped': curr.product_name_wrapped,
+ 'note': curr.note,
+ 'qty': old.qty - curr.qty,
+ });
+ }
+ }
+
+ for (line_hash in old_res) {
+ var found = false;
+ for(var id in current_res) {
+ if(current_res[id].product_id === old_res[line_hash].product_id)
+ found = true;
+ }
+ if (!found) {
+ var old = old_res[line_hash];
+ rem.push({
+ 'id': old.product_id,
+ 'name': this.pos.db.get_product_by_id(old.product_id).display_name,
+ 'name_wrapped': old.product_name_wrapped,
+ 'note': old.note,
+ 'qty': old.qty,
+ });
+ }
+ }
+
+ if(categories && categories.length > 0){
+ // filter the added and removed orders to only contains
+ // products that belong to one of the categories supplied as a parameter
+
+ var self = this;
+
+ var _add = [];
+ var _rem = [];
+
+ for(var i = 0; i < add.length; i++){
+ if(self.pos.db.is_product_in_category(categories,add[i].id)){
+ _add.push(add[i]);
+ }
+ }
+ add = _add;
+
+ for(var i = 0; i < rem.length; i++){
+ if(self.pos.db.is_product_in_category(categories,rem[i].id)){
+ _rem.push(rem[i]);
+ }
+ }
+ rem = _rem;
+ }
+
+ var d = new Date();
+ var hours = '' + d.getHours();
+ hours = hours.length < 2 ? ('0' + hours) : hours;
+ var minutes = '' + d.getMinutes();
+ minutes = minutes.length < 2 ? ('0' + minutes) : minutes;
+
+ return {
+ 'new': add,
+ 'cancelled': rem,
+ 'table': json.table || false,
+ 'floor': json.floor || false,
+ 'name': json.name || 'unknown order',
+ 'time': {
+ 'hours': hours,
+ 'minutes': minutes,
+ },
+ };
+
+ },
+ printChanges: async function(){
+ var printers = this.pos.printers;
+ let isPrintSuccessful = true;
+ for(var i = 0; i < printers.length; i++){
+ var changes = this.computeChanges(printers[i].config.product_categories_ids);
+ if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
+ var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this});
+ const result = await printers[i].print_receipt(receipt);
+ if (!result.successful) {
+ isPrintSuccessful = false;
+ }
+ }
+ }
+ return isPrintSuccessful;
+ },
+ hasChangesToPrint: function(){
+ var printers = this.pos.printers;
+ for(var i = 0; i < printers.length; i++){
+ var changes = this.computeChanges(printers[i].config.product_categories_ids);
+ if ( changes['new'].length > 0 || changes['cancelled'].length > 0){
+ return true;
+ }
+ }
+ return false;
+ },
+ hasSkippedChanges: function() {
+ var orderlines = this.get_orderlines();
+ for (var i = 0; i < orderlines.length; i++) {
+ if (orderlines[i].mp_skip) {
+ return true;
+ }
+ }
+ return false;
+ },
+ export_as_JSON: function(){
+ var json = _super_order.export_as_JSON.apply(this,arguments);
+ json.multiprint_resume = JSON.stringify(this.saved_resume);
+ return json;
+ },
+ init_from_JSON: function(json){
+ _super_order.init_from_JSON.apply(this,arguments);
+ this.saved_resume = json.multiprint_resume && JSON.parse(json.multiprint_resume);
+ },
+});
+
+
+});
diff --git a/addons/pos_restaurant/static/src/js/notes.js b/addons/pos_restaurant/static/src/js/notes.js
new file mode 100644
index 00000000..c2ed95dd
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/notes.js
@@ -0,0 +1,42 @@
+odoo.define('pos_restaurant.notes', function (require) {
+"use strict";
+
+var models = require('point_of_sale.models');
+
+var _super_orderline = models.Orderline.prototype;
+models.Orderline = models.Orderline.extend({
+ initialize: function(attr, options) {
+ _super_orderline.initialize.call(this,attr,options);
+ this.note = this.note || "";
+ },
+ set_note: function(note){
+ this.note = note;
+ this.trigger('change',this);
+ },
+ get_note: function(note){
+ return this.note;
+ },
+ can_be_merged_with: function(orderline) {
+ if (orderline.get_note() !== this.get_note()) {
+ return false;
+ } else {
+ return _super_orderline.can_be_merged_with.apply(this,arguments);
+ }
+ },
+ clone: function(){
+ var orderline = _super_orderline.clone.call(this);
+ orderline.note = this.note;
+ return orderline;
+ },
+ export_as_JSON: function(){
+ var json = _super_orderline.export_as_JSON.call(this);
+ json.note = this.note;
+ return json;
+ },
+ init_from_JSON: function(json){
+ _super_orderline.init_from_JSON.apply(this,arguments);
+ this.note = json.note;
+ },
+});
+
+});
diff --git a/addons/pos_restaurant/static/src/js/payment.js b/addons/pos_restaurant/static/src/js/payment.js
new file mode 100644
index 00000000..355d6742
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/payment.js
@@ -0,0 +1,24 @@
+odoo.define('pos_restaurant.PaymentInterface', function (require) {
+ "use strict";
+
+ var PaymentInterface = require('point_of_sale.PaymentInterface');
+
+ PaymentInterface.include({
+ /**
+ * Return true if the amount that was authorized can be modified,
+ * false otherwise
+ * @param {string} cid - The id of the paymentline
+ */
+ canBeAdjusted(cid) {
+ return false;
+ },
+
+ /**
+ * Called when the amount authorized by a payment request should
+ * be adjusted to account for a new order line, it can only be called if
+ * canBeAdjusted returns True
+ * @param {string} cid - The id of the paymentline
+ */
+ send_payment_adjust: function (cid) {},
+ });
+});