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(); }