[IMP] point_of_sale,pos_*: top bar improvements and ticket list

The following changes are made in this task:

1. Remove order tabs and replace with ticket list.
2. Top bar color change.
3. Search bar in the product screen is moved in the top bar.
4. List of sub-categories are now in the same line as the category
breadcrumbs.

closes odoo/odoo#55100

Task-id: 2276678
Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
This commit is contained in:
Joseph Caburnay
2020-08-05 13:35:08 +00:00
parent 71f719a61e
commit 9ea0aca6ea
56 changed files with 1468 additions and 587 deletions
+3 -1
View File
@@ -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',
+2
View File
@@ -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
+375 -42
View File
@@ -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;
}
+6 -2
View File
@@ -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() {
@@ -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;
});
@@ -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;
});
@@ -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());
}
}
@@ -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;
});
@@ -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);
}
}
}
@@ -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() {
@@ -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);
}
@@ -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;
});
@@ -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();
+27 -17
View File
@@ -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.
*/
+3 -1
View File
@@ -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() };
});
+16 -9
View File
@@ -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">
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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');
@@ -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');
@@ -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());
});
@@ -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);
@@ -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`,
@@ -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);
});
@@ -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({
@@ -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);
@@ -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');
@@ -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)
@@ -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>
@@ -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>
+2 -2
View File
@@ -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);
}
+2 -1
View File
@@ -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',
],
@@ -1,22 +1,28 @@
odoo.define('pos_restaurant.BackToFloorButton', function(require) {
odoo.define('pos_restaurant.BackToFloorButton', function (require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const { useListener } = require('web.custom_hooks');
const Registries = require('point_of_sale.Registries');
const { posbus } = require('point_of_sale.utils');
class BackToFloorButton extends PosComponent {
constructor() {
super(...arguments);
useListener('click', this._backToFloorScreen);
mounted() {
posbus.on('table-set', this, this.render);
}
willUnmount() {
posbus.on('table-set', this);
}
get table() {
return this.props.table;
return (this.env.pos && this.env.pos.table) || null;
}
get floor() {
return this.props.table.floor;
const table = this.table;
return table ? table.floor : null;
}
async _backToFloorScreen() {
get hasTable() {
return this.table !== null;
}
backToFloorScreen() {
this.showScreen('FloorScreen', { floor: this.floor });
}
}
@@ -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;
});
@@ -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;
});
@@ -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() {
@@ -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);
@@ -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);
@@ -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);
@@ -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;
});
+17 -5
View File
@@ -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);
}
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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();
@@ -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')
@@ -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());
});
@@ -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 () {},
},
];
}
@@ -1,252 +1,108 @@
odoo.define('pos_reataurant.tour.synchronized_table_management', function (require) {
"use strict";
'use strict';
var Tour = require("web_tour.tour");
const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods');
const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods');
const { ProductScreen } = require('pos_restaurant.tour.ProductScreenTourMethods');
const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods');
const { getSteps, startSteps } = require('point_of_sale.tour.utils');
const Tour = require('web_tour.tour');
function verify_order_total(total_str) {
return [{
content: 'order total contains ' + total_str,
trigger: '.order .total .value:contains("' + total_str + '")',
run: function () {}, // it's a check
}];
}
startSteps();
function verify_orders_synced(order_count) {
return [{
content: "check synced",
trigger: ".order-sequence",
run: function() {
var orders = $('.order-sequence');
if (orders.length === order_count) {
return
} else {
throw `sync failed 'there should be ${order_count} orders, however, ${orders.length} orders are displayed' `;
}
},
}];
}
FloorScreen.do.clickTable('T5');
function add_product_to_order(product_name) {
return [{
content: 'buy ' + product_name,
trigger: '.product-list .product-name:contains("' + product_name + '")',
}, {
content: 'the ' + product_name + ' have been added to the order',
trigger: '.order .product-name:contains("' + product_name + '")',
run: function () {}, // it's a check
}];
}
// Create first order
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.selectedOrderlineHas('Coca-Cola');
ProductScreen.do.clickDisplayedProduct('Water');
ProductScreen.check.selectedOrderlineHas('Water');
ProductScreen.check.totalAmountIs('4.40');
function generate_keypad_steps(amount_str, keypad_selector) {
var i, steps = [], current_char;
for (i = 0; i < amount_str.length; ++i) {
current_char = amount_str[i];
steps.push({
content: 'press ' + current_char + ' on payment keypad',
trigger: keypad_selector + ' .input-button:contains("' + current_char + '"):visible'
});
}
// Create 2nd order (paid)
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.selectedOrderlineHas('Coca-Cola');
ProductScreen.do.clickDisplayedProduct('Minute Maid');
ProductScreen.check.selectedOrderlineHas('Minute Maid');
ProductScreen.check.totalAmountIs('4.40');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
return steps;
}
// After clicking next order, floor screen is shown.
// It should have 1 as number of draft synced order.
FloorScreen.check.orderCountSyncedInTableIs('T5', '1');
FloorScreen.do.clickTable('T5');
ProductScreen.check.totalAmountIs('4.40');
function generate_payment_screen_keypad_steps(amount_str) {
return generate_keypad_steps(amount_str, '.payment-numpad');
}
// Create another draft order and go back to floor
Chrome.do.clickTicketButton();
TicketScreen.do.clickNewTicket();
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.selectedOrderlineHas('Coca-Cola');
ProductScreen.do.clickDisplayedProduct('Minute Maid');
ProductScreen.check.selectedOrderlineHas('Minute Maid');
Chrome.do.backToFloor();
function generate_product_screen_keypad_steps(amount_str) {
return generate_keypad_steps(amount_str, '.numpad');
}
// At floor screen, there should be 2 synced draft orders
FloorScreen.check.orderCountSyncedInTableIs('T5', '2');
function goto_payment_screen_and_select_payment_method() {
return [{
content: "go to payment screen",
trigger: '.button.pay',
}, {
content: "pay with cash",
trigger: '.paymentmethod:contains("Cash")',
}];
}
// Delete the first order then go back to floor
FloorScreen.do.clickTable('T5');
ProductScreen.check.isShown();
Chrome.do.clickTicketButton();
TicketScreen.do.deleteOrder('-0001');
Chrome.do.confirmPopup();
TicketScreen.do.selectOrder('-0003');
Chrome.do.backToFloor();
function open_table(table_id, order_count) {
order_count = order_count || null;
var steps = [{
content: 'open table ' + table_id,
trigger: '.label:contains(' + table_id +')',
run: 'click',
}];
if (order_count !== null){
steps = steps.concat(verify_orders_synced(order_count));
}
return steps;
}
// There should be 1 synced draft order.
FloorScreen.check.orderCountSyncedInTableIs('T5', '1');
function transfer_order_to_table(table_id, order_uid) {
return [{
content: 'Click transfer button',
trigger: '.control-button:contains("Transfer")',
run: 'click',
}, {
content: 'Transfer order to table ' + table_id,
trigger: '.label:contains(' + table_id +')',
run: 'click',
}, {
content: 'Check if order ' + order_uid + ' is open after transfer',
trigger: '.order-button.selected .order-sequence:contains("' + order_uid + '")',
run: function(){} // Check
}];
}
function finish_order() {
var steps = [{
content: "validate the order",
trigger: '.payment-screen .button.next:visible',
}];
steps = steps.concat([{
content: "next order",
trigger: '.receipt-screen .button.next:visible',
}]);
return steps;
}
/* pos_restaurant_sync
*
* Run on new session.
*/
var steps = [{
content: 'waiting for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
}]
steps = steps.concat(open_table('T5'));
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat(add_product_to_order('Water'));
steps = steps.concat(verify_order_total('4.40'));
steps = steps.concat([{
content: 'start new order',
trigger: '.neworder-button',
run: 'click',
}]);
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat(add_product_to_order('Minute Maid'));
steps = steps.concat(verify_order_total('4.40'));
steps = steps.concat(goto_payment_screen_and_select_payment_method());
steps = steps.concat(generate_payment_screen_keypad_steps('6.05'));
steps = steps.concat(finish_order());
steps = steps.concat(open_table('T5', 1));
steps = steps.concat(verify_order_total('4.40'));
steps = steps.concat([{
content: 'start new order',
trigger: '.neworder-button',
run: 'click',
}]);
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat(add_product_to_order('Minute Maid'));
steps = steps.concat([{
content: 'back to floor',
trigger: '.floor-button',
run: 'click',
}]);
steps = steps.concat(open_table('T5', 2));
steps = steps.concat([{
content: 'delete order',
trigger: '.deleteorder-button',
run: 'click',
}, {
content: 'confirm delete',
trigger: '.button.confirm',
run: 'click',
}, {
content: 'back to floor',
trigger: '.floor-button',
run: 'click',
}, {
content: 'back to floor',
trigger: '.oe_status .js_connected',
run: function() {},
}]);
steps = steps.concat(open_table('T5', 1));
Tour.register('pos_restaurant_sync', { test: true, url: '/pos/web' }, steps);
Tour.register('pos_restaurant_sync', { test: true, url: '/pos/web' }, getSteps());
startSteps();
/* pos_restaurant_sync_second_login
*
* This tour should be run after the first tour is done.
*/
var steps = [{
content: 'waiting for loading to finish',
trigger: 'body:not(:has(.loader))',
run: function () {},
}];
steps = steps.concat(open_table('T5', 1));
steps = steps.concat(verify_order_total('4.40'));
// There is one draft synced order from the previous tour
FloorScreen.check.orderCountSyncedInTableIs('T5', '1');
FloorScreen.do.clickTable('T5');
ProductScreen.check.totalAmountIs('4.40');
// Test transfering an order
steps = steps.concat(transfer_order_to_table('T4', '002-0001'));
ProductScreen.do.clickTransferButton();
FloorScreen.do.clickTable('T4');
// Test if products still get merged after transfering the order
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat({
content: 'check the order-line for Coca-Cola has 2 Units',
trigger: '.orderlines:has(.orderline .product-name:contains("Coca-Cola")) .info-list:contains("2.000")',
run: function () {},
})
steps = steps.concat(generate_product_screen_keypad_steps('1'));
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.selectedOrderlineHas('Coca-Cola', '2.0');
ProductScreen.check.totalAmountIs('6.60');
ProductScreen.do.pressNumpad('1');
ProductScreen.check.totalAmountIs('4.40');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.clickValidate();
ReceiptScreen.do.clickNextOrder();
// At this point, there are no draft orders.
steps = steps.concat(goto_payment_screen_and_select_payment_method());
steps = steps.concat(generate_payment_screen_keypad_steps('4.4'));
steps = steps.concat(finish_order());
steps = steps.concat(open_table('T2'));
// Test transfering an empty order
steps = steps.concat(transfer_order_to_table('T4', '2'));
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat(verify_order_total('2.20'));
// Take a synced order with products, remove the products
// and check if the order is still available in the front-end
steps = steps.concat([{
content: 'back to floor',
trigger: '.floor-button',
run: 'click',
}]);
steps = steps.concat(open_table('T4', 1));
steps = steps.concat([{
content: 'click backspace to set quantity to 0',
trigger: '.numpad-backspace',
run: 'click',
}, {
content: 'click backspace to remove line',
trigger: '.numpad-backspace',
run: 'click',
}]);
steps = steps.concat([{
content: 'back to floor',
trigger: '.floor-button',
run: 'click',
}]);
steps = steps.concat(open_table('T4', 1));
steps = steps.concat(add_product_to_order('Coca-Cola'));
steps = steps.concat(verify_order_total('2.20'));
steps = steps.concat([{
content: 'back to floor',
trigger: '.floor-button',
run: 'click',
}, {
content: 'check if in floor screen',
trigger: '.pos-content .floor-screen',
run: function() {},
}, {
content: 'wait for sync to finish',
trigger: '.oe_status .js_connected',
run: function() {},
}]);
Tour.register('pos_restaurant_sync_second_login', { test: true, url: '/pos/web' }, steps);
FloorScreen.do.clickTable('T2');
ProductScreen.check.isShown();
ProductScreen.check.orderIsEmpty();
ProductScreen.do.clickTransferButton();
FloorScreen.do.clickTable('T4');
ProductScreen.do.clickDisplayedProduct('Coca-Cola');
ProductScreen.check.totalAmountIs('2.20');
Chrome.do.backToFloor();
FloorScreen.check.orderCountSyncedInTableIs('T4', '1');
Tour.register('pos_restaurant_sync_second_login', { test: true, url: '/pos/web' }, getSteps());
});
@@ -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)
@@ -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>