[IMP] point_of_sale,pos_*: combine ticket and management screens
We are referring to TicketScreen and OrderManagementScreen. The two screens almost serves the same purpose. There is not much reason to keep them separate. We combine the two in this commit. Basically, we remove OrderManagementScreen and its components. Some are kept and moved to the TicketScreen. Task-id: 2481404 Part-of: odoo/odoo#75322
This commit is contained in:
@@ -87,17 +87,11 @@
|
||||
'point_of_sale/static/src/js/Screens/ClientListScreen/ClientLine.js',
|
||||
'point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js',
|
||||
'point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js',
|
||||
'point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/InvoiceButton.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/ControlButtons/ReprintReceiptButton.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/OrderDetails.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/OrderlineDetails.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/ReprintReceiptScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js',
|
||||
'point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js',
|
||||
'point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js',
|
||||
@@ -126,7 +120,6 @@
|
||||
'point_of_sale/static/src/js/ChromeWidgets/CashierName.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js',
|
||||
|
||||
@@ -203,7 +203,6 @@ class PosConfig(models.Model):
|
||||
module_pos_discount = fields.Boolean("Global Discounts")
|
||||
module_pos_loyalty = fields.Boolean("Loyalty Program")
|
||||
module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
|
||||
manage_orders = fields.Boolean(string="Manage Orders")
|
||||
product_configurator = fields.Boolean(string="Product Configurator")
|
||||
is_posbox = fields.Boolean("PosBox")
|
||||
is_header_or_footer = fields.Boolean("Header & Footer")
|
||||
@@ -220,7 +219,6 @@ class PosConfig(models.Model):
|
||||
cash_rounding = fields.Boolean(string="Cash Rounding")
|
||||
only_round_cash_method = fields.Boolean(string="Only apply rounding on cash")
|
||||
has_active_session = fields.Boolean(compute='_compute_current_session')
|
||||
show_allow_invoicing_alert = fields.Boolean(compute="_compute_show_allow_invoicing_alert")
|
||||
manual_discount = fields.Boolean(string="Manual Discounts", default=True)
|
||||
ship_later = fields.Boolean(string="Ship Later")
|
||||
warehouse_id = fields.Many2one('stock.warehouse', default=_default_warehouse_id, ondelete='restrict')
|
||||
@@ -274,14 +272,6 @@ class PosConfig(models.Model):
|
||||
pos_config.current_session_id = session and session[0].id or False
|
||||
pos_config.current_session_state = session and session[0].state or False
|
||||
|
||||
@api.depends('module_account', 'manage_orders')
|
||||
def _compute_show_allow_invoicing_alert(self):
|
||||
for pos_config in self:
|
||||
if not pos_config.manage_orders:
|
||||
pos_config.show_allow_invoicing_alert = False
|
||||
else:
|
||||
pos_config.show_allow_invoicing_alert = not pos_config.module_account
|
||||
|
||||
@api.depends('session_ids')
|
||||
def _compute_last_session(self):
|
||||
PosSession = self.env['pos.session']
|
||||
|
||||
@@ -589,10 +589,11 @@ td {
|
||||
color: #6ec89b;
|
||||
}
|
||||
|
||||
|
||||
.pos .pane-border {
|
||||
border-right: solid 3px #787878;
|
||||
}
|
||||
|
||||
.pos .leftpane {
|
||||
border-right: solid 3px #787878;
|
||||
background: #e2e2e2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -2915,119 +2916,30 @@ td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.order-management-screen .flex-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-management-screen .orders {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.order-management-screen .order-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .item {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .item .page-controls {
|
||||
.item .page-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .page-controls > div {
|
||||
.page-controls > div {
|
||||
border: darkgray solid 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .page-controls > div:hover {
|
||||
.page-controls > div:hover {
|
||||
color: rgb(110,200,155);
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .page-controls .previous {
|
||||
.page-controls .previous {
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .page-controls .next {
|
||||
.page-controls .next {
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
|
||||
.order-management-screen .control-panel .search-box {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .search-box .clear {
|
||||
position: relative;
|
||||
right: 25px;
|
||||
cursor: pointer;
|
||||
color: #808080;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .search-box .icon {
|
||||
position: relative;
|
||||
left: 25px;
|
||||
color: #808080;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .search-box input {
|
||||
border: 1px solid #cecbcb;
|
||||
padding: 10px 30px;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
border-radius: 20px;
|
||||
font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .search-box input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 0px 2px rgb(153, 153, 255) inset;
|
||||
color: rgb(153, 153, 255);
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .button {
|
||||
line-height: 32px;
|
||||
padding: 3px 13px;
|
||||
font-size: 20px;
|
||||
background: rgb(230, 230, 230);
|
||||
border-radius: 3px;
|
||||
border: solid 1px rgb(209, 209, 209);
|
||||
cursor: pointer;
|
||||
transition: all 150ms linear;
|
||||
}
|
||||
|
||||
.order-management-screen .control-panel .button:hover {
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
.order-management-screen .back-to-list {
|
||||
font-size: large;
|
||||
padding: 10px;
|
||||
background-color: #6EC89B;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.order-receipt {
|
||||
color: white;
|
||||
font-size: medium;
|
||||
@@ -3200,10 +3112,16 @@ td {
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.ticket-screen .orders .order-row.highlight {
|
||||
color: white;
|
||||
background-color: rgb(110,200,155);
|
||||
}
|
||||
|
||||
.ticket-screen .orders .col {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.ticket-screen .orders .col.start {
|
||||
@@ -3243,6 +3161,20 @@ td {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.ticket-screen .empty-order-list {
|
||||
text-align: center;
|
||||
margin: 48px;
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
.ticket-screen .empty-order-list h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.ticket-screen .empty-order-list .fa {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.ticket-screen .pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -3336,6 +3268,7 @@ td {
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
padding-left: 35px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ticket-screen .pos-search-bar .search li:hover {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderManagementButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
|
||||
class OrderManagementButton extends PosComponent {
|
||||
async onClick() {
|
||||
try {
|
||||
// ping the server, if no error, show the screen
|
||||
await this.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'browse',
|
||||
args: [[]],
|
||||
kwargs: { context: this.env.session.user_context },
|
||||
});
|
||||
this.showScreen('OrderManagementScreen');
|
||||
} catch (error) {
|
||||
if (isRpcError(error) && error.message.code < 0) {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t('Cannot access order management screen if offline.'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OrderManagementButton.template = 'OrderManagementButton';
|
||||
|
||||
Registries.Component.add(OrderManagementButton);
|
||||
|
||||
return OrderManagementButton;
|
||||
});
|
||||
@@ -2,6 +2,7 @@ odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useState, useExternalListener } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
@@ -17,12 +18,12 @@ odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
*
|
||||
* @prop {{
|
||||
* config: {
|
||||
* searchFields: string[],
|
||||
* filter: { show: boolean, options: string[] }
|
||||
* searchFields: Map<string, string>,
|
||||
* filter: { show: boolean, options: Map<string, string> }
|
||||
* },
|
||||
* placeholder: string,
|
||||
* }}
|
||||
* @emits search @payload { fieldValue: string, searchTerm: '' }
|
||||
* @emits search @payload { fieldName: string, searchTerm: '' }
|
||||
* @emits filter-selected @payload { filter: string }
|
||||
*
|
||||
* NOTE: The payload of the emitted event is accessible via the `detail`
|
||||
@@ -31,40 +32,46 @@ odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
class SearchBar extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.config = this.props.config;
|
||||
useExternalListener(window, 'click', this._hideOptions);
|
||||
useListener('click-search-field', this._onClickSearchField);
|
||||
useListener('select-filter', this._onSelectFilter);
|
||||
this.filterOptionsList = [...this.props.config.filter.options.keys()];
|
||||
this.searchFieldsList = [...this.props.config.searchFields.keys()];
|
||||
const defaultSearchFieldId = this.searchFieldsList.indexOf(
|
||||
this.props.config.defaultSearchDetails.fieldName
|
||||
);
|
||||
this.state = useState({
|
||||
searchInput: '',
|
||||
selectedFieldId: this.config.searchFields.length ? 0 : null,
|
||||
searchInput: this.props.config.defaultSearchDetails.searchTerm || '',
|
||||
selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId,
|
||||
showSearchFields: false,
|
||||
showFilterOptions: false,
|
||||
selectedFilter: this.config.filter.options[0] || this.env._t('Select'),
|
||||
selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0],
|
||||
});
|
||||
useExternalListener(window, 'click', this._hideOptions);
|
||||
}
|
||||
selectFilter(option) {
|
||||
this.state.selectedFilter = option;
|
||||
_onSelectFilter({ detail: key }) {
|
||||
this.state.selectedFilter = key;
|
||||
this.trigger('filter-selected', { filter: this.state.selectedFilter });
|
||||
}
|
||||
get placeholder() {
|
||||
return this.props.placeholder;
|
||||
/**
|
||||
* When pressing vertical arrow keys, do not move the input cursor.
|
||||
*/
|
||||
onSearchInputKeydown(event) {
|
||||
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When vertical arrow keys are pressed, select fields for searching.
|
||||
* When enter key is pressed, trigger search event if there is searchInput.
|
||||
*/
|
||||
onKeydown(event) {
|
||||
onSearchInputKeyup(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;
|
||||
this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key);
|
||||
} else if (event.key === 'Enter' || this.state.searchInput == '') {
|
||||
this._onClickSearchField({ detail: this.searchFieldsList[this.state.selectedSearchFieldId] });
|
||||
} else {
|
||||
if (this.state.selectedFieldId === null && this.config.searchFields.length) {
|
||||
this.state.selectedFieldId = 0;
|
||||
if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) {
|
||||
this.state.selectedSearchFieldId = 0;
|
||||
}
|
||||
this.state.showSearchFields = true;
|
||||
}
|
||||
@@ -72,24 +79,21 @@ odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
/**
|
||||
* Called when a search field is clicked.
|
||||
*/
|
||||
onClickSearchField(id) {
|
||||
_onClickSearchField({ detail: fieldName }) {
|
||||
this.state.showSearchFields = false;
|
||||
this.trigger('search', {
|
||||
fieldValue: this.config.searchFields[id],
|
||||
searchTerm: this.state.searchInput,
|
||||
});
|
||||
this.trigger('search', { fieldName, 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.
|
||||
* Given an arrow key, return the next selectedSearchFieldId.
|
||||
* E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2.
|
||||
*
|
||||
* @param {string} key vertical arrow key
|
||||
*/
|
||||
_fieldIdToSelect(key) {
|
||||
const length = this.config.searchFields.length;
|
||||
const length = this.searchFieldsList.length;
|
||||
if (!length) return null;
|
||||
if (this.state.selectedFieldId === null) return 0;
|
||||
const current = this.state.selectedFieldId || length;
|
||||
if (this.state.selectedSearchFieldId === -1) return 0;
|
||||
const current = this.state.selectedSearchFieldId || length;
|
||||
return (current + (key === 'ArrowDown' ? 1 : -1)) % length;
|
||||
}
|
||||
_hideOptions() {
|
||||
@@ -98,17 +102,6 @@ odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
}
|
||||
}
|
||||
SearchBar.template = 'point_of_sale.SearchBar';
|
||||
SearchBar.defaultProps = {
|
||||
config: {
|
||||
searchFields: [],
|
||||
filter: {
|
||||
show: false,
|
||||
options: [],
|
||||
},
|
||||
},
|
||||
placeholder: 'Search ...',
|
||||
};
|
||||
|
||||
Registries.Component.add(SearchBar);
|
||||
|
||||
return SearchBar;
|
||||
|
||||
@@ -6,7 +6,6 @@ odoo.define('point_of_sale.PosContext', function (require) {
|
||||
// Create global context objects
|
||||
// e.g. component.env.device = new Context({ isMobile: false });
|
||||
return {
|
||||
orderManagement: new Context({ searchString: '', selectedOrder: null }),
|
||||
chrome: new Context({ showOrderSelector: true }),
|
||||
};
|
||||
});
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
odoo.define('point_of_sale.MobileOrderManagementScreen', function (require) {
|
||||
const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useState } = owl.hooks;
|
||||
|
||||
const MobileOrderManagementScreen = (OrderManagementScreen) => {
|
||||
class MobileOrderManagementScreen extends OrderManagementScreen {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click-order', this._onShowDetails)
|
||||
this.mobileState = useState({ showDetails: false });
|
||||
}
|
||||
_onShowDetails() {
|
||||
this.mobileState.showDetails = true;
|
||||
}
|
||||
}
|
||||
MobileOrderManagementScreen.template = 'MobileOrderManagementScreen';
|
||||
return MobileOrderManagementScreen;
|
||||
};
|
||||
|
||||
Registries.Component.addByExtending(MobileOrderManagementScreen, OrderManagementScreen);
|
||||
|
||||
return MobileOrderManagementScreen;
|
||||
});
|
||||
@@ -1,214 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderFetcher', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { EventBus } = owl.core;
|
||||
const { Gui } = require('point_of_sale.Gui');
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
const models = require('point_of_sale.models');
|
||||
|
||||
class OrderFetcher extends EventBus {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentPage = 1;
|
||||
this.ordersToShow = [];
|
||||
this.cache = {};
|
||||
this.totalCount = 0;
|
||||
}
|
||||
get activeOrders() {
|
||||
const allActiveOrders = this.comp.env.pos.get('orders').models;
|
||||
return this.searchDomain
|
||||
? allActiveOrders.filter(this._predicateBasedOnSearchDomain.bind(this))
|
||||
: allActiveOrders;
|
||||
}
|
||||
_predicateBasedOnSearchDomain(order) {
|
||||
function check(order, field, searchWord) {
|
||||
searchWord = searchWord.toLowerCase();
|
||||
switch (field) {
|
||||
case 'pos_reference':
|
||||
return order.name.toLowerCase().includes(searchWord);
|
||||
case 'partner_id.display_name':
|
||||
const client = order.get_client();
|
||||
return client ? client.name.toLowerCase().includes(searchWord) : false;
|
||||
case 'date_order':
|
||||
return moment(order.creation_date).format('YYYY-MM-DD hh:mm A').includes(searchWord);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (let [field, _, searchWord] of (this.searchDomain || []).filter((item) => item !== '|')) {
|
||||
// remove surrounding "%" from `searchWord`
|
||||
searchWord = searchWord.substring(1, searchWord.length - 1);
|
||||
if (check(order, field, searchWord)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
get nActiveOrders() {
|
||||
return this.activeOrders.length;
|
||||
}
|
||||
get lastPageFullOfActiveOrders() {
|
||||
return Math.trunc(this.nActiveOrders / this.nPerPage);
|
||||
}
|
||||
get remainingActiveOrders() {
|
||||
return this.nActiveOrders % this.nPerPage;
|
||||
}
|
||||
/**
|
||||
* for nPerPage = 10
|
||||
* +--------+----------+
|
||||
* | nItems | lastPage |
|
||||
* +--------+----------+
|
||||
* | 2 | 1 |
|
||||
* | 10 | 1 |
|
||||
* | 11 | 2 |
|
||||
* | 30 | 3 |
|
||||
* | 35 | 4 |
|
||||
* +--------+----------+
|
||||
*/
|
||||
get lastPage() {
|
||||
const nItems = this.nActiveOrders + this.totalCount;
|
||||
return Math.trunc(nItems / (this.nPerPage + 1)) + 1;
|
||||
}
|
||||
/**
|
||||
* Calling this methods populates the `ordersToShow` then trigger `update` event.
|
||||
* @related get
|
||||
*
|
||||
* NOTE: This is tightly-coupled with pagination. So if the current page contains all
|
||||
* active orders, it will not fetch anything from the server but only sets `ordersToShow`
|
||||
* to the active orders that fits the current page.
|
||||
*/
|
||||
async fetch() {
|
||||
try {
|
||||
let limit, offset;
|
||||
let start, end;
|
||||
if (this.currentPage <= this.lastPageFullOfActiveOrders) {
|
||||
// Show only active orders.
|
||||
start = (this.currentPage - 1) * this.nPerPage;
|
||||
end = this.currentPage * this.nPerPage;
|
||||
this.ordersToShow = this.activeOrders.slice(start, end);
|
||||
} else if (this.currentPage === this.lastPageFullOfActiveOrders + 1) {
|
||||
// Show partially the remaining active orders and
|
||||
// some orders from the backend.
|
||||
offset = 0;
|
||||
limit = this.nPerPage - this.remainingActiveOrders;
|
||||
start = (this.currentPage - 1) * this.nPerPage;
|
||||
end = this.nActiveOrders;
|
||||
this.ordersToShow = [
|
||||
...this.activeOrders.slice(start, end),
|
||||
...(await this._fetch(limit, offset)),
|
||||
];
|
||||
} else {
|
||||
// Show orders from the backend.
|
||||
offset =
|
||||
this.nPerPage -
|
||||
this.remainingActiveOrders +
|
||||
(this.currentPage - (this.lastPageFullOfActiveOrders + 1) - 1) *
|
||||
this.nPerPage;
|
||||
limit = this.nPerPage;
|
||||
this.ordersToShow = await this._fetch(limit, offset);
|
||||
}
|
||||
this.trigger('update');
|
||||
} catch (error) {
|
||||
if (isRpcError(error) && error.message.code < 0) {
|
||||
Gui.showPopup('ErrorPopup', {
|
||||
title: this.comp.env._t('Network Error'),
|
||||
body: this.comp.env._t('Unable to fetch orders if offline.'),
|
||||
});
|
||||
Gui.setSyncStatus('error');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This returns the orders from the backend that needs to be shown.
|
||||
* If the order is already in cache, the full information about that
|
||||
* order is not fetched anymore, instead, we use info from cache.
|
||||
*
|
||||
* @param {number} limit
|
||||
* @param {number} offset
|
||||
*/
|
||||
async _fetch(limit, offset) {
|
||||
const { ids, totalCount } = await this._getOrderIdsForCurrentPage(limit, offset);
|
||||
const idsNotInCache = ids.filter((id) => !(id in this.cache));
|
||||
if (idsNotInCache.length > 0) {
|
||||
const fetchedOrders = await this._fetchOrders(idsNotInCache);
|
||||
// Cache these fetched orders so that next time, no need to fetch
|
||||
// them again, unless invalidated. See `invalidateCache`.
|
||||
fetchedOrders.forEach((order) => {
|
||||
this.cache[order.id] = new models.Order(
|
||||
{},
|
||||
{ pos: this.comp.env.pos, json: order }
|
||||
);
|
||||
});
|
||||
}
|
||||
this.totalCount = totalCount;
|
||||
return ids.map((id) => this.cache[id]);
|
||||
}
|
||||
async _getOrderIdsForCurrentPage(limit, offset) {
|
||||
return await this.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'search_paid_order_ids',
|
||||
kwargs: { config_id: this.configId, domain: this.searchDomain ? this.searchDomain : [], limit, offset },
|
||||
context: this.comp.env.session.user_context,
|
||||
});
|
||||
}
|
||||
async _fetchOrders(ids) {
|
||||
return await this.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'export_for_ui',
|
||||
args: [ids],
|
||||
context: this.comp.env.session.user_context,
|
||||
});
|
||||
}
|
||||
nextPage() {
|
||||
if (this.currentPage < this.lastPage) {
|
||||
this.currentPage += 1;
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
prevPage() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage -= 1;
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {integer|undefined} id id of the cached order
|
||||
* @returns {Array<models.Order>}
|
||||
*/
|
||||
get(id) {
|
||||
if (id) return this.cache[id];
|
||||
return this.ordersToShow;
|
||||
}
|
||||
setSearchDomain(searchDomain) {
|
||||
this.searchDomain = searchDomain;
|
||||
}
|
||||
setComponent(comp) {
|
||||
this.comp = comp;
|
||||
return this;
|
||||
}
|
||||
setConfigId(configId) {
|
||||
this.configId = configId;
|
||||
}
|
||||
setNPerPage(val) {
|
||||
this.nPerPage = val;
|
||||
}
|
||||
setPage(page) {
|
||||
this.currentPage = page;
|
||||
}
|
||||
invalidateCache(ids) {
|
||||
for (let id of ids) {
|
||||
delete this.cache[id];
|
||||
}
|
||||
}
|
||||
async rpc() {
|
||||
Gui.setSyncStatus('connecting');
|
||||
const result = await this.comp.rpc(...arguments);
|
||||
Gui.setSyncStatus('connected');
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return new OrderFetcher();
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderList', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useState } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* @props {models.Order} [initHighlightedOrder] initially highligted order
|
||||
* @props {Array<models.Order>} orders
|
||||
*/
|
||||
class OrderList extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click-order', this._onClickOrder);
|
||||
this.state = useState({ highlightedOrder: this.props.initHighlightedOrder || null });
|
||||
}
|
||||
get highlightedOrder() {
|
||||
return this.state.highlightedOrder;
|
||||
}
|
||||
_onClickOrder({ detail: order }) {
|
||||
this.state.highlightedOrder = order;
|
||||
}
|
||||
}
|
||||
OrderList.template = 'OrderList';
|
||||
|
||||
Registries.Component.add(OrderList);
|
||||
|
||||
return OrderList;
|
||||
});
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderManagementControlPanel', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useContext } = owl.hooks;
|
||||
const { useAutofocus, useListener } = require('web.custom_hooks');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const OrderFetcher = require('point_of_sale.OrderFetcher');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
|
||||
// NOTE: These are constants so that they are only instantiated once
|
||||
// and they can be used efficiently by the OrderManagementControlPanel.
|
||||
const VALID_SEARCH_TAGS = new Set(['date', 'customer', 'client', 'name', 'order']);
|
||||
const FIELD_MAP = {
|
||||
date: 'date_order',
|
||||
customer: 'partner_id.display_name',
|
||||
client: 'partner_id.display_name',
|
||||
name: 'pos_reference',
|
||||
order: 'pos_reference',
|
||||
};
|
||||
const SEARCH_FIELDS = ['pos_reference', 'partner_id.display_name', 'date_order'];
|
||||
|
||||
function getDomainForSingleCondition(fields, toSearch) {
|
||||
const orSymbols = Array(fields.length - 1).fill('|');
|
||||
return orSymbols.concat(fields.map((field) => [field, 'ilike', `%${toSearch}%`]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @emits close-screen
|
||||
* @emits prev-page
|
||||
* @emits next-page
|
||||
* @emits search
|
||||
*/
|
||||
class OrderManagementControlPanel extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// We are using context because we want the `searchString` to be alive
|
||||
// even if this component is destroyed (unmounted).
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
useListener('clear-search', this._onClearSearch);
|
||||
useAutofocus({ selector: 'input' });
|
||||
}
|
||||
onInputKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.trigger('search', this._computeDomain());
|
||||
}
|
||||
}
|
||||
get showPageControls() {
|
||||
return OrderFetcher.lastPage > 1;
|
||||
}
|
||||
get pageNumber() {
|
||||
const currentPage = OrderFetcher.currentPage;
|
||||
const lastPage = OrderFetcher.lastPage;
|
||||
return isNaN(lastPage) ? '' : `(${currentPage}/${lastPage})`;
|
||||
}
|
||||
get validSearchTags() {
|
||||
return VALID_SEARCH_TAGS;
|
||||
}
|
||||
get fieldMap() {
|
||||
return FIELD_MAP;
|
||||
}
|
||||
get searchFields() {
|
||||
return SEARCH_FIELDS;
|
||||
}
|
||||
/**
|
||||
* E.g. 1
|
||||
* ```
|
||||
* searchString = 'Customer 1'
|
||||
* result = [
|
||||
* '|',
|
||||
* '|',
|
||||
* ['pos_reference', 'ilike', '%Customer 1%'],
|
||||
* ['partner_id.display_name', 'ilike', '%Customer 1%'],
|
||||
* ['date_order', 'ilike', '%Customer 1%']
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* E.g. 2
|
||||
* ```
|
||||
* searchString = 'date: 2020-05'
|
||||
* result = [
|
||||
* ['date_order', 'ilike', '%2020-05%']
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* E.g. 3
|
||||
* ```
|
||||
* searchString = 'customer: Steward, date: 2020-05-01'
|
||||
* result = [
|
||||
* ['partner_id.display_name', 'ilike', '%Steward%'],
|
||||
* ['date_order', 'ilike', '%2020-05-01%']
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
_computeDomain() {
|
||||
const input = this.orderManagementContext.searchString.trim();
|
||||
if (!input) return;
|
||||
|
||||
const searchConditions = this.orderManagementContext.searchString.split(/[,&]\s*/);
|
||||
if (searchConditions.length === 1) {
|
||||
let cond = searchConditions[0].split(/:\s*/);
|
||||
if (cond.length === 1) {
|
||||
return getDomainForSingleCondition(this.searchFields, cond[0]);
|
||||
}
|
||||
}
|
||||
const domain = [];
|
||||
for (let cond of searchConditions) {
|
||||
let [tag, value] = cond.split(/:\s*/);
|
||||
if (!this.validSearchTags.has(tag)) continue;
|
||||
domain.push([this.fieldMap[tag], 'ilike', `%${value}%`]);
|
||||
}
|
||||
return domain;
|
||||
}
|
||||
_onClearSearch() {
|
||||
this.orderManagementContext.searchString = '';
|
||||
this.onInputKeydown({ key: 'Enter' });
|
||||
}
|
||||
}
|
||||
OrderManagementControlPanel.template = 'OrderManagementControlPanel';
|
||||
|
||||
Registries.Component.add(OrderManagementControlPanel);
|
||||
|
||||
return OrderManagementControlPanel;
|
||||
});
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useContext } = owl.hooks;
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const OrderFetcher = require('point_of_sale.OrderFetcher');
|
||||
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
|
||||
class OrderManagementScreen extends ControlButtonsMixin(IndependentToOrderScreen) {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('close-screen', this.close);
|
||||
useListener('set-numpad-mode', this._setNumpadMode);
|
||||
useListener('click-order', this._onClickOrder);
|
||||
useListener('next-page', this._onNextPage);
|
||||
useListener('prev-page', this._onPrevPage);
|
||||
useListener('search', this._onSearch);
|
||||
NumberBuffer.use({
|
||||
nonKeyboardInputEvent: 'numpad-click-input',
|
||||
useWithBarcode: true,
|
||||
});
|
||||
this.numpadMode = 'quantity';
|
||||
OrderFetcher.setComponent(this);
|
||||
OrderFetcher.setConfigId(this.env.pos.config_id);
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
}
|
||||
mounted() {
|
||||
OrderFetcher.on('update', this, this.render);
|
||||
this.env.pos.get('orders').on('add remove', this.render, this);
|
||||
|
||||
// calculate how many can fit in the screen.
|
||||
// It is based on the height of the header element.
|
||||
// So the result is only accurate if each row is just single line.
|
||||
const flexContainer = this.el.querySelector('.flex-container');
|
||||
const cpEl = this.el.querySelector('.control-panel');
|
||||
const headerEl = this.el.querySelector('.order-row.header');
|
||||
const val = Math.trunc(
|
||||
(flexContainer.offsetHeight - cpEl.offsetHeight - headerEl.offsetHeight) /
|
||||
headerEl.offsetHeight
|
||||
);
|
||||
OrderFetcher.setNPerPage(val);
|
||||
|
||||
// Fetch the order after mounting so that order management screen
|
||||
// is shown while fetching.
|
||||
setTimeout(() => OrderFetcher.fetch(), 0);
|
||||
}
|
||||
willUnmount() {
|
||||
OrderFetcher.off('update', this);
|
||||
this.env.pos.get('orders').off('add remove', null, this);
|
||||
}
|
||||
get selectedClient() {
|
||||
const order = this.orderManagementContext.selectedOrder;
|
||||
return order ? order.get_client() : null;
|
||||
}
|
||||
get orders() {
|
||||
return OrderFetcher.get();
|
||||
}
|
||||
async _setNumpadMode(event) {
|
||||
const { mode } = event.detail;
|
||||
this.numpadMode = mode;
|
||||
NumberBuffer.reset();
|
||||
}
|
||||
_onNextPage() {
|
||||
OrderFetcher.nextPage();
|
||||
}
|
||||
_onPrevPage() {
|
||||
OrderFetcher.prevPage();
|
||||
}
|
||||
_onSearch({ detail: domain }) {
|
||||
OrderFetcher.setSearchDomain(domain);
|
||||
OrderFetcher.setPage(1);
|
||||
OrderFetcher.fetch();
|
||||
}
|
||||
_onClickOrder({ detail: clickedOrder }) {
|
||||
if (!clickedOrder || clickedOrder.locked) {
|
||||
this.orderManagementContext.selectedOrder = clickedOrder;
|
||||
} else {
|
||||
this._setOrder(clickedOrder);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {models.Order} order
|
||||
*/
|
||||
_setOrder(order) {
|
||||
this.env.pos.set_order(order);
|
||||
if (order === this.env.pos.get_order()) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
OrderManagementScreen.template = 'OrderManagementScreen';
|
||||
OrderManagementScreen.hideOrderSelector = true;
|
||||
|
||||
Registries.Component.add(OrderManagementScreen);
|
||||
|
||||
return OrderManagementScreen;
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
odoo.define('point_of_sale.OrderRow', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* @props {models.Order} order
|
||||
* @props columns
|
||||
* @emits click-order
|
||||
*/
|
||||
class OrderRow extends PosComponent {
|
||||
get order() {
|
||||
return this.props.order;
|
||||
}
|
||||
get highlighted() {
|
||||
const highlightedOrder = this.props.highlightedOrder;
|
||||
return !highlightedOrder ? false : highlightedOrder.backendId === this.props.order.backendId;
|
||||
}
|
||||
|
||||
// Column getters //
|
||||
|
||||
get name() {
|
||||
return this.order.get_name();
|
||||
}
|
||||
get date() {
|
||||
return moment(this.order.validation_date).format('YYYY-MM-DD hh:mm A');
|
||||
}
|
||||
get customer() {
|
||||
const customer = this.order.get('client');
|
||||
return customer ? customer.name : null;
|
||||
}
|
||||
get total() {
|
||||
return this.env.pos.format_currency(this.order.get_total_with_tax());
|
||||
}
|
||||
}
|
||||
OrderRow.template = 'OrderRow';
|
||||
|
||||
Registries.Component.add(OrderRow);
|
||||
|
||||
return OrderRow;
|
||||
});
|
||||
+12
-38
@@ -2,44 +2,30 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useContext } = owl.hooks;
|
||||
const { isRpcError } = require('point_of_sale.utils');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
|
||||
const OrderFetcher = require('point_of_sale.OrderFetcher');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
|
||||
class InvoiceButton extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click', this._onClick);
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
}
|
||||
get selectedOrder() {
|
||||
return this.orderManagementContext.selectedOrder;
|
||||
}
|
||||
set selectedOrder(value) {
|
||||
this.orderManagementContext.selectedOrder = value;
|
||||
}
|
||||
get isAlreadyInvoiced() {
|
||||
if (!this.selectedOrder) return false;
|
||||
return Boolean(this.selectedOrder.account_move);
|
||||
if (!this.props.order) return false;
|
||||
return Boolean(this.props.order.account_move);
|
||||
}
|
||||
get commandName() {
|
||||
if (!this.selectedOrder) {
|
||||
return 'Invoice';
|
||||
if (!this.props.order) {
|
||||
return this.env._t('Invoice');
|
||||
} else {
|
||||
return this.isAlreadyInvoiced
|
||||
? 'Reprint Invoice'
|
||||
: this.selectedOrder.isFromClosedSession
|
||||
? 'Cannot Invoice'
|
||||
: 'Invoice';
|
||||
? this.env._t('Reprint Invoice')
|
||||
: this.props.order.isFromClosedSession
|
||||
? this.env._t('Cannot Invoice')
|
||||
: this.env._t('Invoice');
|
||||
}
|
||||
}
|
||||
get isHighlighted() {
|
||||
return this.selectedOrder && !this.isAlreadyInvoiced && !this.selectedOrder.isFromClosedSession;
|
||||
}
|
||||
async _downloadInvoice(orderId) {
|
||||
try {
|
||||
const [orderWithInvoice] = await this.rpc({
|
||||
@@ -68,7 +54,7 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
|
||||
}
|
||||
}
|
||||
async _invoiceOrder() {
|
||||
const order = this.selectedOrder;
|
||||
const order = this.props.order;
|
||||
if (!order) return;
|
||||
|
||||
const orderId = order.backendId;
|
||||
@@ -93,8 +79,8 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
|
||||
// Write to pos.order the selected client.
|
||||
if (!order.get_client()) {
|
||||
const { confirmed: confirmedPopup } = await this.showPopup('ConfirmPopup', {
|
||||
title: 'Need customer to invoice',
|
||||
body: 'Do you want to open the customer list to select customer?',
|
||||
title: this.env._t('Need customer to invoice'),
|
||||
body: this.env._t('Do you want to open the customer list to select customer?'),
|
||||
});
|
||||
if (!confirmedPopup) return;
|
||||
|
||||
@@ -127,11 +113,7 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
|
||||
|
||||
// Part 3: Download invoice.
|
||||
await this._downloadInvoice(orderId);
|
||||
|
||||
// Invalidate the cache then fetch the updated order.
|
||||
OrderFetcher.invalidateCache([orderId]);
|
||||
await OrderFetcher.fetch();
|
||||
this.selectedOrder = OrderFetcher.get(this.selectedOrder.backendId);
|
||||
this.trigger('order-invoiced', orderId);
|
||||
}
|
||||
async _onClick() {
|
||||
try {
|
||||
@@ -149,14 +131,6 @@ odoo.define('point_of_sale.InvoiceButton', function (require) {
|
||||
}
|
||||
}
|
||||
InvoiceButton.template = 'InvoiceButton';
|
||||
|
||||
OrderManagementScreen.addControlButton({
|
||||
component: InvoiceButton,
|
||||
condition: function () {
|
||||
return this.env.pos.config.module_account;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(InvoiceButton);
|
||||
|
||||
return InvoiceButton;
|
||||
+2
-16
@@ -2,34 +2,20 @@ odoo.define('point_of_sale.ReprintReceiptButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useContext } = owl.hooks;
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const contexts = require('point_of_sale.PosContext');
|
||||
|
||||
class ReprintReceiptButton extends PosComponent {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
useListener('click', this._onClick);
|
||||
this.orderManagementContext = useContext(contexts.orderManagement);
|
||||
}
|
||||
async _onClick() {
|
||||
const order = this.orderManagementContext.selectedOrder;
|
||||
if (!order) return;
|
||||
|
||||
this.showScreen('ReprintReceiptScreen', { order: order });
|
||||
if (!this.props.order) return;
|
||||
this.showScreen('ReprintReceiptScreen', { order: this.props.order });
|
||||
}
|
||||
}
|
||||
ReprintReceiptButton.template = 'ReprintReceiptButton';
|
||||
|
||||
OrderManagementScreen.addControlButton({
|
||||
component: ReprintReceiptButton,
|
||||
condition: function () {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(ReprintReceiptButton);
|
||||
|
||||
return ReprintReceiptButton;
|
||||
+1
-1
@@ -38,7 +38,7 @@ odoo.define('point_of_sale.OrderlineDetails', function (require) {
|
||||
return this.line.quantity;
|
||||
}
|
||||
get unitPrice() {
|
||||
return this.line.unitPrice;
|
||||
return this.env.pos.format_currency(this.line.unitPrice);
|
||||
}
|
||||
get unit() {
|
||||
return this.line.unit;
|
||||
+2
-2
@@ -10,13 +10,13 @@ odoo.define('point_of_sale.ReprintReceiptScreen', function (require) {
|
||||
this.printReceipt();
|
||||
}
|
||||
confirm() {
|
||||
this.showScreen('OrderManagementScreen');
|
||||
this.showScreen('TicketScreen', { reuseSavedUIState: true });
|
||||
}
|
||||
async printReceipt() {
|
||||
if(this.env.pos.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
|
||||
let result = await this._printReceipt();
|
||||
if(result)
|
||||
this.showScreen('OrderManagementScreen');
|
||||
this.showScreen('TicketScreen', { reuseSavedUIState: true });
|
||||
}
|
||||
}
|
||||
async tryReprint() {
|
||||
@@ -1,85 +1,102 @@
|
||||
odoo.define('point_of_sale.TicketScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const models = require('point_of_sale.models');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
|
||||
const { useListener } = require('web.custom_hooks');
|
||||
const { useListener, useAutofocus } = require('web.custom_hooks');
|
||||
const { posbus } = require('point_of_sale.utils');
|
||||
|
||||
const makeDefaultSearchDetails = () => ({
|
||||
fieldName: 'RECEIPT_NUMBER',
|
||||
searchTerm: '',
|
||||
});
|
||||
const TICKET_SCREEN_STATE = {
|
||||
syncedOrders: {
|
||||
currentPage: 1,
|
||||
cache: {},
|
||||
toShow: [],
|
||||
nPerPage: 80,
|
||||
totalCount: null,
|
||||
},
|
||||
ui: {
|
||||
selectedSyncedOrderId: null,
|
||||
searchDetails: makeDefaultSearchDetails(),
|
||||
filter: null,
|
||||
},
|
||||
};
|
||||
|
||||
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();
|
||||
useListener('click-order', this._onClickOrder);
|
||||
useListener('create-new-order', this._onCreateNewOrder);
|
||||
useListener('delete-order', this._onDeleteOrder);
|
||||
useListener('next-page', this._onNextPage);
|
||||
useListener('prev-page', this._onPrevPage);
|
||||
useListener('order-invoiced', this._onInvoiceOrder);
|
||||
useAutofocus({ selector: '.search input'});
|
||||
this._state = TICKET_SCREEN_STATE;
|
||||
const defaultUIState = this.props.reuseSavedUIState
|
||||
? this._state.ui
|
||||
: {
|
||||
selectedSyncedOrderId: null,
|
||||
searchDetails: makeDefaultSearchDetails(),
|
||||
filter: null,
|
||||
};
|
||||
Object.assign(this._state.ui, defaultUIState, this.props.ui || {});
|
||||
}
|
||||
//#region LIFECYCLE METHODS
|
||||
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);
|
||||
setTimeout(() => {
|
||||
// Show updated list of synced orders when going back to the screen.
|
||||
this._onFilterSelected({ detail: { filter: this._state.ui.filter } });
|
||||
});
|
||||
}
|
||||
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 { AllTickets } = this.getOrderStates();
|
||||
const filterCheck = (order) => {
|
||||
if (this.filter && this.filter !== AllTickets) {
|
||||
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();
|
||||
//#endregion
|
||||
//#region EVENT HANDLERS
|
||||
async _onFilterSelected(event) {
|
||||
this._state.ui.filter = event.detail.filter;
|
||||
if (this._state.ui.filter == 'SYNCED') {
|
||||
await this._fetchSyncedOrders();
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
_setOrder(order) {
|
||||
this.env.pos.set_order(order);
|
||||
async _onSearch(event) {
|
||||
Object.assign(this._state.ui.searchDetails, event.detail);
|
||||
if (this._state.ui.filter == 'SYNCED') {
|
||||
this._state.syncedOrders.currentPage = 1;
|
||||
await this._fetchSyncedOrders();
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
createNewOrder() {
|
||||
_onClickOrder({ detail: clickedOrder }) {
|
||||
if (!clickedOrder || clickedOrder.locked) {
|
||||
if (this._state.ui.selectedSyncedOrderId == clickedOrder.backendId) {
|
||||
this._state.ui.selectedSyncedOrderId = null;
|
||||
} else {
|
||||
this._state.ui.selectedSyncedOrderId = clickedOrder.backendId;
|
||||
}
|
||||
} else {
|
||||
this._setOrder(clickedOrder);
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
_onCreateNewOrder() {
|
||||
this.env.pos.add_new_order();
|
||||
}
|
||||
async deleteOrder(order) {
|
||||
async _onDeleteOrder({ detail: order }) {
|
||||
const screen = order.get_screen_data();
|
||||
if (['ProductScreen', 'PaymentScreen'].includes(screen.name) && order.get_orderlines().length > 0) {
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
@@ -90,14 +107,70 @@ odoo.define('point_of_sale.TicketScreen', function (require) {
|
||||
});
|
||||
if (!confirmed) return;
|
||||
}
|
||||
if (order) {
|
||||
await this._canDeleteOrder(order);
|
||||
if (order && (await this._onBeforeDeleteOrder(order))) {
|
||||
order.destroy({ reason: 'abandon' });
|
||||
posbus.trigger('order-deleted');
|
||||
}
|
||||
posbus.trigger('order-deleted');
|
||||
}
|
||||
async _onNextPage() {
|
||||
if (this._state.syncedOrders.currentPage < this._getLastPage()) {
|
||||
this._state.syncedOrders.currentPage += 1;
|
||||
await this._fetchSyncedOrders();
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
async _onPrevPage() {
|
||||
if (this._state.syncedOrders.currentPage > 1) {
|
||||
this._state.syncedOrders.currentPage -= 1;
|
||||
await this._fetchSyncedOrders();
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
async _onInvoiceOrder({ detail: orderId }) {
|
||||
this._invalidateSyncedOrdersCache([orderId]);
|
||||
await this._fetchSyncedOrders();
|
||||
this.render();
|
||||
}
|
||||
//#endregion
|
||||
//#region PUBLIC METHODS
|
||||
getSelectedSyncedOrder() {
|
||||
if (this._state.ui.filter == 'SYNCED') {
|
||||
return this._state.syncedOrders.cache[this._state.ui.selectedSyncedOrderId];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Override to conditionally show the new ticket button.
|
||||
*/
|
||||
shouldShowNewOrderButton() {
|
||||
return true;
|
||||
}
|
||||
getFilteredOrderList() {
|
||||
if (this._state.ui.filter == 'SYNCED') return this._state.syncedOrders.toShow;
|
||||
const filterCheck = (order) => {
|
||||
if (this._state.ui.filter && this._state.ui.filter !== 'ACTIVE_ORDERS') {
|
||||
const screen = order.get_screen_data();
|
||||
return this._state.ui.filter === this._getScreenToStatusMap()[screen.name];
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const { fieldName, searchTerm } = this._state.ui.searchDetails;
|
||||
const searchField = this._getSearchFields()[fieldName];
|
||||
const searchCheck = (order) => {
|
||||
if (!searchField) return true;
|
||||
const repr = searchField.repr(order);
|
||||
if (repr === null) return true;
|
||||
if (!searchTerm) return true;
|
||||
return repr && repr.toString().toLowerCase().includes(searchTerm.toLowerCase());
|
||||
};
|
||||
const predicate = (order) => {
|
||||
return filterCheck(order) && searchCheck(order);
|
||||
};
|
||||
return this._getOrderList().filter(predicate);
|
||||
}
|
||||
getDate(order) {
|
||||
return moment(order.creation_date).format('YYYY-MM-DD hh:mm A');
|
||||
return moment(order.validation_date).format('YYYY-MM-DD hh:mm A');
|
||||
}
|
||||
getTotal(order) {
|
||||
return this.env.pos.format_currency(order.get_total_with_tax());
|
||||
@@ -112,64 +185,104 @@ odoo.define('point_of_sale.TicketScreen', function (require) {
|
||||
return order.employee ? order.employee.name : '';
|
||||
}
|
||||
getStatus(order) {
|
||||
const screen = order.get_screen_data();
|
||||
return this.constants.screenToStatusMap[screen.name];
|
||||
if (order.locked) {
|
||||
return this.env._t('Paid');
|
||||
} else {
|
||||
const screen = order.get_screen_data();
|
||||
return this._getOrderStates().get(this._getScreenToStatusMap()[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');
|
||||
shouldHideDeleteButton(order) {
|
||||
return (
|
||||
order.locked ||
|
||||
order
|
||||
.get_paymentlines()
|
||||
.some((payment) => payment.is_electronic() && payment.get_payment_status() === 'done')
|
||||
);
|
||||
}
|
||||
isHighlighted(order) {
|
||||
if (this._state.ui.filter == 'SYNCED') {
|
||||
const selectedOrder = this.getSelectedSyncedOrder();
|
||||
return selectedOrder ? order.backendId == selectedOrder.backendId : false;
|
||||
} else {
|
||||
const activeOrder = this.env.pos.get_order();
|
||||
return activeOrder ? activeOrder.uid == order.uid : false;
|
||||
}
|
||||
}
|
||||
showCardholderName() {
|
||||
return this.env.pos.payment_methods.some(method => method.use_payment_terminal);
|
||||
return this.env.pos.payment_methods.some((method) => method.use_payment_terminal);
|
||||
}
|
||||
get searchBarConfig() {
|
||||
getSearchBarConfig() {
|
||||
return {
|
||||
searchFields: this.constants.searchFieldNames,
|
||||
filter: { show: true, options: this.filterOptions },
|
||||
searchFields: new Map(
|
||||
Object.entries(this._getSearchFields()).map(([key, val]) => [key, val.displayName])
|
||||
),
|
||||
filter: { show: true, options: this._getFilterOptions() },
|
||||
defaultSearchDetails: this._state.ui.searchDetails,
|
||||
defaultFilter: this._state.ui.filter,
|
||||
};
|
||||
}
|
||||
get filterOptions() {
|
||||
const { AllTickets, Ongoing, Payment, Receipt } = this.getOrderStates();
|
||||
return [AllTickets, Ongoing, Payment, Receipt];
|
||||
shouldShowPageControls() {
|
||||
return this._state.ui.filter == 'SYNCED' && this._getLastPage() > 1;
|
||||
}
|
||||
getPageNumber() {
|
||||
if (!this._state.syncedOrders.totalCount) {
|
||||
return `1/1`;
|
||||
} else {
|
||||
return `${this._state.syncedOrders.currentPage}/${this._getLastPage()}`;
|
||||
}
|
||||
}
|
||||
getSelectedClient() {
|
||||
const order = this.getSelectedSyncedOrder();
|
||||
return order ? order.get_client() : null;
|
||||
}
|
||||
//#endregion
|
||||
//#region PRIVATE METHODS
|
||||
_setOrder(order) {
|
||||
this.env.pos.set_order(order);
|
||||
if (order === this.env.pos.get_order()) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
_getOrderList() {
|
||||
return this.env.pos.get_order_list();
|
||||
}
|
||||
_getFilterOptions() {
|
||||
const orderStates = this._getOrderStates();
|
||||
orderStates.set('SYNCED', this.env._t('Paid'));
|
||||
return orderStates;
|
||||
}
|
||||
/**
|
||||
* 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>
|
||||
* @returns {Record<string, { repr: (order: models.Order) => string, displayName: string, modelField: string }>}
|
||||
*/
|
||||
get _searchFields() {
|
||||
const { ReceiptNumber, Date, Customer, CardholderName } = this.getSearchFieldNames();
|
||||
var fields = {
|
||||
[ReceiptNumber]: (order) => order.name,
|
||||
[Date]: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'),
|
||||
[Customer]: (order) => order.get_client_name(),
|
||||
_getSearchFields() {
|
||||
const fields = {
|
||||
RECEIPT_NUMBER: {
|
||||
repr: (order) => order.name,
|
||||
displayName: this.env._t('Receipt Number'),
|
||||
modelField: 'pos_reference',
|
||||
},
|
||||
DATE: {
|
||||
repr: (order) => moment(order.creation_date).format('YYYY-MM-DD hh:mm A'),
|
||||
displayName: this.env._t('Date'),
|
||||
modelField: 'date_order',
|
||||
},
|
||||
CUSTOMER: {
|
||||
repr: (order) => order.get_client_name(),
|
||||
displayName: this.env._t('Customer'),
|
||||
modelField: 'partner_id.display_name',
|
||||
},
|
||||
};
|
||||
|
||||
if (this.showCardholderName()) {
|
||||
fields[CardholderName] = (order) => order.get_cardholder_name();
|
||||
fields.CARDHOLDER_NAME = {
|
||||
repr: (order) => order.get_cardholder_name(),
|
||||
displayName: this.env._t('Cardholder Name'),
|
||||
modelField: 'payment_ids.cardholder_name',
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
@@ -177,42 +290,101 @@ odoo.define('point_of_sale.TicketScreen', function (require) {
|
||||
/**
|
||||
* Maps the order screen params to order status.
|
||||
*/
|
||||
get _screenToStatusMap() {
|
||||
const { Ongoing, Payment, Receipt } = this.getOrderStates();
|
||||
_getScreenToStatusMap() {
|
||||
return {
|
||||
ProductScreen: Ongoing,
|
||||
PaymentScreen: Payment,
|
||||
ReceiptScreen: Receipt,
|
||||
ProductScreen: 'ONGOING',
|
||||
PaymentScreen: 'PAYMENT',
|
||||
ReceiptScreen: 'RECEIPT',
|
||||
};
|
||||
}
|
||||
_initializeSearchFieldConstants() {
|
||||
this.constants = {};
|
||||
Object.assign(this.constants, {
|
||||
searchFieldNames: Object.keys(this._searchFields),
|
||||
screenToStatusMap: this._screenToStatusMap,
|
||||
});
|
||||
}
|
||||
async _canDeleteOrder(order) {
|
||||
/**
|
||||
* Override to do something before deleting the order.
|
||||
* Make sure to return true to proceed on deleting the order.
|
||||
* @param {*} order
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async _onBeforeDeleteOrder(order) {
|
||||
return true;
|
||||
}
|
||||
getOrderStates() {
|
||||
return {
|
||||
AllTickets: this.env._t('All Tickets'),
|
||||
Ongoing: this.env._t('Ongoing'),
|
||||
Payment: this.env._t('Payment'),
|
||||
Receipt: this.env._t('Receipt'),
|
||||
};
|
||||
_getOrderStates() {
|
||||
// We need the items to be ordered, therefore, Map is used instead of normal object.
|
||||
const states = new Map();
|
||||
states.set('ACTIVE_ORDERS', this.env._t('Active Orders'));
|
||||
states.set('ONGOING', this.env._t('Ongoing'));
|
||||
states.set('PAYMENT', this.env._t('Payment'));
|
||||
states.set('RECEIPT', this.env._t('Receipt'));
|
||||
return states;
|
||||
}
|
||||
getSearchFieldNames() {
|
||||
return {
|
||||
ReceiptNumber: this.env._t('Receipt Number'),
|
||||
Date: this.env._t('Date'),
|
||||
Customer: this.env._t('Customer'),
|
||||
CardholderName: this.env._t('Cardholder Name'),
|
||||
};
|
||||
//#region SEARCH SYNCED ORDERS
|
||||
_computeSyncedOrdersDomain() {
|
||||
const { fieldName, searchTerm } = this._state.ui.searchDetails;
|
||||
if (!searchTerm) return [];
|
||||
const modelField = this._getSearchFields()[fieldName].modelField;
|
||||
if (modelField) {
|
||||
return [[modelField, 'ilike', `%${searchTerm}%`]];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Fetches the done orders from the backend that needs to be shown.
|
||||
* If the order is already in cache, the full information about that
|
||||
* order is not fetched anymore, instead, we use info from cache.
|
||||
*/
|
||||
async _fetchSyncedOrders() {
|
||||
const domain = this._computeSyncedOrdersDomain();
|
||||
const limit = this._state.syncedOrders.nPerPage;
|
||||
const offset = (this._state.syncedOrders.currentPage - 1) * this._state.syncedOrders.nPerPage;
|
||||
const { ids, totalCount } = await this.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'search_paid_order_ids',
|
||||
kwargs: { config_id: this.env.pos.config.id, domain, limit, offset },
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
const idsNotInCache = ids.filter((id) => !(id in this._state.syncedOrders.cache));
|
||||
if (idsNotInCache.length > 0) {
|
||||
const fetchedOrders = await this.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'export_for_ui',
|
||||
args: [idsNotInCache],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
// Cache these fetched orders so that next time, no need to fetch
|
||||
// them again, unless invalidated. See `_onInvoiceOrder`.
|
||||
fetchedOrders.forEach((order) => {
|
||||
this._state.syncedOrders.cache[order.id] = new models.Order({}, { pos: this.env.pos, json: order });
|
||||
});
|
||||
}
|
||||
this._state.syncedOrders.totalCount = totalCount;
|
||||
this._state.syncedOrders.toShow = ids.map((id) => this._state.syncedOrders.cache[id]);
|
||||
}
|
||||
_getLastPage() {
|
||||
const totalCount = this._state.syncedOrders.totalCount;
|
||||
const nPerPage = this._state.syncedOrders.nPerPage;
|
||||
const remainder = totalCount % nPerPage;
|
||||
if (remainder == 0) {
|
||||
return totalCount / nPerPage;
|
||||
} else {
|
||||
return Math.ceil(totalCount / nPerPage);
|
||||
}
|
||||
}
|
||||
_invalidateSyncedOrdersCache(ids) {
|
||||
for (let id of ids) {
|
||||
delete this._state.syncedOrders.cache[id];
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
//#endregion
|
||||
}
|
||||
TicketScreen.template = 'TicketScreen';
|
||||
TicketScreen.defaultProps = {
|
||||
// When passed as true, it will use the saved _state.ui as default
|
||||
// value when this component is reinstantiated.
|
||||
// After setting the default value, the _state.ui will be overridden
|
||||
// by the passed props.ui if there is any.
|
||||
reuseSavedUIState: false,
|
||||
ui: {},
|
||||
};
|
||||
|
||||
Registries.Component.add(TicketScreen);
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
<t t-if="!env.isMobile">
|
||||
<CashierName />
|
||||
</t>
|
||||
<OrderManagementButton t-if="env.pos.config.manage_orders" />
|
||||
<SaleDetailsButton t-if="env.pos.proxy.printer" />
|
||||
<ProxyStatus t-if="env.pos.config.use_proxy" />
|
||||
<ClientScreenButton t-if="clientScreenButtonIsShown" />
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderManagementButton" owl="1">
|
||||
<div class="oe_status order-management" t-on-click="onClick">
|
||||
<div class="oe_icon oe_green">
|
||||
<i class="fa fa-fw fa-search" role="img" aria-label="Order Management Button" title="Order Management Button"></i>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -6,13 +6,13 @@
|
||||
<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" />
|
||||
t-model="state.searchInput" t-on-keydown="onSearchInputKeydown" t-on-keyup="onSearchInputKeyup" type="text" t-att-placeholder="props.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)">
|
||||
<t t-foreach="searchFieldsList" t-as="fieldName" t-key="fieldName">
|
||||
<li t-att-class="{ highlight: fieldName == searchFieldsList[state.selectedSearchFieldId] }"
|
||||
t-on-click="trigger('click-search-field', fieldName)">
|
||||
<span class="field">
|
||||
<t t-esc="value"></t>
|
||||
<t t-esc="props.config.searchFields.get(fieldName)"></t>
|
||||
</span>
|
||||
<span>: </span>
|
||||
<span class="term">
|
||||
@@ -28,12 +28,12 @@
|
||||
<i class="fa fa-chevron-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
<span>
|
||||
<t t-esc="state.selectedFilter" />
|
||||
<t t-esc="props.config.filter.options.get(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>
|
||||
<t t-foreach="filterOptionsList" t-as="key" t-key="key">
|
||||
<li t-on-click="trigger('select-filter', key)">
|
||||
<t t-esc="props.config.filter.options.get(key)"></t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<div t-name="MobileOrderManagementScreen" class="screen-full-width" owl="1">
|
||||
<div t-if="mobileState.showDetails" class="leftpane">
|
||||
<OrderDetails order="orderManagementContext.selectedOrder" />
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
|
||||
<t t-component="cb.component" t-key="cb.name" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="subpads">
|
||||
<ActionpadWidget client="selectedClient" />
|
||||
<NumpadWidget />
|
||||
</div>
|
||||
</div>
|
||||
<div class="back-to-list" t-on-click="mobileState.showDetails = false">
|
||||
<span>Back to list</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="" class="rightpane">
|
||||
<div class="flex-container">
|
||||
<OrderManagementControlPanel />
|
||||
<OrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderList" owl="1">
|
||||
<div class="orders">
|
||||
<div class="order-row header">
|
||||
<div class="header name">Order</div>
|
||||
<div class="header date">Date</div>
|
||||
<div class="header customer">Customer</div>
|
||||
<div class="header total">Total</div>
|
||||
</div>
|
||||
<div class="order-list">
|
||||
<t t-foreach="props.orders" t-as="order" t-key="order.cid">
|
||||
<OrderRow order="order" highlightedOrder="highlightedOrder" />
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderManagementControlPanel" owl="1">
|
||||
<div class="control-panel">
|
||||
<div class="item button back" t-on-click="trigger('close-screen')">
|
||||
<i class="fa fa-angle-double-left"></i>
|
||||
<span> Back</span>
|
||||
</div>
|
||||
<div class="item search-box">
|
||||
<span class="icon">
|
||||
<i class="fa fa-search" />
|
||||
</span>
|
||||
<input type="text" t-model="orderManagementContext.searchString" t-on-keydown="onInputKeydown" placeholder="E.g. customer: Steward, date: 2020-05-09" />
|
||||
<span class="clear" t-on-click="trigger('clear-search')">
|
||||
<i class="fa fa-remove" />
|
||||
</span>
|
||||
</div>
|
||||
<div t-if="showPageControls" class="item">
|
||||
<div class="page-controls">
|
||||
<div class="previous" t-on-click="trigger('prev-page')">
|
||||
<i class="fa fa-fw fa-caret-left" role="img" aria-label="Previous Order List" title="Previous Order List"></i>
|
||||
</div>
|
||||
<div class="next" t-on-click="trigger('next-page')">
|
||||
<i class="fa fa-fw fa-caret-right" role="img" aria-label="Next Order List" title="Next Order List"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<span><t t-esc="pageNumber" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="" class="item"></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderManagementScreen" owl="1">
|
||||
<div class="order-management-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
|
||||
<div t-if="!env.isMobile" class="screen-full-width">
|
||||
<div class="leftpane">
|
||||
<OrderDetails order="orderManagementContext.selectedOrder" />
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
<t t-foreach="controlButtons" t-as="cb" t-key="cb.name">
|
||||
<t t-component="cb.component" t-key="cb.name" />
|
||||
</t>
|
||||
</div>
|
||||
<div class="subpads">
|
||||
<ActionpadWidget client="selectedClient" />
|
||||
<NumpadWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rightpane">
|
||||
<div class="flex-container">
|
||||
<OrderManagementControlPanel />
|
||||
<OrderList orders="orders" initHighlightedOrder="orderManagementContext.selectedOrder" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MobileOrderManagementScreen t-else="" />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderRow" owl="1">
|
||||
<div class="order-row"
|
||||
t-att-class="{ highlight: highlighted, lighter: !props.order.locked }"
|
||||
t-on-click="trigger('click-order', props.order)">
|
||||
<div class="item name"><t t-esc="name" /></div>
|
||||
<div class="item date"><t t-esc="date" /></div>
|
||||
<div class="item customer"><t t-esc="customer" /></div>
|
||||
<div class="item total"><t t-esc="total" /></div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -4,7 +4,7 @@
|
||||
<t t-name="ProductScreen" owl="1">
|
||||
<div class="product-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
|
||||
<div class="screen-full-width">
|
||||
<div class="leftpane" t-if="!env.isMobile || mobile_pane === 'left'">
|
||||
<div class="leftpane pane-border" t-if="!env.isMobile || mobile_pane === 'left'">
|
||||
<OrderWidget/>
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="InvoiceButton" owl="1">
|
||||
<div class="control-button" t-att-class="{ highlight: isHighlighted }">
|
||||
<div class="control-button">
|
||||
<i class="fa fa-file-pdf-o"></i>
|
||||
<span> </span>
|
||||
<span><t t-esc="commandName"></t></span>
|
||||
+17
-1
@@ -25,7 +25,23 @@
|
||||
<OrderlineDetails line="orderline" />
|
||||
</t>
|
||||
</ul>
|
||||
<OrderSummary total="total" tax="tax" />
|
||||
<!-- Not using OrderSummary because we don't need it's overrides in this view. -->
|
||||
<div class="summary clearfix">
|
||||
<div class="line">
|
||||
<div class="entry total">
|
||||
<span class="badge">Total: </span>
|
||||
<span class="value">
|
||||
<t t-esc="total" />
|
||||
</span>
|
||||
<div t-if="tax" class="subentry">
|
||||
Taxes:
|
||||
<span class="value">
|
||||
<t t-esc="tax" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,56 +2,94 @@
|
||||
<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 Order</button>
|
||||
<button class="discard" t-on-click="trigger('close-screen')">Discard</button>
|
||||
</div>
|
||||
<t t-set="placeholder">Search Tickets...</t>
|
||||
<SearchBar config="searchBarConfig" placeholder="placeholder" />
|
||||
</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 wide" t-if="showCardholderName()">Cardholder Name</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 t-if="showCardholderName()" class="col start">
|
||||
<t t-esc="getCardholderName(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 class="ticket-screen screen" t-att-class="{ oe_hidden: !props.isShown }">
|
||||
<div class="screen-full-width">
|
||||
<div class="rightpane pane-border">
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<button t-if="shouldShowNewOrderButton()" class="highlight" t-on-click="trigger('create-new-order')">New Order</button>
|
||||
<button class="discard" t-on-click="trigger('close-screen')">Back</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="_placeholder">Search Orders...</t>
|
||||
<SearchBar config="getSearchBarConfig()" placeholder="_placeholder" />
|
||||
<div t-if="shouldShowPageControls()" class="item">
|
||||
<div class="page-controls">
|
||||
<div class="previous" t-on-click="trigger('prev-page')">
|
||||
<i class="fa fa-fw fa-caret-left" role="img" aria-label="Previous Order List" title="Previous Order List"></i>
|
||||
</div>
|
||||
<div class="next" t-on-click="trigger('next-page')">
|
||||
<i class="fa fa-fw fa-caret-right" role="img" aria-label="Next Order List" title="Next Order List"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<span><t t-esc="getPageNumber()" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="orders">
|
||||
<t t-set="_filteredOrderList" t-value="getFilteredOrderList()" />
|
||||
<t t-if="_filteredOrderList.length !== 0">
|
||||
<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 wide" t-if="showCardholderName()">Cardholder Name</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-att-class="{ highlight: isHighlighted(order) }" t-on-click="trigger('click-order', 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 t-if="showCardholderName()" class="col start">
|
||||
<t t-esc="getCardholderName(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="!shouldHideDeleteButton(order)" class="col center very-narrow delete-button" name="delete" t-on-click.stop="trigger('delete-order', order)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div t-else="" class="col center very-narrow delete-button"></div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="empty-order-list">
|
||||
<i role="img" aria-label="Shopping cart" title="Shopping cart" class="fa fa-shopping-cart"></i>
|
||||
<h1>No orders found</h1>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leftpane">
|
||||
<t t-set="_selectedSyncedOrder" t-value="getSelectedSyncedOrder()" />
|
||||
<OrderDetails order="_selectedSyncedOrder" />
|
||||
<div class="pads">
|
||||
<div class="control-buttons">
|
||||
<InvoiceButton t-if="env.pos.config.module_account" order="_selectedSyncedOrder" />
|
||||
<ReprintReceiptButton order="_selectedSyncedOrder" />
|
||||
</div>
|
||||
<div class="subpads">
|
||||
<ActionpadWidget client="getSelectedClient()" />
|
||||
<NumpadWidget />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,6 +56,7 @@ odoo.define('point_of_sale.tour.Chrome', function (require) {
|
||||
// Pay order 1, with change
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectOrder('-0001');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.do.clickPayButton();
|
||||
PaymentScreen.do.clickPaymentMethod('Cash');
|
||||
PaymentScreen.do.pressNumpad('2 0');
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
odoo.define('point_of_sale.tour.OrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { OrderManagementScreen } = require('point_of_sale.tour.OrderManagementScreenTourMethods');
|
||||
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');
|
||||
var Tour = require('web_tour.tour');
|
||||
|
||||
// signal to start generating steps
|
||||
// when finished, steps can be taken from getSteps
|
||||
startSteps();
|
||||
|
||||
// Go by default to home category
|
||||
ProductScreen.do.clickHomeCategory();
|
||||
|
||||
// make one order and check if it can be seen from the management screen.
|
||||
// order 0001
|
||||
makeFullOrder({ orderlist: [['Whiteboard Pen', '5', '6']], payment: ['Cash', '30'], customerNote: 'Customer Note' });
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0001', total: '30' });
|
||||
|
||||
OrderManagementScreen.do.clickBack();
|
||||
|
||||
// make multiple orders and check them in the management screen.
|
||||
// order 0002
|
||||
makeFullOrder({
|
||||
orderlist: [
|
||||
['Desk Pad', '1', '2'],
|
||||
['Monitor Stand', '3', '4'],
|
||||
['Whiteboard Pen', '5', '6'],
|
||||
],
|
||||
payment: ['Bank', '44'],
|
||||
});
|
||||
// order 0003
|
||||
makeFullOrder({
|
||||
orderlist: [
|
||||
['Desk Pad', '1', '2'],
|
||||
['Whiteboard Pen', '5', '6'],
|
||||
],
|
||||
customer: 'Colleen Diaz',
|
||||
payment: ['Cash', '50'],
|
||||
});
|
||||
// order 0004
|
||||
makeFullOrder({
|
||||
orderlist: [
|
||||
['Monitor Stand', '3', '4'],
|
||||
['Whiteboard Pen', '5', '6'],
|
||||
],
|
||||
payment: ['Bank', '42'],
|
||||
});
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0002', total: '44' });
|
||||
OrderManagementScreen.check.orderlistHas({
|
||||
orderName: '0003',
|
||||
total: '32',
|
||||
customer: 'Colleen Diaz',
|
||||
});
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0004', total: '42' });
|
||||
|
||||
// click the currently active order
|
||||
OrderManagementScreen.do.clickOrder('-0005');
|
||||
ProductScreen.check.isShown();
|
||||
|
||||
// Add 2 orders, they should appear in order management screen
|
||||
// order 0006
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.clickNewTicket();
|
||||
ProductScreen.exec.addOrderline('Whiteboard Pen', '66', '6');
|
||||
|
||||
// order 0007, should be at payment screen
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.clickNewTicket();
|
||||
ProductScreen.exec.addOrderline('Monitor Stand', '55', '5');
|
||||
ProductScreen.do.clickCustomerButton();
|
||||
ClientListScreen.exec.setClient('Azure Interior');
|
||||
ProductScreen.do.clickPayButton();
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0006', total: '396' });
|
||||
OrderManagementScreen.check.orderlistHas({
|
||||
orderName: '-0007',
|
||||
total: '275',
|
||||
customer: 'Azure Interior',
|
||||
});
|
||||
|
||||
// select a paid order, order row should be highlighted and should show order details
|
||||
OrderManagementScreen.do.clickOrder('-0004');
|
||||
OrderManagementScreen.check.highlightedOrderRowHas('-0004');
|
||||
OrderManagementScreen.check.orderDetailsHas({
|
||||
lines: [
|
||||
{ product: 'Monitor Stand', quantity: '3' },
|
||||
{ product: 'Whiteboard Pen', quantity: '5' },
|
||||
],
|
||||
total: '42',
|
||||
});
|
||||
OrderManagementScreen.do.clickOrder('-0001');
|
||||
OrderManagementScreen.check.highlightedOrderRowHas('-0001');
|
||||
// 0004 should not be highlighted anymore
|
||||
OrderManagementScreen.check.orderRowIsNotHighlighted('-0004');
|
||||
OrderManagementScreen.check.orderDetailsHas({
|
||||
lines: [{ product: 'Whiteboard Pen', quantity: '5' }],
|
||||
total: '30',
|
||||
customerNote: 'Customer Note',
|
||||
});
|
||||
//verify the customer note in the receipt
|
||||
OrderManagementScreen.do.clickPrintReceiptButton();
|
||||
OrderManagementScreen.check.reprintReceiptIsShown();
|
||||
OrderManagementScreen.check.receiptCustomerNoteIs('Customer Note');
|
||||
OrderManagementScreen.do.closeReceipt();
|
||||
|
||||
|
||||
// Select a paid order then invoice it. The selected order should remain selected
|
||||
// and will contain a new customer. After invoice, the current customer should be removed.
|
||||
// TODO: enable the following steps once the issue in invoicing is solved.
|
||||
// OrderManagementScreen.do.clickInvoiceButton();
|
||||
// Chrome.do.confirmPopup();
|
||||
// ClientListScreen.check.isShown();
|
||||
// ClientListScreen.exec.setClient('Jesse Brown');
|
||||
// OrderManagementScreen.check.highlightedOrderRowHas('Jesse Brown');
|
||||
|
||||
// Check if order 0007 is selected, it should be at payment screen
|
||||
OrderManagementScreen.do.clickOrder('-0007');
|
||||
PaymentScreen.check.isShown();
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.do.clickOrder('-0003');
|
||||
OrderManagementScreen.do.clickPrintReceiptButton();
|
||||
OrderManagementScreen.check.reprintReceiptIsShown();
|
||||
OrderManagementScreen.check.receiptChangeIs('18.0');
|
||||
OrderManagementScreen.check.receiptOrderDataContains('-0003');
|
||||
OrderManagementScreen.check.receiptAmountIs('32.0');
|
||||
OrderManagementScreen.do.closeReceipt();
|
||||
OrderManagementScreen.check.isNotHidden();
|
||||
|
||||
Tour.register('OrderManagementScreenTour', { test: true, url: '/pos/ui' }, getSteps());
|
||||
});
|
||||
@@ -4,6 +4,7 @@ odoo.define('point_of_sale.tour.TicketScreen', 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 { ClientListScreen } = require('point_of_sale.tour.ClientListScreenTourMethods');
|
||||
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');
|
||||
@@ -41,7 +42,7 @@ odoo.define('point_of_sale.tour.TicketScreen', function (require) {
|
||||
TicketScreen.check.nthRowContains(2, 'Payment');
|
||||
TicketScreen.do.selectFilter('Ongoing');
|
||||
TicketScreen.check.nthRowContains(2, 'Ongoing');
|
||||
TicketScreen.do.selectFilter('All');
|
||||
TicketScreen.do.selectFilter('Active Orders');
|
||||
TicketScreen.check.nthRowContains(4, 'Receipt');
|
||||
TicketScreen.do.search('Customer', 'Nicole');
|
||||
TicketScreen.check.nthRowContains(2, 'Nicole');
|
||||
@@ -49,6 +50,41 @@ odoo.define('point_of_sale.tour.TicketScreen', function (require) {
|
||||
TicketScreen.check.nthRowContains(2, 'Brandon');
|
||||
TicketScreen.do.search('Receipt Number', '-0003');
|
||||
TicketScreen.check.nthRowContains(2, 'Receipt');
|
||||
// Close the TicketScreen to see the current order which is in ReceiptScreen.
|
||||
// This is just to remove the search string in the search bar.
|
||||
TicketScreen.do.clickDiscard();
|
||||
ReceiptScreen.check.isShown();
|
||||
// Open again the TicketScreen to check the Paid filter.
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectFilter('Paid');
|
||||
TicketScreen.check.nthRowContains(2, '-0003');
|
||||
// Pay the order that was in PaymentScreen.
|
||||
TicketScreen.do.selectFilter('Payment');
|
||||
TicketScreen.do.selectOrder('-0002');
|
||||
PaymentScreen.do.clickPaymentMethod('Cash');
|
||||
PaymentScreen.do.clickValidate();
|
||||
ReceiptScreen.check.isShown();
|
||||
ReceiptScreen.do.clickNextOrder();
|
||||
ProductScreen.check.isShown();
|
||||
// Check that the Paid filter will show the 2 synced orders.
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectFilter('Paid');
|
||||
TicketScreen.check.nthRowContains(2, 'Brandon Freeman');
|
||||
TicketScreen.check.nthRowContains(3, '-0003');
|
||||
// Invoice order
|
||||
TicketScreen.do.selectOrder('-0003');
|
||||
TicketScreen.check.orderWidgetIsNotEmpty();
|
||||
TicketScreen.do.clickControlButton('Invoice');
|
||||
Chrome.do.confirmPopup();
|
||||
ClientListScreen.check.isShown();
|
||||
ClientListScreen.exec.setClient('Colleen Diaz');
|
||||
TicketScreen.check.customerIs('Colleen Diaz');
|
||||
// Reprint receipt
|
||||
TicketScreen.do.clickControlButton('Print Receipt');
|
||||
ReceiptScreen.check.isShown();
|
||||
ReceiptScreen.do.clickBack();
|
||||
// When going back, the ticket screen should be in its previous state.
|
||||
TicketScreen.check.filterIs('Paid');
|
||||
|
||||
Tour.register('TicketScreenTour', { test: true, url: '/pos/ui' }, getSteps());
|
||||
});
|
||||
|
||||
@@ -12,19 +12,6 @@ odoo.define('point_of_sale.tour.ChromeTourMethods', function (require) {
|
||||
},
|
||||
];
|
||||
}
|
||||
clickOrderManagementButton() {
|
||||
return [
|
||||
{
|
||||
content: 'check order management button is shown',
|
||||
trigger: '.pos .pos-rightheader .order-management',
|
||||
run: () => {},
|
||||
},
|
||||
{
|
||||
content: 'click order management button',
|
||||
trigger: '.pos .pos-rightheader .order-management',
|
||||
},
|
||||
];
|
||||
}
|
||||
clickTicketButton() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
odoo.define('point_of_sale.tour.OrderManagementScreenTourMethods', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { createTourMethods } = require('point_of_sale.tour.utils');
|
||||
|
||||
class Do {
|
||||
clickBack() {
|
||||
return [
|
||||
{
|
||||
content: 'order management screen, click back button',
|
||||
trigger: '.order-management-screen .control-panel .button.back',
|
||||
},
|
||||
];
|
||||
}
|
||||
clickOrder(name, [otherCol, otherColVal] = [null, null]) {
|
||||
let trigger = `.order-management-screen .order-list .order-row .item.name:contains("${name}")`;
|
||||
if (otherCol) {
|
||||
trigger = `${trigger} ~ .item.${otherCol}:contains("${otherColVal}")`;
|
||||
}
|
||||
return [
|
||||
{
|
||||
content: `clicking order '${name}' from orderlist`,
|
||||
trigger,
|
||||
},
|
||||
];
|
||||
}
|
||||
clickInvoiceButton() {
|
||||
return [
|
||||
{
|
||||
content: 'click invoice button',
|
||||
trigger: '.order-management-screen .control-button span:contains("Invoice")',
|
||||
},
|
||||
];
|
||||
}
|
||||
clickPrintReceiptButton() {
|
||||
return [
|
||||
{
|
||||
content: 'click reprint receipt button',
|
||||
trigger: '.order-management-screen .control-button span:contains("Print Receipt")'
|
||||
}
|
||||
]
|
||||
}
|
||||
clickCustomerButton() {
|
||||
return [
|
||||
{
|
||||
content: 'click customer button',
|
||||
trigger: '.order-management-screen .actionpad .button.set-customer',
|
||||
},
|
||||
];
|
||||
}
|
||||
closeReceipt() {
|
||||
return [
|
||||
{
|
||||
content: 'close receipt',
|
||||
trigger: '.receipt-screen .button.back',
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
class Check {
|
||||
isShown() {
|
||||
return [
|
||||
{
|
||||
content: 'order management screen is shown',
|
||||
trigger: '.pos .pos-content .order-management-screen',
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
orderlistHas({ orderName, total, customer }) {
|
||||
const steps = [];
|
||||
steps.push({
|
||||
content: `order list has row having: name '${orderName}', total '${total}'`,
|
||||
trigger: `.order-list .order-row .item:contains("${orderName}") ~ .item:contains("${total}")`,
|
||||
run: () => {},
|
||||
});
|
||||
if (customer) {
|
||||
steps.push({
|
||||
content: `order list has row having: name '${orderName}', customer '${customer}'`,
|
||||
trigger: `.order-list .order-row .item:contains("${orderName}") ~ .item:contains("${customer}")`,
|
||||
run: () => {},
|
||||
});
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
highlightedOrderRowHas(name) {
|
||||
return [
|
||||
{
|
||||
content: `order '${name}' in orderlist is highligted`,
|
||||
trigger: `.order-list .order-row.highlight:has(> .item:contains("${name}"))`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
orderRowIsNotHighlighted(name) {
|
||||
return [
|
||||
{
|
||||
content: `order '${name}' in orderlist is not highligted`,
|
||||
trigger: `.order-list .order-row:not(:has(.highlight)):has(> .item:contains("${name}"))`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
orderDetailsHas({ lines, total, customerNote }) {
|
||||
const steps = [];
|
||||
for (let { product, quantity } of lines) {
|
||||
steps.push({
|
||||
content: `order details has product '${product}' and quantity '${quantity}'`,
|
||||
trigger: `.orderlines .product-name:contains("${product}") ~ .info strong:contains("${quantity}")`,
|
||||
run: () => {},
|
||||
});
|
||||
}
|
||||
if (total) {
|
||||
steps.push({
|
||||
content: `order details has total amount of ${total}`,
|
||||
trigger: `.order-container .summary .total .value:contains("${total}")`,
|
||||
run: () => {},
|
||||
});
|
||||
}
|
||||
if (customerNote) {
|
||||
steps.push({
|
||||
content: `order details has a line with a customer note: ${customerNote}`,
|
||||
trigger: `.orderlines .orderline-note:contains("${customerNote}")`,
|
||||
run: () => {},
|
||||
})
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
customerIs(name) {
|
||||
return [
|
||||
{
|
||||
content: `set customer is '${name}'`,
|
||||
trigger: `.order-management-screen .actionpad .set-customer:contains("${name}")`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
reprintReceiptIsShown() {
|
||||
return [
|
||||
{
|
||||
content: 'reprint receipt screen is shown',
|
||||
trigger: '.pos .receipt-screen',
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
receiptChangeIs(amount) {
|
||||
return [
|
||||
{
|
||||
content: `receipt change is ${amount}`,
|
||||
trigger: `.pos-receipt-amount.receipt-change:contains("${amount}")`,
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
receiptOrderDataContains(orderInfo) {
|
||||
return [
|
||||
{
|
||||
content: `order data contains ${orderInfo}`,
|
||||
trigger: `.pos-receipt-order-data:contains("${orderInfo}")`,
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
receiptAmountIs(amount) {
|
||||
return [
|
||||
{
|
||||
content: `receipt amount is ${amount}`,
|
||||
trigger: `.pos-receipt-amount:contains("${amount}")`,
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
isNotHidden() {
|
||||
return [
|
||||
{
|
||||
content: 'order management screen is not hidden',
|
||||
trigger: `.order-management-screen:not(:has(.oe_hidden))`,
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
receiptCustomerNoteIs(note) {
|
||||
return [
|
||||
{
|
||||
content: `receipt has customer note: ${note}`,
|
||||
trigger: `.orderlines .pos-receipt-left-padding:contains("${note}")`,
|
||||
run: () => {},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return createTourMethods('OrderManagementScreen', Do, Check);
|
||||
});
|
||||
@@ -27,6 +27,13 @@ odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) {
|
||||
},
|
||||
];
|
||||
}
|
||||
clickBack() {
|
||||
return [
|
||||
{
|
||||
trigger: '.receipt-screen .button.back',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class Check {
|
||||
|
||||
@@ -46,14 +46,14 @@ odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) {
|
||||
},
|
||||
{
|
||||
/**
|
||||
* Manually trigger keydown event to show the search field list
|
||||
* because the previous step do not trigger keydown event.
|
||||
* Manually trigger keyup event to show the search field list
|
||||
* because the previous step do not trigger keyup event.
|
||||
*/
|
||||
trigger: '.pos-search-bar input',
|
||||
run: function () {
|
||||
document
|
||||
.querySelector('.pos-search-bar input')
|
||||
.dispatchEvent(new KeyboardEvent('keydown', { key: '' }));
|
||||
.dispatchEvent(new KeyboardEvent('keyup', { key: '' }));
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -68,6 +68,13 @@ odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) {
|
||||
},
|
||||
];
|
||||
}
|
||||
clickControlButton(name) {
|
||||
return [
|
||||
{
|
||||
trigger: `.ticket-screen .control-button:contains("${name}")`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class Check {
|
||||
@@ -99,6 +106,30 @@ odoo.define('point_of_sale.tour.TicketScreenTourMethods', function (require) {
|
||||
},
|
||||
];
|
||||
}
|
||||
orderWidgetIsNotEmpty() {
|
||||
return [
|
||||
{
|
||||
trigger: '.ticket-screen:not(:has(.order-empty))',
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
customerIs(name) {
|
||||
return [
|
||||
{
|
||||
trigger: `.ticket-screen .set-customer:contains("${name}")`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
filterIs(name) {
|
||||
return [
|
||||
{
|
||||
trigger: `.ticket-screen .filter:contains("${name}")`,
|
||||
run: () => {},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class Execute {}
|
||||
|
||||
@@ -519,13 +519,6 @@ class TestUi(TestPointOfSaleHttpCommon):
|
||||
self.assertEqual(n_invoiced, 1, 'There should be 1 invoiced order.')
|
||||
self.assertEqual(n_paid, 2, 'There should be 2 paid order.')
|
||||
|
||||
def test_03_order_management(self):
|
||||
if hasattr(self.main_pos_config, 'module_pos_restaurant'):
|
||||
self.main_pos_config.module_pos_restaurant = False
|
||||
self.main_pos_config.write({ 'manage_orders': True, 'module_account': True })
|
||||
self.main_pos_config.open_session_cb(check_coa=False)
|
||||
self.start_tour("/pos/ui?config_id=%d" % self.main_pos_config.id, 'OrderManagementScreenTour', login="accountman")
|
||||
|
||||
def test_04_product_configurator(self):
|
||||
self.main_pos_config.write({ 'product_configurator': True })
|
||||
self.main_pos_config.open_session_cb(check_coa=False)
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
<field name="company_has_template" invisible="1"/>
|
||||
<field name="allowed_pricelist_ids" invisible="1"/>
|
||||
<field name="has_active_session" invisible="1"/>
|
||||
<field name="show_allow_invoicing_alert" invisible="1"/>
|
||||
<div class="oe_title" id="title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. NYC Shop"/></h1>
|
||||
@@ -91,20 +90,6 @@
|
||||
</div>
|
||||
<h2 name="order">PoS Interface</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="manage_orders" />
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="manage_orders" />
|
||||
<div class="text-muted">
|
||||
Reprint receipt, refund and exchange orders
|
||||
</div>
|
||||
<div class="o_notification_alert alert alert-warning" attrs="{'invisible':[('show_allow_invoicing_alert', '=', False)]}" role="alert">
|
||||
Allow invoicing of paid orders by activating 'Invoicing' in the 'Bills & Receipts' section.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" groups="product.group_product_variant" >
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="product_configurator" />
|
||||
|
||||
@@ -51,8 +51,6 @@ This module adds several features to the Point of Sale that are specific to rest
|
||||
'pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js',
|
||||
'pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js',
|
||||
'pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js',
|
||||
'pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js',
|
||||
'pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js',
|
||||
'pos_restaurant/static/src/js/Screens/TicketScreen.js',
|
||||
'pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js',
|
||||
'pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js',
|
||||
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
odoo.define('pos_restaurant.OrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const OrderManagementScreen = require('point_of_sale.OrderManagementScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosResOrderManagementScreen = (OrderManagementScreen) =>
|
||||
class extends OrderManagementScreen {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_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 {
|
||||
super._setOrder(order);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(OrderManagementScreen, PosResOrderManagementScreen);
|
||||
|
||||
return OrderManagementScreen;
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
odoo.define('pos_restaurant.OrderRow', function (require) {
|
||||
'use strict';
|
||||
|
||||
const OrderRow = require('point_of_sale.OrderRow');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosResOrderRow = (OrderRow) =>
|
||||
class extends OrderRow {
|
||||
get table() {
|
||||
return this.order.table ? this.order.table.name : '';
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(OrderRow, PosResOrderRow);
|
||||
|
||||
return OrderRow;
|
||||
});
|
||||
@@ -19,20 +19,10 @@ odoo.define('pos_restaurant.TicketScreen', function (require) {
|
||||
posbus.trigger('table-set');
|
||||
}
|
||||
}
|
||||
get filterOptions() {
|
||||
const { Payment, Open, Tipping } = this.getOrderStates();
|
||||
var filterOptions = super.filterOptions;
|
||||
if (this.env.pos.config.set_tip_after_payment) {
|
||||
var idx = filterOptions.indexOf(Payment);
|
||||
filterOptions[idx] = Open;
|
||||
}
|
||||
return [...filterOptions, Tipping];
|
||||
}
|
||||
get _screenToStatusMap() {
|
||||
const { Open, Tipping } = this.getOrderStates();
|
||||
return Object.assign(super._screenToStatusMap, {
|
||||
PaymentScreen: this.env.pos.config.set_tip_after_payment ? Open : super._screenToStatusMap.PaymentScreen,
|
||||
TipScreen: Tipping,
|
||||
_getScreenToStatusMap() {
|
||||
return Object.assign(super._getScreenToStatusMap(), {
|
||||
PaymentScreen: this.env.pos.config.set_tip_after_payment ? 'OPEN' : super._getScreenToStatusMap().PaymentScreen,
|
||||
TipScreen: 'TIPPING',
|
||||
});
|
||||
}
|
||||
getTable(order) {
|
||||
@@ -47,7 +37,7 @@ odoo.define('pos_restaurant.TicketScreen', function (require) {
|
||||
});
|
||||
}
|
||||
_setOrder(order) {
|
||||
if (!this.env.pos.config.iface_floorplan) {
|
||||
if (!this.env.pos.config.iface_floorplan || order === this.env.pos.get_order()) {
|
||||
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.
|
||||
@@ -56,19 +46,19 @@ odoo.define('pos_restaurant.TicketScreen', function (require) {
|
||||
this.env.pos.set_table(order.table, order);
|
||||
}
|
||||
}
|
||||
get showNewTicketButton() {
|
||||
return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.showNewTicketButton;
|
||||
shouldShowNewOrderButton() {
|
||||
return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.shouldShowNewOrderButton();
|
||||
}
|
||||
get orderList() {
|
||||
_getOrderList() {
|
||||
if (this.env.pos.table) {
|
||||
return super.orderList;
|
||||
return super._getOrderList();
|
||||
} else {
|
||||
return this.env.pos.get('orders').models;
|
||||
}
|
||||
}
|
||||
async settleTips() {
|
||||
// set tip in each order
|
||||
for (const order of this.filteredOrderList) {
|
||||
for (const order of this.getFilteredOrderList()) {
|
||||
const tipAmount = parse.float(order.uiState.TipScreen.state.inputTipAmount || '0');
|
||||
const serverId = this.env.pos.validated_orders_name_server_id_map[order.name];
|
||||
if (!serverId) {
|
||||
@@ -118,11 +108,14 @@ odoo.define('pos_restaurant.TicketScreen', function (require) {
|
||||
args: [serverId],
|
||||
});
|
||||
}
|
||||
getOrderStates() {
|
||||
return Object.assign(super.getOrderStates(), {
|
||||
Tipping: this.env._t('Tipping'),
|
||||
Open: this.env._t('Open'),
|
||||
});
|
||||
_getOrderStates() {
|
||||
const result = super._getOrderStates();
|
||||
if (this.env.pos.config.set_tip_after_payment) {
|
||||
result.delete('PAYMENT');
|
||||
result.set('OPEN', this.env._t('Open'));
|
||||
result.set('TIPPING', this.env._t('Tipping'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderList" t-inherit="point_of_sale.OrderList" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('order-row')]//div[hasclass('customer')]" position="after">
|
||||
<div t-if="env.pos.config.module_pos_restaurant" class="header table">
|
||||
Table
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderRow" t-inherit="point_of_sale.OrderRow" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('order-row')]//div[hasclass('customer')]" position="after">
|
||||
<div t-if="env.pos.config.module_pos_restaurant" class="item table">
|
||||
<t t-esc="table" />
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -4,18 +4,18 @@
|
||||
<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>
|
||||
<div t-if="filter === 'Tipping'" class="col end narrow" name="tip">Tip</div>
|
||||
<div t-if="_state.ui.filter == 'TIPPING'" class="col end narrow" name="tip">Tip</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>
|
||||
<div t-if="filter === 'Tipping'" class="col end narrow" name="tip">
|
||||
<div t-if="_state.ui.filter == 'TIPPING'" class="col end narrow" name="tip">
|
||||
<TipCell order="order" />
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('buttons')]" position="inside">
|
||||
<button class="settle-tips" t-if="filter === 'Tipping'" t-on-click="settleTips">Settle</button>
|
||||
<button class="settle-tips" t-if="_state.ui.filter == 'TIPPING'" t-on-click="settleTips">Settle</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
odoo.define('pos_restaurant.tour.OrderManagementScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
// This tour tests the integration of OrderManagementScreen
|
||||
// to some pos_restaurant features.
|
||||
|
||||
const { makeFullOrder } = require('point_of_sale.tour.CompositeTourMethods');
|
||||
const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods');
|
||||
const {
|
||||
OrderManagementScreen,
|
||||
} = 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');
|
||||
|
||||
// signal to start generating steps
|
||||
// when finished, steps can be taken from getSteps
|
||||
startSteps();
|
||||
|
||||
FloorScreen.do.clickTable('T2');
|
||||
|
||||
// make one order and check if it can be seen from the management screen.
|
||||
makeFullOrder({ orderlist: [['Minute Maid', '5', '6']], payment: ['Cash', '30'] });
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0001', total: '30' });
|
||||
|
||||
// go back to create multiple unpaid orders
|
||||
OrderManagementScreen.do.clickBack();
|
||||
|
||||
// create 2 unpaid orders on T2 (orders 0002 and 0003)
|
||||
FloorScreen.do.clickTable('T2');
|
||||
ProductScreen.exec.addMultiOrderlines(
|
||||
['Coca-Cola', '1', '2'],
|
||||
['Water', '3', '4'],
|
||||
['Minute Maid', '5', '6']
|
||||
);
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.clickNewTicket();
|
||||
ProductScreen.exec.addMultiOrderlines(['Coca-Cola', '1', '2'], ['Minute Maid', '5', '6']);
|
||||
Chrome.do.backToFloor();
|
||||
|
||||
// create 2 unpaid orders on T5 (orders 0004 and 0005)
|
||||
FloorScreen.do.clickTable('T5');
|
||||
ProductScreen.exec.addMultiOrderlines(
|
||||
['Coca-Cola', '7', '8'],
|
||||
['Water', '9', '10'],
|
||||
['Minute Maid', '11', '12']
|
||||
);
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.clickNewTicket();
|
||||
ProductScreen.exec.addMultiOrderlines(['Coca-Cola', '13', '14'], ['Minute Maid', '15', '16']);
|
||||
Chrome.do.backToFloor();
|
||||
|
||||
// check that the unpaid orders are listed in the management screen
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0002', total: '44.0' });
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0003', total: '32.0' });
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0004', total: '278.0' });
|
||||
OrderManagementScreen.check.orderlistHas({ orderName: '-0005', total: '422.0' });
|
||||
|
||||
// select order 0003 and check if it is really in T2
|
||||
OrderManagementScreen.do.clickOrder('-0003');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('32');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T2');
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
|
||||
// select order 0005 and check it it is really in T5
|
||||
OrderManagementScreen.check.isShown();
|
||||
OrderManagementScreen.do.clickOrder('-0005');
|
||||
ProductScreen.check.totalAmountIs('422');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T5');
|
||||
|
||||
// go back to floor screen and start order management from there
|
||||
Chrome.do.backToFloor();
|
||||
FloorScreen.check.selectedFloorIs('Main Floor');
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
|
||||
// select order 0002 and check if it is really in T2
|
||||
OrderManagementScreen.do.clickOrder('-0002');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('44');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T2');
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
|
||||
// select order 0004 and check if it is really in T5
|
||||
OrderManagementScreen.do.clickOrder('-0004');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('278');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T5');
|
||||
|
||||
// transfer order 0004 to T2
|
||||
ProductScreen.do.clickTransferButton();
|
||||
FloorScreen.do.clickTable('T2');
|
||||
ProductScreen.check.isShown();
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
|
||||
// select order 0004 and check if it is now in T2
|
||||
OrderManagementScreen.do.clickOrder('-0004', ['table', 'T2']);
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('278');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T2');
|
||||
|
||||
Chrome.do.clickOrderManagementButton();
|
||||
OrderManagementScreen.check.isShown();
|
||||
|
||||
// finally, select order 0002
|
||||
OrderManagementScreen.do.clickOrder('-0002');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('44');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T2');
|
||||
|
||||
Tour.register('PosResOrderManagementScreenTour', { test: true, url: '/pos/ui' }, getSteps());
|
||||
});
|
||||
@@ -45,6 +45,7 @@ odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) {
|
||||
// go back to the original order and see if the order is changed
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectOrder('-0001');
|
||||
ProductScreen.check.isShown()
|
||||
ProductScreen.do.clickOrderline('Water', '2.0')
|
||||
ProductScreen.do.clickOrderline('Minute Maid', '3.0')
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ odoo.define('pos_restaurant.tour.TicketScreen', function (require) {
|
||||
TicketScreen.do.deleteOrder('-0003');
|
||||
Chrome.do.confirmPopup();
|
||||
TicketScreen.do.selectOrder('-0002');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('2.0');
|
||||
Chrome.check.backToFloorTextIs('Main Floor', 'T2');
|
||||
Chrome.do.backToFloor();
|
||||
|
||||
@@ -85,6 +85,7 @@ odoo.define('pos_restaurant.tour.TipScreen', function (require) {
|
||||
|
||||
// finalize order 4 then tip custom amount
|
||||
TicketScreen.do.selectOrder('-0004');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.check.totalAmountIs('8.0');
|
||||
ProductScreen.do.clickPayButton();
|
||||
PaymentScreen.do.clickPaymentMethod('Bank');
|
||||
@@ -103,12 +104,13 @@ odoo.define('pos_restaurant.tour.TipScreen', function (require) {
|
||||
Chrome.do.clickTicketButton();
|
||||
TicketScreen.do.selectFilter('Tipping');
|
||||
TicketScreen.do.settleTips();
|
||||
TicketScreen.do.selectFilter('All Tickets');
|
||||
TicketScreen.do.selectFilter('Active Orders');
|
||||
TicketScreen.check.nthRowContains(2, 'Ongoing');
|
||||
|
||||
// tip order2 during payment
|
||||
// tip screen should not show after validating payment screen
|
||||
TicketScreen.do.selectOrder('-0002');
|
||||
ProductScreen.check.isShown();
|
||||
ProductScreen.do.clickPayButton();
|
||||
PaymentScreen.do.clickTipButton();
|
||||
NumberPopup.check.isShown();
|
||||
|
||||
@@ -174,11 +174,6 @@ class TestFrontend(odoo.tests.HttpCase):
|
||||
self.start_tour("/pos/ui?config_id=%d" % self.pos_config.id, 'ControlButtonsTour', login="admin")
|
||||
self.start_tour("/pos/ui?config_id=%d" % self.pos_config.id, 'FloorScreenTour', login="admin")
|
||||
|
||||
def test_03_order_management_integration(self):
|
||||
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/ui?config_id=%d" % self.pos_config.id, 'PosResOrderManagementScreenTour', login="admin")
|
||||
|
||||
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/ui?config_id=%d" % self.pos_config.id, 'PosResTicketScreenTour', login="admin")
|
||||
|
||||
Reference in New Issue
Block a user