[IMP] pos_restaurant: swap payment and order button

Previously, the payment button was larger than the others and next to
the numpad. The other buttons were managed by a mixin and arranged
in a predefined order.

Now, the payment button only stays in place when the "order" button is
not present, currently it can only be present if the restaurant module
is installed and a printer configured.

When it is present, it takes the place of the payment button and the
latter is added to the mixins with the other buttons

closes odoo/odoo#115988

X-original-commit: f32c07f28e2fd2830f568757d4700fa77b83ced7
Related: odoo/enterprise#38476
Signed-off-by: Trinh Jacky (trj) <trj@odoo.com>
This commit is contained in:
David Monnom (moda)
2023-03-21 18:56:15 +01:00
parent 6273ac721c
commit a9608cdf1e
22 changed files with 284 additions and 125 deletions
@@ -1,10 +1,15 @@
/** @odoo-module */
import { Component } from "@odoo/owl";
import { usePos } from "@point_of_sale/app/pos_hook";
export class MobileOrderWidget extends Component {
static template = "MobileOrderWidget";
setup() {
super.setup(...arguments);
this.pos = usePos();
}
get order() {
return this.env.pos.get_order();
}
@@ -17,4 +22,11 @@ export class MobileOrderWidget extends Component {
? this.order.orderlines.reduce((items_number, line) => items_number + line.quantity, 0)
: 0;
}
clickPay() {
const order = this.pos.globalState.get_order();
if (order.orderlines.length) {
order.pay();
}
}
}
@@ -19,4 +19,16 @@ export class ActionpadWidget extends Component {
get isLongName() {
return this.props.partner && this.props.partner.name.length > 10;
}
clickPay() {
const order = this.pos.globalState.get_order();
if (order.orderlines.length) {
order.pay();
}
}
get highlightPay() {
return this.pos.globalState.get_order()?.orderlines?.length;
}
}
@@ -9,7 +9,6 @@ import { parse } from "web.field_utils";
import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { ControlButtonPopup } from "@point_of_sale/js/Popups/ControlButtonPopup";
import { ConnectionLostError } from "@web/core/network/rpc_service";
@@ -20,6 +19,7 @@ import { OrderWidget } from "./OrderWidget";
import { ProductsWidget } from "./ProductsWidget";
import { usePos } from "@point_of_sale/app/pos_hook";
import { Component, onMounted, useState } from "@odoo/owl";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
export class ProductScreen extends ControlButtonsMixin(Component) {
static template = "ProductScreen";
@@ -240,14 +240,15 @@ export class ProductScreen extends ControlButtonsMixin(Component) {
this.env.pos.get_order().server_id,
]);
if (isPaid) {
const searchDetails = { fieldName: "RECEIPT_NUMBER", searchTerm: this.env.pos.get_order().uid }
const searchDetails = {
fieldName: "RECEIPT_NUMBER",
searchTerm: this.env.pos.get_order().uid,
};
this.pos.showScreen("TicketScreen", {
ui: { filter: "SYNCED", searchDetails }
ui: { filter: "SYNCED", searchDetails },
});
this.notification.add(
_.str.sprintf(
this.env._t('The order has been already paid.')
),
_.str.sprintf(this.env._t("The order has been already paid.")),
3000
);
this.env.pos.removeOrder(this.env.pos.get_order(), false);
@@ -223,7 +223,7 @@ export class TicketScreen extends IndependentToOrderScreen {
async onDoRefund() {
const order = this.getSelectedSyncedOrder();
if (this._doesOrderHaveSoleItem(order)) {
if (order && this._doesOrderHaveSoleItem(order)) {
if (!this._prepareAutoRefundOnOrder(order)) {
// Don't proceed on refund if preparation returned false.
return;
@@ -12,6 +12,7 @@ import { ErrorPopup } from "./Popups/ErrorPopup";
import { ProductConfiguratorPopup } from "@point_of_sale/js/Popups/ProductConfiguratorPopup";
import { EditListPopup } from "@point_of_sale/js/Popups/EditListPopup";
import { markRaw, reactive } from "@odoo/owl";
import { ConfirmPopup } from "@point_of_sale/js/Popups/ConfirmPopup";
import { escape } from "@web/core/utils/strings";
var QWeb = core.qweb;
@@ -3115,6 +3116,28 @@ export class Order extends PosModel {
return receipt;
}
async pay() {
if (
this.orderlines.some(
(line) => line.get_product().tracking !== "none" && !line.has_valid_product_lot()
) &&
(this.pos.picking_type.use_create_lots || this.pos.picking_type.use_existing_lots)
) {
const { confirmed } = await this.pos.env.services.popup.add(ConfirmPopup, {
title: _t("Some Serial/Lot Numbers are missing"),
body: _t(
"You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?"
),
confirmText: _t("Yes"),
cancelText: _t("No"),
});
if (confirmed) {
this.pos.env.services.pos.showScreen("PaymentScreen");
}
} else {
this.pos.env.services.pos.showScreen("PaymentScreen");
}
}
is_empty() {
return this.orderlines.length === 0;
}
@@ -604,6 +604,8 @@ td {
.pos .control-buttons {
display: flex;
flex-flow: row wrap;
white-space: nowrap;
overflow: hidden;
}
.pos .popup-control-buttons .control-buttons {
flex-direction: column;
@@ -627,6 +629,7 @@ td {
cursor: pointer;
overflow: hidden;
transition: all linear 150ms;
text-overflow: ellipsis;
}
.pos .control-button:hover {
background: $gray-200;
@@ -3,7 +3,7 @@
<t t-name="MobileOrderWidget" owl="1">
<div class="switchpane">
<button class="btn-switchpane" t-on-click="() => this.props.clickPay()">
<button class="btn-switchpane" t-on-click="clickPay">
<h1>Pay</h1>
<span><t t-esc="total" /></span>
</button>
@@ -13,8 +13,8 @@
Customer
</t>
</button>
<button class="button pay validation" t-on-click="props.actionToTrigger ? this.props.actionToTrigger : () => this.props.clickPay()">
<div class="pay-circle" t-att-class="{ 'highlight': props.isActionButtonHighlighted }">
<button class="button pay validation pay-order-button" t-on-click="props.actionToTrigger ? this.props.actionToTrigger : () => this.clickPay()">
<div class="pay-circle" t-att-class="{ 'highlight': this.highlightPay }">
<i class="fa fa-chevron-right" role="img" aria-label="Pay" title="Pay" />
</div>
<t t-esc="props.actionName" />
@@ -20,7 +20,6 @@
<div class="subpads">
<t t-set="_actionName">Payment</t>
<ActionpadWidget
clickPay.bind="onClickPay"
partner="partner"
actionName="_actionName"
onSwitchPane.bind="switchPane" />
@@ -33,7 +32,6 @@
<div class="rightpane" t-if="!env.isMobile || state.mobile_pane === 'right'">
<ProductsWidget />
<MobileOrderWidget
clickPay.bind="onClickPay"
t-if="env.isMobile"
pane="state.mobile_pane"
onSwitchPane.bind="switchPane"/>
@@ -81,7 +81,7 @@ class Do {
clickPayButton(shouldCheck = true) {
const steps = [
{ content: "click pay button", trigger: ".product-screen .actionpad .button.pay" },
{ content: "click pay button", trigger: ".product-screen .pay-order-button" },
];
if (shouldCheck) {
steps.push({
@@ -327,6 +327,29 @@ patch(Order.prototype, "pos_loyalty.Order", {
this.codeActivatedProgramRules = json.codeActivatedProgramRules;
this.codeActivatedCoupons = json.codeActivatedCoupons;
},
async pay() {
const eWalletLine = this.get_orderlines().find(
(line) => line.getEWalletGiftCardProgramType() === "ewallet"
);
if (eWalletLine && !this.get_partner()) {
const { confirmed } = await this.pos.env.services.popup.add(ConfirmPopup, {
title: _t("Customer needed"),
body: _t("eWallet requires a customer to be selected"),
});
if (confirmed) {
const { confirmed, payload: newPartner } =
await this.pos.env.services.pos.showTempScreen("PartnerListScreen", {
partner: null,
});
if (confirmed) {
this.set_partner(newPartner);
this.updatePricelist(newPartner);
}
}
} else {
return this._super(...arguments);
}
},
/**
* We need to update the rewards upon changing the partner as it may impact the points available
* for rewards.
@@ -14,30 +14,6 @@ patch(ProductScreen.prototype, "pos_loyalty.ProductScreen", {
coupon: this._onCouponScan,
});
},
async onClickPay() {
const order = this.env.pos.get_order();
const eWalletLine = order
.get_orderlines()
.find((line) => line.getEWalletGiftCardProgramType() === "ewallet");
if (eWalletLine && !order.get_partner()) {
const { confirmed } = await this.popup.add(ConfirmPopup, {
title: this.env._t("Customer needed"),
body: this.env._t("eWallet requires a customer to be selected"),
});
if (confirmed) {
const { confirmed, payload: newPartner } = await this.pos.showTempScreen(
"PartnerListScreen",
{ partner: null }
);
if (confirmed) {
order.set_partner(newPartner);
order.updatePricelist(newPartner);
}
}
} else {
return this._super(...arguments);
}
},
_onCouponScan(code) {
// IMPROVEMENT: Ability to understand if the scanned code is to be paid or to be redeemed.
this.currentOrder.activateCode(code.base_code);
@@ -200,7 +200,6 @@
<field name="iface_splitbill">True</field>
<field name="iface_printbill">True</field>
<field name="iface_orderline_notes">True</field>
<field name="printer_ids" eval="[(6, 0, [ref('pos_restaurant.kitchen_printer')])]" />
<field name="iface_start_categ_id" ref="drinks"/>
<field name="start_category">True</field>
</record>
@@ -0,0 +1,47 @@
/** @odoo-module */
import { patch } from "@web/core/utils/patch";
import { ActionpadWidget } from "@point_of_sale/js/Screens/ProductScreen/ActionpadWidget";
/**
* @props partner
*/
patch(ActionpadWidget.prototype, "point_of_sale.ActionpadWidget", {
get swapButton() {
return (
this.props.actionName === "Payment" &&
this.pos.globalState.config.module_pos_restaurant &&
this.pos.globalState.printers_category_ids_set.size
);
},
get currentOrder() {
return this.pos.globalState.get_order();
},
get addedClasses() {
if (!this.currentOrder) {
return {};
}
const hasChanges = this.currentOrder.hasChangesToPrint();
const skipped = hasChanges ? false : this.currentOrder.hasSkippedChanges();
return {
highlight: hasChanges,
altlight: skipped,
};
},
async submitOrder() {
if (!this.clicked) {
this.clicked = true;
try {
this.currentOrder.submitOrder();
} finally {
this.clicked = false;
}
}
},
get highlightPay() {
return (
this._super(...arguments) &&
this.pos.globalState.printers_category_ids_set.size &&
!this.currentOrder.hasChangesToPrint()
);
},
});
@@ -0,0 +1,29 @@
/** @odoo-module */
import { usePos } from "@point_of_sale/app/pos_hook";
import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { Component } from "@odoo/owl";
export class CustomerButton extends Component {
static template = "pos_restaurant.CustomerButton";
setup() {
this.pos = usePos();
}
get partner() {
const order = this.pos.globalState.get_order();
return order ? order.get_partner() : null;
}
}
ProductScreen.addControlButton({
component: CustomerButton,
position: ["before", "SetFiscalPositionButton"],
condition: function () {
return (
this.pos.globalState.config.module_pos_restaurant &&
this.pos.globalState.printers_category_ids_set.size
);
},
});
@@ -1,64 +0,0 @@
/** @odoo-module */
import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
import { useService } from "@web/core/utils/hooks";
import { Component } from "@odoo/owl";
/**
* IMPROVEMENT: Perhaps this class is quite complicated for its worth.
* This is because it needs to listen to changes to the current order.
* Also, the current order changes when the selectedOrder in pos is changed.
* After setting new current order, we update the listeners.
*/
export class SubmitOrderButton extends Component {
static template = "SubmitOrderButton";
setup() {
super.setup();
this.popup = useService("popup");
this.clicked = false; //mutex, we don't want to be able to spam the printers
}
async _onClick() {
if (!this.clicked) {
try {
this.clicked = true;
const order = this.env.pos.get_order();
if (order.hasChangesToPrint()) {
const isPrintSuccessful = await order.printChanges();
if (isPrintSuccessful) {
order.updatePrintedResume();
} else {
this.popup.add(ErrorPopup, {
title: this.env._t("Printing failed"),
body: this.env._t("Failed in printing the changes in the order"),
});
}
}
} finally {
this.clicked = false;
}
}
}
get currentOrder() {
return this.env.pos.get_order();
}
get addedClasses() {
if (!this.currentOrder) {
return {};
}
const hasChanges = this.currentOrder.hasChangesToPrint();
const skipped = hasChanges ? false : this.currentOrder.hasSkippedChanges();
return {
highlight: hasChanges,
altlight: skipped,
};
}
}
ProductScreen.addControlButton({
component: SubmitOrderButton,
condition: function () {
return this.env.pos.addSubmitOrderButton();
},
});
@@ -138,11 +138,10 @@ patch(TicketScreen.prototype, "pos_restaurant.TicketScreen", {
return result;
},
async onDoRefund() {
if (this.env.pos.config.iface_floorplan) {
const order = this.getSelectedSyncedOrder();
if (this.env.pos.config.iface_floorplan && order) {
this.env.pos.setTable(
this.getSelectedSyncedOrder().table
? this.getSelectedSyncedOrder().table
: Object.values(this.env.pos.tables_by_id)[0]
order.table ? order.table : Object.values(this.env.pos.tables_by_id)[0]
);
}
this._super(...arguments);
+29 -6
View File
@@ -5,7 +5,10 @@ import { uuidv4, batched } from "@point_of_sale/js/utils";
import core from "web.core";
import { Printer } from "@point_of_sale/js/printers";
import { patch } from "@web/core/utils/patch";
import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup";
const QWeb = core.qweb;
const _t = core._t;
patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
setup() {
@@ -143,7 +146,10 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
return this._super(...arguments) && !this.transferredOrdersSet.has(order);
},
_shouldCreateOrder(json) {
return (!this._transferredOrder(json) || this._isSameTable(json)) && (!this.selectedOrder || this._super(...arguments));
return (
(!this._transferredOrder(json) || this._isSameTable(json)) &&
(!this.selectedOrder || this._super(...arguments))
);
},
_shouldRemoveSelectedOrder(removeSelected) {
return this.selectedOrder && this._super(...arguments);
@@ -156,12 +162,10 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", {
return transferredOrder && transferredOrder.tableId === json.tableId;
},
_transferredOrder(json) {
return [...this.transferredOrdersSet].find(
(order) => order.uid === json.uid
);
return [...this.transferredOrdersSet].find((order) => order.uid === json.uid);
},
_createOrder(json) {
const transferredOrder = this._transferredOrder(json)
const transferredOrder = this._transferredOrder(json);
if (this._isSameTable(json)) {
// this means we transferred back to the original table, we'll prioritize the server state
this.removeOrder(transferredOrder, false);
@@ -249,7 +253,12 @@ patch(Order.prototype, "pos_restaurant.Order", {
setup(options) {
this._super(...arguments);
if (this.pos.config.module_pos_restaurant) {
if (this.pos.config.iface_floorplan && !this.tableId && !options.json && this.pos.table) {
if (
this.pos.config.iface_floorplan &&
!this.tableId &&
!options.json &&
this.pos.table
) {
this.tableId = this.pos.table.id;
}
this.customerCount = this.customerCount || 1;
@@ -296,6 +305,20 @@ patch(Order.prototype, "pos_restaurant.Order", {
this.printingChanges = json.printing_changes && JSON.parse(json.printing_changes);
}
},
async submitOrder() {
if (this.pos.unwatched.printers.length) {
if (this.hasChangesToPrint()) {
const isPrintSuccessful = await this.printChanges();
if (!isPrintSuccessful) {
this.pos.env.services.popup.add(ErrorPopup, {
title: _t("Printing failed"),
body: _t("Failed in printing the changes in the order"),
});
}
}
}
this.updatePrintedResume();
},
//@override
export_for_printing() {
const json = this._super(...arguments);
@@ -1,5 +1,51 @@
/* ------ FLOOR BUTTON IN THE ORDER SELECTOR ------- */
.pos .actionpad .pay-order-button {
border: none;
border-bottom: solid 1px #C9CCD2;
}
.pos .actionpad .pay-order-button.highlight {
border-bottom-color: #C9CCD2;
}
.pos .actionpad .submit-order {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: calc(var(--btn-height-size) * 3);
border: none;
font-size: 14px;
font-weight: bold;
transition: all 150ms linear;
border-bottom: none;
}
@media screen and (max-width: 768px) {
.pos .actionpad .submit-order{
height: calc(var(--btn-height-size) * 2);
}
}
.pos .actionpad .submit-order.highlight {
background: $primary;
color: #fff;
}
.pos .actionpad .submit-order.highlight:hover, .pos .actionpad .pay-order-button.highlight:hover{
background: darken($primary, 5%);
}
.pos .submit-order .submit-circle {
display: block;
font-size: 20px;
line-height: var(--btn-height-size);
padding-top: 6px;
background: $gray-900;
color: white;
width: 60px;
text-align: center;
border-radius: 30px;
margin-bottom: 10px;
}
.pos .submit-order .submit-circle .fa {
position: relative;
top: -1px;
}
.pos .order-button.floor-button {
display: flex;
align-items: center;
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ActionpadWidget" t-inherit="point_of_sale.ActionpadWidget" t-inherit-mode="extension" owl="1">
<!-- Replace the payment button by the order button -->
<xpath expr="//button[hasclass('validation')]" position="after">
<button
t-if="this.swapButton"
class="button submit-order"
t-att-class="addedClasses"
t-on-click="submitOrder">
<div class="submit-circle">
<i class="fa fa-cutlery"></i>
</div>
Order
</button>
</xpath>
<xpath expr="//button[hasclass('validation')]" position="attributes">
<attribute name="t-if">!this.swapButton</attribute>
</xpath>
<!-- Replace the customer button by the payment button, the customer button will be added in the mixins -->
<xpath expr="//button[hasclass('set-partner')]" position="after">
<button t-on-click="clickPay" class="button pay-order-button" t-att-class="{ 'highlight': this.highlightPay }" t-if="this.swapButton">
<i class="fa fa-chevron-right" role="img" aria-label="Pay" title="Pay" />
Payment
</button>
</xpath>
<xpath expr="//button[hasclass('set-partner')]" position="attributes">
<attribute name="t-if">!this.swapButton</attribute>
</xpath>
</t>
</templates>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_restaurant.CustomerButton" owl="1">
<div class="control-button customer-button" t-on-click="() => this.pos.selectPartner()">
<i class="fa fa-user" role="img" aria-label="Customer" title="Customer" />
<t t-esc="this.partner.name" t-if="this.partner" />
<t t-else="">
Customer
</t>
</div>
</t>
</templates>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SubmitOrderButton" owl="1">
<span class="control-button" t-att-class="addedClasses" t-on-click="_onClick">
<i class="fa fa-cutlery"></i>
<span> </span>
<span>Order</span>
</span>
</t>
</templates>