summaryrefslogtreecommitdiff
path: root/addons/pos_restaurant/static/src
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/pos_restaurant/static/src
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/pos_restaurant/static/src')
-rw-r--r--addons/pos_restaurant/static/src/css/restaurant.css583
-rw-r--r--addons/pos_restaurant/static/src/js/Chrome.js99
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js34
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js33
-rw-r--r--addons/pos_restaurant/static/src/js/Resizeable.js330
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/BillScreen.js31
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js19
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js60
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js315
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js73
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js28
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js17
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js48
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.js42
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js38
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SplitBillButton.js33
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/SubmitOrderButton.js70
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TableGuestsButton.js44
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/TransferOrderButton.js30
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ProductScreen/Orderline.js46
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js28
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js197
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitOrderline.js25
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TicketScreen.js158
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TipScreen.js156
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js399
-rw-r--r--addons/pos_restaurant/static/src/js/multiprint.js315
-rw-r--r--addons/pos_restaurant/static/src/js/notes.js42
-rw-r--r--addons/pos_restaurant/static/src/js/payment.js24
-rw-r--r--addons/pos_restaurant/static/src/xml/Chrome.xml10
-rw-r--r--addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml19
-rw-r--r--addons/pos_restaurant/static/src/xml/Resizeable.xml8
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml36
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditBar.xml63
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/FloorScreen/EditableTable.xml31
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/FloorScreen/FloorScreen.xml37
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/FloorScreen/TableWidget.xml19
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml24
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml19
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SplitBillButton.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/SubmitOrderButton.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml14
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ControlButtons/TransferOrderButton.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ProductScreen/Orderline.xml15
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml50
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitBillScreen.xml46
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/SplitBillScreen/SplitOrderline.xml48
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml33
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml61
-rw-r--r--addons/pos_restaurant/static/src/xml/TipReceipt.xml79
-rw-r--r--addons/pos_restaurant/static/src/xml/multiprint.xml78
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() &amp;&amp; props.line.order.get_total_paid() &lt; 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>