[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:
committed by
Pedram (PEBR)
parent
c348ea89a6
commit
dac0e87e0f
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user