diff options
| author | Joseph Caburnay <jcb@odoo.com> | 2020-07-31 09:36:39 +0000 |
|---|---|---|
| committer | Joseph Caburnay <jcb@odoo.com> | 2020-08-12 09:17:34 +0000 |
| commit | b0107e9230da5cb5c90819e5bee3e9cd323816ee (patch) | |
| tree | 110e65cd9a180fbf782a70c392b9378a1f662e89 /addons | |
| parent | 96f7c5eda84d6b7a1eade31efb8c35aff4ce94c4 (diff) | |
[IMP] pos_restaurant,point_of_sale: set tip after payment
1. Introduce an option in restaurant config to allow setting tips after payment.
2. Tip form is shown in bills so the customers can choose/set their tips.
3. Validated orders are kept in TipScreen (access via TicketScreen) so that at the
end of the day, the user/cashier can set the tips for each validated order.
closes odoo/odoo#55488
Task-id: 2117029
Related: odoo/upgrade#1606
Signed-off-by: pimodoo <pimodoo@users.noreply.github.com>
Diffstat (limited to 'addons')
24 files changed, 673 insertions, 26 deletions
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index ecdb7ae0e45..500890cf0fe 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -49,6 +49,8 @@ class PosOrder(models.Model): 'amount_return': ui_order['amount_return'], 'company_id': self.env['pos.session'].browse(ui_order['pos_session_id']).company_id.id, 'to_invoice': ui_order['to_invoice'] if "to_invoice" in ui_order else False, + 'is_tipped': ui_order.get('is_tipped', False), + 'tip_amount': ui_order.get('tip_amount', 0), } @api.model @@ -253,6 +255,8 @@ class PosOrder(models.Model): session_move_id = fields.Many2one('account.move', string='Session Journal Entry', related='session_id.move_id', readonly=True, copy=False) to_invoice = fields.Boolean('To invoice') is_invoiced = fields.Boolean('Is Invoiced', compute='_compute_is_invoiced') + is_tipped = fields.Boolean('Is this already tipped?', readonly=True) + tip_amount = fields.Float(string='Tip Amount', digits=0, readonly=True) @api.depends('account_move') def _compute_is_invoiced(self): @@ -615,6 +619,8 @@ class PosOrder(models.Model): 'state': order.state, 'account_move': order.account_move.id, 'id': order.id, + 'is_tipped': order.is_tipped, + 'tip_amount': order.tip_amount, } def export_for_ui(self): diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js index 2d552e4a1d4..900204dadb0 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -199,7 +199,7 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { const shouldShowPrintInvoice = errorCode ? this.currentOrder.is_to_invoice() && errorCode < 0 : false; - this.showScreen('ReceiptScreen', { printInvoiceIsShown: shouldShowPrintInvoice }); + this.showScreen(this.nextScreen, { printInvoiceIsShown: shouldShowPrintInvoice }); // If we succeeded in syncing the current order, and // there are still other orders that are left unsynced, @@ -219,6 +219,9 @@ odoo.define('point_of_sale.PaymentScreen', function (require) { } } } + get nextScreen() { + return 'ReceiptScreen'; + } async _isOrderValid(isForceValidate) { if (this.currentOrder.get_orderlines().length === 0) { this.showPopup('ErrorPopup', { diff --git a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js index 724766e93da..52ff376d36b 100644 --- a/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js @@ -163,6 +163,7 @@ odoo.define('point_of_sale.TicketScreen', function (require) { ProductScreen: 'Ongoing', PaymentScreen: 'Payment', ReceiptScreen: 'Receipt', + TipScreen: 'Tipping', }; } _initializeSearchFieldConstants() { diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 6d0d5ff63f8..71d5453874d 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -70,6 +70,10 @@ exports.PosModel = Backbone.Model.extend({ this.order_sequence = 1; window.posmodel = this; + // Object mapping the order's name (which contains the uid) to it's server_id after + // validation (order paid then sent to the backend). + this.validated_orders_name_server_id_map = {}; + // Extract the config id from the url. var given_config = new RegExp('[\?&]config_id=([^&#]*)').exec(window.location.href); this.config_id = given_config && given_config[1] && parseInt(given_config[1]) || false; @@ -1035,6 +1039,9 @@ exports.PosModel = Backbone.Model.extend({ return this._save_to_server(orders, options).then(function (server_ids) { self.set_synch('connected'); + for (let i = 0; i < server_ids.length; i++) { + self.validated_orders_name_server_id_map[server_ids[i].pos_reference] = server_ids[i].id; + } return _.pluck(server_ids, 'id'); }).catch(function(error){ self.set_synch(self.get('failed') ? 'error' : 'disconnected'); @@ -1372,7 +1379,11 @@ exports.PosModel = Backbone.Model.extend({ disallowLineQuantityChange() { return false; - } + }, + + getCurrencySymbol() { + return this.currency ? this.currency.symbol : '$'; + }, }); /** @@ -2624,6 +2635,8 @@ exports.Order = Backbone.Model.extend({ this.account_move = json.account_move; this.backendId = json.id; this.isFromClosedSession = json.is_session_closed; + this.is_tipped = json.is_tipped || false; + this.tip_amount = json.tip_amount || 0; }, export_as_JSON: function() { var orderLines, paymentLines; @@ -2654,6 +2667,8 @@ exports.Order = Backbone.Model.extend({ fiscal_position_id: this.fiscal_position ? this.fiscal_position.id : false, server_id: this.server_id ? this.server_id : false, to_invoice: this.to_invoice ? this.to_invoice : false, + is_tipped: this.is_tipped || false, + tip_amount: this.tip_amount || 0, }; if (!this.is_paid && this.user_id) { json.user_id = this.user_id; @@ -2848,7 +2863,8 @@ exports.Order = Backbone.Model.extend({ return; } } - this.add_product(tip_product, { + return this.add_product(tip_product, { + is_tip: true, quantity: 1, price: tip, lst_price: tip, @@ -2921,6 +2937,10 @@ exports.Order = Backbone.Model.extend({ line[prop] = options.extras[prop]; } } + if (options.is_tip) { + this.is_tipped = true; + this.tip_amount = options.price; + } var to_merge_orderline; for (var i = 0; i < this.orderlines.length; i++) { @@ -3299,7 +3319,7 @@ exports.Order = Backbone.Model.extend({ if (this.get_paymentlines().length > 0) return { name: 'PaymentScreen' }; return { name: 'ProductScreen' }; } - if (screen.name !== 'ReceiptScreen' && this.get_paymentlines().length > 0) { + if (!this.finalized && this.get_paymentlines().length > 0) { return { name: 'PaymentScreen' }; } return screen; 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 8d65e98d63e..6950d01de5a 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js @@ -36,6 +36,14 @@ odoo.define('point_of_sale.tour.PaymentScreenTourMethods', function (require) { ]; } + clickTipButton() { + return [ + { + trigger: `.payment-buttons .js_tip`, + }, + ]; + } + clickInvoiceButton() { return [{ content: 'click invoice button', trigger: '.payment-buttons .js_invoice' }]; } diff --git a/addons/pos_restaurant/__manifest__.py b/addons/pos_restaurant/__manifest__.py index 6b5e14d4b0c..6d7ac74b47b 100644 --- a/addons/pos_restaurant/__manifest__.py +++ b/addons/pos_restaurant/__manifest__.py @@ -46,6 +46,7 @@ This module adds several features to the Point of Sale that are specific to rest 'static/src/xml/Screens/FloorScreen/EditBar.xml', 'static/src/xml/Screens/FloorScreen/TableWidget.xml', 'static/src/xml/Screens/FloorScreen/EditableTable.xml', + 'static/src/xml/Screens/TipScreen.xml', 'static/src/xml/ChromeWidgets/BackToFloorButton.xml', 'static/src/xml/multiprint.xml', ], diff --git a/addons/pos_restaurant/models/__init__.py b/addons/pos_restaurant/models/__init__.py index 2b2442fbddd..09ed911c296 100644 --- a/addons/pos_restaurant/models/__init__.py +++ b/addons/pos_restaurant/models/__init__.py @@ -3,4 +3,5 @@ from . import pos_config from . import pos_order +from . import pos_payment from . import pos_restaurant diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index c2047671b2c..04002463778 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -14,6 +14,7 @@ class PosConfig(models.Model): printer_ids = fields.Many2many('restaurant.printer', 'pos_config_printer_rel', 'config_id', 'printer_id', string='Order Printers') is_table_management = fields.Boolean('Floors & Tables') is_order_printer = fields.Boolean('Order Printer') + set_tip_after_payment = fields.Boolean('Set Tip After Payment') module_pos_restaurant = fields.Boolean(default=True) @api.onchange('module_pos_restaurant') @@ -25,6 +26,11 @@ class PosConfig(models.Model): 'is_table_management': False, 'iface_orderline_notes': False}) + @api.onchange('iface_tipproduct') + def _onchange_iface_tipproduct(self): + if not self.iface_tipproduct: + self.set_tip_after_payment = False + def get_tables_order_count(self): """ """ self.ensure_one() diff --git a/addons/pos_restaurant/models/pos_order.py b/addons/pos_restaurant/models/pos_order.py index 644c53be8e7..0f081830c20 100644 --- a/addons/pos_restaurant/models/pos_order.py +++ b/addons/pos_restaurant/models/pos_order.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from itertools import groupby from re import search +from functools import partial from odoo import api, fields, models @@ -182,6 +183,36 @@ class PosOrder(models.Model): return table_orders + def set_tip(self, tip_line_vals): + """Set tip to `self` based on values in `tip_line_vals`.""" + + self.ensure_one() + PosOrderLine = self.env['pos.order.line'] + process_line = partial(PosOrderLine._order_line_fields, session_id=self.session_id.id) + + # 1. add/modify tip orderline + processed_tip_line_vals = process_line([0, 0, tip_line_vals])[2] + processed_tip_line_vals.update({ "order_id": self.id }) + tip_line = self.lines.filtered(lambda line: line.product_id == self.session_id.config_id.tip_product_id) + if not tip_line: + tip_line = PosOrderLine.create(processed_tip_line_vals) + else: + tip_line.write(processed_tip_line_vals) + + # 2. modify payment + payment_line = self.payment_ids.filtered(lambda line: not line.is_change)[0] + # TODO it would be better to throw error if there are multiple payment lines + # then ask the user to select which payment to update, no? + payment_line._update_payment_line_for_tip(tip_line.price_subtotal_incl) + + # 3. flag order as tipped and update order fields + self.write({ + "is_tipped": True, + "tip_amount": tip_line.price_subtotal_incl, + "amount_total": self.amount_total + tip_line.price_subtotal_incl, + "amount_paid": self.amount_paid + tip_line.price_subtotal_incl, + }) + @api.model def _order_fields(self, ui_order): order_fields = super(PosOrder, self)._order_fields(ui_order) diff --git a/addons/pos_restaurant/models/pos_payment.py b/addons/pos_restaurant/models/pos_payment.py new file mode 100644 index 00000000000..d4bfae1572f --- /dev/null +++ b/addons/pos_restaurant/models/pos_payment.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class PosConfig(models.Model): + _inherit = 'pos.payment' + + def _update_payment_line_for_tip(self, tip_amount): + """Inherit this method to perform reauthorization or capture on electronic payment.""" + self.ensure_one() + self.write({ + "amount": self.amount + tip_amount, + }) diff --git a/addons/pos_restaurant/static/src/css/restaurant.css b/addons/pos_restaurant/static/src/css/restaurant.css index c7cb17e3643..cd4ec3043fd 100644 --- a/addons/pos_restaurant/static/src/css/restaurant.css +++ b/addons/pos_restaurant/static/src/css/restaurant.css @@ -391,3 +391,159 @@ flex: none; } } + +.tip-screen .tip-options .total-amount { + text-align: center; + font-size: x-large; + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.tip-screen .tip-amount-options { + background: white; + border-radius: 3px; + margin-left: 4rem; + margin-right: 4rem; +} + +.tip-screen .percentage-amounts { + display: flex; +} + +.tip-screen .tip-options .button { + background: #FAFAFA; + border: solid 1px rgb(209, 209, 209); + border-radius: 3px; + flex: 1; + margin: 1rem; + display: flex; + flex-flow: column nowrap; + justify-content: center; + cursor: pointer; +} + +.tip-screen .tip-options .button .percentage { + text-align: center; + font-size: xx-large; + color: #2196F3; + padding-top: 4rem; + padding-bottom: 0.75rem; +} + +.tip-screen .tip-options .button .amount { + text-align: center; + font-size: large; + color: #757575; + padding-bottom: 4rem; +} + +.tip-screen .custom-amount-form { + display: flex; +} + +.tip-screen .custom-amount-form .item { + font-size: x-large; + color: #2196F3; + margin: 1rem; + flex: 1 +} + +.tip-screen .custom-amount-form .label { + padding-top: 1.5rem; + padding-bottom: 1.5rem; + text-align: center; +} + +.tip-screen .custom-amount-form .input { + background: #FAFAFA; + border: solid 1px rgb(209, 209, 209); + border-radius: 3px; + margin: 1rem; + text-align: right; + overflow: hidden; + position: relative; +} + +.tip-screen .custom-amount-form .input .currency { + position: absolute; + display: flex; + right: 0; + top: 0; + bottom: 0; + width: 3rem; + justify-content: center; + align-items: center; +} + +.tip-screen .custom-amount-form .input input { + border: none; + padding: 0; + outline: none; + font-size: x-large; + text-align: right; + margin-left: 1rem; + margin-right: 3rem; + height: 100%; + width: calc(100% - 4rem); + background: #FAFAFA; + color: #2196F3; +} + +.tip-screen .custom-amount-form .input input:focus { + border: none; + padding: 0; + outline: none; +} + +.tip-screen .custom-amount-form .add { + text-align: center; +} + +.tip-screen .no-tip { + display: flex; + color: #2196F3; + font-size: x-large; +} + +.tip-screen .no-tip .button { + text-align: center; + flex: 1; + padding: 1.5rem; +} + +.pos-receipt .tip-form { + padding-top: 10px; + padding-bottom: 10px; +} + +.pos-receipt .tip-form > div { + margin-top: 10px; +} + +.pos-receipt .tip-form .checkbox { + display: inline-block; + height: 25px; + width: 25px; + border: solid 1px black; + margin-right: 10px; + box-shadow: 2px 2px 0px 0px rgba(171,171,171,1); +} + +.pos-receipt .tip-form .option { + display: flex; + flex-flow: row nowrap; + align-items: center; + flex: 1; + justify-content: center; +} + +.pos-receipt .tip-form .option .percentage { + font-size: 10px; + color: #757575; +} + +.pos-receipt .tip-form .percentage-options { + display: flex; + flex-flow: row nowrap; + margin-bottom: 16px; +} 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 1bf06c838f5..1a4a1a2f14d 100644 --- a/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js @@ -271,7 +271,9 @@ odoo.define('pos_restaurant.FloorScreen', function (require) { .filter( (o) => o.server_id === undefined && - (o.orderlines.length !== 0 || o.paymentlines.length !== 0) + (o.orderlines.length !== 0 || o.paymentlines.length !== 0) && + // do not count the orders that are already finalized + !o.finalized ).length; table_obj.order_count = table.orders + unsynced_orders; }); diff --git a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js new file mode 100644 index 00000000000..f45d32ba6e2 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js @@ -0,0 +1,17 @@ +odoo.define('pos_restaurant.PosResPaymentScreen', function (require) { + 'use strict'; + + const PaymentScreen = require('point_of_sale.PaymentScreen'); + const Registries = require('point_of_sale.Registries'); + + const PosResPaymentScreen = (PaymentScreen) => + class extends PaymentScreen { + get nextScreen() { + return this.env.pos.config.set_tip_after_payment && !this.currentOrder.is_tipped ? 'TipScreen' : super.nextScreen; + } + }; + + Registries.Component.extend(PaymentScreen, PosResPaymentScreen); + + return PosResPaymentScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js index 5909292e0db..689c3bf40ca 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -20,18 +20,6 @@ odoo.define('pos_restaurant.ReceiptScreen', function(require) { return super.nextScreen; } } - /** - * If this order is synced to/from the server, then the draft needs to be deleted - * after the order is paid and the receipt screen is exited via 'Next Order' button. - * Thus, we set this order to be removed from server. Note that only the draft version - * is deleted if we trace the algorithm. - */ - orderDone() { - if (this.env.pos.config.iface_floorplan) { - this.env.pos.db.set_order_to_remove_from_server(this.currentOrder); - } - super.orderDone(); - } }; Registries.Component.extend(ReceiptScreen, PosResReceiptScreen); diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js new file mode 100644 index 00000000000..efbd83ea007 --- /dev/null +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -0,0 +1,80 @@ +odoo.define('pos_restaurant.TipScreen', function (require) { + 'use strict'; + + const Registries = require('point_of_sale.Registries'); + const PosComponent = require('point_of_sale.PosComponent'); + const { parse } = require('web.field_utils'); + const { useState } = owl.hooks; + + class TipScreen extends PosComponent { + constructor() { + super(...arguments); + this.state = useState({ inputCustomAmount: '' }); + this._totalAmount = this.currentOrder.get_total_with_tax(); + } + get totalAmount() { + return this._totalAmount; + } + get currentOrder() { + return this.env.pos.get_order(); + } + get percentageTips() { + return [ + { percentage: '15%', amount: 0.15 * this.totalAmount }, + { percentage: '20%', amount: 0.2 * this.totalAmount }, + { percentage: '25%', amount: 0.25 * this.totalAmount }, + ]; + } + async setTip(amount) { + const order = this.env.pos.get_order(); + const serverId = this.env.pos.validated_orders_name_server_id_map[order.name]; + + if (!serverId) { + this.showPopup('ErrorPopup', { + title: 'Unsynced order', + body: 'This order is not yet synced to server. Make sure it is synced then try again.', + }); + return; + } + + // set the tip by temporarily allowing order modification + order.finalized = false; + order.set_tip(amount); + order.finalized = true; + + // set_tip calls add_product which sets the new line as the selected_orderline + const tip_line = order.selected_orderline; + await this.rpc({ + method: 'set_tip', + model: 'pos.order', + args: [serverId, tip_line.export_as_JSON()], + }); + this.goNextScreen(); + } + async tipCustomAmount() { + const tipAmount = parse.float(this.state.inputCustomAmount); + if (tipAmount > 0.25 * this._totalAmount) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: 'Are you sure?', + body: `${this.env.pos.format_currency( + tipAmount + )} is more than 25% of the order's total amount. Are you sure of this tip amount?`, + }); + if (!confirmed) return; + } + await this.setTip(tipAmount); + } + goNextScreen() { + this.env.pos.get_order().finalize(); + this.showScreen(this.nextScreen); + } + get nextScreen() { + return 'FloorScreen'; + } + } + TipScreen.template = 'pos_restaurant.TipScreen'; + + Registries.Component.add(TipScreen); + + return TipScreen; +}); diff --git a/addons/pos_restaurant/static/src/js/floors.js b/addons/pos_restaurant/static/src/js/floors.js index 51ded242c63..807176d5fbc 100644 --- a/addons/pos_restaurant/static/src/js/floors.js +++ b/addons/pos_restaurant/static/src/js/floors.js @@ -201,7 +201,17 @@ models.PosModel = models.PosModel.extend({ var orders = this.get_order_list(); if (orders.length) { order = order ? orders.find((o) => o.uid === order.uid) : null; - this.set_order(order || orders[0]); + if (order) { + this.set_order(order); + } else { + // do not mindlessly set the first order in the list. + orders = orders.filter(order => !order.finalized); + if (orders.length) { + this.set_order(orders[0]); + } else { + this.add_new_order(); + } + } } else { this.add_new_order(); // or create a new order with the current table } @@ -215,12 +225,10 @@ models.PosModel = models.PosModel.extend({ this._get_from_server(table.id).then(function (server_orders) { var orders = self.get_order_list(); orders.forEach(function(order){ - const screen = order.get_screen_data(); - // Only remove the 'Receipt' status order when 'Next Order' - // from the receipt screen is clicked. We are bypassing the - // removal of paid order here to make sure those orders are - // still in the tickets list. - if (order.server_id && screen.name !== 'ReceiptScreen'){ + // We don't remove the validated orders because we still want to see them + // in the ticket screen. Orders in 'ReceiptScreen' or 'TipScreen' are validated + // orders. + if (order.server_id && !order.finalized){ self.get("orders").remove(order); order.destroy(); } @@ -339,7 +347,7 @@ models.PosModel = models.PosModel.extend({ // get customer count at table get_customer_count: function(table) { - var orders = this.get_table_orders(table); + var orders = this.get_table_orders(table).filter(order => !order.finalized); var count = 0; for (var i = 0; i < orders.length; i++) { count += orders[i].get_customer_count(); diff --git a/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml index 0c904ece232..34fe66e36ec 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml @@ -10,7 +10,9 @@ <span> </span> <span>Back</span> </span> - <h1>Bill Printing</h1> + <div class="top-content-center"> + <h1>Bill Printing</h1> + </div> <span class="button next highlight" t-on-click="confirm"> <span>Ok</span> <span> </span> diff --git a/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml index 8d195f6cca2..e8f129507e7 100644 --- a/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml +++ b/addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml @@ -10,6 +10,50 @@ <xpath expr="//div[hasclass('receipt-change')]" position="attributes"> <attribute name="t-if">!props.isBill</attribute> </xpath> + <xpath expr="//div[hasclass('before-footer')]" position="after"> + <t t-if="env.pos.config.set_tip_after_payment and !props.order.is_tipped"> + <div class="tip-form"> + <div>Add a tip:</div> + <div class="percentage-options"> + <div class="option"> + <div class="checkbox"></div> + <div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.15)"></t> + </div> + <div class="percentage">(15%)</div> + </div> + </div> + <div class="option"> + <div class="checkbox"></div> + <div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.20)"></t> + </div> + <div class="percentage">(20%)</div> + </div> + </div> + <div class="option"> + <div class="checkbox"></div> + <div> + <div class="amount"> + <t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.25)"></t> + </div> + <div class="percentage">(25%)</div> + </div> + </div> + </div> + <div class="custom-amount"> + <span>Custom amount:</span> + <div class="pos-receipt-right-align">________________________</div> + </div> + <div class="signature"> + <span>Signature:</span> + <div class="pos-receipt-right-align">________________________</div> + </div> + </div> + </t> + </xpath> </t> </templates> diff --git a/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml new file mode 100644 index 00000000000..467292a7646 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="pos_restaurant.TipScreen" owl="1"> + <div class="tip-screen screen"> + <div class="screen-content"> + <div class="top-content"> + <span class="button back" t-on-click="showScreen('FloorScreen')"> + <i class="fa fa-angle-double-left"></i> + <span> </span> + <span>Back</span> + </span> + <div class="top-content-center"> + <h1>Add a tip</h1> + </div> + </div> + <div class="tip-options"> + <div class="total-amount"> + <t t-esc="env.pos.format_currency(totalAmount)" /> + </div> + <div class="tip-amount-options"> + <div class="percentage-amounts"> + <t t-foreach="percentageTips" t-as="tip" t-key="tip.percentage"> + <div class="button" t-on-click="setTip(tip.amount)"> + <div class="percentage"> + <t t-esc="tip.percentage"></t> + </div> + <div class="amount"> + <t t-esc="env.pos.format_currency(tip.amount)" /> + </div> + </div> + </t> + </div> + <div class="custom-amount-form"> + <div class="item label">Custom Amount</div> + <div class="item input"> + <input type="text" t-model="state.inputCustomAmount" /> + <div class="currency"> + <t t-esc="env.pos.getCurrencySymbol()" /> + </div> + </div> + <div class="item add button" t-on-click="tipCustomAmount"> + <span>Add</span> + </div> + </div> + <div class="no-tip" t-on-click="goNextScreen"> + <div class="button">No Tip</div> + </div> + </div> + </div> + </div> + </div> + </t> + +</templates> diff --git a/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js new file mode 100644 index 00000000000..05c7532d83d --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js @@ -0,0 +1,113 @@ +odoo.define('pos_restaurant.tour.TipScreen', function (require) { + 'use strict'; + + const { ProductScreen } = require('point_of_sale.tour.ProductScreenTourMethods'); + const { PaymentScreen } = require('point_of_sale.tour.PaymentScreenTourMethods'); + const { ReceiptScreen } = require('point_of_sale.tour.ReceiptScreenTourMethods'); + const { FloorScreen } = require('pos_restaurant.tour.FloorScreenTourMethods'); + const { TicketScreen } = require('point_of_sale.tour.TicketScreenTourMethods'); + const { TipScreen } = require('pos_restaurant.tour.TipScreenTourMethods'); + const { NumberPopup } = require('point_of_sale.tour.NumberPopupTourMethods'); + const { Chrome } = require('pos_restaurant.tour.ChromeTourMethods'); + const { getSteps, startSteps } = require('point_of_sale.tour.utils'); + var Tour = require('web_tour.tour'); + + startSteps(); + + // Create order that is synced when draft. + // order 1 + FloorScreen.do.clickTable('T2'); + ProductScreen.exec.addOrderline('Minute Maid', '1', '2'); + ProductScreen.check.totalAmountIs('2.0'); + Chrome.do.backToFloor(); + FloorScreen.check.orderCountSyncedInTableIs('T2', '1'); + FloorScreen.do.clickTable('T2'); + ProductScreen.check.totalAmountIs('2.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + TipScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + // order 2 + ProductScreen.exec.addOrderline('Coca-Cola', '2', '2'); + ProductScreen.check.totalAmountIs('4.0'); + Chrome.do.backToFloor(); + FloorScreen.check.orderCountSyncedInTableIs('T2', '1'); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains('2', 'Tipping'); + TicketScreen.do.clickDiscard(); + + // Create without syncing the draft. + // order 3 + FloorScreen.do.clickTable('T5'); + ProductScreen.exec.addOrderline('Minute Maid', '3', '2'); + ProductScreen.check.totalAmountIs('6.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + TipScreen.check.isShown(); + Chrome.do.clickTicketButton(); + TicketScreen.do.clickNewTicket(); + // order 4 + ProductScreen.exec.addOrderline('Coca-Cola', '4', '2'); + ProductScreen.check.totalAmountIs('8.0'); + Chrome.do.backToFloor(); + FloorScreen.check.orderCountSyncedInTableIs('T5', '1'); + Chrome.do.clickTicketButton(); + TicketScreen.check.nthRowContains('4', 'Tipping'); + + // Tip 20% on order1 + TicketScreen.do.selectOrder('-0001'); + TipScreen.check.isShown(); + TipScreen.check.totalAmountIs('2.0'); + TipScreen.check.percentAmountIs('15%', '0.30'); + TipScreen.check.percentAmountIs('20%', '0.40'); + TipScreen.check.percentAmountIs('25%', '0.50'); + TipScreen.do.clickPercentTip('20%'); + FloorScreen.check.isShown(); + Chrome.do.clickTicketButton(); + + // Tip 25% on order3 + TicketScreen.do.selectOrder('-0003'); + TipScreen.check.isShown(); + TipScreen.check.totalAmountIs('6.0'); + TipScreen.check.percentAmountIs('15%', '0.90'); + TipScreen.check.percentAmountIs('20%', '1.20'); + TipScreen.check.percentAmountIs('25%', '1.50'); + TipScreen.do.clickPercentTip('25%'); + FloorScreen.check.isShown(); + Chrome.do.clickTicketButton(); + + // finalize order 4 then tip custom amount + TicketScreen.do.selectOrder('-0004'); + ProductScreen.check.totalAmountIs('8.0'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + TipScreen.check.isShown(); + TipScreen.check.totalAmountIs('8.0'); + TipScreen.check.percentAmountIs('15%', '1.20'); + TipScreen.check.percentAmountIs('20%', '1.60'); + TipScreen.check.percentAmountIs('25%', '2.00'); + TipScreen.do.setCustomTip('1.00'); + FloorScreen.check.isShown(); + + + // tip order2 during payment + // tip screen should not show after validating payment screen + Chrome.do.clickTicketButton(); + TicketScreen.check.checkStatus('-0002', 'Ongoing'); + TicketScreen.do.selectOrder('-0002'); + ProductScreen.do.clickPayButton(); + PaymentScreen.do.clickTipButton(); + NumberPopup.check.isShown(); + NumberPopup.do.pressNumpad('1'); + NumberPopup.check.inputShownIs('1'); + NumberPopup.do.clickConfirm(); + PaymentScreen.do.clickPaymentMethod('Cash'); + PaymentScreen.do.clickValidate(); + ReceiptScreen.check.isShown(); + + Tour.register('PosResTipScreenTour', { test: true, url: '/pos/web' }, getSteps()); +}); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/TipScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/TipScreenTourMethods.js new file mode 100644 index 00000000000..eb4104d4af8 --- /dev/null +++ b/addons/pos_restaurant/static/tests/tours/helpers/TipScreenTourMethods.js @@ -0,0 +1,55 @@ +odoo.define('pos_restaurant.tour.TipScreenTourMethods', function (require) { + 'use strict'; + + const { createTourMethods } = require('point_of_sale.tour.utils'); + + class Do { + clickPercentTip(percent) { + return [ + { + trigger: `.tip-screen .percentage:contains("${percent}")`, + }, + ]; + } + setCustomTip(amount) { + return [ + { + trigger: `.tip-screen .custom-amount-form input`, + run: `text ${amount}`, + }, + { + trigger: '.tip-screen .custom-amount-form .button', + }, + ]; + } + } + + class Check { + isShown() { + return [ + { + trigger: '.pos .tip-screen', + run: () => {}, + }, + ]; + } + totalAmountIs(amount) { + return [ + { + trigger: `.tip-screen .total-amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + percentAmountIs(percent, amount) { + return [ + { + trigger: `.tip-screen .percentage:contains("${percent}") ~ .amount:contains("${amount}")`, + run: () => {}, + }, + ]; + } + } + + return createTourMethods('TipScreen', Do, Check); +}); diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py index 2f30f2f2d49..f13cec07767 100644 --- a/addons/pos_restaurant/tests/test_frontend.py +++ b/addons/pos_restaurant/tests/test_frontend.py @@ -183,3 +183,17 @@ class TestFrontend(odoo.tests.HttpCase): def test_04_ticket_screen(self): self.pos_config.with_user(self.env.ref('base.user_admin')).open_session_cb(check_coa=False) self.start_tour("/pos/web?config_id=%d" % self.pos_config.id, 'PosResTicketScreenTour', login="admin", step_delay=50) + + def test_05_tip_screen(self): + self.pos_config.write({'set_tip_after_payment': True, 'iface_tipproduct': True, 'tip_product_id': self.env.ref('point_of_sale.product_product_tip')}) + 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, 'PosResTipScreenTour', login="admin", step_delay=50) + + order1 = self.env['pos.order'].search([('pos_reference', 'ilike', '%-0001')]) + order2 = self.env['pos.order'].search([('pos_reference', 'ilike', '%-0002')]) + order3 = self.env['pos.order'].search([('pos_reference', 'ilike', '%-0003')]) + order4 = self.env['pos.order'].search([('pos_reference', 'ilike', '%-0004')]) + self.assertTrue(order1.is_tipped and order1.tip_amount == 0.40) + self.assertTrue(order2.is_tipped and order2.tip_amount == 1.00) + self.assertTrue(order3.is_tipped and order3.tip_amount == 1.50) + self.assertTrue(order4.is_tipped and order4.tip_amount == 1.00) diff --git a/addons/pos_restaurant/views/pos_config_views.xml b/addons/pos_restaurant/views/pos_config_views.xml index 14fd1f2e5d7..b9c32ef0322 100644 --- a/addons/pos_restaurant/views/pos_config_views.xml +++ b/addons/pos_restaurant/views/pos_config_views.xml @@ -62,6 +62,23 @@ </div> </div> </div> + <div id="iface_tipproduct" position="after"> + <div class="col-12 col-lg-6 o_setting_box" + id="set_tip_after_payment" + title="Activate to add tip form in bill and set tip on order after payment." + attrs="{'invisible': ['|', ('module_pos_restaurant', '=', False), ('iface_tipproduct', '=', False)]}"> + <div class="o_setting_left_pane"> + <field name="set_tip_after_payment" attrs="{'readonly': [('has_active_session','=', True)]}"/> + </div> + <div class="o_setting_right_pane"> + <label for="set_tip_after_payment"/> + <span class="fa fa-lg fa-cutlery" title="Ask tips on bill" role="img" aria-label="Ask tips on bill"/> + <div class="text-muted"> + Ask tips on bill and set tip on order after payment. + </div> + </div> + </div> + </div> <div id="category_reference" position="before"> <div class="col-12 col-lg-6 o_setting_box" id="is_table_management" diff --git a/addons/pos_restaurant/views/pos_restaurant_templates.xml b/addons/pos_restaurant/views/pos_restaurant_templates.xml index c3d51a8bbc3..e10b3b3cc6d 100644 --- a/addons/pos_restaurant/views/pos_restaurant_templates.xml +++ b/addons/pos_restaurant/views/pos_restaurant_templates.xml @@ -29,6 +29,8 @@ <script type="text/javascript" src="/pos_restaurant/static/src/js/ChromeWidgets/TicketButton.js"></script> <script type="text/javascript" src="/pos_restaurant/static/src/js/Chrome.js"></script> <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/PaymentScreen.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/src/js/Screens/TipScreen.js"></script> </xpath> <xpath expr="//link[@id='pos-stylesheet']" position="after"> <link rel="stylesheet" href="/pos_restaurant/static/src/css/restaurant.css"/> @@ -50,12 +52,14 @@ <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/TextAreaPopupTourMethods.js"></script> <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/TextInputPopupTourMethods.js"></script> <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/BillScreenTourMethods.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/helpers/TipScreenTourMethods.js"></script> <script type="text/javascript" src="/pos_restaurant/static/tests/tours/pos_restaurant.js"></script> <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> <script type="text/javascript" src="/pos_restaurant/static/tests/tours/TicketScreen.tour.js"></script> + <script type="text/javascript" src="/pos_restaurant/static/tests/tours/TipScreen.tour.js"></script> </xpath> </template> |
