[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>
This commit is contained in:
Joseph Caburnay
2020-08-12 09:17:34 +00:00
parent 96f7c5eda8
commit b0107e9230
24 changed files with 673 additions and 26 deletions
+6
View File
@@ -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):
@@ -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', {
@@ -163,6 +163,7 @@ odoo.define('point_of_sale.TicketScreen', function (require) {
ProductScreen: 'Ongoing',
PaymentScreen: 'Payment',
ReceiptScreen: 'Receipt',
TipScreen: 'Tipping',
};
}
_initializeSearchFieldConstants() {
+23 -3
View File
@@ -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;
@@ -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' }];
}
+1
View File
@@ -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',
],
+1
View File
@@ -3,4 +3,5 @@
from . import pos_config
from . import pos_order
from . import pos_payment
from . import pos_restaurant
@@ -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()
+31
View File
@@ -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)
@@ -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,
})
@@ -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;
}
@@ -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;
});
@@ -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;
});
@@ -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);
@@ -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;
});
+16 -8
View File
@@ -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();
@@ -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>
@@ -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>
@@ -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>
@@ -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());
});
@@ -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);
});
@@ -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)
@@ -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"
@@ -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>