[FIX] point_of_sale,pos_restaurant: ui issues

In this PR we fix a number of small issues related to the pos ui.

In addition to the specific fixes, this PR also simplifies many
of the components, most notably the `category_selector`.

In the interest of providing a cleaner API, this PR introduces
a new folder called `generic_components` whose goal is to contain
stateless components. These stateless components would allow for more
reusability, while simplifying the logic. The `category_selector` is
the first component in this folder.

Descriptions of each of the issues fixed in this PR can be found in the

closes odoo/odoo#131038

Tasks: 3457213
Related: odoo/enterprise#45600
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
This commit is contained in:
vlst
2023-08-10 17:02:03 +02:00
parent 98218629f8
commit 8f3971a780
38 changed files with 257 additions and 287 deletions
@@ -10,7 +10,7 @@
</t>
<t t-name="l10n_fr_pos_cert.OrderLinesReceipt" t-inherit="point_of_sale.OrderLinesReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-foreach='receipt.orderlines']" position="inside">
<xpath expr="//div[@t-foreach='receipt.orderlines']" position="inside">
<t t-if="receipt.l10n_fr_hash !== false and line.price_type === 'manual'">
<div class="pos-receipt-right-padding">
Old unit price:
@@ -18,7 +18,7 @@
</t>
<t t-name="l10n_in_pos.OrderLinesReceipt" t-inherit="point_of_sale.OrderLinesReceipt" t-inherit-mode="extension" owl="1">
<xpath expr="//t[@t-foreach='receipt.orderlines']" position="inside">
<xpath expr="//div[@t-foreach='receipt.orderlines']" position="inside">
<t t-if="line.l10n_in_hsn_code and pos.company.country and pos.company.country.code == 'IN'">
<div class="pos-receipt-left-padding">
<span>HSN Code: </span>
@@ -0,0 +1,30 @@
/** @odoo-module */
import { Component } from "@odoo/owl";
/**
* @typedef {Object} Category
* @property {number} id
* @property {string?} name
* @property {string?} icon
* @property {string?} separator
* @property {string?} imageUrl
*/
export class CategorySelector extends Component {
static template = "point_of_sale.CategorySelector";
static props = {
categories: {
type: Array,
element: Object,
shape: {
id: Number,
name: { type: String, optional: true },
icon: { type: String, optional: true },
separator: { type: String, optional: true },
imageUrl: { type: String, optional: true },
},
},
class: { type: String, optional: true },
onClick: { type: Function },
};
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CategorySelector" owl="1">
<div t-attf-class="{{props.class or ''}} d-flex overflow-auto">
<div t-foreach="props.categories" t-as="category" t-key="category.id"
class="d-flex align-items-center">
<i t-if="category.separator"
t-attf-class="px-2 fa {{category.separator}}"
aria-hidden="true"/>
<button class="category-button btn btn-light h-100 fw-bolder"
t-on-click="() => props.onClick(category.id)">
<div t-if="category.imageUrl"
class="category-img">
<img t-att-src="category.imageUrl" alt="Category" />
</div>
<i t-if="category.icon"
t-attf-class="fa {{category.icon}}"
aria-hidden="true"/>
<t t-esc="category.name" />
</button>
</div>
</div>
</t>
</templates>
@@ -6,7 +6,7 @@
class="order-button floor-button d-flex align-items-center gap-2 px-4 fw-bolder cursor-pointer"
t-on-click="backToFloorScreen"
>
<i class="fa fa-2x fa-angle-left" role="img" aria-label="Go Back" title="Go Back" />
<i class="fa fa-2x fa-angle-left pb-1" role="img" aria-label="Go Back" title="Go Back" />
<span t-if="!ui.isSmall">BACK</span>
</span>
</t>
@@ -6,27 +6,20 @@
margin: 0;
background-color: #f0eeee;
color: $gray-700;
/* Some elements inside .pos are allowed to be moved/resized. When
* this 'move/resize' event is performed in touch devices, .pos
* element also tries to scroll (as the default action of touchstart+
* touchmove events).
* Position is fixed to prevent the movement of .pos element during
* the described touch events.
*/
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
text-shadow: none;
}
/* ********* Generic element styling ********* */
.dvh-100 {
// we specify the 100vh as a fallback in case the browser does not
// know dvh
height: 100vh !important;
height: 100dvh !important;
}
.pos a {
text-decoration: none;
color: $gray-700;
@@ -59,9 +52,6 @@ td {
padding: 0px;
}
.oe_hidden{
display: none !important;
}
.oe_invisible{
visibility: hidden !important;
}
@@ -2,10 +2,10 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.Chrome" owl="1">
<div class="pos vh-100 d-flex flex-column">
<div class="pos dvh-100 d-flex flex-column">
<div class="pos-receipt-print"></div>
<Navbar />
<div class="pos-content w-100 h-100 bg-200 overflow-auto">
<div class="pos-content flex-grow-1 overflow-auto w-100 bg-200">
<!-- FIXME POSREF: better error handling in main screens (currently, a crash in owl lifecycle of a main screen blows up the application and the error can't be displayed) -->
<t isShown="!pos.tempScreen" t-component="pos.mainScreen.component"
t-props="pos.mainScreen.props"/>
@@ -122,7 +122,7 @@ export class PartnerDetailsEdit extends Component {
}
/**
* Loading image is converted to a Promise to allow await when
* loading an image. It resolves to the loaded image if succesful,
* loading an image. It resolves to the loaded image if successful,
* else, resolves to false.
*
* [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await)
@@ -13,7 +13,7 @@
</t>
<input type="file" class="image-uploader" t-on-change="uploadImage" />
</div>
<input class="detail partner-name form-control form-control-lg" name="name" t-att-value="props.partner.name or ''" placeholder="Name" t-on-change="captureChange" />
<input class="detail partner-name form-control form-control-lg" t-model="changes.name" name="name" placeholder="Name" />
</div>
<div class="partner-details-box row row-cols-1 row-cols-sm-2 gy-3 mt-3">
<t t-foreach="['Street', 'City', 'Zip', 'Email', 'Phone', 'Mobile', 'Barcode' ]" t-as="item" t-key="item">
@@ -3,7 +3,7 @@
<t t-name="point_of_sale.PaymentScreen" owl="1">
<t t-if="ui.isSmall">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
<PaymentScreenStatus order="currentOrder" />
<t t-call="point_of_sale.PaymentScreenMethods" />
<t t-call="point_of_sale.PaymentScreenButtons" />
@@ -11,7 +11,7 @@
</div>
</t>
<t t-else="">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="payment-screen screen d-flex flex-column h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
<t t-call="point_of_sale.PaymentScreenTop" />
<div class="main-content d-flex overflow-auto h-100">
<div class="left-content d-flex flex-column w-25 bg-400">
@@ -24,4 +24,7 @@ export class ActionpadWidget extends Component {
get highlightPay() {
return this.pos.get_order()?.orderlines?.length;
}
getMainButtonClasses() {
return "button btn d-flex flex-column flex-fill align-items-center justify-content-center fw-bolder btn-lg py-5 rounded-0";
}
}
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ActionpadWidget" owl="1">
<div class="actionpad d-flex flex-column flex-grow-1 mw-50 p-0 border-end">
<div class="actionpad d-flex flex-column flex-grow-1 p-0 border-end">
<button class="button mobile-more-button btn btn-secondary flex-fill border-bottom" t-if="ui.isSmall and props.onClickMore" t-on-click="props.onClickMore">
<span>More...</span>
</button>
@@ -20,10 +20,11 @@
</div>
</div>
</button>
<button class="button pay validation pay-order-button btn btn-primary btn-lg py-5 rounded-0 highlight d-flex flex-column flex-fill align-items-center justify-content-center fw-bolder"
<button class="pay validation pay-order-button btn-primary"
t-attf-class="{{getMainButtonClasses()}}"
t-att-class="{ 'with-more-button': props.onClickMore and ui.isSmall }"
t-on-click="props.actionToTrigger ? this.props.actionToTrigger : () => pos.get_order().pay()">
<div class="pay-circle d-flex align-items-center justify-content-center py-2 mb-2" t-att-class="{ 'highlight': this.highlightPay }">
<div class="pay-circle d-flex align-items-center justify-content-center py-2 mb-2">
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<t class="text-white" t-esc="props.actionName" />
@@ -1,12 +0,0 @@
/** @odoo-module */
import { Component } from "@odoo/owl";
export class CategoryButton extends Component {
static template = "point_of_sale.CategoryButton";
get imageUrl() {
const category = this.props.category;
return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`;
}
}
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.CategoryButton" owl="1">
<button class="category-button btn btn-light" t-on-click="() => this.props.switchCategory(props.category.id)">
<div class="category-img">
<img t-att-src="imageUrl" alt="Category" />
</div>
<div class="category-name mt-2 fw-bolder">
<t t-esc="props.category.name" />
</div>
</button>
</t>
</templates>
@@ -1,6 +1,5 @@
.numpad {
grid-template-columns: repeat(4, 1fr);
min-width: 216px;
gap: 1px;
}
@@ -2,35 +2,35 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.NumpadWidget" owl="1">
<div class="numpad d-grid text-center flex-grow-1 w-50 bg-300">
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('1')">1</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('2')">2</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('3')">3</button>
<div class="numpad d-grid text-center flex-grow-1 bg-300">
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('1')">1</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('2')">2</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('3')">3</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{'selected-mode active': props.activeMode === 'quantity'}"
t-on-click="() => this.changeMode('quantity')">Qty</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('4')">4</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('5')">5</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('6')">6</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('4')">4</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('5')">5</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('6')">6</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder text-nowrap" t-att-class="{
'selected-mode active': props.activeMode === 'discount',
'disabled-mode': !hasManualDiscount
}"
t-att-disabled="!hasManualDiscount"
t-on-click="() => this.changeMode('discount')">% Disc</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('7')">7</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('8')">8</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('9')">9</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('7')">7</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('8')">8</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('9')">9</button>
<button class="mode-button btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{
'selected-mode active': props.activeMode === 'price',
'disabled-mode': !hasPriceControlRights
}" t-att-disabled="!hasPriceControlRights"
t-on-click="() => this.changeMode('price')">Price</button>
<button class="input-button numpad-minus btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{ 'disabled-mode': props.disableSign }" t-on-click="() => this.sendInput('-')">+/-</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('0')">0</button>
<button class="input-button number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput(decimalSeparator)">
<button class="input-button px-4 numpad-minus btn btn-light w-100 rounded-0 py-3 fw-bolder" t-att-class="{ 'disabled-mode': props.disableSign }" t-on-click="() => this.sendInput('-')">+/-</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('0')">0</button>
<button class="input-button px-4 number-char btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput(decimalSeparator)">
<t t-esc="decimalSeparator" />
</button>
<button class="input-button numpad-backspace btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('Backspace')">
<button class="input-button px-4 numpad-backspace btn btn-light w-100 rounded-0 py-3 fw-bolder" t-on-click="() => this.sendInput('Backspace')">
<img style="pointer-events: none;" src="/point_of_sale/static/src/img/backspace.png"
width="24" height="21" alt="Backspace" />
</button>
@@ -2,14 +2,14 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductItem" owl="1">
<article t-attf-class="product position-relative btn btn-light d-flex flex-column align-items-stretch p-0 m-0 text-start cursor-pointer overflow-hidden transition-base" tabindex="0" t-on-keypress="spaceClickProduct"
<article t-attf-class="product btn btn-light d-flex flex-column align-items-stretch p-0 m-0 text-start cursor-pointer overflow-hidden transition-base" tabindex="0" t-on-keypress="spaceClickProduct"
t-on-click="() => this.pos.addProductToCurrentOrder(props.product)"
t-att-data-product-id="props.product.id"
t-attf-aria-labelledby="article_product_{{props.product.id}}">
<div class="product-img">
<div class="product-img flex-grow-1">
<img t-if="props.product.image_128" class="w-100" t-att-src="imageUrl" t-att-alt="props.product.display_name" />
</div>
<div class="product-content d-flex flex-column justify-content-between h-100 mx-2 py-1">
<div class="product-content d-flex flex-column justify-content-between mx-2 py-1">
<div t-attf-class="overflow-hidden lh-sm fw-bolder #{props.product.image_128 ? 'product-name' : 'product-name no-image' }" t-attf-id="article_product_{{props.product.id}}">
<t t-esc="props.product.display_name" />
</div>
@@ -2,14 +2,12 @@
import { debounce } from "@web/core/utils/timing";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { CategoryButton } from "@point_of_sale/app/screens/product_screen/category_button/category_button";
import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { CategorySelector } from "@point_of_sale/app/generic_components/category_selector/category_selector";
export class ProductsWidgetControlPanel extends Component {
static components = { CategoryButton };
static components = { CategorySelector };
static template = "point_of_sale.ProductsWidgetControlPanel";
setup() {
@@ -30,6 +28,43 @@ export class ProductsWidgetControlPanel extends Component {
window.removeEventListener("resize", toggleIsMobile);
});
}
getCategoryImageUrl(category) {
return `/web/image?model=pos.category&field=image_128&id=${category.id}&unique=${category.write_date}`;
}
/**
* @param {Object} category - the object from `this.pos.db.category_by_id`
* @returns {import("@point_of_sale/app/generic_components/category_selector/category_selector").Category}
*/
formatCategoryObject(category) {
const isRootCategory = category.id === this.pos.db.root_category_id;
const hasSeparator =
!isRootCategory &&
[
...this.pos.db.get_category_ancestors_ids(this.pos.selectedCategoryId),
this.pos.selectedCategoryId,
].includes(category.id);
return {
id: category.id,
name: !isRootCategory ? category.name : "",
icon: isRootCategory ? "fa-home fa-2x" : "",
separator: hasSeparator ? "fa-caret-right" : "",
imageUrl: category?.has_image && this.getCategoryImageUrl(category),
};
}
/**
* @returns {import("@point_of_sale/app/generic_components/category_selector/category_selector").Category[]}
*/
getCategories() {
return [
...this.pos.db.get_category_ancestors_ids(this.pos.selectedCategoryId),
this.pos.selectedCategoryId,
...this.pos.db.get_category_childs_ids(this.pos.selectedCategoryId),
]
.map((id) => this.pos.db.category_by_id[id])
.map((category) => this.formatCategoryObject(category));
}
toggleIsMobile() {
// In addition to the UI service we need to check the width of the window
// because the search bar glitches on a specific width between when the
@@ -1,8 +1,4 @@
.category-list-scroller{
max-height:40vh;
}
.category-button {
.category-img {
width: 120px;
@@ -33,11 +29,3 @@
display: block;
margin : 3px auto 0 auto;
}
@media screen and (max-width: 768px) {
.pos .search-bar-container.sb-product {
max-width: none;
width: auto;
background-color: #FFFFFF;
}
}
@@ -2,66 +2,27 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductsWidgetControlPanel" owl="1">
<div class="products-widget-control d-grid shadow-sm z-index-1" t-ref="products-widget-control">
<div class="rightpane-header d-flex justify-content-between align-items-center text-center bg-view p-1 overflow-x-auto overflow-y-hidden">
<t t-if="!props.hasNoCategories">
<div class="categories-header d-flex overflow-x-auto">
<!-- Breadcrumbs -->
<div class="breadcrumbs d-flex">
<span class="breadcrumb float-start d-flex align-items-center">
<button class="breadcrumb-button breadcrumb-home btn btn-light fw-bolder" t-on-click="() => this.props.switchCategory(0)">
<i class="fa fa-home fa-2x py-1" role="img" aria-label="Home" title="Home"></i>
</button>
</span>
<t t-foreach="props.breadcrumbs" t-as="category" t-key="category.id">
<span class="breadcrumb float-start d-flex align-items-center">
<i class="breadcrumb-arrow fa fa-caret-right px-2" aria-hidden="true"/>
<button class="breadcrumb-button btn btn-light h-100 fw-bolder"
t-on-click="() => this.props.switchCategory(category.id)">
<t t-esc="category.name"></t>
</button>
</span>
</t>
</div>
<!-- Subcategories -->
<div t-if="props.subcategories.length > 0 and !displayCategImages" class="category-list simple d-flex align-items-center ms-1">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
<button class="category-simple-button btn btn-light h-100 fw-bolder"
t-on-click="() => this.props.switchCategory(category.id)">
<t t-esc="category.name" />
</button>
</t>
<div class="products-widget-control d-flex bg-view shadow-sm" t-ref="products-widget-control">
<CategorySelector
categories="getCategories()"
onClick="(id) => this.pos.setSelectedCategoryId(id)"
/>
<div class="ms-auto search-bar-container d-flex align-items-center">
<button t-if="state.isMobile"
class="p-3 rounded"
t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</button>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="input-group h-100">
<div class="pos-search-bar form-control d-flex align-items-center py-1 bg-view">
<i class="oi oi-search me-2"/>
<input class="flex-grow-1 w-auto h-100 border-0 px-2 me-4" t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times position-absolute end-0 me-2 pe-1 cursor-pointer" t-on-click="_clearSearch"/>
</div>
</div>
</t>
<div class="search-bar-container sb-product d-flex align-items-center h-100">
<span t-if="state.isMobile" class="oe_status oe_icon icon d-flex align-items-center h-100 me-1 px-4 rounded text-bg-secondary cursor-pointer" t-on-click="toggleMobileSearchBar">
<i t-attf-class="fa {{state.mobileSearchBarIsShown ? 'fa-arrow-right' : 'fa-search'}}"/>
</span>
<t t-if="!state.isMobile || state.mobileSearchBarIsShown">
<div class="input-group h-100">
<div class="pos-search-bar form-control d-flex align-items-center py-1 bg-view">
<i class="oi oi-search me-2"/>
<input class="flex-grow-1 w-auto h-100 border-0 px-2 me-4" t-model="this.pos.searchProductWord" placeholder="Search Products..." type="text" autofocus="autofocus" t-on-keyup="updateSearch" />
<i t-if="this.pos.searchProductWord" class="fa fa-times position-absolute end-0 me-2 pe-1 cursor-pointer" t-on-click="_clearSearch"/>
</div>
</div>
</t>
</div>
</div>
<t t-if="props.subcategories.length > 0 and displayCategImages">
<div class="categories border-bottom shadow-sm">
<div class="category-list-scroller w-100 h-100 overflow-hidden overflow-y-auto">
<div class="category-list d-flex flex-wrap px-1 py-2 gap-3">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
<CategoryButton
switchCategory.bind="props.switchCategory"
category="category" />
</t>
</div>
</div>
</div>
</t>
</div>
</t>
@@ -52,22 +52,6 @@ export class ProductsWidget extends Component {
return a.display_name.localeCompare(b.display_name);
});
}
get subcategories() {
const { db } = this.pos;
return db
.get_category_childs_ids(this.selectedCategoryId)
.map((id) => db.get_category_by_id(id));
}
get breadcrumbs() {
const { db } = this.pos;
if (this.selectedCategoryId === db.root_category_id) {
return [];
}
return [
...db.get_category_ancestors_ids(this.selectedCategoryId).slice(1),
this.selectedCategoryId,
].map((id) => db.get_category_by_id(id));
}
get hasNoCategories() {
return this.pos.db.get_category_childs_ids(0).length === 0;
}
@@ -2,17 +2,13 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductsWidget" owl="1">
<div class="products-widget d-flex flex-column flex-grow-1 overflow-hidden">
<div class="products-widget d-flex flex-column flex-grow-1 overflow-auto">
<ProductsWidgetControlPanel
updateProductList.bind="updateProductList"
clearSearch.bind="clearSearch"
updateSearch.bind="updateSearch"
switchCategory.bind="switchCategory"
breadcrumbs="breadcrumbs"
subcategories="subcategories"
hasNoCategories="hasNoCategories"
loadProductFromServer.bind="onPressEnterKey" />
<div class="product-list-container flex-grow-1 overflow-hidden overflow-y-auto">
<div class="product-list-container flex-grow-1 overflow-y-auto">
<div t-if="productsToDisplay.length != 0 and posHasValidProduct" t-attf-class="{{this.pos.productListViewMode}} product-list d-grid gap-1 p-1">
<t t-foreach="productsToDisplay" t-as="product" t-key="product.id">
<ProductItem
@@ -30,7 +26,7 @@
<t t-else="">
<div>
<p>No products here, but don't worry, we've got your back.</p>
<p>Play with <a role="button" class="button-no-demo btn btn-lg btn-primary" t-on-click="loadDemoDataProducts">our demo products</a> or <a role="button" class="button-no-demo" t-on-click="createNewProducts">create your own products</a></p>
<p>Play with <a role="button" class="text-primary" t-on-click="loadDemoDataProducts">our demo products</a> or <a role="button" class="text-primary" t-on-click="createNewProducts">create your own products</a></p>
<div t-if="state.showReloadMessage" class="info-load-product-message">
Please reload this page once products have been created
</div>
@@ -41,7 +37,7 @@
<div t-if="searchWord" class="search-more-button d-flex justify-content-center">
<button class="btn btn-primary btn-lg" t-on-click="onPressEnterKey">Search more</button>
</div>
<div class="portal search-database-button no-results-message" t-att-class="{ oe_hidden: !shouldShowButton }"></div>
<div class="portal search-database-button no-results-message" t-att-class="{ 'd-none': !shouldShowButton }"></div>
</div>
</div>
</t>
@@ -2,48 +2,47 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.ProductScreen" owl="1">
<div class="product-screen screen h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="screen-full-width d-flex w-100 h-100">
<div class="leftpane pane-border d-flex flex-column flex-grow-1 w-100 h-100 border-end bg-200" t-if="!ui.isSmall || pos.mobile_pane === 'left'">
<OrderWidget/>
<div class="pads border-top">
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300">
<t t-if="!ui.isSmall">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name"/>
</t>
</t>
</div>
<div class="subpads d-flex">
<ActionpadWidget
partner="partner"
actionName="constructor.numpadActionName"
actionType="'payment'"
onClickMore.bind="displayAllControlPopup" />
<NumpadWidget
updateSelectedOrderline.bind="updateSelectedOrderline"
activeMode="pos.numpadMode" />
</div>
<div class="product-screen d-flex h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
<div class="leftpane d-flex flex-column border-end bg-200"
t-if="!ui.isSmall || pos.mobile_pane === 'left'"
t-att-class="{'flex-grow-1': ui.isSmall}">
<OrderWidget/>
<div class="pads border-top">
<div class="control-buttons d-flex flex-wrap border-bottom overflow-hidden bg-300"
t-if="!ui.isSmall">
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
<t t-component="cb.component" t-key="cb.name"/>
</t>
</div>
<div class="subpads d-flex">
<ActionpadWidget
partner="partner"
actionName="constructor.numpadActionName"
actionType="'payment'"
onClickMore.bind="displayAllControlPopup" />
<NumpadWidget
updateSelectedOrderline.bind="updateSelectedOrderline"
activeMode="pos.numpadMode" />
</div>
</div>
<div class="rightpane d-flex flex-column flex-grow-1 w-100 h-100 bg-300" t-if="!ui.isSmall || pos.mobile_pane === 'right'">
<ProductsWidget />
<t t-if="ui.isSmall">
<div class="product-reminder d-flex justify-content-center align-items-center py-1 text-bg-warning bg-opacity-50 fw-bolder" t-if="showProductReminder" t-key="animationKey" >
<span><t t-esc="selectedOrderlineQuantity"/> <t t-esc="selectedOrderlineDisplayName"/> <t t-esc="selectedOrderlineTotal"/></span>
</div>
<div class="switchpane d-flex">
<button class="btn-switchpane pay-button btn btn-primary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryPayButton(), 'secondary': !primaryPayButton()}" t-on-click="() => currentOrder.pay()">
<span class="fs-1 d-block">Pay</span>
<span><t t-esc="total" /></span>
</button>
<button class="btn-switchpane btn btn-secondary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryReviewButton(), 'secondary': !primaryReviewButton()}" t-on-click="switchPane">
<span class="fs-1 d-block">Review</span>
<span><t t-esc="items"/> items</span>
</button>
</div>
</t>
</div>
</div>
<div class="rightpane overflow-auto d-flex flex-grow-1 flex-column bg-300" t-if="!ui.isSmall || pos.mobile_pane === 'right'">
<ProductsWidget />
<t t-if="ui.isSmall">
<div class="product-reminder d-flex justify-content-center align-items-center py-1 text-bg-warning bg-opacity-50 fw-bolder" t-if="showProductReminder" t-key="animationKey" >
<span><t t-esc="selectedOrderlineQuantity"/> <t t-esc="selectedOrderlineDisplayName"/> <t t-esc="selectedOrderlineTotal"/></span>
</div>
<div class="switchpane d-flex">
<button class="btn-switchpane pay-button btn btn-primary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryPayButton(), 'secondary': !primaryPayButton()}" t-on-click="() => currentOrder.pay()">
<span class="fs-1 d-block">Pay</span>
<span><t t-esc="total" /></span>
</button>
<button class="btn-switchpane btn btn-secondary w-50 rounded-0 fw-bolder" t-att-class="{'primary': primaryReviewButton(), 'secondary': !primaryReviewButton()}" t-on-click="switchPane">
<span class="fs-1 d-block">Review</span>
<span><t t-esc="items"/> items</span>
</button>
</div>
</t>
</div>
</div>
</t>
@@ -186,7 +186,7 @@
</div>
</t>
<t t-name="point_of_sale.OrderLinesReceipt" owl="1">
<t t-foreach="receipt.orderlines" t-as="line" t-key="line.id">
<div t-foreach="receipt.orderlines" t-as="line" t-key="line.id">
<div t-esc="line.product_name_wrapped[0]" />
<WrappedProductNameLines line="line" />
<t t-if="line.display_discount_policy == 'without_discount' and line.price != line.price_lst">
@@ -237,7 +237,7 @@
</ul>
</div>
</t>
</t>
</div>
</t>
</templates>
@@ -580,7 +580,8 @@ export class TicketScreen extends Component {
modelField: "pos_reference",
},
DATE: {
repr: (order) => deserializeDate(order.creation_date).toFormat("yyyy-MM-dd HH:mm a"),
repr: (order) =>
deserializeDate(order.creation_date).toFormat("yyyy-MM-dd HH:mm a"),
displayName: this.env._t("Date"),
modelField: "date_order",
},
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="point_of_sale.TicketScreen" owl="1">
<div class="ticket-screen screen h-100 bg-100" t-att-class="{ oe_hidden: !props.isShown }">
<div class="ticket-screen screen h-100 bg-100" t-att-class="{ 'd-none': !props.isShown }">
<div class="screen-full-width d-flex w-100 h-100">
<div class="rightpane pane-border d-flex flex-column flex-grow-1 w-100 h-50 h-md-100 bg-300 overflow-y-auto">
<div class="controls d-flex align-items-center justify-content-between mt-1 mt-md-0 p-2 bg-400">
@@ -58,7 +58,7 @@
<div class="orders overflow-y-auto">
<t t-set="_filteredOrderList" t-value="getFilteredOrderList()" />
<t t-if="_filteredOrderList.length !== 0">
<div class="header-row d-flex text-bg-700 fw-bolder" t-att-class="{ oe_hidden: ui.isSmall }">
<div class="header-row d-flex text-bg-700 fw-bolder" t-att-class="{ 'd-none': ui.isSmall }">
<div class="col wide p-2">Date</div>
<div class="col wide p-2">Receipt Number</div>
<div class="col p-2">Customer</div>
@@ -1,5 +1,6 @@
/** @odoo-module */
import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
import { registry } from "@web/core/registry";
function add_product_to_order(product_name) {
@@ -91,15 +92,12 @@ function fillPaymentLineAmountMobile(lineName, keys) {
content: "click confirm button",
trigger: ".popup .footer .confirm",
mobile: true,
}
},
];
}
function fillPaymentValue(lineName, val) {
return [
...press_payment_numpad(val),
...fillPaymentLineAmountMobile(lineName, val),
]
return [...press_payment_numpad(val), ...fillPaymentLineAmountMobile(lineName, val)];
}
function press_product_numpad(val) {
@@ -230,7 +228,7 @@ function finish_order() {
},
{
content: "check if we left the receipt screen",
trigger: ".pos-content .screen:not(:has(.receipt-screen))",
trigger: ".pos-content div:not(:has(.receipt-screen))",
run: function () {},
},
];
@@ -242,12 +240,8 @@ var steps = [
trigger: "body:not(:has(.loader))",
run: function () {},
},
{
// Leave category displayed by default
content: "click category switch",
trigger: ".breadcrumb-home",
},
];
steps = steps.concat(...ProductScreen.do.clickHomeCategory());
steps = steps.concat(add_product_to_order("Desk Organizer"));
steps = steps.concat(verify_order_total("5.10"));
@@ -353,4 +347,6 @@ steps = steps.concat([
},
]);
registry.category("web_tour.tours").add("pos_basic_order", { test: true, url: "/pos/ui", steps: () => steps });
registry
.category("web_tour.tours")
.add("pos_basic_order", { test: true, url: "/pos/ui", steps: () => steps });
@@ -65,7 +65,7 @@ class Check {
return [
{
content: "product configurator is shown",
trigger: ".product-configurator-popup:not(:has(.oe_hidden))",
trigger: ".product-configurator-popup:not(:has(.d-none))",
run: () => {},
},
];
@@ -41,11 +41,11 @@ class Do {
return [
{
content: `selecting '${name}' subcategory`,
trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`,
trigger: `.products-widget > .products-widget-control .category-button:contains("${name}")`,
},
{
content: `'${name}' subcategory selected`,
trigger: `.breadcrumbs .breadcrumb-button:contains("${name}")`,
trigger: `i.fa-caret-right ~ button.category-button:contains("${name}")`,
run: () => {},
},
];
@@ -55,7 +55,7 @@ class Do {
return [
{
content: `click Home subcategory`,
trigger: `.breadcrumbs .breadcrumb-home`,
trigger: `button.category-button i.fa-home`,
},
];
}
@@ -281,8 +281,10 @@ class Do {
return [
{
content: `PoS model scan barcode '${barcode}'`,
trigger: '.pos', // The element here does not really matter as long as it is present
run: () => { window.posmodel.env.services.barcode_reader.scan(barcode); },
trigger: ".pos", // The element here does not really matter as long as it is present
run: () => {
window.posmodel.env.services.barcode_reader.scan(barcode);
},
},
];
}
@@ -290,12 +292,10 @@ class Do {
return [
{
content: `PoS model scan EAN13 barcode '${barcode}'`,
trigger: '.pos', // The element here does not really matter as long as it is present
trigger: ".pos", // The element here does not really matter as long as it is present
run: () => {
const barcode_reader = window.posmodel.env.services.barcode_reader;
barcode_reader.scan(
barcode_reader.parser.sanitize_ean(barcode)
);
barcode_reader.scan(barcode_reader.parser.sanitize_ean(barcode));
},
},
];
@@ -307,7 +307,7 @@ class Do {
trigger: ".pos-rightheader .floor-button",
mobile: true,
},
]
];
}
}
@@ -2,6 +2,7 @@
/* global posmodel */
import { registry } from "@web/core/registry";
import { ProductScreen } from "@point_of_sale/../tests/tours/helpers/ProductScreenTourMethods";
import { roundDecimals as round_di } from "@web/core/utils/numbers";
import { nbsp } from "@web/core/utils/strings";
@@ -100,11 +101,7 @@ steps = steps.concat([
trigger: ".pos.done-testing",
run: function () {}, // it's a check
},
{
content: "click category switch",
trigger: ".breadcrumb-home",
run: "click",
},
...ProductScreen.do.clickHomeCategory(),
{
content: "click review button",
trigger: ".btn-switchpane:contains('Review')",
@@ -324,8 +321,8 @@ steps = steps.concat([
},
]);
registry.category("web_tour.tours").add("pos_pricelist", {
test: true,
url: "/pos/ui",
registry.category("web_tour.tours").add("pos_pricelist", {
test: true,
url: "/pos/ui",
steps: () => steps,
});
@@ -4,7 +4,7 @@
<div class="login-overlay fixed-top w-100 h-100 bg-view">
<div class="screen-login position-absolute top-0 start-0 bottom-0 end-0 d-flex flex-column py-4 m-auto w-100 rounded bg-view text-center fw-bolder">
<div class="login-title fs-2 mb-3 mb-md-0">Log in to
<span class="text-primary"><t t-esc="shopName" /></span>
<span class="text-primary" t-esc="shopName" />
</div>
<div class="login-body d-flex d-flex flex-column flex-sm-row align-items-center justify-content-around px-3 py-4">
<span class="login-element border p-3 rounded">
@@ -1,20 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.Table" owl="1">
<div class="table position-absolute d-flex flex-column align-items-center justify-content-between cursor-pointer overflow-hidden"
<div class="table position-absolute d-flex flex-column align-items-center justify-content-between cursor-pointer"
t-on-click.stop="(ev) => props.onClick(props.table, ev)"
t-att-style="style">
<div class="infos d-flex align-items-center flex-grow-1">
<span class="label fw-bolder fs-4 z-index-1"
<span class="label fw-bolder fs-4"
t-esc="props.table.name" />
<span
t-att-class="orderCountClass"
class="badge d-flex align-items-center justify-content-center position-absolute top-0 start-100 translate-middle rounded-pill"
class="badge d-flex align-items-center justify-content-center position-absolute rounded-pill"
t-attf-class="{{ orderCount === 0 ? 'd-none' : ''}}"
t-att-style="badgeStyle"
t-esc="orderCount"/>
</div>
<span class="table-seats position-absolute bottom-0 start-50 translate-middle-x mb-1 px-2 py-1 rounded text-bg-dark bg-opacity-25 fs-4 z-index-1">
<span class="table-seats position-absolute bottom-0 start-50 translate-middle-x mb-1 px-2 py-1 rounded text-bg-dark bg-opacity-25 fs-4">
<div class="cover" t-att-style="`width: ${Math.ceil(fill * 100)}%`" />
<t t-esc="customerCountDisplay" />
</span>
@@ -1,7 +1,6 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad";
import { nbsp } from "@web/core/utils/strings";
/**
* @props partner
*/
@@ -13,15 +12,11 @@ patch(ActionpadWidget.prototype, {
get currentOrder() {
return this.pos.get_order();
},
get addedClasses() {
if (!this.currentOrder) {
return {};
}
const hasChanges = this.currentOrder.hasChangesToPrint();
const skipped = hasChanges ? false : this.currentOrder.hasSkippedChanges();
get swapButtonClasses() {
return {
highlight: hasChanges,
altlight: skipped,
"highlight btn-primary": this.currentOrder?.hasChangesToPrint(),
altlight:
!this.currentOrder?.hasChangesToPrint() && this.currentOrder?.hasSkippedChanges(),
};
},
async submitOrder() {
@@ -61,12 +56,8 @@ patch(ActionpadWidget.prototype, {
}
const category = this.pos.db.category_by_id[categoryId].name;
const numProd = orderline.quantity;
categories[category] = categories[category] ? categories[category] + numProd : numProd;
categories[category] = categories[category] ?? 0 + numProd;
}
let result = "";
for (const key in categories) {
result = result + categories[key] + nbsp + key + " | ";
}
return result.slice(0, -2);
return Object.entries(categories);
},
});
@@ -1,21 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.ActionpadWidget" t-inherit="point_of_sale.ActionpadWidget" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('actionpad')]" position="attributes">
<attribute name="t-att-class">{'w-50' : this.swapButton}</attribute>
</xpath>
<!-- Replace the payment button by the order button -->
<xpath expr="//button[hasclass('validation')]" position="after">
<button
t-if="this.swapButton"
class="button submit-order btn btn-lg py-5 rounded-0 highlight d-flex flex-column flex-fill align-items-center justify-content-center fw-bolder"
t-att-class="addedClasses" t-attf-class="{{ !this.highlightPay ? 'highlight btn-primary' : 'btn-secondary' }}"
t-attf-class="submit-order w-100 h-100 {{getMainButtonClasses()}}"
t-att-class="swapButtonClasses"
t-on-click="submitOrder">
<div t-att-class ="categoryCount ? 'smaller-display' : 'none'">
<div t-att-class = "categoryCount ? 'submit-circle-smaller' : 'submit-circle'">
<i class="fa fa-cutlery"></i>
</div>
Order
</div>
<div class = "break-line">
<span t-esc="categoryCount"/>
<i class="fa fa-cutlery"></i>
Order
<div class="break-line">
<t t-foreach="categoryCount" t-as="category" t-key="category[0]">
<t t-esc="category[1]"/> <t t-esc="category[0]"/> |
</t>
</div>
</button>
</xpath>
@@ -25,7 +27,11 @@
<!-- Replace the customer button by the payment button, the customer button will be added in the mixins -->
<xpath expr="//button[hasclass('set-partner')]" position="after">
<button t-on-click="() => pos.get_order().pay()" class="button pay-order-button btn btn-lg rounded-0" t-attf-class="{{ this.highlightPay ? 'highlight btn-primary' : 'btn-secondary' }}" t-if="this.swapButton">
<button t-if="this.swapButton"
t-on-click="() => pos.get_order().pay()"
class="button pay-order-button btn btn-lg rounded-0"
t-attf-class="{{ this.highlightPay ? 'highlight btn-primary' : 'btn-secondary' }}"
>
<i class="oi oi-chevron-right" role="img" aria-label="Pay" title="Pay" />
Payment
</button>
@@ -10,10 +10,7 @@ patch(ProductScreen.prototype, {
get selectedOrderlineQuantity() {
const order = this.pos.get_order();
const orderline = order.get_selected_orderline();
if (
this.pos.config.module_pos_restaurant &&
this.pos.orderPreparationCategories.size
) {
if (this.pos.config.module_pos_restaurant && this.pos.orderPreparationCategories.size) {
let orderline_name = orderline.product.display_name;
if (orderline.description) {
orderline_name += " (" + orderline.description + ")";
@@ -31,10 +28,7 @@ patch(ProductScreen.prototype, {
);
},
get swapButton() {
return (
this.pos.config.module_pos_restaurant &&
this.pos.orderPreparationCategories.size
);
return this.pos.config.module_pos_restaurant && this.pos.orderPreparationCategories.size;
},
submitOrder() {
this.pos.sendOrderInPreparation(this.pos.get_order());
@@ -1,3 +1,5 @@
// TODO this class is used also in other modules.
// Is there a way to not duplicate it? ( ex: `pos_self_order` )
.break-line {
box-sizing: border-box;
overflow: hidden;
@@ -3,7 +3,7 @@
<t t-name="pos_sale.SaleOrderList" owl="1">
<div class="orders overflow-y-auto">
<div class="header-row d-flex text-bg-700 fw-bolder" t-att-class="{ oe_hidden: ui.isSmall }">
<div class="header-row d-flex text-bg-700 fw-bolder" t-att-class="{ 'd-none': ui.isSmall }">
<div class="col name p-2">Order</div>
<div class="col date p-2">Date</div>
<div class="col customer p-2">Customer</div>
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_sale.SaleOrderManagementScreen" owl="1">
<div class="order-management-screen screen h-100 bg-100 overflow-auto" t-att-class="{ oe_hidden: !props.isShown }" t-ref="root">
<div class="order-management-screen screen h-100 bg-100 overflow-auto" t-att-class="{ 'd-none': !props.isShown }" t-ref="root">
<div class="rightpane">
<div class="flex-container flex-container d-flex flex-column h-100">
<SaleOrderManagementControlPanel