diff options
| author | Joseph Caburnay <jcb@odoo.com> | 2020-06-15 09:59:37 +0000 |
|---|---|---|
| committer | Joseph Caburnay <jcb@odoo.com> | 2020-07-17 10:00:04 +0000 |
| commit | b2f8170d88239b5eaf04f1e3c0dcef613b78fedc (patch) | |
| tree | ae1a178dab9afd382cd9e45e8db5b28d6a6424e5 | |
| parent | 39ff9d95c2c2b09e7d6ec47051ca0bb3c9f6165a (diff) | |
[IMP] point_of_sale,pos_*: allow users to manage orders in the pos frontend
We now allow invoicing of finalized orders in an open session in this first
iteration of order management in the pos frontend. Additionally, receipt
reprinting is also introduced. With this feature, we can now reprint the
receipt of old orders.
closes odoo/odoo#51141
Task-id: 1981354
Related: odoo/enterprise#11690
Related: odoo/upgrade#1472
Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
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 & 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> |
