diff options
Diffstat (limited to 'addons')
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> |
