[FIX] point_of_sale: enhance cash input handling

This commit addresses several issues with inputting cash values in the
closing and opening pop-up, particularly when the computer's locale
doesn't match the Odoo instance's locale and when using the Safari
browser. Previously, pressing the wrong decimal point would remove the
inputted value, and the input handling was not consistent in Safari.

Changes:
- Added restrictions similar to the number popup
- Ensured compatibility with localization settings, allowing the
configured decimal point to work correctly
- Improved input handling consistency across browsers, including Safari

opw-3288550

closes odoo/odoo#128308

X-original-commit: c017a9f947843dbef99546e12057452be2fd5e76
Signed-off-by: Joseph Caburnay (jcb) <jcb@odoo.com>
Signed-off-by: Pedram Bi Ria (pebr) <pebr@odoo.com>
This commit is contained in:
pedrambiria
2023-07-13 08:28:14 +02:00
committed by Pedram (PEBR)
parent c348ea89a6
commit dac0e87e0f
9 changed files with 110 additions and 23 deletions
@@ -10,6 +10,7 @@ import { usePos } from "@point_of_sale/app/store/pos_hook";
import { AbstractAwaitablePopup } from "@point_of_sale/app/popup/abstract_awaitable_popup";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { useValidateCashInput } from "@point_of_sale/app/utils/hooks";
export class CashMovePopup extends AbstractAwaitablePopup {
static template = "point_of_sale.CashMovePopup";
@@ -27,8 +28,10 @@ export class CashMovePopup extends AbstractAwaitablePopup {
amount: "",
reason: "",
errorMessage: "",
parsedAmount: 0,
});
this.amountInput = useAutofocus({ refName: "amountInput" });
useValidateCashInput('amountInput');
}
async confirm() {
let amount;
@@ -114,13 +117,13 @@ export class CashMovePopup extends AbstractAwaitablePopup {
this.state.errorMessage = "";
this.amountInput.el.focus();
}
async cancel() {
super.cancel();
if (
this.pos.config.iface_cashdrawer &&
this.pos.hardwareProxy.connectionInfo.status === "connected"
) {
this.pos.logEmployeeMessage(_t("Cash in / out"), "ACTION_CANCELLED");
handleInputChange() {
if (this.amountInput.el.classList.contains('invalid-cash-input')) {
this.amountInput.el.parentElement.classList.add('invalid-cash-input');
this.amountInput.el.style.border = 'none';
return;
}
this.amountInput.el.parentElement.classList.remove('invalid-cash-input');
this.state.parsedAmount = parseFloat(this.state.amount);
}
}
@@ -12,7 +12,7 @@
<span t-on-click="() => this.onClickButton('out')" class="input-type" t-att-class="{ 'red-highlight': state.type === 'out' }">
Cash Out
</span>
<div class="input-container popup-input" >
<div class="input-container popup-input" t-on-input="handleInputChange" >
<span class="currency" t-esc="pos.currency.symbol" t-if="pos.currency.position=='before'"/>
<input class="input-field" type="text" name="amount" t-model="state.amount" t-ref="amountInput" t-on-keypress="_onAmountKeypress"/>
<span class="currency" t-esc="pos.currency.symbol" t-if="pos.currency.position=='after'"/>
@@ -25,8 +25,8 @@
</div>
</main>
<footer class="footer cash-move">
<div class="button confirm disable highlight" t-on-click="confirm">
Confirm
<div class="button confirm disable highlight dynamic-size" t-on-click="confirm">
Confirm <span t-esc="env.utils.formatCurrency(state.parsedAmount)"/>
</div>
<div class="button cancel" t-on-click="cancel">
Discard
@@ -13,6 +13,8 @@ import { identifyError } from "@point_of_sale/app/errors/error_handlers";
import { _t } from "@web/core/l10n/translation";
import { usePos } from "@point_of_sale/app/store/pos_hook";
import { sprintf } from "@web/core/utils/strings";
import { parseFloat } from "@web/views/fields/parsers";
import { useValidateCashInput } from "@point_of_sale/app/utils/hooks";
export class ClosePosPopup extends AbstractAwaitablePopup {
static components = { SaleDetailsButton };
@@ -35,6 +37,14 @@ export class ClosePosPopup extends AbstractAwaitablePopup {
displayMoneyDetailsPopup: false,
});
Object.assign(this.state, this.props.info.state);
useValidateCashInput("closingCashInput");
if (this.otherPaymentMethods && this.otherPaymentMethods.length > 0) {
this.otherPaymentMethods.forEach(pm => {
if (this._getShowDiff(pm)) {
useValidateCashInput("closingCashInput_" + pm.id, this.state.payments[pm.id].counted);
}
})
}
}
//@override
async confirm() {
@@ -100,7 +110,8 @@ export class ClosePosPopup extends AbstractAwaitablePopup {
this.pos.pos_session.id,
]);
}
handleInputChange(paymentId) {
handleInputChange(paymentId, event) {
if (event.target.classList.contains('invalid-cash-input')) return;
let expectedAmount;
if (paymentId === this.defaultCashDetails.id) {
this.manualInputCashCount = true;
@@ -110,6 +121,7 @@ export class ClosePosPopup extends AbstractAwaitablePopup {
} else {
expectedAmount = this.otherPaymentMethods.find((pm) => paymentId === pm.id).amount;
}
this.state.payments[paymentId].counted = parseFloat(event.target.value);
this.state.payments[paymentId].difference = this.env.utils.roundCurrency(
this.state.payments[paymentId].counted - expectedAmount
);
@@ -26,8 +26,8 @@
<tr>
<td t-esc="defaultCashDetails.name"/>
<td t-esc="env.utils.formatCurrency(defaultCashDetails.amount)"/>
<td class="flex" t-on-input="() => this.handleInputChange(defaultCashDetails.id)">
<input class="pos-input" type="number" t-model.number="state.payments[defaultCashDetails.id].counted" t-on-focus="ev=>ev.target.select()"/>
<td class="flex" t-on-input="(event) => this.handleInputChange(defaultCashDetails.id, event)">
<input class="pos-input" t-ref="closingCashInput" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon ClosePosPopup" t-on-click="openDetailsPopup">
<i class="fa fa-money" role="img" title="Open the money details popup"/>
</div>
@@ -65,7 +65,9 @@
<td t-esc="pm.name"/>
<td t-esc="env.utils.formatCurrency(pm.amount)"/>
<t t-set="_showDiff" t-value="_getShowDiff(pm)" />
<td t-if="_showDiff" t-on-input="() => this.handleInputChange(pm.id)"><input class="pos-input" type="number" t-model.number="state.payments[pm.id].counted" t-on-focus="ev=>ev.target.select()"/></td>
<td t-if="_showDiff" t-on-input="() => this.handleInputChange(pm.id)">
<input class="pos-input" t-ref="closingCashInput_{{pm.id}}" t-on-focus="ev=>ev.target.select()"/>
</td>
<td t-if="_showDiff" t-esc="env.utils.formatCurrency(state.payments[pm.id].difference)" t-att-class="{'warning': state.payments[pm.id].difference}"/>
</tr>
</tbody>
@@ -6,6 +6,8 @@ import { usePos } from "@point_of_sale/app/store/pos_hook";
import { MoneyDetailsPopup } from "@point_of_sale/app/utils/money_details_popup/money_details_popup";
import { useState } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { parseFloat } from "@web/views/fields/parsers";
import { useValidateCashInput } from "@point_of_sale/app/utils/hooks";
export class CashOpeningPopup extends AbstractAwaitablePopup {
static template = "point_of_sale.CashOpeningPopup";
@@ -24,6 +26,7 @@ export class CashOpeningPopup extends AbstractAwaitablePopup {
this.orm = useService("orm");
useAutofocus({ refName: "cash-input" });
this.hardwareProxy = useService("hardware_proxy");
useValidateCashInput("cash-input", this.pos.pos_session.cash_register_balance_start);
}
//@override
async confirm() {
@@ -54,12 +57,9 @@ export class CashOpeningPopup extends AbstractAwaitablePopup {
this.moneyDetails = moneyDetails;
}
}
handleInputChange() {
handleInputChange(event) {
if (event.target.classList.contains('invalid-cash-input')) return;
this.manualInputCashCount = true;
this.moneyDetails = null;
this.state.notes = "";
if (typeof this.state.openingCash !== "number") {
this.state.openingCash = 0;
}
this.state.openingCash = parseFloat(event.target.value);
}
}
@@ -9,7 +9,7 @@
<div class="opening-cash-section">
<span class="info-title">Opening cash</span>
<div class="cash-input-sub-section" t-on-input="handleInputChange">
<input class="pos-input" type="number" t-model.number="state.openingCash" t-ref="cash-input" t-on-focus="ev=>ev.target.select()"/>
<input class="pos-input" t-ref="cash-input" t-on-focus="ev=>ev.target.select()"/>
<div class="button icon" t-on-click="openDetailsPopup">
<i class="fa fa-money" role="img" title="Open the money details popup"/>
</div>
@@ -18,7 +18,9 @@
<textarea placeholder="Add an opening note..." class="opening-cash-notes" t-model="state.notes"/>
</main>
<footer class="footer">
<div class="button" t-on-click="confirm">Open session</div>
<div class="button dynamic-size" t-on-click="confirm">
Open session <span t-esc="env.utils.formatCurrency(state.openingCash)"/>
</div>
</footer>
</div>
</t>
@@ -3,7 +3,9 @@ import { OfflineErrorPopup } from "@point_of_sale/app/errors/popups/offline_erro
import { ConfirmPopup } from "@point_of_sale/app/utils/confirm_popup/confirm_popup";
import { ErrorTracebackPopup } from "@point_of_sale/app/errors/popups/error_traceback_popup";
import { ErrorPopup } from "@point_of_sale/app/errors/popups/error_popup";
import { useEnv, onMounted, onPatched, useComponent, useRef } from "@odoo/owl";
import { useEnv, onMounted, onPatched, onWillUnmount, useComponent, useRef } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { escapeRegExp } from '@web/core/utils/strings';
/**
* Introduce error handlers in the component.
@@ -93,3 +95,43 @@ export function useAutoFocusToLast() {
onMounted(autofocus);
onPatched(autofocus);
}
export function useValidateCashInput(inputRef, startingValue) {
const cashInput = useRef(inputRef);
const localization = useService("localization");
const decimalPoint = localization.decimalPoint;
const thousandsSep = localization.thousandsSep;
// Replace the thousands separator and decimal point with regex-escaped versions
const escapedDecimalPoint = escapeRegExp(decimalPoint);
let floatRegex;
if (thousandsSep) {
const escapedThousandsSep = escapeRegExp(thousandsSep);
floatRegex = new RegExp(`^-?(?:\\d+(${escapedThousandsSep}\\d+)*)?(?:${escapedDecimalPoint}\\d*)?$`);
} else {
floatRegex = new RegExp(`^-?(?:\\d+)?(?:${escapedDecimalPoint}\\d*)?$`);
}
function isValidFloat(inputValue) {
return ![decimalPoint, '-'].includes(inputValue) && floatRegex.test(inputValue);
}
function handleCashInputChange(event) {
let inputValue = (event.target.value || "").trim();
// Check if the current input value is a valid float
if (!isValidFloat(inputValue)) {
event.target.classList.add('invalid-cash-input');
} else {
event.target.classList.remove('invalid-cash-input');
}
}
onMounted(() => {
if (cashInput.el) {
cashInput.el.value = (startingValue || 0).toString().replace('.', decimalPoint);
cashInput.el.addEventListener("input", handleCashInputChange);
}
});
onWillUnmount(() => {
if (cashInput.el) {
cashInput.el.removeEventListener("input", handleCashInputChange);
}
})
}
@@ -30,3 +30,24 @@
.pos .popup .footer-flex .button {
margin-right: 0;
}
.pos .popup .invalid-cash-input {
color: red;
animation: blink 0.5s linear;
animation-iteration-count: 2;
border: 1px solid red;
box-shadow: none !important;
}
.pos .popup .invalid-cash-input:focus {
border-color: red;
}
@keyframes blink {
0%, 100% {
border-color: red;
}
50% {
border-color: transparent;
}
}
@@ -2629,6 +2629,11 @@ td {
border: 1px solid $gray-200;
color: $primary;
}
.pos .popup .button.dynamic-size {
width: auto;
white-space: nowrap;
padding: 0 15px;
}
.pos .button .flex-column {
display: flex;