19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:29:53 +01:00
parent 6e54c1af6c
commit 3ca647e428
1087 changed files with 132065 additions and 108499 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -1,188 +0,0 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, 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);

View file

@ -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 ", "");
},
});

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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%;
}
}

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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>

View file

@ -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 };

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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()));
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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"];
},
});

View file

@ -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;
},
});

View file

@ -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
);
},
});

View file

@ -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;
},
});

View file

@ -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"
);
},
});

View file

@ -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);

View file

@ -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);

View file

@ -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();
}
},
});

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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");
},
});

View file

@ -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() &amp;&amp; line.order.get_total_paid() &lt; 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() &amp;&amp; line.pos_order_id.amountPaid &lt; 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>

View file

@ -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();
},
});

View file

@ -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;
}

View file

@ -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>

View file

@ -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,
},
});

View file

@ -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>

View file

@ -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]);
}
},
});

View file

@ -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>

View file

@ -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);
},
});

Some files were not shown because too many files have changed in this diff Show more