summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/point_of_sale/__manifest__.py11
-rw-r--r--addons/point_of_sale/models/pos_config.py11
-rw-r--r--addons/point_of_sale/models/pos_order.py67
-rw-r--r--addons/point_of_sale/models/pos_payment.py16
-rw-r--r--addons/point_of_sale/static/src/css/pos.css179
-rw-r--r--addons/point_of_sale/static/src/js/Chrome.js34
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js36
-rw-r--r--addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js7
-rw-r--r--addons/point_of_sale/static/src/js/ControlButtonsMixin.js84
-rw-r--r--addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js66
-rw-r--r--addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js18
-rw-r--r--addons/point_of_sale/static/src/js/PosContext.js12
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js155
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js48
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js25
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js29
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js193
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js31
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js124
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js102
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js42
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js55
-rw-r--r--addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js23
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js4
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js69
-rw-r--r--addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js184
-rw-r--r--addons/point_of_sale/static/src/js/models.js36
-rw-r--r--addons/point_of_sale/static/src/js/printers.js2
-rw-r--r--addons/point_of_sale/static/src/js/utils.js32
-rw-r--r--addons/point_of_sale/static/src/xml/Chrome.xml1
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/OrderManagementButton.xml12
-rw-r--r--addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml8
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.xml12
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.xml12
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/MobileOrderManagementScreen.xml30
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml35
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderList.xml20
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementControlPanel.xml36
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml32
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml15
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml21
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml26
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ProductScreen/OrderSummary.xml2
-rw-r--r--addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml6
-rw-r--r--addons/point_of_sale/static/tests/tours/Chrome.tour.js8
-rw-r--r--addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js135
-rw-r--r--addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js2
-rw-r--r--addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js8
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js15
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js57
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js23
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js2
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js2
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js176
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js19
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js25
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js12
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js2
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/utils.js23
-rw-r--r--addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js1
-rw-r--r--addons/point_of_sale/tests/test_frontend.py5
-rw-r--r--addons/point_of_sale/views/pos_assets_common.xml16
-rw-r--r--addons/point_of_sale/views/pos_assets_index.xml4
-rw-r--r--addons/point_of_sale/views/pos_config_view.xml31
-rw-r--r--addons/pos_hr/static/tests/tours/PosHrTourMethods.js2
-rw-r--r--addons/pos_mercury/static/src/js/pos_mercury.js6
-rw-r--r--addons/pos_mercury/static/src/xml/OrderReceipt.xml2
-rw-r--r--addons/pos_restaurant/__manifest__.py3
-rw-r--r--addons/pos_restaurant/models/pos_order.py5
-rw-r--r--addons/pos_restaurant/static/src/js/Chrome.js5
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js1
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js29
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js17
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js21
-rw-r--r--addons/pos_restaurant/static/src/xml/Chrome.xml13
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml12
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml12
-rw-r--r--addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js4
-rw-r--r--addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js121
-rw-r--r--addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js6
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js2
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js5
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js7
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js7
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js7
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js2
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js2
-rw-r--r--addons/pos_restaurant/tests/test_frontend.py8
-rw-r--r--addons/pos_restaurant/views/pos_restaurant_templates.xml3
89 files changed, 2507 insertions, 324 deletions
diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py
index 798c8bd49e5..0b8ddfa4148 100644
--- a/addons/point_of_sale/__manifest__.py
+++ b/addons/point_of_sale/__manifest__.py
@@ -55,6 +55,16 @@
'static/src/xml/Screens/ClientListScreen/ClientDetails.xml',
'static/src/xml/Screens/ClientListScreen/ClientDetailsEdit.xml',
'static/src/xml/Screens/ClientListScreen/ClientListScreen.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/MobileOrderManagementScreen.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderManagementControlPanel.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderList.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderRow.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml',
'static/src/xml/Screens/PaymentScreen/PSNumpadInputButton.xml',
'static/src/xml/Screens/PaymentScreen/PaymentScreenNumpad.xml',
'static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml',
@@ -83,6 +93,7 @@
'static/src/xml/ChromeWidgets/CashierName.xml',
'static/src/xml/ChromeWidgets/ProxyStatus.xml',
'static/src/xml/ChromeWidgets/SyncNotification.xml',
+ 'static/src/xml/ChromeWidgets/OrderManagementButton.xml',
'static/src/xml/ChromeWidgets/HeaderButton.xml',
'static/src/xml/ChromeWidgets/SaleDetailsButton.xml',
'static/src/xml/SaleDetailsReport.xml',
diff --git a/addons/point_of_sale/models/pos_config.py b/addons/point_of_sale/models/pos_config.py
index 2c772499794..0820787f879 100644
--- a/addons/point_of_sale/models/pos_config.py
+++ b/addons/point_of_sale/models/pos_config.py
@@ -201,7 +201,7 @@ 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")
- module_pos_reprint = fields.Boolean(string="Reprint Receipt")
+ manage_orders = fields.Boolean(string="Manage Orders")
is_posbox = fields.Boolean("PosBox")
is_header_or_footer = fields.Boolean("Header & Footer")
module_pos_hr = fields.Boolean(help="Show employee login screen")
@@ -217,6 +217,7 @@ 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")
@api.depends('use_pricelist', 'available_pricelist_ids')
def _compute_allowed_pricelist_ids(self):
@@ -260,6 +261,14 @@ 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']
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index ccb05ff7d42..65961226330 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -177,6 +177,7 @@ class PosOrder(models.Model):
'amount': -pos_order['amount_return'],
'payment_date': fields.Date.context_today(self),
'payment_method_id': cash_payment_method.id,
+ 'is_change': True,
}
order.add_payment(return_payment_vals)
@@ -253,7 +254,6 @@ class PosOrder(models.Model):
to_invoice = fields.Boolean('To invoice')
is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced')
-
@api.depends('account_move')
def _compute_is_invoiced(self):
for order in self:
@@ -572,6 +572,48 @@ class PosOrder(models.Model):
orders.sudo().unlink()
return orders.ids
+ @api.model
+ def search_paid_order_ids(self, config_id, domain, limit, offset):
+ """Search for 'paid' orders that satisfy the given domain, limit and offset."""
+ default_domain = ['&', ('config_id', '=', config_id), '!', '|', ('state', '=', 'draft'), ('state', '=', 'cancelled')]
+ real_domain = AND([domain, default_domain])
+ ids = self.search(AND([domain, default_domain]), limit=limit, offset=offset).ids
+ totalCount = self.search_count(real_domain)
+ return {'ids': ids, 'totalCount': totalCount}
+
+ def _export_for_ui(self, order):
+ timezone = pytz.timezone(self._context.get('tz') or self.env.user.tz or 'UTC')
+ return {
+ 'lines': [[0, 0, line] for line in order.lines.export_for_ui()],
+ 'statement_ids': [[0, 0, payment] for payment in order.payment_ids.export_for_ui()],
+ 'name': order.pos_reference,
+ 'uid': order.pos_reference[6:],
+ 'amount_paid': order.amount_paid,
+ 'amount_total': order.amount_total,
+ 'amount_tax': order.amount_tax,
+ 'amount_return': order.amount_return,
+ 'pos_session_id': order.session_id.id,
+ 'is_session_closed': order.session_id.state == 'closed',
+ 'pricelist_id': order.pricelist_id.id,
+ 'partner_id': order.partner_id.id,
+ 'user_id': order.user_id.id,
+ 'sequence_number': order.sequence_number,
+ 'creation_date': order.date_order.astimezone(timezone),
+ 'fiscal_position_id': order.fiscal_position_id.id,
+ 'to_invoice': order.to_invoice,
+ 'state': order.state,
+ 'account_move': order.account_move.id,
+ 'id': order.id,
+ }
+
+ def export_for_ui(self):
+ """ Returns a list of dict with each item having similar signature as the return of
+ `export_as_JSON` of models.Order. This is useful for back-and-forth communication
+ between the pos frontend and backend.
+ """
+ return self.mapped(self._export_for_ui) if self else []
+
+
class PosOrderLine(models.Model):
_name = "pos.order.line"
_description = "Point of Sale Order Lines"
@@ -702,6 +744,22 @@ class PosOrderLine(models.Model):
for line in self:
line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids, line.product_id, line.order_id.partner_id)
+ def _export_for_ui(self, orderline):
+ return {
+ 'qty': orderline.qty,
+ 'price_unit': orderline.price_unit,
+ 'price_subtotal': orderline.price_subtotal,
+ 'price_subtotal_incl': orderline.price_subtotal_incl,
+ 'product_id': orderline.product_id.id,
+ 'discount': orderline.discount,
+ 'tax_ids': [[6, False, orderline.tax_ids.mapped(lambda tax: tax.id)]],
+ 'id': orderline.id,
+ 'pack_lot_ids': [[0, 0, lot] for lot in orderline.pack_lot_ids.export_for_ui()],
+ }
+
+ def export_for_ui(self):
+ return self.mapped(self._export_for_ui) if self else []
+
class PosOrderLineLot(models.Model):
_name = "pos.pack.operation.lot"
@@ -713,6 +771,13 @@ class PosOrderLineLot(models.Model):
lot_name = fields.Char('Lot Name')
product_id = fields.Many2one('product.product', related='pos_order_line_id.product_id', readonly=False)
+ def _export_for_ui(self, lot):
+ return {
+ 'lot_name': lot.lot_name,
+ }
+
+ def export_for_ui(self):
+ return self.mapped(self._export_for_ui) if self else []
class ReportSaleDetails(models.AbstractModel):
diff --git a/addons/point_of_sale/models/pos_payment.py b/addons/point_of_sale/models/pos_payment.py
index 8dea1f62b92..f5f21f1c12b 100644
--- a/addons/point_of_sale/models/pos_payment.py
+++ b/addons/point_of_sale/models/pos_payment.py
@@ -27,6 +27,8 @@ class PosPayment(models.Model):
card_type = fields.Char('Type of card used')
transaction_id = fields.Char('Payment Transaction ID')
payment_status = fields.Char('Payment Status')
+ ticket = fields.Char('Payment Receipt Info')
+ is_change = fields.Boolean(string='Is this payment change?', default=False)
@api.model
def name_get(self):
@@ -37,3 +39,17 @@ class PosPayment(models.Model):
else:
res.append((payment.id, formatLang(self.env, payment.amount, currency_obj=payment.currency_id)))
return res
+
+ def _export_for_ui(self, payment):
+ return {
+ 'payment_method_id': payment.payment_method_id.id,
+ 'amount': payment.amount,
+ 'payment_status': payment.payment_status,
+ 'card_type': payment.card_type,
+ 'transaction_id': payment.transaction_id,
+ 'ticket': payment.ticket,
+ 'is_change': payment.is_change,
+ }
+
+ def export_for_ui(self):
+ return self.mapped(self._export_for_ui) if self else []
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index b55060fd140..7b393bafcd6 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -2838,3 +2838,182 @@ td {
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
+
+
+/* Order Management */
+
+.order-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ background-color: rgb(230,230,230);
+}
+
+.order-list .order-row:hover {
+ color: white;
+ background-color: rgb(110,200,155);
+ font-weight: bold;
+}
+
+.order-row.highlight {
+ color: white;
+ background-color: rgb(110,200,155);
+ font-weight: bold;
+}
+
+.order-row.lighter {
+ background-color: #f5f5f5;
+ cursor: pointer;
+}
+
+.order-row .header {
+ font-size: medium;
+ font-weight: bolder;
+ flex-grow: 1;
+ flex-basis: 0;
+ text-align: left;
+ padding: 10px 10px;
+ background-color: #cecece;
+ border-bottom: solid 1px;
+ border-top: solid 1px
+}
+
+.order-row .header.total {
+ text-align: right;
+}
+
+.order-row .item {
+ font-size: medium;
+ flex-grow: 1;
+ flex-basis: 0;
+ text-align: left;
+ padding: 10px 10px;
+ border-bottom: solid rgb(150,150,150) 1px;
+}
+
+.order-row .item.total {
+ 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 {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: x-large;
+}
+
+.order-management-screen .control-panel .page-controls > div {
+ border: darkgray solid 1px;
+ border-radius: 2px;
+}
+
+.order-management-screen .control-panel .page-controls > div:hover {
+ color: rgb(110,200,155);
+}
+
+.order-management-screen .control-panel .page-controls .previous {
+ margin-right: 0.2rem;
+}
+
+.order-management-screen .control-panel .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;
+ text-align: center;
+}
+
+.order-receipt .title {
+ font-size: large;
+}
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
index d54b5f22170..7f950cfab71 100644
--- a/addons/point_of_sale/static/src/js/Chrome.js
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -1,7 +1,7 @@
odoo.define('point_of_sale.Chrome', function(require) {
'use strict';
- const { useState, useRef } = owl.hooks;
+ const { useState, useRef, useContext } = owl.hooks;
const { debounce } = owl.utils;
const { loadCSS } = require('web.ajax');
const { useListener } = require('web.custom_hooks');
@@ -11,6 +11,8 @@ odoo.define('point_of_sale.Chrome', function(require) {
const NumberBuffer = require('point_of_sale.NumberBuffer');
const PopupControllerMixin = require('point_of_sale.PopupControllerMixin');
const Registries = require('point_of_sale.Registries');
+ const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
+ const contexts = require('point_of_sale.PosContext');
// This is kind of a trick.
// We get a reference to the whole exports so that
@@ -34,6 +36,8 @@ odoo.define('point_of_sale.Chrome', function(require) {
useListener('set-sync-status', this._onSetSyncStatus);
NumberBuffer.activate();
+ this.chromeContext = useContext(contexts.chrome);
+
this.state = useState({
uiState: 'LOADING', // 'LOADING' | 'READY' | 'CLOSING'
debugWidgetIsShown: true,
@@ -138,10 +142,13 @@ odoo.define('point_of_sale.Chrome', function(require) {
if (_.isEmpty(this.env.pos.db.product_by_category_id)) {
this._loadDemoData();
}
- this.env.pos.push_orders(); // push order in the background, no need to await
- // Allow using the app even if not all the images are loaded.
- // Basically, preload the images in the background.
- setTimeout(() => this._preloadImages());
+ setTimeout(() => {
+ // push order in the background, no need to await
+ this.env.pos.push_orders();
+ // Allow using the app even if not all the images are loaded.
+ // Basically, preload the images in the background.
+ this._preloadImages();
+ });
} catch (error) {
let title = 'Unknown Error',
body;
@@ -202,14 +209,21 @@ odoo.define('point_of_sale.Chrome', function(require) {
__closeTempScreen() {
this.tempScreen.isShown = false;
}
- __showScreen({ detail: { name, props } }) {
+ __showScreen({ detail: { name, props = {} } }) {
+ const component = this.constructor.components[name];
// 1. Set the information of the screen to display.
this.mainScreen.name = name;
- this.mainScreen.component = this.constructor.components[name];
- this.mainScreenProps = props || {};
- // 2. Save the screen to the order.
+ this.mainScreen.component = component;
+ this.mainScreenProps = props;
+
+ // 2. Set some options
+ this.chromeContext.showOrderSelector = !component.hideOrderSelector;
+
+ // 3. Save the screen to the order.
// - This screen is shown when the order is selected.
- this._setScreenData(name, props);
+ if (!(component.prototype instanceof IndependentToOrderScreen)) {
+ this._setScreenData(name, props);
+ }
}
/**
* Set the latest screen to the current order. This is done so that
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js
new file mode 100644
index 00000000000..0bee888052d
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js
@@ -0,0 +1,36 @@
+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;
+});
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
index 1eed8ab4010..ad2d8265417 100644
--- a/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
+++ b/addons/point_of_sale/static/src/js/ChromeWidgets/OrderSelector.js
@@ -1,11 +1,18 @@
odoo.define('point_of_sale.OrderSelector', function(require) {
'use strict';
+ const { useContext } = owl.hooks;
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
+ const contexts = require('point_of_sale.PosContext');
// Previously OrderSelectorWidget
class OrderSelector extends PosComponent {
+ constructor() {
+ super(...arguments);
+ // this is used in its template
+ this.chromeContext = useContext(contexts.chrome);
+ }
mounted() {
this.env.pos.get('orders').on('add remove change', () => this.render(), this);
this.env.pos.on('change:selectedOrder', () => this.render(), this);
diff --git a/addons/point_of_sale/static/src/js/ControlButtonsMixin.js b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js
new file mode 100644
index 00000000000..02b4c3673e6
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ControlButtonsMixin.js
@@ -0,0 +1,84 @@
+odoo.define('point_of_sale.ControlButtonsMixin', function (require) {
+ 'use strict';
+
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * Component that has this mixin allows the use of `addControlButton`.
+ * All added control buttons that satisfies the condition can be accessed
+ * thru the `controlButtons` field of the Component's instance. These
+ * control buttons can then be rendered in the Component.
+ * @param {Function} x superclass
+ */
+ const ControlButtonsMixin = (x) => {
+ class Extended extends x {
+ get controlButtons() {
+ return this.constructor.controlButtons
+ .filter((cb) => {
+ return cb.condition.bind(this)();
+ })
+ .map((cb) =>
+ Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
+ );
+ }
+ }
+ Extended.controlButtons = [];
+ /**
+ * @param {Object} controlButton
+ * @param {Function} controlButton.component
+ * Base class that is added in the Registries.Component.
+ * @param {Function} controlButton.condition zero argument function that is bound
+ * to the instance of ProductScreen, such that `this.env.pos` can be used
+ * inside the function.
+ * @param {Array} [controlButton.position] array of two elements
+ * [locator, relativeTo]
+ * locator: string -> any of ('before', 'after', 'replace')
+ * relativeTo: string -> other controlButtons component name
+ */
+ Extended.addControlButton = function (controlButton) {
+ // We set the name first.
+ if (!controlButton.name) {
+ controlButton.name = controlButton.component.name;
+ }
+
+ // If no position is set, we just push it to the array.
+ if (!controlButton.position) {
+ this.controlButtons.push(controlButton);
+ } else {
+ // Find where to put the new controlButton.
+ const [locator, relativeTo] = controlButton.position;
+ let whereIndex = -1;
+ for (let i = 0; i < this.controlButtons.length; i++) {
+ if (this.controlButtons[i].name === relativeTo) {
+ if (['before', 'replace'].includes(locator)) {
+ whereIndex = i;
+ } else if (locator === 'after') {
+ whereIndex = i + 1;
+ }
+ break;
+ }
+ }
+
+ // If found where to put, then perform the necessary mutation of
+ // the buttons array.
+ // Else, we just push this controlButton to the array.
+ if (whereIndex > -1) {
+ this.controlButtons.splice(
+ whereIndex,
+ locator === 'replace' ? 1 : 0,
+ controlButton
+ );
+ } else {
+ let warningMessage =
+ `'${controlButton.name}' has invalid 'position' ([${locator}, ${relativeTo}]).` +
+ 'It is pushed to the controlButtons stack instead.';
+ console.warn(warningMessage);
+ this.controlButtons.push(controlButton);
+ }
+ }
+ };
+ return Extended;
+ };
+
+ return ControlButtonsMixin;
+});
diff --git a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js
new file mode 100644
index 00000000000..6abf4b2e93d
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js
@@ -0,0 +1,66 @@
+odoo.define('point_of_sale.AbstractReceiptScreen', function (require) {
+ 'use strict';
+
+ const { useRef } = owl.hooks;
+ const { nextFrame } = require('point_of_sale.utils');
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * This relies on the assumption that there is a reference to
+ * `order-receipt` so it is important to declare a `t-ref` to
+ * `order-receipt` in the template of the Component that extends
+ * this abstract component.
+ */
+ class AbstractReceiptScreen extends PosComponent {
+ constructor() {
+ super(...arguments);
+ this.orderReceipt = useRef('order-receipt');
+ }
+ async _printReceipt() {
+ if (this.env.pos.proxy.printer) {
+ const printResult = await this.env.pos.proxy.printer.print_receipt(this.orderReceipt.el.outerHTML);
+ if (printResult.successful) {
+ return true;
+ } else {
+ const { confirmed } = await this.showPopup('ConfirmPopup', {
+ title: printResult.message.title,
+ body: 'Do you want to print using the web printer?',
+ });
+ if (confirmed) {
+ // We want to call the _printWeb when the popup is fully gone
+ // from the screen which happens after the next animation frame.
+ await nextFrame();
+ return await this._printWeb();
+ }
+ return false;
+ }
+ } else {
+ return await this._printWeb();
+ }
+ }
+ /**
+ * https://stackoverflow.com/questions/21285902/printing-a-part-of-webpage-with-javascript
+ */
+ async _printWeb() {
+ try {
+ const isPrinted = document.execCommand('print', false, null);
+ if (!isPrinted) window.print();
+ return true;
+ } catch (err) {
+ await this.showPopup('ErrorPopup', {
+ title: this.env._t('Printing is not supported on some browsers'),
+ body: this.env._t(
+ 'Printing is not supported on some browsers due to no default printing protocol ' +
+ 'is available. It is possible to print your tickets by making use of an IoT Box.'
+ ),
+ });
+ return false;
+ }
+ }
+ }
+
+ Registries.Component.add(AbstractReceiptScreen);
+
+ return AbstractReceiptScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
new file mode 100644
index 00000000000..939f0daa43f
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js
@@ -0,0 +1,18 @@
+odoo.define('point_of_sale.IndependentToOrderScreen', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+
+ class IndependentToOrderScreen extends PosComponent {
+ close() {
+ // To close this order-indenpendent screen, we forcefully trigger change
+ // on the selectedOrder attribute, which then shows the screen of the
+ // current order.
+ this.env.pos.trigger('change:selectedOrder', this.env.pos, this.env.pos.get_order(), {
+ silent: true,
+ });
+ }
+ }
+
+ return IndependentToOrderScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/PosContext.js b/addons/point_of_sale/static/src/js/PosContext.js
new file mode 100644
index 00000000000..b52a155d3cc
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/PosContext.js
@@ -0,0 +1,12 @@
+odoo.define('point_of_sale.PosContext', function (require) {
+ 'use strict';
+
+ const { Context } = owl;
+
+ // 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 }),
+ };
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js
new file mode 100644
index 00000000000..53b858ba492
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js
@@ -0,0 +1,155 @@
+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);
+ }
+ get commandName() {
+ if (!this.selectedOrder) {
+ return 'Invoice';
+ } else {
+ return this.isAlreadyInvoiced
+ ? 'Reprint Invoice'
+ : this.selectedOrder.isFromClosedSession
+ ? 'Cannot Invoice'
+ : 'Invoice';
+ }
+ }
+ get isHighlighted() {
+ return this.selectedOrder && !this.isAlreadyInvoiced && !this.selectedOrder.isFromClosedSession;
+ }
+ async _downloadInvoice(orderId) {
+ try {
+ await this.env.pos.do_action('point_of_sale.pos_invoice_report', {
+ additional_context: {
+ active_ids: [orderId],
+ },
+ });
+ } catch (error) {
+ if (error instanceof Error) {
+ throw error;
+ } else {
+ // NOTE: error here is most probably undefined
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Network Error'),
+ body: this.env._t('Unable to download invoice.'),
+ });
+ }
+ }
+ }
+ async _invoiceOrder() {
+ const order = this.selectedOrder;
+ if (!order) return;
+
+ const orderId = order.backendId;
+
+ // Part 0.1. If already invoiced, print the invoice.
+ if (this.isAlreadyInvoiced) {
+ await this._downloadInvoice(orderId);
+ return;
+ }
+
+ // Part 0.2. Check if order belongs to an active session.
+ // If not, do not allow invoicing.
+ if (order.isFromClosedSession) {
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Session is closed'),
+ body: this.env._t('Cannot invoice order from closed session.'),
+ });
+ return;
+ }
+
+ // Part 1: Handle missing client.
+ // 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?',
+ });
+ if (!confirmedPopup) return;
+
+ const { confirmed: confirmedTempScreen, payload: newClient } = await this.showTempScreen(
+ 'ClientListScreen'
+ );
+ if (!confirmedTempScreen) return;
+
+ await this.rpc({
+ model: 'pos.order',
+ method: 'write',
+ args: [[orderId], { partner_id: newClient.id }],
+ kwargs: { context: this.env.session.user_context },
+ });
+ }
+
+ // Part 2: Invoice the order.
+ await this.rpc(
+ {
+ model: 'pos.order',
+ method: 'action_pos_order_invoice',
+ args: [orderId],
+ kwargs: { context: this.env.session.user_context },
+ },
+ {
+ timeout: 30000,
+ shadow: true,
+ }
+ );
+
+ // 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);
+ }
+ async _onClick() {
+ try {
+ await this._invoiceOrder();
+ } catch (error) {
+ if (isRpcError(error) && error.message.code < 0) {
+ this.showPopup('ErrorPopup', {
+ title: this.env._t('Network Error'),
+ body: this.env._t('Unable to invoice order.'),
+ });
+ } else {
+ throw error;
+ }
+ }
+ }
+ }
+ InvoiceButton.template = 'InvoiceButton';
+
+ OrderManagementScreen.addControlButton({
+ component: InvoiceButton,
+ condition: function () {
+ return this.env.pos.config.module_account;
+ },
+ });
+
+ Registries.Component.add(InvoiceButton);
+
+ return InvoiceButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
new file mode 100644
index 00000000000..913f98af4e6
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
@@ -0,0 +1,48 @@
+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 OrderReceipt = require('point_of_sale.OrderReceipt');
+ 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;
+
+ if (this.env.pos.proxy.printer) {
+ const fixture = document.createElement('div');
+ const orderReceipt = new (Registries.Component.get(OrderReceipt))(this, { order });
+ await orderReceipt.mount(fixture);
+ const receiptHtml = orderReceipt.el.outerHTML;
+ const printResult = await this.env.pos.proxy.printer.print_receipt(receiptHtml);
+ if (!printResult.successful) {
+ this.showTempScreen('ReprintReceiptScreen', { order: order });
+ }
+ } else {
+ this.showTempScreen('ReprintReceiptScreen', { order: order });
+ }
+ }
+ }
+ ReprintReceiptButton.template = 'ReprintReceiptButton';
+
+ OrderManagementScreen.addControlButton({
+ component: ReprintReceiptButton,
+ condition: function () {
+ return true;
+ },
+ });
+
+ Registries.Component.add(ReprintReceiptButton);
+
+ return ReprintReceiptButton;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js
new file mode 100644
index 00000000000..b5766ccf486
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js
@@ -0,0 +1,25 @@
+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;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js
new file mode 100644
index 00000000000..cc0c671c31a
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js
@@ -0,0 +1,29 @@
+odoo.define('point_of_sale.OrderDetails', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+
+ /**
+ * @props {models.Order} order
+ */
+ class OrderDetails extends PosComponent {
+ get order() {
+ return this.props.order;
+ }
+ get orderlines() {
+ return this.order ? this.order.orderlines.models : [];
+ }
+ get total() {
+ return this.env.pos.format_currency(this.order ? this.order.get_total_with_tax() : 0);
+ }
+ get tax() {
+ return this.env.pos.format_currency(this.order ? this.order.get_total_tax() : 0)
+ }
+ }
+ OrderDetails.template = 'OrderDetails';
+
+ Registries.Component.add(OrderDetails);
+
+ return OrderDetails;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js
new file mode 100644
index 00000000000..3490a46378a
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js
@@ -0,0 +1,193 @@
+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 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();
+ }
+ }
+ /**
+ * NOTE: If there is searchDomain, do not show the active orders.
+ * Unless we want the searchDomain to be applicable to the active orders
+ * as well. Don't we?
+ *
+ * @param {integer|undefined} id id of the cached order
+ * @returns {Array<models.Order>}
+ */
+ get(id) {
+ if (id) return this.cache[id];
+ if (this.searchDomain) {
+ return this.ordersToShow.filter((order) => order.locked);
+ } else {
+ return this.ordersToShow;
+ }
+ }
+ setSearchDomain(searchDomain) {
+ this.searchDomain = searchDomain;
+ }
+ setComponent(comp) {
+ this.comp = comp;
+ this.activeOrders = this.comp.env.pos.get('orders').models;
+ 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();
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js
new file mode 100644
index 00000000000..2b4d3cd9654
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js
@@ -0,0 +1,31 @@
+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;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js
new file mode 100644
index 00000000000..951a09561af
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js
@@ -0,0 +1,124 @@
+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;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
new file mode 100644
index 00000000000..f4b80c12190
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -0,0 +1,102 @@
+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) {
+ if (order === this.env.pos.get_order()) {
+ this.close();
+ } else {
+ this.env.pos.set_order(order);
+ }
+ }
+ }
+ OrderManagementScreen.template = 'OrderManagementScreen';
+ OrderManagementScreen.hideOrderSelector = true;
+
+ Registries.Component.add(OrderManagementScreen);
+
+ return OrderManagementScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js
new file mode 100644
index 00000000000..959ea5a1477
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js
@@ -0,0 +1,42 @@
+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;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js
new file mode 100644
index 00000000000..78a023a2564
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js
@@ -0,0 +1,55 @@
+odoo.define('point_of_sale.OrderlineDetails', function (require) {
+ 'use strict';
+
+ const PosComponent = require('point_of_sale.PosComponent');
+ const Registries = require('point_of_sale.Registries');
+ const { format } = require('web.field_utils');
+ const { round_precision: round_pr } = require('web.utils');
+
+ /**
+ * @props {pos.order.line} line
+ */
+ class OrderlineDetails extends PosComponent {
+ get line() {
+ const line = this.props.line;
+ const formatQty = (line) => {
+ const quantity = line.get_quantity();
+ const unit = line.get_unit();
+ const decimals = this.env.pos.dp['Product Unit of Measure'];
+ const rounding = Math.max(unit.rounding, Math.pow(10, -decimals));
+ const roundedQuantity = round_pr(quantity, rounding);
+ return format.float(roundedQuantity, { digits: [69, decimals] });
+ };
+ return {
+ productName: line.get_product().display_name,
+ totalPrice: line.get_price_with_tax(),
+ quantity: formatQty(line),
+ unit: line.get_unit().name,
+ unitPrice: line.get_unit_price(),
+ };
+ }
+ get productName() {
+ return this.line.productName;
+ }
+ get totalPrice() {
+ return this.env.pos.format_currency(this.line.totalPrice);
+ }
+ get quantity() {
+ return this.line.quantity;
+ }
+ get unitPrice() {
+ return this.line.unitPrice;
+ }
+ get unit() {
+ return this.line.unit;
+ }
+ get pricePerUnit() {
+ return ` ${this.unit} at ${this.unitPrice} / ${this.unit}`;
+ }
+ }
+ OrderlineDetails.template = 'OrderlineDetails';
+
+ Registries.Component.add(OrderlineDetails);
+
+ return OrderlineDetails;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
new file mode 100644
index 00000000000..ef5d396f616
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
@@ -0,0 +1,23 @@
+odoo.define('point_of_sale.ReprintReceiptScreen', function (require) {
+ 'use strict';
+
+ const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen');
+ const Registries = require('point_of_sale.Registries');
+
+ const ReprintReceiptScreen = (AbstractReceiptScreen) => {
+ class ReprintReceiptScreen extends AbstractReceiptScreen {
+ confirm() {
+ this.props.resolve({ confirmed: true, payload: null });
+ this.trigger('close-temp-screen');
+ }
+ tryReprint() {
+ this._printReceipt();
+ }
+ }
+ ReprintReceiptScreen.template = 'ReprintReceiptScreen';
+ return ReprintReceiptScreen;
+ };
+ Registries.Component.addByExtending(ReprintReceiptScreen, AbstractReceiptScreen);
+
+ return ReprintReceiptScreen;
+});
diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js
index 67341d2c84f..d30fe85e956 100644
--- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js
+++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ActionpadWidget.js
@@ -6,8 +6,8 @@ odoo.define('point_of_sale.ActionpadWidget', function(require) {
/**
* @props client
- * @trigger click-customer
- * @trigger click-pay
+ * @emits click-customer
+ * @emits click-pay
*/
class ActionpadWidget extends PosComponent {
get isLongName() {
diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js
index 5658b5b1493..69741a8a090 100644
--- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js
@@ -2,12 +2,13 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
+ const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
const NumberBuffer = require('point_of_sale.NumberBuffer');
const { useListener } = require('web.custom_hooks');
const Registries = require('point_of_sale.Registries');
const { onChangeOrder, useBarcodeReader } = require('point_of_sale.custom_hooks');
- class ProductScreen extends PosComponent {
+ class ProductScreen extends ControlButtonsMixin(PosComponent) {
constructor() {
super(...arguments);
useListener('update-selected-orderline', this._updateSelectedOrderline);
@@ -53,15 +54,6 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
get currentOrder() {
return this.env.pos.get_order();
}
- get controlButtons() {
- return ProductScreen.controlButtons
- .filter((cb) => {
- return cb.condition.bind(this)();
- })
- .map((cb) =>
- Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
- );
- }
async _clickProduct(event) {
if (!this.currentOrder) {
this.env.pos.add_new_order();
@@ -281,63 +273,6 @@ odoo.define('point_of_sale.ProductScreen', function(require) {
}
ProductScreen.template = 'ProductScreen';
- ProductScreen.controlButtons = [];
-
- /**
- * @param {Object} controlButton
- * @param {Function} controlButton.component
- * Can be any base class or base class callback that is added in the Registries.Component.
- * @param {Function} controlButton.condition zero argument function that is bound
- * to the instance of ProductScreen, such that `this.env.pos` can be used
- * inside the function.
- * @param {Array} [controlButton.position] array of two elements
- * [locator, relativeTo]
- * locator: string -> any of ('before', 'after', 'replace')
- * relativeTo: string -> other controlButtons component name
- */
- ProductScreen.addControlButton = function(controlButton) {
- // We set the name first.
- if (!controlButton.name) {
- controlButton.name = controlButton.component.name;
- }
-
- // If no position is set, we just push it to the array.
- if (!controlButton.position) {
- this.controlButtons.push(controlButton);
- } else {
- // Find where to put the new controlButton.
- const [locator, relativeTo] = controlButton.position;
- let whereIndex = -1;
- for (let i = 0; i < this.controlButtons.length; i++) {
- if (this.controlButtons[i].name === relativeTo) {
- if (['before', 'replace'].includes(locator)) {
- whereIndex = i;
- } else if (locator === 'after') {
- whereIndex = i + 1;
- }
- break;
- }
- }
-
- // If found where to put, then perform the necessary mutation of
- // the buttons array.
- // Else, we just push this controlButton to the array.
- if (whereIndex > -1) {
- this.controlButtons.splice(
- whereIndex,
- locator === 'replace' ? 1 : 0,
- controlButton
- );
- } else {
- let warningMessage =
- `'${controlButton.name}' has invalid 'position' ([${locator}, ${relativeTo}]).` +
- 'It is pushed to the controlButtons stack instead.';
- console.warn(warningMessage);
- this.controlButtons.push(controlButton);
- }
- }
- };
-
Registries.Component.add(ProductScreen);
return ProductScreen;
diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
index 8faa786c661..a5c9adfbc5d 100644
--- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
@@ -1,132 +1,92 @@
-odoo.define('point_of_sale.ReceiptScreen', function(require) {
+odoo.define('point_of_sale.ReceiptScreen', function (require) {
'use strict';
- const core = require('web.core');
- const { useRef, useState } = owl.hooks;
- const PosComponent = require('point_of_sale.PosComponent');
+ const { useState } = owl.hooks;
const { useErrorHandlers, onChangeOrder } = require('point_of_sale.custom_hooks');
const Registries = require('point_of_sale.Registries');
+ const AbstractReceiptScreen = require('point_of_sale.AbstractReceiptScreen');
- const _t = core._t;
-
- class ReceiptScreen extends PosComponent {
- /**
- * Optional props:
- * printInvoiceIsShown: Boolean
- */
- constructor() {
- super(...arguments);
- useErrorHandlers();
- onChangeOrder(null, (newOrder) => newOrder && this.render());
- this.state = useState({ printInvoiceIsShown: this.props.printInvoiceIsShown });
- this.orderReceipt = useRef('order-receipt');
- }
- mounted() {
- // Here, we send a task to the event loop that handles
- // the printing of the receipt when the component is mounted.
- // We are doing this because we want the receipt screen to be
- // displayed regardless of what happen to the handleAutoPrint
- // call.
- setTimeout(async () => await this.handleAutoPrint(), 0);
- }
- get change() {
- return this.env.pos.format_currency(this.currentOrder.get_change());
- }
- get currentOrder() {
- return this.env.pos.get_order();
- }
- get nextScreen() {
- return { name: 'ProductScreen' };
- }
- /**
- * This function is called outside the rendering call stack. This way,
- * we don't block the displaying of ReceiptScreen when it is mounted; additionally,
- * any error that can happen during the printing does not affect the rendering.
- */
- async handleAutoPrint() {
- if (this._shouldAutoPrint() && !this.currentOrder.is_to_email()) {
- await this.printReceipt();
- if (this.currentOrder._printed && this._shouldCloseImmediately()) {
- this.orderDone();
- }
+ const ReceiptScreen = (AbstractReceiptScreen) => {
+ class ReceiptScreen extends AbstractReceiptScreen {
+ /**
+ * Optional props:
+ * printInvoiceIsShown: Boolean
+ */
+ constructor() {
+ super(...arguments);
+ useErrorHandlers();
+ onChangeOrder(null, (newOrder) => newOrder && this.render());
+ this.state = useState({ printInvoiceIsShown: this.props.printInvoiceIsShown });
}
- }
- orderDone() {
- this.currentOrder.finalize();
- const { name, props } = this.nextScreen;
- this.showScreen(name, props);
- }
- async printReceipt() {
- if (this.env.pos.proxy.printer) {
- const printResult = await this.env.pos.proxy.printer.print_receipt(
- this.orderReceipt.el.outerHTML
- );
- if (printResult.successful) {
- this.currentOrder._printed = true;
- } else {
- await this.showPopup('ErrorPopup', {
- title: printResult.message.title,
- body: printResult.message.body,
- });
+ mounted() {
+ // Here, we send a task to the event loop that handles
+ // the printing of the receipt when the component is mounted.
+ // We are doing this because we want the receipt screen to be
+ // displayed regardless of what happen to the handleAutoPrint
+ // call.
+ setTimeout(async () => await this.handleAutoPrint(), 0);
+ }
+ get change() {
+ return this.env.pos.format_currency(this.currentOrder.get_change());
+ }
+ get currentOrder() {
+ return this.env.pos.get_order();
+ }
+ get nextScreen() {
+ return { name: 'ProductScreen' };
+ }
+ /**
+ * This function is called outside the rendering call stack. This way,
+ * we don't block the displaying of ReceiptScreen when it is mounted; additionally,
+ * any error that can happen during the printing does not affect the rendering.
+ */
+ async handleAutoPrint() {
+ if (this._shouldAutoPrint() && !this.currentOrder.is_to_email()) {
+ await this.printReceipt();
+ if (this.currentOrder._printed && this._shouldCloseImmediately()) {
+ this.orderDone();
+ }
}
- } else {
- await this._printWeb();
}
- }
- async onPrintInvoice() {
- // The button element of this event handler appears when the order
- // failed to sync and is to be invoiced. What we do here is to try
- // to sync again to eventually print the invoice.
- try {
- await this.env.pos.push_and_invoice_order(this.currentOrder);
- this.state.printInvoiceIsShown = false;
- } catch (error) {
- if (error instanceof Error) {
- throw error;
- } else {
- await this._handlePushOrderError(error);
+ orderDone() {
+ this.currentOrder.finalize();
+ const { name, props } = this.nextScreen;
+ this.showScreen(name, props);
+ }
+ async printReceipt() {
+ const isPrinted = await this._printReceipt();
+ if (isPrinted) {
+ this.currentOrder._printed = true;
}
}
- }
- _shouldAutoPrint() {
- return this.env.pos.config.iface_print_auto && !this.currentOrder._printed;
- }
- _shouldCloseImmediately() {
- var invoiced_finalized = this.currentOrder.is_to_invoice()
- ? this.currentOrder.finalized
- : true;
- return (
- this.env.pos.proxy.printer &&
- this.env.pos.config.iface_print_skip_screen &&
- invoiced_finalized
- );
- }
- async _printWeb() {
- if ($.browser.safari) {
- document.execCommand('print', false, null);
- } else {
+ async onPrintInvoice() {
+ // The button element of this event handler appears when the order
+ // failed to sync and is to be invoiced. What we do here is to try
+ // to sync again to eventually print the invoice.
try {
- window.print();
- this.currentOrder._printed = true;
- } catch (err) {
- if (navigator.userAgent.toLowerCase().indexOf('android') > -1) {
- await this.showPopup('ErrorPopup', {
- title: _t('Printing is not supported on some android browsers'),
- body: _t(
- 'Printing is not supported on some android browsers due to no default printing protocol ' +
- 'is available. It is possible to print your tickets by making use of an IoT Box.'
- ),
- });
+ await this.env.pos.push_and_invoice_order(this.currentOrder);
+ this.state.printInvoiceIsShown = false;
+ } catch (error) {
+ if (error instanceof Error) {
+ throw error;
} else {
- throw err;
+ await this._handlePushOrderError(error);
}
}
}
+ _shouldAutoPrint() {
+ return this.env.pos.config.iface_print_auto && !this.currentOrder._printed;
+ }
+ _shouldCloseImmediately() {
+ var invoiced_finalized = this.currentOrder.is_to_invoice() ? this.currentOrder.finalized : true;
+ return this.env.pos.proxy.printer && this.env.pos.config.iface_print_skip_screen && invoiced_finalized;
+ }
}
- }
- ReceiptScreen.template = 'ReceiptScreen';
+ ReceiptScreen.template = 'ReceiptScreen';
+ return ReceiptScreen;
+ };
- Registries.Component.add(ReceiptScreen);
+ Registries.Component.addByExtending(ReceiptScreen, AbstractReceiptScreen);
return ReceiptScreen;
});
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 8f6e419f557..9875d3030ab 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -2312,10 +2312,12 @@ exports.Paymentline = Backbone.Model.extend({
init_from_JSON: function(json){
this.amount = json.amount;
this.payment_method = this.pos.payment_methods_by_id[json.payment_method_id];
+ this.name = this.payment_method.name;
this.payment_status = json.payment_status;
this.ticket = json.ticket;
this.card_type = json.card_type;
this.transaction_id = json.transaction_id;
+ this.is_change = json.is_change;
},
//sets the amount of money on this payment line
set_amount: function(value){
@@ -2391,7 +2393,8 @@ exports.Paymentline = Backbone.Model.extend({
return {
cid: this.cid,
amount: this.get_amount(),
- payment_method: this.payment_method.name,
+ name: this.name,
+ ticket: this.ticket,
};
},
});
@@ -2410,7 +2413,7 @@ exports.Order = Backbone.Model.extend({
var self = this;
options = options || {};
- this.init_locked = true;
+ this.locked = false;
this.pos = options.pos;
this.selected_orderline = undefined;
this.selected_paymentline = undefined;
@@ -2453,13 +2456,12 @@ exports.Order = Backbone.Model.extend({
this.paymentlines.on('remove', this.pos.send_current_order_to_customer_facing_display, this.pos);
}
- this.init_locked = false;
this.save_to_db();
return this;
},
save_to_db: function(){
- if (!this.temporary && !this.init_locked) {
+ if (!this.temporary && !this.locked) {
this.pos.db.save_unpaid_order(this);
}
},
@@ -2536,6 +2538,14 @@ exports.Order = Backbone.Model.extend({
this.select_paymentline(newpaymentline);
}
}
+
+ // Tag this order as 'locked' if it is already paid.
+ this.locked = ['paid', 'done', 'invoiced'].includes(json.state);
+ this.state = json.state;
+ this.amount_return = json.amount_return;
+ this.account_move = json.account_move;
+ this.backendId = json.id;
+ this.isFromClosedSession = json.is_session_closed;
},
export_as_JSON: function() {
var orderLines, paymentLines;
@@ -2552,7 +2562,7 @@ exports.Order = Backbone.Model.extend({
amount_paid: this.get_total_paid() - this.get_change(),
amount_total: this.get_total_with_tax(),
amount_tax: this.get_total_tax(),
- amount_return: this.get_change(),
+ amount_return: this.amount_return ? this.amount_return : this.get_change(),
lines: orderLines,
statement_ids: paymentLines,
pos_session_id: this.pos_session_id,
@@ -2580,10 +2590,16 @@ exports.Order = Backbone.Model.extend({
orderlines.push(orderline.export_for_printing());
});
- var paymentlines = [];
- this.paymentlines.each(function(paymentline){
- paymentlines.push(paymentline.export_for_printing());
- });
+ // If order is locked (paid), the 'change' is saved as negative payment,
+ // and is flagged with is_change = true. A receipt that is printed first
+ // time doesn't show this negative payment so we filter it out.
+ var paymentlines = this.paymentlines.models
+ .filter(function (paymentline) {
+ return !paymentline.is_change;
+ })
+ .map(function (paymentline) {
+ return paymentline.export_for_printing();
+ });
var client = this.get('client');
var cashier = this.pos.get_cashier();
var company = this.pos.company;
@@ -2619,7 +2635,7 @@ exports.Order = Backbone.Model.extend({
total_discount: this.get_total_discount(),
rounding_applied: this.get_rounding_applied(),
tax_details: this.get_tax_details(),
- change: this.get_change(),
+ change: this.locked ? this.amount_return : this.get_change(),
name : this.get_name(),
client: client ? client : null ,
invoice_id: null, //TODO
diff --git a/addons/point_of_sale/static/src/js/printers.js b/addons/point_of_sale/static/src/js/printers.js
index b0332f604c9..4c2b465828d 100644
--- a/addons/point_of_sale/static/src/js/printers.js
+++ b/addons/point_of_sale/static/src/js/printers.js
@@ -68,7 +68,7 @@ var PrinterMixin = {
}
// rpc call is okay but printing failed because
// IoT box can't find a printer.
- if (!sendPrintResult) {
+ if (!sendPrintResult || (sendPrintResult && !sendPrintResult.result)) {
this.receipt_queue.length = 0;
return this.printResultGenerator.IoTResultError();
}
diff --git a/addons/point_of_sale/static/src/js/utils.js b/addons/point_of_sale/static/src/js/utils.js
index ae698ab60d3..26442ef776b 100644
--- a/addons/point_of_sale/static/src/js/utils.js
+++ b/addons/point_of_sale/static/src/js/utils.js
@@ -1,4 +1,4 @@
-odoo.define('point_of_sale.utils', function(require) {
+odoo.define('point_of_sale.utils', function (require) {
'use strict';
function getFileAsText(file) {
@@ -7,7 +7,7 @@ odoo.define('point_of_sale.utils', function(require) {
reject();
} else {
const reader = new FileReader();
- reader.addEventListener('load', function() {
+ reader.addEventListener('load', function () {
resolve(reader.result);
});
reader.addEventListener('abort', reject);
@@ -17,5 +17,31 @@ odoo.define('point_of_sale.utils', function(require) {
});
}
- return { getFileAsText };
+ /**
+ * This global variable is used by nextFrame to store the timer and
+ * be able to cancel it before another request for animation frame.
+ */
+ let timer = null;
+
+ /**
+ * Wait for the next animation frame to finish.
+ */
+ const nextFrame = () => {
+ return new Promise((resolve) => {
+ cancelAnimationFrame(timer);
+ timer = requestAnimationFrame(() => {
+ resolve();
+ });
+ });
+ };
+
+ function isRpcError(error) {
+ return (
+ !(error instanceof Error) &&
+ error.message &&
+ [100, 200, 404, -32098].includes(error.message.code)
+ );
+ }
+
+ return { getFileAsText, nextFrame, isRpcError };
});
diff --git a/addons/point_of_sale/static/src/xml/Chrome.xml b/addons/point_of_sale/static/src/xml/Chrome.xml
index f99905f8024..243641a5592 100644
--- a/addons/point_of_sale/static/src/xml/Chrome.xml
+++ b/addons/point_of_sale/static/src/xml/Chrome.xml
@@ -16,6 +16,7 @@
<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" />
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderManagementButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderManagementButton.xml
new file mode 100644
index 00000000000..062e11c3f49
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderManagementButton.xml
@@ -0,0 +1,12 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
index 000a743da04..353ea5f93a4 100644
--- a/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
+++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/OrderSelector.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
- <t t-name="OrderSelector" owl="1">
- <div class="order-selector">
+ <div class="order-selector" t-name="OrderSelector" owl="1">
+ <t t-if="chromeContext.showOrderSelector">
<span class="orders">
<t t-foreach="orderList" t-as="order" t-key="order.cid">
<t t-if="order === env.pos.get_order()">
@@ -28,7 +28,7 @@
<span class="order-button square deleteorder-button" t-on-click="deleteCurrentOrder">
<i class="fa fa-minus" role="img" aria-label="Delete order" title="Delete order" />
</span>
- </div>
- </t>
+ </t>
+ </div>
</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.xml
new file mode 100644
index 00000000000..72c85188c2e
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="InvoiceButton" owl="1">
+ <div class="control-button" t-att-class="{ highlight: isHighlighted }">
+ <i class="fa fa-file-pdf-o"></i>
+ <span> </span>
+ <span><t t-esc="commandName"></t></span>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.xml
new file mode 100644
index 00000000000..df3e4e0611c
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="ReprintReceiptButton" owl="1">
+ <div class="control-button">
+ <i class="fa fa-print"></i>
+ <span> </span>
+ <span>Print Receipt</span>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/MobileOrderManagementScreen.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/MobileOrderManagementScreen.xml
new file mode 100644
index 00000000000..479b35a4ca4
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/MobileOrderManagementScreen.xml
@@ -0,0 +1,30 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml
new file mode 100644
index 00000000000..87579d09a1e
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderDetails.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="OrderDetails" owl="1">
+ <div class="order-container">
+ <div t-ref="scrollable" class="order-scroller touch-scrollable">
+ <div class="order">
+ <t t-if="!props.order">
+ <div class="order-empty">
+ <i class="fa fa-shopping-cart" role="img" aria-label="Shopping cart"
+ title="Shopping cart" />
+ <h1>Select an order</h1>
+ </div>
+ </t>
+ <t t-elif="orderlines.length === 0">
+ <div class="order-empty">
+ <i class="fa fa-shopping-cart" role="img" aria-label="Shopping cart"
+ title="Shopping cart" />
+ <h1>Order is empty</h1>
+ </div>
+ </t>
+ <t t-else="">
+ <ul class="orderlines">
+ <t t-foreach="orderlines" t-as="orderline" t-key="orderline.id">
+ <OrderlineDetails line="orderline" />
+ </t>
+ </ul>
+ <OrderSummary total="total" tax="tax" />
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderList.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderList.xml
new file mode 100644
index 00000000000..865f609b8b2
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderList.xml
@@ -0,0 +1,20 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementControlPanel.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementControlPanel.xml
new file mode 100644
index 00000000000..3a294bfdcd1
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementControlPanel.xml
@@ -0,0 +1,36 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml
new file mode 100644
index 00000000000..8992e2c80ee
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml
@@ -0,0 +1,32 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml
new file mode 100644
index 00000000000..29b07cfe736
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml
@@ -0,0 +1,15 @@
+<?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>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml
new file mode 100644
index 00000000000..2e6869e5650
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderlineDetails.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="OrderlineDetails" owl="1">
+ <li class="orderline">
+ <span class="product-name">
+ <t t-esc="productName" />
+ </span>
+ <span class="price">
+ <t t-esc="totalPrice" />
+ </span>
+ <li class="info">
+ <strong>
+ <t t-esc="quantity" />
+ </strong>
+ <span><t t-esc="pricePerUnit" /></span>
+ </li>
+ </li>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml
new file mode 100644
index 00000000000..0a80a0e027a
--- /dev/null
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/ReprintReceiptScreen.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates id="template" xml:space="preserve">
+
+ <t t-name="ReprintReceiptScreen" owl="1">
+ <div class="receipt-screen screen">
+ <div class="screen-content">
+ <div class="top-content">
+ <span class="button back" t-on-click="confirm">
+ <i class="fa fa-angle-double-left"></i>
+ <span> </span>
+ <span>Back</span>
+ </span>
+ </div>
+ <div class="centered-content">
+ <div class="button print" t-on-click="tryReprint">
+ <i class="fa fa-print"></i> Print Receipt
+ </div>
+ <div class="pos-receipt-container">
+ <OrderReceipt order="props.order" t-ref="order-receipt" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/OrderSummary.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/OrderSummary.xml
index 04e1ab32446..a229c53a86d 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/OrderSummary.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/OrderSummary.xml
@@ -9,7 +9,7 @@
<span class="value">
<t t-esc="props.total" />
</span>
- <div class="subentry">
+ <div t-if="props.tax" class="subentry">
Taxes:
<span class="value">
<t t-esc="props.tax" />
diff --git a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml
index ea29ec1a02e..636d97e90d8 100644
--- a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml
@@ -124,10 +124,10 @@
<!-- Payment Lines -->
- <t t-foreach="paymentlines" t-as="line" t-key="line.cid">
+ <t t-foreach="receipt.paymentlines" t-as="line" t-key="line.cid">
<div>
<t t-esc="line.name" />
- <span t-esc="env.pos.format_currency_no_symbol(line.get_amount())" class="pos-receipt-right-align"/>
+ <span t-esc="env.pos.format_currency_no_symbol(line.amount)" class="pos-receipt-right-align"/>
</div>
</t>
<br/>
@@ -174,7 +174,7 @@
</div>
<div class="after-footer">
- <t t-foreach="paymentlines" t-as="line">
+ <t t-foreach="receipt.paymentlines" t-as="line">
<t t-if="line.ticket">
<br />
<div class="pos-payment-terminal-receipt">
diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js
index 2e06d183bd3..7789af6e7da 100644
--- a/addons/point_of_sale/static/tests/tours/Chrome.tour.js
+++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js
@@ -12,17 +12,17 @@ odoo.define('point_of_sale.tour.Chrome', function (require) {
// Order 1 is at Product Screen
ProductScreen.do.clickHomeCategory();
- ProductScreen.exec.order('Desk Pad', '1', '2');
+ ProductScreen.exec.addOrderline('Desk Pad', '1', '2');
// Order 2 is at Payment Screen
Chrome.do.newOrder();
- ProductScreen.exec.order('Monitor Stand', '3', '4');
+ ProductScreen.exec.addOrderline('Monitor Stand', '3', '4');
ProductScreen.do.clickPayButton();
PaymentScreen.check.isShown();
// Order 3 is at Receipt Screen
Chrome.do.newOrder();
- ProductScreen.exec.order('Whiteboard Pen', '5', '6');
+ ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
@@ -71,7 +71,7 @@ odoo.define('point_of_sale.tour.Chrome', function (require) {
ReceiptScreen.do.clickNextOrder();
// Invoice an order
- ProductScreen.exec.order('Whiteboard Pen', '5', '6');
+ ProductScreen.exec.addOrderline('Whiteboard Pen', '5', '6');
ProductScreen.do.clickCustomerButton();
ProductScreen.do.clickCustomer('Nicole Ford');
ProductScreen.do.clickSetCustomer();
diff --git a/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js
new file mode 100644
index 00000000000..44a09579fe3
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js
@@ -0,0 +1,135 @@
+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 { 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'] });
+ 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.newOrder();
+ ProductScreen.exec.addOrderline('Whiteboard Pen', '66', '6');
+
+ // order 0007, should be at payment screen
+ Chrome.do.newOrder();
+ 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',
+ });
+
+ // 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/web' }, getSteps());
+});
diff --git a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js
index 6a58c5bf231..f9ebfb0a4d1 100644
--- a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js
+++ b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js
@@ -8,7 +8,7 @@ odoo.define('point_of_sale.tour.PaymentScreen', function (require) {
startSteps();
- ProductScreen.exec.order('Letter Tray', '10');
+ ProductScreen.exec.addOrderline('Letter Tray', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.check.emptyPaymentlines('52.8');
diff --git a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js
index f6ee30bd405..afbf7dce73b 100644
--- a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js
+++ b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js
@@ -10,7 +10,7 @@ odoo.define('point_of_sale.tour.ReceiptScreen', function (require) {
startSteps();
// pay exact amount
- ProductScreen.exec.order('Letter Tray', '10');
+ ProductScreen.exec.addOrderline('Letter Tray', '10');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Bank');
PaymentScreen.do.clickValidate();
@@ -20,9 +20,9 @@ odoo.define('point_of_sale.tour.ReceiptScreen', function (require) {
// pay more than total price
ProductScreen.do.clickHomeCategory();
- ProductScreen.exec.order('Desk Pad', '6', '5.0');
- ProductScreen.exec.order('Whiteboard Pen', '6', '6.1');
- ProductScreen.exec.order('Monitor Stand', '6', '1.3');
+ ProductScreen.exec.addOrderline('Desk Pad', '6', '5.0');
+ ProductScreen.exec.addOrderline('Whiteboard Pen', '6', '6.1');
+ ProductScreen.exec.addOrderline('Monitor Stand', '6', '1.3');
ProductScreen.do.clickPayButton();
PaymentScreen.do.clickPaymentMethod('Cash');
PaymentScreen.do.pressNumpad('8 0 0');
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js
index 8f3969906d8..a5bd006fad1 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js
@@ -36,7 +36,20 @@ 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',
+ },
+ ];
+ }
}
- return { Do, Chrome: createTourMethods('Chrome', Do) };
+ return createTourMethods('Chrome', Do);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js
new file mode 100644
index 00000000000..d6be643e6e5
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js
@@ -0,0 +1,57 @@
+odoo.define('point_of_sale.tour.ClientListScreenTourMethods', function (require) {
+ 'use strict';
+
+ const { createTourMethods } = require('point_of_sale.tour.utils');
+
+ class Do {
+ clickClient(name) {
+ return [
+ {
+ content: `click client '${name}' from client list screen`,
+ trigger: `.clientlist-screen .client-list-contents .client-line td:contains("${name}")`,
+ },
+ {
+ content: `check if client '${name}' is highlighted`,
+ trigger: `.clientlist-screen .client-list-contents .client-line.highlight td:contains("${name}")`,
+ run: () => {},
+ },
+ ];
+ }
+ clickSet() {
+ return [
+ {
+ content: 'check if set button shown',
+ trigger: '.clientlist-screen .button.next.highlight',
+ run: () => {},
+ },
+ {
+ content: 'click set button',
+ trigger: '.clientlist-screen .button.next.highlight',
+ },
+ ];
+ }
+ }
+
+ class Check {
+ isShown() {
+ return [
+ {
+ content: 'client list screen is shown',
+ trigger: '.pos-content .clientlist-screen',
+ run: () => {},
+ },
+ ];
+ }
+ }
+
+ class Execute {
+ setClient(name) {
+ const steps = [];
+ steps.push(...this._do.clickClient(name));
+ steps.push(...this._do.clickSet());
+ return steps;
+ }
+ }
+
+ return createTourMethods('ClientListScreen', Do, Check, Execute);
+});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js
new file mode 100644
index 00000000000..c361a532d89
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js
@@ -0,0 +1,23 @@
+odoo.define('point_of_sale.tour.CompositeTourMethods', function (require) {
+ 'use strict';
+
+ const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods');
+ const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods');
+ const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods');
+ const { ClientListScreen } = require('point_of_sale.tour.ClientListScreenTourMethods');
+
+ function makeFullOrder({ orderlist, customer, payment, ntimes = 1 }) {
+ for (let i = 0; i < ntimes; i++) {
+ ProductScreen.exec.addMultiOrderlines(...orderlist);
+ if (customer) {
+ ProductScreen.do.clickCustomerButton();
+ ClientListScreen.exec.setClient(customer);
+ }
+ ProductScreen.do.clickPayButton();
+ PaymentScreen.exec.pay(...payment);
+ ReceiptScreen.exec.nextOrder();
+ }
+ }
+
+ return { makeFullOrder };
+});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js
index eab6b05725b..3d8c07cf1f2 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js
@@ -26,5 +26,5 @@ odoo.define('point_of_sale.tour.ErrorPopupTourMethods', function (require) {
}
}
- return { Do, Check, ErrorPopup: createTourMethods('ErrorPopup', Do, Check) };
+ return createTourMethods('ErrorPopup', Do, Check);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js
index e3d0a321814..44fe6890c9e 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js
@@ -65,5 +65,5 @@ odoo.define('point_of_sale.tour.NumberPopupTourMethods', function (require) {
}
}
- return { Do, Check, NumberPopup: createTourMethods('NumberPopup', Do, Check) };
+ return createTourMethods('NumberPopup', Do, Check);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js
new file mode 100644
index 00000000000..b00453e0b4d
--- /dev/null
+++ b/addons/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js
@@ -0,0 +1,176 @@
+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) {
+ return [
+ {
+ content: `clicking order '${name}' from orderlist`,
+ trigger: `.order-management-screen .order-list .order-row .item:contains("${name}")`,
+ },
+ ];
+ }
+ 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 }) {
+ 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: () => {},
+ });
+ }
+ 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: () => {},
+ }
+ ]
+ }
+ }
+
+ return createTourMethods('OrderManagementScreen', Do, Check);
+});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js
index 0e2ae3a51f5..8d65e98d63e 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js
@@ -191,9 +191,18 @@ odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) {
}
}
- return {
- Do,
- Check,
- PaymentScreen: createTourMethods('PaymentScreen', Do, Check),
- };
+ class Execute {
+ pay(method, amount) {
+ const steps = [];
+ steps.push(...this._do.clickPaymentMethod(method));
+ for (let char of amount.split('')) {
+ steps.push(...this._do.pressNumpad(char));
+ }
+ steps.push(...this._check.validateButtonIsHighlighted());
+ steps.push(...this._do.clickValidate());
+ return steps;
+ }
+ }
+
+ return createTourMethods('PaymentScreen', Do, Check, Execute);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js
index 6546dab0bcd..2bb4d025ae0 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js
@@ -181,10 +181,19 @@ odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) {
},
];
}
+ totalAmountIs(amount) {
+ return [
+ {
+ content: `order total amount is '${amount}'`,
+ trigger: `.order-container .order .summary .value:contains("${amount}")`,
+ run: () => {},
+ },
+ ];
+ }
}
class Execute {
- order(productName, quantity, price) {
+ addOrderline(productName, quantity, price) {
const res = this._do.clickDisplayedProduct(productName);
if (price) {
res.push(...this._do.pressNumpad('Price'));
@@ -200,12 +209,14 @@ odoo.define('point_of_sale.tour.ProductScreenTourMethods', function (require) {
}
return res;
}
+ addMultiOrderlines(...list) {
+ const steps = [];
+ for (let [product, qty, price] of list) {
+ steps.push(...this.addOrderline(product, qty, price));
+ }
+ return steps;
+ }
}
- return {
- Do,
- Check,
- Execute,
- ProductScreen: createTourMethods('ProductScreen', Do, Check, Execute),
- };
+ return createTourMethods('ProductScreen', Do, Check, Execute);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js
index 6000af6a962..799daace7eb 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js
@@ -51,9 +51,11 @@ odoo.define('point_of_sale.tour.ReceiptScreenTourMethods', function (require) {
}
}
- return {
- Do,
- Check,
- ReceiptScreen: createTourMethods('ReceiptScreen', Do, Check),
- };
+ class Execute {
+ nextOrder() {
+ return [...this._check.isShown(), ...this._do.clickNextOrder()];
+ }
+ }
+
+ return createTourMethods('ReceiptScreen', Do, Check, Execute);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js
index c13971a17f3..bbe4fc2d369 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js
@@ -35,5 +35,5 @@ odoo.define('point_of_sale.tour.SelectionPopupTourMethods', function (require) {
}
}
- return { Do, Check, SelectionPopup: createTourMethods('SelectionPopup', Do, Check) };
+ return createTourMethods('SelectionPopup', Do, Check);
});
diff --git a/addons/point_of_sale/static/tests/tours/helpers/utils.js b/addons/point_of_sale/static/tests/tours/helpers/utils.js
index b76c2677e35..e8fcc5913bd 100644
--- a/addons/point_of_sale/static/tests/tours/helpers/utils.js
+++ b/addons/point_of_sale/static/tests/tours/helpers/utils.js
@@ -97,7 +97,11 @@ odoo.define('point_of_sale.tour.utils', function (require) {
// we proxy get of the method to decorate the method call
const proxyHandler = {
get(target, key) {
- return new Proxy(target[key].bind(target), methodProxyHandler);
+ const method = target[key];
+ if (!method) {
+ throw new Error(`Tour method '${key}' is not available.`);
+ }
+ return new Proxy(method.bind(target), methodProxyHandler);
},
};
@@ -131,12 +135,17 @@ odoo.define('point_of_sale.tour.utils', function (require) {
methods.exec._do = methods.do;
methods.exec._check = methods.check;
return {
- do: new Proxy(methods.do, proxyHandler),
- check: new Proxy(methods.check, proxyHandler),
- exec: new Proxy(methods.exec, proxyHandler),
- _do: methods.do,
- _check: methods.check,
- _exec: methods.exec,
+ Do,
+ Check,
+ Execute,
+ [name]: {
+ do: new Proxy(methods.do, proxyHandler),
+ check: new Proxy(methods.check, proxyHandler),
+ exec: new Proxy(methods.exec, proxyHandler),
+ _do: methods.do,
+ _check: methods.check,
+ _exec: methods.exec,
+ },
};
}
diff --git a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js
index 9570aa0b7e8..c1ad31804e6 100644
--- a/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js
+++ b/addons/point_of_sale/static/tests/unit/test_ChromeWidgets.js
@@ -54,6 +54,7 @@ odoo.define('point_of_sale.tests.ChromeWidgets', function (require) {
class Parent extends PopupControllerMixin(PosComponent) {}
Parent.env = makePosTestEnv();
+ Parent.env.chrome = new owl.Context({ showOrderSelector: true });
Parent.template = xml/* html */ `
<div>
<OrderSelector></OrderSelector>
diff --git a/addons/point_of_sale/tests/test_frontend.py b/addons/point_of_sale/tests/test_frontend.py
index 08aeac8e6dd..759c23bee1e 100644
--- a/addons/point_of_sale/tests/test_frontend.py
+++ b/addons/point_of_sale/tests/test_frontend.py
@@ -432,3 +432,8 @@ class TestUi(TestPointOfSaleHttpCommon):
n_paid = self.env['pos.order'].search_count([('state', '=', 'paid')])
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):
+ 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/web?config_id=%d" % self.main_pos_config.id, 'OrderManagementScreenTour', login="admin", step_delay=50)
diff --git a/addons/point_of_sale/views/pos_assets_common.xml b/addons/point_of_sale/views/pos_assets_common.xml
index ce431f13069..2b68aa31138 100644
--- a/addons/point_of_sale/views/pos_assets_common.xml
+++ b/addons/point_of_sale/views/pos_assets_common.xml
@@ -15,6 +15,7 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/utils.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ClassRegistry.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/PosComponent.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/PosContext.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ComponentRegistry.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Registries.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/db.js"></script>
@@ -23,6 +24,7 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/barcode_reader.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Gui.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/PopupControllerMixin.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/ControlButtonsMixin.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Chrome.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/devices.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/payment.js"></script>
@@ -32,6 +34,17 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetails.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/ClientListScreen/ClientListScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/InvoiceButton.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderFetcher.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/MobileOrderManagementScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementControlPanel.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderList.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderRow.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderDetails.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderlineDetails.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenElectronicPayment.js"></script>
@@ -60,10 +73,13 @@
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/CashierName.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/ProxyStatus.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/SyncNotification.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/Draggable.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Misc/NotificationSound.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Misc/IndependentToOrderScreen.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/ChromeWidgets/DebugWidget.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js"></script>
<script type="text/javascript" src="/point_of_sale/static/src/js/Popups/ErrorPopup.js"></script>
diff --git a/addons/point_of_sale/views/pos_assets_index.xml b/addons/point_of_sale/views/pos_assets_index.xml
index 730624eeb54..90d35211bd2 100644
--- a/addons/point_of_sale/views/pos_assets_index.xml
+++ b/addons/point_of_sale/views/pos_assets_index.xml
@@ -7,14 +7,18 @@
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/utils.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/OrderManagementScreenTourMethods.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ClientListScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ChromeTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/ErrorPopupTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/SelectionPopupTourMethods.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/helpers/CompositeTourMethods.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/point_of_sale.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ProductScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/PaymentScreen.tour.js"></script>
+ <script type="text/javascript" src="/point_of_sale/static/tests/tours/OrderManagementScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/ReceiptScreen.tour.js"></script>
<script type="text/javascript" src="/point_of_sale/static/tests/tours/Chrome.tour.js"></script>
</xpath>
diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml
index c1dd061b0d6..572786b33ee 100644
--- a/addons/point_of_sale/views/pos_config_view.xml
+++ b/addons/point_of_sale/views/pos_config_view.xml
@@ -37,6 +37,7 @@
<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" class="oe_edit_only"/>
<h1><field name="name"/></h1>
@@ -81,6 +82,20 @@
</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 &amp; Receipts' section.
+ </div>
+ </div>
+ </div>
<div id="category_reference" class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="iface_display_categ_images"/>
@@ -454,22 +469,6 @@
</div>
</div>
</div>
- <div class="col-12 col-lg-6 o_setting_box pos_reprint_receipt">
- <div class="o_setting_left_pane">
- <field name='module_pos_reprint'/>
- </div>
- <div class="o_setting_right_pane">
- <label for="module_pos_reprint"/>
- <div class="text-muted">
- Allow cashier to reprint receipts
- </div>
- <div class="content-group" attrs="{'invisible': [('module_pos_reprint', '=', False)]}">
- <div class="mt16" id="btn_use_pos_reprint">
- <button string="Save" type="object" name="execute" class="oe_highlight"/>
- </div>
- </div>
- </div>
- </div>
<div class="col-12 col-lg-6 o_setting_box" id="iface_invoicing">
<div class="o_setting_left_pane">
<field name="module_account"/>
diff --git a/addons/pos_hr/static/tests/tours/PosHrTourMethods.js b/addons/pos_hr/static/tests/tours/PosHrTourMethods.js
index 391fc1421b6..2ad6c1aa2d4 100644
--- a/addons/pos_hr/static/tests/tours/PosHrTourMethods.js
+++ b/addons/pos_hr/static/tests/tours/PosHrTourMethods.js
@@ -63,5 +63,5 @@ odoo.define('pos_hr.tour.PosHrTourMethods', function (require) {
}
}
- return { Do, Check, Execute, PosHr: createTourMethods('PosHr', Do, Check, Execute) };
+ return createTourMethods('PosHr', Do, Check, Execute);
});
diff --git a/addons/pos_mercury/static/src/js/pos_mercury.js b/addons/pos_mercury/static/src/js/pos_mercury.js
index 93687a68b6d..48342f24d2d 100644
--- a/addons/pos_mercury/static/src/js/pos_mercury.js
+++ b/addons/pos_mercury/static/src/js/pos_mercury.js
@@ -119,6 +119,12 @@ pos_model.Paymentline = pos_model.Paymentline.extend({
is_done: function () {
var res = _paylineproto.is_done.apply(this);
return res && !this.mercury_swipe_pending;
+ },
+ export_for_printing: function () {
+ const result = _paylineproto.export_for_printing.apply(this, arguments);
+ result.mercury_data = this.mercury_data;
+ result.mercury_auth_code = this.mercury_auth_code;
+ return result;
}
});
diff --git a/addons/pos_mercury/static/src/xml/OrderReceipt.xml b/addons/pos_mercury/static/src/xml/OrderReceipt.xml
index b05ebcada28..014884c0a00 100644
--- a/addons/pos_mercury/static/src/xml/OrderReceipt.xml
+++ b/addons/pos_mercury/static/src/xml/OrderReceipt.xml
@@ -2,7 +2,7 @@
<templates id="template" xml:space="preserve">
<t t-name="pos_mercury.OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
- <xpath expr="//t[@t-foreach='paymentlines']" position="inside">
+ <xpath expr="//t[@t-foreach='receipt.paymentlines']" position="inside">
<t t-if="line.mercury_data">
<div class="pos-receipt-left-padding">
<span>APPROVAL CODE: </span>
diff --git a/addons/pos_restaurant/__manifest__.py b/addons/pos_restaurant/__manifest__.py
index b6c573586e1..fab9834bb16 100644
--- a/addons/pos_restaurant/__manifest__.py
+++ b/addons/pos_restaurant/__manifest__.py
@@ -27,6 +27,8 @@ This module adds several features to the Point of Sale that are specific to rest
],
'qweb': [
'static/src/xml/Resizeable.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderList.xml',
+ 'static/src/xml/Screens/OrderManagementScreen/OrderRow.xml',
'static/src/xml/Screens/ProductScreen/ControlButtons/OrderlineNoteButton.xml',
'static/src/xml/Screens/ProductScreen/ControlButtons/TableGuestsButton.xml',
'static/src/xml/Screens/ProductScreen/ControlButtons/PrintBillButton.xml',
@@ -44,7 +46,6 @@ This module adds several features to the Point of Sale that are specific to rest
'static/src/xml/Screens/FloorScreen/EditableTable.xml',
'static/src/xml/ChromeWidgets/OrderSelector.xml',
'static/src/xml/ChromeWidgets/BackToFloorButton.xml',
- 'static/src/xml/Chrome.xml',
'static/src/xml/multiprint.xml',
],
'demo': [
diff --git a/addons/pos_restaurant/models/pos_order.py b/addons/pos_restaurant/models/pos_order.py
index 70966ea47ba..644c53be8e7 100644
--- a/addons/pos_restaurant/models/pos_order.py
+++ b/addons/pos_restaurant/models/pos_order.py
@@ -189,3 +189,8 @@ class PosOrder(models.Model):
order_fields['customer_count'] = ui_order.get('customer_count', 0)
order_fields['multiprint_resume'] = ui_order.get('multiprint_resume', False)
return order_fields
+
+ def _export_for_ui(self, order):
+ result = super(PosOrder, self)._export_for_ui(order)
+ result['table_id'] = order.table_id.id
+ return result
diff --git a/addons/pos_restaurant/static/src/js/Chrome.js b/addons/pos_restaurant/static/src/js/Chrome.js
index 399a3887125..949082f1d8e 100644
--- a/addons/pos_restaurant/static/src/js/Chrome.js
+++ b/addons/pos_restaurant/static/src/js/Chrome.js
@@ -45,7 +45,7 @@ odoo.define('pos_restaurant.chrome', function (require) {
*/
_showSavedScreen(pos, newSelectedOrder) {
if (!newSelectedOrder) {
- this.showScreen('FloorScreen', { floor: pos.table.floor });
+ this.showScreen('FloorScreen', { floor: pos.table ? pos.table.floor : null });
} else {
super._showSavedScreen(pos, newSelectedOrder);
}
@@ -68,7 +68,8 @@ odoo.define('pos_restaurant.chrome', function (require) {
if (this.tempScreen.isShown) {
this.trigger('close-temp-screen');
}
- this.showScreen('FloorScreen', { floor: this.env.pos.table.floor });
+ const table = this.env.pos.table;
+ this.showScreen('FloorScreen', { floor: table ? table.floor : null });
}
_shouldResetIdleTimer() {
return this.env.pos.config.iface_floorplan && this.mainScreen.name !== 'FloorScreen';
diff --git a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js
index aaf929ed0fe..276663cf57c 100644
--- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js
+++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js
@@ -289,6 +289,7 @@ odoo.define('pos_restaurant.FloorScreen', function (require) {
}
}
FloorScreen.template = 'FloorScreen';
+ FloorScreen.hideOrderSelector = true;
Registries.Component.add(FloorScreen);
diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
new file mode 100644
index 00000000000..e2a1ce65dce
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -0,0 +1,29 @@
+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();
+ if (currentOrder && currentOrder.uid === order.uid) {
+ this.close();
+ } else {
+ this.env.pos.set_table(order.table, order);
+ }
+ } else {
+ super._setOrder(order);
+ }
+ }
+ };
+
+ Registries.Component.extend(OrderManagementScreen, PosResOrderManagementScreen);
+
+ return OrderManagementScreen;
+});
diff --git a/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js
new file mode 100644
index 00000000000..f5a32114553
--- /dev/null
+++ b/addons/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js
@@ -0,0 +1,17 @@
+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;
+});
diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js
index 1276fea9f8b..b6614c07ebe 100644
--- a/addons/pos_restaurant/static/src/js/floors.js
+++ b/addons/pos_restaurant/static/src/js/floors.js
@@ -45,9 +45,9 @@ models.load_models({
// New orders are now associated with the current table, if any.
var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
- initialize: function() {
+ initialize: function(attr,options) {
_super_order.initialize.apply(this,arguments);
- if (!this.table) {
+ if (!this.table && !options.json) {
this.table = this.pos.table;
}
this.customer_count = this.customer_count || 1;
@@ -192,16 +192,20 @@ models.PosModel = models.PosModel.extend({
}
},
- set_order_on_table: function() {
+ /**
+ * @param {models.Order} order order to set
+ */
+ set_order_on_table: function(order) {
var orders = this.get_order_list();
if (orders.length) {
- this.set_order(orders[0]); // and go to the first one ...
+ order = order ? orders.find((o) => o.uid === order.uid) : null;
+ this.set_order(order || orders[0]);
} else {
this.add_new_order(); // or create a new order with the current table
}
},
- sync_to_server: function(table) {
+ sync_to_server: function(table, order) {
var self = this;
var ids_to_remove = this.db.get_ids_to_remove_from_server();
@@ -229,7 +233,7 @@ models.PosModel = models.PosModel.extend({
}).catch(function(reason){
self.set_synch('error');
}).finally(function(){
- self.set_order_on_table();
+ self.set_order_on_table(order);
});
},
@@ -247,8 +251,9 @@ models.PosModel = models.PosModel.extend({
*
* Switch table and make sure all nececery syncing tasks are done.
* @param {object} table.
+ * @param {models.Order|undefined} order if provided, set to this order
*/
- set_table: function(table) {
+ set_table: function(table, order) {
if(!table){
this.sync_from_server(table, this.get_order_list(), this.get_order_with_uid());
this.set_order(null);
@@ -262,7 +267,7 @@ models.PosModel = models.PosModel.extend({
this.set_order(null);
} else {
this.table = table;
- this.sync_to_server(table);
+ this.sync_to_server(table, order);
}
},
diff --git a/addons/pos_restaurant/static/src/xml/Chrome.xml b/addons/pos_restaurant/static/src/xml/Chrome.xml
deleted file mode 100644
index ee29abe2e36..00000000000
--- a/addons/pos_restaurant/static/src/xml/Chrome.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<templates id="template" xml:space="preserve">
-
- <t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
- <xpath expr="//OrderSelector" position="attributes">
- <attribute name="t-if">mainScreen.name !== 'FloorScreen'</attribute>
- </xpath>
- <xpath expr="//OrderSelector" position="after">
- <div t-else="" class="order-selector"></div>
- </xpath>
- </t>
-
-</templates>
diff --git a/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml
new file mode 100644
index 00000000000..7960710260f
--- /dev/null
+++ b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderList.xml
@@ -0,0 +1,12 @@
+<?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>
diff --git a/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml
new file mode 100644
index 00000000000..3006e5bfb42
--- /dev/null
+++ b/addons/pos_restaurant/static/src/xml/Screens/OrderManagementScreen/OrderRow.xml
@@ -0,0 +1,12 @@
+<?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>
diff --git a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js
index 924d34010e4..102ff280977 100644
--- a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js
+++ b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js
@@ -16,7 +16,7 @@ odoo.define('pos_restaurant.tour.ControlButtons', function (require) {
// Test TransferOrderButton
FloorScreen.do.clickTable('T2');
- ProductScreen.exec.order('Water', '5.0', '2.0');
+ ProductScreen.exec.addOrderline('Water', '5.0', '2.0');
ProductScreen.do.clickTransferButton();
FloorScreen.do.clickTable('T4');
ProductScreen.do.clickOrderline('Water', '5.0', '2.0');
@@ -37,7 +37,7 @@ odoo.define('pos_restaurant.tour.ControlButtons', function (require) {
TextAreaPopup.do.inputText('test note');
TextAreaPopup.do.clickConfirm();
ProductScreen.check.orderlineHasNote('Water', '5.0', 'test note');
- ProductScreen.exec.order('Water', '8', '1.1');
+ ProductScreen.exec.addOrderline('Water', '8', '1.1');
ProductScreen.check.selectedOrderlineHas('Water', '8', '8.80');
// Test PrintBillButton
diff --git a/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js b/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js
new file mode 100644
index 00000000000..2a198c9e7ab
--- /dev/null
+++ b/addons/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js
@@ -0,0 +1,121 @@
+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 { 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.newOrder();
+ 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.newOrder();
+ 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');
+ 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/web' }, getSteps());
+});
diff --git a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js
index 07147a70624..82e783dbcc7 100644
--- a/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js
+++ b/addons/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js
@@ -14,9 +14,9 @@ odoo.define('pos_restaurant.tour.SplitBillScreen', function (require) {
startSteps();
FloorScreen.do.clickTable('T2');
- ProductScreen.exec.order('Water', '5', '1.2');
- ProductScreen.exec.order('Minute Maid', '3', '1.2');
- ProductScreen.exec.order('Coca-Cola', '1', '1.2');
+ ProductScreen.exec.addOrderline('Water', '5', '1.2');
+ ProductScreen.exec.addOrderline('Minute Maid', '3', '1.2');
+ ProductScreen.exec.addOrderline('Coca-Cola', '1', '1.2');
ProductScreen.do.clickSplitBillButton();
// Check if the screen contains all the orderlines
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js
index 1bb976cd8a2..9f831367aac 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js
@@ -26,5 +26,5 @@ odoo.define('pos_restaurant.tour.BillScreenTourMethods', function (require) {
}
}
- return { Do, Check, BillScreen: createTourMethods('BillScreen', Do, Check) };
+ return createTourMethods('BillScreen', Do, Check);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js
index 696b1d793c0..d60e39864cc 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js
@@ -29,8 +29,5 @@ odoo.define('pos_restaurant.tour.ChromeTourMethods', function (require) {
class Execute {}
- return {
- Do: DoExt,
- Chrome: createTourMethods('Chrome', DoExt, Check, Execute),
- };
+ return createTourMethods('Chrome', DoExt, Check, Execute);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js
index ce64f8cb3d3..6895dc3e257 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/FloorScreenTourMethods.js
@@ -129,10 +129,5 @@ odoo.define('pos_restaurant.tour.FloorScreenTourMethods', function (require) {
class Execute {}
- return {
- Do,
- Check,
- Execute,
- FloorScreen: createTourMethods('FloorScreen', Do, Check, Execute),
- };
+ return createTourMethods('FloorScreen', Do, Check, Execute);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js
index f14434ec705..decd30c4a1b 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js
@@ -66,10 +66,5 @@ odoo.define('pos_restaurant.tour.ProductScreenTourMethods', function (require) {
class ExecuteExt extends Execute {}
- return {
- Do: DoExt,
- Check: CheckExt,
- Execute: ExecuteExt,
- ProductScreen: createTourMethods('ProductScreen', DoExt, CheckExt, ExecuteExt),
- };
+ return createTourMethods('ProductScreen', DoExt, CheckExt, ExecuteExt);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js
index d1c802bf8d4..7f58a1fc083 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/SplitBillScreenTourMethods.js
@@ -61,10 +61,5 @@ odoo.define('pos_restaurant.tour.SplitBillScreenTourMethods', function (require)
class Execute {}
- return {
- Do,
- Check,
- Execute,
- SplitBillScreen: createTourMethods('SplitBillScreen', Do, Check, Execute),
- };
+ return createTourMethods('SplitBillScreen', Do, Check, Execute);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js
index e2bbb2bde47..8f833370271 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js
@@ -35,5 +35,5 @@ odoo.define('pos_restaurant.tour.TextAreaPopupTourMethods', function (require) {
}
}
- return { Do, Check, TextAreaPopup: createTourMethods('TextAreaPopup', Do, Check) };
+ return createTourMethods('TextAreaPopup', Do, Check);
});
diff --git a/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js
index 6ce5e4ce035..b46841c60d1 100644
--- a/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js
+++ b/addons/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js
@@ -35,5 +35,5 @@ odoo.define('pos_restaurant.tour.TextInputPopupTourMethods', function (require)
}
}
- return { Do, Check, TextInputPopup: createTourMethods('TextInputPopup', Do, Check) };
+ return createTourMethods('TextInputPopup', Do, Check);
});
diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py
index 28d023f294e..87f8a4b7822 100644
--- a/addons/pos_restaurant/tests/test_frontend.py
+++ b/addons/pos_restaurant/tests/test_frontend.py
@@ -149,6 +149,9 @@ class TestFrontend(odoo.tests.HttpCase):
'taxes_id': [(6, 0, [])],
})
+ pricelist = self.env['product.pricelist'].create({'name': 'Restaurant Pricelist'})
+ pos_config.write({'pricelist_id': pricelist.id})
+
self.pos_config = pos_config
def test_01_pos_restaurant(self):
@@ -171,3 +174,8 @@ class TestFrontend(odoo.tests.HttpCase):
self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'SplitBillScreenTour', login="admin", step_delay=50)
self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'ControlButtonsTour', login="admin", step_delay=50)
self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'FloorScreenTour', login="admin", step_delay=50)
+
+ 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/web?config_id=%d" % self.pos_config.id, 'PosResOrderManagementScreenTour', login="admin", step_delay=50)
diff --git a/addons/pos_restaurant/views/pos_restaurant_templates.xml b/addons/pos_restaurant/views/pos_restaurant_templates.xml
index cbef00a5d77..a2226920294 100644
--- a/addons/pos_restaurant/views/pos_restaurant_templates.xml
+++ b/addons/pos_restaurant/views/pos_restaurant_templates.xml
@@ -22,6 +22,8 @@
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/EditBar.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/TableWidget.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/FloorScreen/EditableTable.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/OrderManagementScreen/OrderRow.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/OrderSelector.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/BackToFloorButton.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/src/js/Chrome.js"></script>
@@ -51,6 +53,7 @@
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/SplitBillScreen.tour.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/ControlButtons.tour.js"></script>
<script type="text/javascript" src="/pos_restaurant/static/tests/tours/FloorScreen.tour.js"></script>
+ <script type="text/javascript" src="/pos_restaurant/static/tests/tours/OrderManagementScreen.tour.js"></script>
</xpath>
</template>