diff options
| author | Joseph Caburnay <jcb@odoo.com> | 2020-05-05 07:50:04 +0000 |
|---|---|---|
| committer | Pierre Masereel <pim@odoo.com> | 2020-05-12 12:48:58 +0000 |
| commit | d8726778b670611326851d9baa980edbd21b3b09 (patch) | |
| tree | 0dd236dd942cd65b224a208f14ce9c099bf35c72 /addons | |
| parent | c6397ab3b3654da336a4269a81d5d91016baf520 (diff) | |
[REF] pos_restaurant: migration to OWL framework
Diffstat (limited to 'addons')
64 files changed, 2838 insertions, 1559 deletions
diff --git a/addons/pos_restaurant/__manifest__.py b/addons/pos_restaurant/__manifest__.py index dc095174469..b6c573586e1 100644 --- a/addons/pos_restaurant/__manifest__.py +++ b/addons/pos_restaurant/__manifest__.py @@ -26,11 +26,26 @@ This module adds several features to the Point of Sale that are specific to rest 'views/pos_restaurant_templates.xml', ], 'qweb': [ + 'static/src/xml/Resizeable.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/SplitBillButton.xml', + 'static/src/xml/Screens/ProductScreen/ControlButtons/TransferOrderButton.xml', + 'static/src/xml/Screens/BillScreen.xml', + 'static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml', + 'static/src/xml/Screens/SplitBillScreen/SplitOrderline.xml', + 'static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml', + 'static/src/xml/Screens/ProductScreen/Orderline.xml', + 'static/src/xml/Screens/FloorScreen/FloorScreen.xml', + 'static/src/xml/Screens/FloorScreen/EditBar.xml', + 'static/src/xml/Screens/FloorScreen/TableWidget.xml', + 'static/src/xml/Screens/FloorScreen/EditableTable.xml', + 'static/src/xml/ChromeWidgets/OrderSelector.xml', + 'static/src/xml/ChromeWidgets/BackToFloorButton.xml', + 'static/src/xml/Chrome.xml', 'static/src/xml/multiprint.xml', - 'static/src/xml/splitbill.xml', - 'static/src/xml/printbill.xml', - 'static/src/xml/notes.xml', - 'static/src/xml/floors.xml', ], 'demo': [ 'data/pos_restaurant_demo.xml', diff --git a/addons/pos_restaurant/static/src/css/restaurant.css b/addons/pos_restaurant/static/src/css/restaurant.css index b17a3002ce6..6dccbc2de50 100644 --- a/addons/pos_restaurant/static/src/css/restaurant.css +++ b/addons/pos_restaurant/static/src/css/restaurant.css @@ -111,7 +111,6 @@ overflow: hidden; position: relative; bottom: 5px; - z-index: 5; } .floor-map .table.selected { outline: solid rgba(255,255,255,0.3); @@ -166,7 +165,7 @@ } .floor-map .edit-bar .color-picker { position: absolute; - left: -106px; + left: 36px; top: 40px; width: 180px; height: 180px; @@ -209,26 +208,24 @@ } .floor-map .table.selected .table-handle { + padding: 0px; position: absolute; width: 48px; height: 48px; left: 50%; top: 50%; border-radius: 24px; - margin-left: -24px; - margin-top: -24px; background: white; box-shadow: 0px 2px 3px rgba(0,0,0,0.2); cursor: grab; transition: all 150ms linear; z-index: 100; + transform: translate(-50%, -50%); } .floor-map .table.selected .table-handle:hover { width: 60px; height: 60px; border-radius: 30px; - margin-left: -30px; - margin-top: -30px; } .floor-map .table .table-handle.top { top: 0; } .floor-map .table .table-handle.bottom { top: 100%; } 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 00000000000..cff845b2650 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Chrome.js @@ -0,0 +1,88 @@ +odoo.define('pos_restaurant.chrome', function(require) { + 'use strict'; + + const Chrome = require('point_of_sale.Chrome'); + const { onWillUnmount } = owl.hooks; + const Registries = require('point_of_sale.Registries'); + + 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.floor }); + } else { + super._showSavedScreen(pos, newSelectedOrder); + } + } + _setActivityListeners() { + const events = 'mousemove mousedown touchstart click scroll keypress'.split(' '); + const boundHandler = this._setIdleTimer.bind(this); + for (let eventName of events) { + this.el.addEventListener(eventName, boundHandler); + onWillUnmount(() => { + this.el.removeEventListener(eventName, boundHandler); + }); + } + } + _setIdleTimer() { + if (this._shouldResetIdleTimer()) { + clearTimeout(this.idleTimer); + this.idleTimer = setTimeout(() => { + this._actionAfterIdle(); + }, 60000); + } + } + _actionAfterIdle() { + this.showScreen('FloorScreen', { floor: this.env.pos.table.floor }); + } + _shouldResetIdleTimer() { + return ( + this.env.pos.config.iface_floorplan && + this.mainScreen.name !== 'FloorScreen' && + !this.tempScreen.isShown + ); + } + __showScreen() { + super.__showScreen(...arguments); + this._setIdleTimer(); + } + }; + + 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 00000000000..dbd21c5fd32 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js @@ -0,0 +1,28 @@ +odoo.define('pos_restaurant.BackToFloorButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class BackToFloorButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this._backToFloorScreen); + } + get table() { + return this.props.table; + } + get floor() { + return this.props.table.floor; + } + async _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/OrderSelector.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js new file mode 100644 index 00000000000..255b84db073 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js @@ -0,0 +1,20 @@ +odoo.define('pos_restaurant.OrderSelector', function(require) { + 'use strict'; + + const OrderSelector = require('point_of_sale.OrderSelector'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderSelector = OrderSelector => + class extends OrderSelector { + get currentTable() { + return this.env.pos.table; + } + get backToFloorButtonIsShown() { + return this.env.pos.config.iface_floorplan && this.currentTable; + } + }; + + Registries.Component.extend(OrderSelector, PosResOrderSelector); + + return OrderSelector; +}); 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 00000000000..a5bfe243c9a --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Resizeable.js @@ -0,0 +1,302 @@ +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); + + 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`; + } + event.preventDefault(); + } + 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`; + } + event.preventDefault(); + } + 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`; + } + event.preventDefault(); + } + 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`; + } + event.preventDefault(); + } + 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 00000000000..7b7c5b0f8b9 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js @@ -0,0 +1,21 @@ +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'); + } + } + 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 00000000000..43b16d93c3e --- /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 00000000000..4aeb781d712 --- /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 00000000000..aaf929ed0fe --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -0,0 +1,296 @@ +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, + }); + this.floorMapRef = useRef('floor-map-ref'); + } + patched() { + this.floorMapRef.el.style.background = this.state.floorBackground; + } + mounted() { + this.env.pos.set_table(null); + this.floorMapRef.el.style.background = this.state.floorBackground; + // 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() { + if (!this.selectedTable) return; + const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { + startingValue: this.selectedTable.seats, + cheap: true, + title: this.env._t('Number of Seats ?'), + }); + if (!confirmed) return; + const newSeatsNum = parseInt(inputNumber, 10) || this.selectedTable.seats; + if (newSeatsNum !== this.selectedTable.seats) { + this.selectedTable.seats = newSeatsNum; + await this._save(this.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() { + if (!this.selectedTable) return; + const { confirmed, payload: newName } = await this.showPopup('TextInputPopup', { + startingValue: this.selectedTable.name, + title: this.env._t('Table Name ?'), + }); + if (!confirmed) return; + if (newName !== this.selectedTable.name) { + this.selectedTable.name = newName; + await this._save(this.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 { + await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [{ active: false, id: this.state.selectedTableId }], + }); + this.activeFloor.tables = this.activeTables.filter( + (table) => table.id !== this.state.selectedTableId + ); + 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; + } + } + } + async _onSelectTable(event) { + const table = event.detail; + if (this.state.isEditMode) { + this.state.selectedTableId = table.id; + } else { + await 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) + ).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'; + + 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 00000000000..48dfdad71e8 --- /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/ProductScreen/ControlButtons/OrderlineNoteButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js new file mode 100644 index 00000000000..58681810cb0 --- /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 00000000000..7563d9fc6ea --- /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 00000000000..58ace925080 --- /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 00000000000..948410e5d08 --- /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 00000000000..e015496d47a --- /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 00000000000..ed72e2a7119 --- /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 00000000000..2204405cd2d --- /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 00000000000..689c3bf40ca --- /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 00000000000..1c5613b3166 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -0,0 +1,164 @@ +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('screen', { 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('screen', { 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] = { quantity: 0 }; + } + return splitlines; + } + _splitQuantity(line) { + const split = this.splitlines[line.id]; + 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 < line.get_quantity()) { + 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() { + return _.every(this.currentOrder.get_orderlines(), orderLine => { + var split = this.splitlines[orderLine.id]; + return split && split.quantity === orderLine.get_quantity(); + }); + } + _setQuantityOnCurrentOrder() { + for (var id in this.splitlines) { + var split = this.splitlines[id]; + var line = this.currentOrder.get_orderline(parseInt(id)); + 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); + } + } + } + _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 00000000000..51c0b9296f8 --- /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/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 0a4f711a868..1276fea9f8b 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -1,17 +1,7 @@ odoo.define('pos_restaurant.floors', function (require) { "use strict"; -var PosBaseWidget = require('point_of_sale.BaseWidget'); -var chrome = require('point_of_sale.chrome'); -var gui = require('point_of_sale.gui'); var models = require('point_of_sale.models'); -var screens = require('point_of_sale.screens'); -var core = require('web.core'); -var rpc = require('web.rpc'); -var session = require('web.session'); - -var QWeb = core.qweb; -var _t = core._t; // At POS Startup, load the floors, and add them to the pos model models.load_models({ @@ -52,707 +42,6 @@ models.load_models({ }, }); -// The Table GUI element, should always be a child of the FloorScreenWidget -var TableWidget = PosBaseWidget.extend({ - template: 'TableWidget', - init: function(parent, options){ - this._super(parent, options); - this.table = options.table; - this.selected = false; - this.moved = false; - this.dragpos = {x:0, y:0}; - }, - // computes the absolute position of a DOM mouse event, used - // when resizing tables - event_position: function(event){ - if(event.touches && event.touches[0]){ - return {x: event.touches[0].screenX, y: event.touches[0].screenY}; - }else{ - return {x: event.screenX, y: event.screenY}; - } - }, - // when a table is clicked, go to the table's orders - // but if we're editing, we select/deselect it. - click_handler: function(){ - var self = this; - var floorplan = this.getParent(); - if (floorplan.editing) { - setTimeout(function(){ // in a setTimeout to debounce with drag&drop start - if (!self.dragging) { - if (self.moved) { - self.moved = false; - } else if (!self.selected) { - self.getParent().select_table(self); - } else { - self.getParent().deselect_tables(); - } - } - },50); - } else { - floorplan.pos.set_table(this.table); - } - }, - // drag and drop for moving the table, at drag start - dragstart_handler: function(event, ui){ - this.dragging = true; - }, - // drag and drop for moving the table, at drag end - dragend_handler: function(event, ui){ - this.dragging = false; - this.moved = true; - this.table.position_h = ui.position.left - this.table.width/2; - this.table.position_v = ui.position.top - this.table.height/2; - this.$el.css(this.table_style()); - }, - // drag and dropping the resizing handles - handle_dragmove_handler: function(event, ui) { - this.moved = true; - this.table.width = ui.size.width; - this.table.height = ui.size.height; - this.table.position_h = ui.position.left - ui.originalSize.width/2; - this.table.position_v = ui.position.top - ui.originalSize.height/2; - this.$el.css(this.table_style()); - }, - set_table_color: function(color){ - this.table.color = _.escape(color); - this.$el.css({'background': this.table.color}); - }, - set_table_name: function(name){ - if (name) { - this.table.name = name; - this.renderElement(); - } - }, - set_table_seats: function(seats){ - if (seats) { - this.table.seats = Number(seats); - this.renderElement(); - } - }, - // The table's positioning is handled via css absolute positioning, - // which is handled here. - table_style: function(){ - var table = this.table; - function unit(val){ return '' + val + 'px'; } - var style = { - 'width': unit(table.width), - 'height': unit(table.height), - 'line-height': unit(table.height), - 'margin-left': unit(-table.width/2), - 'margin-top': unit(-table.height/2), - 'top': unit(table.position_v + table.height/2), - 'left': unit(table.position_h + table.width/2), - 'border-radius': table.shape === 'round' ? - unit(Math.max(table.width,table.height)/2) : '3px', - }; - if (table.color) { - style.background = table.color; - } - if (table.height >= 150 && table.width >= 150) { - style['font-size'] = '32px'; - } - - return style; - }, - // convert the style dictionary to a ; separated string for inclusion in templates - table_style_str: function(){ - var style = this.table_style(); - var str = ""; - var s; - for (s in style) { - str += s + ":" + style[s] + "; "; - } - return str; - }, - // select the table (should be called via the floorplan) - select: function() { - this.selected = true; - this.renderElement(); - - this.$el.resizable({ - handles: 'all', - resize: this.handle_dragmove_handler.bind(this), - }); - - this.$el.draggable({ - stop: this.dragend_handler.bind(this), - }); - }, - // deselect the table (should be called via the floorplan) - deselect: function() { - this.selected = false; - this.renderElement(); - this.save_changes(); - }, - // sends the table's modification to the server - save_changes: function(){ - var self = this; - var fields = _.find(this.pos.models,function(model){ return model.model === 'restaurant.table'; }).fields; - - // we need a serializable copy of the table, containing only the fields defined on the server - var serializable_table = {}; - for (var i = 0; i < fields.length; i++) { - if (typeof this.table[fields[i]] !== 'undefined') { - serializable_table[fields[i]] = this.table[fields[i]]; - } - } - // and the id ... - serializable_table.id = this.table.id; - - rpc.query({ - model: 'restaurant.table', - method: 'create_from_ui', - args: [serializable_table], - }) - .then(function (table_id){ - rpc.query({ - model: 'restaurant.table', - method: 'search_read', - args: [[['id', '=', table_id]], fields], - limit: 1, - }) - .then(function (result){ - var table = result[0]; - for (var field in table) { - self.table[field] = table[field]; - } - self.pos.tables_by_id[table.id] = self.table; - // If selected, render with drag and resize event handlers. - if (!self.selected) { - self.renderElement(); - } else { - self.select(); - } - }); - }, function(type,err) { - self.gui.show_sync_error_popup(); - }); - }, - // destroy the table. We do not really destroy it, we set it - // to inactive so that it doesn't show up anymore, but it still - // available on the database for the orders that depend on it. - trash: function(){ - var self = this; - rpc.query({ - model: 'restaurant.table', - method: 'create_from_ui', - args: [{'active':false,'id':this.table.id}], - }) - .then(function (table_id){ - // Removing all references from the table and the table_widget in in the UI ... - for (var i = 0; i < self.pos.floors.length; i++) { - var floor = self.pos.floors[i]; - for (var j = 0; j < floor.tables.length; j++) { - if (floor.tables[j].id === table_id) { - floor.tables.splice(j,1); - break; - } - } - } - var floorplan = self.getParent(); - for (var i = 0; i < floorplan.table_widgets.length; i++) { - if (floorplan.table_widgets[i] === self) { - floorplan.table_widgets.splice(i,1); - } - } - if (floorplan.selected_table === self) { - floorplan.selected_table = null; - } - floorplan.update_toolbar(); - self.destroy(); - }, function(type, err) { - self.gui.show_sync_error_popup(); - }); - }, - get_notifications: function(){ //FIXME : Make this faster - var orders = this.pos.get_table_orders(this.table); - var notifications = {}; - for (var i = 0; i < orders.length; i++) { - if (orders[i].hasChangesToPrint()) { - notifications.printing = true; - break; - } else if (orders[i].hasSkippedChanges()) { - notifications.skipped = true; - } - } - return notifications; - }, - update_click_handlers: function(editing){ - var self = this; - this.$el.off('mouseup touchend touchcancel click dragend'); - - if (editing) { - this.$el.on('mouseup touchend touchcancel', function(event){ self.click_handler(event,$(this)); }); - } else { - this.$el.on('click dragend', function(event){ self.click_handler(event,$(this)); }); - } - }, - renderElement: function(){ - var self = this; - this.order_count = this.table.order_count !== undefined ? - this.table.order_count : - this.pos.get_table_orders(this.table) - .filter(o => o.orderlines.length !== 0 || o.paymentlines.length !==0).length; - this.customer_count = this.pos.get_customer_count(this.table); - this.fill = Math.min(1,Math.max(0,this.customer_count / this.table.seats)); - this.notifications = this.get_notifications(); - this._super(); - - this.update_click_handlers(); - }, -}); - -// The screen that allows you to select the floor, see and select the table, -// as well as edit them. -var FloorScreenWidget = screens.ScreenWidget.extend({ - template: 'FloorScreenWidget', - - // Ignore products, discounts, and client barcodes - barcode_product_action: function(code){}, - barcode_discount_action: function(code){}, - barcode_client_action: function(code){}, - - init: function(parent, options) { - this._super(parent, options); - this.floor = this.pos.floors[0]; - this.table_widgets = []; - this.selected_table = null; - this.editing = false; - }, - _table_longpolling: function(){ - if (this.editing) { - return; - } - var self = this; - rpc.query({ - model: 'pos.config', - method: 'get_tables_order_count', - args: [this.pos.config.id], - }) - .then(function (result){ - result.forEach(function(table){ - var table_obj = self.pos.tables_by_id[table.id]; - var unsynced_orders = self.pos.get_table_orders(table_obj) - .filter(o => o.server_id === undefined && - (o.orderlines.length !== 0 || o.paymentlines.length !== 0)).length - table_obj.order_count = table.orders + unsynced_orders; - }); - self.table_widgets.forEach( - function(tw){ - tw.renderElement(); - }); - }, function(type,err) { - self.table_widgets.forEach( - function(tw){ - tw.table.order_count = self.pos.get_table_orders(tw.table) - .filter(o => o.orderlines.length !== 0 || o.paymentlines.length !== 0).length - tw.renderElement(); - }); - }); - }, - hide: function(){ - this._super(); - if (this.editing) { - this.toggle_editing(); - } - this.chrome.widget.order_selector.show(); - clearInterval(this.table_longpolling); - }, - show: function(){ - this._super(); - this.chrome.widget.order_selector.hide(); - for (var i = 0; i < this.table_widgets.length; i++) { - this.table_widgets[i].renderElement(); - } - this.check_empty_floor(); - - this._table_longpolling(); - this.table_longpolling = setInterval(this._table_longpolling.bind(this), 5000); - }, - click_floor_button: function(event,$el){ - var floor = this.pos.floors_by_id[$el.data('id')]; - if (floor !== this.floor) { - if (this.editing) { - this.toggle_editing(); - } - this.floor = floor; - this.selected_table = null; - this.renderElement(); - this.check_empty_floor(); - } - }, - background_image_url: function(floor) { - return '/web/image?model=restaurant.floor&id='+floor.id+'&field=background_image'; - }, - get_floor_style: function() { - var style = ""; - if (this.floor.background_image) { - style += "background-image: url(" + this.background_image_url(this.floor) + "); "; - } - if (this.floor.background_color) { - style += "background-color: " + _.escape(this.floor.background_color) + ";"; - } - return style; - }, - set_background_color: function(background) { - var self = this; - this.floor.background_color = background; - rpc.query({ - model: 'restaurant.floor', - method: 'write', - args: [[this.floor.id], {'background_color': background}], - }) - .guardedCatch(function (){ - self.gui.show_sync_error_popup(); - }); - this.$('.floor-map').css({"background-color": _.escape(background)}); - }, - deselect_tables: function(){ - for (var i = 0; i < this.table_widgets.length; i++) { - var table = this.table_widgets[i]; - if (table.selected) { - table.deselect(); - } - } - this.selected_table = null; - this.update_toolbar(); - }, - select_table: function(table_widget){ - if (!table_widget.selected) { - this.deselect_tables(); - table_widget.select(); - this.selected_table = table_widget; - this.update_toolbar(); - } - }, - tool_shape_action: function(){ - if (this.selected_table) { - var table = this.selected_table.table; - if (table.shape === 'square') { - table.shape = 'round'; - } else { - table.shape = 'square'; - } - this.selected_table.renderElement(); - this.update_toolbar(); - } - }, - tool_colorpicker_open: function(){ - this.$('.color-picker').addClass('oe_hidden'); - if (this.selected_table) { - this.$('.color-picker.fg-picker').removeClass('oe_hidden'); - } else { - this.$('.color-picker.bg-picker').removeClass('oe_hidden'); - } - }, - tool_colorpicker_pick: function(event,$el){ - if (this.selected_table) { - this.selected_table.set_table_color($el[0].style['background-color']); - } else { - this.set_background_color($el[0].style['background-color']); - } - }, - tool_colorpicker_close: function(){ - this.$('.color-picker').addClass('oe_hidden'); - }, - tool_rename_table: function(){ - var self = this; - if (this.selected_table) { - this.gui.show_popup('textinput',{ - 'title':_t('Table Name ?'), - 'value': this.selected_table.table.name, - 'confirm': function(value) { - self.selected_table.set_table_name(value); - }, - }); - } - }, - tool_change_seats: function(){ - var self = this; - if (this.selected_table) { - this.gui.show_popup('number',{ - 'title':_t('Number of Seats ?'), - 'cheap': true, - 'value': this.selected_table.table.seats, - 'confirm': function(value) { - self.selected_table.set_table_seats(value); - }, - }); - } - }, - tool_duplicate_table: function(){ - if (this.selected_table) { - var tw = this.create_table(this.selected_table.table); - tw.table.position_h += 10; - tw.table.position_v += 10; - tw.save_changes(); - this.select_table(tw); - } - }, - tool_new_table: function(){ - var tw = this.create_table({ - 'position_v': 100, - 'position_h': 100, - 'width': 75, - 'height': 75, - 'shape': 'square', - 'seats': 1, - }); - tw.save_changes(); - this.select_table(tw); - this.check_empty_floor(); - }, - new_table_name: function(name){ - if (name) { - var num = Number((name.match(/\d+/g) || [])[0] || 0); - var str = (name.replace(/\d+/g,'')); - var n = {num: num, str:str}; - n.num += 1; - this.last_name = n; - } else if (this.last_name) { - this.last_name.num += 1; - } else { - this.last_name = {num: 1, str:'T'}; - } - return '' + this.last_name.str + this.last_name.num; - }, - create_table: function(params) { - var table = {}; - for (var p in params) { - table[p] = params[p]; - } - - table.name = this.new_table_name(params.name); - - delete table.id; - table.floor_id = [this.floor.id,'']; - table.floor = this.floor; - - this.floor.tables.push(table); - var tw = new TableWidget(this,{table: table}); - tw.appendTo('.floor-map .tables'); - this.table_widgets.push(tw); - return tw; - }, - tool_trash_table: function(){ - var self = this; - if (this.selected_table) { - this.gui.show_popup('confirm',{ - 'title': _t('Are you sure ?'), - 'comment':_t('Removing a table cannot be undone'), - 'confirm': function(){ - self.selected_table.trash(); - }, - }); - } - }, - toggle_editing: function(){ - this.editing = !this.editing; - this.update_toolbar(); - this.update_table_click_handlers(); - - if (!this.editing) { - this.deselect_tables(); - } - }, - update_table_click_handlers: function(){ - for (var i = 0; i < this.table_widgets.length; ++i) { - if (this.editing) { - this.table_widgets[i].update_click_handlers("editing"); - } else { - this.table_widgets[i].update_click_handlers(); - } - } - }, - check_empty_floor: function(){ - if (!this.floor.tables.length) { - if (!this.editing) { - this.toggle_editing(); - } - this.$('.empty-floor').removeClass('oe_hidden'); - } else { - this.$('.empty-floor').addClass('oe_hidden'); - } - }, - update_toolbar: function(){ - - if (this.editing) { - this.$('.edit-bar').removeClass('oe_hidden'); - this.$('.edit-button.editing').addClass('active'); - } else { - this.$('.edit-bar').addClass('oe_hidden'); - this.$('.edit-button.editing').removeClass('active'); - } - - if (this.selected_table) { - this.$('.needs-selection').removeClass('disabled'); - var table = this.selected_table.table; - if (table.shape === 'square') { - this.$('.button-option.square').addClass('oe_hidden'); - this.$('.button-option.round').removeClass('oe_hidden'); - } else { - this.$('.button-option.square').removeClass('oe_hidden'); - this.$('.button-option.round').addClass('oe_hidden'); - } - } else { - this.$('.needs-selection').addClass('disabled'); - } - this.tool_colorpicker_close(); - }, - renderElement: function(){ - var self = this; - - // cleanup table widgets from previous renders - for (var i = 0; i < this.table_widgets.length; i++) { - this.table_widgets[i].destroy(); - } - - this.table_widgets = []; - - this._super(); - - for (var i = 0; i < this.floor.tables.length; i++) { - var tw = new TableWidget(this,{ - table: this.floor.tables[i], - }); - tw.appendTo(this.$('.floor-map .tables')); - this.table_widgets.push(tw); - } - - $('body').on('keyup', function (event) { - if (event.which === $.ui.keyCode.ESCAPE) { - if(self.editing) { - self.toggle_editing(); - } - } - }); - - this.$('.floor-selector .button').click(function(event){ - self.click_floor_button(event,$(this)); - }); - - this.$('.edit-button.shape').click(function(){ - self.tool_shape_action(); - }); - - this.$('.edit-button.color').click(function(){ - self.tool_colorpicker_open(); - }); - - this.$('.edit-button.dup-table').click(function(){ - self.tool_duplicate_table(); - }); - - this.$('.edit-button.new-table').click(function(){ - self.tool_new_table(); - }); - - this.$('.edit-button.rename').click(function(){ - self.tool_rename_table(); - }); - - this.$('.edit-button.seats').click(function(){ - self.tool_change_seats(); - }); - - this.$('.edit-button.trash').click(function(){ - self.tool_trash_table(); - }); - - this.$('.color-picker .close-picker').click(function(event){ - self.tool_colorpicker_close(); - event.stopPropagation(); - }); - - this.$('.color-picker .color').click(function(event){ - self.tool_colorpicker_pick(event,$(this)); - event.stopPropagation(); - }); - - this.$('.edit-button.editing').click(function(){ - self.toggle_editing(); - }); - - this.$('.floor-map,.floor-map .tables').click(function(event){ - if (event.target === self.$('.floor-map')[0] || - event.target === self.$('.floor-map .tables')[0]) { - self.deselect_tables(); - } - }); - - this.$('.color-picker .close-picker').click(function(event){ - self.tool_colorpicker_close(); - event.stopPropagation(); - }); - - this.update_toolbar(); - - }, -}); - -screens.ProductScreenWidget.include({ - show: function () { - var self = this; - this._super(); - if (this.pos.config.iface_floorplan) { - this.$el.bind("mousemove mousedown touchstart click scroll keypress", function () { - self.set_idle_timer(); - }); - this.set_idle_timer(); - } - }, - hide: function () { - this._super(); - if (this.pos.config.iface_floorplan) { - this.$el.unbind("mousemove mousedown touchstart click scroll keypress"); - clearTimeout(this.idle_timer); - } - }, - /** - * Set a timeout to go back to the floorplan and clear the previous timeout - * - * @param {number} timeout, optional timeout in miliseconds, default one minute. - */ - set_idle_timer: function(timeout=60000) { - var self = this; - clearTimeout(this.idle_timer); - this.idle_timer = setTimeout(function () { - self.pos.set_table(null); - }, timeout); - }, -}); - -gui.define_screen({ - 'name': 'floors', - 'widget': FloorScreenWidget, - 'condition': function(){ - return this.pos.config.iface_floorplan; - }, -}); - -gui.Gui.include({ - show_sync_error_popup: function() { - if (this.show_sync_errors) { - this.show_popup('error-sync',{ - 'title':_t('Changes could not be saved'), - 'body': _t('You must be connected to the internet to save your changes.\n\n' + - 'Changes made to previously synced orders will get lost at the next sync.\n' + - 'Orders that where not synced before will be synced next time you open and close the same table.'), - }); - } - }, -}); - -// Add the FloorScreen to the GUI, and set it as the default screen -chrome.Chrome.include({ - build_widgets: function(){ - this._super(); - if (this.pos.config.iface_floorplan) { - this.gui.set_startup_screen('floors'); - } - }, -}); - // New orders are now associated with the current table, if any. var _super_order = models.Order.prototype; models.Order = models.Order.extend({ @@ -795,37 +84,6 @@ models.Order = models.Order.extend({ }, }); -// We need to modify the OrderSelector to hide itself when we're on -// the floor plan -chrome.OrderSelectorWidget.include({ - floor_button_click_handler: function(){ - this.pos.set_table(null); - }, - hide: function(){ - this.$el.addClass('oe_invisible'); - }, - show: function(){ - this.$el.removeClass('oe_invisible'); - }, - renderElement: function(){ - var self = this; - this._super(); - if (this.pos.config.iface_floorplan) { - if (this.pos.get_order()) { - if (this.pos.table && this.pos.table.floor) { - this.$('.orders').prepend(QWeb.render('BackToFloorButton',{table: this.pos.table, floor:this.pos.table.floor})); - this.$('.floor-button').click(function(){ - self.floor_button_click_handler(); - }); - } - this.$el.removeClass('oe_invisible'); - } else { - this.$el.addClass('oe_invisible'); - } - } - }, -}); - // 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. @@ -834,8 +92,8 @@ chrome.OrderSelectorWidget.include({ // if there is none. var _super_posmodel = models.PosModel.prototype; models.PosModel = models.PosModel.extend({ - after_load_server_data: function() { - var res = _super_posmodel.after_load_server_data.call(this); + 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; } @@ -854,11 +112,12 @@ models.PosModel = models.PosModel.extend({ remove_from_server_and_set_sync_state: function(ids_to_remove){ var self = this; this.set_synch('connecting', ids_to_remove.length); - self._remove_from_server(ids_to_remove) + 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; }); }, @@ -871,13 +130,12 @@ models.PosModel = models.PosModel.extend({ */ _get_from_server: function (table_id, options) { options = options || {}; - var self = this; var timeout = typeof options.timeout === 'number' ? options.timeout : 7500; - return rpc.query({ + return this.rpc({ model: 'pos.order', method: 'get_table_draft_orders', args: [table_id], - kwargs: {context: session.user_context}, + kwargs: {context: this.session.user_context}, }, { timeout: timeout, shadow: false, @@ -947,8 +205,6 @@ models.PosModel = models.PosModel.extend({ var self = this; var ids_to_remove = this.db.get_ids_to_remove_from_server(); - clearInterval(this.table_longpolling); - this.set_synch('connecting', 1); this._get_from_server(table.id).then(function (server_orders) { var orders = self.get_order_list(); @@ -1085,9 +341,8 @@ models.PosModel = models.PosModel.extend({ } if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ this.set_order(order_list[index] || order_list[order_list.length -1]); - }else{ - // back to the floor plan - this.set_table(null); + } else if (order_list.length === 0) { + this.set_order(null); } } else { _super_posmodel.on_removed_order.apply(this,arguments); @@ -1097,68 +352,4 @@ models.PosModel = models.PosModel.extend({ }); -var TableGuestsButton = screens.ActionButtonWidget.extend({ - template: 'TableGuestsButton', - guests: function() { - if (this.pos.get_order()) { - return this.pos.get_order().customer_count; - } else { - return 0; - } - }, - button_click: function() { - var self = this; - this.gui.show_popup('number', { - 'title': _t('Guests ?'), - 'cheap': true, - 'value': this.pos.get_order().customer_count, - 'confirm': function(value) { - value = Math.max(1,Number(value)); - self.pos.get_order().set_customer_count(value); - self.renderElement(); - }, - }); - }, -}); - -screens.OrderWidget.include({ - update_summary: function(){ - this._super(); - if (this.getParent().action_buttons && - this.getParent().action_buttons.guests) { - this.getParent().action_buttons.guests.renderElement(); - } - }, -}); - -screens.define_action_button({ - 'name': 'guests', - 'widget': TableGuestsButton, - 'condition': function(){ - return this.pos.config.iface_floorplan; - }, -}); - -var TransferOrderButton = screens.ActionButtonWidget.extend({ - template: 'TransferOrderButton', - button_click: function() { - this.pos.transfer_order_to_different_table(); - }, -}); - -screens.define_action_button({ - 'name': 'transfer', - 'widget': TransferOrderButton, - 'condition': function(){ - return this.pos.config.iface_floorplan; - }, -}); - -return { - TableGuestsButton: TableGuestsButton, - TransferOrderButton:TransferOrderButton, - TableWidget: TableWidget, - FloorScreenWidget: FloorScreenWidget, -}; - }); diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js index d284917b940..709de43ef7b 100644 --- a/addons/pos_restaurant/static/src/js/multiprint.js +++ b/addons/pos_restaurant/static/src/js/multiprint.js @@ -2,7 +2,6 @@ odoo.define('pos_restaurant.multiprint', function (require) { "use strict"; var models = require('point_of_sale.models'); -var screens = require('point_of_sale.screens'); var core = require('web.core'); var Printer = require('point_of_sale.Printer').Printer; @@ -125,36 +124,6 @@ models.Orderline = models.Orderline.extend({ }, }); -screens.OrderWidget.include({ - render_orderline: function(orderline) { - var node = this._super(orderline); - if (this.pos.config.iface_printers) { - if (orderline.mp_skip) { - node.classList.add('skip'); - } else if (orderline.mp_dirty) { - node.classList.add('dirty'); - } - } - return node; - }, - click_line: function(line, event) { - if (!this.pos.config.iface_printers) { - this._super(line, event); - } else if (this.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(); - } - - this._super(line, event); - }, -}); - var _super_order = models.Order.prototype; models.Order = models.Order.extend({ build_line_resume: function(){ @@ -299,13 +268,18 @@ models.Order = models.Order.extend({ }, 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}); - await printers[i].print_receipt(receipt); + const result = await printers[i].print_receipt(receipt); + if (!result.successful) { + isPrintSuccessful = false; + } } } + return isPrintSuccessful; }, hasChangesToPrint: function(){ var printers = this.pos.printers; @@ -337,41 +311,5 @@ models.Order = models.Order.extend({ }, }); -var SubmitOrderButton = screens.ActionButtonWidget.extend({ - 'template': 'SubmitOrderButton', - button_click: async function(){ - var order = this.pos.get_order(); - if(order.hasChangesToPrint()){ - await order.printChanges(); - order.saveChanges(); - } - }, -}); - -screens.define_action_button({ - 'name': 'submit_order', - 'widget': SubmitOrderButton, - 'condition': function() { - return this.pos.printers.length; - }, -}); - -screens.OrderWidget.include({ - update_summary: function(){ - this._super(); - var changes = this.pos.get_order().hasChangesToPrint(); - var skipped = changes ? false : this.pos.get_order().hasSkippedChanges(); - var buttons = this.getParent().action_buttons; - - if (buttons && buttons.submit_order) { - buttons.submit_order.highlight(changes); - buttons.submit_order.altlight(skipped); - } - }, -}); - -return { - SubmitOrderButton: SubmitOrderButton, -} }); diff --git a/addons/pos_restaurant/static/src/js/notes.js b/addons/pos_restaurant/static/src/js/notes.js index a000b9fd871..c2ed95ddd26 100644 --- a/addons/pos_restaurant/static/src/js/notes.js +++ b/addons/pos_restaurant/static/src/js/notes.js @@ -2,14 +2,8 @@ odoo.define('pos_restaurant.notes', function (require) { "use strict"; var models = require('point_of_sale.models'); -var screens = require('point_of_sale.screens'); -var core = require('web.core'); - -var QWeb = core.qweb; -var _t = core._t; var _super_orderline = models.Orderline.prototype; - models.Orderline = models.Orderline.extend({ initialize: function(attr, options) { _super_orderline.initialize.call(this,attr,options); @@ -45,30 +39,4 @@ models.Orderline = models.Orderline.extend({ }, }); -var OrderlineNoteButton = screens.ActionButtonWidget.extend({ - template: 'OrderlineNoteButton', - button_click: function(){ - var line = this.pos.get_order().get_selected_orderline(); - if (line) { - this.gui.show_popup('textarea',{ - title: _t('Add Note'), - value: line.get_note(), - confirm: function(note) { - line.set_note(note); - }, - }); - } - }, -}); - -screens.define_action_button({ - 'name': 'orderline_note', - 'widget': OrderlineNoteButton, - 'condition': function(){ - return this.pos.config.iface_orderline_notes; - }, -}); -return { - OrderlineNoteButton: OrderlineNoteButton, -} }); diff --git a/addons/pos_restaurant/static/src/js/printbill.js b/addons/pos_restaurant/static/src/js/printbill.js deleted file mode 100644 index 02e7c3bd36c..00000000000 --- a/addons/pos_restaurant/static/src/js/printbill.js +++ /dev/null @@ -1,57 +0,0 @@ -odoo.define('pos_restaurant.printbill', function (require) { -"use strict"; - -var core = require('web.core'); -var screens = require('point_of_sale.screens'); -var gui = require('point_of_sale.gui'); -var _t = core._t; -var QWeb = core.qweb; - -var BillScreenWidget = screens.ReceiptScreenWidget.extend({ - template: 'BillScreenWidget', - click_next: function(){ - this.gui.show_screen('products'); - }, - click_back: function(){ - this.gui.show_screen('products'); - }, - get_receipt_render_env: function(){ - var render_env = this._super(); - render_env.receipt.bill = true; - return render_env; - }, - render_receipt: function(){ - this._super(); - this.$('.receipt-change').remove(); - }, -}); - -gui.define_screen({name:'bill', widget: BillScreenWidget}); - -var PrintBillButton = screens.ActionButtonWidget.extend({ - template: 'PrintBillButton', - button_click: function(){ - var order = this.pos.get('selectedOrder'); - if(order.get_orderlines().length > 0) { - this.gui.show_screen('bill'); - } else { - this.gui.show_popup('error', { - 'title': _t('Nothing to Print'), - 'body': _t('There are no order lines'), - }); - } - }, -}); - -screens.define_action_button({ - 'name': 'print_bill', - 'widget': PrintBillButton, - 'condition': function(){ - return this.pos.config.iface_printbill; - }, -}); -return { - BillScreenWidget: BillScreenWidget, - PrintBillButton: PrintBillButton, -}; -}); diff --git a/addons/pos_restaurant/static/src/js/splitbill.js b/addons/pos_restaurant/static/src/js/splitbill.js deleted file mode 100644 index 12e11769de9..00000000000 --- a/addons/pos_restaurant/static/src/js/splitbill.js +++ /dev/null @@ -1,203 +0,0 @@ -odoo.define('pos_restaurant.splitbill', function (require) { -"use strict"; - -var gui = require('point_of_sale.gui'); -var models = require('point_of_sale.models'); -var screens = require('point_of_sale.screens'); -var core = require('web.core'); - -var QWeb = core.qweb; - -var SplitbillScreenWidget = screens.ScreenWidget.extend({ - template: 'SplitbillScreenWidget', - - previous_screen: 'products', - - renderElement: function(){ - var self = this; - var linewidget; - - this._super(); - var order = this.pos.get_order(); - if(!order){ - return; - } - var orderlines = order.get_orderlines(); - for(var i = 0; i < orderlines.length; i++){ - var line = orderlines[i]; - linewidget = $(QWeb.render('SplitOrderline',{ - widget:this, - line:line, - selected: false, - quantity: 0, - id: line.id, - })); - linewidget.data('id',line.id); - this.$('.orderlines').append(linewidget); - } - this.$('.back').click(function(){ - self.gui.show_screen(self.previous_screen); - }); - }, - - split_quantity: function(split, line, splitlines) { - 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 < line.get_quantity()){ - 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; - } - } - }, - - set_line_on_order: function(neworder, split, line) { - if( split.quantity ){ - if ( !split.line ){ - split.line = line.clone(); - neworder.add_orderline(split.line); - } - split.line.set_quantity(split.quantity, 'do not recompute unit price'); - }else if( split.line ) { - neworder.remove_orderline(split.line); - split.line = null; - } - }, - - lineselect: function($el,order,neworder,splitlines,line_id){ - var split = splitlines[line_id] || {'quantity': 0, line: null}; - var line = order.get_orderline(line_id); - - this.split_quantity(split, line, null); - - this.set_line_on_order(neworder, split, line); - - splitlines[line_id] = split; - $el.replaceWith($(QWeb.render('SplitOrderline',{ - widget: this, - line: line, - selected: split.quantity !== 0, - quantity: split.quantity, - id: line_id, - }))); - this.$('.order-info .subtotal').text(this.format_currency(neworder.get_subtotal())); - }, - - check_full_pay_order: function (order, splitlines) { - return _.every(order.get_orderlines(), function(orderLine) { - var split = splitlines[orderLine.id]; - return split && split.quantity === orderLine.get_quantity(); - }); - }, - - set_quantity_on_order: function(splitlines, order) { - for(var id in splitlines){ - var split = splitlines[id]; - var line = order.get_orderline(parseInt(id)); - line.set_quantity(line.get_quantity() - split.quantity, 'do not recompute unit price'); - if(Math.abs(line.get_quantity()) < 0.00001){ - order.remove_orderline(line); - } - delete splitlines[id]; - } - }, - - pay: function(order,neworder,splitlines){ - if(_.isEmpty(splitlines)) // Splitlines is empty - return; - - delete neworder.temporary; - - if(this.check_full_pay_order(order, splitlines)){ - this.gui.show_screen('payment'); - }else{ - this.set_quantity_on_order(splitlines, order); - - neworder.set_screen_data('screen','payment'); - - // 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 ( neworder.saveChanges ) { - order.saveChanges(); - neworder.saveChanges(); - } - - neworder.set_customer_count(1); - order.set_customer_count(order.get_customer_count() - 1); - order.set_screen_data('screen','products'); - - this.pos.get('orders').add(neworder); - this.pos.set('selectedOrder',neworder); - } - }, - show: function(){ - var self = this; - this._super(); - this.renderElement(); - - var order = this.pos.get_order(); - var neworder = new models.Order({},{ - pos: this.pos, - temporary: true, - }); - neworder.set('client',order.get('client')); - - var splitlines = {}; - - this.$('.orderlines').on('click','.orderline',function(){ - var id = parseInt($(this).data('id')); - var $el = $(this); - self.lineselect($el,order,neworder,splitlines,id); - }); - - this.$('.paymentmethods .button').click(function(){ - self.pay(order,neworder,splitlines); - }); - }, -}); - -gui.define_screen({ - 'name': 'splitbill', - 'widget': SplitbillScreenWidget, - 'condition': function(){ - return this.pos.config.iface_splitbill; - }, -}); - -var SplitbillButton = screens.ActionButtonWidget.extend({ - template: 'SplitbillButton', - button_click: function(){ - if(this.pos.get_order().get_orderlines().length > 0){ - this.gui.show_screen('splitbill'); - } - }, -}); - -screens.define_action_button({ - 'name': 'splitbill', - 'widget': SplitbillButton, - 'condition': function(){ - return this.pos.config.iface_splitbill; - }, -}); - -return { - SplitbillButton: SplitbillButton, - SplitbillScreenWidget: SplitbillScreenWidget, -} - -}); - diff --git a/addons/pos_restaurant/static/src/xml/Chrome.xml b/addons/pos_restaurant/static/src/xml/Chrome.xml new file mode 100644 index 00000000000..ee29abe2e36 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Chrome.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1"> + <xpath expr="//OrderSelector" position="attributes"> + <attribute name="t-if">mainScreen.name !== 'FloorScreen'</attribute> + </xpath> + <xpath expr="//OrderSelector" position="after"> + <div t-else="" class="order-selector"></div> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml new file mode 100644 index 00000000000..1f36d2763f3 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="BackToFloorButton" owl="1"> + <span class="order-button floor-button"> + <i class="fa fa-angle-double-left" role="img" aria-label="Back to floor" title="Back to floor" /> + <span> </span> + <span t-esc="floor.name" /> + <span> </span> + <span class="table-name"> + <span>( </span> + <span t-esc="table.name" /> + <span> )</span> + </span> + </span> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml new file mode 100644 index 00000000000..824ff26bcee --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="OrderSelector" t-inherit="point_of_sale.OrderSelector" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('order-selector')]//span[hasclass('orders')]//t" position="before"> + <BackToFloorButton t-if="backToFloorButtonIsShown" table="currentTable" /> + </xpath> + <xpath expr="//span[hasclass('order-sequence')]" position="replace"> + <span class="order-sequence"> + <t t-if="order.server_id and env.pos.pos_session.login_number != order.uid.split('-')[1]"> + <t t-esc="order.uid.substring(order.uid.indexOf('-') +1)" /> + </t> + <t t-else=""> + <t t-esc="order.sequence_number" /> + </t> + </span> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Resizeable.xml b/addons/pos_restaurant/static/src/xml/Resizeable.xml new file mode 100644 index 00000000000..169c888e415 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Resizeable.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="Resizeable" owl="1"> + <t t-slot="default"></t> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml new file mode 100644 index 00000000000..0c904ece232 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="BillScreen" owl="1"> + <div class="receipt-screen screen"> + <div class="screen-content"> + <div class="top-content"> + <span class="button back" t-on-click="confirm"> + <i class="fa fa-angle-double-left"></i> + <span> </span> + <span>Back</span> + </span> + <h1>Bill Printing</h1> + <span class="button next highlight" t-on-click="confirm"> + <span>Ok</span> + <span> </span> + <i class="fa fa-angle-double-right"></i> + </span> + </div> + <div class="centered-content"> + <div class="button print" t-on-click="printReceipt"> + <i class="fa fa-print"></i> + <span> </span> + <span>Print</span> + </div> + <div class="pos-receipt-container"> + <OrderReceipt order="currentOrder" isBill="true"/> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml new file mode 100644 index 00000000000..00e96d951b5 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates id="template" xml:space="preserve"> + + <t t-name="EditBar" owl="1"> + <div class="edit-bar"> + <span class="edit-button" t-on-click.stop="trigger('create-table')"> + <i class="fa fa-plus" role="img" aria-label="Add" title="Add"></i> + </span> + <span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="trigger('duplicate-table')"> + <i class="fa fa-files-o" role="img" aria-label="Duplicate" title="Duplicate"></i> + </span> + <span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="trigger('rename-table')"> + <i class="fa fa-font" role="img" aria-label="Rename" title="Rename"></i> + </span> + <span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="trigger('change-seats-num')"> + <i class="fa fa-user" role="img" aria-label="Seats" title="Seats"></i> + </span> + <span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="trigger('change-shape')"> + <span t-if="!props.selectedTable or props.selectedTable.shape == 'square'" class="button-option square"> + <i class="fa fa-square-o" role="img" aria-label="Square Shape" title="Square Shape"></i> + </span> + <span t-else="" class="button-option round"> + <i class="fa fa-circle-o" role="img" aria-label="Round Shape" title="Round Shape"></i> + </span> + </span> + <span class="edit-button" t-on-click.stop="state.isColorPicker = !state.isColorPicker"> + <i class="fa fa-tint" role="img" aria-label="Tint" title="Tint"></i> + </span> + <div t-if="state.isColorPicker and props.selectedTable" class="color-picker fg-picker"> + <div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="state.isColorPicker = false"> + <i class="fa fa-times" /> + </div> + <span class="color tl" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="trigger('set-table-color', '#EB6D6D')" /> + <span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="trigger('set-table-color', '#35D374')" /> + <span class="color tr" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="trigger('set-table-color', '#6C6DEC')" /> + <span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="trigger('set-table-color', '#EBBF6D')" /> + <span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="trigger('set-table-color', '#EBEC6D')" /> + <span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="trigger('set-table-color', '#AC6DAD')" /> + <span class="color bl" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="trigger('set-table-color', '#6C6D6D')" /> + <span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="trigger('set-table-color', '#ACADAD')" /> + <span class="color br" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="trigger('set-table-color', '#4ED2BE')" /> + </div> + <div t-if="state.isColorPicker and !props.selectedTable" class="color-picker bg-picker"> + <div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="state.isColorPicker = false"> + <i class="fa fa-times" /> + </div> + <span class="color tl" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="trigger('set-floor-color', 'rgb(244, 149, 149)')" /> + <span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="trigger('set-floor-color', 'rgb(130, 233, 171)')" /> + <span class="color tr" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="trigger('set-floor-color', 'rgb(136, 137, 242)')" /> + <span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="trigger('set-floor-color', 'rgb(255, 214, 136)')" /> + <span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="trigger('set-floor-color', 'rgb(254, 255, 154)')" /> + <span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="trigger('set-floor-color', 'rgb(209, 171, 210)')" /> + <span class="color bl" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="trigger('set-floor-color', 'rgb(75, 75, 75)')" /> + <span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="trigger('set-floor-color', 'rgb(210, 210, 210)')" /> + <span class="color br" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="trigger('set-floor-color', 'rgb(127, 221, 236)')" /> + </div> + <span class="edit-button trash" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="trigger('delete-table')"> + <i class="fa fa-trash" role="img" aria-label="Delete" title="Delete"></i> + </span> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditableTable.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditableTable.xml new file mode 100644 index 00000000000..9c3a6079a1e --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditableTable.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates id="template" xml:space="preserve"> + + <t t-name="EditableTable" owl="1"> + <Draggable limitArea="'.floor-map'"> + <Resizeable limitArea="'.floor-map'"> + <div class="table selected" t-on-click.stop=""> + <span class="label drag-handle"> + <t t-esc="props.table.name" /> + </span> + <span class="table-seats"> + <t t-esc="props.table.seats" /> + </span> + <t t-if="props.table.shape === 'round'"> + <div class="table-handle top resize-handle-n"></div> + <div class="table-handle bottom resize-handle-s"></div> + <div class="table-handle left resize-handle-w"></div> + <div class="table-handle right resize-handle-e"></div> + </t> + <t t-if="props.table.shape === 'square'"> + <span class='table-handle top right resize-handle-ne'></span> + <span class='table-handle top left resize-handle-nw'></span> + <span class='table-handle bottom right resize-handle-se'></span> + <span class='table-handle bottom left resize-handle-sw'></span> + </t> + </div> + </Resizeable> + </Draggable> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml new file mode 100644 index 00000000000..0f3dc40680a --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="FloorScreen" owl="1"> + <div class="floor-screen screen"> + <div class="screen-content-flexbox"> + <t t-if="env.pos.floors.length > 1"> + <div class="floor-selector"> + <t t-foreach="env.pos.floors" t-as="floor" t-key="floor.id"> + <span class="button button-floor" t-att-class="{ active: floor.id === state.selectedFloorId }" t-on-click="selectFloor(floor)"> + <t t-esc="floor.name" /> + </span> + </t> + </div> + </t> + <div class="floor-map" t-on-click="trigger('deselect-table')" t-ref="floor-map-ref"> + <div t-if="isFloorEmpty" class="empty-floor"> + <span>This floor has no tables yet, use the </span> + <i class="fa fa-plus" role="img" aria-label="Add button" title="Add button"></i> + <span> button in the editing toolbar to create new tables.</span> + </div> + <div t-else="" class="tables"> + <t t-foreach="activeTables" t-as="table" t-key="table.id"> + <TableWidget t-if="table.id !== state.selectedTableId" table="table" /> + <EditableTable t-else="" table="table" /> + </t> + </div> + <span t-if="env.pos.user.role == 'manager'" class="edit-button editing" t-att-class="{ active: state.isEditMode }" t-on-click.stop="toggleEditMode"> + <i class="fa fa-pencil" role="img" aria-label="Edit" title="Edit"></i> + </span> + <EditBar t-if="state.isEditMode" selectedTable="selectedTable" /> + </div> + </div> + </div> + </t> +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml new file mode 100644 index 00000000000..32b9bc94047 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<templates id="template" xml:space="preserve"> + + <t t-name="TableWidget" owl="1"> + <div t-if="!props.isSelected" class="table" t-on-click.stop="trigger('select-table', props.table)"> + <span class="table-cover" t-att-class="{ full: fill >= 1 }"></span> + <span t-if="orderCount" t-att-class="orderCountClass"> + <t t-esc="orderCount" /> + </span> + <span class="label"> + <t t-esc="props.table.name" /> + </span> + <span class="table-seats"> + <t t-esc="props.table.seats" /> + </span> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml new file mode 100644 index 00000000000..407103a5ab0 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="OrderlineNoteButton" owl="1"> + <div class="control-button"> + <i class="fa fa-tag" /> + <span> </span> + <span>Note</span> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml new file mode 100644 index 00000000000..12a4e397edd --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="PrintBillButton" owl="1"> + <span class="control-button order-printbill"> + <i class="fa fa-print"></i> + <span> </span> + <span>Bill</span> + </span> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SplitBillButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SplitBillButton.xml new file mode 100644 index 00000000000..e8efb306466 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SplitBillButton.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SplitBillButton" owl="1"> + <span class="control-button order-split"> + <i class="fa fa-files-o"></i> + <span> </span> + <span>Split</span> + </span> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml new file mode 100644 index 00000000000..e0229af81fc --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SubmitOrderButton" owl="1"> + <span class="control-button" t-att-class="addedClasses"> + <i class="fa fa-cutlery"></i> + <span> </span> + <span>Order</span> + </span> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml new file mode 100644 index 00000000000..5a33b17ec32 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="TableGuestsButton" owl="1"> + <div class="control-button"> + <span class="control-button-number"> + <t t-esc="nGuests" /> + </span> + <span> </span> + <span>Guests</span> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TransferOrderButton.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TransferOrderButton.xml new file mode 100644 index 00000000000..6eb0c975573 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TransferOrderButton.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="TransferOrderButton" owl="1"> + <div class="control-button"> + <i class="fa fa-arrow-right" /> + <span> </span> + <span>Transfer</span> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/Orderline.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/Orderline.xml new file mode 100644 index 00000000000..a5c96dedad9 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/Orderline.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1"> + <xpath expr="//ul[hasclass('info-list')]" position="inside"> + <t t-if="props.line.get_note()"> + <li class="info orderline-note"> + <i class="fa fa-tag" role="img" aria-label="Note" title="Note"/> + <t t-esc="props.line.get_note()" /> + </li> + </t> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml new file mode 100644 index 00000000000..8d195f6cca2 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('pos-receipt-order-data')]" position="inside"> + <t t-if="props.isBill"> + <div>PRO FORMA</div> + </t> + </xpath> + <xpath expr="//div[hasclass('receipt-change')]" position="attributes"> + <attribute name="t-if">!props.isBill</attribute> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml new file mode 100644 index 00000000000..f09ecdcdc1d --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SplitBillScreen" owl="1"> + <div class="splitbill-screen screen"> + <div class="screen-content"> + <div class="top-content"> + <span class="button back" t-on-click="back"> + <i class="fa fa-angle-double-left"></i> + <span> </span> + <span>Back</span> + </span> + <h1>Bill Splitting</h1> + </div> + <div class="left-content touch-scrollable scrollable-y"> + <div class="order"> + <ul class="orderlines"> + <t t-foreach="orderlines" t-as="line" t-key="line.cid"> + <SplitOrderline line="line" split="splitlines[line.id]" /> + </t> + </ul> + </div> + </div> + <div class="right-content touch-scrollable scrollable-y"> + <div class="order-info"> + <span class="subtotal"> + <t t-esc="env.pos.format_currency(newOrder.get_subtotal())" /> + </span> + </div> + <div class="paymentmethods"> + <div class="button payment" t-on-click="proceed"> + <i class="fa fa-chevron-right" /> + <span> </span> + <span>Payment</span> + </div> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitOrderline.xml b/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitOrderline.xml new file mode 100644 index 00000000000..fd71374fa98 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitOrderline.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="SplitOrderline" owl="1"> + <li class="orderline" t-att-class="{ selected: isSelected, partially: props.split.quantity !== props.line.get_quantity() }"> + <span class="product-name"> + <t t-esc="props.line.get_product().display_name" /> + </span> + <span class="price"> + <t t-esc="env.pos.format_currency(props.line.get_display_price())" /> + </span> + <ul class="info-list"> + <t t-if="props.line.get_quantity_str() !== '1'"> + <li class="info"> + <t t-if="isSelected and props.line.get_unit().is_pos_groupable"> + <em class="big"> + <t t-esc="props.split.quantity" /> + </em> + / + <t t-esc="props.line.get_quantity_str()" /> + </t> + <t t-if="!(isSelected and props.line.get_unit().is_pos_groupable)"> + <em> + <t t-esc="props.line.get_quantity_str()" /> + </em> + </t> + <t t-esc="props.line.get_unit().name" /> + at + <t t-esc="env.pos.format_currency(props.line.get_unit_price())" /> + / + <t t-esc="props.line.get_unit().name" /> + </li> + </t> + <t t-if="props.line.get_discount_str() !== '0'"> + <li class="info"> + <span>With a </span> + <em> + <t t-esc="props.line.get_discount_str()" /> + <span>%</span> + </em> + <span> discount</span> + </li> + </t> + </ul> + </li> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/floors.xml b/addons/pos_restaurant/static/src/xml/floors.xml deleted file mode 100644 index b3019a915d4..00000000000 --- a/addons/pos_restaurant/static/src/xml/floors.xml +++ /dev/null @@ -1,181 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates id="template" xml:space="preserve"> - - <t t-extend="OrderReceipt"> - <t t-jquery='.cashier' t-operation='append'> - <t t-if='receipt.table'> - at table <t t-esc='receipt.table' /> - </t> - <t t-if='receipt.table && receipt.customer_count'> - <div>Guests: <t t-esc='receipt.customer_count' /></div> - </t> - </t> - </t> - - <t t-extend="BillReceipt"> - <t t-jquery='.cashier' t-operation='append'> - <t t-if='receipt.table'> - at table <t t-esc='receipt.table' /> - </t> - <t t-if='receipt.customer_count'> - <div>Guests: <t t-esc='receipt.customer_count' /></div> - </t> - </t> - </t> - - <t t-extend="OrderSelectorWidget"> - <t t-jquery=".order-sequence" t-operation="replace"> - <span class="order-sequence"> - <t t-if="order.server_id && widget.pos.pos_session.login_number != order.uid.split('-')[1]"> - <t t-esc="order.uid.substring(order.uid.indexOf('-') +1)"/> - </t> - <t t-else=""> - <t t-esc="order.sequence_number"/> - </t> - </span> - </t> - </t> - - <t t-name="TableGuestsButton"> - <div class='control-button'> - <span class='control-button-number'> - <t t-esc="widget.guests()" /> - </span> - Guests - </div> - </t> - - <t t-name="TransferOrderButton"> - <div class='control-button'> - <i class='fa fa-arrow-right' /> Transfer - </div> - </t> - - <t t-name="TableWidget"> - <t t-if='!widget.selected'> - <div class='table' t-att-style='widget.table_style_str()'> - <span - t-if="widget.table.shape" - t-att-class='"table-cover " + (widget.fill >= 1 ? "full" : "")' - t-att-style='"height: " + Math.ceil(widget.fill * 100) + "%;"' - ></span> - <t t-if='widget.order_count'> - <span t-att-class='"order-count " + (widget.notifications.printing ? "notify-printing":"") + (widget.notifications.skipped ? "notify-skipped" : "")'><t t-esc='widget.order_count'/></span> - </t> - <span class='label'> - <t t-esc='widget.table.name' /> - </span> - <span class="table-seats"><t t-esc="widget.table.seats" /></span> - </div> - </t> - <t t-if='widget.selected'> - <div class='table selected' t-att-style='widget.table_style_str()'> - <span class='label'> - <t t-esc='widget.table.name' /> - </span> - <span class="table-seats"><t t-esc="widget.table.seats" /></span> - <t t-if="widget.table.shape === 'round'"> - <span class='table-handle top ui-resizable-n'></span> - <span class='table-handle bottom ui-resizable-s'></span> - <span class='table-handle left ui-resizable-w'></span> - <span class='table-handle right ui-resizable-e'></span> - </t> - <t t-if="widget.table.shape === 'square'"> - <span class='table-handle top right ui-resizable-ne'></span> - <span class='table-handle top left ui-resizable-nw'></span> - <span class='table-handle bottom right ui-resizable-se'></span> - <span class='table-handle bottom left ui-resizable-sw'></span> - </t> - </div> - </t> - </t> - - <t t-name="BackToFloorButton"> - <span class="order-button floor-button"> - <i class='fa fa-angle-double-left' role="img" aria-label="Back to floor" title="Back to floor"/> - <t t-esc="floor.name"/> - <span class='table-name'> - ( <t t-esc="table.name" /> ) - </span> - </span> - </t> - - <t t-name="FloorScreenWidget"> - <div class='floor-screen screen'> - <div class='screen-content-flexbox'> - <t t-if='widget.pos.floors.length > 1'> - <div class='floor-selector'> - <t t-foreach="widget.pos.floors" t-as="floor"> - <t t-if="floor.id === widget.floor.id"> - <span class='button button-floor active' t-att-data-id="floor.id"><t t-esc="floor.name" /></span> - </t> - <t t-if="floor.id !== widget.floor.id"> - <span class='button button-floor' t-att-data-id="floor.id"><t t-esc="floor.name" /></span> - </t> - </t> - </div> - </t> - <div class='floor-map' t-att-style='widget.get_floor_style()' > - <div class='empty-floor oe_hidden'> - This floor has no tables yet, use the <i class="fa fa-plus" role="img" aria-label="Add button" title="Add button"></i> button in the editing toolbar to create new tables. - </div> - <div class='tables'></div> - <span t-if="widget.pos.user.role == 'manager'" class='edit-button editing'><i class='fa fa-pencil' role="img" aria-label="Edit" title="Edit"></i></span> - <div class='edit-bar oe_hidden'> - <span class='edit-button new-table'> - <i class='fa fa-plus' role="img" aria-label="Add" title="Add"></i> - </span> - <span class='edit-button dup-table needs-selection'> - <i class='fa fa-files-o' role="img" aria-label="Duplicate" title="Duplicate"></i> - </span> - <span class='edit-button rename needs-selection'> - <i class='fa fa-font' role="img" aria-label="Rename" title="Rename"></i> - </span> - <span class='edit-button seats needs-selection'> - <i class='fa fa-user' role="img" aria-label="Seats" title="Seats"></i> - </span> - <span class='edit-button shape needs-selection'> - <span class='button-option square'><i class='fa fa-square-o' role="img" aria-label="Square Shape" title="Square Shape"></i></span> - <span class='button-option round oe_hidden'><i class='fa fa-circle-o' role="img" aria-label="Round Shape" title="Round Shape"></i></span> - </span> - <span class='edit-button color'> - <i class='fa fa-tint' role="img" aria-label="Tint" title="Tint"></i> - <div class='color-picker fg-picker oe_hidden'> - <div class='close-picker' title="Close" role="img" aria-label="Close"> - <i class='fa fa-times' /> - </div> - <span class='color tl' style='background-color:#EB6D6D' role="img" aria-label="Red" title="Red"/> - <span class='color' style='background-color:#35D374' role="img" aria-label="Green" title="Green"/> - <span class='color tr' style='background-color:#6C6DEC' role="img" aria-label="Blue" title="Blue"/> - <span class='color' style='background-color:#EBBF6D' role="img" aria-label="Orange" title="Orange"/> - <span class='color' style='background-color:#EBEC6D' role="img" aria-label="Yellow" title="Yellow"/> - <span class='color' style='background-color:#AC6DAD' role="img" aria-label="Purple" title="Purple"/> - <span class='color bl' style='background-color:#6C6D6D' role="img" aria-label="Grey" title="Grey"/> - <span class='color' style='background-color:#ACADAD' role="img" aria-label="Light grey" title="Light grey"/> - <span class='color br' style='background-color:#4ED2BE' role="img" aria-label="Turquoise" title="Turquoise"/> - </div> - <div class='color-picker bg-picker oe_hidden'> - <div class='close-picker' title="Close" role="img" aria-label="Close"> - <i class='fa fa-times' /> - </div> - <span class='color tl' style='background-color:rgb(244, 149, 149)' role="img" aria-label="Red" title="Red"/> - <span class='color' style='background-color:rgb(130, 233, 171)' role="img" aria-label="Green" title="Green"/> - <span class='color tr' style='background-color:rgb(136, 137, 242)' role="img" aria-label="Blue" title="Blue"/> - <span class='color' style='background-color:rgb(255, 214, 136)' role="img" aria-label="Orange" title="Orange"/> - <span class='color' style='background-color:rgb(254, 255, 154)' role="img" aria-label="Yellow" title="Yellow"/> - <span class='color' style='background-color:rgb(209, 171, 210)' role="img" aria-label="Purple" title="Purple"/> - <span class='color bl' style='background-color:rgb(75, 75, 75)' role="img" aria-label="Grey" title="Grey"/> - <span class='color' style='background-color:rgb(210, 210, 210)' role="img" aria-label="Light grey" title="Light grey"/> - <span class='color br' style='background-color:rgb(127, 221, 236)' role="img" aria-label="Turquoise" title="Turquoise"/> - </div> - </span> - <span class='edit-button trash needs-selection'> - <i class='fa fa-trash' role="img" aria-label="Delete" title="Delete"></i> - </span> - </div> - - </div> - </div> - </div> - </t> -</templates> diff --git a/addons/pos_restaurant/static/src/xml/multiprint.xml b/addons/pos_restaurant/static/src/xml/multiprint.xml index e43e63a765b..8a56c283c4c 100644 --- a/addons/pos_restaurant/static/src/xml/multiprint.xml +++ b/addons/pos_restaurant/static/src/xml/multiprint.xml @@ -1,13 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <templates id="template" xml:space="preserve"> - <t t-name="SubmitOrderButton"> - <span class="control-button order-submit"> - <i class="fa fa-cutlery"></i> - Order - </span> - </t> - <t t-name="NameWrapped"> <t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line"> <div> diff --git a/addons/pos_restaurant/static/src/xml/notes.xml b/addons/pos_restaurant/static/src/xml/notes.xml deleted file mode 100644 index 63bd9d8a1c3..00000000000 --- a/addons/pos_restaurant/static/src/xml/notes.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates id="template" xml:space="preserve"> - - <t t-extend="Orderline"> - <t t-jquery=".info-list" t-operation="append"> - <t t-if="line.get_note()"> - <li class="info orderline-note"> - <i class='fa fa-tag' role="img" aria-label="Note" title="Note"/><t t-esc="line.get_note()" /> - </li> - </t> - </t> - </t> - - <t t-name="OrderlineNoteButton"> - <div class='control-button'> - <i class='fa fa-tag' /> Note - </div> - </t> - -</templates> diff --git a/addons/pos_restaurant/static/src/xml/printbill.xml b/addons/pos_restaurant/static/src/xml/printbill.xml deleted file mode 100644 index 47fe78fd44f..00000000000 --- a/addons/pos_restaurant/static/src/xml/printbill.xml +++ /dev/null @@ -1,44 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates id="template" xml:space="preserve"> - - <t t-name="BillScreenWidget"> - <div class='receipt-screen screen'> - <div class='screen-content'> - <div class='top-content'> - <span class='button back'> - <i class='fa fa-angle-double-left'></i> - Back - </span> - <h1>Bill Printing</h1> - <span class='button next'> - Ok - <i class='fa fa-angle-double-right'></i> - </span> - </div> - <div class="centered-content"> - <div class="button print"> - <i class='fa fa-print'></i> Print - </div> - <div class="pos-receipt-container"> - </div> - </div> - </div> - </div> - </t> - - <t t-name="PrintBillButton"> - <span class="control-button order-printbill"> - <i class="fa fa-print"></i> - Bill - </span> - </t> - - <t t-extend="OrderReceipt"> - <t t-jquery='.pos-receipt-order-data' t-operation='append'> - <t t-if='receipt.bill === true'> - <div>PRO FORMA</div> - </t> - </t> - </t> - -</templates> diff --git a/addons/pos_restaurant/static/src/xml/splitbill.xml b/addons/pos_restaurant/static/src/xml/splitbill.xml deleted file mode 100644 index afca4071427..00000000000 --- a/addons/pos_restaurant/static/src/xml/splitbill.xml +++ /dev/null @@ -1,86 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates id="template" xml:space="preserve"> - - <t t-name="SplitbillButton"> - <span class="control-button order-split"> - <i class="fa fa-files-o"></i> - Split - </span> - </t> - - <t t-name="SplitOrderline"> - - <li t-attf-class="orderline #{ selected ? 'selected' : ''} #{ quantity !== line.get_quantity() ? 'partially' : '' }" - t-att-data-id="id"> - <span class="product-name"> - <t t-esc="line.get_product().display_name"/> - </span> - <span class="price"> - <t t-esc="widget.format_currency(line.get_display_price())"/> - </span> - <ul class="info-list"> - <t t-if="line.get_quantity_str() !== '1'"> - <li class="info"> - <t t-if='selected and line.get_unit().is_pos_groupable'> - <em class='big'> - <t t-esc='quantity' /> - </em> - / - <t t-esc="line.get_quantity_str()" /> - </t> - <t t-if='!(selected and line.get_unit().is_pos_groupable)'> - <em> - <t t-esc="line.get_quantity_str()" /> - </em> - </t> - <t t-esc="line.get_unit().name" /> - at - <t t-esc="widget.format_currency(line.get_unit_price())" /> - / - <t t-esc="line.get_unit().name" /> - </li> - </t> - <t t-if="line.get_discount_str() !== '0'"> - <li class="info"> - With a - <em> - <t t-esc="line.get_discount_str()" />% - </em> - discount - </li> - </t> - </ul> - </li> - </t> - - <t t-name="SplitbillScreenWidget"> - <div class='splitbill-screen screen'> - <div class='screen-content'> - <div class='top-content'> - <span class='button back'> - <i class='fa fa-angle-double-left'></i> - Back - </span> - <h1>Bill Splitting</h1> - </div> - <div class='left-content touch-scrollable scrollable-y'> - <div class='order'> - <ul class='orderlines'> - </ul> - </div> - </div> - <div class='right-content touch-scrollable scrollable-y'> - <div class='order-info'> - <span class='subtotal'><t t-esc='widget.format_currency(0.0)'/></span> - </div> - <div class='paymentmethods'> - <div class='button payment'> - <i class='fa fa-chevron-right' /> Payment - </div> - </div> - </div> - </div> - </div> - </t> - -</templates> diff --git a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js new file mode 100644 index 00000000000..924d34010e4 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js @@ -0,0 +1,49 @@ +odoo.define('pos_restaurant.tour.ControlButtons', function (require) { + 'use strict'; + + const { TextAreaPopup } = require('pos_restaurant.tour.TextAreaPopupTourMethods'); + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); + const { SplitBillScreen } = require('pos_restaurant.tour.SplitBillScreenTourMethods'); + const { BillScreen } = require('pos_restaurant.tour.BillScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + // Test TransferOrderButton + FloorScreen.do.clickTable('T2'); + ProductScreen.exec.order('Water', '5.0', '2.0'); + ProductScreen.do.clickTransferButton(); + FloorScreen.do.clickTable('T4'); + ProductScreen.do.clickOrderline('Water', '5.0', '2.0'); + Chrome.do.backToFloor(); + FloorScreen.do.clickTable('T2'); + ProductScreen.check.orderIsEmpty(); + Chrome.do.backToFloor(); + FloorScreen.do.clickTable('T4'); + ProductScreen.do.clickOrderline('Water', '5.0', '2.0'); + + // Test SplitBillButton + ProductScreen.do.clickSplitBillButton(); + SplitBillScreen.do.clickBack(); + + // Test OrderlineNoteButton + ProductScreen.do.clickNoteButton(); + TextAreaPopup.check.isShown(); + TextAreaPopup.do.inputText('test note'); + TextAreaPopup.do.clickConfirm(); + ProductScreen.check.orderlineHasNote('Water', '5.0', 'test note'); + ProductScreen.exec.order('Water', '8', '1.1'); + ProductScreen.check.selectedOrderlineHas('Water', '8', '8.80'); + + // Test PrintBillButton + ProductScreen.do.clickPrintBillButton(); + BillScreen.check.isShown(); + BillScreen.do.clickBack(); + + Tour.register('ControlButtonsTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js b/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js new file mode 100644 index 00000000000..4ece19fec4b --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js @@ -0,0 +1,98 @@ +odoo.define('pos_restaurant.tour.FloorScreen', function (require) { + 'use strict'; + + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { TextInputPopup } = require('pos_restaurant.tour.TextInputPopupTourMethods'); + const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods'); + const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + // check floors if they contain their corresponding tables + FloorScreen.check.selectedFloorIs('Main Floor'); + FloorScreen.check.hasTable('T2'); + FloorScreen.check.hasTable('T4'); + FloorScreen.check.hasTable('T5'); + FloorScreen.do.clickFloor('Second Floor'); + FloorScreen.check.hasTable('T3'); + FloorScreen.check.hasTable('T1'); + + // clicking table in active mode does not open product screen + // instead, table is selected + FloorScreen.do.clickEdit(); + FloorScreen.check.editModeIsActive(true); + FloorScreen.do.clickTable('T3'); + FloorScreen.check.selectedTableIs('T3'); + FloorScreen.do.clickTable('T1'); + FloorScreen.check.selectedTableIs('T1'); + + // switching floor in edit mode deactivates edit mode + FloorScreen.do.clickFloor('Main Floor'); + FloorScreen.check.editModeIsActive(false); + FloorScreen.do.clickEdit(); + + // test add table + FloorScreen.do.clickAddTable(); + FloorScreen.check.selectedTableIs('T1'); + FloorScreen.do.clickRename(); + TextInputPopup.check.isShown(); + TextInputPopup.do.inputText('T100'); + TextInputPopup.do.clickConfirm(); + FloorScreen.check.selectedTableIs('T100'); + + // test duplicate table + FloorScreen.do.clickDuplicate(); + FloorScreen.do.clickRename(); + TextInputPopup.do.inputText('T101'); + FloorScreen.check.selectedTableIs('T101'); + + // switch floor, switch back and check if + // the new tables are still there + FloorScreen.do.clickFloor('Second Floor'); + FloorScreen.check.hasTable('T3'); + FloorScreen.check.hasTable('T1'); + + FloorScreen.do.clickFloor('Main Floor'); + FloorScreen.check.hasTable('T2'); + FloorScreen.check.hasTable('T4'); + FloorScreen.check.hasTable('T5'); + FloorScreen.check.hasTable('T100'); + FloorScreen.check.hasTable('T101'); + + // test delete table + FloorScreen.do.clickEdit(); + FloorScreen.do.clickTable('T2'); + FloorScreen.do.clickTrash(); + Chrome.do.confirmPopup(); + + // change number of seats + FloorScreen.do.clickTable('T4'); + FloorScreen.do.clickSeats(); + NumberPopup.do.pressNumpad('Backspace 1 0'); + NumberPopup.do.clickConfirm(); + FloorScreen.check.tableSeatIs('T4', '10'); + + // change shape + FloorScreen.do.changeShapeTo('round'); + + // Opening product screen in main floor should go back to main floor + FloorScreen.do.clickEdit(); + FloorScreen.check.hasTable('T4'); + FloorScreen.do.clickTable('T4'); + ProductScreen.check.isShown(); + Chrome.check.backToFloorTextIs('Main Floor', 'T4'); + Chrome.do.backToFloor(); + + // Opening product screen in second floor should go back to second floor + FloorScreen.do.clickFloor('Second Floor'); + FloorScreen.check.hasTable('T1'); + FloorScreen.do.clickTable('T1'); + Chrome.check.backToFloorTextIs('Second Floor', 'T1'); + + Tour.register('FloorScreenTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js new file mode 100644 index 00000000000..07147a70624 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js @@ -0,0 +1,50 @@ +odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) { + 'use strict'; + + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); + const { SplitBillScreen } = require('pos_restaurant.tour.SplitBillScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + // signal to start generating steps + // when finished, steps can be taken from getSteps + startSteps(); + + FloorScreen.do.clickTable('T2'); + ProductScreen.exec.order('Water', '5', '1.2'); + ProductScreen.exec.order('Minute Maid', '3', '1.2'); + ProductScreen.exec.order('Coca-Cola', '1', '1.2'); + ProductScreen.do.clickSplitBillButton(); + + // Check if the screen contains all the orderlines + SplitBillScreen.check.orderlineHas('Water', '5', '0'); + SplitBillScreen.check.orderlineHas('Minute Maid', '3', '0'); + SplitBillScreen.check.orderlineHas('Coca-Cola', '1', '0'); + + // split 3 water and 1 coca-cola + SplitBillScreen.do.clickOrderline('Water'); + SplitBillScreen.check.orderlineHas('Water', '5', '1'); + SplitBillScreen.do.clickOrderline('Water'); + SplitBillScreen.do.clickOrderline('Water'); + SplitBillScreen.check.orderlineHas('Water', '5', '3'); + SplitBillScreen.check.subtotalIs('3.60') + SplitBillScreen.do.clickOrderline('Coca-Cola'); + SplitBillScreen.check.orderlineHas('Coca-Cola', '1', '1'); + SplitBillScreen.check.subtotalIs('4.80') + + // click pay to split, go back to check the lines + SplitBillScreen.do.clickPay(); + PaymentScreen.do.clickBack(); + ProductScreen.do.clickOrderline('Water', '3.0') + ProductScreen.do.clickOrderline('Coca-Cola', '1.0') + + // go back to the original order and see if the order is changed + Chrome.do.selectOrder('1'); + ProductScreen.do.clickOrderline('Water', '2.0') + ProductScreen.do.clickOrderline('Minute Maid', '3.0') + + Tour.register('SplitBillScreenTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js new file mode 100644 index 00000000000..1bb976cd8a2 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js @@ -0,0 +1,30 @@ +odoo.define('pos_restaurant.tour.BillScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickBack() { + return [ + { + content: `go back`, + trigger: `.receipt-screen .back`, + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'Bill screen is shown', + trigger: '.receipt-screen h1:contains("Bill Printing")', + run: () => {}, + }, + ]; + } + } + + return { Do, Check, BillScreen: createTourMethods('BillScreen', Do, Check) }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js new file mode 100644 index 00000000000..696b1d793c0 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js @@ -0,0 +1,36 @@ +odoo.define('pos_restaurant.tour.ChromeTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + const { Do } = require('point_of_sale.tour.ChromeTourMethods'); + + class DoExt extends Do { + backToFloor() { + return [ + { + content: 'back to floor', + trigger: '.floor-button', + }, + ]; + } + } + + class Check { + backToFloorTextIs(floor, table) { + return [ + { + content: `back to floor text is '${floor} ( ${table} )'`, + trigger: `.floor-button span:contains("${floor}") ~ .table-name:contains("${table}")`, + run: () => {}, + }, + ]; + } + } + + class Execute {} + + return { + Do: DoExt, + Chrome: createTourMethods('Chrome', DoExt, Check, Execute), + }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js new file mode 100644 index 00000000000..ce64f8cb3d3 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js @@ -0,0 +1,138 @@ +odoo.define('pos_restaurant.tour.FloorScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickTable(name) { + return [ + { + content: `click table '${name}'`, + trigger: `.floor-map .table .label:contains("${name}")`, + }, + ]; + } + clickFloor(name) { + return [ + { + content: `click '${name}' floor`, + trigger: `.floor-selector .button-floor:contains("${name}")`, + }, + ]; + } + clickEdit() { + return [ + { + content: `click edit button`, + trigger: `.floor-map .edit-button`, + }, + ]; + } + clickAddTable() { + return [ + { + content: 'add table', + trigger: `.floor-map .edit-button i[aria-label=Add]`, + }, + ]; + } + clickDuplicate() { + return [ + { + content: 'duplicate table', + trigger: `.floor-map .edit-button i[aria-label=Duplicate]`, + }, + ]; + } + clickRename() { + return [ + { + content: 'rename table', + trigger: `.floor-map .edit-button i[aria-label=Rename]`, + }, + ]; + } + clickSeats() { + return [ + { + content: 'change number of seats', + trigger: `.floor-map .edit-button i[aria-label=Seats]`, + }, + ]; + } + clickTrash() { + return [ + { + content: 'trash table', + trigger: `.floor-map .edit-button.trash`, + }, + ]; + } + changeShapeTo(shape) { + return [ + { + content: `change shape to '${shape}'`, + trigger: `.edit-button .button-option${ + shape === 'round' ? '.square' : '.round' + }`, + }, + ]; + } + } + + class Check { + selectedFloorIs(name) { + return [ + { + content: `selected floor is '${name}'`, + trigger: `.floor-selector .button-floor.active:contains("${name}")`, + run: () => {}, + }, + ]; + } + selectedTableIs(name) { + return [ + { + content: `selected table is '${name}'`, + trigger: `.floor-map .table.selected .label:contains("${name}")`, + run: () => {}, + }, + ]; + } + hasTable(name) { + return [ + { + content: `selected floor has '${name}' table`, + trigger: `.floor-map .tables .table .label:contains("${name}")`, + run: () => {}, + }, + ]; + } + editModeIsActive(flag) { + return [ + { + content: `check if edit mode is ${flag ? 'active' : 'inactive'}`, + trigger: `.floor-map .edit-button${flag ? '.active' : ':not(:has(.active))'}`, + run: () => {}, + }, + ]; + } + tableSeatIs(table, val) { + return [ + { + content: `number of seats in table '${table}' is '${val}'`, + trigger: `.floor-map .tables .table .label:contains("${table}") ~ .table-seats:contains("${val}")`, + }, + ]; + } + } + + class Execute {} + + return { + Do, + Check, + Execute, + FloorScreen: createTourMethods('FloorScreen', Do, Check, Execute), + }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js new file mode 100644 index 00000000000..f14434ec705 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -0,0 +1,75 @@ +odoo.define('pos_restaurant.tour.ProductScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + const { Do, Check, Execute } = require('point_of_sale.tour.ProductScreenTourMethods'); + + class DoExt extends Do { + clickSplitBillButton() { + return [ + { + content: 'click split bill button', + trigger: '.control-buttons .control-button.order-split', + }, + ]; + } + clickTransferButton() { + return [ + { + content: 'click transfer button', + trigger: '.control-buttons .control-button span:contains("Transfer")', + }, + ]; + } + clickNoteButton() { + return [ + { + content: 'click note button', + trigger: '.control-buttons .control-button span:contains("Note")', + }, + ]; + } + clickPrintBillButton() { + return [ + { + content: 'click print bill button', + trigger: '.control-buttons .control-button.order-printbill', + }, + ]; + } + clickSubmitButton() { + return [ + { + content: 'click print bill button', + trigger: '.control-buttons .control-button span:contains("Order")', + }, + ]; + } + } + + class CheckExt extends Check { + orderlineHasNote(name, quantity, note) { + return [ + { + content: `line has ${quantity} quantity`, + trigger: `.order .orderline .product-name:contains("${name}") ~ .info-list em:contains("${quantity}")`, + run: function () {}, // it's a check + }, + { + content: `line has '${note}' note`, + trigger: `.order .orderline .info-list .orderline-note:contains("${note}")`, + run: function () {}, // it's a check + }, + ]; + } + } + + class ExecuteExt extends Execute {} + + return { + Do: DoExt, + Check: CheckExt, + Execute: ExecuteExt, + ProductScreen: createTourMethods('ProductScreen', DoExt, CheckExt, ExecuteExt), + }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js new file mode 100644 index 00000000000..77b7e392174 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js @@ -0,0 +1,70 @@ +odoo.define('pos_restaurant.tour.SplitBillScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickOrderline(name, totalQuantity) { + let trigger = `li.orderline .product-name:contains("${name}")`; + if (totalQuantity) { + trigger += ` ~ .info-list .info:contains("${totalQuantity}")`; + } + return [ + { + content: `click '${name}' orderline with total quantity of '${totalQuantity}'`, + trigger, + }, + ]; + } + clickBack() { + return [ + { + content: 'click back button', + trigger: `.splitbill-screen .button.back`, + }, + ]; + } + clickPay() { + return [ + { + content: 'click pay button', + trigger: `.splitbill-screen .paymentmethods .payment` + } + ] + } + } + + class Check { + orderlineHas(name, totalQuantity, splitQuantity) { + return [ + { + content: `'${name}' orderline has total quantity of '${totalQuantity}'`, + trigger: `li.orderline .product-name:contains("${name}") ~ .info-list .info:contains("${totalQuantity}")`, + run: () => {}, + }, + { + content: `'${name}' orderline has '${splitQuantity}' quantity to split`, + trigger: `li.orderline .product-name:contains("${name}") ~ .info-list .info em:contains("${splitQuantity}")`, + run: () => {}, + }, + ]; + } + subtotalIs(amount) { + return [ + { + content: `total amount of split is '${amount}'`, + trigger: `.splitbill-screen .order-info .subtotal:contains("${amount}")`, + }, + ]; + } + } + + class Execute {} + + return { + Do, + Check, + Execute, + SplitBillScreen: createTourMethods('SplitBillScreen', Do, Check, Execute), + }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js new file mode 100644 index 00000000000..e2bbb2bde47 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js @@ -0,0 +1,39 @@ +odoo.define('pos_restaurant.tour.TextAreaPopupTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + inputText(val) { + return [ + { + content: `input text '${val}'`, + trigger: `.modal-dialog .popup-textarea textarea`, + run: `text ${val}`, + }, + ]; + } + clickConfirm() { + return [ + { + content: 'confirm text input popup', + trigger: '.modal-dialog .confirm', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'text input popup is shown', + trigger: '.modal-dialog .popup-textarea', + run: () => {}, + }, + ]; + } + } + + return { Do, Check, TextAreaPopup: createTourMethods('TextAreaPopup', Do, Check) }; +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js new file mode 100644 index 00000000000..6ce5e4ce035 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js @@ -0,0 +1,39 @@ +odoo.define('pos_restaurant.tour.TextInputPopupTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + inputText(val) { + return [ + { + content: `input text '${val}'`, + trigger: `.modal-dialog .popup-textinput input`, + run: `text ${val}`, + }, + ]; + } + clickConfirm() { + return [ + { + content: 'confirm text input popup', + trigger: '.modal-dialog .confirm', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + content: 'text input popup is shown', + trigger: '.modal-dialog .popup-textinput', + run: () => {}, + }, + ]; + } + } + + return { Do, Check, TextInputPopup: createTourMethods('TextInputPopup', Do, Check) }; +}); diff --git a/addons/pos_restaurant/static/src/js/tours/pos_restaurant.js b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js index 9e112e3d34c..8d5e8f0a80d 100644 --- a/addons/pos_restaurant/static/src/js/tours/pos_restaurant.js +++ b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js @@ -20,7 +20,7 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi if (orders.length === order_count) { return } else { - throw "sync failed"; + throw `sync failed 'there should be ${order_count} orders, however, ${orders.length} orders are displayed' `; } }, }]; @@ -100,11 +100,11 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi function finish_order() { var steps = [{ content: "validate the order", - trigger: '.button.next:visible', + trigger: '.payment-screen .button.next:visible', }]; steps = steps.concat([{ content: "next order", - trigger: '.button.next:visible', + trigger: '.receipt-screen .button.next:visible', }]); return steps; } @@ -115,7 +115,7 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi */ var steps = [{ content: 'waiting for loading to finish', - trigger: 'body:has(.loader:hidden)', + trigger: 'body:not(:has(.loader))', run: function () {}, }] @@ -163,6 +163,10 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi content: 'back to floor', trigger: '.floor-button', run: 'click', + }, { + content: 'back to floor', + trigger: '.oe_status .js_connected', + run: function() {}, }]); steps = steps.concat(open_table('T5', 1)); @@ -175,7 +179,7 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi */ var steps = [{ content: 'waiting for loading to finish', - trigger: 'body:has(.loader:hidden)', + trigger: 'body:not(:has(.loader))', run: function () {}, }]; steps = steps.concat(open_table('T5', 1)); @@ -233,6 +237,14 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (requi content: 'back to floor', trigger: '.floor-button', run: 'click', + }, { + content: 'check if in floor screen', + trigger: '.pos-content .floor-screen', + run: function() {}, + }, { + content: 'wait for sync to finish', + trigger: '.oe_status .js_connected', + run: function() {}, }]); Tour.register('pos_restaurant_sync_second_login', { test: true, url: '/pos/web' }, steps); diff --git a/addons/pos_restaurant/static/tests/unit/test_FloorScreen.js b/addons/pos_restaurant/static/tests/unit/test_FloorScreen.js new file mode 100644 index 00000000000..34f13671338 --- /dev/null +++ b/addons/pos_restaurant/static/tests/unit/test_FloorScreen.js @@ -0,0 +1,197 @@ +odoo.define('pos_restaurant.tests.FloorScreen', function (require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const { useListener } = require('web.custom_hooks'); + const testUtils = require('web.test_utils'); + const makePosTestEnv = require('point_of_sale.test_env'); + const { xml } = owl.tags; + const { useRef } = owl.hooks; + + QUnit.module('FloorScreen components', {}); + + QUnit.test('TableWidget', async function (assert) { + assert.expect(9); + + class Parent extends PosComponent { + constructor() { + super(); + useListener('select-table', () => assert.step('select-table')); + } + get table() { + // render table T1 + return Object.values(this.env.pos.tables_by_id).find( + (table) => table.name === 'T1' + ); + } + } + Parent.env = makePosTestEnv(); + Parent.template = xml/* html */ ` + <div><TableWidget table="table" /></div> + `; + + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + + const tableEl = parent.el.querySelector('.table'); + assert.ok(tableEl.querySelector('span.label').textContent.includes('T1')); + assert.ok(tableEl.querySelector('span.table-seats').textContent.includes('4')); + assert.strictEqual(tableEl.style.width, '100px'); + assert.strictEqual(tableEl.style.height, '100px'); + assert.strictEqual(tableEl.style.background, 'rgb(53, 211, 116)'); + assert.strictEqual(tableEl.style.top, '50px'); + assert.strictEqual(tableEl.style.left, '50px'); + + await testUtils.dom.click(tableEl); + assert.verifySteps(['select-table']); + + parent.unmount(); + parent.destroy(); + }); + + QUnit.test('EditableTable', async function (assert) { + assert.expect(11); + + class Parent extends PosComponent { + constructor() { + super(); + useListener('save-table', () => assert.step('save-table')); + this.tableRef = useRef('table-ref'); + } + get table() { + // render table T1 + return Object.values(this.env.pos.tables_by_id).find( + (table) => table.name === 'T1' + ); + } + } + Parent.env = makePosTestEnv(); + Parent.template = xml/* html */ ` + <div class="floor-map"> + <EditableTable table="table" t-ref="table-ref" /> + </div> + `; + + const parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + + const tableEl = parent.el.querySelector('.table.selected'); + assert.ok(tableEl.querySelector('span.label').textContent.includes('T1')); + assert.ok(tableEl.querySelector('span.table-seats').textContent.includes('4')); + assert.strictEqual(tableEl.style.width, '100px'); + assert.strictEqual(tableEl.style.height, '100px'); + assert.strictEqual(tableEl.style.background, 'rgb(53, 211, 116)'); + assert.strictEqual(tableEl.style.top, '50px'); + assert.strictEqual(tableEl.style.left, '50px'); + + parent.tableRef.comp.trigger('resize-end', { + size: { width: 100, height: 100 }, + loc: { top: 50, left: 50 }, + }); + assert.verifySteps(['save-table']); + parent.tableRef.comp.trigger('drag-end', { loc: { top: 50, left: 50 } }); + assert.verifySteps(['save-table']); + + parent.unmount(); + parent.destroy(); + }); + + QUnit.test('EditBar', async function (assert) { + assert.expect(26); + + class Parent extends PosComponent { + constructor() { + super(); + useListener('create-table', () => assert.step('create-table')); + useListener('duplicate-table', () => assert.step('duplicate-table')); + useListener('rename-table', () => assert.step('rename-table')); + useListener('change-seats-num', () => assert.step('change-seats-num')); + useListener('change-shape', () => assert.step('change-shape')); + useListener('set-table-color', this._onSetTableColor); + useListener('set-floor-color', this._onSetFloorColor); + useListener('delete-table', () => assert.step('delete-table')); + } + get table() { + // render table T1 + return Object.values(this.env.pos.tables_by_id).find( + (table) => table.name === 'T1' + ); + } + _onSetTableColor({ detail: color }) { + assert.step('set-table-color'); + assert.step(color); + } + _onSetFloorColor({ detail: color }) { + assert.step('set-floor-color'); + assert.step(color); + } + } + Parent.env = makePosTestEnv(); + + // Part 1: Test EditBar with selected table + + Parent.template = xml/* html */ ` + <div> + <EditBar selectedTable="table" /> + </div> + `; + + let parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Add]')); + assert.verifySteps(['create-table']); + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Duplicate]')); + assert.verifySteps(['duplicate-table']); + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Rename]')); + assert.verifySteps(['rename-table']); + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Seats]')); + assert.verifySteps(['change-seats-num']); + await testUtils.dom.click( + parent.el.querySelector('.edit-button i[aria-label="Square Shape"]') + ); + assert.verifySteps(['change-shape']); + + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Tint]')); + await testUtils.nextTick(); + + assert.ok(parent.el.querySelector('.color-picker.fg-picker')); + await testUtils.dom.click(parent.el.querySelector('.fg-picker .color.tl')); + assert.verifySteps(['set-table-color', '#EB6D6D']); + + await testUtils.dom.click(parent.el.querySelector('.edit-button.trash')); + assert.verifySteps(['delete-table']); + + parent.unmount(); + parent.destroy(); + + // Part 2: Test EditBar without selected table + + Parent.template = xml/* html */ ` + <div> + <EditBar /> + </div> + `; + + parent = new Parent(); + await parent.mount(testUtils.prepareTarget()); + + assert.ok(parent.el.querySelector('.edit-button.disabled i[aria-label=Duplicate]')); + assert.ok(parent.el.querySelector('.edit-button.disabled i[aria-label=Rename]')); + assert.ok(parent.el.querySelector('.edit-button.disabled i[aria-label=Seats]')); + assert.ok(parent.el.querySelector('.edit-button.disabled i[aria-label="Square Shape"]')); + assert.ok(parent.el.querySelector('.edit-button.disabled i[aria-label=Delete]')); + + await testUtils.dom.click(parent.el.querySelector('.edit-button i[aria-label=Tint]')); + await testUtils.nextTick(); + + assert.notOk(parent.el.querySelector('.color-picker.fg-picker')); + assert.ok(parent.el.querySelector('.color-picker.bg-picker')); + + await testUtils.dom.click(parent.el.querySelector('.bg-picker .color.tl')); + assert.verifySteps(['set-floor-color', 'rgb(244, 149, 149)']); + + parent.unmount(); + parent.destroy(); + }); +}); diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py index 03120b757c6..44ae56444c2 100644 --- a/addons/pos_restaurant/tests/test_frontend.py +++ b/addons/pos_restaurant/tests/test_frontend.py @@ -5,9 +5,10 @@ import odoo.tests @odoo.tests.tagged('post_install', '-at_install') class TestFrontend(odoo.tests.HttpCase): - def test_01_pos_restaurant(self): - env = self.env(user=self.env.ref('base.user_admin')) - account_obj = env['account.account'] + def setUp(self): + super().setUp() + self.env = self.env(user=self.env.ref('base.user_admin')) + account_obj = self.env['account.account'] printer = self.env['restaurant.printer'].create({ 'name': 'Kitchen Printer', @@ -38,23 +39,51 @@ class TestFrontend(odoo.tests.HttpCase): 'name': 'T5', 'floor_id': main_floor.id, 'seats': 4, + 'position_h': 100, + 'position_v': 100, }) table_04 = self.env['restaurant.table'].create({ 'name': 'T4', 'floor_id': main_floor.id, 'seats': 4, + 'shape': 'square', + 'position_h': 150, + 'position_v': 100, }) table_02 = self.env['restaurant.table'].create({ 'name': 'T2', 'floor_id': main_floor.id, 'seats': 4, + 'position_h': 250, + 'position_v': 100, }) - main_company = env.ref('base.main_company') + second_floor = self.env['restaurant.floor'].create({ + 'name': 'Second Floor', + 'pos_config_id': pos_config.id, + }) + + table_01 = self.env['restaurant.table'].create({ + 'name': 'T1', + 'floor_id': second_floor.id, + 'seats': 4, + 'shape': 'square', + 'position_h': 100, + 'position_v': 150, + }) + table_03 = self.env['restaurant.table'].create({ + 'name': 'T3', + 'floor_id': second_floor.id, + 'seats': 4, + 'position_h': 100, + 'position_v': 250, + }) + + main_company = self.env.ref('base.main_company') account_receivable = account_obj.create({'code': 'X1012', 'name': 'Account Receivable - Test', - 'user_type_id': env.ref('account.data_account_type_receivable').id, + 'user_type_id': self.env.ref('account.data_account_type_receivable').id, 'reconcile': True}) self.env['ir.property'].set_default( @@ -64,14 +93,14 @@ class TestFrontend(odoo.tests.HttpCase): main_company, ) - test_sale_journal = env['account.journal'].create({ + test_sale_journal = self.env['account.journal'].create({ 'name': 'Sales Journal - Test', 'code': 'TSJ', 'type': 'sale', 'company_id': main_company.id }) - cash_journal = env['account.journal'].create({ + cash_journal = self.env['account.journal'].create({ 'name': 'Cash Test', 'code': 'TCJ', 'type': 'sale', @@ -120,15 +149,25 @@ class TestFrontend(odoo.tests.HttpCase): 'taxes_id': [(6, 0, [])], }) - pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) + self.pos_config = pos_config + + def test_01_pos_restaurant(self): + + self.pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) + + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'pos_restaurant_sync', login="admin", step_delay=50) - self.start_tour("/pos/web?config_id=%d" % pos_config.id, 'pos_restaurant_sync', login="admin") + self.assertEqual(1, self.env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'draft')])) + self.assertEqual(1, self.env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'paid')])) - self.assertEqual(1, env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'draft')])) - self.assertEqual(1, env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'paid')])) + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'pos_restaurant_sync_second_login', login="admin", step_delay=50) - self.start_tour("/pos/web?config_id=%d" % pos_config.id, 'pos_restaurant_sync_second_login', login="admin") + self.assertEqual(0, self.env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'draft')])) + self.assertEqual(1, self.env['pos.order'].search_count([('amount_total', '=', 2.2), ('state', '=', 'draft')])) + self.assertEqual(2, self.env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'paid')])) - self.assertEqual(0, env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'draft')])) - self.assertEqual(1, env['pos.order'].search_count([('amount_total', '=', 2.2), ('state', '=', 'draft')])) - self.assertEqual(2, env['pos.order'].search_count([('amount_total', '=', 4.4), ('state', '=', 'paid')])) + def test_02_others(self): + self.pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'SplitBillScreenTour', login="admin", step_delay=50) + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'ControlButtonsTour', login="admin", step_delay=50) + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'FloorScreenTour', login="admin", step_delay=50) diff --git a/addons/pos_restaurant/views/pos_restaurant_templates.xml b/addons/pos_restaurant/views/pos_restaurant_templates.xml index 299dacef693..cbef00a5d77 100644 --- a/addons/pos_restaurant/views/pos_restaurant_templates.xml +++ b/addons/pos_restaurant/views/pos_restaurant_templates.xml @@ -1,19 +1,36 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <template id="assets" inherit_id="point_of_sale.assets"> - <xpath expr="." position="inside"> - <script type="text/javascript" src="/pos_restaurant/static/lib/js/jquery.ui.touch-punch.js"></script> - <script type="text/javascript" src="/pos_restaurant/static/src/js/multiprint.js"></script> - <script type="text/javascript" src="/pos_restaurant/static/src/js/splitbill.js"></script> - <script type="text/javascript" src="/pos_restaurant/static/src/js/printbill.js"></script> - <script type="text/javascript" src="/pos_restaurant/static/src/js/floors.js"></script> - <script type="text/javascript" src="/pos_restaurant/static/src/js/notes.js"></script> - </xpath> - <xpath expr="//link[@id='pos-stylesheet']" position="after"> - <link rel="stylesheet" href="/pos_restaurant/static/src/css/restaurant.css"/> - </xpath> - </template> + <template id="assets" inherit_id="point_of_sale.assets"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/pos_restaurant/static/lib/js/jquery.ui.touch-punch.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/multiprint.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/floors.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/notes.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Resizeable.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/BillScreen.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Chrome.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js"></script> + </xpath> + <xpath expr="//link[@id='pos-stylesheet']" position="after"> + <link rel="stylesheet" href="/pos_restaurant/static/src/css/restaurant.css"/> + </xpath> + </template> <template id="assets_backend" name="hr assets" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> @@ -23,7 +40,23 @@ <template id="assets_tests" name="POS Restaurant Assets Tests" inherit_id="web.assets_tests"> <xpath expr="." position="inside"> - <script type="text/javascript" src="/pos_restaurant/static/src/js/tours/pos_restaurant.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/pos_restaurant.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/ControlButtons.tour.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/FloorScreen.tour.js"></script> + </xpath> + </template> + + <template id="pos_restaurant.qunit_suite_tests" inherit_id="point_of_sale.qunit_suite_tests"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/pos_restaurant/static/tests/unit/test_FloorScreen.js"></script> </xpath> </template> |
