[IMP] pos: New FloorScreen kanban view and more

Currently, there is only 1 plan view that is unreadable on mobile
and sometimes also complicated in some cases.
This commit  adds a new "Kanban" view used by default on mobile
and available in desktop too. This commit also changes the edit bar
of the FloorScreen: the design is changed and some features are added
like the multiple selection of the tables or the adding and removing
of floors directly from the frontend. It also changes the navbar
of the PoS, moving the buttons in a navigation menu accessible
thanks to a button.

Task-id: 3212032
This commit is contained in:
Adrien Guilliams (adgu)
2023-04-12 15:42:27 +02:00
parent 0651424113
commit 6cabcd7f1c
52 changed files with 914 additions and 437 deletions
@@ -9,9 +9,10 @@ import { HeaderButton } from "@point_of_sale/js/ChromeWidgets/HeaderButton";
import { ProxyStatus } from "@point_of_sale/js/ChromeWidgets/ProxyStatus";
import { SaleDetailsButton } from "@point_of_sale/js/ChromeWidgets/SaleDetailsButton";
import { SyncNotification } from "@point_of_sale/js/ChromeWidgets/SyncNotification";
import { BackendButton } from "@point_of_sale/js/ChromeWidgets/BackendButton";
import { CashMovePopup } from "./cash_move_popup/cash_move_popup";
import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen";
import { Component } from "@odoo/owl";
import { Component, useState, useExternalListener } from "@odoo/owl";
export class Navbar extends Component {
static template = "point_of_sale.Navbar";
@@ -23,6 +24,7 @@ export class Navbar extends Component {
ProxyStatus,
SaleDetailsButton,
SyncNotification,
BackendButton,
};
static props = {
showCashMoveButton: Boolean,
@@ -33,7 +35,16 @@ export class Navbar extends Component {
this.popup = useService("popup");
this.notification = useService("pos_notification");
this.hardwareProxy = useService("hardware_proxy");
this.state = useState({ isMenuOpened: false });
useExternalListener(window, "mouseup", this.onOutsideClick);
}
onOutsideClick() {
if (this.state.isMenuOpened) {
this.state.isMenuOpened = false;
}
}
get customerFacingDisplayButtonIsShown() {
return this.env.pos.config.iface_customer_facing_display;
}
@@ -81,4 +92,16 @@ export class Navbar extends Component {
}
return 0;
}
isBurgerMenuClosed() {
return !this.state.isMenuOpened;
}
closeMenu() {
this.state.isMenuOpened = false;
}
openMenu() {
this.state.isMenuOpened = true;
}
}
@@ -6,16 +6,6 @@
<div t-if="pos.tempScreen" class="block-top-header" />
<div class="pos-branding">
<img t-if="!env.isMobile" class="pos-logo" t-on-click="() => debug.toggleWidget()" src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
<div t-if="props.showCashMoveButton" class="cash-move-button" t-on-click="onCashMoveButtonClick">
<i class="fa fa-money" aria-hidden="true"></i>
<span style="padding-left: 10px;">Cash In/Out</span>
</div>
<div class="ticket-button" t-att-class="{ highlight: isTicketScreenShown }" t-on-click="onTicketButtonClick">
<div class="with-badge" t-att-badge="orderCount">
<i class="fa fa-ticket" aria-hidden="true"></i>
</div>
<div t-if="!env.isMobile">Orders</div>
</div>
</div>
<div class="pos-rightheader">
<div class="status-buttons">
@@ -24,7 +14,33 @@
<ProxyStatus t-if="env.pos.config.use_proxy" />
<CustomerFacingDisplayButton t-if="customerFacingDisplayButtonIsShown" />
<SyncNotification />
<HeaderButton />
</div>
<div t-if="isBurgerMenuClosed()" class="navbar-button menu-button" t-on-click="openMenu">
<i class="fa fa-bars" aria-hidden="true"></i>
</div>
<div t-else="" class="navbar-button menu menu-button" t-on-click="closeMenu">
<ul>
<li class="menu-item">
<i class="fa fa-bars" aria-hidden="true"></i>
<ul class="sub-menu">
<li class="menu-item navbar-button ticket-button" t-on-click="onTicketButtonClick">
<div class="with-badge" t-att-badge="orderCount">
<span>Orders</span>
</div>
</li>
<li t-if="props.showCashMoveButton" class="menu-item navbar-button" t-on-click="onCashMoveButtonClick">
<span>Cash In/Out</span>
</li>
<CustomerFacingDisplayButton t-if="CustomerFacingDisplayButtonIsShown"/>
<div class="end-nav-button">
<BackendButton/>
<div class="close-button">
<HeaderButton/>
</div>
</div>
</ul>
</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,17 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
import { Component } from "@odoo/owl";
export class BackendButton extends Component {
static template = "BackendButton";
setup() {
super.setup(...arguments);
this.pos = useService("pos");
}
async onClick() {
this.pos.closePos();
}
}
+44 -47
View File
@@ -363,18 +363,12 @@ td {
cursor: pointer;
}
.pos .ticket-button {
display: flex;
}
/* b) The right part of the top-bar */
.pos .pos-rightheader {
flex-grow: 1;
height:100%;
display: flex;
overflow : hidden;
overflow-x: auto;
}
.pos .pos-rightheader > * {
border-right: 1px solid $o-brand-odoo;
@@ -394,11 +388,17 @@ td {
padding: 0 10px; /*$dropdown-item-padding-x * .5*/
}
.pos .status-buttons .cashier-name {
display: flex;
align-items: center;
gap: 5px;
}
.pos .status-buttons i.fa {
font-size: large;
}
.pos .status-buttons .avatar, .pos .status-buttons i.fa-sign-out {
.pos .status-buttons .avatar {
vertical-align: middle;
}
@@ -480,20 +480,6 @@ td {
flex: 1;
}
/* c) The session buttons */
.pos .pos-rightheader .header-button {
cursor: pointer;
-webkit-transition-property: background;
-webkit-transition-duration: 0.2s;
-webkit-transition-timing-function: ease-out;
}
.pos .pos-rightheader .header-button:active{
background: rgba(0,0,0,0.2);
color:#EEE;
}
/* c) The notifications indicator */
.pos .oe_status{
@@ -3411,28 +3397,6 @@ td {
transition: all 300ms linear;
}
.ticket-button {
display: flex;
align-items: center;
padding: 0 15px;
font-size: medium;
color: white;
}
.ticket-button.highlight {
background: rgba(0, 0, 0, .08);
}
.ticket-button:hover {
background: rgba(0, 0, 0, .08);
cursor: pointer;
}
.ticket-button .with-badge {
margin-right: 0.7em;
font-size: larger;
}
[badge] {
position: relative;
}
@@ -3447,8 +3411,8 @@ td {
padding: 2px;
position: absolute;
text-align: center;
left: 0.6em;
bottom: 0.6em;
left: 5em;
bottom: 0;
}
[badge^="-"]:after,
@@ -3457,7 +3421,7 @@ td {
display: none;
}
.cash-move-button {
.navbar-button {
display: flex;
align-items: center;
padding: 0 15px;
@@ -3465,7 +3429,7 @@ td {
color: white;
}
.cash-move-button:hover {
.navbar-button:hover {
background: rgba(0, 0, 0, .08);
cursor: pointer;
}
@@ -3783,3 +3747,36 @@ td {
transform: translate(24px, 0);
}
}
.menu {
color: white;
z-index: 1000;
font-size: medium;
&-item {
display: flex;
position: relative;
.sub-menu {
position: absolute;
top: 33px;
right: -16px;
width: 216px;
display: flex;
flex-direction: column;
background-color: white;
z-index: 9999;
align-items: stretch;
.menu-item {
padding: 10px;
span, i {
color: black;
inline-size: 196px;
overflow-wrap: break-word;
}
}
}
}
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="BackendButton" owl="1">
<li class="menu-item navbar-button" t-on-click="onClick">
<span>
Backend
</span>
</li>
</t>
</templates>
@@ -3,7 +3,7 @@
<t t-name="CashierName" owl="1">
<div t-att-class="cssClass">
<span><img t-att-src="avatar" t-att-alt="username" class="avatar"/> <span t-if="!env.isMobile" t-esc="username" class="username"/></span>
<span class="cashier-name"><img t-att-src="avatar" t-att-alt="username" class="avatar"/> <span t-if="!env.isMobile" t-esc="username" class="username"/></span>
</div>
</t>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="CustomerFacingDisplayButton" owl="1">
<div class="oe_status" t-on-click="() => customerDisplay.connect()">
<li class="menu-item navbar-button" t-on-click="() => customerDisplay.connect()">
<span class="message"><t t-esc="message" /></span>
<div t-if="customerDisplay.status === 'warning'" class="js_warning oe_icon oe_orange">
<i class="fa fa-fw fa-desktop" role="img" aria-label="Customer Screen Warning" title="Customer Screen Warning"></i>
@@ -12,8 +12,9 @@
</div>
<div t-if="customerDisplay.status === 'success'" class="js_connected oe_icon oe_green">
<i class="fa fa-fw fa-desktop" role="img" aria-label="Customer Screen Connected" title="Customer Screen Connected"></i>
</div>
</div>
</div>
<span>Customer Screen</span>
</li>
</t>
</templates>
@@ -2,9 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="HeaderButton" owl="1">
<div class="header-button" t-on-click="onClick">
<span><i class="fa fa-sign-out" role="img"/> <t t-if="!env.isMobile">Close</t></span>
</div>
<li class="menu-item navbar-button header-button" t-on-click="onClick">
<span>
Close Session
</span>
</li>
</t>
</templates>
@@ -80,7 +80,6 @@
</main>
<footer class="footer">
<div class="button highlight" t-on-click="confirm">Close Session</div>
<div class="button" t-on-click="() => pos.closePos()" title="Visit the Backend but keep session open">Backend</div>
<div class="button" t-att-class="{'disabled': !canCancel()}" t-on-click="cancel">Discard</div>
<!-- Download Sale Details -->
<div class="small button icon"
@@ -14,6 +14,7 @@ startSteps();
ProductScreen.do.confirmOpeningPopup();
ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline("Desk Pad", "1", "2", "2.0");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus("-0001", "Ongoing");
@@ -22,6 +23,7 @@ TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline("Monitor Stand", "3", "4", "12.0");
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus("-0002", "Payment");
@@ -34,6 +36,7 @@ PaymentScreen.check.remainingIs("0.0");
PaymentScreen.check.validateButtonIsHighlighted(true);
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus("-0003", "Receipt");
@@ -43,17 +46,20 @@ ProductScreen.check.productIsDisplayed("Desk Pad");
ProductScreen.check.selectedOrderlineHas("Desk Pad", "1.0", "2.0");
// Select order 2, should be at Payment Screen
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0002");
PaymentScreen.check.emptyPaymentlines("12.0");
PaymentScreen.check.validateButtonIsHighlighted(false);
// Select order 3, should be at Receipt Screen
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0003");
ReceiptScreen.check.totalAmountContains("30.0");
// Pay order 1, with change
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0001");
ProductScreen.check.isShown();
@@ -66,6 +72,7 @@ PaymentScreen.do.clickValidate();
ReceiptScreen.check.totalAmountContains("2.0");
// Order 1 now should have Receipt status
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.checkStatus("-0001", "Receipt");
@@ -78,6 +85,7 @@ ReceiptScreen.check.totalAmountContains("30.0");
// then delete the new empty order
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.orderIsEmpty();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder("-0004");
TicketScreen.do.deleteOrder("-0001");
@@ -95,6 +95,7 @@ PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
@@ -121,6 +122,7 @@ PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
@@ -147,6 +149,7 @@ PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
@@ -159,6 +162,7 @@ PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.check.remainingIs('0.0');
PaymentScreen.check.changeIs('0.0');
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
@@ -17,6 +17,7 @@ ProductScreen.do.clickHomeCategory();
ProductScreen.exec.addOrderline("Desk Pad", "1", "2");
ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("Nicole Ford");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, "Nicole Ford");
TicketScreen.do.clickNewTicket();
@@ -25,6 +26,7 @@ ProductScreen.do.clickPartnerButton();
ProductScreen.do.clickCustomer("Brandon Freeman");
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(3, "Brandon Freeman");
TicketScreen.do.clickNewTicket();
@@ -33,6 +35,7 @@ ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod("Bank");
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(4, "Receipt");
TicketScreen.do.selectFilter("Receipt");
@@ -54,6 +57,7 @@ TicketScreen.check.nthRowContains(2, "Receipt");
TicketScreen.do.clickDiscard();
ReceiptScreen.check.isShown();
// Open again the TicketScreen to check the Paid filter.
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter("Paid");
TicketScreen.check.nthRowContains(2, "-0003");
@@ -66,6 +70,7 @@ ReceiptScreen.check.isShown();
ReceiptScreen.do.clickNextOrder();
ProductScreen.check.isShown();
// Check that the Paid filter will show the 2 synced orders.
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter("Paid");
TicketScreen.check.nthRowContains(3, "Brandon Freeman");
@@ -230,6 +230,10 @@ steps = steps.concat(set_fiscal_position_on_order("FP-POS-2M"));
steps = steps.concat(verify_order_total("5.52"));
steps = steps.concat([
{
content: "open the navbar menu",
trigger: ".menu-button",
},
{
content: "open closing the Point of Sale frontend popup",
trigger: ".header-button",
@@ -22,6 +22,14 @@ class Do {
},
];
}
clickMenuButton() {
return [
{
content: "Click on the menu button",
trigger: ".menu-button",
}
];
}
}
// FIXME: this is a horrible hack to export an object as named exports.
// eslint-disable-next-line no-undef
@@ -258,9 +258,13 @@ class Check {
}
checkSecondCashClosingDetailsLineAmount(amount, sign) {
return [
{
content: 'Open menu',
trigger: '.menu-button',
},
{
content: 'Click close session button',
trigger: '.fa-sign-out',
trigger: '.header-button',
},
{
content: 'Check closing details',
@@ -259,8 +259,8 @@ steps = steps.concat([
trigger: ".selection-item:contains('min_quantity ordering')",
},
{
content: "close the Point of Sale frontend",
trigger: ".header-button",
content: "open the navbar menu",
trigger: ".menu-button",
},
{
content: "confirm closing the frontend",
+9 -3
View File
@@ -2,9 +2,15 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_hr.Navbar" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension" owl="1">
<xpath expr="//HeaderButton" position="replace">
<HeaderLockButton t-if="env.pos.config.module_pos_hr" />
<HeaderButton t-if="!env.pos.config.module_pos_hr || env.pos.get_cashier().role === 'manager' || env.pos.get_cashier_user_id() === env.pos.user.id" />
<xpath expr="//div[hasclass('end-nav-button')]" position="replace">
<div class="end-nav-button">
<BackendButton t-if="!env.pos.config.module_pos_hr"/>
<BackendButton t-elif="(env.pos.get_cashier().role === 'manager' || env.pos.get_cashier_user_id() === env.pos.user.id)"/>
<HeaderLockButton t-if="env.pos.config.module_pos_hr" />
<div class="close-button" t-if="!env.pos.config.module_pos_hr || env.pos.get_cashier().role === 'manager' || env.pos.get_cashier_user_id() === env.pos.user.id">
<HeaderButton/>
</div>
</div>
</xpath>
</t>
-11
View File
@@ -114,14 +114,3 @@
height: 55px;
}
}
.pos .pos-rightheader .header-button.lock-button {
font-size: 20px;
color: rgb(94, 185, 55);
transition: all 200ms ease-in-out;
width: 18px;
}
.pos .pos-rightheader .header-button.lock-button:hover {
color: rgb(197, 52, 0);
}
@@ -1,6 +1,6 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/pos_hook";
export class HeaderLockButton extends Component {
@@ -8,15 +8,10 @@ export class HeaderLockButton extends Component {
setup() {
super.setup();
this.state = useState({ isUnlockIcon: true, title: "Unlocked" });
this.pos = usePos();
}
async showLoginScreen() {
this.env.pos.reset_cashier();
await this.pos.showTempScreen("LoginScreen");
}
onMouseOver(isMouseOver) {
this.state.isUnlockIcon = !isMouseOver;
this.state.title = isMouseOver ? "Lock" : "Unlocked";
}
}
@@ -2,14 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="HeaderLockButton" owl="1">
<div class="header-button lock-button" t-on-mouseover="() => this.onMouseOver(true)"
t-on-click="showLoginScreen" t-on-mouseout="() => this.onMouseOver(false)">
<span class="lock-button">
<i class="fa"
t-att-class="{ 'fa-unlock': state.isUnlockIcon, 'fa-lock': !state.isUnlockIcon }"
role="img" t-att-aria-label="state.title" t-att-title="state.title"></i>
<li class="menu-item navbar-button lock-button" t-on-click="showLoginScreen">
<span>
Lock
</span>
</div>
</li>
</t>
</templates>
@@ -41,6 +41,7 @@ PosHr.check.cashierNameIs("Pos Employee1");
PosHr.do.clickCashierName();
SelectionPopup.do.clickItem("Mitchell Admin");
PosHr.check.cashierNameIs("Mitchell Admin");
Chrome.do.clickMenuButton();
PosHr.do.clickLockButton();
PosHr.do.clickLoginButton();
SelectionPopup.do.clickItem("Pos Employee2");
@@ -56,15 +57,18 @@ ProductScreen.do.clickHomeCategory();
// order for employee 2
ProductScreen.exec.addOrderline("Desk Pad", "1", "2");
ProductScreen.check.totalAmountIs("2.0");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, "Pos Employee2");
// order for employee 1
Chrome.do.clickMenuButton();
PosHr.do.clickLockButton();
PosHr.exec.login("Pos Employee1", "2580");
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline("Desk Pad", "1", "4");
ProductScreen.check.totalAmountIs("4.0");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, "Pos Employee2");
TicketScreen.check.nthRowContains(3, "Pos Employee1");
@@ -76,6 +80,7 @@ PosHr.check.cashierNameIs("Mitchell Admin");
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline("Desk Pad", "1", "8");
ProductScreen.check.totalAmountIs("8.0");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(4, "Mitchell Admin");
@@ -17,7 +17,7 @@ class Do {
return [
{
content: "click lock button",
trigger: ".header-button .lock-button",
trigger: ".lock-button",
},
];
}
+33 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo import api, fields, models, _, Command
from odoo.exceptions import UserError
@@ -41,6 +41,34 @@ class RestaurantFloor(models.Model):
'Open session: %s' % (' '.join(config.mapped('name')),))
return super(RestaurantFloor, self).write(vals)
def rename_floor(self, new_name):
for floor in self:
floor.name = new_name
@api.model
def create_from_ui(self, name, background_color, config_id):
floor_fields = {
"name": name,
"background_color": background_color,
}
pos_floor = self.create(floor_fields)
pos_floor.pos_config_ids = [Command.link(config_id)]
return {
'id': pos_floor.id,
'name': pos_floor.name,
'background_color': pos_floor.background_color,
'table_ids': [],
'sequence': pos_floor.sequence,
'tables': [],
}
def deactivate_floor(self, session_id):
draft_orders = self.env['pos.order'].search([('session_id', '=', session_id), ('state', '=', 'draft'), ('table_id.floor_id', '=', self.id)])
if draft_orders:
raise UserError(_("You cannot delete a floor when orders are still in draft for this floor."))
for table in self.table_ids:
table.active = False
self.active = False
class RestaurantTable(models.Model):
@@ -60,6 +88,10 @@ class RestaurantTable(models.Model):
color = fields.Char('Color', help="The table's color, expressed as a valid 'background' CSS property value")
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.
@@ -61,3 +61,6 @@ class PosSession(models.Model):
}
def _get_pos_ui_restaurant_printer(self, params):
return self.env['restaurant.printer'].search_read(**params['search_params'])
def get_pos_ui_restaurant_floor(self):
return self._get_pos_ui_restaurant_floor(self._loader_params_restaurant_floor())
@@ -1,24 +1,45 @@
/** @odoo-module */
import { Component, useState } from "@odoo/owl";
import { Component, useExternalListener } from "@odoo/owl";
export class EditBar extends Component {
static template = "pos_restaurant.EditBar";
static props = {
selectedTable: [{ type: Object, shape: { shape: String, "*": true } }, { value: false }],
selectedTables: Object,
nbrFloors: Number,
floorMapScrollTop: Number,
isColorPicker: Boolean,
toggleColorPicker: Function,
createTable: Function,
duplicateTable: Function,
duplicateTableOrFloor: Function,
renameTable: Function,
changeSeatsNum: Function,
changeShape: Function,
changeToCircle: Function,
changeToSquare: Function,
setTableColor: Function,
setFloorColor: Function,
deleteTable: Function,
deleteFloorOrTable: Function,
toggleEditMode: Function,
};
setup() {
super.setup();
this.state = useState({ isColorPicker: false });
useExternalListener(window, "click", this.onOutsideClick);
}
onOutsideClick() {
if (this.props.isColorPicker) {
this.props.isColorPicker = false;
}
}
getSelectedTablesShape() {
let shape = 'round';
this.props.selectedTables.forEach((table) => {
if (table.shape == 'square') {
shape = 'square';
}
});
return shape;
}
}
@@ -2,62 +2,79 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.EditBar" owl="1">
<div class="edit-bar" t-attf-style="top:{{props.floorMapScrollTop}}px;">
<div class="edit-bar-top">
<span class="edit-button first-button" t-if="!env.isMobile"></span>
<span class="edit-button" t-on-click.stop="props.createTable">
<i class="fa fa-plus" role="img" aria-label="Add" title="Add"></i>
<i class="fa fa-plus icon-button" role="img" aria-label="Add" title="Add"></i>
<span class="text-button">ADD</span>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.duplicateTable">
<i class="fa fa-files-o" role="img" aria-label="Duplicate" title="Duplicate"></i>
<span class="edit-button" t-att-class="{ disabled: props.selectedTables.length == 0 }" t-on-click.stop="props.changeSeatsNum">
<i class="fa fa-user icon-button" role="img" aria-label="Seats" title="Seats"></i>
<span class="text-button">SEATS</span>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.renameTable">
<i class="fa fa-font" role="img" aria-label="Rename" title="Rename"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeSeatsNum">
<i class="fa fa-user" role="img" aria-label="Seats" title="Seats"></i>
</span>
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeShape">
<span t-if="!props.selectedTable or props.selectedTable.shape == 'square'" class="button-option square">
<i class="fa fa-square-o" role="img" aria-label="Square Shape" title="Square Shape"></i>
<span class="edit-button" t-if="getSelectedTablesShape() == 'square'" t-att-class="{ disabled: props.selectedTables.length == 0 }" t-on-click.stop="props.changeToCircle">
<span class="button-option round">
<i class="fa fa-circle-o icon-button" role="img" aria-label="Round Shape" title="Round Shape"></i>
</span>
<span t-else="" class="button-option round">
<i class="fa fa-circle-o" role="img" aria-label="Round Shape" title="Round Shape"></i>
<span class="text-button">SHAPE</span>
</span>
<span class="edit-button" t-else="" t-att-class="{ disabled: props.selectedTables.length == 0 }" t-on-click.stop="props.changeToSquare">
<span class="button-option square">
<i class="fa fa-square-o icon-button" role="img" aria-label="Square Shape" title="Square Shape"></i>
</span>
<span class="text-button">SHAPE</span>
</span>
<span class="edit-button" t-on-click.stop="() => { state.isColorPicker = !state.isColorPicker }">
<i class="fa fa-tint" role="img" aria-label="Tint" title="Tint"></i>
<span t-if="!props.isColorPicker" class="edit-button" t-on-click.stop="props.toggleColorPicker">
<i class="fa fa-paint-brush icon-button" role="img" aria-label="Tint" title="Tint"></i>
<span class="text-button">FILL</span>
</span>
<div t-if="state.isColorPicker and props.selectedTable" class="color-picker fg-picker">
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
<i class="fa fa-times" />
<span t-else="" class="edit-button is-active" t-on-click.stop="props.toggleColorPicker">
<i class="fa fa-paint-brush icon-button" role="img" aria-label="Tint" title="Tint"></i>
<span class="text-button">FILL</span>
</span>
<span class="edit-button" t-att-class="{ disabled: props.selectedTables.length > 1 }" t-on-click.stop="props.renameTable">
<i class="fa fa-pencil-square-o icon-button" role="img" aria-label="Rename" title="Rename"></i>
<span class="text-button">RENAME</span>
</span>
<span class="edit-button" t-on-click.stop="props.duplicateTableOrFloor">
<i class="fa fa-clone icon-button" role="img" aria-label="Copy" title="Copy"></i>
<span class="text-button">COPY</span>
</span>
<span class="edit-button trash" t-on-click.stop="props.deleteFloorOrTable">
<i class="fa fa-trash icon-button" role="img" aria-label="Delete" title="Delete"></i>
<span class="text-button">DELETE</span>
</span>
<span class="edit-button" t-on-click.stop="props.toggleEditMode">
<div class="close-edit-button">
<i class="fa fa-times icon-button" role="img" aria-label="Close" title="Close"></i>
<span class="text-button">CLOSE</span>
</div>
<span class="color tl" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setTableColor('#EB6D6D')" />
<span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setTableColor('#35D374')" />
<span class="color tr" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setTableColor('#6C6DEC')" />
<span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setTableColor('#EBBF6D')" />
<span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setTableColor('#EBEC6D')" />
<span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setTableColor('#AC6DAD')" />
<span class="color bl" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setTableColor('#6C6D6D')" />
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setTableColor('#ACADAD')" />
<span class="color br" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setTableColor('#4ED2BE')" />
</div>
<div t-if="state.isColorPicker and !props.selectedTable" class="color-picker bg-picker">
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
<i class="fa fa-times" />
</div>
<span class="color tl" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setFloorColor('rgb(244, 149, 149)')" />
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setFloorColor('rgb(130, 233, 171)')" />
<span class="color tr" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setFloorColor('rgb(136, 137, 242)')" />
<span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setFloorColor('rgb(255, 214, 136)')" />
<span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setFloorColor('rgb(254, 255, 154)')" />
<span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setFloorColor('rgb(209, 171, 210)')" />
<span class="color bl" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setFloorColor('rgb(75, 75, 75)')" />
<span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setFloorColor('rgb(210, 210, 210)')" />
<span class="color br" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setFloorColor('rgb(127, 221, 236)')" />
</div>
<span class="edit-button trash" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.deleteTable">
<i class="fa fa-trash" role="img" aria-label="Delete" title="Delete"></i>
</span>
</div>
<div t-if="props.isColorPicker and props.selectedTables.length > 0" class="color-picker">
<span class="color" style="background-color:#FFFFFF" role="img" aria-label="White" title="White" t-on-click.stop="() => props.setTableColor('#FFFFFF')" />
<span class="color" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setTableColor('#EB6D6D')" />
<span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setTableColor('#35D374')" />
<span class="color" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setTableColor('#6C6DEC')" />
<span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setTableColor('#EBBF6D')" />
<span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setTableColor('#EBEC6D')" />
<span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setTableColor('#AC6DAD')" />
<span class="color" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setTableColor('#6C6D6D')" />
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setTableColor('#ACADAD')" />
<span class="color" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setTableColor('#4ED2BE')" />
</div>
<div t-if="props.isColorPicker and props.selectedTables.length == 0" class="color-picker">
<span class="color" style="background-color:rgb(255, 255, 255)" role="img" aria-label="White" title="White" t-on-click.stop="() => props.setFloorColor('rgb(255, 255, 255)')" />
<span class="color" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setFloorColor('rgb(244, 149, 149)')" />
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setFloorColor('rgb(130, 233, 171)')" />
<span class="color" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setFloorColor('rgb(136, 137, 242)')" />
<span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setFloorColor('rgb(255, 214, 136)')" />
<span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setFloorColor('rgb(254, 255, 154)')" />
<span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setFloorColor('rgb(209, 171, 210)')" />
<span class="color" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setFloorColor('rgb(75, 75, 75)')" />
<span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setFloorColor('rgb(210, 210, 210)')" />
<span class="color" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setFloorColor('rgb(127, 221, 236)')" />
</div>
</t>
</templates>
@@ -12,6 +12,7 @@ export class EditableTable extends Component {
onSaveTable: Function,
limit: { type: Object, shape: { el: [HTMLElement, { value: null }] } },
table: Table.props.table,
selectedTables: Array,
};
setup() {
@@ -42,7 +43,14 @@ export class EditableTable extends Component {
}
onMoveStart() {
if (this.env.pos.floorPlanStyle == 'kanban') {
return;
}
this.startTable = { ...this.props.table };
this.selectedTablesCopy = {};
for (let i = 0; i < this.props.selectedTables.length; i++) {
this.selectedTablesCopy[i] = { ...this.props.selectedTables[i] };
}
// stop the next click event from the touch/click release from unselecting the table
document.addEventListener("click", (ev) => ev.stopPropagation(), {
capture: true,
@@ -51,14 +59,25 @@ export class EditableTable extends Component {
}
onMove({ dx, dy }) {
const { position_h, position_v } = this.startTable;
if (this.env.pos.floorPlanStyle == 'kanban') {
return;
}
const { minX, minY, maxX, maxY } = getLimits(this.root.el, this.props.limit.el);
this.props.table.position_h = constrain(position_h + dx, minX, maxX);
this.props.table.position_v = constrain(position_v + dy, minY, maxY);
for (const [index, table] of Object.entries(this.selectedTablesCopy)) {
const position_h = table.position_h;
const position_v = table.position_v;
this.props.selectedTables[index].position_h = constrain(position_h + dx, minX, maxX);
this.props.selectedTables[index].position_v = constrain(position_v + dy, minY, maxY);
}
this._setElementStyle();
}
onResizeHandleMove([moveX, moveY], { dx, dy }) {
if (this.env.pos.floorPlanStyle == 'kanban') {
return;
}
// Working with min/max x and y makes constraints much easier to apply uniformly
const { width, height, position_h: minX, position_v: minY } = this.startTable;
const newTable = { minX, minY, maxX: minX + width, maxY: minY + height };
@@ -108,6 +127,28 @@ export class EditableTable extends Component {
_setElementStyle() {
const table = this.props.table;
if (this.env.pos.floorPlanStyle == 'kanban') {
const floor = table.floor;
const index = floor.tables.indexOf(table);
const minWidth = 100 + 20;
const nbrHorizontal = Math.floor(window.innerWidth / minWidth);
const widthTable = (window.innerWidth - nbrHorizontal*10) / nbrHorizontal;
const position_h = widthTable * (index % nbrHorizontal) + 5 + (index % nbrHorizontal) * 10;
const position_v = widthTable * Math.floor(index / nbrHorizontal) + 5 + Math.floor(index / nbrHorizontal) * 10;
Object.assign(this.root.el.style, {
left: `${position_h}px`,
top: `${position_v}px`,
width: `${widthTable}px`,
height: `${widthTable}px`,
background: table.color || "rgb(53, 211, 116)",
"line-height": `${widthTable}px`,
"border-radius": table.shape === "round" ? "1000px" : "3px",
"font-size": widthTable >= 150 ? "32px" : "16px",
"opacity": "0.7",
});
return;
}
Object.assign(this.root.el.style, {
left: `${table.position_h}px`,
top: `${table.position_v}px`,
@@ -9,7 +9,7 @@
<span class="table-seats">
<t t-esc="props.table.seats" />
</span>
<t t-foreach="handles" t-as="handle" t-key="handle">
<t t-if="env.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>
</div>
@@ -7,6 +7,7 @@ import { registry } from "@web/core/registry";
import { TextInputPopup } from "@point_of_sale/js/Popups/TextInputPopup";
import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { EditableTable } from "./editable_table";
import { EditBar } from "./edit_bar";
@@ -28,13 +29,21 @@ export class FloorScreen extends Component {
this.orm = useService("orm");
const floor = this.props.floor ? this.props.floor : this.env.pos.floors[0];
this.state = useState({
selectedFloorId: floor.id,
selectedTableId: null,
isEditMode: false,
floorBackground: floor.background_color,
selectedFloorId: floor ? floor.id : null,
selectedTableIds: [],
floorBackground: floor ? floor.background_color : null,
floorMapScrollTop: 0,
isColorPicker: false,
});
if (!this.env.pos.floorPlanStyle) {
if (this.env.isMobile) {
this.env.pos.floorPlanStyle = "kanban";
} else {
this.env.pos.floorPlanStyle = "default";
}
}
this.floorMapRef = useRef("floor-map-ref");
this.addFloorRef = useRef("add-floor-ref");
this.map = useRef("map");
onPatched(this.onPatched);
onMounted(this.onMounted);
@@ -42,11 +51,23 @@ export class FloorScreen extends Component {
}
onPatched() {
this.floorMapRef.el.style.background = this.state.floorBackground;
this.addFloorRef.el.style.background = this.state.floorBackground;
if (!this.env.pos.isEditMode && this.env.pos.floors.length > 0) {
this.addFloorRef.el.style.display = "none";
} else {
this.addFloorRef.el.style.display = "initial";
}
this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top;
}
onMounted() {
this.pos.openCashControl();
this.floorMapRef.el.style.background = this.state.floorBackground;
this.addFloorRef.el.style.background = this.state.floorBackground;
if (!this.env.pos.isEditMode && this.env.pos.floors.length > 0) {
this.addFloorRef.el.style.display = "none";
} else {
this.addFloorRef.el.style.display = "initial";
}
this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top;
// call _tableLongpolling once then set interval of 5sec.
this._tableLongpolling();
@@ -75,15 +96,19 @@ export class FloorScreen extends Component {
debounce(this._computePinchHypo, 10, true)(ev, this.movePinch.bind(this));
}
_onDeselectTable() {
this.state.selectedTableId = null;
this.state.selectedTableIds = [];
}
async _createTableHelper(copyTable) {
async _createTableHelper(copyTable, duplicateFloor=false) {
const existingTable = this.activeFloor.tables;
let newTable;
if (copyTable) {
newTable = Object.assign({}, copyTable);
newTable.position_h += 10;
newTable.position_v += 10;
if (!duplicateFloor) {
newTable.position_h += 10;
newTable.position_v += 10;
}
delete newTable.id;
newTable.order_count = 0;
} else {
let posV = 0;
let posH = 10;
@@ -191,27 +216,28 @@ export class FloorScreen extends Component {
color: "rgb(53, 211, 116)",
};
}
newTable.name = this._getNewTableName(newTable.name);
delete newTable.id;
if (!duplicateFloor) {
newTable.name = this._getNewTableName();
}
newTable.floor_id = [this.activeFloor.id, ""];
newTable.floor = this.activeFloor;
await this._save(newTable);
this.activeTables.push(newTable);
this.activeFloor.table_ids.push(newTable.id);
return newTable;
}
_getNewTableName(name) {
if (name) {
const num = Number((name.match(/\d+/g) || [])[0] || 0);
const str = name.replace(/\d+/g, "");
const n = { num: num, str: str };
n.num += 1;
this._lastName = n;
} else if (this._lastName) {
this._lastName.num += 1;
} else {
this._lastName = { num: 1, str: "" };
_getNewTableName() {
let firstNum = 1;
const tablesNameNumber = this.activeTables.map(table => +table.name).sort(function(a, b){return a-b});
for (let i = 0; i < tablesNameNumber.length; i++) {
if (tablesNameNumber[i] == firstNum) {
firstNum += 1;
} else {
break;
}
}
return "" + this._lastName.str + this._lastName.num;
return firstNum.toString();
}
async _save(table) {
const tableCopy = { ...table };
@@ -220,14 +246,28 @@ export class FloorScreen extends Component {
table.id = tableId;
this.env.pos.tables_by_id[tableId] = table;
}
async _renameFloor(floorId, newName) {
await this.orm.call("restaurant.floor", "rename_floor", [floorId, newName]);
}
async _tableLongpolling() {
if (this.state.isEditMode) {
if (this.env.pos.isEditMode) {
return;
}
const result = await this.orm.call("pos.config", "get_tables_order_count", [
this.env.pos.config.id,
]);
result.forEach((table) => {
for (const table of result) {
if (!(table.id in this.env.pos.tables_by_id)) {
//We enter this condition if, after loading our PoS, a table is deleted on a PoS and not on another.
//This will lead in receiving information on orders from a table that we do not have.
//If we do not have a table in the PoS but still receive information about it, we reload the tables
//from the server.
const result = await this.orm.call("pos.session", "get_pos_ui_restaurant_floor", [[odoo.pos_session_id]]);
if (this.env.pos.config.is_table_management) {
this.env.pos.floors = result;
this.env.pos.loadRestaurantFloor();
}
}
const table_obj = this.env.pos.tables_by_id[table.id];
const unsynced_orders = this.env.pos.getTableOrders(table_obj.id).filter(
(o) =>
@@ -237,21 +277,26 @@ export class FloorScreen extends Component {
!o.finalized
).length;
table_obj.order_count = table.orders + unsynced_orders;
});
}
}
get activeFloor() {
return this.env.pos.floors_by_id[this.state.selectedFloorId];
get activeFloor() {
return this.state.selectedFloorId ? this.env.pos.floors_by_id[this.state.selectedFloorId] : null;
}
get activeTables() {
return this.activeFloor.tables;
return this.activeFloor ? this.activeFloor.tables : null;
}
get isFloorEmpty() {
return this.activeTables.length === 0;
return this.activeTables ? this.activeTables.length === 0 : true;
}
get selectedTable() {
return this.state.selectedTableId !== null
? this.env.pos.tables_by_id[this.state.selectedTableId]
: false;
get selectedTables() {
const tables = [];
this.state.selectedTableIds.forEach((id) => {
tables.push(this.env.pos.tables_by_id[id]);
});
return tables;
}
get nbrFloors() {
return this.env.pos.floors.length;
}
movePinch(hypot) {
const delta = hypot / this.scalehypot;
@@ -276,16 +321,25 @@ export class FloorScreen extends Component {
selectFloor(floor) {
this.state.selectedFloorId = floor.id;
this.state.floorBackground = this.activeFloor.background_color;
this.state.isEditMode = false;
this.state.selectedTableId = null;
this.state.selectedTableIds = [];
}
toggleEditMode() {
this.state.isEditMode = !this.state.isEditMode;
this.state.selectedTableId = null;
this.env.pos.toggleEditMode();
if (!this.env.pos.isEditMode && this.env.pos.floors.length > 0) {
this.addFloorRef.el.style.display = "none";
} else {
this.addFloorRef.el.style.display = "initial";
}
this.state.selectedTableIds = [];
}
async onSelectTable(table) {
if (this.state.isEditMode) {
this.state.selectedTableId = table.id;
async onSelectTable(table, ev) {
if (this.env.pos.isEditMode) {
if (ev.ctrlKey || ev.metaKey) {
this.state.selectedTableIds.push(table.id);
} else {
this.state.selectedTableIds = [];
this.state.selectedTableIds.push(table.id);
}
} else {
if (this.env.pos.orderToTransfer) {
await this.env.pos.transferTable(table);
@@ -307,26 +361,71 @@ export class FloorScreen extends Component {
async onSaveTable(table) {
await this._save(table);
}
async addFloor() {
const { confirmed, payload: newName } = await this.popup.add(TextInputPopup, {
title: this.env._t("Floor Name"),
});
if (!confirmed) {
return;
}
const floor = await this.orm.call("restaurant.floor", "create_from_ui", [newName, "#ACADAD",this.env.pos.config.id]);
this.env.pos.floors_by_id[floor.id] = floor;
this.env.pos.floors.push(floor);
this.selectFloor(floor);
this.env.pos.isEditMode = true;
}
async createTable() {
const newTable = await this._createTableHelper();
if (newTable) {
this.state.selectedTableId = newTable.id;
this.state.selectedTableIds = [];
this.state.selectedTableIds.push(newTable.id);
}
}
async duplicateTable() {
if (!this.selectedTable) {
async duplicateTableOrFloor() {
if (this.selectedTables.length == 0) {
const floor = this.activeFloor;
const tables = this.activeFloor.tables;
const newFloorName = floor.name + " (copy)";
const newFloor = await this.orm.call("restaurant.floor", "create_from_ui", [newFloorName, floor.background_color, this.env.pos.config.id]);
this.env.pos.floors_by_id[newFloor.id] = newFloor;
this.env.pos.floors.push(newFloor);
this.selectFloor(newFloor)
for (const table of tables) {
await this._createTableHelper(table, true);
}
return;
}
const newTable = await this._createTableHelper(this.selectedTable);
if (newTable) {
this.state.selectedTableId = newTable.id;
const selectedTables = this.selectedTables;
this._onDeselectTable();
for (const table of selectedTables) {
const newTable = await this._createTableHelper(table);
if (newTable) {
this.state.selectedTableIds.push(newTable.id);
}
}
}
async renameTable() {
const selectedTable = this.selectedTable;
if (!selectedTable) {
const selectedTables = this.selectedTables;
const selectedFloor = this.activeFloor;
if (selectedTables.length > 1) {
return;
}
if (selectedTables.length == 0) {
const { confirmed, payload: newName } = await this.popup.add(TextInputPopup, {
startingValue: selectedFloor.name,
title: this.env._t("Floor Name ?"),
});
if (!confirmed) {
return;
}
if (newName !== selectedFloor.name) {
selectedFloor.name = newName;
await this._renameFloor(selectedFloor.id, newName);
}
return;
}
const selectedTable = selectedTables[0];
const { confirmed, payload: newName } = await this.popup.add(TextInputPopup, {
startingValue: selectedTable.name,
title: this.env._t("Table Name?"),
@@ -340,12 +439,12 @@ export class FloorScreen extends Component {
}
}
async changeSeatsNum() {
const selectedTable = this.selectedTable;
if (!selectedTable) {
const selectedTables = this.selectedTables;
if (selectedTables.length == 0) {
return;
}
const { confirmed, payload: inputNumber } = await this.popup.add(NumberPopup, {
startingValue: selectedTable.seats,
startingValue: 0,
cheap: true,
title: this.env._t("Number of Seats?"),
isInputSelected: true,
@@ -353,24 +452,39 @@ export class FloorScreen extends Component {
if (!confirmed) {
return;
}
const newSeatsNum = parseInt(inputNumber, 10) || selectedTable.seats;
if (newSeatsNum !== selectedTable.seats) {
selectedTable.seats = newSeatsNum;
await this._save(selectedTable);
}
const newSeatsNum = parseInt(inputNumber, 10);
selectedTables.forEach(async (selectedTable) => {
if (newSeatsNum !== selectedTable.seats) {
selectedTable.seats = newSeatsNum;
await this._save(selectedTable);
}
})
}
async changeShape() {
if (!this.selectedTable) {
async changeToCircle() {
await this.changeShape("round");
}
async changeToSquare() {
await this.changeShape("square");
}
async changeShape(form) {
if (this.selectedTables.length == 0) {
return;
}
this.selectedTable.shape = this.selectedTable.shape === "square" ? "round" : "square";
this.selectedTables.forEach(async (selectedTable) => {
selectedTable.shape = form;
await this._save(selectedTable);
})
this.render();
await this._save(this.selectedTable);
}
async setTableColor(color) {
this.selectedTable.color = color;
const selectedTables = this.selectedTables;
selectedTables.forEach(async (selectedTable) => {
selectedTable.color = color;
await this._save(selectedTable);
})
this.render();
await this._save(this.selectedTable);
this.state.isColorPicker = false;
}
async setFloorColor(color) {
this.state.floorBackground = color;
@@ -378,9 +492,47 @@ export class FloorScreen extends Component {
await this.orm.write("restaurant.floor", [this.activeFloor.id], {
background_color: color,
});
this.state.isColorPicker = false;
}
async deleteTable() {
if (!this.selectedTable) {
toggleColorPicker() {
this.state.isColorPicker = !this.state.isColorPicker;
}
async deleteFloorOrTable() {
if (this.selectedTables.length == 0) {
const { confirmed } = await this.popup.add(ConfirmPopup, {
title: _.str.sprintf("Removing floor %s", this.activeFloor.name),
body: _.str.sprintf(
this.env._t("Removing a floor cannot be undone. Do you still wanna remove %s?"),
this.activeFloor.name
),
});
if (!confirmed) {
return;
}
const originalSelectedFloorId = this.activeFloor.id;
await this.orm.call("restaurant.floor", "deactivate_floor", [
originalSelectedFloorId, this.env.pos.pos_session.id
]);
const floor = this.env.pos.floors_by_id[originalSelectedFloorId];
const orderList = [...this.env.pos.get_order_list()];
for (const order of orderList) {
if (floor.table_ids.includes(order.tableId)) {
this.env.pos.removeOrder(order, false);
}
}
floor.table_ids.forEach((tableId) => {
delete this.env.pos.tables_by_id[tableId];
});
delete this.env.pos.floors_by_id[originalSelectedFloorId];
this.env.pos.floors = this.env.pos.floors.filter((floor) => floor.id != originalSelectedFloorId);
this.env.pos.TICKET_SCREEN_STATE.syncedOrders.cache = {};
if (this.env.pos.floors.length > 0) {
this.selectFloor(this.env.pos.floors[0]);
} else {
this.env.pos.isEditMode = false;
this.env.pos.floorPlanStyle = "default";
this.state.floorBackground = null;
}
return;
}
const { confirmed } = await this.popup.add(ConfirmPopup, {
@@ -390,24 +542,45 @@ export class FloorScreen extends Component {
if (!confirmed) {
return;
}
const originalSelectedTableId = this.state.selectedTableId;
this.env.pos.tables_by_id[originalSelectedTableId].active = false;
await this.orm.call("restaurant.table", "create_from_ui", [
{ active: false, id: originalSelectedTableId },
]);
this.activeFloor.tables = this.activeTables.filter(
(table) => table.id !== originalSelectedTableId
);
const originalSelectedTableIds = [...this.state.selectedTableIds];
originalSelectedTableIds.forEach(async (id) => {
const response = await this.orm.call("restaurant.table", "are_orders_still_in_draft", [
id,
]);
if (!response) {
//remove order not send to server
for (const order of this.env.pos.get_order_list()) {
if (order.tableId == id) {
this.env.pos.removeOrder(order, false);
}
}
this.env.pos.tables_by_id[id].active = false;
await this.orm.call("restaurant.table", "create_from_ui", [
{ active: false, id: id },
]);
this.activeFloor.tables = this.activeTables.filter(
(table) => table.id !== id
);
delete this.env.pos.tables_by_id[id];
} else {
await this.popup.add(ErrorPopup, {
title: this.env._t("Delete Error"),
body: this.env._t("You cannot delete a table with orders still in draft for this table."),
});
}
})
// Value of an object can change inside async function call.
// Which means that in this code block, the value of `state.selectedTableId`
// before the await call can be different after the finishing the await call.
// Since we wanted to disable the selected table after deletion, we should be
// setting the selectedTableId to null. However, we only do this if nothing
// else is selected during the rpc call.
if (this.state.selectedTableId === originalSelectedTableId) {
this.state.selectedTableId = null;
const equalsCheck = (a, b) => {
return JSON.stringify(a) === JSON.stringify(b);
}
if (equalsCheck(this.state.selectedTableIds, originalSelectedTableIds)) {
this.state.selectedTableIds = [];
}
delete this.env.pos.tables_by_id[originalSelectedTableId];
this.env.pos.TICKET_SCREEN_STATE.syncedOrders.cache = {};
}
}
@@ -6,6 +6,102 @@
position: relative;
display: flex;
flex-flow: column nowrap;
.edit-bar-top {
display: flex;
height: 48px;
align-items: center;
overflow-x: auto;
.disabled {
color: grey;
}
.edit-button {
padding: 3px;
margin: 5px 20px;
text-align: center;
flex: auto;
cursor: pointer;
display: flex;
flex-direction: column;
flex-basis: 0;
.icon-button {
font-size: 18px;
}
.text-button {
font-size: 14px;
}
&.first-button {
cursor: default;
padding-right: 10px;
text-align: right;
flex-basis: 10%;
border: none;
}
&:last-child {
cursor: pointer;
padding-right: 10px;
text-align: right;
flex-basis: 10%;
border: none;
display: flex;
flex-direction: row-reverse;
.close-edit-button {
display: flex;
flex-direction: column;
align-items: center;
}
}
}
.is-active {
background-color: $primary;
border: solid 1px white;
border-radius: 5px;
}
}
.color-picker {
display: flex;
height: 48px;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
right: 0;
top: 48px;
background-color: $o-gray-100;
.color {
float: left;
cursor: pointer;
width: 48px;
height: 48px;
}
}
.close-picker {
position: absolute;
bottom: 0;
left: 50%;
margin-left: -16px;
margin-bottom: -16px;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
font-size: 20px;
border-radius: 16px;
background: black;
color: white;
cursor: pointer;
}
}
.floor-selector {
@@ -18,7 +114,7 @@
.button {
cursor: pointer;
border-left: solid 1px $gray-400;
flex: 1;
flex-grow: 3;
&:first-child {
border-left: none;
@@ -29,6 +125,16 @@
color: white;
}
}
.button-add {
cursor: pointer;
flex-grow: 1;
background-color: transparent;
background-size: cover;
transition: all 300ms ease-in-out;
color: $primary;
border-left: none;
}
}
.floor-map {
@@ -36,7 +142,6 @@
position: relative;
width: auto;
height: 100%;
box-shadow: 0px 6px 0px -3px rgba(0,0,0,0.07) inset;
background: #D8D7D7;
background-repeat: no-repeat;
overflow: auto;
@@ -171,95 +276,6 @@
}
}
.edit-bar {
position: fixed;
top: 0;
right: 40px;
margin: 8px;
line-height: 34px;
text-align: center;
border-radius: 5px;
cursor: pointer;
font-size: 20px;
background: rgba(255,255,255,0.5);
z-index: 100;
.edit-button {
position: relative;
width: 32px;
display: inline-block;
cursor: pointer;
margin-right: -4px;
border-right: solid 1px rgba(0,0,0,0.2);
transition: all 150ms linear;
&.disabled {
cursor: default;
> * {
opacity: 0.5;
}
}
&:last-child {
margin-right: 0;
border-right: none;
}
}
.color-picker {
position: absolute;
left: 36px;
top: 40px;
width: 180px;
height: 180px;
border-radius: 3px;
z-index: 100;
.color {
display: block;
float: left;
cursor: pointer;
width: 60px;
height: 60px;
background-color: gray;
&.tl {
border-top-left-radius: 3px;
}
&.tr {
border-top-right-radius: 3px;
}
&.bl {
border-bottom-left-radius: 3px;
}
&.br {
border-bottom-right-radius: 3px;
}
}
}
.close-picker {
position: absolute;
bottom: 0;
left: 50%;
margin-left: -16px;
margin-bottom: -16px;
width: 32px;
height: 32px;
line-height: 32px;
text-align: center;
font-size: 20px;
border-radius: 16px;
background: black;
color: white;
cursor: pointer;
}
}
.empty-floor {
position: absolute;
top: 0;
@@ -3,15 +3,23 @@
<t t-name="pos_restaurant.FloorScreen" owl="1">
<div class="floor-screen screen">
<t t-if="env.pos.floors.length > 1">
<div class="floor-selector">
<t t-foreach="env.pos.floors" t-as="floor" t-key="floor.id">
<span class="button button-floor" t-att-class="{ active: floor.id === state.selectedFloorId }" t-on-click="() => this.selectFloor(floor)">
<t t-esc="floor.name" />
</span>
</t>
</div>
</t>
<EditBar t-if="env.pos.isEditMode" selectedTables="selectedTables" nbrFloors="nbrFloors"
floorMapScrollTop="state.floorMapScrollTop" isColorPicker="state.isColorPicker" toggleColorPicker.bind="toggleColorPicker"
createTable.bind="createTable" duplicateTableOrFloor.bind="duplicateTableOrFloor" renameTable.bind="renameTable"
changeSeatsNum.bind="changeSeatsNum" changeToCircle.bind="changeToCircle" changeToSquare.bind="changeToSquare"
setTableColor.bind="setTableColor" setFloorColor.bind="setFloorColor" deleteFloorOrTable.bind="deleteFloorOrTable"
toggleEditMode.bind="toggleEditMode"
/>
<div class="floor-selector">
<t t-foreach="env.pos.floors" t-as="floor" t-key="floor.id">
<span class="button button-floor" t-att-class="{ active: floor.id === state.selectedFloorId }" t-on-click="() => this.selectFloor(floor)">
<t t-esc="floor.name" />
</span>
</t>
<span class="button button-add" t-ref="add-floor-ref" t-on-click="addFloor">
ADD FLOOR
</span>
</div>
<div
t-on-click="_onDeselectTable"
@@ -21,26 +29,22 @@
class="floor-map"
t-ref="floor-map-ref"
>
<div t-if="isFloorEmpty" class="empty-floor" t-ref="map">
<span>This floor has no tables yet, use the </span>
<i class="fa fa-plus" role="img" aria-label="Add button" title="Add button"></i>
<span> button in the editing toolbar to create new tables.</span>
</div>
<div t-else="" t-ref="map">
<t t-foreach="activeTables" t-as="table" t-key="table.id">
<Table t-if="table.id !== state.selectedTableId" onClick.bind="onSelectTable" table="table" />
<EditableTable t-else="" table="table" onSaveTable.bind="onSaveTable" limit="floorMapRef" />
</t>
</div>
<span t-if="env.pos.user.role == 'manager'" class="edit-button editing" t-att-class="{ active: state.isEditMode }" t-on-click.stop="toggleEditMode"
t-attf-style="top:{{state.floorMapScrollTop}}px;">
<i class="fa fa-pencil" role="img" aria-label="Edit" title="Edit"></i>
</span>
<EditBar t-if="state.isEditMode" selectedTable="selectedTable" floorMapScrollTop="state.floorMapScrollTop"
createTable.bind="createTable" duplicateTable.bind="duplicateTable" renameTable.bind="renameTable"
changeSeatsNum.bind="changeSeatsNum" changeShape.bind="changeShape" setTableColor.bind="setTableColor"
setFloorColor.bind="setFloorColor" deleteTable.bind="deleteTable"
/>
<t t-if="env.pos.floors.length > 0">
<div t-if="isFloorEmpty" class="empty-floor" t-ref="map">
<span>No tables available,<br/>ADD your first table</span>
</div>
<div t-else="" t-ref="map">
<t t-foreach="activeTables" t-as="table" t-key="table.id">
<Table t-if="!state.selectedTableIds.includes(table.id)" onClick.bind="onSelectTable" table="table" />
<EditableTable t-else="" table="table" selectedTables="selectedTables" onSaveTable.bind="onSaveTable" limit="floorMapRef" />
</t>
</div>
</t>
<t t-else="">
<div class="empty-floor" t-ref="map">
<span>No floors available,<br/>ADD a floor to start</span>
</div>
</t>
</div>
</div>
</t>
@@ -24,16 +24,38 @@ export class Table extends Component {
get style() {
const table = this.props.table;
return `
width: ${table.width}px;
height: ${table.height}px;
line-height: ${table.height}px;
top: ${table.position_v}px;
left: ${table.position_h}px;
border-radius: ${table.shape === "round" ? 1000 : 3}px;
background: ${table.color || "rgb(53, 211, 116)"};
font-size: ${table.height >= 150 && table.width >= 150 ? 32 : 16}px;
`;
if (this.env.pos.floorPlanStyle == "kanban") {
const floor = table.floor;
const index = floor.tables.indexOf(table);
const minWidth = 100 + 20;
const nbrHorizontal = Math.floor(window.innerWidth / minWidth);
const widthTable = (window.innerWidth - nbrHorizontal*10) / nbrHorizontal;
const position_h = widthTable * (index % nbrHorizontal) + 5 + (index % nbrHorizontal) * 10;
const position_v = widthTable * Math.floor(index / nbrHorizontal) + 5 + Math.floor(index / nbrHorizontal) * 10;
return `
width: ${widthTable}px;
height: ${widthTable}px;
line-height: ${widthTable}px;
top: ${position_v}px;
left: ${position_h}px;
border-radius: ${table.shape === "round" ? 1000 : 3}px;
background: ${table.color || "rgb(53, 211, 116)"};
font-size: ${widthTable >= 150 ? 32 : 16}px;
`;
} else {
return `
width: ${table.width}px;
height: ${table.height}px;
line-height: ${table.height}px;
top: ${table.position_v}px;
left: ${table.position_h}px;
border-radius: ${table.shape === "round" ? 1000 : 3}px;
background: ${table.color || "rgb(53, 211, 116)"};
font-size: ${table.height >= 150 && table.width >= 150 ? 32 : 16}px;
`;
}
}
get fill() {
const customerCount = this.env.pos.getCustomerCount(this.props.table.id);
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.Table" owl="1">
<div class="table" t-on-click.stop="() => props.onClick(props.table)" t-att-style="style">
<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" />
@@ -14,15 +14,21 @@ patch(Navbar.prototype, "pos_restaurant.Navbar", {
if (!this.env.pos || !this.env.pos.config) {
return 0;
}
if (this.env.pos.config.iface_floorplan && this.env.pos.table) {
if (this.env.pos.config.is_table_management && this.env.pos.table) {
return this.env.pos.getTableOrders(this.env.pos.table.id).length;
}
return this._super(...arguments);
},
_shouldLoadOrders() {
return (this._super() || (
this.env.pos.config.iface_floorplan &&
this.env.pos.config.is_table_management &&
!this.env.pos.table
));
},
onSwitchButtonClick() {
this.env.pos.floorPlanStyle = this.env.pos.floorPlanStyle == "kanban" ? "default" : "kanban";
},
toggleEditMode() {
this.env.pos.toggleEditMode();
}
});
@@ -2,6 +2,14 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.Navbar" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension">
<xpath expr="//div[hasclass('end-nav-button')]" position="before">
<li t-if="pos.mainScreen.component.name == 'FloorScreen'" class="menu-item navbar-button edit-button" t-on-click="toggleEditMode">
<span>Edit Plan</span>
</li>
<li t-if="pos.mainScreen.component.name == 'FloorScreen'" class="menu-item navbar-button" t-on-click="onSwitchButtonClick">
<span>Switch Floor View</span>
</li>
</xpath>
<xpath expr="//div[hasclass('status-buttons')]" position="before">
<BackToFloorButton t-if="pos.mainScreen.component.showBackToFloorButton"/>
</xpath>
@@ -27,7 +27,7 @@ patch(PosStore.prototype, "pos_restaurant.PosStore", {
setup() {
this._super(...arguments);
this.globalState.ready.then(() => {
if (this.globalState.config.iface_floorplan) {
if (this.globalState.config.is_table_management) {
this.setActivityListeners();
}
});
@@ -63,7 +63,7 @@ patch(PosStore.prototype, "pos_restaurant.PosStore", {
this.mainScreen.component === PaymentScreen &&
this.globalState.get_order().paymentlines.length > 0;
return (
this.globalState.config.iface_floorplan &&
this.globalState.config.is_table_management &&
!stayPaymentScreen &&
this.mainScreen.component !== FloorScreen
);
@@ -76,7 +76,7 @@ patch(PosStore.prototype, "pos_restaurant.PosStore", {
this.setIdleTimer();
},
closeScreen() {
if (this.globalState.config.iface_floorplan && !this.globalState.get_order()) {
if (this.globalState.config.is_table_management && !this.globalState.get_order()) {
return this.showScreen("FloorScreen");
}
return this._super(...arguments);
@@ -10,7 +10,7 @@ patch(Chrome.prototype, "pos_restaurant.Chrome", {
* `FloorScreen` is the start screen if there are floors.
*/
get startScreen() {
if (this.env.pos.config.iface_floorplan) {
if (this.env.pos.config.is_table_management) {
const table = this.env.pos.table;
return { name: "FloorScreen", props: { floor: table ? table.floor : null } };
} else {
@@ -20,6 +20,6 @@ export class TransferOrderButton extends Component {
ProductScreen.addControlButton({
component: TransferOrderButton,
condition: function () {
return this.env.pos.config.iface_floorplan;
return this.env.pos.config.is_table_management;
},
});
@@ -23,12 +23,12 @@ patch(ReceiptScreen.prototype, "pos_restaurant.ReceiptScreen", {
},
//@override
_addNewOrder() {
if (!this.env.pos.config.iface_floorplan) {
if (!this.env.pos.config.is_table_management) {
this._super(...arguments);
}
},
isResumeVisible() {
if (this.env.pos.config.iface_floorplan &&
if (this.env.pos.config.is_table_management &&
this.env.pos.table) {
return this.env.pos.getTableOrders(this.env.pos.table.id).length > 1;
}
@@ -36,7 +36,7 @@ patch(ReceiptScreen.prototype, "pos_restaurant.ReceiptScreen", {
},
//@override
get nextScreen() {
if (this.env.pos.config.iface_floorplan) {
if (this.env.pos.config.is_table_management) {
const table = this.env.pos.table;
return { name: "FloorScreen", props: { floor: table ? table.floor : null } };
} else {
@@ -22,7 +22,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
},
//@override
_getSearchFields() {
if (!this.env.pos.config.iface_floorplan) {
if (!this.env.pos.config.is_table_management) {
return this._super(...arguments);
}
return Object.assign({}, this._super(...arguments), {
@@ -34,7 +34,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
});
},
async _setOrder(order) {
if (!this.env.pos.config.iface_floorplan || this.env.pos.table) {
if (!this.env.pos.config.is_table_management || this.env.pos.table) {
this._super(order);
} else {
// we came from the FloorScreen
@@ -44,7 +44,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
}
},
get allowNewOrders() {
return this.env.pos.config.iface_floorplan
return this.env.pos.config.is_table_management
? Boolean(this.env.pos.table)
: this._super(...arguments);
},
@@ -73,14 +73,14 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
},
//@override
_selectNextOrder(currentOrder) {
if (this.env.pos.config.iface_floorplan && this.env.pos.table) {
if (this.env.pos.config.is_table_management && this.env.pos.table) {
return this._super(...arguments);
}
},
//@override
async onDeleteOrder(order) {
const _super = this._super;
if (this.env.pos.config.iface_floorplan) {
if (this.env.pos.config.is_table_management) {
this.env.pos.setOrderToRemove(order);
if (!this.env.pos.table) {
try {
@@ -139,7 +139,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
},
async onDoRefund() {
const order = this.getSelectedSyncedOrder();
if (this.env.pos.config.iface_floorplan && order) {
if (this.env.pos.config.is_table_management && order) {
this.env.pos.setTable(
order.table ? order.table : Object.values(this.env.pos.tables_by_id)[0]
);
@@ -147,7 +147,7 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
this._super(...arguments);
},
isDefaultOrderEmpty(order) {
if (this.env.pos.config.iface_floorplan) {
if (this.env.pos.config.is_table_management) {
return false;
}
return this._super(...arguments);
@@ -97,14 +97,14 @@ export class TipScreen extends Component {
}
goNextScreen() {
this.env.pos.removeOrder(this.currentOrder);
if (!this.env.pos.config.iface_floorplan) {
if (!this.env.pos.config.is_table_management) {
this.env.pos.add_new_order();
}
const { name, props } = this.nextScreen;
this.pos.showScreen(name, props);
}
get nextScreen() {
if (this.env.pos.config.module_pos_restaurant && this.env.pos.config.iface_floorplan) {
if (this.env.pos.config.module_pos_restaurant && this.env.pos.config.is_table_management) {
const table = this.env.pos.table;
return { name: "FloorScreen", props: { floor: table ? table.floor : null } };
} else {
+23 -21
View File
@@ -16,6 +16,8 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
this.orderToTransfer = null; // table transfer feature
this.transferredOrdersSet = new Set(); // used to know which orders has been transferred but not sent to the back end yet
this.printers_category_ids_set = new Set();
this.floorPlanStyle = "default";
this.isEditMode = false;
},
//@override
async _processData(loadedData) {
@@ -31,7 +33,7 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
//@override
async after_load_server_data() {
var res = await this._super(...arguments);
if (this.config.iface_floorplan) {
if (this.config.is_table_management) {
this.table = null;
}
return res;
@@ -40,7 +42,7 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
// if we have tables, we do not load a default order, as the default order will be
// set when the user selects a table.
set_start_order() {
if (!this.config.iface_floorplan) {
if (!this.config.is_table_management) {
this._super(...arguments);
}
},
@@ -131,7 +133,7 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
this._replaceOrders(tableOrders, ordersJsons);
},
async _getOrdersJson() {
if (this.config.iface_floorplan) {
if (this.config.is_table_management) {
const tableIds = [].concat(
...this.floors.map((floor) => floor.tables.map((table) => table.id))
);
@@ -175,16 +177,13 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
loadRestaurantFloor() {
// we do this in the front end due to the circular/recursive reference needed
// Ignore floorplan features if no floor specified.
this.config.iface_floorplan = !!(this.floors && this.floors.length > 0);
if (this.config.iface_floorplan) {
this.floors_by_id = {};
this.tables_by_id = {};
for (const floor of this.floors) {
this.floors_by_id[floor.id] = floor;
for (const table of floor.tables) {
this.tables_by_id[table.id] = table;
table.floor = floor;
}
this.floors_by_id = {};
this.tables_by_id = {};
for (const floor of this.floors) {
this.floors_by_id[floor.id] = floor;
for (const table of floor.tables) {
this.tables_by_id[table.id] = table;
table.floor = floor;
}
}
},
@@ -244,8 +243,11 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
return new Printer(url, this);
},
isOpenOrderShareable() {
return this._super(...arguments) || this.config.iface_floorplan;
return this._super(...arguments) || this.config.is_table_management;
},
toggleEditMode() {
this.isEditMode = !this.isEditMode;
}
});
// New orders are now associated with the current table, if any.
@@ -254,7 +256,7 @@ patch(Order.prototype, "pos_restaurant.Order", {
this._super(...arguments);
if (this.pos.config.module_pos_restaurant) {
if (
this.pos.config.iface_floorplan &&
this.pos.config.is_table_management &&
!this.tableId &&
!options.json &&
this.pos.table
@@ -278,7 +280,7 @@ patch(Order.prototype, "pos_restaurant.Order", {
export_as_JSON() {
const json = this._super(...arguments);
if (this.pos.config.module_pos_restaurant) {
if (this.pos.config.iface_floorplan) {
if (this.pos.config.is_table_management) {
json.table_id = this.tableId;
}
json.customer_count = this.customerCount;
@@ -294,7 +296,7 @@ patch(Order.prototype, "pos_restaurant.Order", {
init_from_JSON(json) {
this._super(...arguments);
if (this.pos.config.module_pos_restaurant) {
if (this.pos.config.iface_floorplan) {
if (this.pos.config.is_table_management) {
this.tableId = json.table_id;
this.validation_date = moment.utc(json.creation_date).local().toDate();
}
@@ -323,7 +325,7 @@ patch(Order.prototype, "pos_restaurant.Order", {
export_for_printing() {
const json = this._super(...arguments);
if (this.pos.config.module_pos_restaurant) {
if (this.pos.config.iface_floorplan && this.getTable()) {
if (this.pos.config.is_table_management && this.getTable()) {
json.table = this.getTable().name;
}
json.customer_count = this.getCustomerCount();
@@ -410,7 +412,7 @@ patch(Order.prototype, "pos_restaurant.Order", {
this.customerCount = Math.max(count, 0);
},
getTable() {
if (this.pos.config.iface_floorplan) {
if (this.pos.config.is_table_management) {
return this.pos.tables_by_id[this.tableId];
}
return null;
@@ -496,8 +498,8 @@ patch(Order.prototype, "pos_restaurant.Order", {
const printingChanges = {
new: changes["new"],
cancelled: changes["cancelled"],
table_name: this.pos.config.iface_floorplan ? this.getTable().name : false,
floor_name: this.pos.config.iface_floorplan
table_name: this.pos.config.is_table_management ? this.getTable().name : false,
floor_name: this.pos.config.is_table_management
? this.getTable().floor.name
: false,
name: this.name || "unknown order",
@@ -3,11 +3,11 @@
<t t-name="TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('header-row')]//div[@name='delete']" position="before">
<div t-if="env.pos.config.iface_floorplan" class="col" name="table">Table</div>
<div t-if="env.pos.config.is_table_management" class="col" name="table">Table</div>
<div t-if="_state.ui.filter == 'TIPPING'" class="col end narrow" name="tip">Tip</div>
</xpath>
<xpath expr="//div[hasclass('order-row')]//div[@name='delete']" position="before">
<div t-if="env.pos.config.iface_floorplan" class="col" name="table">
<div t-if="env.pos.config.is_table_management" class="col" name="table">
<t t-if="order.tableId">
<div t-if="env.isMobile">Table</div>
<div><t t-esc="getTable(order)"></t></div>
@@ -24,44 +24,52 @@ FloorScreen.check.hasTable("1");
// clicking table in active mode does not open product screen
// instead, table is selected
FloorScreen.do.clickEdit();
FloorScreen.check.editModeIsActive(true);
FloorScreen.do.clickTable("3");
FloorScreen.check.selectedTableIs("3");
FloorScreen.do.clickTable("1");
FloorScreen.check.selectedTableIs("1");
// switching floor in edit mode deactivates edit mode
FloorScreen.do.clickFloor("Main Floor");
FloorScreen.check.editModeIsActive(false);
FloorScreen.do.clickEdit();
FloorScreen.check.editModeIsActive(true);
// test add table
FloorScreen.do.clickFloor("Main Floor");
FloorScreen.do.clickAddTable();
FloorScreen.check.selectedTableIs("1");
FloorScreen.do.clickRename();
TextInputPopup.check.isShown();
TextInputPopup.do.inputText("100");
TextInputPopup.do.clickConfirm();
FloorScreen.do.clickTable("100");
FloorScreen.check.selectedTableIs("100");
// test duplicate table
FloorScreen.do.clickDuplicate();
// new table is already named 101
FloorScreen.check.selectedTableIs("101");
// the name is the first number available on the floor
FloorScreen.check.selectedTableIs("1");
FloorScreen.do.clickRename();
TextInputPopup.check.isShown();
TextInputPopup.do.inputText("1111");
TextInputPopup.do.clickConfirm();
FloorScreen.do.clickTable("1111");
FloorScreen.check.selectedTableIs("1111");
// switch floor, switch back and check if
// the new tables are still there
FloorScreen.do.clickFloor("Second Floor");
FloorScreen.check.editModeIsActive(false);
FloorScreen.check.hasTable("3");
FloorScreen.check.hasTable("1");
//test duplicate multiple tables
FloorScreen.do.clickTable("1");
FloorScreen.check.selectedTableIs("1");
FloorScreen.do.ctrlClickTable("3");
FloorScreen.check.selectedTableIs("3");
FloorScreen.do.clickDuplicate();
FloorScreen.check.selectedTableIs("2");
FloorScreen.check.selectedTableIs("4");
//test delete multiple tables
FloorScreen.do.clickTrash();
Chrome.do.confirmPopup();
FloorScreen.do.clickFloor("Main Floor");
FloorScreen.check.hasTable("2");
FloorScreen.check.hasTable("4");
@@ -70,8 +78,6 @@ FloorScreen.check.hasTable("100");
FloorScreen.check.hasTable("1111");
// test delete table
FloorScreen.do.clickEdit();
FloorScreen.check.editModeIsActive(true);
FloorScreen.do.clickTable("2");
FloorScreen.check.selectedTableIs("2");
FloorScreen.do.clickTrash();
@@ -96,11 +102,11 @@ NumberPopup.do.clickConfirm();
FloorScreen.check.tableSeatIs("4", "15");
// change shape
FloorScreen.do.clickTable("4");
FloorScreen.do.changeShapeTo("round");
// Opening product screen in main floor should go back to main floor
FloorScreen.do.clickEdit();
FloorScreen.check.editModeIsActive(false);
FloorScreen.do.closeEdit();
FloorScreen.check.tableIsNotSelected("4");
FloorScreen.do.clickTable("4");
ProductScreen.check.isShown();
@@ -43,6 +43,7 @@ ProductScreen.do.clickOrderline("Water", "3.0");
ProductScreen.do.clickOrderline("Coca-Cola", "1.0");
// go back to the original order and see if the order is changed
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0001");
ProductScreen.check.isShown();
@@ -68,11 +69,13 @@ SplitBillScreen.do.clickOrderline("Coca-Cola");
SplitBillScreen.check.orderlineHas("Coca-Cola", "1", "1");
SplitBillScreen.do.clickPay();
PaymentScreen.do.clickBack();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0002");
ProductScreen.do.clickOrderline("Water", "1.0");
ProductScreen.do.clickOrderline("Coca-Cola", "1.0");
ProductScreen.check.totalAmountIs("4.00");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectOrder("-0001");
ProductScreen.do.clickOrderline("Minute Maid", "1.0");
@@ -11,6 +11,7 @@ startSteps();
ProductScreen.do.confirmOpeningPopup();
// New Ticket button should not be in the ticket screen if no table is selected.
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.noNewTicketButton();
TicketScreen.do.clickDiscard();
@@ -18,6 +19,7 @@ TicketScreen.do.clickDiscard();
// Deleting the last order in the table brings back to floorscreen
FloorScreen.do.clickTable("4");
ProductScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains(2, "-0001");
TicketScreen.do.deleteOrder("-0001");
@@ -28,12 +30,14 @@ TicketScreen.do.clickDiscard();
FloorScreen.do.clickTable("2");
ProductScreen.exec.addOrderline("Minute Maid", "1", "2");
ProductScreen.check.totalAmountIs("2.0");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.exec.addOrderline("Coca-Cola", "2", "2");
ProductScreen.check.totalAmountIs("4.0");
Chrome.do.backToFloor();
FloorScreen.check.orderCountSyncedInTableIs("2", "2");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder("-0003");
Chrome.do.confirmPopup();
@@ -49,6 +53,7 @@ ProductScreen.exec.addOrderline("Minute Maid", "1", "3");
ProductScreen.check.totalAmountIs("3.0");
Chrome.do.backToFloor();
FloorScreen.check.orderCountSyncedInTableIs("5", "1");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder("-0004");
Chrome.do.confirmPopup();
@@ -27,6 +27,7 @@ ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod("Bank");
PaymentScreen.do.clickValidate();
TipScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
// order 2
@@ -34,6 +35,7 @@ ProductScreen.exec.addOrderline("Coca-Cola", "2", "2");
ProductScreen.check.totalAmountIs("4.0");
Chrome.do.backToFloor();
FloorScreen.check.orderCountSyncedInTableIs("2", "1");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains("2", "Tipping");
TicketScreen.do.clickDiscard();
@@ -47,6 +49,7 @@ ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod("Bank");
PaymentScreen.do.clickValidate();
TipScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
// order 4
@@ -54,6 +57,7 @@ ProductScreen.exec.addOrderline("Coca-Cola", "4", "2");
ProductScreen.check.totalAmountIs("8.0");
Chrome.do.backToFloor();
FloorScreen.check.orderCountSyncedInTableIs("5", "1");
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.check.nthRowContains("4", "Tipping");
@@ -68,6 +72,7 @@ TipScreen.do.clickPercentTip("20%");
TipScreen.check.inputAmountIs("0.40");
Chrome.do.backToFloor();
FloorScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
// Tip 25% on order3
@@ -81,6 +86,7 @@ TipScreen.do.clickPercentTip("25%");
TipScreen.check.inputAmountIs("1.50");
Chrome.do.backToFloor();
FloorScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
// finalize order 4 then tip custom amount
@@ -101,6 +107,7 @@ Chrome.do.backToFloor();
FloorScreen.check.isShown();
// settle tips here
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.selectFilter("Tipping");
TicketScreen.check.tipContains("1.00");
@@ -21,9 +21,13 @@ class Do {
}
clickEdit() {
return [
{
content: 'Click Menu button',
trigger: '.menu-button',
},
{
content: `click edit button`,
trigger: `.floor-map .edit-button`,
trigger: `.edit-button`,
},
];
}
@@ -31,7 +35,7 @@ class Do {
return [
{
content: "add table",
trigger: `.floor-map .edit-button i[aria-label=Add]`,
trigger: `.edit-button i[aria-label=Add]`,
},
];
}
@@ -39,7 +43,7 @@ class Do {
return [
{
content: "duplicate table",
trigger: `.floor-map .edit-button i[aria-label=Duplicate]`,
trigger: `.edit-button i[aria-label=Copy]`,
},
];
}
@@ -47,7 +51,7 @@ class Do {
return [
{
content: "rename table",
trigger: `.floor-map .edit-button i[aria-label=Rename]`,
trigger: `.edit-button i[aria-label=Rename]`,
},
];
}
@@ -55,7 +59,7 @@ class Do {
return [
{
content: "change number of seats",
trigger: `.floor-map .edit-button i[aria-label=Seats]`,
trigger: `.edit-button i[aria-label=Seats]`,
},
];
}
@@ -63,15 +67,35 @@ class Do {
return [
{
content: "trash table",
trigger: `.floor-map .edit-button.trash`,
trigger: `.edit-button.trash`,
},
];
}
closeEdit() {
return [
{
content: 'Close edit mode',
trigger: '.edit-button .close-edit-button',
}
];
}
changeShapeTo(shape) {
return [
{
content: `change shape to '${shape}'`,
trigger: `.edit-button .button-option${shape === "round" ? ".square" : ".round"}`,
trigger: `.edit-button .button-option${shape === "round" ? ".round" : ".square"}`,
},
];
}
ctrlClickTable(name) {
return [
{
content: `ctrl click table '${name}'`,
trigger: `.floor-map .table .label:contains("${name}")`,
run() {
const el = this.$anchor[0];
el.dispatchEvent(new MouseEvent("click", {bubbles: true, ctrlKey: true}));
}
},
];
}
@@ -105,15 +129,6 @@ class Check {
},
];
}
editModeIsActive(flag) {
return [
{
content: `check if edit mode is ${flag ? "active" : "inactive"}`,
trigger: `.floor-map .edit-button${flag ? ".active" : ":not(:has(.active))"}`,
run: () => {},
},
];
}
tableSeatIs(table, val) {
return [
{
@@ -22,6 +22,7 @@ ProductScreen.check.selectedOrderlineHas("Water");
ProductScreen.check.totalAmountIs("4.40");
// Create 2nd order (paid)
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.do.clickDisplayedProduct("Coca-Cola");
@@ -41,6 +42,7 @@ FloorScreen.do.clickTable("5");
ProductScreen.check.totalAmountIs("4.40");
// Create another draft order and go back to floor
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.do.clickDisplayedProduct("Coca-Cola");
@@ -55,6 +57,7 @@ FloorScreen.check.orderCountSyncedInTableIs("5", "2");
// Delete the first order then go back to floor
FloorScreen.do.clickTable("5");
ProductScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder("-0001");
Chrome.do.confirmPopup();
@@ -21,6 +21,7 @@ ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
ReceiptScreen.check.isShown();
Chrome.do.clickMenuButton();
Chrome.do.clickTicketButton();
registry.category("web_tour.tours").add('PosSettleOrder', { test: true, url: '/pos/ui', steps: getSteps() });
+1 -1
View File
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_six.Navbar" t-inherit="point_of_sale.Navbar" t-inherit-mode="extension" owl="1">
<xpath expr="//SyncNotification" position="after">
<xpath expr="//div[hasclass('end-nav-button')]" position="before">
<BalanceButton t-if="this.env.pos.payment_methods.some((pm) => pm.use_payment_terminal === 'six')" />
</xpath>
</t>
@@ -2,11 +2,11 @@
<templates id="template" xml:space="preserve">
<t t-name="BalanceButton" owl="1">
<div class="header-button balance-button" t-on-click="sendBalance">
<span class="lock-button">
<li class="menu-item navbar-button lock-button" t-on-click="sendBalance">
<span>
Send Balance
</span>
</div>
</li>
</t>
</templates>