diff --git a/addons/base_iban/__manifest__.py b/addons/base_iban/__manifest__.py
index 66af15cef9b..8ae9b42fbe2 100644
--- a/addons/base_iban/__manifest__.py
+++ b/addons/base_iban/__manifest__.py
@@ -18,8 +18,8 @@ with a single statement.
'demo': ['data/res_partner_bank_demo.xml'],
'assets': {
'web.assets_backend': [
- 'base_iban/static/src/js/iban_widget.js',
- 'base_iban/static/src/scss/iban_widget_view.scss',
+ 'base_iban/static/src/components/**/*',
+ 'base_iban/static/src/js/*.js',
],
'web.qunit_suite_tests': [
'base_iban/static/src/tests/**/*',
diff --git a/addons/base_iban/static/src/components/iban_widget/iban_widget.js b/addons/base_iban/static/src/components/iban_widget/iban_widget.js
new file mode 100644
index 00000000000..991c237f5ff
--- /dev/null
+++ b/addons/base_iban/static/src/components/iban_widget/iban_widget.js
@@ -0,0 +1,30 @@
+/** @odoo-module **/
+
+import { registry } from "@web/core/registry";
+import { CharField } from "@web/views/fields/char/char_field";
+import { useDebounced } from "@web/core/utils/timing";
+import { useService } from "@web/core/utils/hooks";
+
+const { useState } = owl;
+export const DELAY = 400;
+
+export class IbanWidget extends CharField {
+ setup() {
+ super.setup();
+ this.state = useState({ isValidIBAN: null });
+ this.orm = useService("orm");
+ this.validateIbanDebounced = useDebounced(async (ev) => {
+ const iban = ev.target.value;
+ if (!iban) {
+ this.state.isValidIBAN = null;
+ } else if (!/[A-Za-z]{2}.{3,}/.test(iban)) {
+ this.state.isValidIBAN = false;
+ } else {
+ this.state.isValidIBAN = await this.orm.call("res.partner.bank", "check_iban", [[], iban]);
+ }
+ }, DELAY);
+ }
+}
+IbanWidget.template = "base_iban.iban";
+
+registry.category("fields").add("iban", IbanWidget);
diff --git a/addons/base_iban/static/src/scss/iban_widget_view.scss b/addons/base_iban/static/src/components/iban_widget/iban_widget.scss
similarity index 64%
rename from addons/base_iban/static/src/scss/iban_widget_view.scss
rename to addons/base_iban/static/src/components/iban_widget/iban_widget.scss
index a226960e975..10ab6c8187c 100644
--- a/addons/base_iban/static/src/scss/iban_widget_view.scss
+++ b/addons/base_iban/static/src/components/iban_widget/iban_widget.scss
@@ -4,13 +4,12 @@
.o_iban {
display: inline-flex;
margin-left: -15px;
- margin-top: 5px;
}
.o_iban_fail {
cursor: help;
}
- .o_iban_input_with_validator {
- padding-right: 20px!important;
+ input.o_input.o_iban_input_with_validator {
+ padding-right: 17px!important;
}
}
diff --git a/addons/base_iban/static/src/components/iban_widget/iban_widget.xml b/addons/base_iban/static/src/components/iban_widget/iban_widget.xml
new file mode 100644
index 00000000000..c0b95eac41f
--- /dev/null
+++ b/addons/base_iban/static/src/components/iban_widget/iban_widget.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+ validateIbanDebounced
+ validateIbanDebounced
+ (ev) => { this.state.isValidIBAN = null; }
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/base_iban/static/src/js/iban_widget.js b/addons/base_iban/static/src/js/legacy_iban_widget.js
similarity index 100%
rename from addons/base_iban/static/src/js/iban_widget.js
rename to addons/base_iban/static/src/js/legacy_iban_widget.js
diff --git a/addons/base_iban/static/src/tests/helpers/model_definitions_setup.js b/addons/base_iban/static/src/tests/helpers/model_definitions_setup.js
new file mode 100644
index 00000000000..81ca26b62ec
--- /dev/null
+++ b/addons/base_iban/static/src/tests/helpers/model_definitions_setup.js
@@ -0,0 +1,5 @@
+/** @odoo-module **/
+
+import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
+
+addModelNamesToFetch(['res.partner.bank']);
diff --git a/addons/base_iban/static/src/tests/iban_widget_test.js b/addons/base_iban/static/src/tests/iban_widget_test.js
index 3fe954d73f0..b3848d2d27f 100644
--- a/addons/base_iban/static/src/tests/iban_widget_test.js
+++ b/addons/base_iban/static/src/tests/iban_widget_test.js
@@ -1,65 +1,94 @@
-odoo.define('base_iban.iban_widget_tests', function (require) {
-"use strict";
+/** @odoo-module **/
+import { start, startServer } from '@mail/../tests/helpers/test_utils';
+import { click, clickEdit, clickSave, editInput, getFixture } from "@web/../tests/helpers/utils";
+import { DELAY } from "@base_iban/components/iban_widget/iban_widget";
-var FormView = require('web.FormView');
-var testUtils = require('web.test_utils');
-var createView = testUtils.createView;
+QUnit.module('Fields', {}, function () {
+ QUnit.module("IbanWidget");
+ const [validIban, invalidIban] = ["BE12651194580992", "invalidIban!"];
-QUnit.module('fields', {
- beforeEach: function () {
- this.data = {
- partner: {
- fields: {
- acc_number: {string: "acc_number", type: "char"},
- },
- records: [{
- id: 1,
- acc_number: "",
- }]
+ const openPreparedView = async (assert, validIbanList, startingAccNumber) => {
+ const target = getFixture();
+ const pyEnv = await startServer();
+ const partnerId = pyEnv['res.partner'].create([
+ {
+ name: "Awesome partner",
+ bank_ids: [
+ pyEnv['res.partner.bank'].create([{ acc_number: startingAccNumber }]),
+ ],
},
+ ]);
+ const views = {
+ 'res.partner,false,form':
+ `
`,
};
- // patch _.debounce to be fast and synchronous
- this.underscoreDebounce = _.debounce;
- _.debounce = _.identity;
- },
- afterEach: function () {
- // unpatch _.debounce
- _.debounce = this.underscoreDebounce;
- }
-}, function () {
-
- QUnit.module('IbanWidget');
-
- QUnit.test('Iban widgets are correctly rendered', async function (assert) {
- assert.expect(6);
- var form = await createView({
- View: FormView,
- model: 'partner',
- data: this.data,
- arch: '',
+ const { advanceTime, afterNextRender, openView } = await start({
+ serverData: { views },
+ hasTimeControl: true,
mockRPC: function (route, args) {
- if (args.method === 'check_iban') {
- console.log(args.args[1] === "BE00 0000 0000 0000 0000")
- return Promise.resolve(args.args[1] === "BE00 0000 0000 0000 0000");
+ if (args.method === "check_iban") {
+ const iban = args.args[1].replace(/\s/g, '');
+ return Promise.resolve(iban === validIban);
}
- return this._super.apply(this, arguments);
},
});
+ await openView({
+ res_id: partnerId,
+ res_model: 'res.partner',
+ views: [[false, 'form']],
+ });
+ return { target, advanceTime, afterNextRender };
+ };
- await testUtils.fields.editAndTrigger(form.$('.o_field_widget'), "BE00", 'input');
- assert.containsOnce(form, '.o_iban_fail', "Should be a False account, it's too short");
- assert.containsOnce(form, '.fa-times', "Should have a cross pictogram");
+ QUnit.test('Iban Widget full flow', async assert => {
+ const { target, advanceTime, afterNextRender } = await openPreparedView(assert, [validIban], "");
- await testUtils.fields.editAndTrigger(form.$('.o_field_widget'), "BE00 0000 0000 0000 0000", 'input');
- assert.containsOnce(form, '.text-success', "Should have text-success");
- assert.containsOnce(form, '.fa-check', "Should have a valid pictogram");
+ assert.containsNone(target, ".o_iban", "Shouldn't display any validation icon while not editing");
+ await clickEdit(target);
+ assert.containsNone(target, ".o_iban",
+ "Shouldn't display any validation icon while not editing a specific line");
- await testUtils.fields.editAndTrigger(form.$('.o_field_widget'), "BE00 xxxx xxxx xxxx xxxx", 'input');
- assert.containsOnce(form, '.o_iban_fail', "Should be False account");
- assert.containsOnce(form, '.fa-times', "Should have a cross pictogram");
+ await click(target, "td.o_iban_cell");
+ assert.containsNone(target, ".o_iban", "Shouldn't display any validation icon while iban is empty");
- form.destroy();
+ await editInput(target, ".o_iban_cell .o_input", invalidIban);
+ assert.containsNone(target, ".o_iban", "Shouldn't change its state of display before edition is finished");
+ await afterNextRender(() => advanceTime(DELAY));
+ assert.containsOnce(target, ".o_iban", "Should contain a validation icon 400ms after edition");
+ assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon should be the failed one");
+ assert.containsNone(target, "i.fa.fa-check.o_iban", "The validation icon shouldn't be the successful one");
+
+ await clickSave(target);
+ assert.containsNone(target, ".o_iban", "Shouldn't display any validation while not editing");
+
+ await clickEdit(target);
+ assert.containsNone(target, ".o_iban",
+ "Shouldn't display any validation icon while not editing a specific line");
+
+ await click(target, "td.o_iban_cell");
+ await afterNextRender(() => advanceTime(DELAY));
+ assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon should be present while clicking on an already filled IBAN");
+
+ await editInput(target, ".o_iban_cell .o_input", validIban);
+ assert.containsOnce(target, "i.fa.fa-times.o_iban_fail", "The validation icon shouldn't change during the edition");
+ await afterNextRender(() => advanceTime(DELAY));
+ assert.containsOnce(target, ".o_iban", "Should contain a validation icon 400ms after edition");
+ assert.containsOnce(target, "i.fa.fa-check.o_iban", "The validation icon should be the successful one");
+ assert.containsNone(target, "i.fa.fa-times.o_iban_fail", "The validation icon shouldn't be the failed one");
+
+ await clickSave(target);
+ assert.containsNone(target, ".o_iban", "Shouldn't display any validation while not editing");
});
});
-});