19.0 vanilla
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><circle cx="25" cy="25.001" r="17" fill="#985184"/><path d="m42.586 15.587-8.172-8.172c-1.26-1.26-.367-3.414 1.414-3.414H42a4 4 0 0 1 4 4v6.171c0 1.782-2.154 2.675-3.414 1.415ZM7.414 34.415l8.172 8.172c1.26 1.26.367 3.414-1.414 3.414H8a4 4 0 0 1-4-4v-6.172c0-1.781 2.154-2.674 3.414-1.414Zm27 8.172 8.172-8.172c1.26-1.26 3.414-.367 3.414 1.414v6.172a4 4 0 0 1-4 4h-6.172c-1.781 0-2.674-2.154-1.414-3.414ZM15.586 7.415l-8.172 8.172C6.154 16.847 4 15.954 4 14.172v-6.17a4 4 0 0 1 4-4h6.172c1.781 0 2.674 2.154 1.414 3.414Z" fill="#FBB945"/></svg>
|
||||
|
After Width: | Height: | Size: 628 B |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="23" height="23" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.214 6.16a.41.41 0 0 1-.41.411H.41A.41.41 0 0 0 0 6.982V22.59c0 .227.184.411.41.411h22.18a.41.41 0 0 0 .41-.41V.41a.41.41 0 0 0-.41-.41H8.624a.41.41 0 0 0-.41.41v5.75Zm13.143-4.517h-11.5V6.16c0 1.134-.92 2.053-2.053 2.053H1.643v13.143h6.571v-7.393h9.036v1.643H9.857v5.75h11.5V1.643Z" fill="#000"/></svg>
|
||||
|
After Width: | Height: | Size: 429 B |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="23" height="23" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" fill="#000"><path d="M13.79 4.356c.443-.743.71-1.617.71-2.356 0-1.657-1.343-2-3-2s-3 .343-3 2c0 .738.267 1.613.709 2.356A7.495 7.495 0 0 1 11.499 4c.8 0 1.57.125 2.292.356ZM17.25 11.5a5.75 5.75 0 1 1-11.5 0 5.75 5.75 0 0 1 11.5 0ZM9.21 18.644c-.443.743-.71 1.618-.71 2.356 0 1.657 1.343 2 3 2s3-.343 3-2c0-.738-.267-1.613-.71-2.356A7.496 7.496 0 0 1 11.5 19a7.495 7.495 0 0 1-2.29-.356ZM18.993 11.5c0 .952-.177 1.863-.5 2.7.771.497 1.713.8 2.5.8 1.657 0 2-1.343 2-3s-.343-3-2-3c-.698 0-1.518.238-2.233.638.152.596.233 1.22.233 1.862ZM2 9c.697 0 1.517.238 2.231.637a7.515 7.515 0 0 0-.233 1.863c0 .953.178 1.864.501 2.702C3.728 14.697 2.787 15 2 15c-1.657 0-2-1.343-2-3s.343-3 2-3Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h23v23H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
|
Before Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -1,188 +0,0 @@
|
|||
/*!
|
||||
* jQuery UI Touch Punch 0.2.3
|
||||
*
|
||||
* Copyright 2011–2014, Dave Furfero
|
||||
* Dual licensed under the MIT or GPL Version 2 licenses.
|
||||
*
|
||||
* Depends:
|
||||
* jquery.ui.widget.js
|
||||
* jquery.ui.mouse.js
|
||||
*/
|
||||
(function ($) {
|
||||
|
||||
// Detect touch support
|
||||
$.support.touch = (
|
||||
'ontouchend' in document || // Default check
|
||||
// Odoo fix for Chrome
|
||||
// See: https://github.com/furf/jquery-ui-touch-punch/issues/309
|
||||
'ontouchstart' in document ||
|
||||
'ontouchstart' in window ||
|
||||
navigator.maxTouchPoints > 0 ||
|
||||
navigator.msMaxTouchPoints > 0
|
||||
);
|
||||
|
||||
// Ignore browsers without touch support
|
||||
if (!$.support.touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
var mouseProto = $.ui.mouse.prototype,
|
||||
_mouseInit = mouseProto._mouseInit,
|
||||
_mouseDestroy = mouseProto._mouseDestroy,
|
||||
touchHandled;
|
||||
|
||||
/**
|
||||
* Simulate a mouse event based on a corresponding touch event
|
||||
* @param {Object} event A touch event
|
||||
* @param {String} simulatedType The corresponding mouse event
|
||||
*/
|
||||
function simulateMouseEvent (event, simulatedType) {
|
||||
|
||||
// Ignore multi-touch events
|
||||
if (event.originalEvent.touches.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var touch = event.originalEvent.changedTouches[0],
|
||||
simulatedEvent = document.createEvent('MouseEvents');
|
||||
|
||||
// Initialize the simulated mouse event using the touch event's coordinates
|
||||
simulatedEvent.initMouseEvent(
|
||||
simulatedType, // type
|
||||
true, // bubbles
|
||||
true, // cancelable
|
||||
window, // view
|
||||
1, // detail
|
||||
touch.screenX, // screenX
|
||||
touch.screenY, // screenY
|
||||
touch.clientX, // clientX
|
||||
touch.clientY, // clientY
|
||||
false, // ctrlKey
|
||||
false, // altKey
|
||||
false, // shiftKey
|
||||
false, // metaKey
|
||||
0, // button
|
||||
null // relatedTarget
|
||||
);
|
||||
|
||||
// Dispatch the simulated event to the target element
|
||||
event.target.dispatchEvent(simulatedEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchstart events
|
||||
* @param {Object} event The widget element's touchstart event
|
||||
*/
|
||||
mouseProto._touchStart = function (event) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Ignore the event if another widget is already being handled
|
||||
if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the flag to prevent other widgets from inheriting the touch event
|
||||
touchHandled = true;
|
||||
|
||||
// Track movement to determine if interaction was a click
|
||||
self._touchMoved = false;
|
||||
|
||||
// Simulate the mouseover event
|
||||
simulateMouseEvent(event, 'mouseover');
|
||||
|
||||
// Simulate the mousemove event
|
||||
simulateMouseEvent(event, 'mousemove');
|
||||
|
||||
// Simulate the mousedown event
|
||||
simulateMouseEvent(event, 'mousedown');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchmove events
|
||||
* @param {Object} event The document's touchmove event
|
||||
*/
|
||||
mouseProto._touchMove = function (event) {
|
||||
|
||||
// Ignore event if not handled
|
||||
if (!touchHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Interaction was not a click
|
||||
this._touchMoved = true;
|
||||
|
||||
// Simulate the mousemove event
|
||||
simulateMouseEvent(event, 'mousemove');
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the jQuery UI widget's touchend events
|
||||
* @param {Object} event The document's touchend event
|
||||
*/
|
||||
mouseProto._touchEnd = function (event) {
|
||||
|
||||
// Ignore event if not handled
|
||||
if (!touchHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulate the mouseup event
|
||||
simulateMouseEvent(event, 'mouseup');
|
||||
|
||||
// Simulate the mouseout event
|
||||
simulateMouseEvent(event, 'mouseout');
|
||||
|
||||
// If the touch interaction did not move, it should trigger a click
|
||||
if (!this._touchMoved) {
|
||||
|
||||
// Simulate the click event
|
||||
simulateMouseEvent(event, 'click');
|
||||
}
|
||||
|
||||
// Unset the flag to allow other widgets to inherit the touch event
|
||||
touchHandled = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* A duck punch of the $.ui.mouse _mouseInit method to support touch events.
|
||||
* This method extends the widget with bound touch event handlers that
|
||||
* translate touch events to mouse events and pass them to the widget's
|
||||
* original mouse event handling methods.
|
||||
*/
|
||||
mouseProto._mouseInit = function () {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Delegate the touch handlers to the widget's element
|
||||
self.element.bind({
|
||||
touchstart: $.proxy(self, '_touchStart'),
|
||||
touchmove: $.proxy(self, '_touchMove'),
|
||||
touchend: $.proxy(self, '_touchEnd')
|
||||
});
|
||||
|
||||
// Call the original $.ui.mouse init method
|
||||
_mouseInit.call(self);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the touch event handlers
|
||||
*/
|
||||
mouseProto._mouseDestroy = function () {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Delegate the touch handlers to the widget's element
|
||||
self.element.unbind({
|
||||
touchstart: $.proxy(self, '_touchStart'),
|
||||
touchmove: $.proxy(self, '_touchMove'),
|
||||
touchend: $.proxy(self, '_touchEnd')
|
||||
});
|
||||
|
||||
// Call the original $.ui.mouse destroy method
|
||||
_mouseDestroy.call(self);
|
||||
};
|
||||
|
||||
})(jQuery);
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { Navbar } from "@point_of_sale/app/components/navbar/navbar";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Navbar.prototype, {
|
||||
showTabs() {
|
||||
if (this.pos.config.module_pos_restaurant) {
|
||||
return !this.pos.selectedTable;
|
||||
} else {
|
||||
return super.showTabs();
|
||||
}
|
||||
},
|
||||
onSwitchButtonClick() {
|
||||
const mode = this.pos.floorPlanStyle === "kanban" ? "default" : "kanban";
|
||||
localStorage.setItem("floorPlanStyle", mode);
|
||||
this.pos.floorPlanStyle = mode;
|
||||
},
|
||||
get showEditPlanButton() {
|
||||
return true;
|
||||
},
|
||||
makeButtonBounce() {
|
||||
this.pos.shouldSetTable = true;
|
||||
setTimeout(() => (this.pos.shouldSetTable = false), 400);
|
||||
},
|
||||
canClick() {
|
||||
if (this.pos.getOrder()?.isFilledDirectSale) {
|
||||
this.makeButtonBounce();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onTicketButtonClick() {
|
||||
return this.canClick() && this.pos.navigate("TicketScreen");
|
||||
},
|
||||
onClickPlanButton() {
|
||||
this.pos.getOrder()?.cleanCourses();
|
||||
return this.canClick() && this.pos.navigate("FloorScreen");
|
||||
},
|
||||
get mainButton() {
|
||||
return this.pos.router.state.current === "FloorScreen" ? "table" : super.mainButton;
|
||||
},
|
||||
get currentOrderName() {
|
||||
return this.pos.getOrder().getName().replace("T ", "");
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.Navbar" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension">
|
||||
<xpath expr="//DropdownItem[contains(text(), 'Backend')]" position="before">
|
||||
<t t-if="pos.router.state.current == 'FloorScreen'">
|
||||
<DropdownItem t-if="showEditPlanButton and this.pos.config.floor_ids.length" onSelected="() => this.pos.toggleEditMode()">
|
||||
Edit Plan
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<DropdownItem t-if="pos.router.state.current == 'FloorScreen'" onSelected="() => this.onSwitchButtonClick()">
|
||||
Switch Floor View
|
||||
</DropdownItem>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('pos-leftheader')]//OrderTabs" position="attributes">
|
||||
<attribute name="t-if">!pos.config.module_pos_restaurant</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('pos-leftheader')]//OrderTabs" position="after">
|
||||
<span t-if="pos.config.module_pos_restaurant and pos.getOrder()" t-esc="currentOrderName" class="badge rounded-pill text-bg-info py-2 fs-6 fw-bolder text-truncate"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('register-label')]" position="before">
|
||||
<button t-if="pos.config.module_pos_restaurant" class="table-button btn btn-lg btn-light lh-lg" t-att-class="{'active': mainButton === 'table', 'text-muted': this.pos.getOrder()?.isFilledDirectSale}" t-on-click="() => this.onClickPlanButton()">
|
||||
<span t-if="!ui.isSmall">Tables</span>
|
||||
<img t-else="" src="/pos_restaurant/static/img/plan.svg" class="navbar-icon" t-att-class="{'opacity-50': this.pos.getOrder()?.isFilledDirectSale}" alt="Floor Plan"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('navbar-separator')]" position="after">
|
||||
<div class="d-flex align-items-center" t-if="pos.orderToTransferUuid">
|
||||
<strong class="mx-2 text-warning">
|
||||
Select table to transfer order
|
||||
</strong>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('orders-button')]" position="attributes">
|
||||
<attribute name="t-att-class">{'active': mainButton === 'order', 'text-muted': this.pos.getOrder()?.isFilledDirectSale}</attribute>
|
||||
<attribute name="t-on-click">() => this.onTicketButtonClick()</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { Component, useState } from "@odoo/owl";
|
||||
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { SIZES, utils } from "@web/core/ui/ui_service";
|
||||
import {
|
||||
getButtons,
|
||||
EMPTY,
|
||||
ZERO,
|
||||
BACKSPACE,
|
||||
Numpad,
|
||||
} from "@point_of_sale/app/components/numpad/numpad";
|
||||
|
||||
export class NumpadDropdown extends Component {
|
||||
static template = "pos_restaurant.NumpadDropdown";
|
||||
static props = {};
|
||||
static components = { Numpad };
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
this.ui = useService("ui");
|
||||
this.numberBuffer = useService("number_buffer");
|
||||
this.numberBuffer.use({
|
||||
triggerAtEnter: () => this.pos.searchOrder(this.state.buffer),
|
||||
triggerAtInput: ({ buffer }) => this.checkIsValid(buffer),
|
||||
});
|
||||
this.state = useState({
|
||||
buffer: "",
|
||||
isValidBuffer: true,
|
||||
});
|
||||
}
|
||||
|
||||
get numpadButtons() {
|
||||
const colorClassMap = {
|
||||
[BACKSPACE.value]: "o_colorlist_item_numpad_color_1",
|
||||
};
|
||||
|
||||
return getButtons([{ ...EMPTY, disabled: true }, ZERO, BACKSPACE]).map((button, index) => ({
|
||||
...button,
|
||||
class: `
|
||||
${button.class}
|
||||
${colorClassMap[button.value] || ""}
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
searchOrder() {
|
||||
if (this.state.isValidBuffer) {
|
||||
this.pos.searchOrder(this.state.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
toggleTableSelector() {
|
||||
this.pos.tableSelectorState = !this.pos.tableSelectorState;
|
||||
}
|
||||
|
||||
get isSmall() {
|
||||
return utils.getSize() <= SIZES.SM;
|
||||
}
|
||||
|
||||
checkIsValid(buffer) {
|
||||
this.state.buffer = buffer;
|
||||
const res = this.pos.findTable(buffer);
|
||||
this.state.isValidBuffer = Boolean(res);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
.table-selector {
|
||||
min-width: 300px;
|
||||
height: clamp(200px, 50vh, 500px);
|
||||
|
||||
position: absolute;
|
||||
right: map-get($spacers, 2);
|
||||
top: $pos-navbar-height + map-get($spacers, 2);
|
||||
}
|
||||
|
||||
.table-selector-small, .table-selector {
|
||||
.input {
|
||||
flex: 0 0 10%;
|
||||
}
|
||||
|
||||
.jump-button {
|
||||
flex: 0 0 15%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.NumpadDropdown">
|
||||
<button class="btn btn-secondary btn-lg lh-lg" t-on-click="() => this.toggleTableSelector()">
|
||||
<i class="fa fa-fw fa-hashtag" aria-hidden="true"/>
|
||||
</button>
|
||||
<div t-if="this.pos.tableSelectorState and !this.isSmall" class="table-selector bg-light rounded d-flex flex-column z-3 shadow overflow-hidden">
|
||||
<div class="d-flex justify-content-center p-3">
|
||||
<span t-if="this.state.buffer" t-esc="this.state.buffer" class="input-value fw-bolder fs-3" t-att-class="{'text-danger opacity-75': !this.state.isValidBuffer}"/>
|
||||
<span t-else="" class="text-muted fs-3">Enter a table number</span>
|
||||
</div>
|
||||
<Numpad buttons="this.numpadButtons" class="'flex-grow-1 bg-200'"/>
|
||||
<button class="jump-button w-100 btn btn-primary rounded-0 border-0" t-on-click="() => this.searchOrder()">
|
||||
Jump
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="this.pos.tableSelectorState and this.isSmall" class="table-selector-small h-50 modal-dialog modal-dialog-centered z-3">
|
||||
<div class="modal-content h-100" t-att-class="props.contentClass" t-att-style="contentStyle">
|
||||
<main class="d-flex flex-column justify-content-between bg-light h-100">
|
||||
<div class="input-symbol">
|
||||
<div class="popup-input value active form-control form-control-lg py-3 text-center">
|
||||
<span t-if="this.state.buffer" t-esc="this.state.buffer" class="input-value fw-bolder fs-3" t-att-class="{'text-danger opacity-75': !this.state.isValidBuffer}"/>
|
||||
<span t-else="" class="text-muted fs-3">Enter a table number</span>
|
||||
</div>
|
||||
</div>
|
||||
<Numpad buttons="this.numpadButtons" class="'flex-grow-1 m-0 p-0 border-top rounded-3'"/>
|
||||
<div class="d-flex flex-grow-1 justify-content-center">
|
||||
<button class="jump-button flex-grow-1 btn btn-primary rounded-0 border-0 py-3" t-on-click="() => this.searchOrder()">
|
||||
Jump
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class OrderCourse extends Component {
|
||||
static template = "pos_restaurant.OrderCourse";
|
||||
static props = {
|
||||
course: Object,
|
||||
course_index: Number,
|
||||
slots: { type: Object, optional: true },
|
||||
};
|
||||
|
||||
get course() {
|
||||
return this.props.course;
|
||||
}
|
||||
|
||||
get comboSortedLines() {
|
||||
return this.course.lines.reduce((acc, line) => {
|
||||
if (line.combo_line_ids?.length > 0) {
|
||||
acc.push(line, ...line.combo_line_ids);
|
||||
} else if (!line.combo_parent_id) {
|
||||
acc.push(line);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
clickCourse(evt, course) {
|
||||
const order = course.order_id;
|
||||
order.selectCourse(course);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderCourse">
|
||||
<div t-att-class="{'mb-1' : course.isEmpty()}">
|
||||
<div class="order-course-name p-2 text-truncate d-flex"
|
||||
t-att-class="{
|
||||
'o_colorlist_item_color_10' : course.isSelected(),
|
||||
'bg-secondary': !course.isSelected(),
|
||||
'cursor-pointer': !course.fired}"
|
||||
t-on-click="(event) => this.clickCourse(event, course)">
|
||||
<span class="fw-bolder" t-out="course.name"/>
|
||||
<span t-if="course.fired" class="rounded text-bg-info fs-6 px-2 ms-auto">Fired</span>
|
||||
</div>
|
||||
<t t-foreach="comboSortedLines" t-as="line" t-key="line_index">
|
||||
<t t-slot="default" line="line" />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { OrderDisplay } from "@point_of_sale/app/components/order_display/order_display";
|
||||
import { OrderCourse } from "@pos_restaurant/app/components/order_course/order_course";
|
||||
patch(OrderDisplay.prototype, {
|
||||
get displayCourses() {
|
||||
if (
|
||||
!this.order.config.module_pos_restaurant ||
|
||||
this.props.mode !== "display" ||
|
||||
this.order.finalized
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return this.order.course_ids.length > 0;
|
||||
},
|
||||
});
|
||||
OrderDisplay.components = { ...OrderDisplay.components, OrderCourse };
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderDisplay" t-inherit="point_of_sale.OrderDisplay" t-inherit-mode="extension">
|
||||
|
||||
<xpath expr="(//t[@t-if='lines.length'])[1]" position="attributes" >
|
||||
<attribute name="t-if">lines.length or displayCourses</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="(//div[hasclass('order-container')]/t[@t-foreach='lines'])[1]" position="replace" >
|
||||
<t t-if="displayCourses">
|
||||
<t t-foreach="order.courses" t-as="course" t-key="course_index">
|
||||
<OrderCourse course="course" course_index="course_index" t-slot-scope="course_scope">
|
||||
<t t-if="props.slots.default" t-slot="default" line="course_scope.line"/>
|
||||
<Orderline t-else="" line="course_scope.line" mode="this.props.mode"/>
|
||||
</OrderCourse>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderTabs" t-inherit="point_of_sale.OrderTabs" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('floating-order-container')]" position="before">
|
||||
<t t-set="changes" t-value="this.pos.getOrderChanges(order)" />
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('floating-order-container')]" position="inside">
|
||||
<div t-if="changes.nbrOfChanges and this.pos.config.module_pos_restaurant"
|
||||
class="position-absolute rounded-circle d-flex align-items-center justify-content-center"
|
||||
style="top: -7px; right: -6px; width: 1.5rem; height: 1.5rem"
|
||||
t-att-class="{'text-bg-danger bg-danger': changes.nbrOfChanges}">
|
||||
<span t-esc="changes.nbrOfChanges" />
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('floating-order-container')]" position="attributes">
|
||||
<attribute name="t-att-class">{'me-2': changes.nbrOfChanges and this.pos.config.module_pos_restaurant}</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_sale.Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension">
|
||||
<xpath expr="//t[@t-esc='vals.unitPart']" position="replace">
|
||||
<t t-esc="line.uiState.splitQty or vals.unitPart"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { ListContainer } from "@point_of_sale/app/components/list_container/list_container";
|
||||
import { TextInputPopup } from "@point_of_sale/app/components/popups/text_input_popup/text_input_popup";
|
||||
import { usePos } from "@point_of_sale/app/hooks/pos_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class EditOrderNamePopup extends TextInputPopup {
|
||||
static template = "pos_restaurant.EditOrderNamePopup";
|
||||
static components = { ...super.components, ListContainer };
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
this.dialog = useService("dialog");
|
||||
super.setup();
|
||||
}
|
||||
transferOrder(order) {
|
||||
this.pos.transferOrder(this.currentOrder.uuid, null, order);
|
||||
this.pos.setOrder(order);
|
||||
this.dialog.closeAll();
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.pos.getOrder();
|
||||
}
|
||||
get items() {
|
||||
return this.pos
|
||||
.getOpenOrders()
|
||||
.filter((o) => !o.table_id && o.uuid != this.currentOrder.uuid)
|
||||
.toSorted((a, b) => a.getName().localeCompare(b.getName()));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.EditOrderNamePopup" t-inherit="point_of_sale.TextInputPopup" t-inherit-mode="primary">
|
||||
<xpath expr="//textarea" position="replace">
|
||||
<div class="d-flex flex-column-reverse align-content-center">
|
||||
<ListContainer
|
||||
t-if="items"
|
||||
items="items"
|
||||
t-slot-scope="scope"
|
||||
class="'mb-3 h-100'"
|
||||
forceSmall="false"
|
||||
>
|
||||
<t t-set="order" t-value="scope.item" />
|
||||
<div class="position-relative">
|
||||
<button t-esc="order.getName()"
|
||||
class="btn btn-lg btn-secondary text-truncate me-1"
|
||||
style="min-width: 4rem;"
|
||||
t-on-click="() => this.transferOrder(order)"
|
||||
/>
|
||||
</div>
|
||||
</ListContainer>
|
||||
<textarea t-att-rows="props.rows" class="h-100 flex-grow-1 form-control form-control-lg mx-auto" type="text" t-model="state.inputValue" t-ref="input" t-att-placeholder="props.placeholder" t-on-keydown="onKeydown" />
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('btn-primary')]" position="attributes">
|
||||
<attribute name="t-attf-class">{{state.inputValue ? '' : 'disabled'}}</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { ReceiptHeader } from "@point_of_sale/app/screens/receipt_screen/receipt/receipt_header/receipt_header";
|
||||
|
||||
export class TipReceipt extends Component {
|
||||
static template = "pos_restaurant.TipReceipt";
|
||||
static components = { ReceiptHeader };
|
||||
static props = ["data", "total", "order"];
|
||||
|
||||
get total() {
|
||||
return this.props.total;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.TipReceipt">
|
||||
<div class="pos-receipt">
|
||||
<ReceiptHeader order="props.order" />
|
||||
<div class="pos-payment-terminal-receipt">
|
||||
<pre t-esc="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>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { DataServiceOptions } from "@point_of_sale/app/models/data_service_options";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(DataServiceOptions.prototype, {
|
||||
get databaseTable() {
|
||||
return {
|
||||
...super.databaseTable,
|
||||
"restaurant.order.course": {
|
||||
key: "uuid",
|
||||
condition: (record) => record.order_id?.canBeRemovedFromIndexedDB,
|
||||
},
|
||||
};
|
||||
},
|
||||
get cascadeDeleteModels() {
|
||||
return [...super.cascadeDeleteModels, "restaurant.order.course"];
|
||||
},
|
||||
get dynamicModels() {
|
||||
return [...super.dynamicModels, "restaurant.order.course"];
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { PosConfig } from "@point_of_sale/app/models/pos_config";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(PosConfig.prototype, {
|
||||
get useProxy() {
|
||||
return super.useProxy || (this.iot_device_ids && this.iot_device_ids.length > 0);
|
||||
},
|
||||
get isShareable() {
|
||||
return super.isShareable || this.module_pos_restaurant;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(PosOrder.prototype, {
|
||||
setup(_defaultObj, options) {
|
||||
super.setup(...arguments);
|
||||
if (this.config.module_pos_restaurant) {
|
||||
this.customer_count = this.customer_count || 1;
|
||||
}
|
||||
},
|
||||
initState() {
|
||||
super.initState();
|
||||
this.uiState.selected_course_uuid = undefined;
|
||||
if (this.config.module_pos_restaurant) {
|
||||
this.uiState.mappingOrderlinesUuid = {};
|
||||
}
|
||||
},
|
||||
getCustomerCount() {
|
||||
return this.customer_count;
|
||||
},
|
||||
setCustomerCount(count) {
|
||||
this.customer_count = Math.max(count, 1);
|
||||
},
|
||||
getTable() {
|
||||
return this.table_id;
|
||||
},
|
||||
get isBooked() {
|
||||
const res = super.isBooked;
|
||||
if (this.config.module_pos_restaurant) {
|
||||
return super.isBooked || !this.isDirectSale;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
amountPerGuest(numCustomers = this.customer_count) {
|
||||
if (numCustomers === 0) {
|
||||
return 0;
|
||||
}
|
||||
return this.totalDue / numCustomers;
|
||||
},
|
||||
setBooked(booked) {
|
||||
this.uiState.booked = booked;
|
||||
},
|
||||
getName() {
|
||||
if (this.config.module_pos_restaurant) {
|
||||
if (this.isDirectSale) {
|
||||
return _t("Direct Sale");
|
||||
}
|
||||
if (this.getTable()) {
|
||||
const table = this.getTable();
|
||||
const child_tables = this.models["restaurant.table"].filter((t) => {
|
||||
if (t.floor_id && t.floor_id.id === table.floor_id?.id) {
|
||||
return table.isParent(t);
|
||||
}
|
||||
});
|
||||
let name = "T " + table.table_number.toString();
|
||||
for (const child_table of child_tables) {
|
||||
name += ` & ${child_table.table_number}`;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return super.getName(...arguments);
|
||||
},
|
||||
get isDirectSale() {
|
||||
return Boolean(
|
||||
this.config.module_pos_restaurant &&
|
||||
!this.table_id &&
|
||||
!this.floating_order_name &&
|
||||
this.state == "draft" &&
|
||||
!this.isRefund
|
||||
);
|
||||
},
|
||||
get isFilledDirectSale() {
|
||||
return this.isDirectSale && !this.isEmpty();
|
||||
},
|
||||
setPartner(partner) {
|
||||
if (this.config.module_pos_restaurant && this.isDirectSale) {
|
||||
this.floating_order_name = partner.name;
|
||||
}
|
||||
return super.setPartner(...arguments);
|
||||
},
|
||||
cleanCourses() {
|
||||
if (!this.hasCourses()) {
|
||||
return;
|
||||
}
|
||||
let lastFiredIndex = -1;
|
||||
const courses = this.courses;
|
||||
for (let i = courses.length - 1; i >= 0; i--) {
|
||||
if (courses[i].fired) {
|
||||
lastFiredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const originalLength = courses.length;
|
||||
const removedCourses = [];
|
||||
const cleanedCourses = courses
|
||||
.filter((course, index) => {
|
||||
const shouldKeep = index <= lastFiredIndex || !course.isEmpty();
|
||||
if (!shouldKeep) {
|
||||
removedCourses.push(course);
|
||||
}
|
||||
return shouldKeep;
|
||||
})
|
||||
.map((course, newIndex) => {
|
||||
course.index = newIndex + 1;
|
||||
return course;
|
||||
});
|
||||
removedCourses.forEach((course) => {
|
||||
course.delete();
|
||||
});
|
||||
if (cleanedCourses.length !== originalLength) {
|
||||
this.course_ids = cleanedCourses;
|
||||
}
|
||||
},
|
||||
get courses() {
|
||||
return this.course_ids.toSorted((a, b) => a.index - b.index);
|
||||
},
|
||||
hasCourses() {
|
||||
return this.course_ids.length > 0;
|
||||
},
|
||||
getFirstCourse() {
|
||||
return this.courses[0];
|
||||
},
|
||||
getLastCourse() {
|
||||
return this.courses.at(-1);
|
||||
},
|
||||
ensureCourseSelection() {
|
||||
if (!this.hasCourses()) {
|
||||
return;
|
||||
}
|
||||
// Select the first non fired course
|
||||
const nonFiredCourse = this.courses.find((course) => !course.fired);
|
||||
this.selectCourse(nonFiredCourse ?? this.getLastCourse());
|
||||
},
|
||||
deselectCourse() {
|
||||
this.selectCourse(undefined);
|
||||
},
|
||||
selectCourse(course) {
|
||||
if (course) {
|
||||
this.uiState.selected_course_uuid = course.uuid;
|
||||
this.deselectOrderline();
|
||||
} else {
|
||||
this.uiState.selected_course_uuid = undefined;
|
||||
}
|
||||
},
|
||||
getSelectedCourse() {
|
||||
if (!this.uiState.selected_course_uuid) {
|
||||
return;
|
||||
}
|
||||
return this.course_ids.find((course) => course.uuid === this.uiState.selected_course_uuid);
|
||||
},
|
||||
getNextCourseIndex() {
|
||||
return (
|
||||
this.course_ids.reduce(
|
||||
(maxIndex, course) => (course.index > maxIndex ? course.index : maxIndex),
|
||||
0
|
||||
) + 1
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { PosOrderline } from "@point_of_sale/app/models/pos_order_line";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(PosOrderline.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.note = this.note || "";
|
||||
},
|
||||
//@override
|
||||
clone() {
|
||||
const orderline = super.clone(...arguments);
|
||||
orderline.note = this.note;
|
||||
return orderline;
|
||||
},
|
||||
getDisplayClasses() {
|
||||
return {
|
||||
...super.getDisplayClasses(),
|
||||
"has-change": this.uiState.hasChange && this.config.module_pos_restaurant,
|
||||
};
|
||||
},
|
||||
canBeMergedWith(orderline) {
|
||||
if (this.course_id) {
|
||||
if (this.course_id.uuid !== orderline.course_id?.uuid) {
|
||||
return false;
|
||||
}
|
||||
} else if (orderline.course_id?.uuid) {
|
||||
// In case of order merge
|
||||
return false;
|
||||
}
|
||||
return super.canBeMergedWith(orderline);
|
||||
},
|
||||
// To be overriden by other modules (eg: pos_discount)
|
||||
isGlobalDiscountApplicable() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { PosPayment } from "@point_of_sale/app/models/pos_payment";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(PosPayment.prototype, {
|
||||
//@override
|
||||
canBeAdjusted() {
|
||||
if (this.payment_method_id.payment_terminal) {
|
||||
return this.payment_method_id.payment_terminal.canBeAdjusted(this.uuid);
|
||||
}
|
||||
return (
|
||||
!this.payment_method_id.is_cash_count &&
|
||||
this.payment_method_id.payment_method_type != "qr_code"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { Base } from "@point_of_sale/app/models/related_models";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class RestaurantOrderCourse extends Base {
|
||||
static pythonModel = "restaurant.order.course";
|
||||
|
||||
get name() {
|
||||
return _t("Course") + " " + this.index;
|
||||
}
|
||||
isSelected() {
|
||||
return this.order_id?.uiState.selected_course_uuid === this.uuid;
|
||||
}
|
||||
get lines() {
|
||||
return this.line_ids;
|
||||
}
|
||||
isEmpty() {
|
||||
return this.line_ids?.length === 0;
|
||||
}
|
||||
isReadyToFire() {
|
||||
return !this.fired && !this.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("pos_available_models")
|
||||
.add(RestaurantOrderCourse.pythonModel, RestaurantOrderCourse);
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { Base } from "@point_of_sale/app/models/related_models";
|
||||
|
||||
export class RestaurantTable extends Base {
|
||||
static pythonModel = "restaurant.table";
|
||||
|
||||
setup(vals) {
|
||||
super.setup(vals);
|
||||
this.table_number = vals.table_number || 0;
|
||||
}
|
||||
initState() {
|
||||
super.initState();
|
||||
this.uiState = {
|
||||
initialPosition: {},
|
||||
orderCount: 0,
|
||||
changeCount: 0,
|
||||
};
|
||||
}
|
||||
isParent(t) {
|
||||
return t.parent_id && (t.parent_id.id === this.id || this.isParent(t.parent_id));
|
||||
}
|
||||
getParent() {
|
||||
return this.parent_id?.getParent() || this;
|
||||
}
|
||||
getParentSide() {
|
||||
if (!this.parent_id) {
|
||||
return;
|
||||
}
|
||||
const dx = this.position_h - this.parent_id.getX();
|
||||
const dy = this.position_v - this.parent_id.getY();
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
return dx < 0 ? "right" : "left";
|
||||
}
|
||||
return dy > 0 ? "bottom" : "top";
|
||||
}
|
||||
getX() {
|
||||
if (!this.parent_id) {
|
||||
return this.position_h;
|
||||
}
|
||||
const parent_side = this.parent_side || this.getParentSide();
|
||||
if (["top", "bottom"].includes(parent_side)) {
|
||||
return this.parent_id.getX();
|
||||
}
|
||||
if (parent_side === "left") {
|
||||
return this.parent_id.getX() + this.parent_id.width;
|
||||
}
|
||||
return this.parent_id.getX() - this.width;
|
||||
}
|
||||
getY() {
|
||||
if (!this.parent_id) {
|
||||
return this.position_v;
|
||||
}
|
||||
const parent_side = this.parent_side || this.getParentSide();
|
||||
this.parent_side = parent_side;
|
||||
if (["left", "right"].includes(parent_side)) {
|
||||
return this.parent_id.getY();
|
||||
}
|
||||
if (parent_side === "bottom") {
|
||||
return this.parent_id.getY() + this.parent_id.height;
|
||||
}
|
||||
return this.parent_id.getY() - this.height;
|
||||
}
|
||||
getCenter() {
|
||||
return {
|
||||
x: this.getX() + this.width / 2,
|
||||
y: this.getY() + this.height / 2,
|
||||
};
|
||||
}
|
||||
getOrders() {
|
||||
return this.models["pos.order"].filter(
|
||||
(o) =>
|
||||
o.table_id?.id === this.id &&
|
||||
// Include the orders that are in tipping state.
|
||||
(!o.finalized || o.uiState.screen_data?.value?.name === "TipScreen")
|
||||
);
|
||||
}
|
||||
getOrder() {
|
||||
return (
|
||||
this.parent_id?.getOrder?.() ||
|
||||
this.backLink("<-pos.order.table_id").find((o) => !o.finalized)
|
||||
);
|
||||
}
|
||||
setPositionAsIfLinked(parent, side) {
|
||||
this.parent_id = parent;
|
||||
this.parent_side = side;
|
||||
this.position_h = this.getX();
|
||||
this.position_v = this.getY();
|
||||
this.parent_id = null;
|
||||
}
|
||||
getName() {
|
||||
return this.table_number.toString();
|
||||
}
|
||||
get children() {
|
||||
return this.backLink("<-restaurant.table.parent_id");
|
||||
}
|
||||
get rootTable() {
|
||||
let table = this;
|
||||
while (table.parent_id) {
|
||||
table = table.parent_id;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
}
|
||||
registry.category("pos_available_models").add(RestaurantTable.pythonModel, RestaurantTable);
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { FeedbackScreen } from "@point_of_sale/app/screens/feedback_screen/feedback_screen";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(FeedbackScreen.prototype, {
|
||||
goNext() {
|
||||
if (this.pos.isContinueSplitting(this.currentOrder)) {
|
||||
this.pos.continueSplitting(this.currentOrder);
|
||||
} else {
|
||||
super.goNext();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
.floor-map {
|
||||
.table-handle {
|
||||
padding: 0px;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
background: white;
|
||||
box-shadow: 0px 2px 3px rgba(0,0,0,0.2);
|
||||
/* See o-grab-cursor mixin */
|
||||
cursor: url(/web/static/img/openhand.cur), grab;
|
||||
z-index: 100;
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
transition: 150ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floor-grid {
|
||||
background-image: linear-gradient(0, rgba($o-gray-400, 0.5) 14%, transparent 14%),
|
||||
linear-gradient(90deg, transparent 90%, rgba($o-gray-400, 0.5) 70%);
|
||||
background-size: 10px 10px;
|
||||
}
|
||||
|
||||
.floor-picture {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
.image-uploader {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.floor-dropdown-fill {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.pos-dropdown-menu .dropdown-item.o-dropdown-item {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.floor-kanban {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.floor-screen:where(:has(.table-selector-small)) .floor-kanban {
|
||||
padding-bottom: 80px !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.FloorScreen">
|
||||
<div class="floor-screen screen h-100 position-relative d-flex flex-column flex-nowrap m-0 bg-300 text-start overflow-hidden">
|
||||
<t t-set="editButtonClass" t-value="'btn btn-lg'" />
|
||||
<t t-set="hasSelectedTable" t-value="selectedTables.length > 0" />
|
||||
<t t-set="firstSelectedTable" t-value="selectedTables.length ? selectedTables[0] : null" />
|
||||
<div t-attf-class="d-flex flex-row {{pos.isEditMode ? 'flex-wrap' : ''}} justify-content-between px-2 gap-1 border-bottom bg-view">
|
||||
<button t-if="!pos.isEditMode" class="new-order btn btn-lg lh-lg my-2" t-attf-class="{{ this.pos.getOrder()?.isDirectSale ? 'btn-secondary' : 'btn-primary'}}"
|
||||
t-on-click="() => this.clickNewOrder()">
|
||||
<t t-if="this.pos.getOrder()?.isDirectSale">
|
||||
<i class="fa fa-angle-double-left fa-fw" role="img" aria-label="Back" title="Back"/>
|
||||
<span t-if="!ui.isSmall" class="ms-1">Back</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-plus fa-fw" role="img" aria-label="New Order" title="New Order"/>
|
||||
<span t-if="!ui.isSmall" class="ms-1">New Order</span>
|
||||
</t>
|
||||
</button>
|
||||
<!-- Center Side Div -->
|
||||
<div class="floor-selector d-flex gap-1 py-2 overflow-auto">
|
||||
<t t-foreach="pos.models['restaurant.floor'].filter(f => f.active !== false)" t-as="floor" t-key="floor.id">
|
||||
<button class="button button-floor btn btn-outline-secondary btn-lg px-3 lh-lg text-nowrap text-body" t-attf-class="{{ floor.id === state.selectedFloorId ? 'active' : '' }}" t-on-click="() => this.selectFloor(floor)">
|
||||
<t t-esc="floor.name" />
|
||||
<t t-set="changeCount" t-value="this.getFloorChangeCount(floor)"/>
|
||||
<span t-if="changeCount > 0" class="badge rounded-pill text-bg-danger ms-2 py-1 smaller fw-bolder" t-esc="changeCount"/>
|
||||
</button>
|
||||
</t>
|
||||
<button t-attf-class="{{editButtonClass}} btn-secondary lh-lg" t-if="pos.isEditMode or pos.config.floor_ids?.length === 0" t-on-click="addFloor" title="Add Floor" >
|
||||
<i class="fa fa-plus fa-fw" role="img" aria-label="Add Floor" />
|
||||
</button>
|
||||
</div>
|
||||
<!-- Right Side Div -->
|
||||
<div t-if="pos.isEditMode" class="edit-buttons d-flex flex-grow-1 justify-content-center justify-content-md-end gap-2 m-2 px-1 px-sm-0 overflow-x-auto">
|
||||
<div class="d-flex gap-1 p-1 rounded-3 bg-200">
|
||||
<t t-if="hasSelectedTable">
|
||||
<span t-if="!ui.isSmall" class="mx-2 align-self-center text-uppercase smaller fw-bolder">Table <t t-esc="firstSelectedTable.table_number" /></span>
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="changeSeatsNum" t-att-disabled="!hasSelectedTable" title="Seats">
|
||||
<i class="fa fa-user fa-fw" role="img" aria-label="Seats" />
|
||||
</button>
|
||||
<t t-if="selectedTables.some((t) => t.shape === 'square')">
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="() => this.changeShape('round')" t-att-disabled="!hasSelectedTable" title="Round Shape">
|
||||
<i class="fa fa-circle-o fa-fw" role="img" aria-label="Make Round" />
|
||||
</button>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="() => this.changeShape('square')" t-att-disabled="!hasSelectedTable" title="Square Shape">
|
||||
<i class="fa fa-square-o fa-fw" role="img" aria-label="Make Square" />
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
<Dropdown menuClass="'pos-dropdown-menu px-1'">
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" title="Change Floor Background">
|
||||
<i class="fa fa-paint-brush fa-fw" role="img" aria-label="Change Floor Background"/>
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<div class="d-grid gap-1 py-2 px-1" style="grid-template-columns: repeat(3, 1fr);">
|
||||
<t t-foreach="Object.entries(getColors())" t-as="color" t-key="color[0]">
|
||||
<t t-set="adaptColor" t-value="!hasSelectedTable ? this.getLighterShade(color[0]) : color[0]" />
|
||||
<t t-set="key" t-value="color[0]"/>
|
||||
<DropdownItem closingMode="'none'" onSelected="() => this.setColor(hasSelectedTable, adaptColor, key)">
|
||||
<button
|
||||
class="p-4 border-1 rounded"
|
||||
t-attf-style="background-color: {{adaptColor}}"
|
||||
/>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<DropdownItem closingMode="'none'">
|
||||
<button class="floor-picture border-1 rounded position-relative text-center overflow-hidden d-flex flex-column align-items-center justify-content-center">
|
||||
<i class="fa fa-camera" role="img" aria-label="Picture" title="Picture"></i>
|
||||
File
|
||||
<input type="file" class="image-uploader" t-on-change="uploadImage" />
|
||||
</button>
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</t>
|
||||
</Dropdown>
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="() => this.rename(hasSelectedTable)" title="Rename">
|
||||
<i class="fa fa-pencil-square-o fa-fw" role="img" aria-label="Rename"/>
|
||||
</button>
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="() => this.duplicate(hasSelectedTable)" title="Clone">
|
||||
<i class="fa fa-copy fa-fw" role="img" aria-label="Clone"/>
|
||||
</button>
|
||||
<button t-attf-class="{{editButtonClass}} btn-light" t-on-click.stop="() => this.delete(hasSelectedTable)" title="Delete">
|
||||
<i class="fa fa-trash fa-fw" role="img" aria-label="Delete"/>
|
||||
</button>
|
||||
</div>
|
||||
<button t-attf-class="btn btn-outline-secondary btn-lg d-flex align-items-center justify-content-center gap-2 lh-lg" t-on-click.stop="doCreateTable.call" t-att-disabled="doCreateTable.status === 'loading'">
|
||||
<t t-if="doCreateTable.status === 'loading'">
|
||||
<i class="fa fa-circle-o-notch fa-spin icon-button" role="img" aria-label="Loading" title="Loading"></i>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-plus-circle" role="img" aria-label="Add Table" title="Add Table"/>
|
||||
<t t-if="!ui.isSmall">Table</t>
|
||||
</t>
|
||||
</button>
|
||||
<button t-attf-class="btn btn-primary btn-lg" t-on-click.stop="closeEditMode">
|
||||
<t t-if="!ui.isSmall">Save</t>
|
||||
<i t-else="" class="fa fa-floppy-o" role="img" aria-label="Save" title="Save"/>
|
||||
</button>
|
||||
</div>
|
||||
<div t-else="" class="right-buttons d-flex gap-1 p-2 pe-0">
|
||||
<NumpadDropdown/>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set="isKanban" t-value="pos.floorPlanStyle == 'kanban'"/>
|
||||
<div
|
||||
t-ref="floor-map-scroll"
|
||||
class="overflow-auto flex-grow-1 flex-shrink-1 flex-basis-0 w-auto"
|
||||
t-att-class="{
|
||||
'floor-grid': pos.isEditMode,
|
||||
'bg-view': !activeFloor?.background_color
|
||||
}"
|
||||
t-attf-style="background-color: rgba({{this._getColors()[activeFloor?.background_color]}},.75)">
|
||||
<div t-on-click="onClickFloorMap" t-on-touchstart="_onPinchStart" t-on-touchmove="_onPinchMove" t-on-touchend="_onPinchEnd"
|
||||
t-attf-class="floor-map position-relative w-100 h-100"
|
||||
t-ref="floor-map-ref"
|
||||
t-attf-style="
|
||||
-webkit-touch-callout: none;
|
||||
height: {{state.floorHeight}} !important;
|
||||
width: {{state.floorWidth}} !important;
|
||||
{{ activeFloor?.floor_background_image and !isKanban ?
|
||||
'background-image: url(' + floorBackround + '); background-size: auto; background-repeat: no-repeat; background-attachment: local;' :
|
||||
''
|
||||
}}">
|
||||
<t t-if="pos.config.floor_ids?.length > 0">
|
||||
<div t-if="!activeTables?.length" class="empty-floor d-flex align-items-center justify-content-center h-100 fs-3 text-center text-muted" t-ref="map">
|
||||
<span>Oops! No tables available.<br/>Add a new table to get started.</span>
|
||||
</div>
|
||||
<div t-else="" t-ref="map" t-att-class="{'floor-kanban d-grid gap-1 p-2': isKanban, 'h-100': !isKanban, 'h-50 h-sm-auto overflow-y-auto': isKanban and this.pos.tableSelectorState}">
|
||||
<t t-foreach="activeTables.sort((a,b)=>a.id-b.id)" t-as="table" t-key="table.id" >
|
||||
<t t-set="isOccupied" t-value="pos.tableHasOrders(table)"/>
|
||||
<t t-set="isIntersecting" t-value="state.potentialLink?.child?.id === table.id"/>
|
||||
<t t-set="isIntersected" t-value="state.potentialLink?.parent?.id === table.id"/>
|
||||
<div
|
||||
t-on-click="(ev) => this.onClickTable(table, ev)"
|
||||
class="floor-table table o_draggable d-flex flex-column align-items-center justify-content-between cursor-pointer"
|
||||
t-att-class="{
|
||||
'position-relative m-0': isKanban,
|
||||
'position-absolute': pos.floorPlanStyle !== 'kanban',
|
||||
'selected': state.selectedTableIds.includes(table.id),
|
||||
'occupied': isOccupied,
|
||||
}"
|
||||
t-attf-class="tableId-{{table.id}}"
|
||||
t-attf-style="
|
||||
border: 3px solid {{table.color || 'var(--tableOccupied-bg-color, #35d374)'}};
|
||||
border-radius: {{table.shape === 'round' ? 1000 : 6}}px;
|
||||
background: {{isOccupied ? table.color || 'var(--tableOccupied-bg-color, #35d374)' : 'var(--table-bg-color, #ffffff65)'}};
|
||||
color: {{isOccupied ? 'var(--tableOccupied-color, #000000)' : 'var(--table-color, #FFFFFF)'}};
|
||||
opacity: {{state.potentialLink ? (isIntersecting or isIntersected ? 1 : 0.25) : 1}};
|
||||
{{isKanban ?
|
||||
`
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
` :
|
||||
`
|
||||
width: ${table.width}px;
|
||||
height: ${table.height}px;
|
||||
top: ${table.getY() + state.floorMapOffset.y}px;
|
||||
left: ${table.getX() + state.floorMapOffset.x}px;
|
||||
`
|
||||
}}
|
||||
"
|
||||
>
|
||||
<t t-set="offset" t-value="getTableHandleOffset(table)"/>
|
||||
<div
|
||||
class="info position-relative w-100 h-100 overflow-hidden"
|
||||
t-att-class="{'opacity-25': table.parent_id}"
|
||||
t-attf-style="border-radius: {{table.shape === 'round' ? 1000 : 3}}px;"
|
||||
>
|
||||
<div t-esc="table.table_number" class="label fw-bolder fs-4 position-absolute top-50 start-50 translate-middle" />
|
||||
</div>
|
||||
<t t-set="data" t-value="getChangeCount(table)"/>
|
||||
<div
|
||||
t-if="data.changes > 0"
|
||||
t-esc="this.env.utils.formatProductQty(data.changes, false)"
|
||||
t-att-class="{
|
||||
'text-bg-danger': data.changes
|
||||
}"
|
||||
class="order-count d-flex align-items-center justify-content-center position-absolute top-0 end-0 rounded-3 smaller fw-bolder z-2 badge"
|
||||
/>
|
||||
<t t-if="state.selectedTableIds.includes(table.id)">
|
||||
<span t-attf-class="tableId-{{table.id}}" class="table-handle position-absolute top left" t-attf-style="top: {{offset}}px; left: {{offset}}px"/>
|
||||
<span t-attf-class="tableId-{{table.id}}" class="table-handle position-absolute top right" t-attf-style="top: {{offset}}px; right: {{offset}}px"/>
|
||||
<span t-attf-class="tableId-{{table.id}}" class="table-handle position-absolute bottom right" t-attf-style="bottom: {{offset}}px; right: {{offset}}px"/>
|
||||
<span t-attf-class="tableId-{{table.id}}" class="table-handle position-absolute bottom left" t-attf-style="bottom: {{offset}}px; left: {{offset}}px"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<div t-else="" class="empty-floor d-flex align-items-center justify-content-center h-100 fs-3 text-center text-muted" t-ref="map">
|
||||
<span>Oops! No floors available.<br/>Add a new floor to get started.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.PaymentScreenValidate" t-inherit="point_of_sale.PaymentScreenValidate" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('validation-button') and hasclass('next')]" position="attributes">
|
||||
<attribute name="t-att-hidden">pos.config.set_tip_after_payment and !currentOrder.isPaid()</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[hasclass('validation-button') and hasclass('next')]/span" position="replace">
|
||||
<t t-if="pos.config.set_tip_after_payment and currentOrder.isPaid()">
|
||||
<span class="back_text">Close Tab</span>
|
||||
</t>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="pos_restaurant.PaymentScreenBack" t-inherit="point_of_sale.PaymentScreenBack" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('button') and hasclass('back')]/span[hasclass('back_text')]" position="replace">
|
||||
<t t-if="pos.config.set_tip_after_payment and currentOrder.isPaid()">
|
||||
<span class="back_text">Keep Open</span>
|
||||
</t>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="pos_restaurant.PaymentScreenDue" t-inherit="point_of_sale.PaymentScreenDue" t-inherit-mode="extension">
|
||||
<xpath expr="//section[hasclass('paymentlines-container')]" position="inside">
|
||||
<div t-if="currentOrder.getCustomerCount() > 1" t-attf-class="message text-center {{ui.isSmall ? 'fs-2' : 'fs-1'}}" >
|
||||
<PriceFormatter price="this.env.utils.formatCurrency(currentOrder.amountPerGuest())" /> / Guest
|
||||
<button class="btn rounded-lg p" t-on-click="clickTableGuests" style="background-color: #ced4da;">
|
||||
<span class="text-black fs-4 fw-bolder" t-esc="currentOrder?.getCustomerCount() || 0"/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { PaymentScreenPaymentLines } from "@point_of_sale/app/screens/payment_screen/payment_lines/payment_lines";
|
||||
|
||||
patch(PaymentScreenPaymentLines.prototype, {
|
||||
async sendPaymentAdjust(line) {
|
||||
const prevAmount = line.get_amount();
|
||||
const amountDiff =
|
||||
line.pos_order_id.get_total_with_tax() - line.pos_order_id.get_total_paid();
|
||||
const newAmount = prevAmount + amountDiff;
|
||||
|
||||
line.set_amount(newAmount);
|
||||
line.set_payment_status("waiting");
|
||||
|
||||
const isAdjustSuccessful =
|
||||
await line.payment_method_id.payment_terminal?.send_payment_adjust(line.uuid);
|
||||
if (!isAdjustSuccessful) {
|
||||
line.set_amount(prevAmount);
|
||||
}
|
||||
|
||||
line.set_payment_status("done");
|
||||
},
|
||||
});
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="PaymentScreenPaymentLines" t-inherit="point_of_sale.PaymentScreenPaymentLines" t-inherit-mode="extension" owl="1">
|
||||
<t t-name="pos_restaurant.PaymentScreenPaymentLines" t-inherit="point_of_sale.PaymentScreenPaymentLines" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('send_payment_reversal')]/.." position="replace">
|
||||
<t t-if="line.canBeAdjusted() && line.order.get_total_paid() < line.order.get_total_with_tax()">
|
||||
<div class="button send_adjust_amount" title="Adjust Amount" t-on-click="() => this.trigger('send-payment-adjust', line)">
|
||||
<t t-if="line.canBeAdjusted() && line.pos_order_id.amountPaid < line.pos_order_id.priceIncl">
|
||||
<div class="button send_adjust_amount" title="Adjust Amount" t-on-click="() => this.sendPaymentAdjust(line)">
|
||||
Adjust Amount
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="line.can_be_reversed">
|
||||
<div class="button send_payment_reversal" title="Reverse Payment" t-on-click="() => this.trigger('send-payment-reverse', line)">
|
||||
<div class="button send_payment_reversal" title="Reverse Payment" t-on-click="() => this.props.sendPaymentReverse(line)">
|
||||
Reverse
|
||||
</div>
|
||||
</t>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useTrackedAsync } from "@point_of_sale/app/hooks/hooks";
|
||||
|
||||
/**
|
||||
* @props partner
|
||||
*/
|
||||
patch(ActionpadWidget, {
|
||||
props: {
|
||||
...ActionpadWidget.props,
|
||||
setTable: { type: Function, optional: true },
|
||||
assignOrder: { type: Function, optional: true },
|
||||
},
|
||||
});
|
||||
|
||||
patch(ActionpadWidget.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.doSubmitOrder = useTrackedAsync(() => this.pos.submitOrder());
|
||||
this.doReprintOrder = useTrackedAsync(() => this.pos.reprintOrder());
|
||||
},
|
||||
get swapButton() {
|
||||
return (
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
this.pos.router.state.current !== "TicketScreen"
|
||||
);
|
||||
},
|
||||
get hasChangesToPrint() {
|
||||
return Boolean(this.displayCategoryCount.length);
|
||||
},
|
||||
hasQuantity(order) {
|
||||
if (!order) {
|
||||
return false;
|
||||
} else {
|
||||
return order.lines.reduce((totalQty, line) => totalQty + line.getQuantity(), 0) > 0;
|
||||
}
|
||||
},
|
||||
get highlightPay() {
|
||||
return (
|
||||
this.currentOrder?.lines?.length &&
|
||||
!this.hasChangesToPrint &&
|
||||
this.hasQuantity(this.currentOrder) &&
|
||||
!this.getCourseToFire()
|
||||
);
|
||||
},
|
||||
get displayCategoryCount() {
|
||||
return this.pos.categoryCount.slice(0, 4);
|
||||
},
|
||||
get isCategoryCountOverflow() {
|
||||
if (this.pos.categoryCount.length > 4) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
get displayFireCourseBtn() {
|
||||
const order = this.currentOrder;
|
||||
if (order.isDirectSale || !order.hasCourses()) {
|
||||
return false;
|
||||
}
|
||||
return this.getCourseToFire() != null;
|
||||
},
|
||||
get fireCourseBtnText() {
|
||||
const selectedCourse = this.getCourseToFire();
|
||||
if (selectedCourse) {
|
||||
return _t("Fire %s", selectedCourse.name);
|
||||
}
|
||||
return "";
|
||||
},
|
||||
getCourseToFire() {
|
||||
const course = this.currentOrder.getSelectedCourse();
|
||||
if (course?.isReadyToFire()) {
|
||||
return course;
|
||||
}
|
||||
},
|
||||
async clickFireCourse() {
|
||||
const course = this.getCourseToFire();
|
||||
if (!course) {
|
||||
return;
|
||||
}
|
||||
this.currentOrder.cleanCourses(); //remove empty course on fire course.
|
||||
await this.pos.fireCourse(course);
|
||||
this.pos.showDefault();
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.o_catch_attention_vibrate {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: catchAttentionVibrate 400ms ease 0s infinite normal;
|
||||
}
|
||||
|
||||
// bounce effect
|
||||
@keyframes catchAttentionVibrate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
60% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
80% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.product-category-label {
|
||||
display: -webkit-box !important;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.ActionpadWidget" t-inherit="point_of_sale.ActionpadWidget" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('actionpad')]//button[hasclass('mobile-more-button')]" position="before">
|
||||
<button t-if="pos.config.module_pos_restaurant and !this.currentOrder.isRefund"
|
||||
class="btn btn-secondary btn-lg flex-shrink-0 border-0" t-on-click="()=>this.pos.addCourse()">
|
||||
<span>Course</span>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('validation')]" position="attributes">
|
||||
<!-- Check availability for all children, needed due to condition based pay button. Skip rendering if none are available. -->
|
||||
<attribute name="t-if">!this.swapButton or showFastPaymentMethods or (pos.showBackButton() and !pos.config.module_pos_restaurant)</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('validation')]//button[hasclass('pay-order-button')]" position="attributes">
|
||||
<attribute name="t-if">!this.swapButton</attribute>
|
||||
</xpath>
|
||||
<!-- Replace the payment button by the order button -->
|
||||
<xpath expr="//div[hasclass('validation')]" position="before">
|
||||
<div t-if="this.swapButton and currentOrder" class="d-flex gap-2 flex-fill">
|
||||
<BackButton t-if="pos.showBackButton() and pos.config.module_pos_restaurant" class="{'col-3': showFastPaymentMethods}" onClick="() => pos.onClickBackButton()"/>
|
||||
<button
|
||||
class="submit-order h-100 button btn btn-lg d-flex align-items-center w-50 flex-fill position-relative px-3 highlight btn-primary justify-content-between"
|
||||
t-on-click="() => doSubmitOrder.call()"
|
||||
t-if="!this.currentOrder.isDirectSale and this.displayCategoryCount.length"
|
||||
t-att-disabled="doSubmitOrder.status === 'loading'"
|
||||
>
|
||||
<div t-if="!(ui.isSmall or displayCategoryCount.length > 2) or (!displayCategoryCount.length and ui.isSmall)" class="text-truncate text-start">Send</div>
|
||||
<div t-attf-class="{{ !(displayCategoryCount.length > 2) ? 'd-flex flex-column align-items-end gap-1' : 'row row-cols-2 g-1 gx-2' }} {{ isCategoryCountOverflow ? 'mt-n3' : ''}}">
|
||||
<t t-if="displayCategoryCount.length">
|
||||
<t t-foreach="displayCategoryCount" t-as="categoryCountLine" t-key="categoryCountLine_index">
|
||||
<div class="d-flex align-items-center justify-content-between small" t-att-class="{ 'gap-2' : !(displayCategoryCount.length > 2) }">
|
||||
<div class="flex-grow-1"/>
|
||||
<label class="product-category-label px-2"><t t-esc="categoryCountLine.name"/></label>
|
||||
<label class="rounded px-2 py-0" style="background-color:rgba(0, 0, 0, 0.3);"><t t-esc="this.env.utils.formatProductQty(categoryCountLine.count, false)"/></label>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="isCategoryCountOverflow">
|
||||
<div class="position-absolute bottom-0 start-50 translate-middle-x">...</div>
|
||||
</t>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
class="fire-btn button btn btn-primary btn-lg d-flex flex-row align-items-center justify-content-center w-50"
|
||||
t-on-click="() => this.clickFireCourse()"
|
||||
t-elif="this.displayFireCourseBtn"
|
||||
>
|
||||
<span t-esc="this.fireCourseBtnText"></span>
|
||||
</button>
|
||||
<button
|
||||
class="button btn btn-secondary btn-lg d-flex flex-row align-items-center justify-content-center"
|
||||
t-on-click="() => doReprintOrder.call()"
|
||||
t-elif="this.pos.unwatched.printers.length and this.currentOrder.uiState.lastPrints.length"
|
||||
t-att-disabled="doReprintOrder.status === 'loading'">
|
||||
<i class="fa fa-cutlery" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
class="button btn btn-secondary btn-lg d-flex flex-row align-items-center justify-content-center w-50"
|
||||
t-on-click="() => this.pos.showDefault()"
|
||||
t-elif="!this.currentOrder.isDirectSale and !this.displayCategoryCount.length and !this.currentOrder.isEmpty()"
|
||||
>
|
||||
<span>New</span>
|
||||
</button>
|
||||
<t t-elif="this.currentOrder.isDirectSale and this.pos.numpadMode != 'table'">
|
||||
<button
|
||||
class="set-table h-100 button btn btn-lg d-flex align-items-center flex-fill position-relative px-3 btn-primary justify-content-center"
|
||||
t-att-class="{'o_catch_attention_vibrate': this.pos.shouldSetTable}"
|
||||
t-on-click="() => this.props.setTable()"
|
||||
>
|
||||
Set Table
|
||||
</button>
|
||||
<button
|
||||
class="new-tab h-100 button btn btn-lg d-flex align-items-center flex-fill position-relative px-3 btn-primary justify-content-center"
|
||||
t-on-click="() => this.pos.editFloatingOrderName(this.currentOrder)"
|
||||
>
|
||||
Set Tab
|
||||
</button>
|
||||
</t>
|
||||
<t t-elif="this.currentOrder.isDirectSale and this.pos.numpadMode == 'table'">
|
||||
<BackButton onClick="() => this.pos.numpadMode = 'quantity'"/>
|
||||
<button
|
||||
class="h-100 assign-button button btn-primary btn btn-lg d-flex flex-row align-items-center justify-content-center w-50"
|
||||
t-on-click="() => this.props.assignOrder()"
|
||||
>
|
||||
<span>Assign</span>
|
||||
</button>
|
||||
<button
|
||||
class="h-100 plan-button button btn btn-secondary btn-lg d-flex flex-row align-items-center justify-content-center w-50"
|
||||
t-on-click="() => this.pos.navigate('FloorScreen')"
|
||||
>
|
||||
<span>Plan</span>
|
||||
</button>
|
||||
</t>
|
||||
<button t-if="!currentOrder.isEmpty() and this.pos.numpadMode != 'table'"
|
||||
t-on-click="() => pos.pay()"
|
||||
class="button pay-order-button pay-order-button-restaurant btn btn-lg w-50"
|
||||
t-attf-class="{{ this.highlightPay ? 'highlight btn-primary' : 'btn-secondary' }}"
|
||||
>
|
||||
Payment
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
|
||||
import { SelectPartnerButton } from "@point_of_sale/app/screens/product_screen/control_buttons/select_partner_button/select_partner_button";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useAsyncLockedMethod } from "@point_of_sale/app/hooks/hooks";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ControlButtons.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.alert = useService("alert");
|
||||
this.printer = useService("printer");
|
||||
this.clickPrintBill = useAsyncLockedMethod(this.clickPrintBill);
|
||||
},
|
||||
async clickPrintBill() {
|
||||
// Need to await to have the result in case of automatic skip screen.
|
||||
await this.pos.printReceipt({
|
||||
printBillActionTriggered: true,
|
||||
});
|
||||
},
|
||||
clickTableGuests() {
|
||||
this.pos.setCustomerCount();
|
||||
},
|
||||
clickTransferOrder() {
|
||||
this.dialog.closeAll();
|
||||
this.pos.startTransferOrder();
|
||||
},
|
||||
showTransferCourse() {
|
||||
const order = this.currentOrder;
|
||||
if (!order || !order.hasCourses()) {
|
||||
return false;
|
||||
}
|
||||
return order.getSelectedCourse() || order.getSelectedOrderline();
|
||||
},
|
||||
openSplitPage() {
|
||||
this.pos.navigate("SplitBillScreen", {
|
||||
orderUuid: this.currentOrder.uuid,
|
||||
});
|
||||
},
|
||||
async clickTransferCourse() {
|
||||
this.dialog.closeAll();
|
||||
await this.pos.transferLinesToCourse();
|
||||
},
|
||||
});
|
||||
patch(ControlButtons, {
|
||||
components: {
|
||||
...ControlButtons.components,
|
||||
SelectPartnerButton,
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.ControlButtons" t-inherit="point_of_sale.ControlButtons" t-inherit-mode="extension">
|
||||
<xpath
|
||||
expr="//button[hasclass('more-btn')]"
|
||||
position="before">
|
||||
<button class="course-btn" t-if="pos.config.module_pos_restaurant and !props.showRemainingButtons and !pos.getOrder()?.isRefund"
|
||||
t-att-class="buttonClass" t-on-click="()=>this.pos.addCourse()">
|
||||
<span>Course</span>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath
|
||||
expr="//t[@t-if='props.showRemainingButtons']/div/button[hasclass('o_pricelist_button')]"
|
||||
position="before">
|
||||
<!-- All these buttons will only be displayed in a dialog -->
|
||||
<t t-if="pos.config.module_pos_restaurant">
|
||||
<button t-if="pos.config.iface_printbill" t-att-class="buttonClass"
|
||||
t-att-disabled="!pos.getOrder()?.getOrderlines()?.length"
|
||||
t-on-click="clickPrintBill">
|
||||
<i class="fa fa-print me-1"></i>Bill
|
||||
</button>
|
||||
<button t-att-class="buttonClass" t-on-click="clickTableGuests">
|
||||
<span t-esc="currentOrder?.getCustomerCount() || 0" t-attf-class="rounded-circle text-bg-dark fw-bolder small me-1 {{ui.isSmall ? 'px-1' : 'px-2 py-1'}}"/>
|
||||
<span>Guests</span>
|
||||
</button>
|
||||
</t>
|
||||
<t t-if="pos.config.module_pos_restaurant">
|
||||
<button t-att-class="buttonClass"
|
||||
t-att-disabled="pos.getOrder()?.getOrderlines()?.filter((line) => line.isGlobalDiscountApplicable()).reduce((sum, line) => sum + line.qty, 0) lt 2"
|
||||
t-on-click="openSplitPage">
|
||||
<i class="fa fa-files-o me-1"/>Split
|
||||
</button>
|
||||
<button t-att-class="buttonClass" t-on-click.stop="() => this.clickTransferOrder()">
|
||||
<i class="oi oi-arrow-right me-1" />Transfer / Merge
|
||||
</button>
|
||||
<button t-att-disabled="!this.showTransferCourse()" t-att-class="buttonClass" t-on-click.stop="() => this.clickTransferCourse()">
|
||||
<i class="oi oi-arrow-down me-1" />Transfer course
|
||||
</button>
|
||||
<button t-if="!pos.getOrder()?.table_id" t-att-class="buttonClass" t-on-click="() => this.pos.editFloatingOrderName(this.pos.getOrder())">
|
||||
<i class="fa fa-pencil-square-o me-1" />Edit Order Name
|
||||
</button>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { OrderSummary } from "@point_of_sale/app/screens/product_screen/order_summary/order_summary";
|
||||
|
||||
patch(OrderSummary.prototype, {
|
||||
bookTable() {
|
||||
this.pos.getOrder().setBooked(true);
|
||||
this.pos.navigate("FloorScreen");
|
||||
},
|
||||
showBookButton() {
|
||||
if (!this.pos.selectedTable) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
!this.pos.models["pos.order"].some(
|
||||
(o) =>
|
||||
o.table_id?.id === this.pos.selectedTable.id &&
|
||||
o.finalized === false &&
|
||||
o.isBooked
|
||||
)
|
||||
);
|
||||
},
|
||||
async onOrderlineLongPress(ev, orderline) {
|
||||
const result = await super.onOrderlineLongPress(ev, orderline);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const child of orderline.combo_line_ids || []) {
|
||||
child.course_id = orderline.course_id;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
async unbookTable() {
|
||||
this.env.services.ui.block();
|
||||
try {
|
||||
const order = this.pos.getOrder();
|
||||
await this.pos.deleteOrders([order]);
|
||||
this.pos.showDefault();
|
||||
} finally {
|
||||
this.env.services.ui.unblock();
|
||||
}
|
||||
},
|
||||
showUnbookButton() {
|
||||
if (this.pos.selectedTable) {
|
||||
return (
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
!this.pos.models["pos.order"].some(
|
||||
(o) =>
|
||||
o.table_id?.id === this.pos.selectedTable.id &&
|
||||
o.finalized === false &&
|
||||
!o.isBooked
|
||||
) &&
|
||||
this.pos.getOrder().lines.length === 0 &&
|
||||
!this.pos.getOrder().hasCourses()
|
||||
);
|
||||
}
|
||||
const currentOrder = this.pos.getOrder();
|
||||
if (!currentOrder) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
currentOrder &&
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
!currentOrder.finalized &&
|
||||
currentOrder.isBooked &&
|
||||
currentOrder.isEmpty() &&
|
||||
!currentOrder.hasCourses()
|
||||
);
|
||||
},
|
||||
async updateSelectedOrderline({ buffer, key }) {
|
||||
await super.updateSelectedOrderline(...arguments);
|
||||
|
||||
if (this.pos.getOrder() && this.pos.config.module_pos_restaurant) {
|
||||
this.pos.addPendingOrder([this.pos.getOrder().id]);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.h-unset {
|
||||
height: unset !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderSummary" t-inherit="point_of_sale.OrderSummary" t-inherit-mode="extension">
|
||||
<xpath expr="//OrderDisplay/t[@t-set-slot='details']" position="inside">
|
||||
<div class="d-flex flex-column align-items-center gap-2">
|
||||
<button t-if="showBookButton()" class="btn btn-primary btn-lg py-2 book-table" style="border:none; font-size: 20px;" t-on-click="bookTable">Book table</button>
|
||||
<button t-if="showUnbookButton()" class="btn btn-primary btn-lg py-2 unbook-table" style="border:none; font-size: 20px;" t-on-click="unbookTable">
|
||||
<t t-if="pos.selectedTable">Release table</t>
|
||||
<t t-else="">Release Order</t>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { onWillDestroy } from "@odoo/owl";
|
||||
import { SWITCHSIGN, DECIMAL } from "@point_of_sale/app/components/numpad/numpad";
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { useBus } from "@web/core/utils/hooks";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useTrackedAsync } from "@point_of_sale/app/hooks/hooks";
|
||||
|
||||
patch(ProductScreen.prototype, {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.state.tableBuffer = "";
|
||||
this.state.isValidBuffer = true;
|
||||
this.doSubmitOrder = useTrackedAsync(() => this.pos.submitOrder());
|
||||
this.doReprintOrder = useTrackedAsync(() => this.pos.reprintOrder());
|
||||
useBus(this.numberBuffer, "buffer-update", ({ detail: value }) => {
|
||||
this.checkIsValid(value);
|
||||
});
|
||||
|
||||
onWillDestroy(() => {
|
||||
this.pos.numpadMode = "quantity";
|
||||
});
|
||||
},
|
||||
get nbrOfChanges() {
|
||||
return this.pos.getOrderChanges().nbrOfChanges;
|
||||
},
|
||||
get swapButton() {
|
||||
return this.pos.config.module_pos_restaurant && this.pos.config.preparationCategories.size;
|
||||
},
|
||||
get displayCategoryCount() {
|
||||
return this.pos.categoryCount.slice(0, 3);
|
||||
},
|
||||
get primaryReviewButton() {
|
||||
return (
|
||||
this.pos.config.module_pos_restaurant &&
|
||||
((!this.pos.getOrder().isEmpty() && !this.primaryOrderButton) ||
|
||||
this.pos.getOrder().isDirectSale)
|
||||
);
|
||||
},
|
||||
get primaryOrderButton() {
|
||||
return (
|
||||
this.pos.getOrderChanges().nbrOfChanges !== 0 && this.pos.config.module_pos_restaurant
|
||||
);
|
||||
},
|
||||
getNumpadButtons() {
|
||||
let buttons = super.getNumpadButtons();
|
||||
if (this.pos.numpadMode === "table") {
|
||||
const toDisable = ["quantity", "discount", "price", SWITCHSIGN.value, DECIMAL.value];
|
||||
buttons = buttons.map((button) => ({
|
||||
...button,
|
||||
class: `
|
||||
${button.class}
|
||||
${toDisable.includes(button.value) ? "disabled" : ""}
|
||||
`,
|
||||
}));
|
||||
}
|
||||
return buttons;
|
||||
},
|
||||
onNumpadClick(buttonValue) {
|
||||
super.onNumpadClick(buttonValue);
|
||||
},
|
||||
setTable() {
|
||||
this.pos.numpadMode = "table";
|
||||
this.numberBuffer.reset();
|
||||
},
|
||||
assignOrder() {
|
||||
if (this.state.isValidBuffer) {
|
||||
this.pos.searchOrder(this.state.tableBuffer);
|
||||
this.numberBuffer.reset();
|
||||
this.pos.numpadMode = "quantity";
|
||||
}
|
||||
},
|
||||
checkIsValid(buffer) {
|
||||
this.state.tableBuffer = buffer;
|
||||
const res = this.pos.findTable(buffer);
|
||||
this.state.isValidBuffer = Boolean(res);
|
||||
},
|
||||
});
|
||||