summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/point_of_sale/__manifest__.py4
-rw-r--r--addons/point_of_sale/models/pos_order.py2
-rw-r--r--addons/point_of_sale/static/src/css/pos.css417
-rw-r--r--addons/point_of_sale/static/src/js/Chrome.js8
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js56
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js41
-rw-r--r--addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js15
-rw-r--r--addons/point_of_sale/static/src/js/Misc/SearchBar.js115
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js4
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductsWidget.js3
-rw-r--r--addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js181
-rw-r--r--addons/point_of_sale/static/src/js/main.js2
-rw-r--r--addons/point_of_sale/static/src/js/models.js44
-rw-r--r--addons/point_of_sale/static/src/js/utils.js4
-rw-r--r--addons/point_of_sale/static/src/xml/Chrome.xml25
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml34
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml13
-rw-r--r--addons/point_of_sale/static/src/xml/Misc/SearchBar.xml44
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml6
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml44
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml55
-rw-r--r--addons/point_of_sale/static/tests/tours/Chrome.tour.js44
-rw-r--r--addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js7
-rw-r--r--addons/point_of_sale/static/tests/tours/TicketScreen.tour.js54
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js35
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js2
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js100
-rw-r--r--addons/point_of_sale/static/tests/unit/helpers/test_env.js3
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js47
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ProductScreen.js10
-rw-r--r--addons/point_of_sale/tests/test_frontend.py4
-rw-r--r--addons/point_of_sale/views/pos_assets_common.xml4
-rw-r--r--addons/point_of_sale/views/pos_assets_index.xml2
-rw-r--r--addons/pos_hr/static/src/css/pos.css4
-rw-r--r--addons/pos_restaurant/__manifest__.py3
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js22
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js20
-rw-r--r--addons/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js33
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js4
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js3
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js12
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/SplitBillScreen/SplitBillScreen.js4
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TicketScreen.js54
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js22
-rw-r--r--addons/pos_restaurant/static/src/xml/Chrome.xml10
-rw-r--r--addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml3
-rw-r--r--addons/pos_restaurant/static/src/xml/ChromeWidgets/OrderSelector.xml20
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/TicketScreen.xml15
-rw-r--r--addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js7
-rw-r--r--addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js4
-rw-r--r--addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js44
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js21
-rw-r--r--addons/pos_restaurant/static/tests/tours/pos_restaurant.js338
-rw-r--r--addons/pos_restaurant/tests/test_frontend.py4
-rw-r--r--addons/pos_restaurant/views/pos_restaurant_templates.xml4
56 files changed, 1482 insertions, 601 deletions
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, (models.Order) => string>
+ */
+ get _searchFields() {
+ return {
+ 'Receipt Number': (order) => order.name,
+ Date: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'),
+ Customer: (order) => order.get_client_name(),
+ };
+ }
+ /**
+ * Maps the order screen params to order status.
+ */
+ get _screenToStatusMap() {
+ return {
+ ProductScreen: 'Ongoing',
+ PaymentScreen: 'Payment',
+ ReceiptScreen: 'Receipt',
+ };
+ }
+ _initializeSearchFieldConstants() {
+ this.constants = {};
+ Object.assign(this.constants, {
+ searchFieldNames: Object.keys(this._searchFields),
+ screenToStatusMap: this._screenToStatusMap,
+ });
+ }
+ }
+ TicketScreen.template = 'TicketScreen';
+
+ Registries.Component.add(TicketScreen);
+
+ return TicketScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/main.js b/addons/point_of_sale/static/src/js/main.js
index dc8ea5d7b7b..346a61679f4 100644
--- a/addons/point_of_sale/static/src/js/main.js
+++ b/addons/point_of_sale/static/src/js/main.js
@@ -11,6 +11,8 @@ odoo.define('web.web_client', function (require) {
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
owl.Component.env = env;
+ Registries.Component.add(owl.misc.Portal);
+
function setupResponsivePlugin(env) {
const isMobile = () => window.innerWidth <= 768;
env.isMobile = isMobile();
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 44e9527ba9c..6d0d5ff63f8 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -717,11 +717,11 @@ exports.PosModel = Backbone.Model.extend({
var order_list = this.get_order_list();
if( (reason === 'abandon' || removed_order.temporary) && order_list.length > 0){
// when we intentionally remove an unfinished order, and there is another existing one
- this.set_order(order_list[index] || order_list[order_list.length -1]);
+ this.set_order(order_list[index] || order_list[order_list.length - 1], { silent: true });
}else{
// when the order was automatically removed after completion,
// or when we intentionally delete the only concurrent order
- this.add_new_order();
+ this.add_new_order({ silent: true });
}
},
@@ -739,10 +739,10 @@ exports.PosModel = Backbone.Model.extend({
this.db.set_cashier(this.get('cashier'));
},
// creates a new empty order and sets it as the current order
- add_new_order: function(){
+ add_new_order: function(options){
var order = new exports.Order({},{pos:this});
this.get('orders').add(order);
- this.set('selectedOrder', order);
+ this.set('selectedOrder', order, options);
return order;
},
/**
@@ -810,8 +810,8 @@ exports.PosModel = Backbone.Model.extend({
},
// change the current order
- set_order: function(order){
- this.set({ selectedOrder: order });
+ set_order: function(order, options){
+ this.set({ selectedOrder: order }, options);
},
// return the list of unpaid orders
@@ -2469,6 +2469,12 @@ exports.Paymentline = Backbone.Model.extend({
ticket: this.ticket,
};
},
+ // If payment status is a non-empty string, then it is an electronic payment.
+ // TODO: There has to be a less confusing way to distinguish simple payments
+ // from electronic transactions. Perhaps use a flag?
+ is_electronic: function() {
+ return Boolean(this.get_payment_status());
+ },
});
var PaymentlineCollection = Backbone.Collection.extend({
@@ -3280,23 +3286,27 @@ exports.Order = Backbone.Model.extend({
// the order also stores the screen status, as the PoS supports
// different active screens per order. This method is used to
// store the screen status.
- set_screen_data: function(key,value){
- if(arguments.length === 2){
- this.screen_data[key] = value;
- }else if(arguments.length === 1){
- for(var key in arguments[0]){
- this.screen_data[key] = arguments[0][key];
- }
- }
+ set_screen_data: function(value){
+ this.screen_data['value'] = value;
},
//see set_screen_data
- get_screen_data: function(key){
- return this.screen_data[key];
+ get_screen_data: function(){
+ const screen = this.screen_data['value'];
+ // If no screen data is saved
+ // no payment line -> product screen
+ // with payment line -> payment screen
+ if (!screen) {
+ if (this.get_paymentlines().length > 0) return { name: 'PaymentScreen' };
+ return { name: 'ProductScreen' };
+ }
+ if (screen.name !== 'ReceiptScreen' && this.get_paymentlines().length > 0) {
+ return { name: 'PaymentScreen' };
+ }
+ return screen;
},
wait_for_push_order: function () {
return this.is_to_email();
},
-
/**
* @returns {Object} object to use as props for instantiating OrderReceipt.
*/
diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js
index 26442ef776b..7aa7b35e93d 100644
--- a/addons/point_of_sale/static/src/js/utils.js
+++ b/addons/point_of_sale/static/src/js/utils.js
@@ -1,6 +1,8 @@
odoo.define('point_of_sale.utils', function (require) {
'use strict';
+ const { EventBus } = owl.core;
+
function getFileAsText(file) {
return new Promise((resolve, reject) => {
if (!file) {
@@ -43,5 +45,5 @@ odoo.define('point_of_sale.utils', function (require) {
);
}
- return { getFileAsText, nextFrame, isRpcError };
+ return { getFileAsText, nextFrame, isRpcError, posbus: new EventBus() };
});
diff --git a/addons/point_of_sale/static/src/xml/Chrome.xml b/addons/point_of_sale/static/src/xml/Chrome.xml
index 39b267e9308..6c2d3c5f46d 100644
--- a/addons/point_of_sale/static/src/xml/Chrome.xml
+++ b/addons/point_of_sale/static/src/xml/Chrome.xml
@@ -4,15 +4,22 @@
<t t-name="Chrome" owl="1">
<div class="pos" t-att-class="{ 'big-scrollbars': state.uiState.hasBigScrollBars }">
<div class="pos-receipt-print"></div>
+ <div class="pos-topheader" t-att-class="{ oe_hidden: state.uiState !== 'READY' }">
+ <div t-if="tempScreen.isShown" class="block-top-header" />
+ <div class="pos-branding" t-if= "!env.isMobile">
+ <img class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
+ src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
+ <TicketButton isTicketScreenShown="isTicketScreenShown" />
+ </div>
+ <div class="pos-rightheader">
+ <TicketButton isTicketScreenShown="isTicketScreenShown" t-if="env.isMobile" />
+ <div class="search-bar-portal" />
+ <div class="status-buttons-portal" />
+ </div>
+ </div>
<t t-if="state.uiState === 'READY'">
- <div class="pos-topheader">
- <div t-if="tempScreen.isShown" class="block-top-header" />
- <div class="pos-branding">
- <img t-if= "!env.isMobile" class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
- src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
- </div>
- <div class="pos-rightheader">
- <OrderSelector />
+ <Portal target="'.pos .status-buttons-portal'">
+ <div class="status-buttons">
<t t-if="!env.isMobile">
<CashierName />
</t>
@@ -23,7 +30,7 @@
<SyncNotification />
<HeaderButton />
</div>
- </div>
+ </Portal>
<div class="pos-content">
<div class="window">
<div class="subwindow">
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
deleted file mode 100644
index 353ea5f93a4..00000000000
--- a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<templates id="template" xml:space="preserve">
-
- <div class="order-selector" t-name="OrderSelector" owl="1">
- <t t-if="chromeContext.showOrderSelector">
- <span class="orders">
- <t t-foreach="orderList" t-as="order" t-key="order.cid">
- <t t-if="order === env.pos.get_order()">
- <span class="order-button select-order selected">
- <span class="order-sequence">
- <t t-esc="order.sequence_number" />
- </span>
- <t t-esc="creationTime(order)" />
- </span>
- </t>
- <t t-else="">
- <span class="order-button select-order" t-on-click="selectOrder(order)">
- <span class="order-sequence">
- <t t-esc="order.sequence_number" />
- </span>
- </span>
- </t>
- </t>
- </span>
- <span class="order-button square neworder-button" t-on-click="addNewOrder">
- <i class="fa fa-plus" role="img" aria-label="New order" title="New order" />
- </span>
- <span class="order-button square deleteorder-button" t-on-click="deleteCurrentOrder">
- <i class="fa fa-minus" role="img" aria-label="Delete order" title="Delete order" />
- </span>
- </t>
- </div>
-
-</templates>
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml
new file mode 100644
index 00000000000..66d31dd5584
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/TicketButton.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="TicketButton" owl="1">
+ <div class="ticket-button" t-att-class="{ highlight: props.isTicketScreenShown }" t-on-click="onClick">
+ <div class="with-badge" t-att-badge="count">
+ <i class="fa fa-ticket" aria-hidden="true"></i>
+ </div>
+ <div>Tickets</div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml b/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml
new file mode 100644
index 00000000000..a480f169520
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Misc/SearchBar.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="point_of_sale.SearchBar" owl="1">
+ <div class="pos-search-bar">
+ <div class="search">
+ <span class="search-icon"><i class="fa fa-search"></i></span>
+ <input class="radius-left" t-att-class="{ 'radius-right': !props.config.filter.show }"
+ t-model="state.searchInput" t-on-keydown="onKeydown" type="text" t-att-placeholder="placeholder" />
+ <ul t-if="state.showSearchFields and state.searchInput" class="fields">
+ <t t-foreach="config.searchFields" t-as="value" t-key="value_index">
+ <li t-att-class="{ highlight: value_index == state.selectedFieldId }"
+ t-on-click="onClickSearchField(value_index)">
+ <span class="field">
+ <t t-esc="value"></t>
+ </span>
+ <span>: </span>
+ <span class="term">
+ <t t-esc="state.searchInput"></t>
+ </span>
+ </li>
+ </t>
+ </ul>
+ </div>
+ <div t-if="props.config.filter.show" class="filter radius-right"
+ t-on-click.stop="state.showFilterOptions = !state.showFilterOptions">
+ <span class="down-icon">
+ <i class="fa fa-chevron-down" aria-hidden="true"></i>
+ </span>
+ <span>
+ <t t-esc="state.selectedFilter" />
+ </span>
+ <ul t-if="state.showFilterOptions" class="options">
+ <t t-foreach="config.filter.options" t-as="option" t-key="option">
+ <li t-on-click="selectFilter(option)">
+ <t t-esc="option"></t>
+ </li>
+ </t>
+ </ul>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
index c0a49a555b2..6b3f04bb320 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidget.xml
@@ -3,9 +3,9 @@
<t t-name="ProductsWidget" owl="1">
<div class="products-widget">
- <ProductsWidgetControlPanel breadcrumbs="breadcrumbs"
- subcategories="subcategories" />
-
+ <t t-if="!hasNoCategories">
+ <ProductsWidgetControlPanel breadcrumbs="breadcrumbs" subcategories="subcategories" />
+ </t>
<ProductList products="productsToDisplay" searchWord="searchWord" />
</div>
</t>
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
index c80a2a6d1ef..09c17501d52 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml
@@ -3,7 +3,10 @@
<t t-name="ProductsWidgetControlPanel" owl="1">
<div class="products-widget-control">
- <div class="rightpane-header">
+ <div class="rightpane-header" t-att-class="{
+ 'green-border-bottom': !env.pos.config.iface_display_categ_images,
+ 'grey-border-bottom': env.pos.config.iface_display_categ_images,
+ }">
<!-- Breadcrumbs -->
<div class="breadcrumbs">
<HomeCategoryBreadcrumb subcategories="props.subcategories" currentCat="props.breadcrumbs[props.breadcrumbs.length - 1]"/>
@@ -13,36 +16,33 @@
</t>
</t>
</div>
- <!-- Searchbox -->
- <div class="searchbox">
- <input t-ref="search-word-input" placeholder="Search Products"
- t-on-keyup="updateSearch" />
- <span class="search-clear left">
- <i class="fa fa-search" />
- </span>
- <span class="search-clear right" t-on-click="clearSearch">
- <i class="fa fa-remove" />
- </span>
- </div>
+ <!-- Subcategories -->
+ <t t-if="props.subcategories.length > 0 and !env.pos.config.iface_display_categ_images and !env.isMobile">
+ <t t-foreach="props.subcategories" t-as="category" t-key="category.id">
+ <CategorySimpleButton category="category" />
+ </t>
+ </t>
</div>
- <!-- Subcategories -->
- <t t-if="props.subcategories.length > 0 &amp;&amp; !env.isMobile">
+ <t t-if="props.subcategories.length > 0 and env.pos.config.iface_display_categ_images and !env.isMobile">
<div class="categories">
<div class="category-list-scroller">
- <div class="category-list"
- t-att-class="{simple: !env.pos.config.iface_display_categ_images}">
+ <div class="category-list">
<t t-foreach="props.subcategories" t-as="category" t-key="category.id">
- <t t-if="env.pos.config.iface_display_categ_images">
- <CategoryButton category="category" />
- </t>
- <t t-else="">
- <CategorySimpleButton category="category" />
- </t>
+ <CategoryButton category="category" />
</t>
</div>
</div>
</div>
</t>
+ <Portal target="'.pos .search-bar-portal'">
+ <div class="search-box">
+ <span class="icon"><i class="fa fa-search"></i></span>
+ <span t-on-click="clearSearch" class="clear-icon">
+ <i class="fa fa-times" aria-hidden="true"></i>
+ </span>
+ <input t-ref="search-word-input" type="text" placeholder="Search Products..." t-on-keyup="updateSearch" />
+ </div>
+ </Portal>
</div>
</t>
diff --git a/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml b/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml
new file mode 100644
index 00000000000..2aa890e029d
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/TicketScreen/TicketScreen.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="TicketScreen" owl="1">
+ <div class="ticket-screen screen">
+ <div class="screen-content">
+ <div class="controls">
+ <div class="buttons">
+ <button t-if="showNewTicketButton" class="highlight" t-on-click="createNewOrder">New Ticket</button>
+ <button class="discard" t-on-click="trigger('close-screen')">Discard</button>
+ </div>
+ <SearchBar config="searchBarConfig" placeholder="'Search Tickets...'" />
+ </div>
+ <div class="orders">
+ <div class="header-row">
+ <div class="col start wide">Date</div>
+ <div class="col start wide">Receipt Number</div>
+ <div class="col start">Customer</div>
+ <div class="col start">Employee</div>
+ <div class="col end">Total</div>
+ <div class="col start narrow">Status</div>
+ <div class="col center very-narrow" name="delete"></div>
+ </div>
+ <t t-foreach="filteredOrderList" t-as="order" t-key="order.cid">
+ <div class="order-row pointer" t-on-click="selectOrder(order)">
+ <div class="col start wide">
+ <t t-esc="getDate(order)"></t>
+ </div>
+ <div class="col start wide">
+ <t t-esc="order.name"></t>
+ </div>
+ <div class="col start">
+ <t t-esc="getCustomer(order)"></t>
+ </div>
+ <div class="col start">
+ <t t-esc="getEmployee(order)"></t>
+ </div>
+ <div class="col end">
+ <t t-esc="getTotal(order)"></t>
+ </div>
+ <div class="col start narrow">
+ <t t-esc="getStatus(order)"></t>
+ </div>
+ <div t-if="!hideDeleteButton(order)" class="col center very-narrow delete-button" name="delete" t-on-click.stop="deleteOrder(order)">
+ <i class="fa fa-trash" aria-hidden="true"></i>
+ </div>
+ <div t-else="" class="col center very-narrow delete-button"></div>
+ </div>
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+
+</templates>
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 */ `
- <div>
- <OrderSelector></OrderSelector>
- <t t-if="popup.isShown" t-component="popup.component" t-props="popupProps" t-key="popup.name" />
- </div>
- `;
-
- 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 */ `
- <div>
- <ProductsWidgetControlPanel breadcrumbs="breadcrumbs" subcategories="subcategories" />
+ <div class="pos">
+ <div class="search-bar-portal">
+ <ProductsWidgetControlPanel breadcrumbs="breadcrumbs" subcategories="subcategories" />
+ </div>
</div>
`;
@@ -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 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenElectronicPayment.js"></script>
@@ -68,17 +69,18 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ReceiptScreen/OrderReceipt.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ScaleScreen/ScaleScreen.js"></script>
- <script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/CashierName.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/TicketButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/Draggable.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/NotificationSound.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Misc/SearchBar.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/ErrorPopup.js"></script>
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 @@
<xpath expr="." position="inside">
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/utils.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/TicketScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ProductConfiguratorTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js"></script>
@@ -23,6 +24,7 @@
<script type="text/javascript" src="/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ReceiptScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/Chrome.tour.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/TicketScreen.tour.js"></script>
</xpath>
</template>
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;
+ }
+ get hasTable() {
+ return this.table !== null;
}
- async _backToFloorScreen() {
+ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
+ <xpath expr="//div[hasclass('search-bar-portal')]" position="before">
+ <BackToFloorButton />
+ </xpath>
+ </t>
+
+</templates>
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 @@
<templates id="template" xml:space="preserve">
<t t-name="BackToFloorButton" owl="1">
- <span class="order-button floor-button">
+ <span t-if="hasTable" class="order-button floor-button" t-on-click="backToFloorScreen">
<i class="fa fa-angle-double-left" role="img" aria-label="Back to floor" title="Back to floor" />
<span> </span>
<span t-esc="floor.name" />
@@ -13,6 +13,7 @@
<span> )</span>
</span>
</span>
+ <span t-else=""></span>
</t>
</templates>
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 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<templates id="template" xml:space="preserve">
-
- <t t-name="OrderSelector" t-inherit="point_of_sale.OrderSelector" t-inherit-mode="extension" owl="1">
- <xpath expr="//div[hasclass('order-selector')]//span[hasclass('orders')]//t" position="before">
- <BackToFloorButton t-if="backToFloorButtonIsShown" table="currentTable" />
- </xpath>
- <xpath expr="//span[hasclass('order-sequence')]" position="replace">
- <span class="order-sequence">
- <t t-if="order.server_id and env.pos.pos_session.login_number != order.uid.split('-')[1]">
- <t t-esc="order.uid.substring(order.uid.indexOf('-') +1)" />
- </t>
- <t t-else="">
- <t t-esc="order.sequence_number" />
- </t>
- </span>
- </xpath>
- </t>
-
-</templates>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension" owl="1">
+ <xpath expr="//div[hasclass('header-row')]//div[@name='delete']" position="before">
+ <div t-if="env.pos.config.iface_floorplan" class="col start" name="table">Table</div>
+ </xpath>
+ <xpath expr="//div[hasclass('order-row')]//div[@name='delete']" position="before">
+ <div t-if="env.pos.config.iface_floorplan" class="col start" name="table">
+ <t t-esc="getTable(order)"></t>
+ </div>
+ </xpath>
+ </t>
+
+</templates>
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";
-
- var 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
- }];
- }
-
- 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' `;
- }
- },
- }];
- }
-
- 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
- }];
- }
-
- 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'
- });
- }
-
- return steps;
- }
-
- function generate_payment_screen_keypad_steps(amount_str) {
- return generate_keypad_steps(amount_str, '.payment-numpad');
- }
-
- function generate_product_screen_keypad_steps(amount_str) {
- return generate_keypad_steps(amount_str, '.numpad');
- }
-
- 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")',
- }];
- }
-
- 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;
- }
-
- 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);
-
+ 'use strict';
+
+ 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');
+
+ startSteps();
+
+ FloorScreen.do.clickTable('T5');
+
+ // 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');
+
+ // 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();
+
+ // 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');
+
+ // 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();
+
+ // At floor screen, there should be 2 synced draft orders
+ FloorScreen.check.orderCountSyncedInTableIs('T5', '2');
+
+ // 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();
+
+ // There should be 1 synced draft order.
+ FloorScreen.check.orderCountSyncedInTableIs('T5', '1');
+
+ 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'));
-
- 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);
-
+ 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.
+
+ 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 @@
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js"></script>
- <script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/TicketScreen.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Chrome.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js"></script>
</xpath>
@@ -54,6 +55,7 @@
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/ControlButtons.tour.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/FloorScreen.tour.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/tests/tours/TicketScreen.tour.js"></script>
</xpath>
</template>