summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorJoseph Caburnay <jcb@odoo.com>2020-07-31 09:36:39 +0000
committerJoseph Caburnay <jcb@odoo.com>2020-08-12 09:17:34 +0000
commitb0107e9230da5cb5c90819e5bee3e9cd323816ee (patch)
tree110e65cd9a180fbf782a70c392b9378a1f662e89 /addons
parent96f7c5eda84d6b7a1eade31efb8c35aff4ce94c4 (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')
-rw-r--r--addons/point_of_sale/models/pos_order.py6
-rw-r--r--addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js5
-rw-r--r--addons/point_of_sale/static/src/js/Screens/TicketScreen/TicketScreen.js1
-rw-r--r--addons/point_of_sale/static/src/js/models.js26
-rw-r--r--addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js8
-rw-r--r--addons/pos_restaurant/__manifest__.py1
-rw-r--r--addons/pos_restaurant/models/__init__.py1
-rw-r--r--addons/pos_restaurant/models/pos_config.py6
-rw-r--r--addons/pos_restaurant/models/pos_order.py31
-rw-r--r--addons/pos_restaurant/models/pos_payment.py15
-rw-r--r--addons/pos_restaurant/static/src/css/restaurant.css156
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/FloorScreen/FloorScreen.js4
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js17
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js12
-rw-r--r--addons/pos_restaurant/static/src/js/Screens/TipScreen.js80
-rw-r--r--addons/pos_restaurant/static/src/js/floors.js24
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/BillScreen.xml4
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml44
-rw-r--r--addons/pos_restaurant/static/src/xml/Screens/TipScreen.xml55
-rw-r--r--addons/pos_restaurant/static/tests/tours/TipScreen.tour.js113
-rw-r--r--addons/pos_restaurant/static/tests/tours/helpers/TipScreenTourMethods.js55
-rw-r--r--addons/pos_restaurant/tests/test_frontend.py14
-rw-r--r--addons/pos_restaurant/views/pos_config_views.xml17
-rw-r--r--addons/pos_restaurant/views/pos_restaurant_templates.xml4
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>