diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/static/src | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/static/src')
55 files changed, 4091 insertions, 0 deletions
diff --git a/addons/pos_restaurant/static/src/css/restaurant.css b/addons/pos_restaurant/static/src/css/restaurant.css new file mode 100644 index 00000000..e2943473 --- /dev/null +++ b/addons/pos_restaurant/static/src/css/restaurant.css @@ -0,0 +1,583 @@ +/* --- Restaurant Specific CSS --- */ + +.screen .screen-content-flexbox { + margin: 0px auto; + text-align: left; + height: 100%; + overflow: hidden; + position: relative; + display: -webkit-flex; + -webkit-flex-flow: column nowrap; + flex-flow: column nowrap; +} + +/* ------ FLOOR SELECTOR ------- */ + +.floor-selector { + line-height: 48px; + font-size: 18px; + display: -webkit-flex; + display: flex; + text-align: center; + width: 100%; +} +.floor-selector .button { + cursor: pointer; + border-left: dashed 1px rgb(196,196,196); + -webkit-flex: 1; + flex: 1; +} +.floor-selector .button:first-child { + border-left: none; +} +.floor-selector .button.active { + background: #6EC89B; + color: white; +} + +/* ------ FLOOR MAP ------- */ + +.floor-map { + -webkit-flex: 1; + flex: 1; + position: relative; + width: auto; + height: 100%; + box-shadow: 0px 6px 0px -3px rgba(0,0,0,0.07) inset; + background: #D8D7D7; + background-repeat: no-repeat; + overflow: auto; + background-size: cover; + transition: all 300ms ease-in-out; +} + +.floor-map .tables { + position: relative; +} +@media screen and (min-width: 1024px) { + .floor-map .tables { + max-width: 1024px; + margin: auto; + max-height: 540px; + border-radius: 0px 0px 6px 6px; + border: dashed 2px rgba(0,0,0,0.1); + border-top: none; + height: 100%; + } +} + +.floor-map .table{ + position: absolute; + text-align: center; + font-size: 18px; + color: white; + background: rgb(53, 211, 116); + border-radius: 3px; + cursor: pointer; + box-shadow: 0px 3px rgba(0,0,0,0.07); + transition: background, background-color 300ms ease-in-out; + overflow: hidden; +} +.floor-map .table .table-cover { + display: block; + position: absolute; + left: 0; right: 0; bottom: 0; + border-radius: 0px 0px 3px 3px; + background: rgba(0,0,0,0.2); +} +.floor-map .table .table-cover.full { + border-radius: 3px 3px 3px 3px; +} +.floor-map .table .table-seats { + position: absolute; + display: inline-block; + bottom: 0; + left: 50%; + height: 20px; + width: 20px; + line-height: 20px; + font-size: 16px; + border-radius: 50%; + margin-left: -10px; + margin-bottom: 4px; + background: black; + color: white; + opacity: 0.2; + z-index: 3; +} +.floor-map .table .label { + display: block; + max-height: 100%; + overflow: hidden; + position: relative; + bottom: 5px; +} +.floor-map .table.selected { + outline: solid rgba(255,255,255,0.3); + cursor: move; + z-index: 50; +} +.floor-map .edit-button.editing { + position: fixed; + top: 0; + right: 0; + font-size: 20px; + margin: 8px; + line-height: 32px; + width: 32px; + text-align: center; + border-radius: 5px; + cursor: pointer; + border: solid 1px rgba(0,0,0,0.2); +} +.floor-map .edit-button.editing.active { + background: #444; + border-color: transparent; + color: white; +} +.floor-map .edit-bar { + position: fixed; + top: 0; + right: 40px; + margin: 8px; + line-height: 34px; + text-align: center; + border-radius: 5px; + cursor: pointer; + font-size: 20px; + background: rgba(255,255,255,0.5); + z-index: 100; +} +.floor-map .edit-bar .edit-button { + position: relative; + width: 32px; + display: inline-block; + cursor: pointer; + margin-right: -4px; + border-right: solid 1px rgba(0,0,0,0.2); + transition: all 150ms linear; +} +.floor-map .edit-bar .edit-button.disabled { + cursor: default; +} +.floor-map .edit-bar .edit-button.disabled > * { + opacity: 0.5; +} +.floor-map .edit-bar .color-picker { + position: absolute; + left: 36px; + top: 40px; + width: 180px; + height: 180px; + border-radius: 3px; + z-index: 100; +} +.floor-map .edit-bar .color-picker .color { + display: block; + float: left; + cursor: pointer; + width: 60px; + height: 60px; + background-color: gray; +} +.floor-map .edit-bar .color-picker .color.tl { border-top-left-radius: 3px; } +.floor-map .edit-bar .color-picker .color.tr { border-top-right-radius: 3px; } +.floor-map .edit-bar .color-picker .color.bl { border-bottom-left-radius: 3px; } +.floor-map .edit-bar .color-picker .color.br { border-bottom-right-radius: 3px; } + +.floor-map .edit-bar .close-picker { + position: absolute; + bottom: 0; + left: 50%; + margin-left: -16px; + margin-bottom: -16px; + width: 32px; + height: 32px; + line-height: 32px; + text-align: center; + font-size: 20px; + border-radius: 16px; + background: black; + color: white; + cursor: pointer; +} + +.floor-map .edit-bar .edit-button:last-child { + margin-right: 0; + border-right: none; +} + +.floor-map .table.selected .table-handle { + padding: 0px; + position: absolute; + width: 48px; + height: 48px; + left: 50%; + top: 50%; + border-radius: 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; +} +.floor-map .table .table-handle.top { top: 0; } +.floor-map .table .table-handle.bottom { top: 100%; } +.floor-map .table .table-handle.left { left: 0; } +.floor-map .table .table-handle.right { left: 100%; } + +.floor-map .table .order-count { + position: absolute; + top: 0; + left: 50%; + background: black; + width: 20px; + margin-top: 1px; + margin-left: -10px; + height: 20px; + line-height: 20px; + border-radius: 10px; + font-size: 16px; + z-index: 10; +} +.floor-map .table .order-count.notify-printing { + background: red; +} +.floor-map .table .order-count.notify-skipped { + background: blue; +} + +.floor-map .empty-floor { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: 400px; + height: 40px; + font-size: 18px; + text-align: center; + opacity: 0.6; +} +.floor-map .empty-floor i { + display: inline-block; + padding: 6px 7px 3px; + margin: 0px 3px; + background: rgba(255,255,255,0.5); + border-radius: 3px; +} + + +/* ------ FLOOR BUTTON IN THE ORDER SELECTOR ------- */ + +.pos .order-button.floor-button { + background: #6EC89B; + font-weight: bold; + font-size: 16px; + min-width: 128px; + padding-left: 16px; + padding-right: 16px; +} +.pos .order-button.floor-button .table-name { + font-weight: normal; +} +.pos .order-button.floor-button .fa{ + font-size: 24px; +} +/* ------ ORDER LINE STATUS ------- */ + +.pos .order .orderline.dirty { + border-left: solid 6px #6EC89B; + color: #6EC89B; + padding-left: 9px; +} +.pos .order .orderline.skip { + border-left: solid 6px #7F82AC; + color: #7F82AC; + padding-left: 9px; +} + + +/* ------ ORDER NOTES ------- */ + +.pos .order .orderline-note { + margin: 8px; +} +.orderline-note .fa { + opacity: 0.5; + margin-right: 4px; +} + +/* ------ SPLIT BILL SCREEN ------- */ + +.splitbill-screen.screen .contents { + display: flex; + flex-flow: column nowrap; + margin: 0px auto; + max-width: 1024px; + border-left: dashed 1px rgb(215,215,215); + border-right: dashed 1px rgb(215,215,215); + height: 100%; +} +.splitbill-screen.screen .main { + display: flex; + flex-flow: row nowrap; + /* take the remaining vertical space */ + flex: 1; + /* do not capture overflow in this element */ + overflow: hidden; +} +.splitbill-screen.screen .main .lines { + display: flex; + flex: 1; + justify-content: center; + /* show scrollbar inside this element if its content overflows */ + overflow-y: auto; +} +.splitbill-screen.screen .main .controls { + display: flex; + flex-flow: column nowrap; +} +.splitbill-screen.screen .pay-button { + margin: 16px; +} +.splitbill-screen.screen .pay-button .button { + background: #e2e2e2; + line-height: 74px; + font-size: 16px; + border: solid 1px rgb(202, 202, 202); + border-top-width: 0px; + cursor: pointer; + width: 100%; + text-align: center; +} +.splitbill-screen.screen .pay-button .button:first-child { + border-top-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.splitbill-screen.screen .pay-button .button:last-child { + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} +.splitbill-screen.screen .pay-button .button:active { + background: black; + border-color: black; + color: white; +} + +.splitbill-screen.screen .main .lines { + border-right: dashed 1px rgb(215,215,215); +} + +.splitbill-screen.screen .main .controls { + flex: 1; +} + +/* ------ NARROWER SCREEN ------ */ +@media screen and (max-width: 768px) { + .splitbill-screen.screen .main { + flex-flow: column nowrap; + } + .splitbill-screen.screen .main .controls { + border-top: dashed 1px rgb(215,215,215); + flex: none; + } +} + +.tip-screen .tip-options .total-amount { + text-align: center; + font-size: x-large; + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.tip-screen .tip-amount-options { + background: white; + border-radius: 3px; + margin-left: 4rem; + margin-right: 4rem; +} + +.tip-screen .percentage-amounts { + display: flex; +} + +.tip-screen .tip-options .button { + background: #FAFAFA; + border: solid 1px rgb(209, 209, 209); + border-radius: 3px; + flex: 1; + margin: 1rem; + display: flex; + flex-flow: column nowrap; + justify-content: center; + cursor: pointer; +} + +.tip-screen .tip-options .button .percentage { + text-align: center; + font-size: xx-large; + color: #2196F3; + padding-top: 4rem; + padding-bottom: 0.75rem; +} + +.tip-screen .tip-options .button .amount { + text-align: center; + font-size: large; + color: #757575; + padding-bottom: 4rem; +} + +.tip-screen .custom-amount-form { + display: flex; +} + +.tip-screen .custom-amount-form .item { + font-size: x-large; + color: #2196F3; + margin: 1rem; + flex: 1 +} + +.tip-screen .custom-amount-form .label { + padding-top: 1.5rem; + padding-bottom: 1.5rem; + text-align: center; +} + +.tip-screen .custom-amount-form .input { + background: #FAFAFA; + border: solid 1px rgb(209, 209, 209); + border-radius: 3px; + margin: 1rem; + text-align: right; + overflow: hidden; + position: relative; +} + +.tip-screen .custom-amount-form .input .currency { + position: absolute; + display: flex; + right: 0; + top: 0; + bottom: 0; + width: 3rem; + justify-content: center; + align-items: center; +} + +.tip-screen .custom-amount-form .input input { + border: none; + padding: 0; + outline: none; + font-size: x-large; + text-align: right; + margin-left: 1rem; + margin-right: 3rem; + height: 100%; + width: calc(100% - 4rem); + background: #FAFAFA; + color: #2196F3; +} + +.tip-screen .custom-amount-form .input input:focus { + border: none; + padding: 0; + outline: none; +} + +.tip-screen .custom-amount-form .add { + text-align: center; +} + +.tip-screen .no-tip { + display: flex; + color: #2196F3; + font-size: x-large; +} + +.tip-screen .no-tip .button { + text-align: center; + flex: 1; + padding: 1.5rem; +} + +.tip-screen .pos-receipt-container { + display: none; + position: fixed; + top: 0; + left: 0; + margin: 0; +} + +@media print { + .tip-screen .pos-receipt-container { + display: block; + } + .tip-screen .pos-receipt-container * { + visibility: visible; + } +} + +.pos-receipt .tip-form { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.pos-receipt .tip-form .title { + text-align: center; +} + +.pos-receipt .tip-form > div { + margin-top: 1rem; +} + +.pos-receipt .tip-form .option { + display: flex; + flex-flow: column nowrap; + align-items: center; + flex: 1; + justify-content: center; +} + +.pos-receipt .tip-form .percentage-options { + display: flex; + flex-flow: row nowrap; +} + +.ticket-screen .tip-cell { + height: 100%; + width: 100%; + text-align: right; + white-space: nowrap; +} + +.ticket-screen .tip-cell input { + width: 100%; + text-align: right; + font-size: medium; + color: #555555; + padding-bottom: 5px; + border: none; + outline: none; +} + +.ticket-screen .tip-cell input:focus { + border: none; + outline: none; + border-bottom: solid 1px #555555; +} + +.multiprint-flex { + display: flex; + justify-content: space-between; +} diff --git a/addons/pos_restaurant/static/src/js/Chrome.js b/addons/pos_restaurant/static/src/js/Chrome.js new file mode 100644 index 00000000..949082f1 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Chrome.js @@ -0,0 +1,99 @@ +odoo.define('pos_restaurant.chrome', function (require) { + 'use strict'; + + const Chrome = require('point_of_sale.Chrome'); + const Registries = require('point_of_sale.Registries'); + + const NON_IDLE_EVENTS = 'mousemove mousedown touchstart touchend touchmove click scroll keypress'.split(/\s+/); + let IDLE_TIMER_SETTER; + + const PosResChrome = (Chrome) => + class extends Chrome { + /** + * @override + */ + async start() { + await super.start(); + if (this.env.pos.config.iface_floorplan) { + this._setActivityListeners(); + } + } + /** + * @override + * Do not set `FloorScreen` to the order. + */ + _setScreenData(name) { + if (name === 'FloorScreen') return; + super._setScreenData(...arguments); + } + /** + * @override + * `FloorScreen` is the start screen if there are floors. + */ + get startScreen() { + if (this.env.pos.config.iface_floorplan) { + const table = this.env.pos.table; + return { name: 'FloorScreen', props: { floor: table ? table.floor : null } }; + } else { + return super.startScreen; + } + } + /** + * @override + * Order is set to null when table is selected. There is no saved + * screen for null order so show `FloorScreen` instead. + */ + _showSavedScreen(pos, newSelectedOrder) { + if (!newSelectedOrder) { + this.showScreen('FloorScreen', { floor: pos.table ? pos.table.floor : null }); + } else { + super._showSavedScreen(pos, newSelectedOrder); + } + } + _setActivityListeners() { + IDLE_TIMER_SETTER = this._setIdleTimer.bind(this); + for (const event of NON_IDLE_EVENTS) { + window.addEventListener(event, IDLE_TIMER_SETTER); + } + } + _setIdleTimer() { + if (this._shouldResetIdleTimer()) { + clearTimeout(this.idleTimer); + this.idleTimer = setTimeout(() => { + this._actionAfterIdle(); + }, 60000); + } + } + _actionAfterIdle() { + if (this.tempScreen.isShown) { + this.trigger('close-temp-screen'); + } + const table = this.env.pos.table; + this.showScreen('FloorScreen', { floor: table ? table.floor : null }); + } + _shouldResetIdleTimer() { + return this.env.pos.config.iface_floorplan && this.mainScreen.name !== 'FloorScreen'; + } + __showScreen() { + super.__showScreen(...arguments); + this._setIdleTimer(); + } + /** + * @override + * Before closing pos, we remove the event listeners set on window + * for detecting activities outside FloorScreen. + */ + async _closePos() { + if (IDLE_TIMER_SETTER) { + for (const event of NON_IDLE_EVENTS) { + window.removeEventListener(event, IDLE_TIMER_SETTER); + } + } + await super._closePos(); + } + }; + + Registries.Component.extend(Chrome, PosResChrome); + + return Chrome; +}); diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js new file mode 100644 index 00000000..c2b62350 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js @@ -0,0 +1,34 @@ +odoo.define('pos_restaurant.BackToFloorButton', function (require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + const { posbus } = require('point_of_sale.utils'); + + class BackToFloorButton extends PosComponent { + mounted() { + posbus.on('table-set', this, this.render); + } + willUnmount() { + posbus.on('table-set', this); + } + get table() { + return (this.env.pos && this.env.pos.table) || null; + } + get floor() { + const table = this.table; + return table ? table.floor : null; + } + get hasTable() { + return this.table !== null; + } + backToFloorScreen() { + this.showScreen('FloorScreen', { floor: this.floor }); + } + } + BackToFloorButton.template = 'BackToFloorButton'; + + Registries.Component.add(BackToFloorButton); + + return BackToFloorButton; +}); diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js new file mode 100644 index 00000000..1e66bf91 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js @@ -0,0 +1,33 @@ +odoo.define('pos_restaurant.TicketButton', function (require) { + 'use strict'; + + const TicketButton = require('point_of_sale.TicketButton'); + const Registries = require('point_of_sale.Registries'); + const { posbus } = require('point_of_sale.utils'); + + const PosResTicketButton = (TicketButton) => + class extends TicketButton { + mounted() { + posbus.on('table-set', this, this.render); + } + willUnmount() { + posbus.off('table-set', this); + } + /** + * If no table is set to pos, which means the current main screen + * is floor screen, then the order count should be based on all the orders. + */ + get count() { + if (!this.env.pos || !this.env.pos.config) return 0; + if (this.env.pos.config.iface_floorplan && !this.env.pos.table) { + return this.env.pos.get('orders').models.length; + } else { + return super.count; + } + } + }; + + Registries.Component.extend(TicketButton, PosResTicketButton); + + return TicketButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Resizeable.js b/addons/pos_restaurant/static/src/js/Resizeable.js new file mode 100644 index 00000000..c651ae09 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Resizeable.js @@ -0,0 +1,330 @@ +odoo.define('pos_restaurant.Resizeable', function(require) { + 'use strict'; + + const { useExternalListener } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class Resizeable extends PosComponent { + constructor() { + super(...arguments); + + useExternalListener(document, 'mousemove', this.resizeN); + useExternalListener(document, 'mouseup', this.endResizeN); + useListener('mousedown', '.resize-handle-n', this.startResizeN); + + useExternalListener(document, 'mousemove', this.resizeS); + useExternalListener(document, 'mouseup', this.endResizeS); + useListener('mousedown', '.resize-handle-s', this.startResizeS); + + useExternalListener(document, 'mousemove', this.resizeW); + useExternalListener(document, 'mouseup', this.endResizeW); + useListener('mousedown', '.resize-handle-w', this.startResizeW); + + useExternalListener(document, 'mousemove', this.resizeE); + useExternalListener(document, 'mouseup', this.endResizeE); + useListener('mousedown', '.resize-handle-e', this.startResizeE); + + useExternalListener(document, 'mousemove', this.resizeNW); + useExternalListener(document, 'mouseup', this.endResizeNW); + useListener('mousedown', '.resize-handle-nw', this.startResizeNW); + + useExternalListener(document, 'mousemove', this.resizeNE); + useExternalListener(document, 'mouseup', this.endResizeNE); + useListener('mousedown', '.resize-handle-ne', this.startResizeNE); + + useExternalListener(document, 'mousemove', this.resizeSW); + useExternalListener(document, 'mouseup', this.endResizeSW); + useListener('mousedown', '.resize-handle-sw', this.startResizeSW); + + useExternalListener(document, 'mousemove', this.resizeSE); + useExternalListener(document, 'mouseup', this.endResizeSE); + useListener('mousedown', '.resize-handle-se', this.startResizeSE); + + useExternalListener(document, 'touchmove', this.resizeN); + useExternalListener(document, 'touchend', this.endResizeN); + useListener('touchstart', '.resize-handle-n', this.startResizeN); + + useExternalListener(document, 'touchmove', this.resizeS); + useExternalListener(document, 'touchend', this.endResizeS); + useListener('touchstart', '.resize-handle-s', this.startResizeS); + + useExternalListener(document, 'touchmove', this.resizeW); + useExternalListener(document, 'touchend', this.endResizeW); + useListener('touchstart', '.resize-handle-w', this.startResizeW); + + useExternalListener(document, 'touchmove', this.resizeE); + useExternalListener(document, 'touchend', this.endResizeE); + useListener('touchstart', '.resize-handle-e', this.startResizeE); + + useExternalListener(document, 'touchmove', this.resizeNW); + useExternalListener(document, 'touchend', this.endResizeNW); + useListener('touchstart', '.resize-handle-nw', this.startResizeNW); + + useExternalListener(document, 'touchmove', this.resizeNE); + useExternalListener(document, 'touchend', this.endResizeNE); + useListener('touchstart', '.resize-handle-ne', this.startResizeNE); + + useExternalListener(document, 'touchmove', this.resizeSW); + useExternalListener(document, 'touchend', this.endResizeSW); + useListener('touchstart', '.resize-handle-sw', this.startResizeSW); + + useExternalListener(document, 'touchmove', this.resizeSE); + useExternalListener(document, 'touchend', this.endResizeSE); + useListener('touchstart', '.resize-handle-se', this.startResizeSE); + + this.size = { height: 0, width: 0 }; + this.loc = { top: 0, left: 0 }; + this.tempSize = {}; + } + mounted() { + this.limitArea = this.props.limitArea + ? document.querySelector(this.props.limitArea) + : this.el.offsetParent; + this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect(); + if (this.limitArea === this.el.offsetParent) { + this.limitLeft = 0; + this.limitTop = 0; + this.limitRight = this.limitAreaBoundingRect.width; + this.limitBottom = this.limitAreaBoundingRect.height; + } else { + this.limitLeft = -this.el.offsetParent.offsetLeft; + this.limitTop = -this.el.offsetParent.offsetTop; + this.limitRight = + this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft; + this.limitBottom = + this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop; + } + this.limitAreaWidth = this.limitAreaBoundingRect.width; + this.limitAreaHeight = this.limitAreaBoundingRect.height; + } + startResizeN(event) { + let realEvent; + if (event instanceof CustomEvent) { + realEvent = event.detail; + } else { + realEvent = event; + } + const { y } = this._getEventLoc(realEvent); + this.isResizingN = true; + this.startY = y; + this.size.height = this.el.offsetHeight; + this.loc.top = this.el.offsetTop; + event.stopPropagation(); + } + resizeN(event) { + if (this.isResizingN) { + const { y: newY } = this._getEventLoc(event); + let dY = newY - this.startY; + if (dY < 0 && Math.abs(dY) > this.loc.top) { + dY = -this.loc.top; + } else if (dY > 0 && dY > this.size.height) { + dY = this.size.height; + } + this.el.style.height = `${this.size.height - dY}px`; + this.el.style.top = `${this.loc.top + dY}px`; + } + } + endResizeN() { + if (this.isResizingN && !this.isResizingE && !this.isResizingW && !this.isResizingS) { + this.isResizingN = false; + this._triggerResizeEnd(); + } + } + startResizeS(event) { + let realEvent; + if (event instanceof CustomEvent) { + realEvent = event.detail; + } else { + realEvent = event; + } + const { y } = this._getEventLoc(realEvent); + this.isResizingS = true; + this.startY = y; + this.size.height = this.el.offsetHeight; + this.loc.top = this.el.offsetTop; + event.stopPropagation(); + } + resizeS(event) { + if (this.isResizingS) { + const { y: newY } = this._getEventLoc(event); + let dY = newY - this.startY; + if (dY > 0 && dY > this.limitAreaHeight - (this.size.height + this.loc.top)) { + dY = this.limitAreaHeight - (this.size.height + this.loc.top); + } else if (dY < 0 && Math.abs(dY) > this.size.height) { + dY = -this.size.height; + } + this.el.style.height = `${this.size.height + dY}px`; + } + } + endResizeS() { + if (!this.isResizingN && !this.isResizingE && !this.isResizingW && this.isResizingS) { + this.isResizingS = false; + this._triggerResizeEnd(); + } + } + startResizeW(event) { + let realEvent; + if (event instanceof CustomEvent) { + realEvent = event.detail; + } else { + realEvent = event; + } + const { x } = this._getEventLoc(realEvent); + this.isResizingW = true; + this.startX = x; + this.size.width = this.el.offsetWidth; + this.loc.left = this.el.offsetLeft; + event.stopPropagation(); + } + resizeW(event) { + if (this.isResizingW) { + const { x: newX } = this._getEventLoc(event); + let dX = newX - this.startX; + if (dX > 0 && dX > this.size.width) { + dX = this.size.width; + } else if (dX < 0 && Math.abs(dX) > this.loc.left + Math.abs(this.limitLeft)) { + dX = -this.loc.left + this.limitLeft; + } + this.el.style.width = `${this.size.width - dX}px`; + this.el.style.left = `${this.loc.left + dX}px`; + } + } + endResizeW() { + if (!this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) { + this.isResizingW = false; + this._triggerResizeEnd(); + } + } + startResizeE(event) { + let realEvent; + if (event instanceof CustomEvent) { + realEvent = event.detail; + } else { + realEvent = event; + } + const { x } = this._getEventLoc(realEvent); + this.isResizingE = true; + this.startX = x; + this.size.width = this.el.offsetWidth; + this.loc.left = this.el.offsetLeft; + event.stopPropagation(); + } + resizeE(event) { + if (this.isResizingE) { + const { x: newX } = this._getEventLoc(event); + let dX = newX - this.startX; + if ( + dX > 0 && + dX > + this.limitAreaWidth - + (this.size.width + this.loc.left + Math.abs(this.limitLeft)) + ) { + dX = + this.limitAreaWidth - + (this.size.width + this.loc.left + Math.abs(this.limitLeft)); + } else if (dX < 0 && Math.abs(dX) > this.size.width) { + dX = -this.size.width; + } + this.el.style.width = `${this.size.width + dX}px`; + } + } + endResizeE() { + if (!this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) { + this.isResizingE = false; + this._triggerResizeEnd(); + } + } + startResizeNW(event) { + this.startResizeN(event); + this.startResizeW(event); + } + resizeNW(event) { + this.resizeN(event); + this.resizeW(event); + } + endResizeNW() { + if (this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) { + this.isResizingN = false; + this.isResizingW = false; + this._triggerResizeEnd(); + } + } + startResizeNE(event) { + this.startResizeN(event); + this.startResizeE(event); + } + resizeNE(event) { + this.resizeN(event); + this.resizeE(event); + } + endResizeNE() { + if (this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) { + this.isResizingN = false; + this.isResizingE = false; + this._triggerResizeEnd(); + } + } + startResizeSE(event) { + this.startResizeS(event); + this.startResizeE(event); + } + resizeSE(event) { + this.resizeS(event); + this.resizeE(event); + } + endResizeSE() { + if (!this.isResizingN && this.isResizingE && !this.isResizingW && this.isResizingS) { + this.isResizingS = false; + this.isResizingE = false; + this._triggerResizeEnd(); + } + } + startResizeSW(event) { + this.startResizeS(event); + this.startResizeW(event); + } + resizeSW(event) { + this.resizeS(event); + this.resizeW(event); + } + endResizeSW() { + if (!this.isResizingN && !this.isResizingE && this.isResizingW && this.isResizingS) { + this.isResizingS = false; + this.isResizingW = false; + this._triggerResizeEnd(); + } + } + _getEventLoc(event) { + let coordX, coordY; + if (event.touches && event.touches[0]) { + coordX = event.touches[0].clientX; + coordY = event.touches[0].clientY; + } else { + coordX = event.clientX; + coordY = event.clientY; + } + return { + x: coordX, + y: coordY, + }; + } + _triggerResizeEnd() { + const size = { + height: this.el.offsetHeight, + width: this.el.offsetWidth, + }; + const loc = { + top: this.el.offsetTop, + left: this.el.offsetLeft, + }; + this.trigger('resize-end', { size, loc }); + } + } + Resizeable.template = 'Resizeable'; + + Registries.Component.add(Resizeable); + + return Resizeable; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/BillScreen.js b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js new file mode 100644 index 00000000..8ecd7805 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/BillScreen.js @@ -0,0 +1,31 @@ +odoo.define('pos_restaurant.BillScreen', function (require) { + 'use strict'; + + const ReceiptScreen = require('point_of_sale.ReceiptScreen'); + const Registries = require('point_of_sale.Registries'); + + const BillScreen = (ReceiptScreen) => { + class BillScreen extends ReceiptScreen { + confirm() { + this.props.resolve({ confirmed: true, payload: null }); + this.trigger('close-temp-screen'); + } + whenClosing() { + this.confirm(); + } + /** + * @override + */ + async printReceipt() { + await super.printReceipt(); + this.currentOrder._printed = false; + } + } + BillScreen.template = 'BillScreen'; + return BillScreen; + }; + + Registries.Component.addByExtending(BillScreen, ReceiptScreen); + + return BillScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js new file mode 100644 index 00000000..43b16d93 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js @@ -0,0 +1,19 @@ +odoo.define('pos_restaurant.EditBar', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + const { useState } = owl.hooks; + + class EditBar extends PosComponent { + constructor() { + super(...arguments); + this.state = useState({ isColorPicker: false }) + } + } + EditBar.template = 'EditBar'; + + Registries.Component.add(EditBar); + + return EditBar; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js new file mode 100644 index 00000000..4aeb781d --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js @@ -0,0 +1,60 @@ +odoo.define('pos_restaurant.EditableTable', function(require) { + 'use strict'; + + const { onPatched, onMounted } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class EditableTable extends PosComponent { + constructor() { + super(...arguments); + useListener('resize-end', this._onResizeEnd); + useListener('drag-end', this._onDragEnd); + onPatched(this._setElementStyle.bind(this)); + onMounted(this._setElementStyle.bind(this)); + } + _setElementStyle() { + const table = this.props.table; + function unit(val) { + return `${val}px`; + } + const style = { + width: unit(table.width), + height: unit(table.height), + 'line-height': unit(table.height), + top: unit(table.position_v), + left: unit(table.position_h), + 'border-radius': table.shape === 'round' ? unit(1000) : '3px', + }; + if (table.color) { + style.background = table.color; + } + if (table.height >= 150 && table.width >= 150) { + style['font-size'] = '32px'; + } + Object.assign(this.el.style, style); + } + _onResizeEnd(event) { + const { size, loc } = event.detail; + const table = this.props.table; + table.width = size.width; + table.height = size.height; + table.position_v = loc.top; + table.position_h = loc.left; + this.trigger('save-table', this.props.table); + } + _onDragEnd(event) { + const { loc } = event.detail; + const table = this.props.table; + table.position_v = loc.top; + table.position_h = loc.left; + this.trigger('save-table', this.props.table); + } + } + EditableTable.template = 'EditableTable'; + + Registries.Component.add(EditableTable); + + return EditableTable; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js new file mode 100644 index 00000000..7c46f22b --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -0,0 +1,315 @@ +odoo.define('pos_restaurant.FloorScreen', function (require) { + 'use strict'; + + const { debounce } = owl.utils; + const PosComponent = require('point_of_sale.PosComponent'); + const { useState, useRef } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class FloorScreen extends PosComponent { + /** + * @param {Object} props + * @param {Object} props.floor + */ + constructor() { + super(...arguments); + this._setTableColor = debounce(this._setTableColor, 70); + this._setFloorColor = debounce(this._setFloorColor, 70); + useListener('select-table', this._onSelectTable); + useListener('deselect-table', this._onDeselectTable); + useListener('save-table', this._onSaveTable); + useListener('create-table', this._createTable); + useListener('duplicate-table', this._duplicateTable); + useListener('rename-table', this._renameTable); + useListener('change-seats-num', this._changeSeatsNum); + useListener('change-shape', this._changeShape); + useListener('set-table-color', this._setTableColor); + useListener('set-floor-color', this._setFloorColor); + useListener('delete-table', this._deleteTable); + const floor = this.props.floor ? this.props.floor : this.env.pos.floors[0]; + this.state = useState({ + selectedFloorId: floor.id, + selectedTableId: null, + isEditMode: false, + floorBackground: floor.background_color, + floorMapScrollTop: 0, + }); + this.floorMapRef = useRef('floor-map-ref'); + } + patched() { + this.floorMapRef.el.style.background = this.state.floorBackground; + this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; + } + mounted() { + if (this.env.pos.table) { + this.env.pos.set_table(null); + } + this.floorMapRef.el.style.background = this.state.floorBackground; + this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; + // call _tableLongpolling once then set interval of 5sec. + this._tableLongpolling(); + this.tableLongpolling = setInterval(this._tableLongpolling.bind(this), 5000); + } + willUnmount() { + clearInterval(this.tableLongpolling); + } + get activeFloor() { + return this.env.pos.floors_by_id[this.state.selectedFloorId]; + } + get activeTables() { + return this.activeFloor.tables; + } + get isFloorEmpty() { + return this.activeTables.length === 0; + } + get selectedTable() { + return this.state.selectedTableId !== null + ? this.env.pos.tables_by_id[this.state.selectedTableId] + : false; + } + selectFloor(floor) { + this.state.selectedFloorId = floor.id; + this.state.floorBackground = this.activeFloor.background_color; + this.state.isEditMode = false; + this.state.selectedTableId = null; + } + toggleEditMode() { + this.state.isEditMode = !this.state.isEditMode; + this.state.selectedTableId = null; + } + async _createTable() { + const newTable = await this._createTableHelper(); + if (newTable) { + this.state.selectedTableId = newTable.id; + } + } + async _duplicateTable() { + if (!this.selectedTable) return; + const newTable = await this._createTableHelper(this.selectedTable); + if (newTable) { + this.state.selectedTableId = newTable.id; + } + } + async _changeSeatsNum() { + const selectedTable = this.selectedTable + if (!selectedTable) return; + const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { + startingValue: selectedTable.seats, + cheap: true, + title: this.env._t('Number of Seats ?'), + }); + if (!confirmed) return; + const newSeatsNum = parseInt(inputNumber, 10) || selectedTable.seats; + if (newSeatsNum !== selectedTable.seats) { + selectedTable.seats = newSeatsNum; + await this._save(selectedTable); + } + } + async _changeShape() { + if (!this.selectedTable) return; + this.selectedTable.shape = this.selectedTable.shape === 'square' ? 'round' : 'square'; + this.render(); + await this._save(this.selectedTable); + } + async _renameTable() { + const selectedTable = this.selectedTable; + if (!selectedTable) return; + const { confirmed, payload: newName } = await this.showPopup('TextInputPopup', { + startingValue: selectedTable.name, + title: this.env._t('Table Name ?'), + }); + if (!confirmed) return; + if (newName !== selectedTable.name) { + selectedTable.name = newName; + await this._save(selectedTable); + } + } + async _setTableColor({ detail: color }) { + this.selectedTable.color = color; + this.render(); + await this._save(this.selectedTable); + } + async _setFloorColor({ detail: color }) { + this.state.floorBackground = color; + this.activeFloor.background_color = color; + try { + await this.rpc({ + model: 'restaurant.floor', + method: 'write', + args: [[this.activeFloor.id], { background_color: color }], + }); + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to change background color'), + }); + } else { + throw error; + } + } + } + async _deleteTable() { + if (!this.selectedTable) return; + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: this.env._t('Are you sure ?'), + body: this.env._t('Removing a table cannot be undone'), + }); + if (!confirmed) return; + try { + const originalSelectedTableId = this.state.selectedTableId; + await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [{ active: false, id: originalSelectedTableId }], + }); + this.activeFloor.tables = this.activeTables.filter( + (table) => table.id !== originalSelectedTableId + ); + // Value of an object can change inside async function call. + // Which means that in this code block, the value of `state.selectedTableId` + // before the await call can be different after the finishing the await call. + // Since we wanted to disable the selected table after deletion, we should be + // setting the selectedTableId to null. However, we only do this if nothing + // else is selected during the rpc call. + if (this.state.selectedTableId === originalSelectedTableId) { + this.state.selectedTableId = null; + } + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('OfflineErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to delete table'), + }); + } else { + throw error; + } + } + } + _onSelectTable(event) { + const table = event.detail; + if (this.state.isEditMode) { + this.state.selectedTableId = table.id; + } else { + this.env.pos.set_table(table); + } + } + _onDeselectTable() { + this.state.selectedTableId = null; + } + async _createTableHelper(copyTable) { + let newTable; + if (copyTable) { + newTable = Object.assign({}, copyTable); + newTable.position_h += 10; + newTable.position_v += 10; + } else { + newTable = { + position_v: 100, + position_h: 100, + width: 75, + height: 75, + shape: 'square', + seats: 1, + }; + } + newTable.name = this._getNewTableName(newTable.name); + delete newTable.id; + newTable.floor_id = [this.activeFloor.id, '']; + newTable.floor = this.activeFloor; + try { + await this._save(newTable); + this.activeTables.push(newTable); + return newTable; + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Offline'), + body: this.env._t('Unable to create table because you are offline.'), + }); + return; + } else { + throw error; + } + } + } + _getNewTableName(name) { + if (name) { + const num = Number((name.match(/\d+/g) || [])[0] || 0); + const str = name.replace(/\d+/g, ''); + const n = { num: num, str: str }; + n.num += 1; + this._lastName = n; + } else if (this._lastName) { + this._lastName.num += 1; + } else { + this._lastName = { num: 1, str: 'T' }; + } + return '' + this._lastName.str + this._lastName.num; + } + async _save(table) { + const fields = this.env.pos.models.find((model) => model.model === 'restaurant.table') + .fields; + const serializeTable = {}; + for (let field of fields) { + if (typeof table[field] !== 'undefined') { + serializeTable[field] = table[field]; + } + } + serializeTable.id = table.id; + const tableId = await this.rpc({ + model: 'restaurant.table', + method: 'create_from_ui', + args: [serializeTable], + }); + table.id = tableId; + this.env.pos.tables_by_id[tableId] = table; + } + async _onSaveTable(event) { + const table = event.detail; + await this._save(table); + } + async _tableLongpolling() { + if (this.state.isEditMode) { + return; + } + try { + const result = await this.rpc({ + model: 'pos.config', + method: 'get_tables_order_count', + args: [this.env.pos.config.id], + }); + result.forEach((table) => { + const table_obj = this.env.pos.tables_by_id[table.id]; + const unsynced_orders = this.env.pos + .get_table_orders(table_obj) + .filter( + (o) => + o.server_id === undefined && + (o.orderlines.length !== 0 || o.paymentlines.length !== 0) && + // do not count the orders that are already finalized + !o.finalized + ).length; + table_obj.order_count = table.orders + unsynced_orders; + }); + this.render(); + } catch (error) { + if (error.message.code < 0) { + await this.showPopup('OfflineErrorPopup', { + title: 'Offline', + body: 'Unable to get orders count', + }); + } else { + throw error; + } + } + } + } + FloorScreen.template = 'FloorScreen'; + FloorScreen.hideOrderSelector = true; + + Registries.Component.add(FloorScreen); + + return FloorScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js new file mode 100644 index 00000000..48dfdad7 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js @@ -0,0 +1,73 @@ +odoo.define('pos_restaurant.TableWidget', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class TableWidget extends PosComponent { + mounted() { + const table = this.props.table; + function unit(val) { + return `${val}px`; + } + const style = { + width: unit(table.width), + height: unit(table.height), + 'line-height': unit(table.height), + top: unit(table.position_v), + left: unit(table.position_h), + 'border-radius': table.shape === 'round' ? unit(1000) : '3px', + }; + if (table.color) { + style.background = table.color; + } + if (table.height >= 150 && table.width >= 150) { + style['font-size'] = '32px'; + } + Object.assign(this.el.style, style); + + const tableCover = this.el.querySelector('.table-cover'); + Object.assign(tableCover.style, { height: `${Math.ceil(this.fill * 100)}%` }); + } + get fill() { + const customerCount = this.env.pos.get_customer_count(this.props.table); + return Math.min(1, Math.max(0, customerCount / this.props.table.seats)); + } + get orderCount() { + const table = this.props.table; + return table.order_count !== undefined + ? table.order_count + : this.env.pos + .get_table_orders(table) + .filter(o => o.orderlines.length !== 0 || o.paymentlines.length !== 0).length; + } + get orderCountClass() { + const notifications = this._getNotifications(); + return { + 'order-count': true, + 'notify-printing': notifications.printing, + 'notify-skipped': notifications.skipped, + }; + } + _getNotifications() { + const orders = this.env.pos.get_table_orders(this.props.table); + + let hasChangesCount = 0; + let hasSkippedCount = 0; + for (let i = 0; i < orders.length; i++) { + if (orders[i].hasChangesToPrint()) { + hasChangesCount++; + } else if (orders[i].hasSkippedChanges()) { + hasSkippedCount++; + } + } + + return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {}; + } + } + TableWidget.template = 'TableWidget'; + + Registries.Component.add(TableWidget); + + return TableWidget; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js new file mode 100644 index 00000000..8753be25 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js @@ -0,0 +1,28 @@ +odoo.define('pos_restaurant.OrderManagementScreen', function (require) { + 'use strict'; + + const OrderManagementScreen = require('point_of_sale.OrderManagementScreen'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderManagementScreen = (OrderManagementScreen) => + class extends OrderManagementScreen { + /** + * @override + */ + _setOrder(order) { + if (this.env.pos.config.module_pos_restaurant) { + const currentOrder = this.env.pos.get_order(); + this.env.pos.set_table(order.table, order); + if (currentOrder && currentOrder.uid === order.uid) { + this.close(); + } + } else { + super._setOrder(order); + } + } + }; + + Registries.Component.extend(OrderManagementScreen, PosResOrderManagementScreen); + + return OrderManagementScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js new file mode 100644 index 00000000..f5a32114 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js @@ -0,0 +1,17 @@ +odoo.define('pos_restaurant.OrderRow', function (require) { + 'use strict'; + + const OrderRow = require('point_of_sale.OrderRow'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderRow = (OrderRow) => + class extends OrderRow { + get table() { + return this.order.table ? this.order.table.name : ''; + } + }; + + Registries.Component.extend(OrderRow, PosResOrderRow); + + return OrderRow; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js new file mode 100644 index 00000000..472f7cad --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js @@ -0,0 +1,48 @@ +odoo.define('pos_restaurant.PosResPaymentScreen', function (require) { + 'use strict'; + + const PaymentScreen = require('point_of_sale.PaymentScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + const PosResPaymentScreen = (PaymentScreen) => + class extends PaymentScreen { + constructor() { + super(...arguments); + useListener('send-payment-adjust', this._sendPaymentAdjust); + } + + async _sendPaymentAdjust({ detail: line }) { + const previous_amount = line.get_amount(); + const amount_diff = line.order.get_total_with_tax() - line.order.get_total_paid(); + line.set_amount(previous_amount + amount_diff); + line.set_payment_status('waiting'); + + const payment_terminal = line.payment_method.payment_terminal; + const isAdjustSuccessful = await payment_terminal.send_payment_adjust(line.cid); + if (isAdjustSuccessful) { + line.set_payment_status('done'); + } else { + line.set_amount(previous_amount); + line.set_payment_status('done'); + } + } + + get nextScreen() { + const order = this.currentOrder; + if (!this.env.pos.config.set_tip_after_payment || order.is_tipped) { + return super.nextScreen; + } + // Take the first payment method as the main payment. + const mainPayment = order.get_paymentlines()[0]; + if (mainPayment.canBeAdjusted()) { + return 'TipScreen'; + } + return super.nextScreen; + } + }; + + Registries.Component.extend(PaymentScreen, PosResPaymentScreen); + + return PosResPaymentScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js new file mode 100644 index 00000000..58681810 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js @@ -0,0 +1,42 @@ +odoo.define('pos_restaurant.OrderlineNoteButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class OrderlineNoteButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get selectedOrderline() { + return this.env.pos.get_order().get_selected_orderline(); + } + async onClick() { + if (!this.selectedOrderline) return; + + const { confirmed, payload: inputNote } = await this.showPopup('TextAreaPopup', { + startingValue: this.selectedOrderline.get_note(), + title: this.env._t('Add Note'), + }); + + if (confirmed) { + this.selectedOrderline.set_note(inputNote); + } + } + } + OrderlineNoteButton.template = 'OrderlineNoteButton'; + + ProductScreen.addControlButton({ + component: OrderlineNoteButton, + condition: function() { + return this.env.pos.config.module_pos_restaurant; + }, + }); + + Registries.Component.add(OrderlineNoteButton); + + return OrderlineNoteButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js new file mode 100644 index 00000000..7563d9fc --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js @@ -0,0 +1,38 @@ +odoo.define('pos_restaurant.PrintBillButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class PrintBillButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + async onClick() { + const order = this.env.pos.get_order(); + if (order.get_orderlines().length > 0) { + await this.showTempScreen('BillScreen'); + } else { + await this.showPopup('ErrorPopup', { + title: this.env._t('Nothing to Print'), + body: this.env._t('There are no order lines'), + }); + } + } + } + PrintBillButton.template = 'PrintBillButton'; + + ProductScreen.addControlButton({ + component: PrintBillButton, + condition: function() { + return this.env.pos.config.iface_printbill; + }, + }); + + Registries.Component.add(PrintBillButton); + + return PrintBillButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js new file mode 100644 index 00000000..58ace925 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js @@ -0,0 +1,33 @@ +odoo.define('pos_restaurant.SplitBillButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class SplitBillButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + async onClick() { + const order = this.env.pos.get_order(); + if (order.get_orderlines().length > 0) { + this.showScreen('SplitBillScreen'); + } + } + } + SplitBillButton.template = 'SplitBillButton'; + + ProductScreen.addControlButton({ + component: SplitBillButton, + condition: function() { + return this.env.pos.config.iface_splitbill; + }, + }); + + Registries.Component.add(SplitBillButton); + + return SplitBillButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js new file mode 100644 index 00000000..948410e5 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js @@ -0,0 +1,70 @@ +odoo.define('pos_restaurant.SubmitOrderButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + /** + * IMPROVEMENT: Perhaps this class is quite complicated for its worth. + * This is because it needs to listen to changes to the current order. + * Also, the current order changes when the selectedOrder in pos is changed. + * After setting new current order, we update the listeners. + */ + class SubmitOrderButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + this._currentOrder = this.env.pos.get_order(); + this._currentOrder.orderlines.on('change', this.render, this); + this.env.pos.on('change:selectedOrder', this._updateCurrentOrder, this); + } + willUnmount() { + this._currentOrder.orderlines.off('change', null, this); + this.env.pos.off('change:selectedOrder', null, this); + } + async onClick() { + const order = this.env.pos.get_order(); + if (order.hasChangesToPrint()) { + const isPrintSuccessful = await order.printChanges(); + if (isPrintSuccessful) { + order.saveChanges(); + } else { + await this.showPopup('ErrorPopup', { + title: 'Printing failed', + body: 'Failed in printing the changes in the order', + }); + } + } + } + get addedClasses() { + if (!this._currentOrder) return {}; + const changes = this._currentOrder.hasChangesToPrint(); + const skipped = changes ? false : this._currentOrder.hasSkippedChanges(); + return { + highlight: changes, + altlight: skipped, + }; + } + _updateCurrentOrder(pos, newSelectedOrder) { + this._currentOrder.orderlines.off('change', null, this); + if (newSelectedOrder) { + this._currentOrder = newSelectedOrder; + this._currentOrder.orderlines.on('change', this.render, this); + } + } + } + SubmitOrderButton.template = 'SubmitOrderButton'; + + ProductScreen.addControlButton({ + component: SubmitOrderButton, + condition: function() { + return this.env.pos.printers.length; + }, + }); + + Registries.Component.add(SubmitOrderButton); + + return SubmitOrderButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js new file mode 100644 index 00000000..e015496d --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js @@ -0,0 +1,44 @@ +odoo.define('pos_restaurant.TableGuestsButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class TableGuestsButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get nGuests() { + return this.currentOrder ? this.currentOrder.get_customer_count() : 0; + } + async onClick() { + const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', { + startingValue: this.nGuests, + cheap: true, + title: this.env._t('Guests ?'), + }); + + if (confirmed) { + this.env.pos.get_order().set_customer_count(parseInt(inputNumber, 10) || 1); + } + } + } + TableGuestsButton.template = 'TableGuestsButton'; + + ProductScreen.addControlButton({ + component: TableGuestsButton, + condition: function() { + return this.env.pos.config.module_pos_restaurant; + }, + }); + + Registries.Component.add(TableGuestsButton); + + return TableGuestsButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js new file mode 100644 index 00000000..ed72e2a7 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js @@ -0,0 +1,30 @@ +odoo.define('pos_restaurant.TransferOrderButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const ProductScreen = require('point_of_sale.ProductScreen'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + class TransferOrderButton extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + onClick() { + this.env.pos.transfer_order_to_different_table(); + } + } + TransferOrderButton.template = 'TransferOrderButton'; + + ProductScreen.addControlButton({ + component: TransferOrderButton, + condition: function() { + return this.env.pos.config.iface_floorplan; + }, + }); + + Registries.Component.add(TransferOrderButton); + + return TransferOrderButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js new file mode 100644 index 00000000..2204405c --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js @@ -0,0 +1,46 @@ +odoo.define('pos_restaurant.Orderline', function(require) { + 'use strict'; + + const Orderline = require('point_of_sale.Orderline'); + const Registries = require('point_of_sale.Registries'); + + const PosResOrderline = Orderline => + class extends Orderline { + /** + * @override + */ + get addedClasses() { + const res = super.addedClasses; + Object.assign(res, { + dirty: this.props.line.mp_dirty, + skip: this.props.line.mp_skip, + }); + return res; + } + /** + * @override + * if doubleclick, change mp_dirty to mp_skip + * + * IMPROVEMENT: Instead of handling both double click and click in single + * method, perhaps we can separate double click from single click. + */ + selectLine() { + const line = this.props.line; // the orderline + if (this.env.pos.get_order().selected_orderline !== line) { + this.mp_dbclk_time = new Date().getTime(); + } else if (!this.mp_dbclk_time) { + this.mp_dbclk_time = new Date().getTime(); + } else if (this.mp_dbclk_time + 500 > new Date().getTime()) { + line.set_skip(!line.mp_skip); + this.mp_dbclk_time = 0; + } else { + this.mp_dbclk_time = new Date().getTime(); + } + super.selectLine(); + } + }; + + Registries.Component.extend(Orderline, PosResOrderline); + + return Orderline; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js new file mode 100644 index 00000000..689c3bf4 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -0,0 +1,28 @@ +odoo.define('pos_restaurant.ReceiptScreen', function(require) { + 'use strict'; + + const ReceiptScreen = require('point_of_sale.ReceiptScreen'); + const Registries = require('point_of_sale.Registries'); + + const PosResReceiptScreen = ReceiptScreen => + class extends ReceiptScreen { + /** + * @override + */ + get nextScreen() { + if ( + this.env.pos.config.module_pos_restaurant && + this.env.pos.config.iface_floorplan + ) { + const table = this.env.pos.table; + return { name: 'FloorScreen', props: { floor: table ? table.floor : null } }; + } else { + return super.nextScreen; + } + } + }; + + Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); + + return ReceiptScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js new file mode 100644 index 00000000..f44af5bf --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -0,0 +1,197 @@ +odoo.define('pos_restaurant.SplitBillScreen', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const { useState } = owl.hooks; + const { useListener } = require('web.custom_hooks'); + const models = require('point_of_sale.models'); + const Registries = require('point_of_sale.Registries'); + + class SplitBillScreen extends PosComponent { + constructor() { + super(...arguments); + useListener('click-line', this.onClickLine); + this.splitlines = useState(this._initSplitLines(this.env.pos.get_order())); + this.newOrderLines = {}; + this.newOrder = new models.Order( + {}, + { + pos: this.env.pos, + temporary: true, + } + ); + this._isFinal = false; + } + mounted() { + this.env.pos.on('change:selectedOrder', this._resetState, this); + } + willUnmount() { + this.env.pos.off('change:selectedOrder', null, this); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get orderlines() { + return this.currentOrder.get_orderlines(); + } + onClickLine(event) { + const line = event.detail; + this._splitQuantity(line); + this._updateNewOrder(line); + } + back() { + this.showScreen('ProductScreen'); + } + proceed() { + if (_.isEmpty(this.splitlines)) + // Splitlines is empty + return; + + this._isFinal = true; + delete this.newOrder.temporary; + + if (this._isFullPayOrder()) { + this.showScreen('PaymentScreen'); + } else { + this._setQuantityOnCurrentOrder(); + + this.newOrder.set_screen_data({ name: 'PaymentScreen' }); + + // for the kitchen printer we assume that everything + // has already been sent to the kitchen before splitting + // the bill. So we save all changes both for the old + // order and for the new one. This is not entirely correct + // but avoids flooding the kitchen with unnecessary orders. + // Not sure what to do in this case. + + if (this.newOrder.saveChanges) { + this.currentOrder.saveChanges(); + this.newOrder.saveChanges(); + } + + this.newOrder.set_customer_count(1); + const newCustomerCount = this.currentOrder.get_customer_count() - 1; + this.currentOrder.set_customer_count(newCustomerCount || 1); + this.currentOrder.set_screen_data({ name: 'ProductScreen' }); + + this.env.pos.get('orders').add(this.newOrder); + this.env.pos.set('selectedOrder', this.newOrder); + } + } + /** + * @param {models.Order} order + * @returns {Object<{ quantity: number }>} splitlines + */ + _initSplitLines(order) { + const splitlines = {}; + for (let line of order.get_orderlines()) { + splitlines[line.id] = { product: line.get_product().id, quantity: 0 }; + } + return splitlines; + } + _splitQuantity(line) { + const split = this.splitlines[line.id]; + + let totalQuantity = 0; + + this.env.pos.get_order().get_orderlines().forEach(function(orderLine) { + if(orderLine.get_product().id === split.product) + totalQuantity += orderLine.get_quantity(); + }); + + if(line.get_quantity() > 0) { + if (!line.get_unit().is_pos_groupable) { + if (split.quantity !== line.get_quantity()) { + split.quantity = line.get_quantity(); + } else { + split.quantity = 0; + } + } else { + if (split.quantity < totalQuantity) { + split.quantity += line.get_unit().is_pos_groupable? 1: line.get_unit().rounding; + if (split.quantity > line.get_quantity()) { + split.quantity = line.get_quantity(); + } + } else { + split.quantity = 0; + } + } + } + } + _updateNewOrder(line) { + const split = this.splitlines[line.id]; + let orderline = this.newOrderLines[line.id]; + if (split.quantity) { + if (!orderline) { + orderline = line.clone(); + this.newOrder.add_orderline(orderline); + this.newOrderLines[line.id] = orderline; + } + orderline.set_quantity(split.quantity, 'do not recompute unit price'); + } else if (orderline) { + this.newOrder.remove_orderline(orderline); + this.newOrderLines[line.id] = null; + } + } + _isFullPayOrder() { + let order = this.env.pos.get_order(); + let full = true; + let splitlines = this.splitlines; + let groupedLines = _.groupBy(order.get_orderlines(), line => line.get_product().id); + + Object.keys(groupedLines).forEach(function (lineId) { + var maxQuantity = groupedLines[lineId].reduce(((quantity, line) => quantity + line.get_quantity()), 0); + Object.keys(splitlines).forEach(id => { + let split = splitlines[id]; + if(split.product === groupedLines[lineId][0].get_product().id) + maxQuantity -= split.quantity; + }); + if(maxQuantity !== 0) + full = false; + }); + + return full; + } + _setQuantityOnCurrentOrder() { + let order = this.env.pos.get_order(); + for (var id in this.splitlines) { + var split = this.splitlines[id]; + var line = this.currentOrder.get_orderline(parseInt(id)); + + if(!this.props.disallow) { + line.set_quantity( + line.get_quantity() - split.quantity, + 'do not recompute unit price' + ); + if (Math.abs(line.get_quantity()) < 0.00001) { + this.currentOrder.remove_orderline(line); + } + } else { + if(split.quantity) { + let decreaseLine = line.clone(); + decreaseLine.order = order; + decreaseLine.noDecrease = true; + decreaseLine.set_quantity(-split.quantity); + order.add_orderline(decreaseLine); + } + } + } + } + _resetState() { + if (this._isFinal) return; + + for (let id in this.splitlines) { + delete this.splitlines[id]; + } + for (let line of this.currentOrder.get_orderlines()) { + this.splitlines[line.id] = { quantity: 0 }; + } + this.newOrder.orderlines.reset(); + } + } + SplitBillScreen.template = 'SplitBillScreen'; + + Registries.Component.add(SplitBillScreen); + + return SplitBillScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js new file mode 100644 index 00000000..51c0b929 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js @@ -0,0 +1,25 @@ +odoo.define('pos_restaurant.SplitOrderline', function(require) { + 'use strict'; + + const { useListener } = require('web.custom_hooks'); + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class SplitOrderline extends PosComponent { + constructor() { + super(...arguments); + useListener('click', this.onClick); + } + get isSelected() { + return this.props.split.quantity !== 0; + } + onClick() { + this.trigger('click-line', this.props.line); + } + } + SplitOrderline.template = 'SplitOrderline'; + + Registries.Component.add(SplitOrderline); + + return SplitOrderline; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js new file mode 100644 index 00000000..cbea3d88 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js @@ -0,0 +1,158 @@ +odoo.define('pos_restaurant.TicketScreen', function (require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const TicketScreen = require('point_of_sale.TicketScreen'); + const Registries = require('point_of_sale.Registries'); + const { useAutofocus } = require('web.custom_hooks'); + const { posbus } = require('point_of_sale.utils'); + const { parse } = require('web.field_utils'); + const { useState, useContext } = owl.hooks; + + const PosResTicketScreen = (TicketScreen) => + class extends TicketScreen { + close() { + super.close(); + if (!this.env.pos.config.iface_floorplan) { + // Make sure the 'table-set' event is triggered + // to properly rerender the components that listens to it. + posbus.trigger('table-set'); + } + } + get filterOptions() { + const { Payment, Open, Tipping } = this.getOrderStates(); + var filterOptions = super.filterOptions; + if (this.env.pos.config.set_tip_after_payment) { + var idx = filterOptions.indexOf(Payment); + filterOptions[idx] = Open; + } + return [...filterOptions, Tipping]; + } + get _screenToStatusMap() { + const { Open, Tipping } = this.getOrderStates(); + return Object.assign(super._screenToStatusMap, { + PaymentScreen: this.env.pos.config.set_tip_after_payment ? Open : super._screenToStatusMap.PaymentScreen, + TipScreen: Tipping, + }); + } + getTable(order) { + return `${order.table.floor.name} (${order.table.name})`; + } + get _searchFields() { + if (!this.env.pos.config.iface_floorplan) { + return super._searchFields; + } + return Object.assign({}, super._searchFields, { + Table: (order) => `${order.table.floor.name} (${order.table.name})`, + }); + } + _setOrder(order) { + if (!this.env.pos.config.iface_floorplan) { + super._setOrder(order); + } else if (order !== this.env.pos.get_order()) { + // Only call set_table if the order is not the same as the current order. + // This is to prevent syncing to the server because syncing is only intended + // when going back to the floorscreen or opening a table. + this.env.pos.set_table(order.table, order); + } + } + get showNewTicketButton() { + return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.showNewTicketButton; + } + get orderList() { + if (this.env.pos.table) { + return super.orderList; + } else { + return this.env.pos.get('orders').models; + } + } + async settleTips() { + // set tip in each order + for (const order of this.filteredOrderList) { + const tipAmount = parse.float(order.uiState.TipScreen.state.inputTipAmount || '0'); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + if (!serverId) { + console.warn(`${order.name} is not yet sync. Sync it to server before setting a tip.`); + } else { + const result = await this.setTip(order, serverId, tipAmount); + if (!result) break; + } + } + } + async setTip(order, serverId, amount) { + try { + const paymentline = order.get_paymentlines()[0]; + if (paymentline.payment_method.payment_terminal) { + paymentline.amount += amount; + this.env.pos.set_order(order, {silent: true}); + await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid); + } + + if (!amount) { + await this.setNoTip(); + } else { + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + const tip_line = order.selected_orderline; + await this.rpc({ + method: 'set_tip', + model: 'pos.order', + args: [serverId, tip_line.export_as_JSON()], + }); + } + order.finalize(); + return true; + } catch (error) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: 'Failed to set tip', + body: `Failed to set tip to ${order.name}. Do you want to proceed on setting the tips of the remaining?`, + }); + return confirmed; + } + } + async setNoTip() { + await this.rpc({ + method: 'set_no_tip', + model: 'pos.order', + args: [serverId], + }); + } + getOrderStates() { + return Object.assign(super.getOrderStates(), { + Tipping: this.env._t('Tipping'), + Open: this.env._t('Open'), + }); + } + }; + + Registries.Component.extend(TicketScreen, PosResTicketScreen); + + class TipCell extends PosComponent { + constructor() { + super(...arguments); + this.state = useState({ isEditing: false }); + this.orderUiState = useContext(this.props.order.uiState.TipScreen); + useAutofocus({ selector: 'input' }); + } + get tipAmountStr() { + return this.env.pos.format_currency(parse.float(this.orderUiState.inputTipAmount || '0')); + } + onBlur() { + this.state.isEditing = false; + } + onKeydown(event) { + if (event.key === 'Enter') { + this.state.isEditing = false; + } + } + editTip() { + this.state.isEditing = true; + } + } + TipCell.template = 'TipCell'; + + Registries.Component.add(TipCell); + + return { TicketScreen, TipCell }; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js new file mode 100644 index 00000000..bb6e17a2 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -0,0 +1,156 @@ +odoo.define('pos_restaurant.TipScreen', function (require) { + 'use strict'; + + const Registries = require('point_of_sale.Registries'); + const PosComponent = require('point_of_sale.PosComponent'); + const { parse } = require('web.field_utils'); + const { useContext } = owl.hooks; + + class TipScreen extends PosComponent { + constructor() { + super(...arguments); + this.state = useContext(this.currentOrder.uiState.TipScreen); + this._totalAmount = this.currentOrder.get_total_with_tax(); + } + mounted () { + this.printTipReceipt(); + } + get overallAmountStr() { + const tipAmount = parse.float(this.state.inputTipAmount || '0'); + const original = this.env.pos.format_currency(this.totalAmount); + const tip = this.env.pos.format_currency(tipAmount); + const overall = this.env.pos.format_currency(this.totalAmount + tipAmount); + return `${original} + ${tip} tip = ${overall}`; + } + get totalAmount() { + return this._totalAmount; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get percentageTips() { + return [ + { percentage: '15%', amount: 0.15 * this.totalAmount }, + { percentage: '20%', amount: 0.2 * this.totalAmount }, + { percentage: '25%', amount: 0.25 * this.totalAmount }, + ]; + } + async validateTip() { + const amount = parse.float(this.state.inputTipAmount) || 0; + const order = this.env.pos.get_order(); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + + if (!serverId) { + this.showPopup('ErrorPopup', { + title: 'Unsynced order', + body: 'This order is not yet synced to server. Make sure it is synced then try again.', + }); + return; + } + + if (!amount) { + await this.rpc({ + method: 'set_no_tip', + model: 'pos.order', + args: [serverId], + }); + this.goNextScreen(); + return; + } + + if (amount > 0.25 * this.totalAmount) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: 'Are you sure?', + body: `${this.env.pos.format_currency( + amount + )} is more than 25% of the order's total amount. Are you sure of this tip amount?`, + }); + if (!confirmed) return; + } + + // set the tip by temporarily allowing order modification + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + + const paymentline = this.env.pos.get_order().get_paymentlines()[0]; + if (paymentline.payment_method.payment_terminal) { + paymentline.amount += amount; + await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid); + } + + // set_tip calls add_product which sets the new line as the selected_orderline + const tip_line = order.selected_orderline; + await this.rpc({ + method: 'set_tip', + model: 'pos.order', + args: [serverId, tip_line.export_as_JSON()], + }); + this.goNextScreen(); + } + goNextScreen() { + this.env.pos.get_order().finalize(); + const { name, props } = this.nextScreen; + this.showScreen(name, props); + } + get nextScreen() { + if (this.env.pos.config.module_pos_restaurant && this.env.pos.config.iface_floorplan) { + const table = this.env.pos.table; + return { name: 'FloorScreen', props: { floor: table ? table.floor : null } }; + } else { + return { name: 'ProductScreen' }; + } + } + async printTipReceipt() { + const receipts = [ + this.currentOrder.selected_paymentline.ticket, + this.currentOrder.selected_paymentline.cashier_receipt + ]; + + for (let i = 0; i < receipts.length; i++) { + const data = receipts[i]; + var receipt = this.env.qweb.renderToString('TipReceipt', { + receipt: this.currentOrder.getOrderReceiptEnv().receipt, + data: data, + total: this.env.pos.format_currency(this.totalAmount), + }); + + if (this.env.pos.proxy.printer) { + await this._printIoT(receipt); + } else { + await this._printWeb(receipt); + } + } + } + + async _printIoT(receipt) { + const printResult = await this.env.pos.proxy.printer.print_receipt(receipt); + if (!printResult.successful) { + await this.showPopup('ErrorPopup', { + title: printResult.message.title, + body: printResult.message.body, + }); + } + } + + async _printWeb(receipt) { + try { + $(this.el).find('.pos-receipt-container').html(receipt); + window.print(); + } catch (err) { + await this.showPopup('ErrorPopup', { + title: this.env._t('Printing is not supported on some browsers'), + body: this.env._t( + 'Printing is not supported on some browsers due to no default printing protocol ' + + 'is available. It is possible to print your tickets by making use of an IoT Box.' + ), + }); + } + } + } + TipScreen.template = 'pos_restaurant.TipScreen'; + + Registries.Component.add(TipScreen); + + return TipScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js new file mode 100644 index 00000000..3f5b739a --- /dev/null +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -0,0 +1,399 @@ +odoo.define('pos_restaurant.floors', function (require) { +"use strict"; + +var models = require('point_of_sale.models'); +const { Gui } = require('point_of_sale.Gui'); +const { posbus } = require('point_of_sale.utils'); + +// At POS Startup, load the floors, and add them to the pos model +models.load_models({ + model: 'restaurant.floor', + fields: ['name','background_color','table_ids','sequence'], + domain: function(self){ return [['pos_config_id','=',self.config.id]]; }, + loaded: function(self,floors){ + self.floors = floors; + self.floors_by_id = {}; + for (var i = 0; i < floors.length; i++) { + floors[i].tables = []; + self.floors_by_id[floors[i].id] = floors[i]; + } + + // Make sure they display in the correct order + self.floors = self.floors.sort(function(a,b){ return a.sequence - b.sequence; }); + + // Ignore floorplan features if no floor specified. + self.config.iface_floorplan = !!self.floors.length; + }, +}); + +// At POS Startup, after the floors are loaded, load the tables, and associate +// them with their floor. +models.load_models({ + model: 'restaurant.table', + fields: ['name','width','height','position_h','position_v','shape','floor_id','color','seats'], + loaded: function(self,tables){ + self.tables_by_id = {}; + for (var i = 0; i < tables.length; i++) { + self.tables_by_id[tables[i].id] = tables[i]; + var floor = self.floors_by_id[tables[i].floor_id[0]]; + if (floor) { + floor.tables.push(tables[i]); + tables[i].floor = floor; + } + } + }, +}); + +// New orders are now associated with the current table, if any. +var _super_order = models.Order.prototype; +models.Order = models.Order.extend({ + initialize: function(attr,options) { + _super_order.initialize.apply(this,arguments); + if (!this.table && !options.json) { + this.table = this.pos.table; + } + this.customer_count = this.customer_count || 1; + this.save_to_db(); + }, + export_as_JSON: function() { + var json = _super_order.export_as_JSON.apply(this,arguments); + json.table = this.table ? this.table.name : undefined; + json.table_id = this.table ? this.table.id : false; + json.floor = this.table ? this.table.floor.name : false; + json.floor_id = this.table ? this.table.floor.id : false; + json.customer_count = this.customer_count; + return json; + }, + init_from_JSON: function(json) { + _super_order.init_from_JSON.apply(this,arguments); + this.table = this.pos.tables_by_id[json.table_id]; + this.floor = this.table ? this.pos.floors_by_id[json.floor_id] : undefined; + this.customer_count = json.customer_count || 1; + }, + export_for_printing: function() { + var json = _super_order.export_for_printing.apply(this,arguments); + json.table = this.table ? this.table.name : undefined; + json.floor = this.table ? this.table.floor.name : undefined; + json.customer_count = this.get_customer_count(); + return json; + }, + get_customer_count: function(){ + return this.customer_count; + }, + set_customer_count: function(count) { + this.customer_count = Math.max(count,0); + this.trigger('change'); + }, +}); + +// We need to change the way the regular UI sees the orders, it +// needs to only see the orders associated with the current table, +// and when an order is validated, it needs to go back to the floor map. +// +// And when we change the table, we must create an order for that table +// if there is none. +var _super_posmodel = models.PosModel.prototype; +models.PosModel = models.PosModel.extend({ + after_load_server_data: async function() { + var res = await _super_posmodel.after_load_server_data.call(this); + if (this.config.iface_floorplan) { + this.table = null; + } + return res; + }, + + transfer_order_to_different_table: function () { + this.order_to_transfer_to_different_table = this.get_order(); + + // go to 'floors' screen, this will set the order to null and + // eventually this will cause the gui to go to its + // default_screen, which is 'floors' + this.set_table(null); + }, + + remove_from_server_and_set_sync_state: function(ids_to_remove){ + var self = this; + this.set_synch('connecting', ids_to_remove.length); + return self._remove_from_server(ids_to_remove) + .then(function(server_ids) { + self.set_synch('connected'); + }).catch(function(reason){ + self.set_synch('error'); + throw reason; + }); + }, + + /** + * Request the orders of the table with given id. + * @param {number} table_id. + * @param {dict} options. + * @param {number} options.timeout optional timeout parameter for the rpc call. + * @return {Promise} + */ + _get_from_server: function (table_id, options) { + options = options || {}; + var timeout = typeof options.timeout === 'number' ? options.timeout : 7500; + return this.rpc({ + model: 'pos.order', + method: 'get_table_draft_orders', + args: [table_id], + kwargs: {context: this.session.user_context}, + }, { + timeout: timeout, + shadow: false, + }) + }, + + transfer_order_to_table: function(table) { + this.order_to_transfer_to_different_table.table = table; + this.order_to_transfer_to_different_table.save_to_db(); + }, + + push_order_for_transfer: function(order_ids, table_orders) { + order_ids.push(this.order_to_transfer_to_different_table.uid); + table_orders.push(this.order_to_transfer_to_different_table); + }, + + clean_table_transfer: function(table) { + if (this.order_to_transfer_to_different_table && table) { + this.order_to_transfer_to_different_table = null; + this.set_table(table); + } + }, + + sync_from_server: function(table, table_orders, order_ids) { + var self = this; + var ids_to_remove = this.db.get_ids_to_remove_from_server(); + var orders_to_sync = this.db.get_unpaid_orders_to_sync(order_ids); + if (orders_to_sync.length) { + this.set_synch('connecting', orders_to_sync.length); + this._save_to_server(orders_to_sync, {'draft': true}).then(function (server_ids) { + server_ids.forEach(server_id => self.update_table_order(server_id, table_orders)); + if (!ids_to_remove.length) { + self.set_synch('connected'); + } else { + self.remove_from_server_and_set_sync_state(ids_to_remove); + } + }).catch(function(reason){ + self.set_synch('error'); + }).finally(function(){ + self.clean_table_transfer(table); + }); + } else { + if (ids_to_remove.length) { + self.remove_from_server_and_set_sync_state(ids_to_remove); + } + self.clean_table_transfer(table); + } + }, + + update_table_order: function(server_id, table_orders) { + const order = table_orders.find(o => o.name === server_id.pos_reference); + if (order) { + order.server_id = server_id.id; + order.save_to_db(); + } + return order; + }, + + /** + * @param {models.Order} order order to set + */ + set_order_on_table: function(order) { + var orders = this.get_order_list(); + if (orders.length) { + order = order ? orders.find((o) => o.uid === order.uid) : null; + if (order) { + this.set_order(order); + } else { + // do not mindlessly set the first order in the list. + orders = orders.filter(order => !order.finalized); + if (orders.length) { + this.set_order(orders[0]); + } else { + this.add_new_order(); + } + } + } else { + this.add_new_order(); // or create a new order with the current table + } + }, + + sync_to_server: function(table, order) { + var self = this; + var ids_to_remove = this.db.get_ids_to_remove_from_server(); + + this.set_synch('connecting', 1); + this._get_from_server(table.id).then(function (server_orders) { + var orders = self.get_order_list(); + orders.forEach(function(order){ + // We don't remove the validated orders because we still want to see them + // in the ticket screen. Orders in 'ReceiptScreen' or 'TipScreen' are validated + // orders. + if (order.server_id && !order.finalized){ + self.get("orders").remove(order); + order.destroy(); + } + }); + server_orders.forEach(function(server_order){ + if (server_order.lines.length){ + var new_order = new models.Order({},{pos: self, json: server_order}); + self.get("orders").add(new_order); + new_order.save_to_db(); + } + }) + if (!ids_to_remove.length) { + self.set_synch('connected'); + } else { + self.remove_from_server_and_set_sync_state(ids_to_remove); + } + }).catch(function(reason){ + self.set_synch('error'); + }).finally(function(){ + self.set_order_on_table(order); + }); + }, + + get_order_with_uid: function() { + var order_ids = []; + this.get_order_list().forEach(function(o){ + order_ids.push(o.uid); + }); + + return order_ids; + }, + + /** + * Changes the current table. + * + * Switch table and make sure all nececery syncing tasks are done. + * @param {object} table. + * @param {models.Order|undefined} order if provided, set to this order + */ + set_table: function(table, order) { + if(!table){ + this.sync_from_server(table, this.get_order_list(), this.get_order_with_uid()); + this.set_order(null); + this.table = null; + } else if (this.order_to_transfer_to_different_table) { + var order_ids = this.get_order_with_uid(); + + this.transfer_order_to_table(table); + this.push_order_for_transfer(order_ids, this.get_order_list()); + + this.sync_from_server(table, this.get_order_list(), order_ids); + this.set_order(null); + } else { + this.table = table; + this.sync_to_server(table, order); + } + posbus.trigger('table-set'); + }, + + // if we have tables, we do not load a default order, as the default order will be + // set when the user selects a table. + set_start_order: function() { + if (!this.config.iface_floorplan) { + _super_posmodel.set_start_order.apply(this,arguments); + } + }, + + // we need to prevent the creation of orders when there is no + // table selected. + add_new_order: function() { + if (this.config.iface_floorplan) { + if (this.table) { + return _super_posmodel.add_new_order.apply(this, arguments); + } else { + Gui.showPopup('ConfirmPopup', { + title: 'Unable to create order', + body: 'Orders cannot be created when there is no active table in restaurant mode', + }); + return undefined; + } + } else { + return _super_posmodel.add_new_order.apply(this,arguments); + } + }, + + + // get the list of unpaid orders (associated to the current table) + get_order_list: function() { + var orders = _super_posmodel.get_order_list.call(this); + if (!(this.config && this.config.iface_floorplan)) { + return orders; + } else if (!this.table) { + return []; + } else { + var t_orders = []; + for (var i = 0; i < orders.length; i++) { + if ( orders[i].table === this.table) { + t_orders.push(orders[i]); + } + } + return t_orders; + } + }, + + // get the list of orders associated to a table. FIXME: should be O(1) + get_table_orders: function(table) { + var orders = _super_posmodel.get_order_list.call(this); + var t_orders = []; + for (var i = 0; i < orders.length; i++) { + if (orders[i].table === table) { + t_orders.push(orders[i]); + } + } + return t_orders; + }, + + // get customer count at table + get_customer_count: function(table) { + var orders = this.get_table_orders(table).filter(order => !order.finalized); + var count = 0; + for (var i = 0; i < orders.length; i++) { + count += orders[i].get_customer_count(); + } + return count; + }, + + // When we validate an order we go back to the floor plan. + // When we cancel an order and there is multiple orders + // on the table, stay on the table. + on_removed_order: function(removed_order,index,reason){ + if (this.config.iface_floorplan) { + var order_list = this.get_order_list(); + if (reason === 'abandon') { + this.db.set_order_to_remove_from_server(removed_order); + } + if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ + this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true }); + } else if (order_list.length === 0) { + this.table ? this.set_order(null) : this.set_table(null); + } + } else { + _super_posmodel.on_removed_order.apply(this,arguments); + } + }, + + +}); + + +var _super_paymentline = models.Paymentline.prototype; +models.Paymentline = models.Paymentline.extend({ + /** + * Override this method to be able to show the 'Adjust Authorisation' button + * on a validated payment_line and to show the tip screen which allow + * tipping even after payment. By default, this returns true for all + * non-cash payment. + */ + canBeAdjusted: function() { + if (this.payment_method.payment_terminal) { + return this.payment_method.payment_terminal.canBeAdjusted(this.cid); + } + return !this.payment_method.is_cash_count; + }, +}); + +}); diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js new file mode 100644 index 00000000..709de43e --- /dev/null +++ b/addons/pos_restaurant/static/src/js/multiprint.js @@ -0,0 +1,315 @@ +odoo.define('pos_restaurant.multiprint', function (require) { +"use strict"; + +var models = require('point_of_sale.models'); +var core = require('web.core'); +var Printer = require('point_of_sale.Printer').Printer; + +var QWeb = core.qweb; + +models.PosModel = models.PosModel.extend({ + create_printer: function (config) { + var url = config.proxy_ip || ''; + if(url.indexOf('//') < 0) { + url = window.location.protocol + '//' + url; + } + if(url.indexOf(':', url.indexOf('//') + 2) < 0 && window.location.protocol !== 'https:') { + url = url + ':8069'; + } + return new Printer(url, this); + }, +}); + +models.load_models({ + model: 'restaurant.printer', + fields: ['name','proxy_ip','product_categories_ids', 'printer_type'], + domain: null, + loaded: function(self,printers){ + var active_printers = {}; + for (var i = 0; i < self.config.printer_ids.length; i++) { + active_printers[self.config.printer_ids[i]] = true; + } + + self.printers = []; + self.printers_categories = {}; // list of product categories that belong to + // one or more order printer + + for(var i = 0; i < printers.length; i++){ + if(active_printers[printers[i].id]){ + var printer = self.create_printer(printers[i]); + printer.config = printers[i]; + self.printers.push(printer); + + for (var j = 0; j < printer.config.product_categories_ids.length; j++) { + self.printers_categories[printer.config.product_categories_ids[j]] = true; + } + } + } + self.printers_categories = _.keys(self.printers_categories); + self.config.iface_printers = !!self.printers.length; + }, +}); + +var _super_orderline = models.Orderline.prototype; + +models.Orderline = models.Orderline.extend({ + initialize: function() { + _super_orderline.initialize.apply(this,arguments); + if (!this.pos.config.iface_printers) { + return; + } + if (typeof this.mp_dirty === 'undefined') { + // mp dirty is true if this orderline has changed + // since the last kitchen print + // it's left undefined if the orderline does not + // need to be printed to a printer. + + this.mp_dirty = this.printable() || undefined; + } + if (!this.mp_skip) { + // mp_skip is true if the cashier want this orderline + // not to be sent to the kitchen + this.mp_skip = false; + } + }, + // can this orderline be potentially printed ? + printable: function() { + return this.pos.db.is_product_in_category(this.pos.printers_categories, this.get_product().id); + }, + init_from_JSON: function(json) { + _super_orderline.init_from_JSON.apply(this,arguments); + this.mp_dirty = json.mp_dirty; + this.mp_skip = json.mp_skip; + }, + export_as_JSON: function() { + var json = _super_orderline.export_as_JSON.apply(this,arguments); + json.mp_dirty = this.mp_dirty; + json.mp_skip = this.mp_skip; + return json; + }, + set_quantity: function(quantity) { + if (this.pos.config.iface_printers && quantity !== this.quantity && this.printable()) { + this.mp_dirty = true; + } + _super_orderline.set_quantity.apply(this,arguments); + }, + can_be_merged_with: function(orderline) { + return (!this.mp_skip) && + (!orderline.mp_skip) && + _super_orderline.can_be_merged_with.apply(this,arguments); + }, + set_skip: function(skip) { + if (this.mp_dirty && skip && !this.mp_skip) { + this.mp_skip = true; + this.trigger('change',this); + } + if (this.mp_skip && !skip) { + this.mp_dirty = true; + this.mp_skip = false; + this.trigger('change',this); + } + }, + set_dirty: function(dirty) { + if (this.mp_dirty !== dirty) { + this.mp_dirty = dirty; + this.trigger('change', this); + } + }, + get_line_diff_hash: function(){ + if (this.get_note()) { + return this.id + '|' + this.get_note(); + } else { + return '' + this.id; + } + }, +}); + +var _super_order = models.Order.prototype; +models.Order = models.Order.extend({ + build_line_resume: function(){ + var resume = {}; + this.orderlines.each(function(line){ + if (line.mp_skip) { + return; + } + var line_hash = line.get_line_diff_hash(); + var qty = Number(line.get_quantity()); + var note = line.get_note(); + var product_id = line.get_product().id; + + if (typeof resume[line_hash] === 'undefined') { + resume[line_hash] = { + qty: qty, + note: note, + product_id: product_id, + product_name_wrapped: line.generate_wrapped_product_name(), + }; + } else { + resume[line_hash].qty += qty; + } + + }); + return resume; + }, + saveChanges: function(){ + this.saved_resume = this.build_line_resume(); + this.orderlines.each(function(line){ + line.set_dirty(false); + }); + this.trigger('change',this); + }, + computeChanges: function(categories){ + var current_res = this.build_line_resume(); + var old_res = this.saved_resume || {}; + var json = this.export_as_JSON(); + var add = []; + var rem = []; + var line_hash; + + for ( line_hash in current_res) { + var curr = current_res[line_hash]; + var old = {}; + var found = false; + for(var id in old_res) { + if(old_res[id].product_id === curr.product_id){ + found = true; + old = old_res[id]; + break; + } + } + + if (!found) { + add.push({ + 'id': curr.product_id, + 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, + 'note': curr.note, + 'qty': curr.qty, + }); + } else if (old.qty < curr.qty) { + add.push({ + 'id': curr.product_id, + 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, + 'note': curr.note, + 'qty': curr.qty - old.qty, + }); + } else if (old.qty > curr.qty) { + rem.push({ + 'id': curr.product_id, + 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, + 'note': curr.note, + 'qty': old.qty - curr.qty, + }); + } + } + + for (line_hash in old_res) { + var found = false; + for(var id in current_res) { + if(current_res[id].product_id === old_res[line_hash].product_id) + found = true; + } + if (!found) { + var old = old_res[line_hash]; + rem.push({ + 'id': old.product_id, + 'name': this.pos.db.get_product_by_id(old.product_id).display_name, + 'name_wrapped': old.product_name_wrapped, + 'note': old.note, + 'qty': old.qty, + }); + } + } + + if(categories && categories.length > 0){ + // filter the added and removed orders to only contains + // products that belong to one of the categories supplied as a parameter + + var self = this; + + var _add = []; + var _rem = []; + + for(var i = 0; i < add.length; i++){ + if(self.pos.db.is_product_in_category(categories,add[i].id)){ + _add.push(add[i]); + } + } + add = _add; + + for(var i = 0; i < rem.length; i++){ + if(self.pos.db.is_product_in_category(categories,rem[i].id)){ + _rem.push(rem[i]); + } + } + rem = _rem; + } + + var d = new Date(); + var hours = '' + d.getHours(); + hours = hours.length < 2 ? ('0' + hours) : hours; + var minutes = '' + d.getMinutes(); + minutes = minutes.length < 2 ? ('0' + minutes) : minutes; + + return { + 'new': add, + 'cancelled': rem, + 'table': json.table || false, + 'floor': json.floor || false, + 'name': json.name || 'unknown order', + 'time': { + 'hours': hours, + 'minutes': minutes, + }, + }; + + }, + printChanges: async function(){ + var printers = this.pos.printers; + let isPrintSuccessful = true; + for(var i = 0; i < printers.length; i++){ + var changes = this.computeChanges(printers[i].config.product_categories_ids); + if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ + var receipt = QWeb.render('OrderChangeReceipt',{changes:changes, widget:this}); + const result = await printers[i].print_receipt(receipt); + if (!result.successful) { + isPrintSuccessful = false; + } + } + } + return isPrintSuccessful; + }, + hasChangesToPrint: function(){ + var printers = this.pos.printers; + for(var i = 0; i < printers.length; i++){ + var changes = this.computeChanges(printers[i].config.product_categories_ids); + if ( changes['new'].length > 0 || changes['cancelled'].length > 0){ + return true; + } + } + return false; + }, + hasSkippedChanges: function() { + var orderlines = this.get_orderlines(); + for (var i = 0; i < orderlines.length; i++) { + if (orderlines[i].mp_skip) { + return true; + } + } + return false; + }, + export_as_JSON: function(){ + var json = _super_order.export_as_JSON.apply(this,arguments); + json.multiprint_resume = JSON.stringify(this.saved_resume); + return json; + }, + init_from_JSON: function(json){ + _super_order.init_from_JSON.apply(this,arguments); + this.saved_resume = json.multiprint_resume && JSON.parse(json.multiprint_resume); + }, +}); + + +}); diff --git a/addons/pos_restaurant/static/src/js/notes.js b/addons/pos_restaurant/static/src/js/notes.js new file mode 100644 index 00000000..c2ed95dd --- /dev/null +++ b/addons/pos_restaurant/static/src/js/notes.js @@ -0,0 +1,42 @@ +odoo.define('pos_restaurant.notes', function (require) { +"use strict"; + +var models = require('point_of_sale.models'); + +var _super_orderline = models.Orderline.prototype; +models.Orderline = models.Orderline.extend({ + initialize: function(attr, options) { + _super_orderline.initialize.call(this,attr,options); + this.note = this.note || ""; + }, + set_note: function(note){ + this.note = note; + this.trigger('change',this); + }, + get_note: function(note){ + return this.note; + }, + can_be_merged_with: function(orderline) { + if (orderline.get_note() !== this.get_note()) { + return false; + } else { + return _super_orderline.can_be_merged_with.apply(this,arguments); + } + }, + clone: function(){ + var orderline = _super_orderline.clone.call(this); + orderline.note = this.note; + return orderline; + }, + export_as_JSON: function(){ + var json = _super_orderline.export_as_JSON.call(this); + json.note = this.note; + return json; + }, + init_from_JSON: function(json){ + _super_orderline.init_from_JSON.apply(this,arguments); + this.note = json.note; + }, +}); + +}); diff --git a/addons/pos_restaurant/static/src/js/payment.js b/addons/pos_restaurant/static/src/js/payment.js new file mode 100644 index 00000000..355d6742 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/payment.js @@ -0,0 +1,24 @@ +odoo.define('pos_restaurant.PaymentInterface', function (require) { + "use strict"; + + var PaymentInterface = require('point_of_sale.PaymentInterface'); + + PaymentInterface.include({ + /** + * Return true if the amount that was authorized can be modified, + * false otherwise + * @param {string} cid - The id of the paymentline + */ + canBeAdjusted(cid) { + return false; + }, + + /** + * Called when the amount authorized by a payment request should + * be adjusted to account for a new order line, it can only be called if + * canBeAdjusted returns True + * @param {string} cid - The id of the paymentline + */ + send_payment_adjust: function (cid) {}, + }); +}); 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 00000000..86032c07 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Chrome.xml @@ -0,0 +1,10 @@ +<?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="//div[hasclass('search-bar-portal')]" position="before"> + <BackToFloorButton /> + </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 00000000..2b5c865c --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="BackToFloorButton" owl="1"> + <span t-if="hasTable" class="order-button floor-button" t-on-click="backToFloorScreen"> + <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> + <span t-else=""></span> + </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 00000000..169c888e --- /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 00000000..5df01405 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml @@ -0,0 +1,36 @@ +<?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> + <div class="top-content-center"> + <h1>Bill Printing</h1> + </div> + <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" t-ref="order-receipt"/> + </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 00000000..0d47a5c4 --- /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" t-attf-style="top:{{props.floorMapScrollTop}}px;"> + <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 00000000..9c3a6079 --- /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 00000000..818f2d83 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml @@ -0,0 +1,37 @@ +<?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" + t-attf-style="top:{{state.floorMapScrollTop}}px;"> + <i class="fa fa-pencil" role="img" aria-label="Edit" title="Edit"></i> + </span> + <EditBar t-if="state.isEditMode" selectedTable="selectedTable" floorMapScrollTop="state.floorMapScrollTop"/> + </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 00000000..c9a7549a --- /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-att-class="orderCountClass" t-att-hidden="orderCount === 0"> + <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/OrderManagementScreen/OrderList.xml b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml new file mode 100644 index 00000000..79607102 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="OrderList" t-inherit="point_of_sale.OrderList" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('order-row')]//div[hasclass('customer')]" position="after"> + <div t-if="env.pos.config.module_pos_restaurant" class="header table"> + Table + </div> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml new file mode 100644 index 00000000..3006e5bf --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="OrderRow" t-inherit="point_of_sale.OrderRow" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('order-row')]//div[hasclass('customer')]" position="after"> + <div t-if="env.pos.config.module_pos_restaurant" class="item table"> + <t t-esc="table" /> + </div> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml new file mode 100644 index 00000000..7a460e5e --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="PaymentScreen" t-inherit="point_of_sale.PaymentScreen" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('button') and hasclass('next')]" position="attributes"> + <attribute name="t-att-hidden">env.pos.config.set_tip_after_payment and !currentOrder.is_paid()</attribute> + </xpath> + + <xpath expr="//div[hasclass('button') and hasclass('back')]/span[hasclass('back_text')]" position="replace"> + <t t-if="env.pos.config.set_tip_after_payment and currentOrder.is_paid()"> + <span class="back_text">Keep Open</span> + </t> + <t t-else="">$0</t> + </xpath> + + <xpath expr="//div[hasclass('button') and hasclass('next')]/span[hasclass('next_text')]" position="replace"> + <t t-if="env.pos.config.set_tip_after_payment and currentOrder.is_paid()"> + <span class="back_text">Close Tab</span> + </t> + <t t-else="">$0</t> + </xpath> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml new file mode 100644 index 00000000..11683ddb --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="PaymentScreenElectronicPayment" t-inherit="point_of_sale.PaymentScreenElectronicPayment" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('send_payment_reversal')]/.." position="replace"> + <t t-if="props.line.canBeAdjusted() && props.line.order.get_total_paid() < props.line.order.get_total_with_tax()"> + <div class="button send_adjust_amount" title="Adjust Amount" t-on-click="trigger('send-payment-adjust', props.line)"> + Adjust Amount + </div> + </t> + <t t-elif="props.line.can_be_reversed"> + <div class="button send_payment_reversal" title="Reverse Payment" t-on-click="trigger('send-payment-reverse', props.line)"> + Reverse + </div> + </t> + </xpath> + </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 00000000..407103a5 --- /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 00000000..12a4e397 --- /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 00000000..e8efb306 --- /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 00000000..e0229af8 --- /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 00000000..5a33b17e --- /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 00000000..6eb0c975 --- /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 00000000..a5c96ded --- /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 00000000..94a8e45e --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -0,0 +1,50 @@ +<?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> + <xpath expr="//div[hasclass('cashier')]" position="after"> + <t t-if="receipt.table"> + at table <t t-esc="receipt.table" /> + </t> + <t t-if="receipt.table and receipt.customer_count"> + <div>Guests: <t t-esc="receipt.customer_count" /></div> + </t> + </xpath> + <xpath expr="//div[hasclass('before-footer')]" position="after"> + <t t-if="props.isBill and env.pos.config.set_tip_after_payment"> + <div class="tip-form"> + <div class="title">For convenience, we are providing the following gratuity calculations:</div> + <div class="percentage-options"> + <div class="option"> + <div>15%</div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.15)"></t> + </div> + </div> + <div class="option"> + <div>20%</div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.20)"></t> + </div> + </div> + <div class="option"> + <div>25%</div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.25)"></t> + </div> + </div> + </div> + </div> + </t> + </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 00000000..f853d8aa --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml @@ -0,0 +1,46 @@ +<?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="contents"> + <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> + <div class="top-content-center"> + <h1>Bill Splitting</h1> + </div> + </div> + <div class="main"> + <div class="lines"> + <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="controls"> + <div class="order-info"> + <span class="subtotal"> + <t t-esc="env.pos.format_currency(newOrder.get_subtotal())" /> + </span> + </div> + <div class="pay-button"> + <div class="button" t-on-click="proceed"> + <i class="fa fa-chevron-right" /> + <span> </span> + <span>Payment</span> + </div> + </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 00000000..fd71374f --- /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/Screens/TicketScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml new file mode 100644 index 00000000..1c0ac260 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension" owl="1"> + <xpath expr="//div[hasclass('header-row')]//div[@name='delete']" position="before"> + <div t-if="env.pos.config.iface_floorplan" class="col start" name="table">Table</div> + <div t-if="filter === 'Tipping'" class="col end narrow" name="tip">Tip</div> + </xpath> + <xpath expr="//div[hasclass('order-row')]//div[@name='delete']" position="before"> + <div t-if="env.pos.config.iface_floorplan" class="col start" name="table"> + <t t-esc="getTable(order)"></t> + </div> + <div t-if="filter === 'Tipping'" class="col end narrow" name="tip"> + <TipCell order="order" /> + </div> + </xpath> + <xpath expr="//div[hasclass('buttons')]" position="inside"> + <button class="settle-tips" t-if="filter === 'Tipping'" t-on-click="settleTips">Settle</button> + </xpath> + </t> + + <t t-name="TipCell" owl="1"> + <div class="tip-cell" t-on-click.stop="editTip"> + <t t-if="state.isEditing"> + <input type="text" name="tip-amount" t-model="orderUiState.inputTipAmount" t-on-blur="onBlur" t-on-keydown="onKeydown" /> + </t> + <div t-else=""> + <t t-esc="tipAmountStr"></t> + </div> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml new file mode 100644 index 00000000..bae809a8 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="pos_restaurant.TipScreen" owl="1"> + <div class="tip-screen screen"> + <div class="pos-receipt-container"/> + <div class="screen-content"> + <div class="top-content"> + <span class="button back" t-on-click="showScreen('FloorScreen')"> + <i class="fa fa-angle-double-left"></i> + <span> </span> + <span>Back</span> + </span> + <span class="button" t-if="env.pos.proxy.printer" t-on-click="printTipReceipt()"> + <i class="fa fa-print"></i> + <span> </span> + <span>Reprint receipts</span> + </span> + <div class="top-content-center"> + <h1>Add a tip</h1> + </div> + <div class="button highlight next" t-on-click="validateTip"> + Settle <i class="fa fa-angle-double-right"></i> + </div> + </div> + <div class="tip-options"> + <div class="total-amount"> + <t t-esc="overallAmountStr" /> + </div> + <div class="tip-amount-options"> + <div class="percentage-amounts"> + <t t-foreach="percentageTips" t-as="tip" t-key="tip.percentage"> + <div class="button" t-on-click="state.inputTipAmount = tip.amount.toFixed(2)"> + <div class="percentage"> + <t t-esc="tip.percentage"></t> + </div> + <div class="amount"> + <t t-esc="env.pos.format_currency(tip.amount)" /> + </div> + </div> + </t> + </div> + <div class="no-tip" t-on-click="state.inputTipAmount = '0'"> + <div class="button">No Tip</div> + </div> + <div class="custom-amount-form"> + <div class="item label">Amount</div> + <div class="item input"> + <input type="text" t-model="state.inputTipAmount" t-att-data-amount="state.inputTipAmount" /> + <div class="currency"> + <t t-esc="env.pos.getCurrencySymbol()" /> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/src/xml/TipReceipt.xml b/addons/pos_restaurant/static/src/xml/TipReceipt.xml new file mode 100644 index 00000000..2a82356e --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/TipReceipt.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="TipReceipt" owl="1"> + <div class="pos-receipt"> + <t t-if="receipt.company.logo"> + <img class="pos-receipt-logo" t-att-src="receipt.company.logo" alt="Logo"/> + <br/> + </t> + <t t-if="!receipt.company.logo"> + <h2 class="pos-receipt-center-align"> + <t t-esc="receipt.company.name" /> + </h2> + <br/> + </t> + <div class="pos-receipt-contact"> + <t t-if="receipt.company.contact_address"> + <div><t t-esc="receipt.company.contact_address" /></div> + </t> + <t t-if="receipt.company.phone"> + <div>Tel:<t t-esc="receipt.company.phone" /></div> + </t> + <t t-if="receipt.company.vat"> + <div>VAT:<t t-esc="receipt.company.vat" /></div> + </t> + <t t-if="receipt.company.email"> + <div><t t-esc="receipt.company.email" /></div> + </t> + <t t-if="receipt.company.website"> + <div><t t-esc="receipt.company.website" /></div> + </t> + <t t-if="receipt.header_html"> + <t t-raw="receipt.header_html" /> + </t> + <t t-if="!receipt.header_html and receipt.header"> + <div><t t-esc="receipt.header" /></div> + </t> + <t t-if="receipt.cashier"> + <div class="cashier"> + <div>--------------------------------</div> + <div>Served by <t t-esc="receipt.cashier" /></div> + </div> + </t> + </div> + <br/> + + <div class="pos-payment-terminal-receipt"> + <t t-raw="data"/> + </div> + <br/> + + + <div class="subtotal"> + <span>Subtotal</span> + <div class="pos-receipt-right-align"><t t-esc="total"/></div> + </div> + <br/> + + <div class="tip"> + <span>Tip:</span> + <div class="pos-receipt-right-align">________________________</div> + </div> + <br/> + + <div class="total"> + <span>Total:</span> + <div class="pos-receipt-right-align">________________________</div> + </div> + <br/> + <br/> + + <div class="signature"> + <div>______________________________________________</div> + <div>Signature</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 new file mode 100644 index 00000000..e162e03d --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/multiprint.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="NameWrapped"> + <t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line"> + <div style="text-align: right"> + <span t-esc="wrapped_line"/> + </div> + </t> + </t> + + <t t-name="OrderChangeReceipt"> + <div class="pos-receipt"> + <div class="pos-receipt-order-data"><t t-esc="changes.name" /></div> + <t t-if="changes.floor || changes.table"> + <br /> + <div class="pos-receipt-title"> + <t t-esc="changes.floor" /> / <t t-esc="changes.table"/> + </div> + </t> + <br /> + <br /> + <t t-if="changes.cancelled.length > 0"> + <div class="pos-order-receipt-cancel"> + <div class="pos-receipt-title"> + CANCELLED + <t t-esc='changes.time.hours'/>:<t t-esc='changes.time.minutes'/> + </div> + <br /> + <br /> + <t t-foreach="changes.cancelled" t-as="change"> + <div class="multiprint-flex"> + <t t-esc="change.qty"/> + <span t-esc="change.name_wrapped[0]"/> + </div> + <t t-call="NameWrapped"/> + <t t-if="change.note"> + <div> + NOTE + <span class="pos-receipt-right-align">...</span> + </div> + <div><span class="pos-receipt-left-padding">--- <t t-esc="change.note" /></span></div> + <br/> + </t> + </t> + <br /> + <br /> + </div> + </t> + <t t-if="changes.new.length > 0"> + <div class="pos-receipt-title"> + NEW + <t t-esc='changes.time.hours'/>:<t t-esc='changes.time.minutes'/> + </div> + <br /> + <br /> + <t t-foreach="changes.new" t-as="change"> + <div class="multiprint-flex"> + <t t-esc="change.qty"/> + <span t-esc="change.name_wrapped[0]"/> + </div> + <t t-call="NameWrapped"/> + <t t-if="change.note"> + <div> + NOTE + <span class="pos-receipt-right-align">...</span> + </div> + <div><span class="pos-receipt-left-padding">--- <t t-esc="change.note" /></span></div> + <br/> + </t> + </t> + <br /> + <br /> + </t> + </div> + </t> + +</templates> |
