summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src
diff options
context:
space:
mode:
authorJoseph Caburnay <jcb@odoo.com>2020-07-09 12:07:14 +0000
committerJoseph Caburnay <jcb@odoo.com>2020-08-05 13:35:08 +0000
commit9ea0aca6ea30fe42e1a7e0a4eaeb79f92a521152 (patch)
treeb3c969ef00f0bbc637d94c124202a96887df450a /addons/point_of_sale/static/src
parent71f719a61ea34252abdc9f1b2fb2caf8e62c7917 (diff)
[IMP] point_of_sale,pos_*: top bar improvements and ticket list
The following changes are made in this task: 1. Remove order tabs and replace with ticket list. 2. Top bar color change. 3. Search bar in the product screen is moved in the top bar. 4. List of sub-categories are now in the same line as the category breadcrumbs. closes odoo/odoo#55100 Task-id: 2276678 Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
Diffstat (limited to 'addons/point_of_sale/static/src')
-rw-r--r--addons/point_of_sale/static/src/css/pos.css417
-rw-r--r--addons/point_of_sale/static/src/js/Chrome.js8
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js56
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js41
-rw-r--r--addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js15
-rw-r--r--addons/point_of_sale/static/src/js/Misc/SearchBar.js115
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js4
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js181
-rw-r--r--addons/point_of_sale/static/src/js/main.js2
-rw-r--r--addons/point_of_sale/static/src/js/models.js44
-rw-r--r--addons/point_of_sale/static/src/js/utils.js4
-rw-r--r--addons/point_of_sale/static/src/xml/Chrome.xml25
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml34
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml13
-rw-r--r--addons/point_of_sale/static/src/xml/Misc/SearchBar.xml44
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml6
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml44
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml55
20 files changed, 919 insertions, 195 deletions
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 39ee679e8ca..62321e1932c 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -319,22 +319,24 @@ td {
margin:0;
padding:0;
color: gray;
- background: #393939;
+ background: #875A7B;
display: flex;
}
/* a) The left part of the top-bar */
.pos .pos-branding{
- max-width: 459px;
+ min-width: 503px;
+ max-width: 503px;
flex-grow: 1;
height:100%;
margin:0;
padding:0;
- border-right: 1px solid #373737;
text-align:left;
line-height:100%;
vertical-align: middle;
+ display: flex;
+ justify-content: space-between;
}
.pos .pos-logo {
height: 35px;
@@ -352,6 +354,10 @@ td {
cursor: pointer;
}
+.pos .ticket-button {
+ display: flex;
+}
+
/* b) The right part of the top-bar */
.pos .pos-rightheader {
@@ -362,7 +368,17 @@ td {
overflow-x: auto;
}
.pos .pos-rightheader > * {
- border-right: 1px solid #292929;
+ border-right: 1px solid #875A7B;
+}
+
+.pos .status-buttons-portal {
+ flex: 1;
+}
+
+.pos .status-buttons {
+ display: flex;
+ justify-content: flex-end;
+ flex: 1;
}
.pos .order-button{
@@ -456,8 +472,8 @@ td {
height: 48px;
padding-left: 16px;
padding-right: 16px;
- border-right: 1px solid #292929;
- border-left: 1px solid #292929;
+ border-right: 1px solid #875A7B;
+ border-left: 1px solid #875A7B;
color: #DDD;
line-height: 48px;
text-align: center;
@@ -468,7 +484,7 @@ td {
-webkit-transition-timing-function: ease-out;
}
.pos .pos-rightheader .header-button:last-child{
- border-left: 1px solid #3a3a3a;
+ border-left: 1px solid #875A7B;
}
.pos .pos-rightheader .header-button:active{
background: rgba(0,0,0,0.2);
@@ -852,11 +868,18 @@ td {
.pos .rightpane-header {
padding: 0;
- height: 48px;
- border-bottom: 1px solid #c7c7c7;
background: #d3d3d3;
text-align: center;
display: flex;
+ flex-flow: row wrap;
+}
+
+.pos .green-border-bottom {
+ border-bottom: solid 3px rgb(110, 200, 155);
+}
+
+.pos .grey-border-bottom {
+ border-bottom: 1px solid #c7c7c7;
}
/* ********* The product list ********* */
@@ -885,11 +908,6 @@ td {
/* a) the product list navigation bar */
-.pos .breadcrumbs{
- display: inline-block;
- text-align: left;
- float:left;
-}
.pos .breadcrumb{
float: left;
display: inline-block;
@@ -911,8 +929,6 @@ td {
}
.pos .breadcrumb-button.breadcrumb-home {
line-height: 50px;
- padding: 0;
- width: 50px;
font-size: 25px;
text-align: center;
}
@@ -936,16 +952,13 @@ td {
/* b) the search box */
.pos .searchbox {
- position: absolute;
- right: 2px;
flex-grow: 1;
+ position: relative;
}
.pos .searchbox input {
- width: 100px;
+ width: 150px;
border: 1px solid #cecbcb;
padding: 10px 20px;
- padding-left: 38px;
- padding-right: 33px;
margin: 6px;
background-color: white;
border-radius: 20px;
@@ -959,7 +972,6 @@ td {
}
.pos .search-clear {
- position: absolute;
top: 9px;
width: 30px;
height: 30px;
@@ -971,12 +983,13 @@ td {
.search-clear.left {
left: 11px;
color: #808080;
+ margin: 6px;
}
.search-clear.right {
- left: 145px;
color: #808080;
cursor: pointer;
+ margin: 6px;
}
@media screen and (max-width: 768px) {
@@ -1004,6 +1017,7 @@ td {
.pos .categories {
position: relative;
border-bottom: solid 3px rgb(110, 200, 155);
+ flex: 1;
}
.pos .categories h4 {
display: inline-block;
@@ -1016,7 +1030,7 @@ td {
box-sizing: border-box;
width:100%;
height:100%;
- max-height:60vh;
+ max-height:40vh;
overflow: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
@@ -1027,16 +1041,13 @@ td {
text-align: left;
padding: 10px;
background: rgb(229, 229, 229);
- border-bottom: 1px solid #cecece;
}
.pos .category-list.simple {
padding: 0px;
background: #cecece;
- display: -webkit-flex;
display: flex;
- -webkit-flex-flow: row wrap;
flex-flow: row wrap;
- border-bottom: none;
+ flex: 1;
}
@@ -1058,17 +1069,16 @@ td {
}
.pos .category-simple-button{
- position: relative;
- display: inline-block;
+ display: flex;
+ align-items: center;
font-size: 14px;
- margin-right: 1px;
- margin-bottom: 1px;
padding: 5px 12px;
- line-height: 32px;
- flex-grow: 1;
- -webkit-flex-grow: 1;
cursor: pointer;
- background: #e2e2e2;
+ flex: 1;
+ text-align: left;
+ background: rgb(229, 229, 229);
+ border-right: solid 1px #d3d3d3;
+ border-top: solid 1px #d3d3d3;
}
.pos .category-simple-button:active{
color: white;
@@ -1372,12 +1382,6 @@ td {
flex-grow: 1;
}
-.pos .screen .header-row {
- border:none;
- width:100%;
- height:0px;
-}
-
.pos .screen .header-cell{
border:none;
width:100%;
@@ -3144,3 +3148,332 @@ td {
.pos .product_configurator_attributes .configurator_color.active input {
margin: 18px;
}
+
+/* TICKET SCREEN */
+
+.ticket-screen {
+ font-size: medium;
+}
+
+.ticket-screen .orders {
+ display: flex;
+ flex-flow: column nowrap;
+ overflow: hidden;
+ overflow-y: hidden;
+ overflow-y: auto;
+}
+
+.ticket-screen .orders .header-row{
+ display: flex;
+ flex-flow: row nowrap;
+ flex: 1;
+ justify-content: space-evenly;
+ background: #868686;
+ color: white;
+}
+
+.ticket-screen .orders .order-row {
+ display: flex;
+ flex-flow: row nowrap;
+ flex: 1;
+ justify-content: space-evenly;
+}
+
+.ticket-screen .orders .col {
+ display: flex;
+ flex: 1;
+ padding: 10px;
+}
+
+.ticket-screen .orders .col.start {
+ justify-content: flex-start;
+}
+
+.ticket-screen .orders .col.center {
+ justify-content: center;
+}
+
+.ticket-screen .orders .col.end {
+ justify-content: flex-end;
+}
+
+.ticket-screen .orders .col.very-narrow {
+ flex: 0.2;
+}
+
+.ticket-screen .orders .col.narrow {
+ flex: 0.5;
+}
+
+.ticket-screen .orders .col.wide {
+ flex: 1.5;
+}
+
+.ticket-screen .order-row:nth-child(odd) {
+ background: #DDD;
+}
+
+.ticket-screen .order-row:nth-child(even) {
+ background: white;
+}
+
+.ticket-screen .order-row:hover {
+ background: rgb(110,200,155);
+ color: white;
+}
+
+.ticket-screen .pointer {
+ cursor: pointer;
+}
+
+.ticket-screen .controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 0px 20px;
+ border-bottom: dashed 1px rgb(215,215,215);
+ flex: 0 0 80px;
+}
+
+.ticket-screen .controls button {
+ font-size: medium;
+ padding: 12px;
+ margin-right: 20px;
+ font-weight: bold;
+}
+
+.ticket-screen .controls button.discard {
+ background: rgb(230, 230, 230);
+ border: solid 1px rgb(209, 209, 209);
+}
+
+.ticket-screen .controls button.highlight {
+ background: #6EC89B;
+ border: solid 1px #64AF8A;
+ color: white;
+}
+
+.ticket-screen .pos-search-bar {
+ vertical-align: middle;
+ white-space: nowrap;
+ position: relative;
+ display: flex;
+ max-width: 500px;
+ flex: 1;
+}
+
+.ticket-screen .pos-search-bar .search {
+ display: flex;
+ position: relative;
+ flex: 1;
+}
+
+.ticket-screen .pos-search-bar .search input {
+ height: 40px;
+ font-size: medium;
+ color: #63717f;
+ padding-left: 40px;
+ border: solid 1px rgb(209, 209, 209);
+ flex: 1;
+}
+
+.ticket-screen .pos-search-bar .radius-right {
+ border-top-right-radius: 5px;
+ border-bottom-right-radius: 5px;
+}
+
+.ticket-screen .pos-search-bar .radius-left {
+ border-top-left-radius: 5px;
+ border-bottom-left-radius: 5px;
+}
+
+.ticket-screen .pos-search-bar .search input:focus {
+ outline: none;
+}
+
+.ticket-screen .pos-search-bar .search .search-icon {
+ position: absolute;
+ left: 15px;
+ top: 12px;
+ z-index: 1;
+ color: #4f5b66;
+}
+
+.ticket-screen .pos-search-bar .search ul {
+ background: white;
+ position: absolute;
+ top: calc(100% + 5px);
+ right: 2px;
+ left: 2px;
+ box-shadow: 1px 1px 3px grey;
+ font-size: small;
+}
+
+.ticket-screen .pos-search-bar .search li {
+ color: rgb(1,160,157);
+ margin: 0.2em 0;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ padding-left: 35px;
+}
+
+.ticket-screen .pos-search-bar .search li:hover {
+ background: #DDD;
+}
+
+.ticket-screen .pos-search-bar .search li .field {
+ font-style: italic;
+}
+
+.ticket-screen .pos-search-bar .search li .term {
+ font-weight: bold;
+}
+
+.ticket-screen .pos-search-bar .search li.highlight {
+ background: #DDD;
+}
+
+.ticket-screen .pos-search-bar .filter .down-icon {
+ position: absolute;
+ right: 13px;
+ top: 10px;
+}
+
+.ticket-screen .pos-search-bar .filter {
+ height: 40px;
+ background: white;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ padding-left: 20px;
+ padding-right: 40px;
+ border: solid 1px rgb(209, 209, 209);
+ border-left: none;
+ position: relative;
+ display: flex;
+ align-items: center;
+ max-width: 150px;
+ font-size: medium;
+}
+
+.ticket-screen .pos-search-bar .filter:hover {
+ color: #868686;
+}
+
+.ticket-screen .pos-search-bar .filter .options {
+ display: block;
+ position: absolute;
+ top: calc(100% + 5px);
+ right: 0;
+ z-index: 1;
+ box-shadow: 1px 1px 5px grey;
+ padding: 0.5em 0;
+ background: white;
+ color: #555555;
+}
+
+.ticket-screen .pos-search-bar .filter ul.options li {
+ padding: 0.2em 1.2em;
+ border-top: none;
+ display: flex;
+ justify-content: start;
+ align-items: center;
+}
+
+.ticket-screen .pos-search-bar .filter ul.options li:hover {
+ background-color: #DDD;
+}
+
+.ticket-screen .pos-search-bar .search {
+ display: flex;
+}
+
+.ticket-button {
+ display: flex;
+ align-items: center;
+ padding: 0 15px;
+ font-size: medium;
+ color: white;
+}
+
+.ticket-button.highlight {
+ background: rgb(104,69,95);
+}
+
+.ticket-button:hover {
+ background: rgb(104,69,95);
+ cursor: pointer;
+}
+
+.ticket-button .with-badge {
+ margin-right: 0.7em;
+ font-size: larger;
+}
+
+[badge] {
+ position: relative;
+}
+
+[badge]:after {
+ background: rgb(1,160,157);
+ border-radius: 10rem;
+ color: #fff;
+ content: attr(badge);
+ font-size: small;
+ min-width: 20px;
+ padding: 2px;
+ position: absolute;
+ text-align: center;
+ left: 0.6em;
+ bottom: 0.6em;
+}
+
+[badge^="-"]:after,
+[badge="0"]:after,
+[badge=""]:after {
+ display: none;
+}
+
+/* Product Screen Search Bar */
+
+.search-bar-portal {
+ display: flex;
+}
+
+.search-bar-portal .search-box {
+ font-size: medium;
+ position: relative;
+ display: flex;
+ align-items: center;
+ margin: 0 15px;
+}
+
+.search-bar-portal .search-box input {
+ font-size: medium;
+ height: 30px;
+ border: none;
+ color: #63717f;
+ padding-left: 38px;
+ padding-right: 38px;
+ border-radius: 5px;
+}
+
+.search-bar-portal .search-box input:focus {
+ outline: none;
+}
+
+.search-bar-portal .search-box .icon {
+ position: absolute;
+ left: 0;
+ margin-left: 12px;
+ z-index: 1;
+ color: #4f5b66;
+}
+
+.search-bar-portal .search-box .clear-icon {
+ position: absolute;
+ right: 0;
+ margin-right: 12px;
+ z-index: 1;
+ color: #4f5b66;
+ cursor: pointer;
+}
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
index 7f950cfab71..e01a434d9a8 100644
--- a/addons/point_of_sale/static/src/js/Chrome.js
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -197,7 +197,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
this.showScreen(name, props);
}
_getSavedScreen(order) {
- return order.get_screen_data('screen') || { name: 'ProductScreen' };
+ return order.get_screen_data();
}
__showTempScreen(event) {
const { name, props, resolve } = event.detail;
@@ -236,7 +236,7 @@ odoo.define('point_of_sale.Chrome', function(require) {
_setScreenData(name, props) {
const order = this.env.pos.get_order();
if (order) {
- order.set_screen_data('screen', { name, props });
+ order.set_screen_data({ name, props });
}
}
async _closePos() {
@@ -325,6 +325,10 @@ odoo.define('point_of_sale.Chrome', function(require) {
}
}
+ get isTicketScreenShown() {
+ return this.mainScreen.name === 'TicketScreen';
+ }
+
// MISC METHODS //
async _loadDemoData() {
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
deleted file mode 100644
index ad2d8265417..00000000000
--- a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
+++ /dev/null
@@ -1,56 +0,0 @@
-odoo.define('point_of_sale.OrderSelector', function(require) {
- 'use strict';
-
- const { useContext } = owl.hooks;
- const PosComponent = require('point_of_sale.PosComponent');
- const Registries = require('point_of_sale.Registries');
- const contexts = require('point_of_sale.PosContext');
-
- // Previously OrderSelectorWidget
- class OrderSelector extends PosComponent {
- constructor() {
- super(...arguments);
- // this is used in its template
- this.chromeContext = useContext(contexts.chrome);
- }
- mounted() {
- this.env.pos.get('orders').on('add remove change', () => this.render(), this);
- this.env.pos.on('change:selectedOrder', () => this.render(), this);
- }
- willUnmount() {
- this.env.pos.get('orders').off('add remove change', null, this);
- this.env.pos.off('change:selectedOrder', null, this);
- }
- selectOrder(order) {
- this.env.pos.set_order(order);
- }
- addNewOrder() {
- this.env.pos.add_new_order();
- }
- get orderList() {
- return this.env.pos.get_order_list();
- }
- async deleteCurrentOrder() {
- const order = this.env.pos.get_order();
- if (!order) {
- return;
- } else if (!order.is_empty()) {
- const { confirmed } = await this.showPopup('ConfirmPopup', {
- title: this.env._t('Destroy Current Order ?'),
- body: this.env._t('You will lose any data associated with the current order'),
- });
- if (confirmed) this.env.pos.delete_current_order();
- } else {
- this.env.pos.delete_current_order();
- }
- }
- creationTime(order) {
- return moment(order.creation_date).format('hh:mm');
- }
- }
- OrderSelector.template = 'OrderSelector';
-
- Registries.Component.add(OrderSelector);
-
- return OrderSelector;
-});
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js
new file mode 100644
index 00000000000..d142bbdeac0
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js
@@ -0,0 +1,41 @@
+odoo.define('point_of_sale.TicketButton', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { posbus } = require('point_of_sale.utils');
+
+ class TicketButton extends PosComponent {
+ onClick() {
+ if (this.props.isTicketScreenShown) {
+ posbus.trigger('ticket-button-clicked');
+ } else {
+ this.showScreen('TicketScreen');
+ }
+ }
+ willPatch() {
+ posbus.off('order-deleted', this);
+ }
+ patched() {
+ posbus.on('order-deleted', this, this.render);
+ }
+ mounted() {
+ posbus.on('order-deleted', this, this.render);
+ }
+ willUnmount() {
+ posbus.off('order-deleted', this);
+ }
+ get count() {
+ if (this.env.pos) {
+ return this.env.pos.get_order_list().length;
+ } else {
+ return 0;
+ }
+ }
+ }
+ TicketButton.template = 'TicketButton';
+
+ Registries.Component.add(TicketButton);
+
+ return TicketButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
index 939f0daa43f..e2f2148b0c7 100644
--- a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
+++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
@@ -4,13 +4,18 @@ odoo.define('point_of_sale.IndependentToOrderScreen', function (require) {
const PosComponent = require('point_of_sale.PosComponent');
class IndependentToOrderScreen extends PosComponent {
+ /**
+ * Alias the forceTriggerSelectedOrder method as it also
+ * means 'closing' this screen.
+ */
close() {
- // To close this order-indenpendent screen, we forcefully trigger change
+ this.forceTriggerSelectedOrder();
+ }
+ forceTriggerSelectedOrder() {
+ // Calling this method forcefully trigger change
// on the selectedOrder attribute, which then shows the screen of the
- // current order.
- this.env.pos.trigger('change:selectedOrder', this.env.pos, this.env.pos.get_order(), {
- silent: true,
- });
+ // current order, essentially closing this screen.
+ this.env.pos.trigger('change:selectedOrder', this.env.pos, this.env.pos.get_order());
}
}
diff --git a/addons/point_of_sale/static/src/js/Misc/SearchBar.js b/addons/point_of_sale/static/src/js/Misc/SearchBar.js
new file mode 100644
index 00000000000..d9bcfe879b1
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Misc/SearchBar.js
@@ -0,0 +1,115 @@
+odoo.define('point_of_sale.SearchBar', function (require) {
+ 'use strict';
+
+ const { useState, useExternalListener } = owl.hooks;
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * This is a simple configurable search bar component. It has search fields
+ * and selection filter. Search fields allow the users to specify the type
+ * of their searches. The filter is a dropdown menu for selection. Depending on
+ * user's action, this component emits corresponding event with the action
+ * information (payload).
+ *
+ * TODO: This component can be made more generic and be able to replace
+ * all the search bars across pos ui.
+ *
+ * @prop {{
+ * config: {
+ * searchFields: string[],
+ * filter: { show: boolean, options: string[] }
+ * },
+ * placeholder: string,
+ * }}
+ * @emits search @payload { fieldValue: string, searchTerm: '' }
+ * @emits filter-selected @payload { filter: string }
+ *
+ * NOTE: The payload of the emitted event is accessible via the `detail`
+ * field of the event.
+ */
+ class SearchBar extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.config = this.props.config;
+ this.state = useState({
+ searchInput: '',
+ selectedFieldId: this.config.searchFields.length ? 0 : null,
+ showSearchFields: false,
+ showFilterOptions: false,
+ selectedFilter: this.config.filter.options[0] || 'Select',
+ });
+ useExternalListener(window, 'click', this._hideOptions);
+ }
+ selectFilter(option) {
+ this.state.selectedFilter = option;
+ this.trigger('filter-selected', { filter: this.state.selectedFilter });
+ }
+ get placeholder() {
+ return this.props.placeholder;
+ }
+ /**
+ * When vertical arrow keys are pressed, select fields for searching.
+ * When enter key is pressed, trigger search event if there is searchInput.
+ */
+ onKeydown(event) {
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
+ event.preventDefault();
+ this.state.selectedFieldId = this._fieldIdToSelect(event.key);
+ } else if (event.key === 'Enter') {
+ this.trigger('search', {
+ fieldValue: this.config.searchFields[this.state.selectedFieldId],
+ searchTerm: this.state.searchInput,
+ });
+ this.state.showSearchFields = false;
+ } else {
+ if (this.state.selectedFieldId === null && this.config.searchFields.length) {
+ this.state.selectedFieldId = 0;
+ }
+ this.state.showSearchFields = true;
+ }
+ }
+ /**
+ * Called when a search field is clicked.
+ */
+ onClickSearchField(id) {
+ this.state.showSearchFields = false;
+ this.trigger('search', {
+ fieldValue: this.config.searchFields[id],
+ searchTerm: this.state.searchInput,
+ });
+ }
+ /**
+ * Given an arrow key, return the next selectedFieldId.
+ * E.g. If the selectedFieldId is 1 and ArrowDown is pressed, return 2.
+ *
+ * @param {string} key vertical arrow key
+ */
+ _fieldIdToSelect(key) {
+ const length = this.config.searchFields.length;
+ if (!length) return null;
+ if (this.state.selectedFieldId === null) return 0;
+ const current = this.state.selectedFieldId || length;
+ return (current + (key === 'ArrowDown' ? 1 : -1)) % length;
+ }
+ _hideOptions() {
+ this.state.showFilterOptions = false;
+ this.state.showSearchFields = false;
+ }
+ }
+ SearchBar.template = 'point_of_sale.SearchBar';
+ SearchBar.defaultProps = {
+ config: {
+ searchFields: [],
+ filter: {
+ show: false,
+ options: [],
+ },
+ },
+ placeholder: 'Search ...',
+ };
+
+ Registries.Component.add(SearchBar);
+
+ return SearchBar;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
index f4b80c12190..dcde9739f60 100644
--- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -86,10 +86,9 @@ odoo.define('point_of_sale.OrderManagementScreen', function (require) {
* @param {models.Order} order
*/
_setOrder(order) {
+ this.env.pos.set_order(order);
if (order === this.env.pos.get_order()) {
this.close();
- } else {
- this.env.pos.set_order(order);
}
}
}
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index 641fb621f4b..2d552e4a1d4 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -23,8 +23,6 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
useListener('send-payment-cancel', this._sendPaymentCancel);
useListener('send-payment-reverse', this._sendPaymentReverse);
useListener('send-force-done', this._sendForceDone);
- onChangeOrder(this._onPrevOrder, this._onNewOrder);
- useErrorHandlers();
NumberBuffer.use({
// The numberBuffer listens to this event to update its state.
// Basically means 'update the buffer when this event is triggered'
@@ -33,6 +31,8 @@ odoo.define('point_of_sale.PaymentScreen', function (require) {
// Note that the component listens to it.
triggerAtInput: 'update-selected-paymentline',
});
+ onChangeOrder(this._onPrevOrder, this._onNewOrder);
+ useErrorHandlers();
this.payment_interface = null;
}
get currentOrder() {
diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
index 5bb49de5f07..8475697926d 100644
--- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
+++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js
@@ -54,6 +54,9 @@ odoo.define('point_of_sale.ProductsWidget', function(require) {
this.selectedCategoryId,
].map(id => this.env.pos.db.get_category_by_id(id));
}
+ get hasNoCategories() {
+ return this.env.pos.db.get_category_childs_ids(0).length === 0;
+ }
_switchCategory(event) {
this.env.pos.set('selectedCategoryId', event.detail);
}
diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js
new file mode 100644
index 00000000000..724766e93da
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js
@@ -0,0 +1,181 @@
+odoo.define('point_of_sale.TicketScreen', function (require) {
+ 'use strict';
+
+ const Registries = require('point_of_sale.Registries');
+ const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
+ const { useListener } = require('web.custom_hooks');
+ const { posbus } = require('point_of_sale.utils');
+
+ class TicketScreen extends IndependentToOrderScreen {
+ constructor() {
+ super(...arguments);
+ useListener('close-screen', this.close);
+ useListener('filter-selected', this._onFilterSelected);
+ useListener('search', this._onSearch);
+ this.searchDetails = {};
+ this.filter = null;
+ this._initializeSearchFieldConstants();
+ }
+ mounted() {
+ posbus.on('ticket-button-clicked', this, this.close);
+ this.env.pos.get('orders').on('add remove change', () => this.render(), this);
+ this.env.pos.on('change:selectedOrder', () => this.render(), this);
+ }
+ willUnmount() {
+ posbus.off('ticket-button-clicked', this);
+ this.env.pos.get('orders').off('add remove change', null, this);
+ this.env.pos.off('change:selectedOrder', null, this);
+ }
+ _onFilterSelected(event) {
+ this.filter = event.detail.filter;
+ this.render();
+ }
+ _onSearch(event) {
+ const searchDetails = event.detail;
+ Object.assign(this.searchDetails, searchDetails);
+ this.render();
+ }
+ /**
+ * Override to conditionally show the new ticket button.
+ */
+ get showNewTicketButton() {
+ return true;
+ }
+ get orderList() {
+ return this.env.pos.get_order_list();
+ }
+ get filteredOrderList() {
+ const filterCheck = (order) => {
+ if (this.filter && this.filter !== 'All Tickets') {
+ const screen = order.get_screen_data();
+ return this.filter === this.constants.screenToStatusMap[screen.name];
+ }
+ return true;
+ };
+ const { fieldValue, searchTerm } = this.searchDetails;
+ const fieldAccessor = this._searchFields[fieldValue];
+ const searchCheck = (order) => {
+ if (!fieldAccessor) return true;
+ const fieldValue = fieldAccessor(order);
+ if (fieldValue === null) return true;
+ if (!searchTerm) return true;
+ return fieldValue && fieldValue.toString().toLowerCase().includes(searchTerm.toLowerCase());
+ };
+ const predicate = (order) => {
+ return filterCheck(order) && searchCheck(order);
+ };
+ return this.orderList.filter(predicate);
+ }
+ selectOrder(order) {
+ this._setOrder(order);
+ if (order === this.env.pos.get_order()) {
+ this.close();
+ }
+ }
+ _setOrder(order) {
+ this.env.pos.set_order(order);
+ }
+ createNewOrder() {
+ this.env.pos.add_new_order();
+ }
+ async deleteOrder(order) {
+ const screen = order.get_screen_data();
+ if (['ProductScreen', 'PaymentScreen'].includes(screen.name) && order.get_orderlines().length > 0) {
+ const { confirmed } = await this.showPopup('ConfirmPopup', {
+ title: 'Existing orderlines',
+ body: `${order.name} has total amount of ${this.getTotal(
+ order
+ )}, are you sure you want delete this order?`,
+ });
+ if (!confirmed) return;
+ }
+ if (order) {
+ order.destroy({ reason: 'abandon' });
+ }
+ posbus.trigger('order-deleted');
+ }
+ getDate(order) {
+ return moment(order.creation_date).format('YYYY-MM-DD hh:mm A');
+ }
+ getTotal(order) {
+ return this.env.pos.format_currency(order.get_total_with_tax());
+ }
+ getCustomer(order) {
+ return order.get_client_name();
+ }
+ getEmployee(order) {
+ return order.employee.name;
+ }
+ getStatus(order) {
+ const screen = order.get_screen_data();
+ return this.constants.screenToStatusMap[screen.name];
+ }
+ /**
+ * Hide the delete button if one of the payments is a 'done' electronic payment.
+ */
+ hideDeleteButton(order) {
+ return order
+ .get_paymentlines()
+ .some((payment) => payment.is_electronic() && payment.get_payment_status() === 'done');
+ }
+ get searchBarConfig() {
+ return {
+ searchFields: this.constants.searchFieldNames,
+ filter: { show: true, options: ['All Tickets', 'Ongoing', 'Payment', 'Receipt'] },
+ };
+ }
+ /**
+ * An object with keys containing the search field names which map to functions.
+ * The mapped functions will be used to generate representative string for the order
+ * to match the search term when searching.
+ * E.g. Given 2 orders, search those with `Receipt Number` containing `1111`.
+ * ```
+ * orders = [{
+ * name: '000-1111-222'
+ * total: 10,
+ * }, {
+ * name: '444-5555-666'
+ * total: 15,
+ * }]
+ * ```
+ * `Receipt Number` search field maps to the `name` of the order. So, the orders will be
+ * represented by their name, and the search will result to:
+ * ```
+ * result = [{
+ * name: '000-1111-222',
+ * total: 10,
+ * }]
+ * ```
+ * @returns Record<string, (models.Order) => string>
+ */
+ get _searchFields() {
+ return {
+ 'Receipt Number': (order) => order.name,
+ Date: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'),
+ Customer: (order) => order.get_client_name(),
+ };
+ }
+ /**
+ * Maps the order screen params to order status.
+ */
+ get _screenToStatusMap() {
+ return {
+ ProductScreen: 'Ongoing',
+ PaymentScreen: 'Payment',
+ ReceiptScreen: 'Receipt',
+ };
+ }
+ _initializeSearchFieldConstants() {
+ this.constants = {};
+ Object.assign(this.constants, {
+ searchFieldNames: Object.keys(this._searchFields),
+ screenToStatusMap: this._screenToStatusMap,
+ });
+ }
+ }
+ TicketScreen.template = 'TicketScreen';
+
+ Registries.Component.add(TicketScreen);
+
+ return TicketScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js
index dc8ea5d7b7b..346a61679f4 100644
--- a/addons/point_of_sale/static/src/js/main.js
+++ b/addons/point_of_sale/static/src/js/main.js
@@ -11,6 +11,8 @@ odoo.define('web.web_client', function (require) {
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
owl.Component.env = env;
+ Registries.Component.add(owl.misc.Portal);
+
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 44e9527ba9c..6d0d5ff63f8 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -717,11 +717,11 @@ exports.PosModel = Backbone.Model.extend({
var order_list = this.get_order_list();
if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
// when we intentionally remove an unfinished order, and there is another existing one
- this.set_order(order_list[index] || order_list[order_list.length -1]);
+ this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true });
}else{
// when the order was automatically removed after completion,
// or when we intentionally delete the only concurrent order
- this.add_new_order();
+ this.add_new_order({ silent: true });
}
},
@@ -739,10 +739,10 @@ exports.PosModel = Backbone.Model.extend({
this.db.set_cashier(this.get('cashier'));
},
// creates a new empty order and sets it as the current order
- add_new_order: function(){
+ add_new_order: function(options){
var order = new exports.Order({},{pos:this});
this.get('orders').add(order);
- this.set('selectedOrder', order);
+ this.set('selectedOrder', order, options);
return order;
},
/**
@@ -810,8 +810,8 @@ exports.PosModel = Backbone.Model.extend({
},
// change the current order
- set_order: function(order){
- this.set({ selectedOrder: order });
+ set_order: function(order, options){
+ this.set({ selectedOrder: order }, options);
},
// return the list of unpaid orders
@@ -2469,6 +2469,12 @@ exports.Paymentline = Backbone.Model.extend({
ticket: this.ticket,
};
},
+ // If payment status is a non-empty string, then it is an electronic payment.
+ // TODO: There has to be a less confusing way to distinguish simple payments
+ // from electronic transactions. Perhaps use a flag?
+ is_electronic: function() {
+ return Boolean(this.get_payment_status());
+ },
});
var PaymentlineCollection = Backbone.Collection.extend({
@@ -3280,23 +3286,27 @@ exports.Order = Backbone.Model.extend({
// the order also stores the screen status, as the PoS supports
// different active screens per order. This method is used to
// store the screen status.
- set_screen_data: function(key,value){
- if(arguments.length === 2){
- this.screen_data[key] = value;
- }else if(arguments.length === 1){
- for(var key in arguments[0]){
- this.screen_data[key] = arguments[0][key];
- }
- }
+ set_screen_data: function(value){
+ this.screen_data['value'] = value;
},
//see set_screen_data
- get_screen_data: function(key){
- return this.screen_data[key];
+ get_screen_data: function(){
+ const screen = this.screen_data['value'];
+ // If no screen data is saved
+ // no payment line -> product screen
+ // with payment line -> payment screen
+ if (!screen) {
+ if (this.get_paymentlines().length > 0) return { name: 'PaymentScreen' };
+ return { name: 'ProductScreen' };
+ }
+ if (screen.name !== 'ReceiptScreen' && this.get_paymentlines().length > 0) {
+ return { name: 'PaymentScreen' };
+ }
+ return screen;
},
wait_for_push_order: function () {
return this.is_to_email();
},
-
/**
* @returns {Object} object to use as props for instantiating OrderReceipt.
*/
diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js
index 26442ef776b..7aa7b35e93d 100644
--- a/addons/point_of_sale/static/src/js/utils.js
+++ b/addons/point_of_sale/static/src/js/utils.js
@@ -1,6 +1,8 @@
odoo.define('point_of_sale.utils', function (require) {
'use strict';
+ const { EventBus } = owl.core;
+
function getFileAsText(file) {
return new Promise((resolve, reject) => {
if (!file) {
@@ -43,5 +45,5 @@ odoo.define('point_of_sale.utils', function (require) {
);
}
- return { getFileAsText, nextFrame, isRpcError };
+ return { getFileAsText, nextFrame, isRpcError, posbus: new EventBus() };
});
diff --git a/addons/point_of_sale/static/src/xml/Chrome.xml b/addons/point_of_sale/static/src/xml/Chrome.xml
index 39b267e9308..6c2d3c5f46d 100644
--- a/addons/point_of_sale/static/src/xml/Chrome.xml
+++ b/addons/point_of_sale/static/src/xml/Chrome.xml
@@ -4,15 +4,22 @@
<t t-name="Chrome" owl="1">
<div class="pos" t-att-class="{ 'big-scrollbars': state.uiState.hasBigScrollBars }">
<div class="pos-receipt-print"></div>
+ <div class="pos-topheader" t-att-class="{ oe_hidden: state.uiState !== 'READY' }">
+ <div t-if="tempScreen.isShown" class="block-top-header" />
+ <div class="pos-branding" t-if= "!env.isMobile">
+ <img class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
+ src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
+ <TicketButton isTicketScreenShown="isTicketScreenShown" />
+ </div>
+ <div class="pos-rightheader">
+ <TicketButton isTicketScreenShown="isTicketScreenShown" t-if="env.isMobile" />
+ <div class="search-bar-portal" />
+ <div class="status-buttons-portal" />
+ </div>
+ </div>
<t t-if="state.uiState === 'READY'">
- <div class="pos-topheader">
- <div t-if="tempScreen.isShown" class="block-top-header" />
- <div class="pos-branding">
- <img t-if= "!env.isMobile" class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
- src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
- </div>
- <div class="pos-rightheader">
- <OrderSelector />
+ <Portal target="'.pos .status-buttons-portal'">
+ <div class="status-buttons">
<t t-if="!env.isMobile">
<CashierName />
</t>
@@ -23,7 +30,7 @@
<SyncNotification />
<HeaderButton />
</div>
- </div>
+ </Portal>
<div class="pos-content">
<div class="window">
<div class="subwindow">
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
deleted file mode 100644
index 353ea5f93a4..00000000000
--- a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<templates id="template" xml:space="preserve">
-
- <div class="order-selector" t-name="OrderSelector" owl="1">
- <t t-if="chromeContext.showOrderSelector">
- <span class="orders">
- <t t-foreach="orderList" t-as="order" t-key="order.cid">
- <t t-if="order === env.pos.get_order()">
- <span class="order-button select-order selected">
- <span class="order-sequence">
- <t t-esc="order.sequence_number" />
- </span>
- <t t-esc="creationTime(order)" />
- </span>
- </t>
- <t t-else="">
- <span class="order-button select-order" t-on-click="selectOrder(order)">
- <span class="order-sequence">
- <t t-esc="order.sequence_number" />
- </span>
- </span>
- </t>
- </t>
- </span>
- <span class="order-button square neworder-button" t-on-click="addNewOrder">
- <i class="fa fa-plus" role="img" aria-label="New order" title="New order" />
- </span>
- <span class="order-button square deleteorder-button" t-on-click="deleteCurrentOrder">
- <i class="fa fa-minus" role="img" aria-label="Delete order" title="Delete order" />
- </span>
- </t>
- </div>
-
-</templates>
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml
new file mode 100644
index 00000000000..66d31dd5584
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="TicketButton" owl="1">
+ <div class="ticket-button" t-att-class="{ highlight: props.isTicketScreenShown }" t-on-click="onClick">
+ <div class="with-badge" t-att-badge="count">
+ <i class="fa fa-ticket" aria-hidden="true"></i>
+ </div>
+ <div>Tickets</div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml b/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml
new file mode 100644
index 00000000000..a480f169520
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="point_of_sale.SearchBar" owl="1">
+ <div class="pos-search-bar">
+ <div class="search">
+ <span class="search-icon"><i class="fa fa-search"></i></span>
+ <input class="radius-left" t-att-class="{ 'radius-right': !props.config.filter.show }"
+ t-model="state.searchInput" t-on-keydown="onKeydown" type="text" t-att-placeholder="placeholder" />
+ <ul t-if="state.showSearchFields and state.searchInput" class="fields">
+ <t t-foreach="config.searchFields" t-as="value" t-key="value_index">
+ <li t-att-class="{ highlight: value_index == state.selectedFieldId }"
+ t-on-click="onClickSearchField(value_index)">
+ <span class="field">
+ <t t-esc="value"></t>
+ </span>
+ <span>: </span>
+ <span class="term">
+ <t t-esc="state.searchInput"></t>
+ </span>
+ </li>
+ </t>
+ </ul>
+ </div>
+ <div t-if="props.config.filter.show" class="filter radius-right"
+ t-on-click.stop="state.showFilterOptions = !state.showFilterOptions">
+ <span class="down-icon">
+ <i class="fa fa-chevron-down" aria-hidden="true"></i>
+ </span>
+ <span>
+ <t t-esc="state.selectedFilter" />
+ </span>
+ <ul t-if="state.showFilterOptions" class="options">
+ <t t-foreach="config.filter.options" t-as="option" t-key="option">
+ <li t-on-click="selectFilter(option)">
+ <t t-esc="option"></t>
+ </li>
+ </t>
+ </ul>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
index c0a49a555b2..6b3f04bb320 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
@@ -3,9 +3,9 @@
<t t-name="ProductsWidget" owl="1">
<div class="products-widget">
- <ProductsWidgetControlPanel breadcrumbs="breadcrumbs"
- subcategories="subcategories" />
-
+ <t t-if="!hasNoCategories">
+ <ProductsWidgetControlPanel breadcrumbs="breadcrumbs" subcategories="subcategories" />
+ </t>
<ProductList products="productsToDisplay" searchWord="searchWord" />
</div>
</t>
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
index c80a2a6d1ef..09c17501d52 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
@@ -3,7 +3,10 @@
<t t-name="ProductsWidgetControlPanel" owl="1">
<div class="products-widget-control">
- <div class="rightpane-header">
+ <div class="rightpane-header" t-att-class="{
+ 'green-border-bottom': !env.pos.config.iface_display_categ_images,
+ 'grey-border-bottom': env.pos.config.iface_display_categ_images,
+ }">
<!-- Breadcrumbs -->
<div class="breadcrumbs">
<HomeCategoryBreadcrumb subcategories="props.subcategories" currentCat="props.breadcrumbs[props.breadcrumbs.length - 1]"/>
@@ -13,36 +16,33 @@
</t>
</t>
</div>
- <!-- Searchbox -->
- <div class="searchbox">
- <input t-ref="search-word-input" placeholder="Search Products"
- t-on-keyup="updateSearch" />
- <span class="search-clear left">
- <i class="fa fa-search" />
- </span>
- <span class="search-clear right" t-on-click="clearSearch">
- <i class="fa fa-remove" />
- </span>
- </div>
+ <!-- Subcategories -->
+ <t t-if="props.subcategories.length > 0 and !env.pos.config.iface_display_categ_images and !env.isMobile">
+ <t t-foreach="props.subcategories" t-as="category" t-key="category.id">
+ <CategorySimpleButton category="category" />
+ </t>
+ </t>
</div>
- <!-- Subcategories -->
- <t t-if="props.subcategories.length > 0 &amp;&amp; !env.isMobile">
+ <t t-if="props.subcategories.length > 0 and env.pos.config.iface_display_categ_images and !env.isMobile">
<div class="categories">
<div class="category-list-scroller">
- <div class="category-list"
- t-att-class="{simple: !env.pos.config.iface_display_categ_images}">
+ <div class="category-list">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
- <t t-if="env.pos.config.iface_display_categ_images">
- <CategoryButton category="category" />
- </t>
- <t t-else="">
- <CategorySimpleButton category="category" />
- </t>
+ <CategoryButton category="category" />
</t>
</div>
</div>
</div>
</t>
+ <Portal target="'.pos .search-bar-portal'">
+ <div class="search-box">
+ <span class="icon"><i class="fa fa-search"></i></span>
+ <span t-on-click="clearSearch" class="clear-icon">
+ <i class="fa fa-times" aria-hidden="true"></i>
+ </span>
+ <input t-ref="search-word-input" type="text" placeholder="Search Products..." t-on-keyup="updateSearch" />
+ </div>
+ </Portal>
</div>
</t>
diff --git a/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml b/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml
new file mode 100644
index 00000000000..2aa890e029d
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="TicketScreen" owl="1">
+ <div class="ticket-screen screen">
+ <div class="screen-content">
+ <div class="controls">
+ <div class="buttons">
+ <button t-if="showNewTicketButton" class="highlight" t-on-click="createNewOrder">New Ticket</button>
+ <button class="discard" t-on-click="trigger('close-screen')">Discard</button>
+ </div>
+ <SearchBar config="searchBarConfig" placeholder="'Search Tickets...'" />
+ </div>
+ <div class="orders">
+ <div class="header-row">
+ <div class="col start wide">Date</div>
+ <div class="col start wide">Receipt Number</div>
+ <div class="col start">Customer</div>
+ <div class="col start">Employee</div>
+ <div class="col end">Total</div>
+ <div class="col start narrow">Status</div>
+ <div class="col center very-narrow" name="delete"></div>
+ </div>
+ <t t-foreach="filteredOrderList" t-as="order" t-key="order.cid">
+ <div class="order-row pointer" t-on-click="selectOrder(order)">
+ <div class="col start wide">
+ <t t-esc="getDate(order)"></t>
+ </div>
+ <div class="col start wide">
+ <t t-esc="order.name"></t>
+ </div>
+ <div class="col start">
+ <t t-esc="getCustomer(order)"></t>
+ </div>
+ <div class="col start">
+ <t t-esc="getEmployee(order)"></t>
+ </div>
+ <div class="col end">
+ <t t-esc="getTotal(order)"></t>
+ </div>
+ <div class="col start narrow">
+ <t t-esc="getStatus(order)"></t>
+ </div>
+ <div t-if="!hideDeleteButton(order)" class="col center very-narrow delete-button" name="delete" t-on-click.stop="deleteOrder(order)">
+ <i class="fa fa-trash" aria-hidden="true"></i>
+ </div>
+ <div t-else="" class="col center very-narrow delete-button"></div>
+ </div>
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+
+</templates>