[IMP] pos_restaurant: online table booking
Adds the possibility to book a table online from the website. The booking is then visible in the POS. When a new table is created in the PoS, a linked resource is created in the appointment application, which allows the user to add these resources to appointment_types, authorising visitors to reserve tables online. In pos_config, you need to choose an appointment_type via which the appointments will be retrieved A websocket has been integrated to retrieve new bookings instantly from the PoS, which are displayed directly on the tables. closes odoo/odoo#124850 Related: odoo/enterprise#42489 Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
committed by
Joseph Caburnay
parent
c6ff1034a7
commit
b4339b49a0
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import secrets
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from itertools import groupby
|
||||
@@ -33,6 +33,7 @@ class PosSession(models.Model):
|
||||
required=True,
|
||||
index=True)
|
||||
name = fields.Char(string='Session ID', required=True, readonly=True, default='/')
|
||||
access_token = fields.Char('Security Token', copy=False)
|
||||
user_id = fields.Many2one(
|
||||
'res.users', string='Opened By',
|
||||
required=True,
|
||||
@@ -1757,13 +1758,24 @@ class PosSession(models.Model):
|
||||
|
||||
return taxes
|
||||
|
||||
def _ensure_access_token(self):
|
||||
# Code taken from addons/portal/models/portal_mixin.py
|
||||
if not self.access_token:
|
||||
# we use a `write` to force the cache clearing otherwise `return self.access_token` will return False
|
||||
self.sudo().write({'access_token': secrets.token_hex(16)})
|
||||
return self.access_token
|
||||
|
||||
def _get_bus_channel_name(self):
|
||||
return f'pos_session-{self.id}-{self._ensure_access_token()}'
|
||||
|
||||
def _loader_params_pos_session(self):
|
||||
self._ensure_access_token()
|
||||
return {
|
||||
'search_params': {
|
||||
'domain': [('id', '=', self.id)],
|
||||
'fields': [
|
||||
'id', 'name', 'user_id', 'config_id', 'start_at', 'stop_at', 'sequence_number',
|
||||
'payment_method_ids', 'state', 'update_stock_at_closing', 'cash_register_balance_start'
|
||||
'payment_method_ids', 'state', 'update_stock_at_closing', 'cash_register_balance_start', 'access_token'
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ export class PosBus {
|
||||
this.pos = pos;
|
||||
this.orm = orm;
|
||||
|
||||
// initialize bus_service and listen on pos_config-`id` channel
|
||||
bus_service.addChannel(`pos_config-${pos.config.id}`);
|
||||
bus_service.addChannel(`pos_session-${pos.pos_session.id}-${pos.pos_session.access_token}`);
|
||||
bus_service.addEventListener("notification", ({ detail }) => {
|
||||
for (const message of detail) {
|
||||
this.dispatch(message);
|
||||
|
||||
@@ -15,6 +15,7 @@ class PosConfig(models.Model):
|
||||
floor_ids = fields.Many2many('restaurant.floor', string='Restaurant Floors', help='The restaurant floors served by this point of sale.')
|
||||
set_tip_after_payment = fields.Boolean('Set Tip After Payment', help="Adjust the amount authorized by payment terminals to add a tip after the customers left or at the end of the day.")
|
||||
module_pos_restaurant = fields.Boolean(default=True)
|
||||
module_pos_restaurant_appointment = fields.Boolean("Table Booking")
|
||||
|
||||
def get_tables_order_count_and_printing_changes(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -71,8 +71,11 @@ class PosOrder(models.Model):
|
||||
def send_table_count_notification(self, table_ids):
|
||||
messages = []
|
||||
for config in self.env['pos.config'].search([('floor_ids', 'in', table_ids.floor_id.ids)]):
|
||||
order_count = config.get_tables_order_count_and_printing_changes()
|
||||
messages.append((f'pos_config-{config.id}', 'TABLE_ORDER_COUNT', order_count))
|
||||
config_cur_session = config.current_session_id
|
||||
|
||||
if config_cur_session:
|
||||
order_count = config.get_tables_order_count_and_printing_changes()
|
||||
messages.append((config_cur_session._get_bus_channel_name(), 'TABLE_ORDER_COUNT', order_count))
|
||||
self.env['bus.bus']._sendmany(messages)
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -85,31 +85,13 @@ class RestaurantTable(models.Model):
|
||||
width = fields.Float('Width', default=50, help="The table's width in pixels")
|
||||
height = fields.Float('Height', default=50, help="The table's height in pixels")
|
||||
seats = fields.Integer('Seats', default=1, help="The default number of customer served at this table.")
|
||||
color = fields.Char('Color', help="The table's color, expressed as a valid 'background' CSS property value")
|
||||
color = fields.Char('Color', help="The table's color, expressed as a valid 'background' CSS property value", default="#35D374")
|
||||
active = fields.Boolean('Active', default=True, help='If false, the table is deactivated and will not be available in the point of sale')
|
||||
|
||||
def are_orders_still_in_draft(self):
|
||||
draft_orders = self.env['pos.order'].search([('table_id', '=', self.id), ('state', '=', 'draft')])
|
||||
return len(draft_orders) > 0
|
||||
|
||||
@api.model
|
||||
def create_from_ui(self, table):
|
||||
""" create or modify a table from the point of sale UI.
|
||||
table contains the table's fields. If it contains an
|
||||
id, it will modify the existing table. It then
|
||||
returns the id of the table.
|
||||
"""
|
||||
if table.get('floor_id'):
|
||||
table['floor_id'] = table['floor_id'][0]
|
||||
|
||||
sanitized_table = dict([(key, val) for key, val in table.items() if key in self._fields and val is not None])
|
||||
table_id = sanitized_table.pop('id', False)
|
||||
if table_id:
|
||||
self.browse(table_id).write(sanitized_table)
|
||||
else:
|
||||
table_id = self.create(sanitized_table).id
|
||||
return table_id
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_active_pos_session(self):
|
||||
confs = self.mapped('floor_id.pos_config_ids').filtered(lambda c: c.module_pos_restaurant)
|
||||
|
||||
@@ -11,6 +11,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
pos_iface_printbill = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
|
||||
pos_iface_splitbill = fields.Boolean(compute='_compute_pos_module_pos_restaurant', store=True, readonly=False)
|
||||
pos_set_tip_after_payment = fields.Boolean(compute='_compute_pos_set_tip_after_payment', store=True, readonly=False)
|
||||
pos_module_pos_restaurant_appointment = fields.Boolean(related="pos_config_id.module_pos_restaurant_appointment", readonly=False)
|
||||
|
||||
@api.depends('pos_module_pos_restaurant', 'pos_config_id')
|
||||
def _compute_pos_module_pos_restaurant(self):
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
<span class="label drag-handle">
|
||||
<t t-esc="props.table.name" />
|
||||
</span>
|
||||
<span class="table-seats">
|
||||
<t t-esc="props.table.seats" />
|
||||
</span>
|
||||
<t t-if="pos.floorPlanStyle != 'kanban'" t-foreach="handles" t-as="handle" t-key="handle">
|
||||
<span class="table-handle" t-ref="{{handle}}" t-att-style="computeHandleStyle(handle)"/>
|
||||
</t>
|
||||
|
||||
@@ -44,7 +44,8 @@ export class FloorScreen extends Component {
|
||||
isColorPicker: false,
|
||||
});
|
||||
const ui = useState(useService("ui"));
|
||||
this.pos.floorPlanStyle = ui.isSmall ? "kanban" : "default";
|
||||
const mode = localStorage.getItem("floorPlanStyle");
|
||||
this.pos.floorPlanStyle = ui.isSmall || mode == "kanban" ? "kanban" : "default";
|
||||
this.floorMapRef = useRef("floor-map-ref");
|
||||
this.addFloorRef = useRef("add-floor-ref");
|
||||
this.map = useRef("map");
|
||||
@@ -268,11 +269,26 @@ export class FloorScreen extends Component {
|
||||
return firstNum.toString();
|
||||
}
|
||||
async _save(table) {
|
||||
const tableCopy = { ...table };
|
||||
delete tableCopy.floor;
|
||||
const tableId = await this.orm.call("restaurant.table", "create_from_ui", [tableCopy]);
|
||||
table.id = tableId;
|
||||
this.pos.tables_by_id[tableId] = table;
|
||||
const tableCopy = {
|
||||
floor_id: table.floor.id,
|
||||
color: table.color,
|
||||
height: table.height,
|
||||
name: table.name,
|
||||
position_h: table.position_h,
|
||||
position_v: table.position_v,
|
||||
seats: table.seats,
|
||||
shape: table.shape,
|
||||
width: table.width,
|
||||
};
|
||||
|
||||
if (table.id) {
|
||||
await this.orm.write("restaurant.table", [table.id], tableCopy);
|
||||
} else {
|
||||
const tableId = await this.orm.create("restaurant.table", [tableCopy]);
|
||||
|
||||
table.id = tableId[0];
|
||||
this.pos.tables_by_id[tableId] = table;
|
||||
}
|
||||
}
|
||||
async _renameFloor(floorId, newName) {
|
||||
await this.orm.call("restaurant.floor", "rename_floor", [floorId, newName]);
|
||||
@@ -359,7 +375,9 @@ export class FloorScreen extends Component {
|
||||
}
|
||||
}
|
||||
async onSaveTable(table) {
|
||||
await this._save(table);
|
||||
if (this.pos.tables_by_id[table.id]) {
|
||||
await this._save(table);
|
||||
}
|
||||
}
|
||||
async addFloor() {
|
||||
const { confirmed, payload: newName } = await this.popup.add(TextInputPopup, {
|
||||
@@ -381,6 +399,9 @@ export class FloorScreen extends Component {
|
||||
}
|
||||
async createTable() {
|
||||
const newTable = await this._createTableHelper();
|
||||
newTable.skip_changes = 0;
|
||||
newTable.changes_count = 0;
|
||||
newTable.order_count = 0;
|
||||
if (newTable) {
|
||||
this.state.selectedTableIds = [];
|
||||
this.state.selectedTableIds.push(newTable.id);
|
||||
@@ -564,9 +585,7 @@ export class FloorScreen extends Component {
|
||||
}
|
||||
}
|
||||
this.pos.tables_by_id[id].active = false;
|
||||
await this.orm.call("restaurant.table", "create_from_ui", [
|
||||
{ active: false, id: id },
|
||||
]);
|
||||
this.orm.write("restaurant.table", [id], { active: false });
|
||||
this.activeFloor.tables = this.activeTables.filter((table) => table.id !== id);
|
||||
delete this.pos.tables_by_id[id];
|
||||
} else {
|
||||
|
||||
@@ -150,40 +150,43 @@
|
||||
|
||||
// Should probably be moved to table.scss but currently the selectors are too generic
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
background: rgb(53, 211, 116);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 3px rgba(0,0,0,0.07);
|
||||
transition: background-color 300ms ease-in-out;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.selected {
|
||||
outline: solid rgba(255,255,255,0.3);
|
||||
cursor: move;
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-seats {
|
||||
margin-left: -12px;
|
||||
}
|
||||
.infos {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.order-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
background: black;
|
||||
width: 20px;
|
||||
margin-top: 1px;
|
||||
margin-left: -10px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
border-radius: 999px;
|
||||
z-index: 10;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
padding: 3px;
|
||||
|
||||
&.notify-printing {
|
||||
background: red;
|
||||
@@ -195,37 +198,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-cover {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
border-radius: 0px 0px 3px 3px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
|
||||
&.full {
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-seats {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
margin-left: -16px;
|
||||
margin-bottom: 4px;
|
||||
background: black;
|
||||
background: rgba(0,0,0,0.2);
|
||||
color: white;
|
||||
opacity: 0.2;
|
||||
z-index: 3;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.table-seats-presence {
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
padding: 0 10px;
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.cover {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
background: black;
|
||||
opacity: 0.4;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.table-handle {
|
||||
@@ -253,7 +251,6 @@
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
bottom: 5px;
|
||||
}
|
||||
|
||||
.edit-button.editing {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
import { usePos } from "@point_of_sale/app/store/pos_hook";
|
||||
|
||||
export class Table extends Component {
|
||||
@@ -25,26 +25,64 @@ export class Table extends Component {
|
||||
|
||||
setup() {
|
||||
this.pos = usePos();
|
||||
this.state = useState({
|
||||
containerHeight: 0,
|
||||
containerWidth: 0,
|
||||
});
|
||||
}
|
||||
get fontSize() {
|
||||
const size = this.state.containerHeight / 3;
|
||||
return size > 20 ? 20 : size;
|
||||
}
|
||||
get badgeStyle() {
|
||||
if (this.props.table.shape !== "round") {
|
||||
return `top: -6px; right: -6px;`;
|
||||
}
|
||||
|
||||
const tableHeight = this.state.containerHeight;
|
||||
const tableWidth = this.state.containerWidth;
|
||||
const radius = Math.min(tableWidth, tableHeight) / 2;
|
||||
|
||||
let left = 0;
|
||||
let bottom = 0;
|
||||
|
||||
if (tableHeight > tableWidth) {
|
||||
left = radius;
|
||||
bottom = radius + (tableHeight - tableWidth);
|
||||
} else {
|
||||
bottom = radius;
|
||||
left = radius + (tableWidth - tableHeight);
|
||||
}
|
||||
|
||||
bottom += 0.7 * radius - 8;
|
||||
left += 0.7 * radius - 8;
|
||||
|
||||
return `bottom: ${bottom}px; left: ${left}px;`;
|
||||
}
|
||||
get style() {
|
||||
const table = this.props.table;
|
||||
|
||||
let style = "";
|
||||
let background = table.color ? table.color : "rgb(53, 211, 116)";
|
||||
let textColor = "white";
|
||||
let border = "auto";
|
||||
let boxShadow = "0px 3px rgba(0,0,0,0.07)";
|
||||
|
||||
if (!this.isOccupied()) {
|
||||
background = "transparent";
|
||||
background = "#00000020";
|
||||
const rgb = table.floor.background_color
|
||||
.substring(4, table.floor.background_color.length - 1)
|
||||
.replace(/ /g, "")
|
||||
.split(",");
|
||||
textColor =
|
||||
(0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255 > 0.5 ? "black" : "white";
|
||||
border = "3px solid " + table.color;
|
||||
boxShadow = "none";
|
||||
}
|
||||
|
||||
style += `
|
||||
border: 3px solid ${table.color};
|
||||
border-radius: ${table.shape === "round" ? 1000 : 3}px;
|
||||
background: ${background};
|
||||
box-shadow: 0px 3px rgba(0,0,0,0.07);
|
||||
padding: ${table.shape === "round" ? "4px 10px" : "4px 8px"};
|
||||
color: ${textColor};`;
|
||||
|
||||
if (this.pos.floorPlanStyle == "kanban") {
|
||||
const floor = table.floor;
|
||||
const index = floor.tables.indexOf(table);
|
||||
@@ -54,37 +92,35 @@ export class Table extends Component {
|
||||
const position_h =
|
||||
widthTable * (index % nbrHorizontal) + 5 + (index % nbrHorizontal) * 10;
|
||||
const position_v =
|
||||
widthTable * Math.floor(index / nbrHorizontal) +
|
||||
5 +
|
||||
(widthTable + 25) * Math.floor(index / nbrHorizontal) +
|
||||
10 +
|
||||
Math.floor(index / nbrHorizontal) * 10;
|
||||
return `
|
||||
this.state.containerHeight = widthTable;
|
||||
this.state.containerWidth = widthTable;
|
||||
|
||||
style += `
|
||||
width: ${widthTable}px;
|
||||
height: ${widthTable}px;
|
||||
line-height: ${widthTable}px;
|
||||
top: ${position_v}px;
|
||||
left: ${position_h}px;
|
||||
border: ${border};
|
||||
border-radius: ${table.shape === "round" ? 1000 : 3}px;
|
||||
background: ${background};
|
||||
box-shadow: ${boxShadow};
|
||||
font-size: ${widthTable >= 150 ? 32 : 16}px;
|
||||
color: ${textColor};
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
this.state.containerHeight = table.height;
|
||||
this.state.containerWidth = table.width;
|
||||
|
||||
style += `
|
||||
width: ${table.width}px;
|
||||
height: ${table.height}px;
|
||||
line-height: ${table.height}px;
|
||||
top: ${table.position_v}px;
|
||||
left: ${table.position_h}px;
|
||||
border: ${border};
|
||||
border-radius: ${table.shape === "round" ? 1000 : 3}px;
|
||||
background: ${background};
|
||||
box-shadow: ${boxShadow};
|
||||
font-size: ${table.height >= 150 && table.width >= 150 ? 32 : 16}px;
|
||||
color: ${textColor};
|
||||
`;
|
||||
}
|
||||
|
||||
style += `
|
||||
font-size: ${this.fontSize}px;
|
||||
line-height: ${this.fontSize}px;`;
|
||||
|
||||
return style;
|
||||
}
|
||||
get fill() {
|
||||
const customerCount = this.pos.getCustomerCount(this.props.table.id);
|
||||
@@ -110,18 +146,16 @@ export class Table extends Component {
|
||||
return !Number.isNaN(result) ? result : 0;
|
||||
}
|
||||
get orderCountClass() {
|
||||
const countClass = { "order-count": true };
|
||||
if (this.pos.orderPreparationCategories.size) {
|
||||
const notifications = this._getNotifications();
|
||||
countClass["notify-printing"] = notifications.printing;
|
||||
countClass["notify-skipped"] = notifications.skipped;
|
||||
}
|
||||
const notifications = this._getNotifications();
|
||||
const countClass = {
|
||||
"order-count": true,
|
||||
"notify-printing": notifications.printing,
|
||||
"notify-skipped": notifications.skipped,
|
||||
};
|
||||
return countClass;
|
||||
}
|
||||
get customerCountDisplay() {
|
||||
return `${this.pos.getCustomerCount(this.props.table.id)}/${
|
||||
this.props.table.seats
|
||||
}`;
|
||||
return `${this.pos.getCustomerCount(this.props.table.id)}/${this.props.table.seats}`;
|
||||
}
|
||||
_getNotifications() {
|
||||
const table = this.props.table;
|
||||
@@ -133,8 +167,7 @@ export class Table extends Component {
|
||||
}
|
||||
isOccupied() {
|
||||
return (
|
||||
this.pos.getCustomerCount(this.props.table.id) > 0 ||
|
||||
this.props.table.order_count > 0
|
||||
this.pos.getCustomerCount(this.props.table.id) > 0 || this.props.table.order_count > 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.Table" owl="1">
|
||||
<div class="table" t-on-click.stop="(ev) => props.onClick(props.table, ev)" t-att-style="style">
|
||||
<span class="table-cover" t-att-class="{ full: fill >= 1 }" t-att-style="`height: ${Math.ceil(fill * 100)}%`"></span>
|
||||
<span t-att-class="orderCountClass" t-att-hidden="orderCount === 0">
|
||||
<t t-esc="orderCount" />
|
||||
</span>
|
||||
<span class="label">
|
||||
<t t-esc="props.table.name" />
|
||||
</span>
|
||||
<span class="table-seats">
|
||||
<div class="infos">
|
||||
<span class="label">
|
||||
<t t-esc="props.table.name" />
|
||||
</span>
|
||||
<span t-att-class="orderCountClass" t-att-hidden="orderCount === 0" t-att-style="badgeStyle">
|
||||
<t t-esc="orderCount" />
|
||||
</span>
|
||||
</div>
|
||||
<span class="table-seats">
|
||||
<div class="cover" t-att-style="`width: ${Math.ceil(fill * 100)}%`" />
|
||||
<t t-esc="customerCountDisplay" />
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -18,7 +18,9 @@ patch(Navbar.prototype, "pos_restaurant.Navbar", {
|
||||
return this._super() || (this.pos.config.module_pos_restaurant && !this.pos.table);
|
||||
},
|
||||
onSwitchButtonClick() {
|
||||
this.pos.floorPlanStyle = this.pos.floorPlanStyle == "kanban" ? "default" : "kanban";
|
||||
const mode = this.pos.floorPlanStyle == "kanban" ? "default" : "kanban";
|
||||
localStorage.setItem("floorPlanStyle", mode);
|
||||
this.pos.floorPlanStyle = mode;
|
||||
},
|
||||
toggleEditMode() {
|
||||
this.pos.toggleEditMode();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { OrderWidget } from "@point_of_sale/app/screens/product_screen/order/order";
|
||||
import { AlertPopup } from "@point_of_sale/app/utils/alert_popup/alert_popup";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(OrderWidget.prototype, "pos_restaurant.OrderWidget", {
|
||||
releaseTable() {
|
||||
const currentTable = this.pos.table;
|
||||
const orderOnTable = this.pos.orders.filter(
|
||||
(o) => o.tableId === currentTable.id && o.finalized === false
|
||||
);
|
||||
const orderOnTableWithOrderline = orderOnTable.find((o) => o.orderlines.length > 0);
|
||||
|
||||
if (orderOnTableWithOrderline) {
|
||||
this.popup.add(AlertPopup, {
|
||||
title: _t("Open orders"),
|
||||
body: _t(
|
||||
"There are open orders on this table. Please close them before releasing the table."
|
||||
),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const order of orderOnTable) {
|
||||
this.pos.removeOrder(order);
|
||||
}
|
||||
|
||||
this.pos.showScreen("FloorScreen");
|
||||
},
|
||||
get showReleaseBtn() {
|
||||
if (!this.pos.config.module_pos_restaurant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTable = this.pos.table;
|
||||
const noEmptyOrderOnTable = this.pos.orders.filter(
|
||||
(o) => o.tableId === currentTable.id && o.finalized === false && o.orderlines.length > 0
|
||||
);
|
||||
return noEmptyOrderOnTable.length ? false : true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_restaurant.OrderWidget" t-inherit="point_of_sale.OrderWidget" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('order-empty')]" position="after">
|
||||
<button t-if="showReleaseBtn" class="btn btn-primary" style="border:none; font-size: 20px;" t-on-click="releaseTable">Release table</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
+7
-1
@@ -30,7 +30,7 @@ export class TableGuestsButton extends Component {
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const guestCount = parseInt(inputNumber, 10) || 1;
|
||||
const guestCount = parseInt(inputNumber, 10) || 0;
|
||||
// Set the maximum number possible for an integer
|
||||
const max_capacity = 2 ** 31 - 1;
|
||||
if (guestCount > max_capacity) {
|
||||
@@ -43,6 +43,12 @@ export class TableGuestsButton extends Component {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (guestCount == 0 && this.currentOrder.orderlines.length === 0) {
|
||||
this.pos.removeOrder(this.currentOrder);
|
||||
this.pos.showScreen("FloorScreen");
|
||||
}
|
||||
|
||||
this.currentOrder.setCustomerCount(guestCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,16 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
|
||||
});
|
||||
},
|
||||
getTable(order) {
|
||||
if (order.getTable()) {
|
||||
return `${order.getTable().floor.name} (${order.getTable().name})`;
|
||||
const table = order.getTable();
|
||||
if (table) {
|
||||
let floorAndTable = "";
|
||||
|
||||
if (this.pos.floors && this.pos.floors.length > 1) {
|
||||
floorAndTable = `${table.floor.name}/`;
|
||||
}
|
||||
|
||||
floorAndTable += table.name;
|
||||
return floorAndTable;
|
||||
}
|
||||
},
|
||||
//@override
|
||||
@@ -118,9 +126,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
|
||||
async onDoRefund() {
|
||||
const order = this.getSelectedSyncedOrder();
|
||||
if (this.pos.config.module_pos_restaurant && order && !this.pos.table) {
|
||||
this.pos.setTable(
|
||||
order.table ? order.table : Object.values(this.pos.tables_by_id)[0]
|
||||
);
|
||||
this.pos.setTable(order.table ? order.table : Object.values(this.pos.tables_by_id)[0]);
|
||||
}
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
<t t-esc="nGuests" />
|
||||
</span>
|
||||
<span> </span>
|
||||
<span>Guests</span>
|
||||
<span>Dine-in Guests</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -131,17 +131,25 @@ class Check {
|
||||
}
|
||||
tableSeatIs(table, val) {
|
||||
return [
|
||||
{
|
||||
content: `Unselect table`,
|
||||
trigger: `.floor-map`,
|
||||
},
|
||||
{
|
||||
content: `number of seats in table '${table}' is '${val}'`,
|
||||
trigger: `.floor-map .table .label:contains("${table}") ~ .table-seats:contains("${val}")`,
|
||||
trigger: `.floor-map .table .infos:has(.label:contains("${table}")) ~ .table-seats:contains("${val}")`,
|
||||
run: function () {},
|
||||
},
|
||||
{
|
||||
content: `click table '${table}'`,
|
||||
trigger: `.floor-map .table .label:contains("${table}")`,
|
||||
},
|
||||
];
|
||||
}
|
||||
orderCountSyncedInTableIs(table, count) {
|
||||
return [
|
||||
{
|
||||
trigger: `.floor-map .table .order-count:contains("${count}") ~ .label:contains("${table}")`,
|
||||
trigger: `.floor-map .table .label:contains("${table}") ~ .order-count:contains("${count}")`,
|
||||
run: function () {},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -28,6 +28,14 @@
|
||||
<setting help="Split total or order lines" id="iface_splitbill" attrs="{'invisible': [('pos_module_pos_restaurant', '=', False)]}">
|
||||
<field name="pos_iface_splitbill" string="Allow Bill Splitting"/>
|
||||
</setting>
|
||||
<setting help="Online reservation for restaurant" attrs="{'invisible': [('pos_module_pos_restaurant', '=', False)]}">
|
||||
<field name="pos_module_pos_restaurant_appointment" string="Table Booking" widget="upgrade_boolean" />
|
||||
<div class="content-group" id="pos_table_booking" attrs="{'invisible': [('pos_module_pos_restaurant_appointment', '=', False)]}">
|
||||
<div class="text-warning mt16 mb4">
|
||||
Save this page and come back here to set up the feature.
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
</block>
|
||||
<div id="tip_product" position="after">
|
||||
|
||||
Reference in New Issue
Block a user