diff --git a/addons/point_of_sale/static/src/app/navbar/BackButton.js b/addons/point_of_sale/static/src/app/navbar/BackButton.js new file mode 100644 index 00000000000..3f67cb88677 --- /dev/null +++ b/addons/point_of_sale/static/src/app/navbar/BackButton.js @@ -0,0 +1,19 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { usePos } from "@point_of_sale/app/pos_hook"; + +export class BackButton extends Component { + static template = "BackButton"; + + setup() { + super.setup(); + this.pos = usePos(); + this.ui = useState(useService("ui")); + } + async backToFloorScreen() { + this.pos.globalState.mobile_pane = "right"; + this.pos.showScreen("ProductScreen"); + } +} diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.js b/addons/point_of_sale/static/src/app/navbar/navbar.js index 5e793d75003..814309dbdde 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.js +++ b/addons/point_of_sale/static/src/app/navbar/navbar.js @@ -10,6 +10,7 @@ import { SaleDetailsButton } from "@point_of_sale/js/ChromeWidgets/SaleDetailsBu import { SyncNotification } from "@point_of_sale/js/ChromeWidgets/SyncNotification"; import { CashMovePopup } from "./cash_move_popup/cash_move_popup"; import { TicketScreen } from "@point_of_sale/js/Screens/TicketScreen/TicketScreen"; +import { BackButton } from "@point_of_sale/app/navbar/BackButton"; import { Component, useState, useExternalListener } from "@odoo/owl"; import { ClosePosPopup } from "@point_of_sale/js/Popups/ClosePosPopup"; @@ -22,6 +23,7 @@ export class Navbar extends Component { ProxyStatus, SaleDetailsButton, SyncNotification, + BackButton, }; static props = { showCashMoveButton: Boolean, @@ -101,4 +103,8 @@ export class Navbar extends Component { const info = await this.pos.globalState.getClosePosInfo(); this.popup.add(ClosePosPopup, { info, keepBehind: true }); } + + showBackButton() { + return this.pos.showBackButton() && this.ui.isSmall; + } } diff --git a/addons/point_of_sale/static/src/app/navbar/navbar.xml b/addons/point_of_sale/static/src/app/navbar/navbar.xml index 8ee47f6643d..decbfe16f96 100644 --- a/addons/point_of_sale/static/src/app/navbar/navbar.xml +++ b/addons/point_of_sale/static/src/app/navbar/navbar.xml @@ -4,10 +4,11 @@
-
- +
+
+
diff --git a/addons/point_of_sale/static/src/app/number_buffer_service.js b/addons/point_of_sale/static/src/app/number_buffer_service.js index e28972ac221..d2c7be2f0f1 100644 --- a/addons/point_of_sale/static/src/app/number_buffer_service.js +++ b/addons/point_of_sale/static/src/app/number_buffer_service.js @@ -313,11 +313,6 @@ class NumberBuffer extends EventBus { // it should not be in a start the buffer over state anymore. this.state.toStartOver = false; - if (this.config.maxValue && this.state.buffer > this.config.maxValue) { - this.state.buffer = this.config.maxValue.toString(); - this.config.maxValueReached(); - } - this.trigger("buffer-update", this.state.buffer); } } diff --git a/addons/point_of_sale/static/src/app/pos_store.js b/addons/point_of_sale/static/src/app/pos_store.js index 04292576d89..21ba4f59e3e 100644 --- a/addons/point_of_sale/static/src/app/pos_store.js +++ b/addons/point_of_sale/static/src/app/pos_store.js @@ -9,6 +9,8 @@ import { ErrorPopup } from "@point_of_sale/js/Popups/ErrorPopup"; import { _t } from "@web/core/l10n/translation"; import { CashOpeningPopup } from "@point_of_sale/js/Popups/CashOpeningPopup"; import { sprintf } from "@web/core/utils/strings"; +import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; export class PosStore extends Reactive { hasBigScrollBars = false; @@ -283,6 +285,13 @@ export class PosStore extends Reactive { false ); } + + showBackButton() { + return ( + this.mainScreen.component === PaymentScreen || + (this.mainScreen.component === ProductScreen && this.globalState.mobile_pane == "left") + ); + } } export const posService = { diff --git a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js index 3a43abac25b..0c48e37162b 100644 --- a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -3,7 +3,7 @@ import { _t } from "@web/core/l10n/translation"; import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { useService } from "@web/core/utils/hooks"; -import { useState } from "@odoo/owl"; +import { useState, useRef, onMounted } from "@odoo/owl"; export class NumberPopup extends AbstractAwaitablePopup { static template = "NumberPopup"; @@ -16,6 +16,7 @@ export class NumberPopup extends AbstractAwaitablePopup { cheap: false, startingValue: null, isPassword: false, + nbrDecimal: 0, inputSuffix: "", getInputBufferReminder: () => false, }; @@ -34,18 +35,32 @@ export class NumberPopup extends AbstractAwaitablePopup { super.setup(); this.localization = useService("localization"); let startingBuffer = ""; + let startingPayload = null; if (typeof this.props.startingValue === "number" && this.props.startingValue > 0) { startingBuffer = this.props.startingValue + .toFixed(this.props.nbrDecimal) .toString() .replace(".", this.decimalSeparator); + startingPayload = this.props.startingValue.toFixed(this.props.nbrDecimal); } - this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected }); + this.state = useState({ + buffer: startingBuffer, + toStartOver: this.props.isInputSelected, + payload: startingPayload, + }); this.numberBuffer = useService("number_buffer"); this.numberBuffer.use({ triggerAtEnter: () => this.confirm(), triggerAtEscape: () => this.cancel(), state: this.state, }); + this.inputRef = useRef("input"); + onMounted(this.onMounted); + } + onMounted() { + if (this.inputRef.el) { + this.inputRef.el.focus(); + } } get decimalSeparator() { return this.localization.decimalPoint; @@ -61,7 +76,7 @@ export class NumberPopup extends AbstractAwaitablePopup { } } confirm(event) { - if (this.numberBuffer.get()) { + if (this.numberBuffer.get() || this.state.payload) { super.confirm(); } } @@ -69,6 +84,16 @@ export class NumberPopup extends AbstractAwaitablePopup { this.numberBuffer.sendKey(key); } getPayload() { + let startingPayload = null; + if (typeof this.props.startingValue === "number" && this.props.startingValue > 0) { + startingPayload = this.props.startingValue.toFixed(this.props.nbrDecimal); + } + if (this.state.payload != startingPayload) { + return this.state.payload; + } return this.numberBuffer.get(); } + isMobile() { + return window.innerWidth <= 768; + } } diff --git a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js index ca3e876ca61..2f01b335932 100644 --- a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js +++ b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js @@ -1,12 +1,11 @@ /** @odoo-module */ import { AbstractAwaitablePopup } from "@point_of_sale/js/Popups/AbstractAwaitablePopup"; import { Component, useRef, useState, useSubEnv } from "@odoo/owl"; -import { usePos } from "@point_of_sale/app/pos_hook"; +import { useService } from "@web/core/utils/hooks"; export class BaseProductAttribute extends Component { setup() { super.setup(); - this.pos = usePos(); this.env.attribute_components.push(this); this.attribute = this.props.attribute; this.values = this.attribute.values; @@ -68,11 +67,16 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup { setup() { super.setup(); useSubEnv({ attribute_components: [] }); + this.state = useState({ + quantity: 1, + }); + this.ui = useService("ui"); } getPayload() { var selected_attributes = []; var price_extra = 0.0; + const quantity = this.state.quantity; this.env.attribute_components.forEach((attribute_component) => { const { value, extra } = attribute_component.getValue(); @@ -80,9 +84,33 @@ export class ProductConfiguratorPopup extends AbstractAwaitablePopup { price_extra += extra; }); + if (quantity > 1) { + return { + selected_attributes, + price_extra, + quantity, + }; + } + return { selected_attributes, price_extra, }; } + get imageUrl() { + const product = this.props.product; + return `/web/image?model=product.product&field=image_128&id=${product.id}&unique=${product.write_date}`; + } + get unitPrice() { + return this.env.utils.formatCurrency(this.props.product.lst_price); + } + addOneQuantity() { + ++this.state.quantity; + } + removeOneQuantity() { + if (this.state.quantity == 1) { + return; + } + --this.state.quantity; + } } 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 da48104c145..8d9d424cb66 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 @@ -59,15 +59,6 @@ export class PaymentScreen extends Component { // Note that the component listens to it. triggerAtInput: () => this.updateSelectedPaymentline(), }; - // Check if pos has a cash payment method - const hasCashPaymentMethod = this.payment_methods_from_config.some( - (method) => method.type === "cash" - ); - - if (!hasCashPaymentMethod) { - config["maxValue"] = this.currentOrder.get_due(); - config["maxValueReached"] = this.showMaxValueError.bind(this); - } return config; } @@ -109,25 +100,47 @@ export class PaymentScreen extends Component { return false; } } - updateSelectedPaymentline() { + updateSelectedPaymentline(amount = false) { if (this.paymentLines.every((line) => line.paid)) { this.currentOrder.add_paymentline(this.payment_methods_from_config[0]); } if (!this.selectedPaymentLine) { return; } // do nothing if no selected payment line + if (amount === false) { + if (this.numberBuffer.get() === null) { + amount = null; + } else if (this.numberBuffer.get() === "") { + amount = 0; + } else { + amount = this.numberBuffer.getFloat(); + } + } // disable changing amount on paymentlines with running or done payments on a payment terminal const payment_terminal = this.selectedPaymentLine.payment_method.payment_terminal; + const hasCashPaymentMethod = this.payment_methods_from_config.some( + (method) => method.type === "cash" + ); + if ( + !hasCashPaymentMethod && + this.currentOrder.get_due() + this.selectedPaymentLine.amount > 0 && + amount > this.currentOrder.get_due() + this.selectedPaymentLine.amount + ) { + this.selectedPaymentLine.set_amount(0); + this.numberBuffer.set(this.currentOrder.get_due().toString()); + amount = this.currentOrder.get_due(); + this.showMaxValueError(); + } if ( payment_terminal && !["pending", "retry"].includes(this.selectedPaymentLine.get_payment_status()) ) { return; } - if (this.numberBuffer.get() === null) { + if (amount === null) { this.deletePaymentLine(this.selectedPaymentLine.cid); } else { - this.selectedPaymentLine.set_amount(this.numberBuffer.getFloat()); + this.selectedPaymentLine.set_amount(amount); } } toggleIsToInvoice() { @@ -146,6 +159,7 @@ export class PaymentScreen extends Component { title: tip ? this.env._t("Change Tip") : this.env._t("Add Tip"), startingValue: value, isInputSelected: true, + nbrDecimal: this.pos.globalState.currency.decimal_places, inputSuffix: this.pos.globalState.currency.symbol, }); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js index dfd0e867514..d8f24df7f0d 100644 --- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js @@ -1,10 +1,20 @@ /** @odoo-module */ -import { Component } from "@odoo/owl"; +import { NumberPopup } from "@point_of_sale/js/Popups/NumberPopup"; +import { useService } from "@web/core/utils/hooks"; +import { Component, useState } from "@odoo/owl"; +import { usePos } from "@point_of_sale/app/pos_hook"; export class PaymentScreenPaymentLines extends Component { static template = "PaymentScreenPaymentLines"; + setup() { + super.setup(); + this.ui = useState(useService("ui")); + this.popup = useService("popup"); + this.pos = usePos(); + } + formatLineAmount(paymentline) { return this.env.utils.formatCurrency(paymentline.get_amount(), false); } @@ -14,4 +24,21 @@ export class PaymentScreenPaymentLines extends Component { unselectedLineClass(line) { return {}; } + async selectLine(paymentline) { + this.props.selectLine(paymentline.cid); + + if (this.ui.isSmall) { + const { confirmed, payload } = await this.popup.add(NumberPopup, { + title: this.env._t("New amount"), + startingValue: parseFloat(paymentline.amount), + isInputSelected: true, + nbrDecimal: this.pos.globalState.currency.decimal_places, + }); + + if (confirmed) { + this.props.updateSelectedPaymentline(parseFloat(payload)); + } + } + return; + } } diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js index d1391274170..fe9b763024d 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/Orderline.js @@ -15,7 +15,6 @@ export class Orderline extends Component { setup() { this.pos = usePos(); } - selectLine() { const line = this.props.line; // the orderline if (this.pos.globalState.get_order().selected_orderline.id !== line.id) { diff --git a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js index 04eae8674c9..db63d02dada 100644 --- a/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/point_of_sale/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -13,7 +13,7 @@ import { ControlButtonPopup } from "@point_of_sale/js/Popups/ControlButtonPopup" import { ConnectionLostError } from "@web/core/network/rpc_service"; import { usePos } from "@point_of_sale/app/pos_hook"; -import { Component, onMounted, useState } from "@odoo/owl"; +import { Component, onMounted, useState, useRef } from "@odoo/owl"; import { ErrorBarcodePopup } from "@point_of_sale/js/Popups/ErrorBarcodePopup"; import { NumpadWidget } from "./NumpadWidget"; @@ -39,6 +39,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this.orm = useService("orm"); this.notification = useService("pos_notification"); this.numberBuffer = useService("number_buffer"); + this.reminderRef = useRef("reminder"); onMounted(this.onMounted); useBarcodeReader({ @@ -49,10 +50,6 @@ export class ProductScreen extends ControlButtonsMixin(Component) { discount: this._barcodeDiscountAction, }); - this.state = useState({ - mobile_pane: this.props.mobile_pane || "right", - }); - // Call `resset` when the `onMounted` callback in `numberBuffer.use` is done. // We don't do this in the `mounted` lifecycle method because it is called before // the callbacks in `onMounted` hook. @@ -198,7 +195,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { last_orderline.set_discount(code.value); } } - async _displayAllControlPopup() { + async displayAllControlPopup() { await this.popup.add(ControlButtonPopup, { controlButtons: this.controlButtons, }); @@ -233,6 +230,40 @@ export class ProductScreen extends ControlButtonsMixin(Component) { order.add_orderline(newLine); } } + get selectedOrderlineQuantity() { + return this.currentOrder.get_selected_orderline()?.get_quantity_str(); + } + get selectedOrderlineDisplayName() { + return this.currentOrder.get_selected_orderline()?.get_full_product_name(); + } + get selectedOrderlineTotal() { + return this.env.utils.formatCurrency( + this.currentOrder.get_selected_orderline()?.get_display_price() + ); + } + /** + * This getter is used to restart the animation on the product-reminder. + * When the information present on the product-reminder will change, + * the key will change and thus a new product-reminder will be created + * and the old one will be garbage collected leading to the animation + * being retriggered. + */ + get animationKey() { + return [ + this.selectedOrderlineQuantity, + this.selectedOrderlineDisplayName, + this.selectedOrderlineTotal, + ].join(","); + } + get showProductReminder() { + return this.currentOrder.get_selected_orderline() && this.selectedOrderlineQuantity; + } + primaryPayButton() { + return !this.currentOrder.is_empty(); + } + primaryReviewButton() { + return !this.primaryPayButton() && !this.currentOrder.is_empty(); + } // FIXME POSREF this is dead code, check if we need the business logic that's left in here // If we do it should be in the model. async onClickPay() { @@ -266,7 +297,7 @@ export class ProductScreen extends ControlButtonsMixin(Component) { this.currentOrder.pay(); } switchPane() { - this.state.mobile_pane = this.state.mobile_pane === "left" ? "right" : "left"; + this.pos.globalState.switchPane(); } } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 30816127b6a..a9339ed6ada 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -167,6 +167,7 @@ export class PosGlobalState extends PosModel { this.validated_orders_name_server_id_map = {}; this.numpadMode = "quantity"; + this.mobile_pane = "right"; // Record this.toRefundLines = {}; @@ -1528,6 +1529,9 @@ export class PosGlobalState extends PosModel { isOpenOrderShareable() { return this.config.trusted_config_ids.length > 0; } + switchPane() { + this.mobile_pane = this.mobile_pane === "left" ? "right" : "left"; + } } PosGlobalState.prototype.electronic_payment_interfaces = {}; @@ -1580,6 +1584,7 @@ export class Product extends PosModel { async getAddProductOptions(base_code) { let price_extra = 0.0; let draftPackLotLines, weight, description, packLotLinesToEdit; + let quantity = 1; if (this.attribute_line_ids.some((id) => id in this.pos.attributes_by_ptal_id)) { const attributes = this.attribute_line_ids @@ -1596,6 +1601,9 @@ export class Product extends PosModel { if (confirmed) { description = payload.selected_attributes.join(", "); price_extra += payload.price_extra; + if (payload.quantity) { + quantity = payload.quantity; + } } else { return; } @@ -1667,7 +1675,7 @@ export class Product extends PosModel { weight = this.pos.db.product_packaging_by_barcode[base_code.code].qty; } - return { draftPackLotLines, quantity: weight, description, price_extra }; + return { draftPackLotLines, quantity, weight, description, price_extra }; } isPricelistItemUsable(item, date) { const categories = this.parent_category_ids.concat(this.categ.id); @@ -1803,7 +1811,7 @@ export class Orderline extends PosModel { this.product = options.product; this.tax_ids = options.tax_ids; this.set_product_lot(this.product); - this.set_quantity(1); + options.quantity ? this.set_quantity(options.quantity) : this.set_quantity(1); this.discount = 0; this.note = ""; this.hasChange = false; @@ -3069,12 +3077,13 @@ export class Order extends PosModel { * it uses the variable lastOrderPrepaChange which contains the last changes sent * to perform this calculation. */ - getOrderChanges() { + getOrderChanges(skipped = false) { const prepaCategoryIds = this.pos.orderPreparationCategories; const oldChanges = this.lastOrderPrepaChange; const changes = {}; + let changesCount = 0; - if (!prepaCategoryIds.size) { + if (!skipped && !prepaCategoryIds.size) { return {}; } @@ -3083,10 +3092,6 @@ export class Order extends PosModel { for (const orderlineIdx in this.orderlines) { const orderline = this.orderlines[orderlineIdx]; - if (orderline.skipChange || orderline.refunded_orderline_id) { - continue; - } - const product = orderline.get_product(); const note = orderline.getNote(); const productKey = `${product.id} - ${orderline.get_full_product_name()} - ${note}`; @@ -3098,14 +3103,18 @@ export class Order extends PosModel { ? quantity - oldChanges[lineKey].quantity : quantity; - if (quantityDiff) { + if (quantityDiff && orderline.skipChange === skipped) { changes[productKey] = { name: orderline.get_full_product_name(), product_id: product.id, quantity: quantityDiff, note: note, }; - orderline.setHasChange(true); + changesCount += quantityDiff; + + if (!orderline.skipChange) { + orderline.setHasChange(true); + } } else { orderline.setHasChange(false); } @@ -3132,7 +3141,10 @@ export class Order extends PosModel { } } - return changes; + return { + orderlines: changes, + count: changesCount, + }; } // This function transforms the data generated by getOrderChanges into the old // pattern used by the printer and the display preparation. This old pattern comes from @@ -3141,7 +3153,7 @@ export class Order extends PosModel { const toAdd = []; const toRemove = []; const changes = !cancelled - ? Object.values(this.getOrderChanges()) + ? Object.values(this.getOrderChanges().orderlines) : Object.values(this.lastOrderPrepaChange); for (const lineChange of changes) { @@ -3166,7 +3178,7 @@ export class Order extends PosModel { return this.orderlines.find((orderline) => orderline.skipChange) ? true : false; } hasChangesToPrint() { - return Object.keys(this.getOrderChanges()).length ? true : false; + return this.getOrderChanges().count ? true : false; } async pay() { if (!this.orderlines.length) { @@ -3187,9 +3199,11 @@ export class Order extends PosModel { cancelText: _t("No"), }); if (confirmed) { + this.pos.mobile_pane = "right"; this.pos.env.services.pos.showScreen("PaymentScreen"); } } else { + this.pos.mobile_pane = "right"; this.pos.env.services.pos.showScreen("PaymentScreen"); } } diff --git a/addons/point_of_sale/static/src/scss/pos.scss b/addons/point_of_sale/static/src/scss/pos.scss index 3aab4b96c42..8c8df7764ee 100644 --- a/addons/point_of_sale/static/src/scss/pos.scss +++ b/addons/point_of_sale/static/src/scss/pos.scss @@ -367,71 +367,22 @@ td { width: 25px; border-radius: 50%; } - -.pos .order-button{ +.pos .order-button.floor-button { color: #f0f0f0; - display: inline-block; - box-sizing: border-box; - -moz-box-sizing: border-box; - height: 46px; - padding: 4px 8px; - margin: 3px; - margin-bottom: 0px; - margin-right: 2px; - padding-top: 0px; - background: #8b8b8b; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - vertical-align: top; - line-height: 42px; - text-align: center; - box-shadow: 0px -5px 10px -6px rgb(82,82,82) inset; cursor: pointer; - min-width: 45px; -} - -.pos .order-button.selected{ - font-weight: 900; - background: #EEEEEE; - color: rgb(75,75,75); - height: 45px; - border-bottom: solid 1px rgb(196, 196, 196); - box-shadow: none; - -webkit-flex-shrink: 0; - flex-shrink: 0; -} - -.pos .order-button .order-sequence{ + display: flex; + gap: 7px; + align-items: center; + font-weight: bold; font-size: 16px; - font-weight: 800; - vertical-align: middle; + padding-left: 16px; + padding-right: 16px; + overflow-wrap: anywhere; + text-overflow: hidden; } -.pos .order-button.selected .order-sequence{ - color: white; - background: black; - display: inline-block; - line-height: 24px; - min-width: 24px; - border-radius: 12px; - margin-right: 4px; - margin-left: -4px; -} - -.pos .order-button.square{ - margin-left:1px; - background: #5c5c5c; - color: rgb(160,160,160); - font-size: 18px; - line-height: 45px; -} -.pos .order-button:not(.square) > .fa { - font-size: 16px; - vertical-align: middle; - margin-right: 4px; -} -.pos .order-button .order-sequence{ - font-size: 16px; - font-weight: 800; +.pos .order-button.floor-button .fa{ + font-size: 24px; + margin-bottom: 3px; } .pos .order-selector { @@ -481,35 +432,64 @@ td { /* ********* The leftpane contains the order, numpad and paypad ********* */ +.pos .product-reminder { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + height: 30px; + background: #F7EFCA; + font-size: 12px; + font-weight: bold; + gap: 9px; + + span { + animation: 0.7s anim-lineUp ease-out 1; + margin: 9px 0; + } + + @keyframes anim-lineUp { + 0% { + opacity: 0; + transform: translateY(80%); + } + 20% { + opacity: 0; + } + 50% { + opacity: 1; + transform: translateY(0%); + } + 100% { + opacity: 1; + transform: translateY(0%); + } + } +} + .pos .switchpane { display: flex; } -.pos .switchpane .order-info { - flex-grow: 1; - border-top: 1px solid #ebebeb; - padding: 8px 16px; - background-color: #FFFFFF; - color: $primary; -} -.pos .switchpane .order-info h2 { - padding: 0px; - margin: 3px 0px; - font-weight: bold; -} - .pos .switchpane .btn-switchpane { + height: 70px; + width: 100%; background-color: $primary; border-radius: 0px; color: #FFFFFF; font-size: 15px; font-weight: bold; flex-grow: 1; - flex-basis: 50%; - padding-bottom: 20px; -} -.pos .switchpane .btn-switchpane h1 { - margin-bottom: 0px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + border: none; + + h1 { + font-size: 20px; + margin: 0; + } } .pos .switchpane .btn-switchpane.secondary { @@ -517,6 +497,14 @@ td { color: $primary; } +.pos .switchpane .btn-switchpane.validation-button { + justify-content: center; + font-size: 20px; + font-weight: bold; + border-left: solid 1px white; +} + + .pos .pane-border { border-right: solid 3px $gray-300; } @@ -701,6 +689,13 @@ td { padding: 14px; } +.pos .actionpad .button.mobile-more-button { + padding-left: 40px; + padding-right: 40px; + border: none; + border-bottom: solid 1px #C9CCD2; +} + .pos .actionpad .button.set-partner{ padding-left: 40px; padding-right: 40px; @@ -838,7 +833,11 @@ td { .pos .rightpane-header .categories-header { display: flex; - margin-right: 48px; + overflow-x: auto; + + &::-webkit-scrollbar { + display: none; + } } .pos .rightpane-header .breadcrumbs { @@ -861,6 +860,11 @@ td { padding: 3px; gap: 3px; } +@media only screen and (max-width: 768px) { + .pos .product-list { + grid-template-columns: 1fr 1fr 1fr; + } +} .pos .product-list-container .product-list-empty p { margin: 10px; text-align: center; @@ -1178,6 +1182,10 @@ td { -webkit-line-clamp: 2; } +.pos .product .product-name.no-image { + -webkit-line-clamp: 9; +} + /* ********* The BlockUI Component (frontend) ********* */ .o_blockUI { position: fixed; @@ -1301,7 +1309,7 @@ td { bottom: 0; height: auto; width: 100%; - padding: map-get($spacers, 5) 0; + padding: 48px 0; } .screen .button.next:not(.highlight) { cursor: not-allowed; @@ -1489,20 +1497,24 @@ td { flex-basis: 100%; min-height: 60px; overflow: auto; + align-items: center; } .paymentline:active { background: black; border-color: black; color: white; } -.paymentline .payment-name { +.paymentline .payment-infos { flex-grow: 1; /* make the name of the payment line to be truncated with ellipsis */ white-space: nowrap; text-overflow:ellipsis; padding: map-get($spacers, 3); + display: flex; + align-items: center; + justify-content: space-between; } -.paymentline .payment-amount { +.paymentline .payment-infos .payment-amount { padding: map-get($spacers, 3); } .paymentline .delete-button { @@ -1648,7 +1660,8 @@ td { } .pos .payment-buttons .button { max-width: inherit; - text-align: center; + display: flex; + align-items: center; } .pos .paymentlines-container { min-height: auto; @@ -2566,6 +2579,9 @@ td { padding: 16px; white-space: pre-line; } +.pos .popup .payment-input-number { + text-align: right; +} .pos .popup-med .body { max-height: 400px; @@ -2863,6 +2879,97 @@ td { bottom: 0.2em; } +.pos .modal-dialog .popup.mobile{ + bottom: 0; + position: absolute; + width: 100%; + font-size:20px; + font-weight:bold; + background-color: #fff; + z-index:1200; + font-family: 'Lato'; + font-family: Lato; + margin: 0; + max-height: none; + max-width: none; + transform: none; + top: auto; + left: auto; +} + +.pos .modal-dialog .popup-med.mobile{ + max-width: 700px; + height: calc(100% - 48px); +} + +.pos .popup-med .body.mobile { + overflow-y: auto; + height: calc(100% - 133px); + max-height: none; + margin-bottom: 0; +} + +.pos .popup .footer.mobile { + display: flex; + flex-direction: column; + align-items: center; +} + +.pos .popup .product_configurator_attributes .product-img { + display: flex; + flex-direction: column; + align-items: center; + height: 50%; + padding: 10px; + + & img { + height: 100%; + } + + & .product-info { + display: flex; + width: 100%; + justify-content: space-between; + } +} + +.pos .popup .footer .confirm-line { + display: flex; + width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 48px; + background: #FFF; + font-size: 12px; + font-weight: bold; + margin: 5px; + + div { + margin: 0; + } +} + +.pos .popup .footer .quantity-selector { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + height: 48px; + background: #FFF; + font-size: 12px; + font-weight: bold; + width: 100%; + border-bottom: 1px solid black; + + button { + margin: 5px; + } +} + +.pos .popup .footer .quantity-selector .quantity{ + margin: 0 10px; +} /* ********* The Webkit Scrollbar ********* */ diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/BackButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/BackButton.xml new file mode 100644 index 00000000000..7f4abd1b696 --- /dev/null +++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/BackButton.xml @@ -0,0 +1,14 @@ + + + + + + + BACK + + + + diff --git a/addons/point_of_sale/static/src/xml/Popups/NumberPopup.xml b/addons/point_of_sale/static/src/xml/Popups/NumberPopup.xml index 6a923540a4d..b51d6f11da8 100644 --- a/addons/point_of_sale/static/src/xml/Popups/NumberPopup.xml +++ b/addons/point_of_sale/static/src/xml/Popups/NumberPopup.xml @@ -2,7 +2,7 @@ - + diff --git a/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml b/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml index a49ca5225b3..6ce367448a7 100644 --- a/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml +++ b/addons/point_of_sale/static/src/xml/Popups/ProductConfiguratorPopup.xml @@ -1,39 +1,13 @@ - - - -
+ t-attf-id="{{ attribute.id }}_{{ value.id }}" t-att-value="value.id"/>
@@ -86,4 +60,71 @@
+ + + + + + + + diff --git a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml index 9c5d3891649..949bc5b633a 100644 --- a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml +++ b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -42,7 +42,8 @@ sendPaymentReverse.bind="sendPaymentReverse" sendPaymentCancel.bind="sendPaymentCancel" sendPaymentRequest.bind="sendPaymentRequest" - selectLine.bind="selectPaymentLine"/> + selectLine.bind="selectPaymentLine" + updateSelectedPaymentline.bind="updateSelectedPaymentline"/>
-
- -
- -
-
+
+
+ +
@@ -110,13 +109,10 @@
- -
diff --git a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreenPaymentLines.xml b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreenPaymentLines.xml index 487acbfc8ab..94191617b60 100644 --- a/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreenPaymentLines.xml +++ b/addons/point_of_sale/static/src/xml/Screens/PaymentScreen/PaymentScreenPaymentLines.xml @@ -7,19 +7,19 @@
-
- -
-
- - - - - - + t-att-class="selectedLineClass(line)"> +
+ +
+ + + + + + +
+
-
+ t-att-class="unselectedLineClass(line)"> +
+
+ + + + + + +
-
- - - - - - -
+
-
diff --git a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductScreen.xml b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductScreen.xml index d61318901eb..e40a7c15636 100644 --- a/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductScreen.xml +++ b/addons/point_of_sale/static/src/xml/Screens/ProductScreen/ProductScreen.xml @@ -4,14 +4,11 @@
-
+
- -
More...
-
- + @@ -22,25 +19,31 @@ partner="partner" actionName="constructor.numpadActionName" actionType="'payment'" - onSwitchPane.bind="switchPane" /> + onClickMore.bind="displayAllControlPopup" />
-
+
-
- - -
+ +
+ + +
+
+ + +
+
diff --git a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml index 3c24b264326..de5be060010 100644 --- a/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml +++ b/addons/point_of_sale/static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml @@ -40,8 +40,8 @@
-
- + +
+
+
+ New Order +
+
+ Resume Order +
+ +
diff --git a/addons/point_of_sale/static/tests/tours/Chrome.tour.js b/addons/point_of_sale/static/tests/tours/Chrome.tour.js index 4cf89092d29..2067ee8501b 100644 --- a/addons/point_of_sale/static/tests/tours/Chrome.tour.js +++ b/addons/point_of_sale/static/tests/tours/Chrome.tour.js @@ -65,7 +65,7 @@ TicketScreen.do.selectOrder("-0001"); ProductScreen.check.isShown(); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod("Cash"); -PaymentScreen.do.pressNumpad("2 0"); +PaymentScreen.do.enterPaymentLineAmount("Cash", "20"); PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(true); PaymentScreen.do.clickValidate(); diff --git a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js index 25634e9398d..0e51fcb3fc2 100644 --- a/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/PaymentScreen.tour.js @@ -15,15 +15,17 @@ ProductScreen.do.clickPayButton(); PaymentScreen.check.emptyPaymentlines("52.8"); PaymentScreen.do.clickPaymentMethod("Cash"); -PaymentScreen.do.pressNumpad("1 1"); +PaymentScreen.do.enterPaymentLineAmount("Cash", "11"); PaymentScreen.check.selectedPaymentlineHas("Cash", "11.00"); PaymentScreen.check.remainingIs("41.8"); PaymentScreen.check.changeIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(false); // remove the selected paymentline with multiple backspace presses PaymentScreen.do.pressNumpad("Backspace Backspace"); +PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "0"); PaymentScreen.check.selectedPaymentlineHas("Cash", "0.00"); PaymentScreen.do.pressNumpad("Backspace"); +PaymentScreen.do.clickPaymentlineDelButton("Cash", "0", true); PaymentScreen.check.emptyPaymentlines("52.8"); // Pay with bank, the selected line should have full amount @@ -37,10 +39,12 @@ PaymentScreen.do.clickPaymentlineDelButton("Bank", "52.8"); // Use +10 and +50 to increment the amount of the paymentline PaymentScreen.do.clickPaymentMethod("Cash"); PaymentScreen.do.pressNumpad("+10"); +PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "10"); PaymentScreen.check.remainingIs("42.8"); PaymentScreen.check.changeIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(false); PaymentScreen.do.pressNumpad("+50"); +PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "60"); PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.changeIs("7.2"); PaymentScreen.check.validateButtonIsHighlighted(true); @@ -49,20 +53,17 @@ PaymentScreen.do.clickPaymentlineDelButton("Cash", "60.0"); // Multiple paymentlines PaymentScreen.do.clickPaymentMethod("Cash"); PaymentScreen.do.pressNumpad("1"); +PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "1"); PaymentScreen.check.remainingIs("51.8"); PaymentScreen.check.changeIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(false); -PaymentScreen.do.clickPaymentMethod("Cash"); +PaymentScreen.do.clickPaymentMethod("Bank"); +PaymentScreen.do.fillPaymentLineAmountMobile("Bank", "5"); PaymentScreen.do.pressNumpad("5"); PaymentScreen.check.remainingIs("46.8"); PaymentScreen.check.changeIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(false); PaymentScreen.do.clickPaymentMethod("Bank"); -PaymentScreen.do.pressNumpad("2 0"); -PaymentScreen.check.remainingIs("26.8"); -PaymentScreen.check.changeIs("0.0"); -PaymentScreen.check.validateButtonIsHighlighted(false); -PaymentScreen.do.clickPaymentMethod("Bank"); PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.changeIs("0.0"); PaymentScreen.check.validateButtonIsHighlighted(true); @@ -78,7 +79,7 @@ ProductScreen.exec.addOrderline("Letter Tray", "1", "10"); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod("Bank"); -PaymentScreen.do.pressNumpad("1 0 0 0"); +PaymentScreen.do.enterPaymentLineAmount("Bank", "1000"); PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.changeIs("0.0"); @@ -196,8 +197,7 @@ ProductScreen.do.clickPayButton(); PaymentScreen.check.totalIs("1.95"); PaymentScreen.do.clickPaymentMethod("Cash"); -PaymentScreen.do.pressNumpad("5"); - +PaymentScreen.do.enterPaymentLineAmount("Cash", "5"); PaymentScreen.check.remainingIs("0.0"); PaymentScreen.check.changeIs("3.05"); PaymentScreen.check.totalDueIs("1.95"); diff --git a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js index 523ba1d7d46..2d400b5d6b4 100644 --- a/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/ReceiptScreen.tour.js @@ -30,9 +30,10 @@ ProductScreen.exec.addOrderline("Whiteboard Pen", "6", "6", "36.0"); ProductScreen.exec.addOrderline("Monitor Stand", "6", "1", "6.0"); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod("Cash"); -PaymentScreen.do.pressNumpad("7 0"); +PaymentScreen.do.enterPaymentLineAmount("Cash", "70"); PaymentScreen.check.remainingIs("2.0"); PaymentScreen.do.pressNumpad("0"); +PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "700"); PaymentScreen.check.remainingIs("0.00"); PaymentScreen.check.changeIs("628.0"); PaymentScreen.do.clickValidate(); @@ -48,7 +49,7 @@ ReceiptScreen.do.clickNextOrder(); ProductScreen.exec.addOrderline("Desk Pad", "6", "5"); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickTipButton(); -NumberPopup.do.pressNumpad("1"); +NumberPopup.do.enterValue("1"); NumberPopup.check.inputShownIs("1"); NumberPopup.do.clickConfirm(); PaymentScreen.check.emptyPaymentlines("31.0"); diff --git a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js index dea24a75eae..8f24324220b 100644 --- a/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js +++ b/addons/point_of_sale/static/tests/tours/TicketScreen.tour.js @@ -125,6 +125,7 @@ TicketScreen.do.pressNumpad("3"); // that can be refunded. ErrorPopup.do.clickConfirm(); TicketScreen.do.clickDiscard(); +ProductScreen.do.goBackToMainScreen(); ProductScreen.check.isShown(); ProductScreen.check.orderIsEmpty(); ProductScreen.do.clickRefund(); @@ -133,6 +134,7 @@ TicketScreen.do.clickOrderline("Desk Pad"); TicketScreen.do.pressNumpad("1"); TicketScreen.check.toRefundTextContains("To Refund: 1.00"); TicketScreen.do.confirmRefund(); +ProductScreen.do.goBackToMainScreen(); ProductScreen.check.isShown(); ProductScreen.check.selectedOrderlineHas("Desk Pad", "-1.00"); // Try changing the refund line to positive number. @@ -151,6 +153,7 @@ ProductScreen.do.clickRefund(); TicketScreen.do.selectOrder("-0005"); TicketScreen.check.toRefundTextContains("Refunding 2.00"); TicketScreen.do.clickDiscard(); +ProductScreen.do.goBackToMainScreen(); // Pay the refund order. ProductScreen.do.clickPayButton(); PaymentScreen.do.clickPaymentMethod("Bank"); diff --git a/addons/point_of_sale/static/tests/tours/acceptance_tour.js b/addons/point_of_sale/static/tests/tours/acceptance_tour.js index 4acc5fb22e0..aee1418c973 100644 --- a/addons/point_of_sale/static/tests/tours/acceptance_tour.js +++ b/addons/point_of_sale/static/tests/tours/acceptance_tour.js @@ -20,7 +20,7 @@ function add_product_to_order(product_name) { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, ]; @@ -35,7 +35,7 @@ function set_fiscal_position_on_order(fp_name) { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -48,7 +48,7 @@ function set_fiscal_position_on_order(fp_name) { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -58,7 +58,7 @@ function set_fiscal_position_on_order(fp_name) { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, ]; @@ -69,10 +69,39 @@ function press_payment_numpad(val) { { content: `press ${val} on payment screen numpad`, trigger: `.payment-numpad .input-button:contains("${val}"):visible`, + mobile: false, }, ]; } +function fillPaymentLineAmountMobile(lineName, keys) { + return [ + { + content: "click payment line", + trigger: `.paymentlines .paymentline .payment-infos:contains("${lineName}")`, + mobile: true, + }, + { + content: `'${keys}' inputed in the number popup`, + trigger: ".popup .payment-input-number", + run: `text ${keys}`, + mobile: true, + }, + { + content: "click confirm button", + trigger: ".popup .footer .confirm", + mobile: true, + } + ]; +} + +function fillPaymentValue(lineName, val) { + return [ + ...press_payment_numpad(val), + ...fillPaymentLineAmountMobile(lineName, val), + ] +} + function press_product_numpad(val) { return [ { @@ -86,7 +115,7 @@ function press_product_numpad(val) { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, ]; @@ -127,7 +156,7 @@ function selected_orderline_has({ product, price = null, quantity = null }) { } result.push({ content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }); return result; @@ -147,7 +176,7 @@ function verify_order_total(total_str) { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, ]; @@ -192,6 +221,12 @@ function finish_order() { { content: "click Next Order", trigger: ".receipt-screen .button.next.highlight:visible", + mobile: false, + }, + { + content: "Click Next Order", + trigger: ".receipt-screen .btn-switchpane.validation-button.highlight[name='done']", + mobile: true, }, { content: "check if we left the receipt screen", @@ -229,7 +264,7 @@ steps = steps.concat(goto_payment_screen_and_select_payment_method()); remaining := 0.00 change := 1.50 */ -steps = steps.concat(press_payment_numpad("5")); +steps = steps.concat(fillPaymentValue("Cash", "5")); steps = steps.concat(selected_payment_has("Cash", "5.0")); steps = steps.concat([ { @@ -255,12 +290,12 @@ steps = steps.concat([ steps = steps.concat([ { content: "pay with cash", - trigger: '.paymentmethod:contains("Cash")', + trigger: '.paymentmethod:contains("Bank")', }, ]); -steps = steps.concat(selected_payment_has("Cash", "5.2")); -steps = steps.concat(press_payment_numpad("6")); -steps = steps.concat(selected_payment_has("Cash", "6.0")); +steps = steps.concat(selected_payment_has("Bank", "5.2")); +steps = steps.concat(fillPaymentValue("Bank", "6")); +steps = steps.concat(selected_payment_has("Bank", "6.0")); steps = steps.concat([ { content: "verify remaining", diff --git a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js index 3a9724a9ed9..bd6da251b1d 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/NumberPopupTourMethods.js @@ -27,16 +27,40 @@ class Do { return { content: `'${key}' pressed in numpad`, trigger, + mobile: false, }; } return keys.split(" ").map(generateStep); } + enterValue(keys) { + const numpadKeys = keys.split('').join(' '); + return [ + ...this.pressNumpad(numpadKeys), + ...this.fillPopupValue(keys) + ]; + } + fillPopupValue(keys) { + return [ + { + content: `'${keys}' inputed in the number popup`, + trigger: ".popup .value", + run: `text ${keys}`, + mobile: true, + }, + ]; + } clickConfirm() { return [ { content: "click confirm button", trigger: ".popup-number .footer .confirm", + mobile: false, }, + { + content: "click confirm button", + trigger: ".popup .footer .confirm", + mobile: true, + } ]; } } @@ -46,7 +70,7 @@ class Check { return [ { content: "number popup is shown", - trigger: ".modal-dialog .popup-number", + trigger: ".modal-dialog .popup .value", run: () => {}, }, ]; @@ -55,13 +79,15 @@ class Check { return [ { content: "number input element check", - trigger: ".modal-dialog .popup-number .popup-input", + trigger: ".modal-dialog .popup-number", run: () => {}, + mobile: false, }, { content: `input shown is '${val}'`, - trigger: `.modal-dialog .popup-number .popup-input:contains("${val}")`, + trigger: `.modal-dialog .popup .value:contains("${val}")`, run: () => {}, + mobile: false, }, ]; } 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 67862f3521b..2575ab03ddf 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/PaymentScreenTourMethods.js @@ -17,11 +17,20 @@ class Do { * @param {String} name payment method * @param {String} amount */ - clickPaymentlineDelButton(name, amount) { + clickPaymentlineDelButton(name, amount, mobile=false) { + if (mobile) { + return [ + { + content: `delete ${name} paymentline with ${amount} amount`, + trigger: `.paymentlines .paymentline .payment-infos:contains("${name}") ~ .delete-button`, + mobile: true, + }, + ]; + } return [ { content: `delete ${name} paymentline with ${amount} amount`, - trigger: `.paymentlines .paymentline .payment-name:contains("${name}") ~ .delete-button`, + trigger: `.paymentlines .paymentline .payment-infos:contains("${name}") ~ .delete-button`, }, ]; } @@ -57,7 +66,17 @@ class Do { /** * Press the numpad in sequence based on the given space-separated keys. * Note: Maximum of 2 characters because NumberBuffer only allows 2 consecutive - * fast inputs. Fast inputs is the case in tours. + * fast inputs. Fast inputs is the case in tours. This method is only for the + * desktop environment. The mobile environment doesn't work exactly the same way + * so we have to call fillPaymentLineAmountMobile to have the same behaviour. + * + * e.g. : + * PaymentScreen.do.enterPaymentLineAmount("Cash", "70"); + * PaymentScreen.check.remainingIs("2.0"); + * PaymentScreen.do.pressNumpad("0"); <- desktop: add a 0 + * PaymentScreen.do.fillPaymentLineAmountMobile("Cash", "700"); <- mobile: rewrite the amount + * PaymentScreen.check.remainingIs("0.00"); + * PaymentScreen.check.changeIs("628.0"); * * @param {String} keys space-separated numpad keys */ @@ -76,9 +95,10 @@ class Do { return { content: `'${key}' pressed in payment numpad`, trigger, + mobile: false, }; } - return keys.split(" ").map(generateStep); + return keys.split(' ').map(generateStep); } clickBack() { @@ -97,6 +117,35 @@ class Do { }, ]; } + + enterPaymentLineAmount(lineName, keys) { + const numpadKeys = keys.split('').join(' '); + return [ + ...this.pressNumpad(numpadKeys), + ...this.fillPaymentLineAmountMobile(lineName, keys) + ]; + } + + fillPaymentLineAmountMobile(lineName, keys) { + return [ + { + content: "click payment line", + trigger: `.paymentlines .paymentline .payment-infos:contains("${lineName}")`, + mobile: true, + }, + { + content: `'${keys}' inputed in the number popup`, + trigger: ".popup .payment-input-number", + run: `text ${keys}`, + mobile: true, + }, + { + content: "click confirm button", + trigger: ".popup .footer .confirm", + mobile: true, + } + ]; + } } class Check { diff --git a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js index cef145ec4a0..cdc967313f1 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -31,7 +31,7 @@ class Do { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -95,7 +95,7 @@ class Do { ...keys.split(" ").map(generateStep), { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -148,7 +148,7 @@ class Do { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -163,7 +163,7 @@ class Do { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -181,7 +181,7 @@ class Do { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -201,7 +201,7 @@ class Do { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { trigger: ".o_pricelist_button" }, @@ -211,7 +211,7 @@ class Do { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -234,7 +234,7 @@ class Do { }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -247,7 +247,7 @@ class Do { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -274,6 +274,15 @@ class Do { }, ]; } + goBackToMainScreen() { + return [ + { + content: "go back to the products", + trigger: ".pos-rightheader .floor-button", + mobile: true, + }, + ] + } } class Check { @@ -321,7 +330,7 @@ class Check { } res.push({ content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }); return res; @@ -340,7 +349,7 @@ class Check { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -369,7 +378,7 @@ class Check { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -388,7 +397,7 @@ class Check { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -442,7 +451,7 @@ class Check { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -461,7 +470,7 @@ class Check { }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }, ]; @@ -537,7 +546,7 @@ class Execute { res.push(...TextAreaPopup._do.clickConfirm()); res.push({ content: "go back to the products", - trigger: ".back-button", + trigger: ".pos-rightheader .floor-button", mobile: true, }); return res; diff --git a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js index 0e4fd2d120f..c74d8e283a5 100644 --- a/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js +++ b/addons/point_of_sale/static/tests/tours/helpers/ReceiptScreenTourMethods.js @@ -8,6 +8,12 @@ class Do { { content: "go to next screen", trigger: ".receipt-screen .button.next.highlight[name='done']", + mobile: false, + }, + { + content: "go to next screen", + trigger: ".receipt-screen .btn-switchpane.validation-button.highlight[name='done']", + mobile: true, }, ]; } diff --git a/addons/point_of_sale/static/tests/tours/pricelist_tour.js b/addons/point_of_sale/static/tests/tours/pricelist_tour.js index 2496155b26f..e75c98729dd 100644 --- a/addons/point_of_sale/static/tests/tours/pricelist_tour.js +++ b/addons/point_of_sale/static/tests/tours/pricelist_tour.js @@ -112,7 +112,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -138,7 +138,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -164,7 +164,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -182,7 +182,7 @@ steps = steps.concat([ }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, { @@ -203,7 +203,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -233,7 +233,7 @@ steps = steps.concat([ }, { content: "go back to the products", - trigger: ".back-button", + trigger: ".floor-button", mobile: true, }, { @@ -284,7 +284,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { @@ -302,7 +302,7 @@ steps = steps.concat([ }, { content: "click more button", - trigger: ".control-button:contains('More')", + trigger: ".mobile-more-button", mobile: true, }, { diff --git a/addons/pos_hr/static/tests/tours/PosHrTour.js b/addons/pos_hr/static/tests/tours/PosHrTour.js index a681899b2f0..869c94d729a 100644 --- a/addons/pos_hr/static/tests/tours/PosHrTour.js +++ b/addons/pos_hr/static/tests/tours/PosHrTour.js @@ -20,9 +20,10 @@ SelectionPopup.check.hasSelectionItem("Pos Employee2"); SelectionPopup.check.hasSelectionItem("Mitchell Admin"); SelectionPopup.do.clickItem("Pos Employee1"); NumberPopup.check.isShown(); -NumberPopup.do.pressNumpad("2 5"); +NumberPopup.do.enterValue("25"); NumberPopup.check.inputShownIs("••"); NumberPopup.do.pressNumpad("8 1"); +NumberPopup.do.fillPopupValue("2581"); NumberPopup.check.inputShownIs("••••"); NumberPopup.do.clickConfirm(); ErrorPopup.check.isShown(); @@ -30,9 +31,10 @@ ErrorPopup.do.clickConfirm(); PosHr.do.clickLoginButton(); SelectionPopup.do.clickItem("Pos Employee1"); NumberPopup.check.isShown(); -NumberPopup.do.pressNumpad("2 5"); +NumberPopup.do.enterValue("25"); NumberPopup.check.inputShownIs("••"); NumberPopup.do.pressNumpad("8 0"); +NumberPopup.do.fillPopupValue("2580"); NumberPopup.check.inputShownIs("••••"); NumberPopup.do.clickConfirm(); ProductScreen.check.isShown(); @@ -45,9 +47,10 @@ Chrome.do.clickMenuButton(); PosHr.do.clickLockButton(); PosHr.do.clickLoginButton(); SelectionPopup.do.clickItem("Pos Employee2"); -NumberPopup.do.pressNumpad("1 2"); +NumberPopup.do.enterValue("12"); NumberPopup.check.inputShownIs("••"); NumberPopup.do.pressNumpad("3 4"); +NumberPopup.do.fillPopupValue("1234"); NumberPopup.check.inputShownIs("••••"); NumberPopup.do.clickConfirm(); ProductScreen.check.isShown(); diff --git a/addons/pos_loyalty/static/src/js/Loyalty.js b/addons/pos_loyalty/static/src/js/Loyalty.js index 623e939176f..dd02f4075dd 100644 --- a/addons/pos_loyalty/static/src/js/Loyalty.js +++ b/addons/pos_loyalty/static/src/js/Loyalty.js @@ -1682,7 +1682,7 @@ patch(Order.prototype, "pos_loyalty.Order", { return sprintf( _t("Gift Card: %s\nBalance: %s"), code, - this.pos.format_currency(coupon.balance) + this.env.utils.formatCurrency(coupon.balance) ); } return true; diff --git a/addons/pos_restaurant/models/pos_config.py b/addons/pos_restaurant/models/pos_config.py index 64e7e903ba2..c3bbbc17122 100644 --- a/addons/pos_restaurant/models/pos_config.py +++ b/addons/pos_restaurant/models/pos_config.py @@ -2,6 +2,8 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ +import json +from collections import defaultdict class PosConfig(models.Model): @@ -14,18 +16,36 @@ class PosConfig(models.Model): set_tip_after_payment = fields.Boolean('Set Tip After Payment', help="Adjust the amount authorized by payment terminals to add a tip after the customers left or at the end of the day.") module_pos_restaurant = fields.Boolean(default=True) - def get_tables_order_count(self): - """ """ + def get_tables_order_count_and_printing_changes(self): self.ensure_one() tables = self.env['restaurant.table'].search([('floor_id.pos_config_ids', '=', self.id)]) domain = [('state', '=', 'draft'), ('table_id', 'in', tables.ids)] order_stats = self.env['pos.order']._read_group(domain, ['table_id'], ['__count']) + linked_orderlines = self.env['pos.order.line'].search([('order_id.state', '=', 'draft'), ('order_id.table_id', 'in', tables.ids)]) orders_map = {table.id: count for table, count in order_stats} + changes_map = defaultdict(lambda: 0) + skip_changes_map = defaultdict(lambda: 0) + + for line in linked_orderlines: + last_order_preparation_change = json.loads(line.order_id.last_order_preparation_change) + prep_change = {} + for line_uuid in last_order_preparation_change: + prep_change[last_order_preparation_change[line_uuid]['line_uuid']] = last_order_preparation_change[line_uuid] + quantity_changed = 0 + if line.uuid in prep_change: + quantity_changed = line.qty - prep_change[line.uuid]['quantity'] + else: + quantity_changed = line.qty + + if line.skip_change: + skip_changes_map[line.order_id.table_id.id] += quantity_changed + else: + changes_map[line.order_id.table_id.id] += quantity_changed result = [] for table in tables: - result.append({'id': table.id, 'orders': orders_map.get(table.id, 0)}) + result.append({'id': table.id, 'orders': orders_map.get(table.id, 0), 'changes': changes_map.get(table.id, 0), 'skip_changes': skip_changes_map.get(table.id, 0)}) return result def _get_forbidden_change_fields(self): diff --git a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js index 61d56663672..025ca81deca 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/floor_screen.js @@ -14,7 +14,15 @@ import { EditBar } from "./edit_bar"; import { Table } from "./table"; import { usePos } from "@point_of_sale/app/pos_hook"; import { useService } from "@web/core/utils/hooks"; -import { Component, onPatched, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl"; +import { + Component, + onPatched, + onMounted, + onWillUnmount, + useRef, + useState, + onWillStart, +} from "@odoo/owl"; import { sprintf } from "@web/core/utils/strings"; export class FloorScreen extends Component { @@ -37,13 +45,14 @@ export class FloorScreen extends Component { isColorPicker: false, }); const ui = useState(useService("ui")); - this.pos.globalState.floorPlanStyle ||= ui.isSmall ? "kanban" : "default"; + this.pos.globalState.floorPlanStyle = ui.isSmall ? "kanban" : "default"; this.floorMapRef = useRef("floor-map-ref"); this.addFloorRef = useRef("add-floor-ref"); this.map = useRef("map"); onPatched(this.onPatched); onMounted(this.onMounted); onWillUnmount(this.onWillUnmount); + onWillStart(this.onWillStart); } onPatched() { this.floorMapRef.el.style.background = this.state.floorBackground; @@ -55,6 +64,10 @@ export class FloorScreen extends Component { } this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; } + async onWillStart() { + await this.pos.globalState.unsetTable(); + await this._tableLongpolling(); + } onMounted() { this.pos.openCashControl(); this.floorMapRef.el.style.background = this.state.floorBackground; @@ -66,7 +79,7 @@ export class FloorScreen extends Component { } this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top; // call _tableLongpolling once then set interval of 5sec. - this._tableLongpolling(); + this.tableLongpolling = setInterval(this._tableLongpolling.bind(this), 5000); } onWillUnmount() { @@ -254,9 +267,11 @@ export class FloorScreen extends Component { if (globalState.isEditMode) { return; } - const result = await this.orm.call("pos.config", "get_tables_order_count", [ - globalState.config.id, - ]); + const result = await this.orm.call( + "pos.config", + "get_tables_order_count_and_printing_changes", + [globalState.config.id] + ); for (const table of result) { if (!(table.id in globalState.tables_by_id)) { //We enter this condition if, after loading our PoS, a table is deleted on a PoS and not on another. @@ -278,8 +293,10 @@ export class FloorScreen extends Component { (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; + ); + table_obj.order_count = table.orders + unsynced_orders.length; + table_obj.changes_count = table.changes; + table_obj.skip_changes = table.skip_changes; } } get activeFloor() { diff --git a/addons/pos_restaurant/static/src/app/floor_screen/table.js b/addons/pos_restaurant/static/src/app/floor_screen/table.js index 4ea3d3a9644..2b88eb53481 100644 --- a/addons/pos_restaurant/static/src/app/floor_screen/table.js +++ b/addons/pos_restaurant/static/src/app/floor_screen/table.js @@ -29,6 +29,20 @@ export class Table extends Component { get style() { const table = this.props.table; + let background = table.color ? table.color : "rgb(53, 211, 116)"; + let textColor = "white"; + let border = "auto"; + let boxShadow = "0px 3px rgba(0,0,0,0.07)"; + if (!this.isOccupied()) { + background = "transparent"; + const rgb = table.floor.background_color.substring(4, table.floor.background_color.length-1) + .replace(/ /g, '') + .split(','); + textColor = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2])/255 > 0.5 ? "black" : "white"; + border = "3px solid " + table.color; + boxShadow = "none"; + } + if (this.pos.globalState.floorPlanStyle == "kanban") { const floor = table.floor; const index = floor.tables.indexOf(table); @@ -47,9 +61,12 @@ export class Table extends Component { line-height: ${widthTable}px; top: ${position_v}px; left: ${position_h}px; + border: ${border}; border-radius: ${table.shape === "round" ? 1000 : 3}px; - background: ${table.color || "rgb(53, 211, 116)"}; + background: ${background}; + box-shadow: ${boxShadow}; font-size: ${widthTable >= 150 ? 32 : 16}px; + color: ${textColor}; `; } else { return ` @@ -58,9 +75,12 @@ export class Table extends Component { line-height: ${table.height}px; top: ${table.position_v}px; left: ${table.position_h}px; + border: ${border}; border-radius: ${table.shape === "round" ? 1000 : 3}px; - background: ${table.color || "rgb(53, 211, 116)"}; + background: ${background}; + box-shadow: ${boxShadow}; font-size: ${table.height >= 150 && table.width >= 150 ? 32 : 16}px; + color: ${textColor}; `; } } @@ -70,11 +90,13 @@ export class Table extends Component { } get orderCount() { const table = this.props.table; - return table.order_count !== undefined - ? table.order_count - : this.pos.globalState - .getTableOrders(table.id) - .filter((o) => o.orderlines.length !== 0 || o.paymentlines.length !== 0).length; + + if (table.changes_count > 0) { + return table.changes_count; + } else if (table.skip_changes > 0) { + return table.skip_changes; + } + return table.order_count; } get orderCountClass() { const countClass = { "order-count": true }; @@ -91,18 +113,14 @@ export class Table extends Component { }`; } _getNotifications() { - const orders = this.pos.globalState.getTableOrders(this.props.table.id); + const table = this.props.table; - let hasChangesCount = 0; - let hasSkippedCount = 0; - for (let i = 0; i < orders.length; i++) { - if (orders[i].hasChangesToPrint()) { - hasChangesCount++; - } else if (orders[i].hasSkippedChanges()) { - hasSkippedCount++; - } - } + const hasChangesCount = table.changes_count; + const hasSkippedCount = table.skip_changes; return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {}; } + isOccupied() { + return this.pos.globalState.getCustomerCount(this.props.table.id) > 0 || this.props.table.order_count > 0; + } } diff --git a/addons/pos_restaurant/static/src/app/navbar/BackButton.js b/addons/pos_restaurant/static/src/app/navbar/BackButton.js new file mode 100644 index 00000000000..fdf0d556914 --- /dev/null +++ b/addons/pos_restaurant/static/src/app/navbar/BackButton.js @@ -0,0 +1,58 @@ +/** @odoo-module */ + +import { BackButton } from "@point_of_sale/app/navbar/BackButton"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; +import { TipScreen } from "@pos_restaurant/js/Screens/TipScreen"; +import { patch } from "@web/core/utils/patch"; + +patch(BackButton.prototype, "pos_restaurant.BackButton", { + get table() { + return this.pos.globalState.table; + }, + get floor() { + return this.table?.floor; + }, + get hasTable() { + return this.table != null; + }, + /** + * @override + * If we have a floor screen, + * the logic of the back button changes a bit. + */ + async backToFloorScreen() { + if (this.pos.mainScreen.component && this.pos.globalState.config.module_pos_restaurant) { + if ( + (this.pos.mainScreen.component === ProductScreen && + this.pos.globalState.mobile_pane == "right") || + this.pos.mainScreen.component === TipScreen + ) { + if (this.table) { + const orders = this.pos.globalState.get_order_list(); + const tableOrders = orders.filter((order) => order.tableId === this.table.id); + const qtyChange = tableOrders.reduce( + (acc, order) => { + const quantityChange = order.getOrderChanges(); + const quantitySkipped = order.getOrderChanges(true); + acc.changed += quantityChange.count; + acc.skipped += quantitySkipped.count; + return acc; + }, + { changed: 0, skipped: 0 } + ); + + this.table.changes_count = qtyChange.changed; + this.table.skip_changes = qtyChange.skipped; + } + + this.pos.showScreen("FloorScreen", { floor: this.floor }); + } else { + this.pos.globalState.mobile_pane = "right"; + this.pos.showScreen("ProductScreen"); + } + } else { + this.pos.globalState.mobile_pane = "right"; + this.pos.showScreen("ProductScreen"); + } + }, +}); diff --git a/addons/pos_restaurant/static/src/app/navbar/BackToFloorButton.js b/addons/pos_restaurant/static/src/app/navbar/BackToFloorButton.js deleted file mode 100644 index 572789f710b..00000000000 --- a/addons/pos_restaurant/static/src/app/navbar/BackToFloorButton.js +++ /dev/null @@ -1,26 +0,0 @@ -/** @odoo-module */ - -import { Component, useState } from "@odoo/owl"; -import { usePos } from "@point_of_sale/app/pos_hook"; -import { useService } from "@web/core/utils/hooks"; - -export class BackToFloorButton extends Component { - static template = "BackToFloorButton"; - - setup() { - this.pos = usePos(); - this.ui = useState(useService("ui")); - } - get table() { - return this.pos.globalState.table; - } - get floor() { - return this.table ? this.table.floor : null; - } - get hasTable() { - return this.table != null; - } - backToFloorScreen() { - this.pos.showScreen("FloorScreen", { floor: this.floor }); - } -} diff --git a/addons/pos_restaurant/static/src/app/navbar/navbar.js b/addons/pos_restaurant/static/src/app/navbar/navbar.js index 08df6c27b74..20faa59c841 100644 --- a/addons/pos_restaurant/static/src/app/navbar/navbar.js +++ b/addons/pos_restaurant/static/src/app/navbar/navbar.js @@ -2,9 +2,7 @@ import { Navbar } from "@point_of_sale/app/navbar/navbar"; import { patch } from "@web/core/utils/patch"; -import { BackToFloorButton } from "./BackToFloorButton"; -patch(Navbar.components, "pos_restaurant.Navbar components", { BackToFloorButton }); patch(Navbar.prototype, "pos_restaurant.Navbar", { /** * If no table is set to pos, which means the current main screen @@ -30,4 +28,7 @@ patch(Navbar.prototype, "pos_restaurant.Navbar", { toggleEditMode() { this.pos.globalState.toggleEditMode(); }, + showBackButton() { + return this._super(...arguments) || (this.pos.showBackButton() && this.pos.globalState.config.module_pos_restaurant); + }, }); diff --git a/addons/pos_restaurant/static/src/app/navbar/navbar.xml b/addons/pos_restaurant/static/src/app/navbar/navbar.xml index e61e4a61a68..3a29521cea7 100644 --- a/addons/pos_restaurant/static/src/app/navbar/navbar.xml +++ b/addons/pos_restaurant/static/src/app/navbar/navbar.xml @@ -10,9 +10,6 @@ Switch Floor View - - - diff --git a/addons/pos_restaurant/static/src/app/pos_store.js b/addons/pos_restaurant/static/src/app/pos_store.js index 119a3cea933..1e6a8c1525b 100644 --- a/addons/pos_restaurant/static/src/app/pos_store.js +++ b/addons/pos_restaurant/static/src/app/pos_store.js @@ -3,7 +3,9 @@ import { patch } from "@web/core/utils/patch"; import { PosStore } from "@point_of_sale/app/pos_store"; import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; +import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { FloorScreen } from "@pos_restaurant/app/floor_screen/floor_screen"; +import { TipScreen } from "@pos_restaurant/js/Screens/TipScreen"; const NON_IDLE_EVENTS = [ "mousemove", @@ -65,9 +67,6 @@ patch(PosStore.prototype, "pos_restaurant.PosStore", { ); }, showScreen(screenName) { - if (screenName === "FloorScreen" && this.globalState.table) { - this.globalState.unsetTable(); - } this._super(...arguments); this.setIdleTimer(); }, @@ -95,4 +94,12 @@ patch(PosStore.prototype, "pos_restaurant.PosStore", { } return this._super(...arguments); }, + showBackButton() { + return ( + this._super(...arguments) || + this.mainScreen.component === TipScreen || + (this.mainScreen.component === ProductScreen && + this.globalState.config.module_pos_restaurant) + ); + }, }); diff --git a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js index abcf22510af..01c667d736a 100644 --- a/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/PaymentScreen.js @@ -3,10 +3,6 @@ import { PaymentScreen } from "@point_of_sale/js/Screens/PaymentScreen/PaymentScreen"; import { patch } from "@web/core/utils/patch"; -patch(PaymentScreen, "pos_restaurant.PaymentScreen", { - showBackToFloorButton: true, -}); - patch(PaymentScreen.prototype, "pos_restaurant.PaymentScreen", { setup() { this._super(...arguments); diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js index b25ddb06e67..5ef40aa34c2 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ActionpadWidget.js @@ -57,7 +57,7 @@ patch(ActionpadWidget.prototype, "point_of_sale.ActionpadWidget", { }, get categoryCount() { const categories = {}; - const orderChange = this.currentOrder.getOrderChanges(); + const orderChange = this.currentOrder.getOrderChanges().orderlines; for (const idx in orderChange) { const orderline = orderChange[idx]; const category = this.pos.globalState.db.get_product_by_id(orderline.product_id) diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js index 3ab74a31efe..07c739f5996 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ControlButtons/PrintBillButton.js @@ -13,6 +13,9 @@ export class PrintBillButton extends Component { } _isDisabled() { const order = this.pos.globalState.get_order(); + if (!order) { + return false; + } return order.get_orderlines().length === 0; } click() { diff --git a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js index 9b95c9738ee..312f472bc1c 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ProductScreen/ProductScreen.js @@ -3,6 +3,47 @@ import { ProductScreen } from "@point_of_sale/js/Screens/ProductScreen/ProductScreen"; import { patch } from "@web/core/utils/patch"; -patch(ProductScreen, "pos_restaurant.ProductScreen", { - showBackToFloorButton: true, +patch(ProductScreen.prototype, "pos_restaurant.ProductScreen", { + /** + * @override + */ + get selectedOrderlineQuantity() { + const order = this.pos.globalState.get_order(); + const orderline = order.get_selected_orderline(); + if ( + this.pos.globalState.config.module_pos_restaurant && + this.pos.globalState.orderPreparationCategories.size + ) { + let orderline_name = orderline.product.display_name; + if (orderline.description) { + orderline_name += " (" + orderline.description + ")"; + } + const changes = Object.values(order.getOrderChanges().orderlines).find( + (change) => change.name == orderline_name + ); + return changes ? changes.quantity : false; + } + return this._super(...arguments); + }, + get selectedOrderlineTotal() { + return this.env.utils.formatCurrency( + this.pos.globalState.get_order().get_selected_orderline().get_display_price() + ); + }, + get swapButton() { + return ( + this.pos.globalState.config.module_pos_restaurant && + this.pos.globalState.orderPreparationCategories.size + ); + }, + submitOrder() { + this.pos.sendOrderInPreparation(this.pos.globalState.get_order()); + }, + primaryPayButton() { + return ( + !this.currentOrder.is_empty() && + ((!this.swapButton && this._super(...arguments)) || + (this.swapButton && this.pos.globalState.get_order().getOrderChanges().count > 0)) + ); + }, }); 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 7399f461fc6..582eec03229 100644 --- a/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js @@ -5,10 +5,6 @@ import { patch } from "@web/core/utils/patch"; import { onWillUnmount } from "@odoo/owl"; import { FloorScreen } from "@pos_restaurant/app/floor_screen/floor_screen"; -patch(ReceiptScreen, "pos_restaurant.ReceiptScreen", { - showBackToFloorButton: true, -}); - patch(ReceiptScreen.prototype, "pos_restaurant.ReceiptScreen", { setup() { this._super(...arguments); diff --git a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js index c07d83d11f3..f6e1eff1b1c 100644 --- a/addons/pos_restaurant/static/src/js/Screens/TipScreen.js +++ b/addons/pos_restaurant/static/src/js/Screens/TipScreen.js @@ -11,7 +11,6 @@ import { Component, useRef, onMounted } from "@odoo/owl"; export class TipScreen extends Component { static template = "pos_restaurant.TipScreen"; - static showBackToFloorButton = true; setup() { super.setup(); this.pos = usePos(); diff --git a/addons/pos_restaurant/static/src/js/models.js b/addons/pos_restaurant/static/src/js/models.js index 4ff0bf4a90e..cb0992dadce 100644 --- a/addons/pos_restaurant/static/src/js/models.js +++ b/addons/pos_restaurant/static/src/js/models.js @@ -1,6 +1,7 @@ /** @odoo-module */ import { PosGlobalState, Order, Orderline, Payment } from "@point_of_sale/js/models"; +import { ConnectionLostError } from "@web/core/network/rpc_service"; import { patch } from "@web/core/utils/patch"; patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", { @@ -156,8 +157,15 @@ patch(PosGlobalState.prototype, "pos_restaurant.PosGlobalState", { getTableOrders(tableId) { return this.get_order_list().filter((order) => order.tableId === tableId); }, - unsetTable() { - this._syncTableOrdersToServer(); + async unsetTable() { + try { + await this._syncTableOrdersToServer(); + } catch (e) { + if (!(e instanceof ConnectionLostError)) { + throw e; + } + Promise.reject(e); + } this.table = null; this.set_order(null); }, diff --git a/addons/pos_restaurant/static/src/scss/restaurant.scss b/addons/pos_restaurant/static/src/scss/restaurant.scss index 44341ecb082..4c272b4d43b 100644 --- a/addons/pos_restaurant/static/src/scss/restaurant.scss +++ b/addons/pos_restaurant/static/src/scss/restaurant.scss @@ -77,24 +77,6 @@ -webkit-box-orient: vertical; -webkit-line-clamp: 2; } -.pos .order-button.floor-button { - display: flex; - align-items: center; - background: $o-navbar-badge-bg; - font-weight: bold; - font-size: 16px; - padding-left: 16px; - padding-right: 16px; - overflow-wrap: anywhere; - text-overflow: hidden; -} -.pos .order-button.floor-button .table-name { - margin-left: 5px; -} -.pos .order-button.floor-button .fa{ - font-size: 24px; - line-height: 42px; -} /* ------ SPLIT BILL SCREEN ------- */ diff --git a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml b/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml deleted file mode 100644 index 06651b617af..00000000000 --- a/addons/pos_restaurant/static/src/xml/ChromeWidgets/BackToFloorButton.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - () - - - - - - diff --git a/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml new file mode 100644 index 00000000000..837b981b806 --- /dev/null +++ b/addons/pos_restaurant/static/src/xml/Screens/ProductScreen/ProductScreen.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + diff --git a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js index bf6a0d6bfe6..2416de9a8d9 100644 --- a/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js +++ b/addons/pos_restaurant/static/tests/tours/ControlButtons.tour.js @@ -46,13 +46,13 @@ BillScreen.do.clickOk(); // Test GuestButton ProductScreen.do.clickGuestButton(); -NumberPopup.do.pressNumpad("1 5"); +NumberPopup.do.enterValue("15"); NumberPopup.check.inputShownIs("15"); NumberPopup.do.clickConfirm(); ProductScreen.check.guestNumberIs("15"); ProductScreen.do.clickGuestButton(); -NumberPopup.do.pressNumpad("5"); +NumberPopup.do.enterValue("5"); NumberPopup.check.inputShownIs("5"); NumberPopup.do.clickConfirm(); ProductScreen.check.guestNumberIs("5"); diff --git a/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js b/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js index 85f6e423208..6fa646ae471 100644 --- a/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/FloorScreen.tour.js @@ -88,6 +88,7 @@ FloorScreen.do.clickTable("4"); FloorScreen.check.selectedTableIs("4"); FloorScreen.do.clickSeats(); NumberPopup.do.pressNumpad("Backspace 9"); +NumberPopup.do.fillPopupValue("9"); NumberPopup.check.inputShownIs("9"); NumberPopup.do.clickConfirm(); FloorScreen.check.tableSeatIs("4", "9"); @@ -96,7 +97,7 @@ FloorScreen.check.tableSeatIs("4", "9"); FloorScreen.do.clickTable("4"); FloorScreen.check.selectedTableIs("4"); FloorScreen.do.clickSeats(); -NumberPopup.do.pressNumpad("1 5"); +NumberPopup.do.enterValue("15"); NumberPopup.check.inputShownIs("15"); NumberPopup.do.clickConfirm(); FloorScreen.check.tableSeatIs("4", "15"); @@ -110,13 +111,11 @@ FloorScreen.do.closeEdit(); FloorScreen.check.tableIsNotSelected("4"); FloorScreen.do.clickTable("4"); ProductScreen.check.isShown(); -Chrome.check.backToFloorTextIs("Main Floor", "4"); Chrome.do.backToFloor(); // Opening product screen in second floor should go back to second floor FloorScreen.do.clickFloor("Second Floor"); FloorScreen.check.hasTable("3"); FloorScreen.do.clickTable("3"); -Chrome.check.backToFloorTextIs("Second Floor", "3"); registry.category("web_tour.tours").add("FloorScreenTour", { test: true, url: "/pos/ui", steps: getSteps() }); diff --git a/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js b/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js index 804a543671a..b942d5f1bed 100644 --- a/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/TicketScreen.tour.js @@ -36,7 +36,7 @@ TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline("Coca-Cola", "2", "2"); ProductScreen.check.totalAmountIs("4.0"); Chrome.do.backToFloor(); -FloorScreen.check.orderCountSyncedInTableIs("2", "2"); +FloorScreen.check.orderCountSyncedInTableIs("2", "3"); Chrome.do.clickMenuButton(); Chrome.do.clickTicketButton(); TicketScreen.do.deleteOrder("-0003"); @@ -44,7 +44,6 @@ Chrome.do.confirmPopup(); TicketScreen.do.selectOrder("-0002"); ProductScreen.check.isShown(); ProductScreen.check.totalAmountIs("2.0"); -Chrome.check.backToFloorTextIs("Main Floor", "2"); Chrome.do.backToFloor(); // Make sure that order is deleted properly. @@ -59,7 +58,6 @@ TicketScreen.do.deleteOrder("-0004"); Chrome.do.confirmPopup(); TicketScreen.do.clickDiscard(); FloorScreen.check.isShown(); -FloorScreen.check.orderCountSyncedInTableIs("5", "0"); FloorScreen.do.clickTable("5"); ProductScreen.check.orderIsEmpty(); diff --git a/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js index 779571c4f7b..5deb4b4e931 100644 --- a/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js +++ b/addons/pos_restaurant/static/tests/tours/TipScreen.tour.js @@ -15,8 +15,8 @@ startSteps(); // Create order that is synced when draft. // order 1 -FloorScreen.do.clickTable("2"); ProductScreen.do.confirmOpeningPopup(); +FloorScreen.do.clickTable("2"); ProductScreen.exec.addOrderline("Minute Maid", "1", "2"); ProductScreen.check.totalAmountIs("2.0"); Chrome.do.backToFloor(); @@ -34,7 +34,7 @@ TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline("Coca-Cola", "2", "2"); ProductScreen.check.totalAmountIs("4.0"); Chrome.do.backToFloor(); -FloorScreen.check.orderCountSyncedInTableIs("2", "1"); +FloorScreen.check.orderCountSyncedInTableIs("2", "2"); Chrome.do.clickMenuButton(); Chrome.do.clickTicketButton(); TicketScreen.check.nthRowContains("2", "Tipping"); @@ -56,7 +56,7 @@ TicketScreen.do.clickNewTicket(); ProductScreen.exec.addOrderline("Coca-Cola", "4", "2"); ProductScreen.check.totalAmountIs("8.0"); Chrome.do.backToFloor(); -FloorScreen.check.orderCountSyncedInTableIs("5", "1"); +FloorScreen.check.orderCountSyncedInTableIs("5", "4"); Chrome.do.clickMenuButton(); Chrome.do.clickTicketButton(); TicketScreen.check.nthRowContains("4", "Tipping"); @@ -122,7 +122,7 @@ ProductScreen.check.isShown(); ProductScreen.do.clickPayButton(); PaymentScreen.do.clickTipButton(); NumberPopup.check.isShown(); -NumberPopup.do.pressNumpad("1"); +NumberPopup.do.enterValue("1"); NumberPopup.check.inputShownIs("1"); NumberPopup.do.clickConfirm(); PaymentScreen.check.emptyPaymentlines("5.0"); @@ -143,4 +143,6 @@ TipScreen.check.isShown(); TipScreen.do.clickSettle(); FloorScreen.check.isShown(); -registry.category("web_tour.tours").add("PosResTipScreenTour", { test: true, url: "/pos/ui", steps: getSteps() }); +registry + .category("web_tour.tours") + .add("PosResTipScreenTour", { test: true, url: "/pos/ui", steps: getSteps() }); diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js index 3732da171ae..0fddfe38978 100644 --- a/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js +++ b/addons/pos_restaurant/static/tests/tours/helpers/ChromeTourMethods.js @@ -14,17 +14,7 @@ class DoExt extends Do { } } -class Check { - backToFloorTextIs(floor, table) { - return [ - { - content: `back to floor text is '${floor} ( ${table} )'`, - trigger: `.floor-button span:contains("${floor}") ~ .table-name:contains("(${table})")`, - run: () => {}, - }, - ]; - } -} +class Check {} class Execute {} diff --git a/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js index fb253484227..fa06dd57f83 100644 --- a/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js +++ b/addons/pos_restaurant/static/tests/tours/helpers/ProductScreenTourMethods.js @@ -12,6 +12,15 @@ class DoExt extends Do { }, ]; } + doubleClickOrderline(name) { + return [ + { + content: "first click orderline", + trigger: `.order .orderline .product-name:contains("${name}")`, + run: "dblclick" + }, + ]; + } clickTransferButton() { return [ { @@ -94,6 +103,15 @@ class CheckExt extends Check { }, ]; } + orderlineIsToSkip(name) { + return [ + { + content: `Line is to order`, + trigger: `.order .orderline.skip-change .product-name:contains("${name}")`, + run: function () {}, // it's a check + }, + ]; + } orderlineHasNote(name, quantity, note) { return [ { diff --git a/addons/pos_restaurant/static/tests/tours/pos_restaurant.js b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js index 211fba1f994..c7c13586fb9 100644 --- a/addons/pos_restaurant/static/tests/tours/pos_restaurant.js +++ b/addons/pos_restaurant/static/tests/tours/pos_restaurant.js @@ -18,10 +18,11 @@ FloorScreen.do.clickTable("5"); ProductScreen.check.orderBtnIsPresent(); ProductScreen.do.clickDisplayedProduct("Coca-Cola"); ProductScreen.check.selectedOrderlineHas("Coca-Cola"); +ProductScreen.do.doubleClickOrderline("Coca-Cola"); ProductScreen.do.clickDisplayedProduct("Water"); ProductScreen.check.selectedOrderlineHas("Water"); ProductScreen.check.orderlineIsToOrder("Water"); -ProductScreen.check.orderlineIsToOrder("Coca-Cola"); +ProductScreen.check.orderlineIsToSkip("Coca-Cola"); ProductScreen.do.clickOrderButton(); ProductScreen.check.orderlinesHaveNoChange(); ProductScreen.check.isPrintingError(); @@ -71,7 +72,7 @@ TicketScreen.do.selectOrder("-0003"); Chrome.do.backToFloor(); // There should be 1 synced draft order. -FloorScreen.check.orderCountSyncedInTableIs("5", "1"); +FloorScreen.check.orderCountSyncedInTableIs("5", "2"); registry.category("web_tour.tours").add("pos_restaurant_sync", { test: true, url: "/pos/ui", steps: getSteps() }); startSteps(); @@ -82,7 +83,6 @@ startSteps(); */ // There is one draft synced order from the previous tour -FloorScreen.check.orderCountSyncedInTableIs("5", "1"); FloorScreen.do.clickTable("5"); ProductScreen.check.totalAmountIs("4.40"); diff --git a/addons/pos_restaurant/tests/test_frontend.py b/addons/pos_restaurant/tests/test_frontend.py index bc63528269d..8b963d8355b 100644 --- a/addons/pos_restaurant/tests/test_frontend.py +++ b/addons/pos_restaurant/tests/test_frontend.py @@ -39,6 +39,13 @@ class TestFrontend(odoo.tests.HttpCase): 'journal_id': second_cash_journal.id, }) + printer = self.env['pos.printer'].create({ + 'name': 'Preparation Printer', + 'epson_printer_ip': '127.0.0.1', + 'printer_type': 'epson_epos', + 'product_categories_ids': [drinks_category.id] + }) + pos_config = self.env['pos.config'].create({ 'name': 'Bar', 'module_pos_restaurant': True, @@ -47,6 +54,7 @@ class TestFrontend(odoo.tests.HttpCase): 'iface_orderline_notes': True, 'iface_start_categ_id': drinks_category.id, 'start_category': True, + 'is_order_printer': True, 'printer_ids': [(4, printer.id)], })