diff --git a/addons/barcodes/__manifest__.py b/addons/barcodes/__manifest__.py
index 96c73e81fb8..96cc1fcdc59 100644
--- a/addons/barcodes/__manifest__.py
+++ b/addons/barcodes/__manifest__.py
@@ -15,13 +15,9 @@
'web.assets_backend': [
'barcodes/static/src/**/*',
],
- 'web.qunit_suite_tests': [
- 'barcodes/static/tests/barcode_tests.js',
- 'barcodes/static/tests/barcode_parser_tests.js',
- ],
- 'web.qunit_mobile_suite_tests': [
- 'barcodes/static/tests/barcode_mobile_tests.js',
- ],
+ 'web.tests_assets': ['barcodes/static/tests/helpers.js'],
+ 'web.qunit_suite_tests': ['barcodes/static/tests/basic/*.js'],
+ 'web.qunit_mobile_suite_tests': ['barcodes/static/tests/mobile/*.js'],
},
'license': 'LGPL-3',
}
diff --git a/addons/barcodes/static/src/barcode_handler_field.js b/addons/barcodes/static/src/barcode_handler_field.js
new file mode 100644
index 00000000000..4cf6cfe39a9
--- /dev/null
+++ b/addons/barcodes/static/src/barcode_handler_field.js
@@ -0,0 +1,23 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { standardFieldProps } from "@web/views/fields/standard_field_props";
+import { useBus, useService } from "@web/core/utils/hooks";
+
+const { Component, xml } = owl;
+
+export class BarcodeHandlerField extends Component {
+ setup() {
+ const barcode = useService("barcode");
+ useBus(barcode.bus, "barcode_scanned", this.onBarcodeScanned);
+ }
+ onBarcodeScanned(event) {
+ const { barcode } = event.detail;
+ this.props.update(barcode);
+ }
+}
+
+BarcodeHandlerField.template = xml``;
+BarcodeHandlerField.props = { ...standardFieldProps };
+
+registry.category("fields").add("barcode_handler", BarcodeHandlerField);
diff --git a/addons/barcodes/static/src/barcode_service.js b/addons/barcodes/static/src/barcode_service.js
index 0c1a2fb6e2b..53b441605e8 100644
--- a/addons/barcodes/static/src/barcode_service.js
+++ b/addons/barcodes/static/src/barcode_service.js
@@ -43,7 +43,8 @@ export const barcodeService = {
function handleBarcode(barcode, target) {
bus.trigger('barcode_scanned', {barcode,target});
if (target.getAttribute('barcode_events') === "true") {
- $(target).trigger('barcode_scanned', barcode);
+ const barcodeScannedEvent = new CustomEvent("barcode_scanned", { detail: { barcode, target } });
+ target.dispatchEvent(barcodeScannedEvent);
}
}
diff --git a/addons/barcodes/static/src/float_scannable_field.js b/addons/barcodes/static/src/float_scannable_field.js
new file mode 100644
index 00000000000..1efc750afd6
--- /dev/null
+++ b/addons/barcodes/static/src/float_scannable_field.js
@@ -0,0 +1,13 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { FloatField } from "@web/views/fields/float/float_field";
+
+export class FloatScannableField extends FloatField {
+ onBarcodeScanned() {
+ this.inputRef.el.dispatchEvent(new InputEvent("input"));
+ }
+}
+FloatScannableField.template = "barcodes.FloatScannableField";
+
+registry.category("fields").add("field_float_scannable", FloatScannableField);
diff --git a/addons/barcodes/static/src/float_scannable_field.xml b/addons/barcodes/static/src/float_scannable_field.xml
new file mode 100644
index 00000000000..cbc490df1c2
--- /dev/null
+++ b/addons/barcodes/static/src/float_scannable_field.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ onBarcodeScanned
+ true
+ true
+
+
+
+
diff --git a/addons/barcodes/static/tests/barcode_parser_tests.js b/addons/barcodes/static/tests/basic/barcode_parser_tests.js
similarity index 100%
rename from addons/barcodes/static/tests/barcode_parser_tests.js
rename to addons/barcodes/static/tests/basic/barcode_parser_tests.js
diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/basic/barcode_tests.js
similarity index 96%
rename from addons/barcodes/static/tests/barcode_tests.js
rename to addons/barcodes/static/tests/basic/barcode_tests.js
index 0e3a758b21e..15b2cc04536 100644
--- a/addons/barcodes/static/tests/barcode_tests.js
+++ b/addons/barcodes/static/tests/basic/barcode_tests.js
@@ -7,34 +7,17 @@ const {barcodeRemapperService} = require("@barcodes/js/barcode_events");
const { makeTestEnv } = require("@web/../tests/helpers/mock_env");
const { registry } = require("@web/core/registry");
const { mockTimeout } = require("@web/../tests/helpers/utils");
+const { simulateBarCode } = require("@barcodes/../tests/helpers");
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
-var triggerEvent = testUtils.dom.triggerEvent;
var core = require('web.core');
const maxTimeBetweenKeysInMs = barcodeService.maxTimeBetweenKeysInMs;
-function simulateBarCode(chars, target = document.body) {
- for (let char of chars) {
- let keycode;
- if (char === 'Enter') {
- keycode = $.ui.keyCode.ENTER;
- } else if (char === "Tab") {
- keycode = $.ui.keyCode.TAB;
- } else {
- keycode = char.charCodeAt(0);
- }
- triggerEvent(target, 'keydown', {
- key: char,
- keyCode: keycode,
- });
- }
-}
-
QUnit.module('Barcodes', {
beforeEach: function () {
barcodeService.maxTimeBetweenKeysInMs = 0;
diff --git a/addons/barcodes/static/tests/basic/float_scannable_field_test.js b/addons/barcodes/static/tests/basic/float_scannable_field_test.js
new file mode 100644
index 00000000000..403a994883d
--- /dev/null
+++ b/addons/barcodes/static/tests/basic/float_scannable_field_test.js
@@ -0,0 +1,142 @@
+/** @odoo-module **/
+
+import { nextTick, patchWithCleanup, getFixture } from "@web/../tests/helpers/utils";
+import { makeView, setupViewRegistries } from "@web/../tests/views/helpers";
+import { barcodeGenericHandlers } from "@barcodes/barcode_handlers";
+import { barcodeService } from "@barcodes/barcode_service";
+import { simulateBarCode } from "../helpers";
+import { FormController } from "@web/views/form/form_controller";
+import { registry } from "@web/core/registry";
+
+const maxTimeBetweenKeysInMs = barcodeService.maxTimeBetweenKeysInMs;
+
+let target;
+let serverData;
+
+QUnit.module("Fields", (hooks) => {
+ hooks.beforeEach(() => {
+ target = getFixture();
+ barcodeService.maxTimeBetweenKeysInMs = 0;
+ registry.category("services").add("barcode", barcodeService, { force: true });
+ registry
+ .category("services")
+ .add("barcode_autoclick", barcodeGenericHandlers, { force: true });
+
+ serverData = {
+ models: {
+ product: {
+ fields: {
+ name: { string: "Product name", type: "char" },
+ float_field: { string: "Float", type: "float" },
+ float_field_2: { string: "Float", type: "float" },
+ barcode: { string: "Barcode", type: "char" },
+ },
+ records: [
+ { id: 1, name: "Large Cabinet", barcode: "1234567890" },
+ { id: 2, name: "Cabinet with Doors", barcode: "0987654321" },
+ ],
+ },
+ },
+ };
+ setupViewRegistries();
+ });
+ hooks.afterEach(() => {
+ barcodeService.maxTimeBetweenKeysInMs = maxTimeBetweenKeysInMs;
+ });
+
+ QUnit.module("FloatScannableField");
+
+ QUnit.test("widget field_float_scannable", async function (assert) {
+ serverData.models.product.records[0].float_field = 4;
+
+ const onBarcodeScanned = (event) => {
+ assert.step(`barcode scanned ${event.detail.barcode}`);
+ };
+
+ const view = await makeView({
+ type: "form",
+ resModel: "product",
+ serverData,
+ resId: 1,
+ arch: /*xml*/ `
+
+ `,
+ });
+ view.env.services.barcode.bus.addEventListener("barcode_scanned", onBarcodeScanned);
+
+ const inputDiv1 = target.querySelector(".o_field_widget[name=float_field]");
+ assert.strictEqual(
+ inputDiv1.querySelector("input").value,
+ "4.00",
+ "should display the correct value"
+ );
+
+ // we check here that a scan on the field_float_scannable widget triggers a
+ // barcode event
+ inputDiv1.querySelector("input").focus();
+ simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"], inputDiv1, "input");
+ await nextTick();
+ assert.verifySteps(["barcode scanned 60164785"]);
+ assert.strictEqual(
+ inputDiv1.querySelector("input"),
+ document.activeElement,
+ "float field should stay focused"
+ );
+
+ // we check here that a scan on the field without widget does not trigger a
+ // barcode event
+ const inputDiv2 = target.querySelector(".o_field_widget[name=float_field_2]");
+ inputDiv2.querySelector("input").focus();
+ simulateBarCode(["6", "0", "1", "6", "4", "7", "8", "5"], inputDiv2, "input");
+ await nextTick();
+ assert.verifySteps([]);
+
+ view.env.services.barcode.bus.removeEventListener("barcode_scanned", onBarcodeScanned);
+ });
+
+ QUnit.test("do no update form twice after a command barcode scanned", async function (assert) {
+ assert.expect(5);
+
+ patchWithCleanup(FormController.prototype, {
+ onPagerUpdate() {
+ assert.step("update");
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await makeView({
+ type: "form",
+ resModel: "product",
+ serverData,
+ arch: /*xml*/ `
+
+ `,
+ mockRPC(_route, args) {
+ if (args.method === "read") {
+ assert.step("read");
+ }
+ },
+ resId: 1,
+ resIds: [1, 2],
+ });
+
+ assert.verifySteps(["read"], "update should not have been called yet");
+
+ // switch to next record
+ simulateBarCode(
+ ["O", "-", "C", "M", "D", ".", "N", "E", "X", "T", "Enter"],
+ document.activeElement
+ );
+ await nextTick();
+ // a first update is done to reload the data (thus followed by a read), but
+ // update shouldn't be called afterwards
+ assert.verifySteps(["update", "read"]);
+ });
+});
diff --git a/addons/barcodes/static/tests/helpers.js b/addons/barcodes/static/tests/helpers.js
new file mode 100644
index 00000000000..e0ec5a89f02
--- /dev/null
+++ b/addons/barcodes/static/tests/helpers.js
@@ -0,0 +1,20 @@
+/* @odoo-module */
+
+import { triggerEvent } from "@web/../tests/helpers/utils";
+
+export function simulateBarCode(chars, target = document.body, selector = undefined) {
+ for (let char of chars) {
+ let keycode;
+ if (char === "Enter") {
+ keycode = $.ui.keyCode.ENTER;
+ } else if (char === "Tab") {
+ keycode = $.ui.keyCode.TAB;
+ } else {
+ keycode = char.charCodeAt(0);
+ }
+ triggerEvent(target, selector, "keydown", {
+ key: char,
+ keyCode: keycode,
+ });
+ }
+}
diff --git a/addons/barcodes/static/tests/barcode_mobile_tests.js b/addons/barcodes/static/tests/mobile/barcode_mobile_tests.js
similarity index 100%
rename from addons/barcodes/static/tests/barcode_mobile_tests.js
rename to addons/barcodes/static/tests/mobile/barcode_mobile_tests.js
diff --git a/addons/web/static/src/views/fields/float/float_field.js b/addons/web/static/src/views/fields/float/float_field.js
index 66e333804ea..494ba6fca6f 100644
--- a/addons/web/static/src/views/fields/float/float_field.js
+++ b/addons/web/static/src/views/fields/float/float_field.js
@@ -12,7 +12,7 @@ const { Component } = owl;
export class FloatField extends Component {
setup() {
- useInputField({
+ this.inputRef = useInputField({
getValue: () => this.formattedValue,
refName: "numpadDecimal",
parse: (v) => this.parse(v),
diff --git a/addons/web/static/src/views/fields/input_field_hook.js b/addons/web/static/src/views/fields/input_field_hook.js
index e4574968eb7..4bfcc32e76a 100644
--- a/addons/web/static/src/views/fields/input_field_hook.js
+++ b/addons/web/static/src/views/fields/input_field_hook.js
@@ -152,4 +152,6 @@ export function useInputField(params) {
}
}
}
+
+ return inputRef;
}
diff --git a/addons/web/static/src/views/form/form_controller.js b/addons/web/static/src/views/form/form_controller.js
index 8560c85f545..875aee6fc6b 100644
--- a/addons/web/static/src/views/form/form_controller.js
+++ b/addons/web/static/src/views/form/form_controller.js
@@ -201,19 +201,7 @@ export class FormController extends Component {
offset: resIds.indexOf(this.model.root.resId),
limit: 1,
total: resIds.length,
- onUpdate: async ({ offset }) => {
- await this.model.root.askChanges(); // ensures that isDirty is correct
- let canProceed = true;
- if (this.model.root.isDirty) {
- canProceed = await this.model.root.save({
- stayInEdition: true,
- useSaveErrorDialog: true,
- });
- }
- if (canProceed) {
- return this.model.load({ resId: resIds[offset] });
- }
- },
+ onUpdate: ({ offset }) => this.onPagerUpdate({ offset, resIds }),
};
}
});
@@ -263,6 +251,20 @@ export class FormController extends Component {
return this.model.root.data.display_name || this.env._t("New");
}
+ async onPagerUpdate({ offset, resIds }) {
+ await this.model.root.askChanges(); // ensures that isDirty is correct
+ let canProceed = true;
+ if (this.model.root.isDirty) {
+ canProceed = await this.model.root.save({
+ stayInEdition: true,
+ useSaveErrorDialog: true,
+ });
+ }
+ if (canProceed) {
+ return this.model.load({ resId: resIds[offset] });
+ }
+ }
+
beforeUnload() {
return this.model.root.urgentSave();
}