From 9ea0aca6ea30fe42e1a7e0a4eaeb79f92a521152 Mon Sep 17 00:00:00 2001 From: Joseph Caburnay Date: Thu, 9 Jul 2020 12:07:14 +0000 Subject: [PATCH] [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 --- addons/point_of_sale/__manifest__.py | 4 +- addons/point_of_sale/models/pos_order.py | 2 + addons/point_of_sale/static/src/css/pos.css | 417 ++++++++++++++++-- addons/point_of_sale/static/src/js/Chrome.js | 8 +- .../src/js/ChromeWidgets/OrderSelector.js | 56 --- .../src/js/ChromeWidgets/TicketButton.js | 41 ++ .../src/js/Misc/IndependentToOrderScreen.js | 15 +- .../static/src/js/Misc/SearchBar.js | 115 +++++ .../OrderManagementScreen.js | 3 +- .../js/Screens/PaymentScreen/PaymentScreen.js | 4 +- .../Screens/ProductScreen/ProductsWidget.js | 3 + .../js/Screens/TicketScreen/TicketScreen.js | 181 ++++++++ addons/point_of_sale/static/src/js/main.js | 2 + addons/point_of_sale/static/src/js/models.js | 44 +- addons/point_of_sale/static/src/js/utils.js | 4 +- .../point_of_sale/static/src/xml/Chrome.xml | 25 +- .../src/xml/ChromeWidgets/OrderSelector.xml | 34 -- .../src/xml/ChromeWidgets/TicketButton.xml | 13 + .../static/src/xml/Misc/SearchBar.xml | 44 ++ .../Screens/ProductScreen/ProductsWidget.xml | 6 +- .../ProductsWidgetControlPanel.xml | 44 +- .../xml/Screens/TicketScreen/TicketScreen.xml | 55 +++ .../static/tests/tours/Chrome.tour.js | 44 +- .../tests/tours/OrderManagementScreen.tour.js | 7 +- .../static/tests/tours/TicketScreen.tour.js | 54 +++ .../tests/tours/helpers/ChromeTourMethods.js | 35 +- .../tours/helpers/ProductScreenTourMethods.js | 2 +- .../tours/helpers/TicketScreenTourMethods.js | 100 +++++ .../static/tests/unit/helpers/test_env.js | 3 + .../static/tests/unit/test_ChromeWidgets.js | 47 -- .../static/tests/unit/test_ProductScreen.js | 10 +- addons/point_of_sale/tests/test_frontend.py | 4 + .../point_of_sale/views/pos_assets_common.xml | 4 +- .../point_of_sale/views/pos_assets_index.xml | 2 + addons/pos_hr/static/src/css/pos.css | 4 +- addons/pos_restaurant/__manifest__.py | 3 +- .../src/js/ChromeWidgets/BackToFloorButton.js | 22 +- .../src/js/ChromeWidgets/OrderSelector.js | 20 - .../src/js/ChromeWidgets/TicketButton.js | 33 ++ .../src/js/Screens/FloorScreen/FloorScreen.js | 4 +- .../OrderManagementScreen.js | 3 +- .../js/Screens/ReceiptScreen/ReceiptScreen.js | 12 + .../SplitBillScreen/SplitBillScreen.js | 4 +- .../static/src/js/Screens/TicketScreen.js | 54 +++ addons/pos_restaurant/static/src/js/floors.js | 22 +- .../pos_restaurant/static/src/xml/Chrome.xml | 10 + .../xml/ChromeWidgets/BackToFloorButton.xml | 3 +- .../src/xml/ChromeWidgets/OrderSelector.xml | 20 - .../static/src/xml/Screens/TicketScreen.xml | 15 + .../tests/tours/OrderManagementScreen.tour.js | 7 +- .../tests/tours/SplitBillScreen.tour.js | 4 +- .../static/tests/tours/TicketScreen.tour.js | 44 ++ .../tours/helpers/FloorScreenTourMethods.js | 21 +- .../static/tests/tours/pos_restaurant.js | 310 ++++--------- addons/pos_restaurant/tests/test_frontend.py | 4 + .../views/pos_restaurant_templates.xml | 4 +- 56 files changed, 1468 insertions(+), 587 deletions(-) delete mode 100644 addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js create mode 100644 addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js create mode 100644 addons/point_of_sale/static/src/js/Misc/SearchBar.js create mode 100644 addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js delete mode 100644 addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml create mode 100644 addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml create mode 100644 addons/point_of_sale/static/src/xml/Misc/SearchBar.xml create mode 100644 addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml create mode 100644 addons/point_of_sale/static/tests/tours/TicketScreen.tour.js create mode 100644 addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js delete mode 100644 addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js create mode 100644 addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js create mode 100644 addons/pos_restaurant/static/src/js/Screens/TicketScreen.js create mode 100644 addons/pos_restaurant/static/src/xml/Chrome.xml delete mode 100644 addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml create mode 100644 addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml create mode 100644 addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py index b66811bc141..3a7819c02d8 100644 --- a/addons/point_of_sale/__manifest__.py +++ b/addons/point_of_sale/__manifest__.py @@ -64,6 +64,7 @@ 'static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml', 'static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml', 'static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml', + 'static/src/xml/Screens/TicketScreen/TicketScreen.xml', 'static/src/xml/Screens/PaymentScreen/PSNumpadInputButton.xml', 'static/src/xml/Screens/PaymentScreen/PaymentScreenNumpad.xml', 'static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml', @@ -88,16 +89,17 @@ 'static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml', 'static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml', 'static/src/xml/Screens/ScaleScreen/ScaleScreen.xml', - 'static/src/xml/ChromeWidgets/OrderSelector.xml', 'static/src/xml/ChromeWidgets/CashierName.xml', 'static/src/xml/ChromeWidgets/ProxyStatus.xml', 'static/src/xml/ChromeWidgets/SyncNotification.xml', 'static/src/xml/ChromeWidgets/OrderManagementButton.xml', 'static/src/xml/ChromeWidgets/HeaderButton.xml', 'static/src/xml/ChromeWidgets/SaleDetailsButton.xml', + 'static/src/xml/ChromeWidgets/TicketButton.xml', 'static/src/xml/SaleDetailsReport.xml', 'static/src/xml/Misc/Draggable.xml', 'static/src/xml/Misc/NotificationSound.xml', + 'static/src/xml/Misc/SearchBar.xml', 'static/src/xml/ChromeWidgets/DebugWidget.xml', 'static/src/xml/Popups/ErrorPopup.xml', 'static/src/xml/Popups/ErrorBarcodePopup.xml', diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index cab1db89058..ecdb7ae0e45 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -578,6 +578,8 @@ class PosOrder(models.Model): """ orders = self.search([('id', 'in', server_ids),('state', '=', 'draft')]) orders.write({'state': 'cancel'}) + # TODO Looks like delete cascade is a better solution. + orders.mapped('payment_ids').sudo().unlink() orders.sudo().unlink() return orders.ids 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> + */ + 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 @@
+
+
+
+ + +
+
+ +
+
+
+
-
-
-
- -
-
- + +
@@ -23,7 +30,7 @@
-
+
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 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
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 @@ + + + + +
+
+ +
+
Tickets
+
+
+ +
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 @@ + + + + + + + + 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 @@
- - + + +
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 @@
-
+
- - + + + + + +
- - +
-
+
- - - - - - +
+ + +
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 @@ + + + + +
+
+
+
+ + +
+ +
+
+
+
Date
+
Receipt Number
+
Customer
+
Employee
+
Total
+
Status
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ +
diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js index 7789af6e7da..438925518f9 100644 --- a/addons/point_of_sale/static/tests/tours/Chrome.tour.js +++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js @@ -4,6 +4,7 @@ odoo.define('point_of_sale.tour.Chrome', function (require) { const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); const { getSteps, startSteps } = require('point_of_sale.tour.utils'); var Tour = require('web_tour.tour'); @@ -13,62 +14,75 @@ odoo.define('point_of_sale.tour.Chrome', function (require) { // Order 1 is at Product Screen ProductScreen.do.clickHomeCategory(); ProductScreen.exec.addOrderline('Desk Pad', '1', '2'); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0001', 'Ongoing'); // Order 2 is at Payment Screen - Chrome.do.newOrder(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline('Monitor Stand', '3', '4'); ProductScreen.do.clickPayButton(); PaymentScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0002', 'Payment'); // Order 3 is at Receipt Screen - Chrome.do.newOrder(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6'); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod('Bank'); PaymentScreen.do.clickValidate(); ReceiptScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0003', 'Receipt'); // Select order 1, should be at Product Screen - Chrome.do.selectOrder('1'); + TicketScreen.do.selectOrder('-0001'); ProductScreen.check.productIsDisplayed('Desk Pad'); ProductScreen.check.selectedOrderlineHas('Desk Pad', '1.0', '2.0'); // Select order 2, should be at Payment Screen - Chrome.do.selectOrder('2'); + 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.selectOrder('3'); + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0003'); ReceiptScreen.check.changeIs('0.0'); // Pay order 1, with change - Chrome.do.selectOrder('1'); + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0001'); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod('Cash'); PaymentScreen.do.pressNumpad('2 0'); PaymentScreen.do.clickValidate(); ReceiptScreen.check.changeIs('18.0'); + // Order 1 now should have Receipt status + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0001', 'Receipt'); + // Select order 3, should still be at Receipt Screen // but change should be different. - Chrome.do.selectOrder('3'); + TicketScreen.do.selectOrder('-0003'); ReceiptScreen.check.changeIs('0.0'); // click next screen on order 3 // then delete the new empty order ReceiptScreen.do.clickNextOrder(); ProductScreen.check.orderIsEmpty(); - Chrome.do.deleteOrder(); + Chrome.do.clickTicketButton(); + TicketScreen.do.deleteOrder('-0004'); + TicketScreen.do.deleteOrder('-0001'); - // Order 2 should be the current order - // Deleting it should open a popup, confirm it. - Chrome.do.deleteOrder(); + // After deleting order 1 above, order 2 became + // the 2nd-row order and it has payment status + TicketScreen.check.nthRowContains(2, 'Payment') + TicketScreen.do.deleteOrder('-0002'); Chrome.do.confirmPopup(); - - // Now left with order 1 in payment screen - // go next screen - ReceiptScreen.do.clickNextOrder(); + TicketScreen.do.clickNewTicket(); // Invoice an order ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6'); diff --git a/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js index 44a09579fe3..e41b4d3b687 100644 --- a/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js @@ -5,6 +5,7 @@ odoo.define('point_of_sale.tour.OrderManagementScreen', function (require) { const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); const { ClientListScreen } = require('point_of_sale.tour.ClientListScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); const { makeFullOrder } = require('point_of_sale.tour.CompositeTourMethods'); const { getSteps, startSteps } = require('point_of_sale.tour.utils'); @@ -70,11 +71,13 @@ odoo.define('point_of_sale.tour.OrderManagementScreen', function (require) { // Add 2 orders, they should appear in order management screen // order 0006 - Chrome.do.newOrder(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline('Whiteboard Pen', '66', '6'); // order 0007, should be at payment screen - Chrome.do.newOrder(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline('Monitor Stand', '55', '5'); ProductScreen.do.clickCustomerButton(); ClientListScreen.exec.setClient('Azure Interior'); diff --git a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js new file mode 100644 index 00000000000..4bf05d02fdd --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js @@ -0,0 +1,54 @@ +odoo.define('point_of_sale.tour.TicketScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { Chrome } = require('point_of_sale.tour.ChromeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + ProductScreen.do.clickHomeCategory(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '2'); + ProductScreen.do.clickCustomerButton(); + ProductScreen.do.clickCustomer('Nicole Ford'); + ProductScreen.do.clickSetCustomer(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(2, 'Nicole Ford'); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '3'); + ProductScreen.do.clickCustomerButton(); + ProductScreen.do.clickCustomer('Brandon Freeman'); + ProductScreen.do.clickSetCustomer(); + ProductScreen.do.clickPayButton(); + PaymentScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(3, 'Brandon Freeman'); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Desk Pad', '1', '4'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Bank'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(4, 'Receipt'); + TicketScreen.do.selectFilter('Receipt'); + TicketScreen.check.nthRowContains(2, 'Receipt'); + TicketScreen.do.selectFilter('Payment'); + TicketScreen.check.nthRowContains(2, 'Payment'); + TicketScreen.do.selectFilter('Ongoing'); + TicketScreen.check.nthRowContains(2, 'Ongoing'); + TicketScreen.do.selectFilter('All'); + TicketScreen.check.nthRowContains(4, 'Receipt'); + TicketScreen.do.search('Customer', 'Nicole'); + TicketScreen.check.nthRowContains(2, 'Nicole'); + TicketScreen.do.search('Customer', 'Brandon'); + TicketScreen.check.nthRowContains(2, 'Brandon'); + TicketScreen.do.search('Receipt Number', '-0003'); + TicketScreen.check.nthRowContains(2, 'Receipt'); + + Tour.register('TicketScreenTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js index a5bd006fad1..58f3bfa4215 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js @@ -4,30 +4,6 @@ odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) { const { createTourMethods } = require('point_of_sale.tour.utils'); class Do { - newOrder() { - return [ - { - content: 'new order', - trigger: '.order-selector .neworder-button', - }, - ]; - } - deleteOrder() { - return [ - { - content: 'delete current order', - trigger: '.order-selector .deleteorder-button', - }, - ]; - } - selectOrder(orderSequence) { - return [ - { - content: `select order '${orderSequence}'`, - trigger: `.order-selector .order-sequence:contains("${orderSequence}")`, - }, - ]; - } confirmPopup() { return [ { @@ -49,6 +25,17 @@ odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) { }, ]; } + clickTicketButton() { + return [ + { + trigger: '.pos-topheader .ticket-button > div:contains("Tickets")', + }, + { + trigger: '.subwindow .ticket-screen', + run: () => {}, + }, + ]; + } } return createTourMethods('Chrome', Do); diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js index 2bb4d025ae0..dab5ae15683 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -31,7 +31,7 @@ odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) { return [ { content: `selecting '${name}' subcategory`, - trigger: `.category-list .category-simple-button:contains("${name}")`, + trigger: `.products-widget > .products-widget-control .category-simple-button:contains("${name}")`, }, { content: `'${name}' subcategory selected`, diff --git a/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js new file mode 100644 index 00000000000..9fa0c9cd558 --- /dev/null +++ b/addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js @@ -0,0 +1,100 @@ +odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickNewTicket() { + return [{ trigger: '.ticket-screen .highlight' }]; + } + clickDiscard() { + return [{ trigger: '.ticket-screen button.discard' }]; + } + selectOrder(orderName) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}")`, + }, + ]; + } + deleteOrder(orderName) { + return [ + { + trigger: `.ticket-screen .orders > .order-row > .col:contains("${orderName}") ~ .col[name="delete"]`, + }, + ]; + } + selectFilter(name) { + return [ + { + trigger: `.pos-search-bar .filter`, + }, + { + trigger: `.pos-search-bar .filter ul`, + run: () => {}, + }, + { + trigger: `.pos-search-bar .filter ul li:contains("${name}")`, + }, + ]; + } + search(field, searchWord) { + return [ + { + trigger: '.pos-search-bar input', + run: `text ${searchWord}`, + }, + { + /** + * Manually trigger keydown event to show the search field list + * because the previous step do not trigger keydown event. + */ + trigger: '.pos-search-bar input', + run: function () { + document + .querySelector('.pos-search-bar input') + .dispatchEvent(new KeyboardEvent('keydown', { key: '' })); + }, + }, + { + trigger: `.pos-search-bar .search ul li:contains("${field}")`, + }, + ]; + } + } + + class Check { + checkStatus(orderName, status) { + return [ + { + trigger: `.ticket-screen .order-row > .col:nth-child(2):contains("${orderName}") ~ .col:nth-child(6):contains(${status})`, + run: () => {}, + }, + ]; + } + /** + * Check if the nth row contains the given string. + * Note that 1st row is the header-row. + */ + nthRowContains(n, string) { + return [ + { + trigger: `.ticket-screen .orders > .order-row:nth-child(${n}):contains("${string}")`, + run: () => {}, + }, + ]; + } + noNewTicketButton() { + return [ + { + trigger: '.ticket-screen .controls .buttons:nth-child(1):has(.discard)', + run: () => {}, + }, + ]; + } + } + + class Execute {} + + return createTourMethods('TicketScreen', Do, Check, Execute); +}); diff --git a/addons/point_of_sale/static/tests/unit/helpers/test_env.js b/addons/point_of_sale/static/tests/unit/helpers/test_env.js index 81ce9ce3ac4..c4b0b3ecef6 100644 --- a/addons/point_of_sale/static/tests/unit/helpers/test_env.js +++ b/addons/point_of_sale/static/tests/unit/helpers/test_env.js @@ -11,6 +11,9 @@ odoo.define('point_of_sale.test_env', async function (require) { const makeTestEnvironment = require('web.test_env'); const env = require('web.env'); const models = require('point_of_sale.models'); + const Registries = require('point_of_sale.Registries'); + + Registries.Component.add(owl.misc.Portal); await env.session.is_bound; const pos = new models.PosModel({ diff --git a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js index c1ad31804e6..a0df97fdbd4 100644 --- a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js +++ b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js @@ -49,53 +49,6 @@ odoo.define('point_of_sale.tests.ChromeWidgets', function (require) { parent.destroy(); }); - QUnit.test('OrderSelector', async function (assert) { - assert.expect(4); - - class Parent extends PopupControllerMixin(PosComponent) {} - Parent.env = makePosTestEnv(); - Parent.env.chrome = new owl.Context({ showOrderSelector: true }); - Parent.template = xml/* html */ ` -
- - -
- `; - - const pos = Parent.env.pos; - - const parent = new Parent(); - await parent.mount(testUtils.prepareTarget()); - - const plusButton = parent.el.querySelector('.neworder-button'); - const minusButton = parent.el.querySelector('.deleteorder-button'); - - await testUtils.dom.click(plusButton); - await testUtils.nextTick(); - assert.strictEqual(2, parent.el.querySelectorAll('.order-sequence').length); - await testUtils.dom.click(minusButton); - await testUtils.nextTick(); - assert.strictEqual(1, parent.el.querySelectorAll('.order-sequence').length); - - const product = Object.values(pos.db.product_by_id)[0]; - pos.get_order().add_product(product); - - // try deleting the order with orderline - await testUtils.dom.click(minusButton); - await testUtils.nextTick(); - - // confirm popup should appear - assert.ok(parent.el.querySelector('.popup')); - // confirm deletion - await testUtils.dom.click(parent.el.querySelector('.confirm')); - await testUtils.nextTick(); - // there should be new order created - assert.strictEqual(1, parent.el.querySelectorAll('.order-sequence').length); - - parent.unmount(); - parent.destroy(); - }); - QUnit.test('SyncNotification', async function (assert) { assert.expect(5); diff --git a/addons/point_of_sale/static/tests/unit/test_ProductScreen.js b/addons/point_of_sale/static/tests/unit/test_ProductScreen.js index 042048c72eb..bc717da38fe 100644 --- a/addons/point_of_sale/static/tests/unit/test_ProductScreen.js +++ b/addons/point_of_sale/static/tests/unit/test_ProductScreen.js @@ -244,8 +244,10 @@ odoo.define('point_of_sale.tests.ProductScreen', function (require) { } Parent.env = makePosTestEnv(); Parent.template = xml/* html */ ` -
- +
+
+ +
`; @@ -337,7 +339,7 @@ odoo.define('point_of_sale.tests.ProductScreen', function (require) { }); }; - const inputEl = parent.el.querySelector('.searchbox input'); + const inputEl = parent.el.querySelector('.search-box input'); await testUtils.dom.triggerEvent(inputEl, 'keyup', { key: 'A' }); // Triggering keyup event doesn't type the key to the input // so we manually assign the value of the input. @@ -358,7 +360,7 @@ odoo.define('point_of_sale.tests.ProductScreen', function (require) { assert.verifySteps(['ABCD']); // clear the search bar - await testUtils.dom.click(parent.el.querySelector('.search-clear.right')); + await testUtils.dom.click(parent.el.querySelector('.search-box .clear-icon')); await testUtils.nextTick(); assert.verifySteps(['cleared']); assert.ok(inputEl.value === '', 'value of the input element should be empty'); diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py index 73e9142de86..71b93b4181c 100644 --- a/addons/point_of_sale/tests/test_frontend.py +++ b/addons/point_of_sale/tests/test_frontend.py @@ -510,3 +510,7 @@ class TestUi(TestPointOfSaleHttpCommon): self.main_pos_config.write({ 'product_configurator': True }) self.main_pos_config.open_session_cb(check_coa=False) self.start_tour("/pos/web?config_id=%d" % self.main_pos_config, 'ProductConfiguratorTour', login="admin", step_delay=50) + + def test_05_ticket_screen(self): + self.main_pos_config.open_session_cb(check_coa=False) + self.start_tour("/pos/web?config_id=%d" % self.main_pos_config.id, 'TicketScreenTour', login="admin", step_delay=50) diff --git a/addons/point_of_sale/views/pos_assets_common.xml b/addons/point_of_sale/views/pos_assets_common.xml index 1c191806a40..38387b5dc51 100644 --- a/addons/point_of_sale/views/pos_assets_common.xml +++ b/addons/point_of_sale/views/pos_assets_common.xml @@ -44,6 +44,7 @@ + @@ -68,17 +69,18 @@ - + + diff --git a/addons/point_of_sale/views/pos_assets_index.xml b/addons/point_of_sale/views/pos_assets_index.xml index 65799f3c027..c32a7f645be 100644 --- a/addons/point_of_sale/views/pos_assets_index.xml +++ b/addons/point_of_sale/views/pos_assets_index.xml @@ -6,6 +6,7 @@ + @@ -23,6 +24,7 @@ + diff --git a/addons/pos_hr/static/src/css/pos.css b/addons/pos_hr/static/src/css/pos.css index 6117d89b7b4..3d27b1fd634 100644 --- a/addons/pos_hr/static/src/css/pos.css +++ b/addons/pos_hr/static/src/css/pos.css @@ -122,11 +122,11 @@ .pos .pos-rightheader .header-button.lock-button { font-size: 20px; - color: green; + color: rgb(94, 185, 55); transition: all 200ms ease-in-out; width: 18px; } .pos .pos-rightheader .header-button.lock-button:hover { - color: red; + color: rgb(197, 52, 0); } diff --git a/addons/pos_restaurant/__manifest__.py b/addons/pos_restaurant/__manifest__.py index fab9834bb16..6b5e14d4b0c 100644 --- a/addons/pos_restaurant/__manifest__.py +++ b/addons/pos_restaurant/__manifest__.py @@ -27,6 +27,8 @@ This module adds several features to the Point of Sale that are specific to rest ], 'qweb': [ 'static/src/xml/Resizeable.xml', + 'static/src/xml/Chrome.xml', + 'static/src/xml/Screens/TicketScreen.xml', 'static/src/xml/Screens/OrderManagementScreen/OrderList.xml', 'static/src/xml/Screens/OrderManagementScreen/OrderRow.xml', 'static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml', @@ -44,7 +46,6 @@ This module adds several features to the Point of Sale that are specific to rest 'static/src/xml/Screens/FloorScreen/EditBar.xml', 'static/src/xml/Screens/FloorScreen/TableWidget.xml', 'static/src/xml/Screens/FloorScreen/EditableTable.xml', - 'static/src/xml/ChromeWidgets/OrderSelector.xml', 'static/src/xml/ChromeWidgets/BackToFloorButton.xml', 'static/src/xml/multiprint.xml', ], diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js index dbd21c5fd32..c2b623500c8 100644 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js @@ -1,22 +1,28 @@ -odoo.define('pos_restaurant.BackToFloorButton', function(require) { +odoo.define('pos_restaurant.BackToFloorButton', function (require) { 'use strict'; const PosComponent = require('point_of_sale.PosComponent'); - const { useListener } = require('web.custom_hooks'); const Registries = require('point_of_sale.Registries'); + const { posbus } = require('point_of_sale.utils'); class BackToFloorButton extends PosComponent { - constructor() { - super(...arguments); - useListener('click', this._backToFloorScreen); + mounted() { + posbus.on('table-set', this, this.render); + } + willUnmount() { + posbus.on('table-set', this); } get table() { - return this.props.table; + return (this.env.pos && this.env.pos.table) || null; } get floor() { - return this.props.table.floor; + const table = this.table; + return table ? table.floor : null; } - async _backToFloorScreen() { + get hasTable() { + return this.table !== null; + } + backToFloorScreen() { this.showScreen('FloorScreen', { floor: this.floor }); } } diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js deleted file mode 100644 index 255b84db073..00000000000 --- a/addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js +++ /dev/null @@ -1,20 +0,0 @@ -odoo.define('pos_restaurant.OrderSelector', function(require) { - 'use strict'; - - const OrderSelector = require('point_of_sale.OrderSelector'); - const Registries = require('point_of_sale.Registries'); - - const PosResOrderSelector = OrderSelector => - class extends OrderSelector { - get currentTable() { - return this.env.pos.table; - } - get backToFloorButtonIsShown() { - return this.env.pos.config.iface_floorplan && this.currentTable; - } - }; - - Registries.Component.extend(OrderSelector, PosResOrderSelector); - - return OrderSelector; -}); diff --git a/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js new file mode 100644 index 00000000000..1e66bf91352 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js @@ -0,0 +1,33 @@ +odoo.define('pos_restaurant.TicketButton', function (require) { + 'use strict'; + + const TicketButton = require('point_of_sale.TicketButton'); + const Registries = require('point_of_sale.Registries'); + const { posbus } = require('point_of_sale.utils'); + + const PosResTicketButton = (TicketButton) => + class extends TicketButton { + mounted() { + posbus.on('table-set', this, this.render); + } + willUnmount() { + posbus.off('table-set', this); + } + /** + * If no table is set to pos, which means the current main screen + * is floor screen, then the order count should be based on all the orders. + */ + get count() { + if (!this.env.pos || !this.env.pos.config) return 0; + if (this.env.pos.config.iface_floorplan && !this.env.pos.table) { + return this.env.pos.get('orders').models.length; + } else { + return super.count; + } + } + }; + + Registries.Component.extend(TicketButton, PosResTicketButton); + + return TicketButton; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js index 276663cf57c..1bf06c838f5 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -171,12 +171,12 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { } } } - async _onSelectTable(event) { + _onSelectTable(event) { const table = event.detail; if (this.state.isEditMode) { this.state.selectedTableId = table.id; } else { - await this.env.pos.set_table(table); + this.env.pos.set_table(table); } } _onDeselectTable() { diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js index e2a1ce65dce..8753be25753 100644 --- a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js @@ -12,10 +12,9 @@ odoo.define('pos_restaurant.OrderManagementScreen', function (require) { _setOrder(order) { if (this.env.pos.config.module_pos_restaurant) { const currentOrder = this.env.pos.get_order(); + this.env.pos.set_table(order.table, order); if (currentOrder && currentOrder.uid === order.uid) { this.close(); - } else { - this.env.pos.set_table(order.table, order); } } else { super._setOrder(order); diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 689c3bf40ca..5909292e0db 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -20,6 +20,18 @@ odoo.define('pos_restaurant.ReceiptScreen', function(require) { return super.nextScreen; } } + /** + * If this order is synced to/from the server, then the draft needs to be deleted + * after the order is paid and the receipt screen is exited via 'Next Order' button. + * Thus, we set this order to be removed from server. Note that only the draft version + * is deleted if we trace the algorithm. + */ + orderDone() { + if (this.env.pos.config.iface_floorplan) { + this.env.pos.db.set_order_to_remove_from_server(this.currentOrder); + } + super.orderDone(); + } }; Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js index 2390e35f7c4..f44af5bff7a 100644 --- a/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js @@ -55,7 +55,7 @@ odoo.define('pos_restaurant.SplitBillScreen', function(require) { } else { this._setQuantityOnCurrentOrder(); - this.newOrder.set_screen_data('screen', { name: 'PaymentScreen' }); + this.newOrder.set_screen_data({ name: 'PaymentScreen' }); // for the kitchen printer we assume that everything // has already been sent to the kitchen before splitting @@ -72,7 +72,7 @@ odoo.define('pos_restaurant.SplitBillScreen', function(require) { this.newOrder.set_customer_count(1); const newCustomerCount = this.currentOrder.get_customer_count() - 1; this.currentOrder.set_customer_count(newCustomerCount || 1); - this.currentOrder.set_screen_data('screen', { name: 'ProductScreen' }); + this.currentOrder.set_screen_data({ name: 'ProductScreen' }); this.env.pos.get('orders').add(this.newOrder); this.env.pos.set('selectedOrder', this.newOrder); diff --git a/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js new file mode 100644 index 00000000000..2d5c3e004b2 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TicketScreen.js @@ -0,0 +1,54 @@ +odoo.define('pos_restaurant.TicketScreen', function (require) { + 'use strict'; + + const TicketScreen = require('point_of_sale.TicketScreen'); + const Registries = require('point_of_sale.Registries'); + const { posbus } = require('point_of_sale.utils'); + + const PosResTicketScreen = (TicketScreen) => + class extends TicketScreen { + close() { + super.close(); + if (!this.env.pos.config.iface_floorplan) { + // Make sure the 'table-set' event is triggered + // to properly rerender the components that listens to it. + posbus.trigger('table-set'); + } + } + getTable(order) { + return `${order.table.floor.name} (${order.table.name})`; + } + get _searchFields() { + if (!this.env.pos.config.iface_floorplan) { + return super._searchFields; + } + return Object.assign({}, super._searchFields, { + Table: (order) => `${order.table.floor.name} (${order.table.name})`, + }); + } + _setOrder(order) { + if (!this.env.pos.config.iface_floorplan) { + super._setOrder(order); + } else if (order !== this.env.pos.get_order()) { + // Only call set_table if the order is not the same as the current order. + // This is to prevent syncing to the server because syncing is only intended + // when going back to the floorscreen or opening a table. + this.env.pos.set_table(order.table, order); + } + } + get showNewTicketButton() { + return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.showNewTicketButton; + } + get orderList() { + if (this.env.pos.table) { + return super.orderList; + } else { + return this.env.pos.get('orders').models; + } + } + }; + + Registries.Component.extend(TicketScreen, PosResTicketScreen); + + return TicketScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index b6614c07ebe..51ded242c63 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -2,6 +2,8 @@ odoo.define('pos_restaurant.floors', function (require) { "use strict"; var models = require('point_of_sale.models'); +const { Gui } = require('point_of_sale.Gui'); +const { posbus } = require('point_of_sale.utils'); // At POS Startup, load the floors, and add them to the pos model models.load_models({ @@ -213,7 +215,12 @@ models.PosModel = models.PosModel.extend({ this._get_from_server(table.id).then(function (server_orders) { var orders = self.get_order_list(); orders.forEach(function(order){ - if (order.server_id){ + const screen = order.get_screen_data(); + // Only remove the 'Receipt' status order when 'Next Order' + // from the receipt screen is clicked. We are bypassing the + // removal of paid order here to make sure those orders are + // still in the tickets list. + if (order.server_id && screen.name !== 'ReceiptScreen'){ self.get("orders").remove(order); order.destroy(); } @@ -257,6 +264,7 @@ models.PosModel = models.PosModel.extend({ if(!table){ this.sync_from_server(table, this.get_order_list(), this.get_order_with_uid()); this.set_order(null); + this.table = null; } else if (this.order_to_transfer_to_different_table) { var order_ids = this.get_order_with_uid(); @@ -269,6 +277,7 @@ models.PosModel = models.PosModel.extend({ this.table = table; this.sync_to_server(table, order); } + posbus.trigger('table-set'); }, // if we have tables, we do not load a default order, as the default order will be @@ -284,9 +293,12 @@ models.PosModel = models.PosModel.extend({ add_new_order: function() { if (this.config.iface_floorplan) { if (this.table) { - return _super_posmodel.add_new_order.call(this); + return _super_posmodel.add_new_order.apply(this, arguments); } else { - console.warn("WARNING: orders cannot be created when there is no active table in restaurant mode"); + Gui.showPopup('ConfirmPopup', { + title: 'Unable to create order', + body: 'Orders cannot be created when there is no active table in restaurant mode', + }); return undefined; } } else { @@ -298,7 +310,7 @@ models.PosModel = models.PosModel.extend({ // get the list of unpaid orders (associated to the current table) get_order_list: function() { var orders = _super_posmodel.get_order_list.call(this); - if (!this.config.iface_floorplan) { + if (!(this.config && this.config.iface_floorplan)) { return orders; } else if (!this.table) { return []; @@ -345,7 +357,7 @@ models.PosModel = models.PosModel.extend({ this.db.set_order_to_remove_from_server(removed_order); } if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){ - 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 if (order_list.length === 0) { this.set_order(null); } diff --git a/addons/pos_restaurant/static/src/xml/Chrome.xml b/addons/pos_restaurant/static/src/xml/Chrome.xml new file mode 100644 index 00000000000..86032c07017 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Chrome.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml index 1f36d2763f3..2b5c865ca18 100644 --- a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml +++ b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml @@ -2,7 +2,7 @@ - + @@ -13,6 +13,7 @@ ) + diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml deleted file mode 100644 index 824ff26bcee..00000000000 --- a/addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml new file mode 100644 index 00000000000..7430ee1a6b7 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml @@ -0,0 +1,15 @@ + + + + + +
Table
+
+ +
+ +
+
+
+ +
diff --git a/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js b/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js index 2a198c9e7ab..5cedaba0a14 100644 --- a/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js @@ -11,6 +11,7 @@ odoo.define('pos_restaurant.tour.OrderManagementScreen', function (require) { } = require('point_of_sale.tour.OrderManagementScreenTourMethods'); const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); const { getSteps, startSteps } = require('point_of_sale.tour.utils'); var Tour = require('web_tour.tour'); @@ -36,7 +37,8 @@ odoo.define('pos_restaurant.tour.OrderManagementScreen', function (require) { ['Water', '3', '4'], ['Minute Maid', '5', '6'] ); - Chrome.do.newOrder(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addMultiOrderlines(['Coca-Cola', '1', '2'], ['Minute Maid', '5', '6']); Chrome.do.backToFloor(); @@ -47,7 +49,8 @@ odoo.define('pos_restaurant.tour.OrderManagementScreen', function (require) { ['Water', '9', '10'], ['Minute Maid', '11', '12'] ); - Chrome.do.newOrder(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); ProductScreen.exec.addMultiOrderlines(['Coca-Cola', '13', '14'], ['Minute Maid', '15', '16']); Chrome.do.backToFloor(); diff --git a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js index 82e783dbcc7..9eb138bbb9b 100644 --- a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js @@ -6,6 +6,7 @@ odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) { const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); const { SplitBillScreen } = require('pos_restaurant.tour.SplitBillScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); const { getSteps, startSteps } = require('point_of_sale.tour.utils'); var Tour = require('web_tour.tour'); @@ -42,7 +43,8 @@ odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) { ProductScreen.do.clickOrderline('Coca-Cola', '1.0') // go back to the original order and see if the order is changed - Chrome.do.selectOrder('1'); + Chrome.do.clickTicketButton(); + TicketScreen.do.selectOrder('-0001'); ProductScreen.do.clickOrderline('Water', '2.0') ProductScreen.do.clickOrderline('Minute Maid', '3.0') diff --git a/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js b/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js new file mode 100644 index 00000000000..790ae060777 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js @@ -0,0 +1,44 @@ +odoo.define('pos_restaurant.tour.TicketScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + // New Ticket button should not be in the ticket screen if no table is selected. + Chrome.do.clickTicketButton(); + TicketScreen.check.noNewTicketButton(); + TicketScreen.do.clickDiscard(); + + // Deleting the last order in the table brings back to floorscreen + FloorScreen.do.clickTable('T4'); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains(2, '-0001'); + TicketScreen.do.deleteOrder('-0001'); + FloorScreen.check.isShown(); + + // Create 2 items in a table. From floorscreen, delete 1 item. Then select the other item. + // Correct order and screen should be displayed and the BackToFloorButton is shown. + FloorScreen.do.clickTable('T2'); + ProductScreen.exec.addOrderline('Minute Maid', '1', '2'); + ProductScreen.check.totalAmountIs('2.0'); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + ProductScreen.exec.addOrderline('Coca-Cola', '2', '2'); + ProductScreen.check.totalAmountIs('4.0'); + Chrome.do.backToFloor(); + FloorScreen.check.orderCountSyncedInTableIs('T2', '2'); + Chrome.do.clickTicketButton(); + TicketScreen.do.deleteOrder('-0003'); + Chrome.do.confirmPopup(); + TicketScreen.do.selectOrder('-0002'); + ProductScreen.check.totalAmountIs('2.0'); + Chrome.check.backToFloorTextIs('Main Floor', 'T2'); + + Tour.register('PosResTicketScreenTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js index 6895dc3e257..b6e614357f0 100644 --- a/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js +++ b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js @@ -72,9 +72,7 @@ odoo.define('pos_restaurant.tour.FloorScreenTourMethods', function (require) { return [ { content: `change shape to '${shape}'`, - trigger: `.edit-button .button-option${ - shape === 'round' ? '.square' : '.round' - }`, + trigger: `.edit-button .button-option${shape === 'round' ? '.square' : '.round'}`, }, ]; } @@ -122,6 +120,23 @@ odoo.define('pos_restaurant.tour.FloorScreenTourMethods', function (require) { { content: `number of seats in table '${table}' is '${val}'`, trigger: `.floor-map .tables .table .label:contains("${table}") ~ .table-seats:contains("${val}")`, + run: function () {}, + }, + ]; + } + orderCountSyncedInTableIs(table, count) { + return [ + { + trigger: `.floor-map .table .order-count:contains("${count}") ~ .label:contains("${table}")`, + run: function () {}, + }, + ]; + } + isShown() { + return [ + { + trigger: '.floor-map', + run: function () {}, }, ]; } diff --git a/addons/pos_restaurant/static/tests/tours/pos_restaurant.js b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js index 8d5e8f0a80d..252e9834e63 100644 --- a/addons/pos_restaurant/static/tests/tours/pos_restaurant.js +++ b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js @@ -1,252 +1,108 @@ odoo.define('pos_reataurant.tour.synchronized_table_management', function (require) { - "use strict"; + 'use strict'; - var Tour = require("web_tour.tour"); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + const Tour = require('web_tour.tour'); - function verify_order_total(total_str) { - return [{ - content: 'order total contains ' + total_str, - trigger: '.order .total .value:contains("' + total_str + '")', - run: function () {}, // it's a check - }]; - } + startSteps(); - function verify_orders_synced(order_count) { - return [{ - content: "check synced", - trigger: ".order-sequence", - run: function() { - var orders = $('.order-sequence'); - if (orders.length === order_count) { - return - } else { - throw `sync failed 'there should be ${order_count} orders, however, ${orders.length} orders are displayed' `; - } - }, - }]; - } + FloorScreen.do.clickTable('T5'); - function add_product_to_order(product_name) { - return [{ - content: 'buy ' + product_name, - trigger: '.product-list .product-name:contains("' + product_name + '")', - }, { - content: 'the ' + product_name + ' have been added to the order', - trigger: '.order .product-name:contains("' + product_name + '")', - run: function () {}, // it's a check - }]; - } + // Create first order + ProductScreen.do.clickDisplayedProduct('Coca-Cola'); + ProductScreen.check.selectedOrderlineHas('Coca-Cola'); + ProductScreen.do.clickDisplayedProduct('Water'); + ProductScreen.check.selectedOrderlineHas('Water'); + ProductScreen.check.totalAmountIs('4.40'); - function generate_keypad_steps(amount_str, keypad_selector) { - var i, steps = [], current_char; - for (i = 0; i < amount_str.length; ++i) { - current_char = amount_str[i]; - steps.push({ - content: 'press ' + current_char + ' on payment keypad', - trigger: keypad_selector + ' .input-button:contains("' + current_char + '"):visible' - }); - } + // Create 2nd order (paid) + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + ProductScreen.do.clickDisplayedProduct('Coca-Cola'); + ProductScreen.check.selectedOrderlineHas('Coca-Cola'); + ProductScreen.do.clickDisplayedProduct('Minute Maid'); + ProductScreen.check.selectedOrderlineHas('Minute Maid'); + ProductScreen.check.totalAmountIs('4.40'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.do.clickNextOrder(); - return steps; - } + // After clicking next order, floor screen is shown. + // It should have 1 as number of draft synced order. + FloorScreen.check.orderCountSyncedInTableIs('T5', '1'); + FloorScreen.do.clickTable('T5'); + ProductScreen.check.totalAmountIs('4.40'); - function generate_payment_screen_keypad_steps(amount_str) { - return generate_keypad_steps(amount_str, '.payment-numpad'); - } + // Create another draft order and go back to floor + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + ProductScreen.do.clickDisplayedProduct('Coca-Cola'); + ProductScreen.check.selectedOrderlineHas('Coca-Cola'); + ProductScreen.do.clickDisplayedProduct('Minute Maid'); + ProductScreen.check.selectedOrderlineHas('Minute Maid'); + Chrome.do.backToFloor(); - function generate_product_screen_keypad_steps(amount_str) { - return generate_keypad_steps(amount_str, '.numpad'); - } + // At floor screen, there should be 2 synced draft orders + FloorScreen.check.orderCountSyncedInTableIs('T5', '2'); - function goto_payment_screen_and_select_payment_method() { - return [{ - content: "go to payment screen", - trigger: '.button.pay', - }, { - content: "pay with cash", - trigger: '.paymentmethod:contains("Cash")', - }]; - } + // Delete the first order then go back to floor + FloorScreen.do.clickTable('T5'); + ProductScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.do.deleteOrder('-0001'); + Chrome.do.confirmPopup(); + TicketScreen.do.selectOrder('-0003'); + Chrome.do.backToFloor(); - function open_table(table_id, order_count) { - order_count = order_count || null; - var steps = [{ - content: 'open table ' + table_id, - trigger: '.label:contains(' + table_id +')', - run: 'click', - }]; - if (order_count !== null){ - steps = steps.concat(verify_orders_synced(order_count)); - } - return steps; - } + // There should be 1 synced draft order. + FloorScreen.check.orderCountSyncedInTableIs('T5', '1'); - function transfer_order_to_table(table_id, order_uid) { - return [{ - content: 'Click transfer button', - trigger: '.control-button:contains("Transfer")', - run: 'click', - }, { - content: 'Transfer order to table ' + table_id, - trigger: '.label:contains(' + table_id +')', - run: 'click', - }, { - content: 'Check if order ' + order_uid + ' is open after transfer', - trigger: '.order-button.selected .order-sequence:contains("' + order_uid + '")', - run: function(){} // Check - }]; - } - - function finish_order() { - var steps = [{ - content: "validate the order", - trigger: '.payment-screen .button.next:visible', - }]; - steps = steps.concat([{ - content: "next order", - trigger: '.receipt-screen .button.next:visible', - }]); - return steps; - } - - /* pos_restaurant_sync - * - * Run on new session. - */ - var steps = [{ - content: 'waiting for loading to finish', - trigger: 'body:not(:has(.loader))', - run: function () {}, - }] - - - steps = steps.concat(open_table('T5')); - - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat(add_product_to_order('Water')); - steps = steps.concat(verify_order_total('4.40')); - steps = steps.concat([{ - content: 'start new order', - trigger: '.neworder-button', - run: 'click', - }]); - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat(add_product_to_order('Minute Maid')); - steps = steps.concat(verify_order_total('4.40')); - steps = steps.concat(goto_payment_screen_and_select_payment_method()); - steps = steps.concat(generate_payment_screen_keypad_steps('6.05')); - steps = steps.concat(finish_order()); - steps = steps.concat(open_table('T5', 1)); - steps = steps.concat(verify_order_total('4.40')); - steps = steps.concat([{ - content: 'start new order', - trigger: '.neworder-button', - run: 'click', - }]); - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat(add_product_to_order('Minute Maid')); - steps = steps.concat([{ - content: 'back to floor', - trigger: '.floor-button', - run: 'click', - }]); - steps = steps.concat(open_table('T5', 2)); - steps = steps.concat([{ - content: 'delete order', - trigger: '.deleteorder-button', - run: 'click', - }, { - content: 'confirm delete', - trigger: '.button.confirm', - run: 'click', - }, { - content: 'back to floor', - trigger: '.floor-button', - run: 'click', - }, { - content: 'back to floor', - trigger: '.oe_status .js_connected', - run: function() {}, - }]); - steps = steps.concat(open_table('T5', 1)); - - Tour.register('pos_restaurant_sync', { test: true, url: '/pos/web' }, steps); + Tour.register('pos_restaurant_sync', { test: true, url: '/pos/web' }, getSteps()); + startSteps(); /* pos_restaurant_sync_second_login * * This tour should be run after the first tour is done. */ - var steps = [{ - content: 'waiting for loading to finish', - trigger: 'body:not(:has(.loader))', - run: function () {}, - }]; - steps = steps.concat(open_table('T5', 1)); - steps = steps.concat(verify_order_total('4.40')); + + // There is one draft synced order from the previous tour + FloorScreen.check.orderCountSyncedInTableIs('T5', '1'); + FloorScreen.do.clickTable('T5'); + ProductScreen.check.totalAmountIs('4.40'); // Test transfering an order - steps = steps.concat(transfer_order_to_table('T4', '002-0001')); + ProductScreen.do.clickTransferButton(); + FloorScreen.do.clickTable('T4'); // Test if products still get merged after transfering the order - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat({ - content: 'check the order-line for Coca-Cola has 2 Units', - trigger: '.orderlines:has(.orderline .product-name:contains("Coca-Cola")) .info-list:contains("2.000")', - run: function () {}, - }) - steps = steps.concat(generate_product_screen_keypad_steps('1')); + ProductScreen.do.clickDisplayedProduct('Coca-Cola'); + ProductScreen.check.selectedOrderlineHas('Coca-Cola', '2.0'); + ProductScreen.check.totalAmountIs('6.60'); + ProductScreen.do.pressNumpad('1'); + ProductScreen.check.totalAmountIs('4.40'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.do.clickNextOrder(); + // At this point, there are no draft orders. - steps = steps.concat(goto_payment_screen_and_select_payment_method()); - steps = steps.concat(generate_payment_screen_keypad_steps('4.4')); - steps = steps.concat(finish_order()); - steps = steps.concat(open_table('T2')); - - // Test transfering an empty order - steps = steps.concat(transfer_order_to_table('T4', '2')); - - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat(verify_order_total('2.20')); - - // Take a synced order with products, remove the products - // and check if the order is still available in the front-end - steps = steps.concat([{ - content: 'back to floor', - trigger: '.floor-button', - run: 'click', - }]); - steps = steps.concat(open_table('T4', 1)); - steps = steps.concat([{ - content: 'click backspace to set quantity to 0', - trigger: '.numpad-backspace', - run: 'click', - }, { - content: 'click backspace to remove line', - trigger: '.numpad-backspace', - run: 'click', - }]); - steps = steps.concat([{ - content: 'back to floor', - trigger: '.floor-button', - run: 'click', - }]); - steps = steps.concat(open_table('T4', 1)); - steps = steps.concat(add_product_to_order('Coca-Cola')); - steps = steps.concat(verify_order_total('2.20')); - steps = steps.concat([{ - content: 'back to floor', - trigger: '.floor-button', - run: 'click', - }, { - content: 'check if in floor screen', - trigger: '.pos-content .floor-screen', - run: function() {}, - }, { - content: 'wait for sync to finish', - trigger: '.oe_status .js_connected', - run: function() {}, - }]); - - Tour.register('pos_restaurant_sync_second_login', { test: true, url: '/pos/web' }, steps); + FloorScreen.do.clickTable('T2'); + ProductScreen.check.isShown(); + ProductScreen.check.orderIsEmpty(); + ProductScreen.do.clickTransferButton(); + FloorScreen.do.clickTable('T4'); + ProductScreen.do.clickDisplayedProduct('Coca-Cola'); + ProductScreen.check.totalAmountIs('2.20'); + Chrome.do.backToFloor(); + FloorScreen.check.orderCountSyncedInTableIs('T4', '1'); + Tour.register('pos_restaurant_sync_second_login', { test: true, url: '/pos/web' }, getSteps()); }); diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py index 87f8a4b7822..2f30f2f2d49 100644 --- a/addons/pos_restaurant/tests/test_frontend.py +++ b/addons/pos_restaurant/tests/test_frontend.py @@ -179,3 +179,7 @@ class TestFrontend(odoo.tests.HttpCase): self.pos_config.write({'manage_orders': True}) self.pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'PosResOrderManagementScreenTour', login="admin", step_delay=50) + + def test_04_ticket_screen(self): + self.pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) + self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'PosResTicketScreenTour', login="admin", step_delay=50) diff --git a/addons/pos_restaurant/views/pos_restaurant_templates.xml b/addons/pos_restaurant/views/pos_restaurant_templates.xml index a2226920294..c3d51a8bbc3 100644 --- a/addons/pos_restaurant/views/pos_restaurant_templates.xml +++ b/addons/pos_restaurant/views/pos_restaurant_templates.xml @@ -24,8 +24,9 @@ - + + @@ -54,6 +55,7 @@ +