[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:
@@ -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() {
|
||||
|
||||
@@ -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' }];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user