summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/fields
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/tests/fields
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/web/static/tests/fields')
-rw-r--r--addons/web/static/tests/fields/basic_fields_mobile_tests.js227
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js7807
-rw-r--r--addons/web/static/tests/fields/field_utils_tests.js437
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2many_tests.js1809
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_many2one_tests.js3565
-rw-r--r--addons/web/static/tests/fields/relational_fields/field_one2many_tests.js9959
-rw-r--r--addons/web/static/tests/fields/relational_fields_mobile_tests.js66
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js3679
-rw-r--r--addons/web/static/tests/fields/signature_tests.js217
-rw-r--r--addons/web/static/tests/fields/special_fields_tests.js365
-rw-r--r--addons/web/static/tests/fields/upgrade_fields_tests.js66
11 files changed, 28197 insertions, 0 deletions
diff --git a/addons/web/static/tests/fields/basic_fields_mobile_tests.js b/addons/web/static/tests/fields/basic_fields_mobile_tests.js
new file mode 100644
index 00000000..4b8b4353
--- /dev/null
+++ b/addons/web/static/tests/fields/basic_fields_mobile_tests.js
@@ -0,0 +1,227 @@
+odoo.define('web.basic_fields_mobile_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('basic_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ date: {string: "A date", type: "date", searchable: true},
+ datetime: {string: "A datetime", type: "datetime", searchable: true},
+ display_name: {string: "Displayed name", type: "char", searchable: true},
+ foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true, trim: true},
+ bar: {string: "Bar", type: "boolean", default: true, searchable: true},
+ int_field: {string: "int_field", type: "integer", sortable: true, searchable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1], searchable: true},
+ },
+ records: [{
+ id: 1,
+ date: "2017-02-03",
+ datetime: "2017-02-08 10:00:00",
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44444,
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 0,
+ qux: 0,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ foo: "abc",
+ int_field: false,
+ qux: false,
+ }],
+ onchanges: {},
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('PhoneWidget');
+
+ QUnit.test('phone field in form view on extra small screens', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $phoneLink = form.$('a.o_form_uri.o_field_widget');
+ assert.strictEqual($phoneLink.length, 1,
+ "should have a anchor with correct classes");
+ assert.strictEqual($phoneLink.text(), 'yop',
+ "value should be displayed properly");
+ assert.hasAttrValue($phoneLink, 'href', 'tel:yop',
+ "should have proper tel prefix");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'input[type="text"].o_field_widget',
+ "should have an int for the phone field");
+ assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new');
+
+ // save
+ await testUtils.form.clickSave(form);
+ $phoneLink = form.$('a.o_form_uri.o_field_widget');
+ assert.strictEqual($phoneLink.text(), 'new',
+ "new value should be displayed properly");
+ assert.hasAttrValue($phoneLink, 'href', 'tel:new',
+ "should still have proper tel prefix");
+
+ form.destroy();
+ });
+
+ QUnit.test('phone field in editable list view on extra small screens', async function (assert) {
+ assert.expect(10);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
+ });
+
+ assert.containsN(list, '.o_data_row', 3,
+ "should have 3 record");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'yop',
+ "value should be displayed properly");
+
+ var $phoneLink = list.$('a.o_form_uri.o_field_widget');
+ assert.strictEqual($phoneLink.length, 3,
+ "should have anchors with correct classes");
+ assert.hasAttrValue($phoneLink.first(), 'href', 'tel:yop',
+ "should have proper tel prefix");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'new');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'new',
+ "value should be properly updated");
+ $phoneLink = list.$('a.o_form_uri.o_field_widget');
+ assert.strictEqual($phoneLink.length, 3,
+ "should still have anchors with correct classes");
+ assert.hasAttrValue($phoneLink.first(), 'href', 'tel:new',
+ "should still have proper tel prefix");
+
+ list.destroy();
+ });
+
+ QUnit.test('phone field does not allow html injections', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var val = '<script>throw Error();</script><script>throw Error();</script>';
+ await testUtils.fields.editInput(form.$('input.o_field_widget[name="foo"]'), val);
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), val,
+ "value should have been correctly escaped");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldDateRange');
+
+ QUnit.test('date field: toggle daterangepicker then scroll', async function (assert) {
+ assert.expect(4);
+ const scrollEvent = new UIEvent('scroll');
+
+ function scrollAtHeight(height) {
+ window.scrollTo(0, height);
+ document.dispatchEvent(scrollEvent);
+ }
+ this.data.partner.fields.date_end = {string: 'Date End', type: 'date'};
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="date" widget="daterange" options="{\'related_end_date\': \'date_end\'}"/>' +
+ '<field name="date_end" widget="daterange" options="{\'related_start_date\': \'date\'}"/>' +
+ '</form>',
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ });
+
+ // Check date range picker initialization
+ assert.containsN(document.body, '.daterangepicker', 2,
+ "should initialize 2 date range picker");
+
+ // Open date range picker
+ await testUtils.dom.click("input[name=date]");
+ assert.isVisible($('.daterangepicker:first'),
+ "date range picker should be opened");
+
+ // Scroll
+ scrollAtHeight(50);
+ assert.isVisible($('.daterangepicker:first'),
+ "date range picker should be opened");
+
+ // Close picker
+ await testUtils.dom.click($('.daterangepicker:first .cancelBtn'));
+ assert.isNotVisible($('.daterangepicker:first'),
+ "date range picker should be closed");
+
+ form.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
new file mode 100644
index 00000000..4ccdd292
--- /dev/null
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -0,0 +1,7807 @@
+odoo.define('web.basic_fields_tests', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var basicFields = require('web.basic_fields');
+var concurrency = require('web.concurrency');
+var config = require('web.config');
+var core = require('web.core');
+var FormView = require('web.FormView');
+var KanbanView = require('web.KanbanView');
+var ListView = require('web.ListView');
+var session = require('web.session');
+var testUtils = require('web.test_utils');
+var testUtilsDom = require('web.test_utils_dom');
+var field_registry = require('web.field_registry');
+
+var createView = testUtils.createView;
+var patchDate = testUtils.mock.patchDate;
+
+var DebouncedField = basicFields.DebouncedField;
+var JournalDashboardGraph = basicFields.JournalDashboardGraph;
+var _t = core._t;
+
+// Base64 images for testing purpose
+const MY_IMAGE = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
+const PRODUCT_IMAGE = 'R0lGODlhDAAMAKIFAF5LAP/zxAAAANyuAP/gaP///wAAAAAAACH5BAEAAAUALAAAAAAMAAwAAAMlWLPcGjDKFYi9lxKBOaGcF35DhWHamZUW0K4mAbiwWtuf0uxFAgA7';
+const FR_FLAG_URL = '/base/static/img/country_flags/fr.png';
+const EN_FLAG_URL = '/base/static/img/country_flags/gb.png';
+
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('basic_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ date: {string: "A date", type: "date", searchable: true},
+ datetime: {string: "A datetime", type: "datetime", searchable: true},
+ display_name: {string: "Displayed name", type: "char", searchable: true},
+ foo: {string: "Foo", type: "char", default: "My little Foo Value", searchable: true, trim: true},
+ bar: {string: "Bar", type: "boolean", default: true, searchable: true},
+ empty_string: {string: "Empty string", type: "char", default: false, searchable: true, trim: true},
+ txt: {string: "txt", type: "text", default: "My little txt Value\nHo-ho-hoooo Merry Christmas"},
+ int_field: {string: "int_field", type: "integer", sortable: true, searchable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1], searchable: true},
+ p: {string: "one2many field", type: "one2many", relation: 'partner', searchable: true},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner', searchable: true},
+ timmy: {string: "pokemon", type: "many2many", relation: 'partner_type', searchable: true},
+ product_id: {string: "Product", type: "many2one", relation: 'product', searchable: true},
+ sequence: {type: "integer", string: "Sequence", searchable: true},
+ currency_id: {string: "Currency", type: "many2one", relation: "currency", searchable: true},
+ selection: {string: "Selection", type: "selection", searchable:true,
+ selection: [['normal', 'Normal'],['blocked', 'Blocked'],['done', 'Done']]},
+ document: {string: "Binary", type: "binary"},
+ hex_color: {string: "hexadecimal color", type: "char"},
+ },
+ records: [{
+ id: 1,
+ date: "2017-02-03",
+ datetime: "2017-02-08 10:00:00",
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44444,
+ p: [],
+ timmy: [],
+ trululu: 4,
+ selection: 'blocked',
+ document: 'coucou==\n',
+ hex_color: '#ff0000',
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 0,
+ qux: 0,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ sequence: 4,
+ currency_id: 2,
+ selection: 'normal',
+ }, {
+ id: 4,
+ display_name: "aaa",
+ foo: "abc",
+ sequence: 9,
+ int_field: false,
+ qux: false,
+ selection: 'done',
+ },
+ {id: 3, bar: true, foo: "gnap", int_field: 80, qux: -3.89859},
+ {id: 5, bar: false, foo: "blop", int_field: -4, qux: 9.1, currency_id: 1}],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char", searchable: true}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char", searchable: true},
+ color: {string: "Color index", type: "integer", searchable: true},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ currency: {
+ fields: {
+ digits: { string: "Digits" },
+ symbol: {string: "Currency Sumbol", type: "char", searchable: true},
+ position: {string: "Currency Position", type: "char", searchable: true},
+ },
+ records: [{
+ id: 1,
+ display_name: "$",
+ symbol: "$",
+ position: "before",
+ }, {
+ id: 2,
+ display_name: "€",
+ symbol: "€",
+ position: "after",
+ }]
+ },
+ "ir.translation": {
+ fields: {
+ lang_code: {type: "char"},
+ value: {type: "char"},
+ res_id: {type: "integer"}
+ },
+ records: [{
+ id: 99,
+ res_id: 37,
+ value: '',
+ lang_code: 'en_US'
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('DebouncedField');
+
+ QUnit.test('debounced fields do not trigger call _setValue once destroyed', async function (assert) {
+ assert.expect(4);
+
+ var def = testUtils.makeTestPromise();
+ var _doAction = DebouncedField.prototype._doAction;
+ DebouncedField.prototype._doAction = function () {
+ _doAction.apply(this, arguments);
+ def.resolve();
+ };
+ var _setValue = DebouncedField.prototype._setValue;
+ DebouncedField.prototype._setValue = function () {
+ assert.step('_setValue');
+ _setValue.apply(this, arguments);
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ fieldDebounce: 3,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // change the value
+ testUtils.fields.editInput(form.$('input[name=foo]'), 'new value');
+ assert.verifySteps([], "_setValue shouldn't have been called yet");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.verifySteps(['_setValue'], "_setValue should have been called once");
+
+ // destroy the form view
+ def = testUtils.makeTestPromise();
+ form.destroy();
+ await testUtils.nextMicrotaskTick();
+
+ // wait for the debounced callback to be called
+ assert.verifySteps([],
+ "_setValue should not have been called after widget destruction");
+
+ DebouncedField.prototype._doAction = _doAction;
+ DebouncedField.prototype._setValue = _setValue;
+ });
+
+ QUnit.module('FieldBoolean');
+
+ QUnit.test('boolean field in form view', async function (assert) {
+ assert.expect(13);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><label for="bar" string="Awesome checkbox"/><field name="bar"/></form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should be checked");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should still be checked");
+
+ // uncheck the checkbox
+ await testUtils.dom.click(form.$('.o_field_boolean input:checked'));
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should no longer be checked");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should still no longer be checked");
+
+ // switch to edit mode and test the opposite change
+ await testUtils.form.clickEdit(form);
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should still be unchecked");
+
+ // check the checkbox
+ await testUtils.dom.click(form.$('.o_field_boolean input'));
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should now be checked");
+
+ // uncheck it back
+ await testUtils.dom.click(form.$('.o_field_boolean input'));
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should now be unchecked");
+
+ // check the checkbox by clicking on label
+ await testUtils.dom.click(form.$('.o_form_view label:first'));
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should now be checked");
+
+ // uncheck it back
+ await testUtils.dom.click(form.$('.o_form_view label:first'));
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should now be unchecked");
+
+ // check the checkbox by hitting the "enter" key after focusing it
+ await testUtils.dom.triggerEvents(form.$('.o_field_boolean input'), [
+ "focusin",
+ {type: "keydown", which: $.ui.keyCode.ENTER},
+ {type: "keyup", which: $.ui.keyCode.ENTER}]);
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should now be checked");
+ // blindly press enter again, it should uncheck the checkbox
+ await testUtils.dom.triggerEvent(document.activeElement, "keydown",
+ {which: $.ui.keyCode.ENTER});
+ assert.containsNone(form, '.o_field_boolean input:checked',
+ "checkbox should not be checked");
+ await testUtils.nextTick();
+ // blindly press enter again, it should check the checkbox back
+ await testUtils.dom.triggerEvent(document.activeElement, "keydown",
+ {which: $.ui.keyCode.ENTER});
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should still be checked");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_boolean input:checked',
+ "checkbox should still be checked");
+ form.destroy();
+ });
+
+ QUnit.test('boolean field in editable list view', async function (assert) {
+ assert.expect(11);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="bar"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5,
+ "should have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 4,
+ "should have 4 checked input");
+
+ // Edit a line
+ var $cell = list.$('tr.o_data_row:has(.custom-checkbox input:checked) td:not(.o_list_record_selector)').first();
+ assert.ok($cell.find('.custom-checkbox input:checked').prop('disabled'),
+ "input should be disabled in readonly mode");
+ await testUtils.dom.click($cell);
+ assert.ok(!$cell.find('.custom-checkbox input:checked').prop('disabled'),
+ "input should not have the disabled property in edit mode");
+ await testUtils.dom.click($cell.find('.custom-checkbox input:checked'));
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
+ assert.ok($cell.find('.custom-checkbox input:not(:checked)').prop('disabled'),
+ "input should be disabled again");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 3,
+ "should now have only 3 checked input");
+
+ // Re-Edit the line and fake-check the checkbox
+ await testUtils.dom.click($cell);
+ await testUtils.dom.click($cell.find('.custom-checkbox input'));
+ await testUtils.dom.click($cell.find('.custom-checkbox input'));
+
+ // Save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 3,
+ "should still have only 3 checked input");
+
+ // Re-Edit the line to check the checkbox back but this time click on
+ // the checkbox directly in readonly mode !
+ $cell = list.$('tr.o_data_row:has(.custom-checkbox input:not(:checked)) td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell.find('.custom-checkbox .custom-control-label'));
+ await testUtils.nextTick();
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input').length, 5,
+ "should still have 5 checkboxes");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) .custom-checkbox input:checked').length, 4,
+ "should now have 4 checked input back");
+ list.destroy();
+ });
+
+ QUnit.module('FieldBooleanToggle');
+
+ QUnit.test('use boolean toggle widget in form view', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="bar" widget="boolean_toggle"/></form>',
+ res_id: 2,
+ });
+
+ assert.containsOnce(form, ".custom-checkbox.o_boolean_toggle", "Boolean toggle widget applied to boolean field");
+ form.destroy();
+ });
+
+ QUnit.module('FieldToggleButton');
+
+ QUnit.test('use toggle_button in list view', async function (assert) {
+ assert.expect(6);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="bar" widget="toggle_button" ' +
+ 'options="{&quot;active&quot;: &quot;Reported in last payslips&quot;, &quot;inactive&quot;: &quot;To Report in Payslip&quot;}"/>' +
+ '</tree>',
+ });
+
+ assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 4,
+ "should have 4 green buttons");
+ assert.containsOnce(list, 'button i.fa.fa-circle.text-muted',
+ "should have 1 muted button");
+
+ assert.hasAttrValue(list.$('.o_list_view button').first(), 'title',
+ "Reported in last payslips", "active buttons should have proper tooltip");
+ assert.hasAttrValue(list.$('.o_list_view button').last(), 'title',
+ "To Report in Payslip", "inactive buttons should have proper tooltip");
+
+ // clicking on first button to check the state is properly changed
+ await testUtils.dom.click(list.$('.o_list_view button').first());
+ assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 3,
+ "should have 3 green buttons");
+
+ await testUtils.dom.click(list.$('.o_list_view button').first());
+ assert.containsN(list, 'button i.fa.fa-circle.o_toggle_button_success', 4,
+ "should have 4 green buttons");
+ list.destroy();
+ });
+
+ QUnit.test('toggle_button in form view (edit mode)', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar" widget="toggle_button" ' +
+ 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.step('write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 2,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
+ 1, "should be green");
+
+ // click on the button to toggle the value
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar]'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should be gray");
+ assert.verifySteps([]);
+
+ // save
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should still be gray");
+ assert.verifySteps(['write']);
+
+ form.destroy();
+ });
+
+ QUnit.test('toggle_button in form view (readonly mode)', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar" widget="toggle_button" ' +
+ 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.step('write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length,
+ 1, "should be green");
+
+ // click on the button to toggle the value
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar]'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length,
+ 1, "should be gray");
+ assert.verifySteps(['write']);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldFloat');
+
+ QUnit.test('float field when unset', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 4,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Non-set float field should be considered as 0.');
+ assert.strictEqual(form.$('.o_field_widget').text(), "0.000",
+ 'Non-set float field should be considered as 0.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float fields use correct digit precision', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('span.o_field_number:contains(0.4)').length, 1,
+ "should contain a number rounded to 1 decimal");
+ form.destroy();
+ });
+
+ QUnit.test('float field in list view no widget', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Float field should be considered set for value 0.');
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=qux]').val(), '0.000',
+ 'The value should be rendered with correct precision.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598');
+ assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field in form view', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Float field should be considered set for value 0.');
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.000',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=qux]').val(), '0.000',
+ 'The value should be rendered with correct precision.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '108.2458938598598');
+ assert.strictEqual(form.$('input[name=qux]').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '18.8958938598598');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '18.896',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field using formula in form view', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // Test computation with priority of operation
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=20+3*2');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '26.000',
+ 'The new value should be calculated properly.');
+
+ // Test computation with ** operand
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=2**3');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '8.000',
+ 'The new value should be calculated properly.');
+
+ // Test computation with ^ operant which should do the same as **
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=2^3');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '8.000',
+ 'The new value should be calculated properly.');
+
+ // Test computation and rounding
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=100/3');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '33.333',
+ 'The new value should be calculated properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field using incorrect formula in form view', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // Test that incorrect value is not computed
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=abc');
+ await testUtils.form.clickSave(form);
+ assert.hasClass(form.$('.o_form_view'),'o_form_editable',
+ "form view should still be editable");
+ assert.hasClass(form.$('input[name=qux]'),'o_field_invalid',
+ "fload field should be displayed as invalid");
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '=3:2?+4');
+ await testUtils.form.clickSave(form);
+ assert.hasClass(form.$('.o_form_view'),'o_form_editable',
+ "form view should still be editable");
+ assert.hasClass(form.$('input[name=qux]'),'o_field_invalid',
+ "float field should be displayed as invalid");
+
+ form.destroy();
+ });
+
+ QUnit.test('float field in editable list view', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '</tree>',
+ });
+
+ var zeroValues = list.$('td.o_data_cell').filter(function () {return $(this).text() === '';});
+ assert.strictEqual(zeroValues.length, 1,
+ 'Unset float values should be rendered as empty strings.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+
+ assert.containsOnce(list, 'input[name="qux"]',
+ 'The view should have 1 input for editable float.');
+
+ await testUtils.fields.editInput(list.$('input[name="qux"]'), '108.2458938598598');
+ assert.strictEqual(list.$('input[name="qux"]').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.fields.editInput(list.$('input[name="qux"]'), '18.8958938598598');
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('.o_field_widget').first().text(), '18.896',
+ 'The new value should be rounded properly.');
+
+ list.destroy();
+ });
+
+ QUnit.test('do not trigger a field_changed if they have not changed', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[1].qux = false;
+ this.data.partner.records[1].int_field = false;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float" digits="[5,3]"/>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['read']); // should not have save as nothing changed
+
+ form.destroy();
+ });
+
+ QUnit.test('float widget on monetary field', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'};
+ this.data.partner.records[0].monetary = 9.99;
+ this.data.partner.records[0].currency_id = 1;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="monetary" widget="float"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=monetary]').text(), '9.99',
+ 'value should be correctly formatted (with the float formatter)');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field with monetary widget and decimal precision', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [{
+ id: 1,
+ qux: -8.89859,
+ currency_id: 1,
+ }];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary" options="{\'field_digits\': True}"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0-8.9',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '-8.9',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ await testUtils.fields.editInput(form.$('.o_field_monetary input'), '109.2458938598598');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '109.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.form.clickSave(form);
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0109.2',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field with type number option', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="qux" options="{\'type\': \'number\'}"/>' +
+ '</form>',
+ res_id: 4,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'),
+ 'Float field with option type must have a type attribute.');
+ assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number',
+ 'Float field with option type must have a type attribute equals to "number".');
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget').val(), '123456.789',
+ 'Float value must be not formatted if input type is number.');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '123,456.8',
+ 'Float value must be formatted in readonly view even if the input type is number.');
+
+ form.destroy();
+ });
+
+ QUnit.test('float field with type number option and comma decimal separator', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="qux" options="{\'type\': \'number\'}"/>' +
+ '</form>',
+ res_id: 4,
+ translateParameters: {
+ thousands_sep: ".",
+ decimal_point: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'),
+ 'Float field with option type must have a type attribute.');
+ assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number',
+ 'Float field with option type must have a type attribute equals to "number".');
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.789');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget').val(), '123456.789',
+ 'Float value must be not formatted if input type is number.');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '123.456,8',
+ 'Float value must be formatted in readonly view even if the input type is number.');
+
+ form.destroy();
+ });
+
+
+ QUnit.test('float field without type number option', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="qux"/>' +
+ '</form>',
+ res_id: 4,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text',
+ 'Float field with option type must have a text type (default type).');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '123456.7890');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget').val(), '123,456.8',
+ 'Float value must be formatted if input type isn\'t number.');
+
+ form.destroy();
+ });
+
+ QUnit.module('Percentage');
+
+ QUnit.test('percentage widget in form view', async function (assert) {
+ assert.expect(6);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: ` <form string="Partners">
+ <field name="qux" widget="percentage"/>
+ </form>`,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].qux, 0.24, 'the correct float value should be saved');
+ }
+ return this._super(...arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '44.4%',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '44.4',
+ 'The input should be rendered without the percentage symbol.');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] span').text(), '%',
+ 'The input should be followed by a span containing the percentage symbol.');
+
+ await testUtils.fields.editInput(form.$('.o_field_float_percentage input'), '24');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '24',
+ 'The value should not be formated yet.');
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '24%',
+ 'The new value should be formatted properly.');
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldEmail');
+
+ QUnit.test('email field in form view', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="email"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 1,
+ "should have a anchor with correct classes");
+ assert.strictEqual($mailtoLink.text(), 'yop',
+ "the value should be displayed properly");
+ assert.hasAttrValue($mailtoLink, 'href', 'mailto:yop',
+ "should have proper mailto prefix");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'input[type="text"].o_field_widget',
+ "should have an input for the email field");
+ assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new');
+
+ // save
+ await testUtils.form.clickSave(form);
+ $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.text(), 'new',
+ "new value should be displayed properly");
+ assert.hasAttrValue($mailtoLink, 'href', 'mailto:new',
+ "should still have proper mailto prefix");
+
+ form.destroy();
+ });
+
+ QUnit.test('email field in editable list view', async function (assert) {
+ assert.expect(10);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="email"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ var $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 5,
+ "should have anchors with correct classes");
+ assert.hasAttrValue($mailtoLink.first(), 'href', 'mailto:yop',
+ "should have proper mailto prefix");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'new');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'new',
+ "value should be properly updated");
+ $mailtoLink = list.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.length, 5,
+ "should still have anchors with correct classes");
+ assert.hasAttrValue($mailtoLink.first(), 'href', 'mailto:new',
+ "should still have proper mailto prefix");
+
+ list.destroy();
+ });
+
+ QUnit.test('email field with empty value', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="empty_string" widget="email"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $mailtoLink = form.$('a.o_form_uri.o_field_widget.o_text_overflow');
+ assert.strictEqual($mailtoLink.text(), '',
+ "the value should be displayed properly");
+
+ form.destroy();
+ });
+
+ QUnit.test('email field trim user value', async function (assert) {
+ assert.expect(1);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="foo" widget="email"/></form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), ' abc@abc.com ');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'abc@abc.com',
+ "Foo value should have been trimmed");
+
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldChar');
+
+ QUnit.test('char widget isValid method works', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.required = true;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var charField = _.find(form.renderer.allFieldWidgets)[0];
+ assert.strictEqual(charField.isValid(), true);
+ form.destroy();
+ });
+
+ QUnit.test('char field in form view', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').text(), 'yop',
+ "the value should be displayed properly");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'input[type="text"].o_field_widget',
+ "should have an input for the char field");
+ assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'limbo');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), 'limbo',
+ 'the new value should be displayed');
+ form.destroy();
+ });
+
+ QUnit.test('setting a char field to empty string is saved as a false value', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].foo, false,
+ 'the foo value should be false');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), '');
+
+ // save
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('char field with size attribute', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.size = 5; // max length
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group><field name="foo"/></group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.hasAttrValue(form.$('input.o_field_widget'), 'maxlength', '5',
+ "maxlength attribute should have been set correctly on the input");
+
+ form.destroy();
+ });
+
+ QUnit.test('char field in editable list view', async function (assert) {
+ assert.expect(6);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'brolo');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'brolo',
+ "value should be properly updated");
+ list.destroy();
+ });
+
+ QUnit.test('char field translatable', async function (assert) {
+ assert.expect(12);
+
+ this.data.partner.fields.foo.translate = true;
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ user_context: {lang: 'en_US'},
+ },
+ mockRPC: function (route, args) {
+ if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route');
+ return Promise.resolve({
+ domain: [],
+ context: {search_default_name: 'partnes,foo'},
+ });
+ }
+ if (route === "/web/dataset/call_kw/res.lang/get_installed") {
+ return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
+ }
+ if (args.method === "search_read" && args.model == "ir.translation") {
+ return Promise.resolve([
+ {lang: 'en_US', src: 'yop', value: 'yop', id: 42},
+ {lang: 'fr_BE', src: 'yop', value: 'valeur français', id: 43}
+ ]);
+ }
+ if (args.method === "write" && args.model == "ir.translation") {
+ assert.deepEqual(args.args[1], {value: "english value"},
+ "the new translation value should be written");
+ return Promise.resolve();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var $button = form.$('input[type="text"].o_field_char + .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ assert.strictEqual($button.text(), 'EN', 'the button should have as test the current language');
+ await testUtils.dom.click($button);
+ await testUtils.nextTick();
+
+ assert.containsOnce($(document), '.modal', 'a translate modal should be visible');
+ assert.containsN($('.modal .o_translation_dialog'), '.translation', 2,
+ 'two rows should be visible');
+
+ var $enField = $('.modal .o_translation_dialog .translation:first() input');
+ assert.strictEqual($enField.val(), 'yop',
+ 'English translation should be filled');
+ assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français',
+ 'French translation should be filled');
+
+ await testUtils.fields.editInput($enField, "english value");
+ await testUtils.dom.click($('.modal button.btn-primary')); // save
+ await testUtils.nextTick();
+
+ var $foo = form.$('input[type="text"].o_field_char');
+ assert.strictEqual($foo.val(), "english value",
+ "the new translation was not transfered to modified record");
+
+ await testUtils.fields.editInput($foo, "new english value");
+
+ await testUtils.dom.click($button);
+ await testUtils.nextTick();
+
+ assert.strictEqual($('.modal .o_translation_dialog .translation:first() input').val(), 'new english value',
+ 'Modified value should be used instead of translation');
+ assert.strictEqual($('.modal .o_translation_dialog .translation:last() input').val(), 'valeur français',
+ 'French translation should be filled');
+
+ form.destroy();
+
+ _t.database.multi_lang = multiLang;
+ });
+
+ QUnit.test('html field translatable', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.fields.foo.translate = true;
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ user_context: {lang: 'en_US'},
+ },
+ mockRPC: function (route, args) {
+ if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ["partner",1,"foo"], 'should call "call_button" route');
+ return Promise.resolve({
+ domain: [],
+ context: {
+ search_default_name: 'partner,foo',
+ translation_type: 'char',
+ translation_show_src: true,
+ },
+ });
+ }
+ if (route === "/web/dataset/call_kw/res.lang/get_installed") {
+ return Promise.resolve([["en_US", "English"], ["fr_BE", "French (Belgium)"]]);
+ }
+ if (args.method === "search_read" && args.model == "ir.translation") {
+ return Promise.resolve([
+ {lang: 'en_US', src: 'first paragraph', value: 'first paragraph', id: 42},
+ {lang: 'en_US', src: 'second paragraph', value: 'second paragraph', id: 43},
+ {lang: 'fr_BE', src: 'first paragraph', value: 'premier paragraphe', id: 44},
+ {lang: 'fr_BE', src: 'second paragraph', value: 'deuxième paragraphe', id: 45},
+ ]);
+ }
+ if (args.method === "write" && args.model == "ir.translation") {
+ assert.deepEqual(args.args[1], {value: "first paragraph modified"},
+ "Wrong update on translation");
+ return Promise.resolve();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var $foo = form.$('input[type="text"].o_field_char');
+
+ // this will not affect the translate_fields effect until the record is
+ // saved but is set for consistency of the test
+ await testUtils.fields.editInput($foo, "<p>first paragraph</p><p>second paragraph</p>");
+
+ var $button = form.$('input[type="text"].o_field_char + .o_field_translate');
+ await testUtils.dom.click($button);
+ await testUtils.nextTick();
+
+ assert.containsOnce($(document), '.modal', 'a translate modal should be visible');
+ assert.containsN($('.modal .o_translation_dialog'), '.translation', 4,
+ 'four rows should be visible');
+
+ var $enField = $('.modal .o_translation_dialog .translation:first() input');
+ assert.strictEqual($enField.val(), 'first paragraph',
+ 'first part of english translation should be filled');
+
+ await testUtils.fields.editInput($enField, "first paragraph modified");
+ await testUtils.dom.click($('.modal button.btn-primary')); // save
+ await testUtils.nextTick();
+
+ assert.strictEqual($foo.val(), "<p>first paragraph</p><p>second paragraph</p>",
+ "the new partial translation should not be transfered");
+
+ form.destroy();
+
+ _t.database.multi_lang = multiLang;
+ });
+
+ QUnit.test('char field translatable in create mode', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.translate = true;
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+ var $button = form.$('input[type="text"].o_field_char + .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button in create mode");
+ form.destroy();
+
+ _t.database.multi_lang = multiLang;
+ });
+
+ QUnit.test('char field does not allow html injections', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('input[name=foo]'), '<script>throw Error();</script>');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '<script>throw Error();</script>',
+ 'the value should have been properly escaped');
+
+ form.destroy();
+ });
+
+ QUnit.test('char field trim (or not) characters', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo2 = {string: "Foo2", type: "char", trim: false};
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="foo2"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), ' abc ');
+ await testUtils.fields.editInput(form.$('input[name="foo2"]'), ' def ');
+
+ await testUtils.form.clickSave(form);
+
+ // edit mode
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'abc', 'Foo value should have been trimmed');
+ assert.strictEqual(form.$('input[name="foo2"]').val(), ' def ', 'Foo2 value should not have been trimmed');
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: change value before pending onchange returns', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ product_id: function () {},
+ };
+
+ var def;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="product_id"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === "onchange") {
+ return Promise.resolve(def).then(function () {
+ return result;
+ });
+ } else {
+ return result;
+ }
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value',
+ 'should contain the default value');
+
+ def = testUtils.makeTestPromise();
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ // set foo before onchange
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala");
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain tralala');
+
+ // complete the onchange
+ def.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain the same value as before onchange');
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: change value before pending onchange returns (with fieldDebounce)', async function (assert) {
+ // this test is exactly the same as the previous one, except that we set
+ // here a fieldDebounce to accurately reproduce what happens in practice:
+ // the field doesn't notify the changes on 'input', but on 'change' event.
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ obj.int_field = obj.product_id ? 7 : false;
+ },
+ };
+
+ let def;
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="p">
+ <tree editable="bottom">
+ <field name="product_id"/>
+ <field name="foo"/>
+ <field name="int_field"/>
+ </tree>
+ </field>
+ </form>`,
+ async mockRPC(route, args) {
+ const result = this._super(...arguments);
+ if (args.method === "onchange") {
+ await Promise.resolve(def);
+ }
+ return result;
+ },
+ fieldDebounce: 5000,
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value',
+ 'should contain the default value');
+
+ def = testUtils.makeTestPromise();
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ // set foo before onchange
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala");
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala');
+ assert.strictEqual(form.$('input[name="int_field"]').val(), '');
+
+ // complete the onchange
+ def.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'foo should contain the same value as before onchange');
+ assert.strictEqual(form.$('input[name="int_field"]').val(), '7',
+ 'int_field should contain the value returned by the onchange');
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: change value before pending onchange renaming', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ obj.foo = 'on change value';
+ },
+ };
+
+ var def = testUtils.makeTestPromise();
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="product_id"/>' +
+ '<field name="foo"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === "onchange") {
+ return def.then(function () {
+ return result;
+ });
+ } else {
+ return result;
+ }
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'yop',
+ 'should contain the correct value');
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ // set foo before onchange
+ testUtils.fields.editInput(form.$('input[name="foo"]'), "tralala");
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain tralala');
+
+ // complete the onchange
+ def.resolve();
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
+ 'input should contain the same value as before onchange');
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: change password value', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo" password="True"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.notEqual(form.$('.o_field_char').text(), "yop",
+ "password field value should not be visible in read mode");
+ assert.strictEqual(form.$('.o_field_char').text(), "***",
+ "password field value should be hidden with '*' in read mode");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasAttrValue(form.$('input.o_field_char'), 'type', 'password',
+ "password field input should be with type 'password' in edit mode");
+ assert.strictEqual(form.$('input.o_field_char').val(), 'yop',
+ "password field input value should be the (non-hidden) password value");
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: empty password', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].foo = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo" password="True"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_char').text(), "",
+ "password field value should be empty in read mode");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasAttrValue(form.$('input.o_field_char'), 'type', 'password',
+ "password field input should be with type 'password' in edit mode");
+ assert.strictEqual(form.$('input.o_field_char').val(), '',
+ "password field input value should be the (non-hidden, empty) password value");
+
+ form.destroy();
+ });
+
+ QUnit.test('input field: set and remove value, then wait for onchange', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ product_id(obj) {
+ obj.foo = obj.product_id ? "onchange value" : false;
+ },
+ };
+
+ let def;
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="p">
+ <tree editable="bottom">
+ <field name="product_id"/>
+ <field name="foo"/>
+ </tree>
+ </field>
+ </form>`,
+ async mockRPC(route, args) {
+ const result = this._super(...arguments);
+ if (args.method === "onchange") {
+ await Promise.resolve(def);
+ }
+ return result;
+ },
+ fieldDebounce: 1000, // needed to accurately mock what really happens
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('input[name="foo"]').val(), "");
+
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), "test"); // set value for foo
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), ""); // remove value for foo
+
+ // trigger the onchange by setting a product
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+ assert.strictEqual(form.$('input[name="foo"]').val(), 'onchange value',
+ 'input should contain correct value after onchange');
+
+ form.destroy();
+ });
+
+ QUnit.module('UrlWidget');
+
+ QUnit.test('url widget in form view', async function (assert) {
+ assert.expect(9);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="url"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'a.o_form_uri.o_field_widget.o_text_overflow',
+ "should have a anchor with correct classes");
+ assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'href', 'http://yop',
+ "should have proper href link");
+ assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'target', '_blank',
+ "should have target attribute set to _blank");
+ assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'yop',
+ "the value should be displayed properly");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'input[type="text"].o_field_widget',
+ "should have an input for the char field");
+ assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'limbo');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, 'a.o_form_uri.o_field_widget.o_text_overflow',
+ "should still have a anchor with correct classes");
+ assert.hasAttrValue(form.$('a.o_form_uri.o_field_widget.o_text_overflow'), 'href', 'http://limbo',
+ "should have proper new href link");
+ assert.strictEqual(form.$('a.o_form_uri.o_field_widget.o_text_overflow').text(), 'limbo',
+ 'the new value should be displayed');
+
+ form.destroy();
+ });
+
+ QUnit.test('url widget takes text from proper attribute', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo" widget="url" text="kebeclibre"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a[name="foo"]').text(), 'kebeclibre',
+ "url text should come from the text attribute");
+ form.destroy();
+ });
+
+ QUnit.test('url widget: href attribute and website_path option', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.url1 = { string: "Url 1", type: "char", default: "www.url1.com" };
+ this.data.partner.fields.url2 = { string: "Url 2", type: "char", default: "www.url2.com" };
+ this.data.partner.fields.url3 = { string: "Url 3", type: "char", default: "http://www.url3.com" };
+ this.data.partner.fields.url4 = { string: "Url 4", type: "char", default: "https://url4.com" };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="url1" widget="url"/>
+ <field name="url2" widget="url" options="{'website_path': True}"/>
+ <field name="url3" widget="url"/>
+ <field name="url4" widget="url"/>
+ </form>`,
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a[name="url1"]').attr('href'), 'http://www.url1.com');
+ assert.strictEqual(form.$('a[name="url2"]').attr('href'), 'www.url2.com');
+ assert.strictEqual(form.$('a[name="url3"]').attr('href'), 'http://www.url3.com');
+ assert.strictEqual(form.$('a[name="url4"]').attr('href'), 'https://url4.com');
+
+ form.destroy();
+ });
+
+ QUnit.test('char field in editable list view', async function (assert) {
+ assert.expect(10);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="url"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').length, 5,
+ "should have 5 cells");
+ assert.containsN(list, 'a.o_form_uri.o_field_widget.o_text_overflow', 5,
+ "should have 5 anchors with correct classes");
+ assert.hasAttrValue(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first(), 'href', 'http://yop',
+ "should have proper href link");
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector)').first().text(), 'yop',
+ "value should be displayed properly as text");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'brolo');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.containsN(list, 'a.o_form_uri.o_field_widget.o_text_overflow', 5,
+ "should still have 5 anchors with correct classes");
+ assert.hasAttrValue(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first(), 'href', 'http://brolo',
+ "should have proper new href link");
+ assert.strictEqual(list.$('a.o_form_uri.o_field_widget.o_text_overflow').first().text(), 'brolo',
+ "value should be properly updated");
+
+ list.destroy();
+ });
+
+ QUnit.module('CopyClipboard');
+
+ QUnit.test('Char & Text Fields: Copy to clipboard button', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div>' +
+ '<field name="txt" widget="CopyClipboardText"/>' +
+ '<field name="foo" widget="CopyClipboardChar"/>' +
+ '</div>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_clipboard_button.o_btn_text_copy',"Should have copy button on text type field");
+ assert.containsOnce(form, '.o_clipboard_button.o_btn_char_copy',"Should have copy button on char type field");
+
+ form.destroy();
+ });
+
+ QUnit.test('CopyClipboard widget on unset field', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].foo = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="CopyClipboardChar" />' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsNone(form, '.o_field_copy[name="foo"] .o_clipboard_button',
+ "foo (unset) should not contain a button");
+
+ form.destroy();
+ });
+
+ QUnit.test('CopyClipboard widget on readonly unset fields in create mode', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.display_name.readonly = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="display_name" widget="CopyClipboardChar" />' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ assert.containsNone(form, '.o_field_copy[name="display_name"] .o_clipboard_button',
+ "the readonly unset field should not contain a button");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldText');
+
+ QUnit.test('text fields are correctly rendered', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.foo.type = 'text';
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('.o_field_text').length, "should have a text area");
+ assert.strictEqual(form.$('.o_field_text').text(), 'yop', 'should be "yop" in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ var $textarea = form.$('textarea.o_field_text');
+ assert.ok($textarea.length, "should have a text area");
+ assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit');
+
+ testUtils.fields.editInput($textarea, 'hello');
+ assert.strictEqual($textarea.val(), 'hello', 'should be "hello" after first edition');
+
+ testUtils.fields.editInput($textarea, 'hello world');
+ assert.strictEqual($textarea.val(), 'hello world', 'should be "hello world" after second edition');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_text').text(), 'hello world',
+ 'should be "hello world" after save');
+ form.destroy();
+ });
+
+ QUnit.test('text fields in edit mode have correct height', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.type = 'text';
+ this.data.partner.records[0].foo = "f\nu\nc\nk\nm\ni\nl\ng\nr\no\nm";
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $field = form.$('.o_field_text');
+
+ assert.strictEqual($field[0].offsetHeight, $field[0].scrollHeight,
+ "text field should not have a scroll bar");
+
+ await testUtils.form.clickEdit(form);
+
+ var $textarea = form.$('textarea:first');
+
+ // the difference is to take small calculation errors into account
+ assert.strictEqual($textarea[0].clientHeight, $textarea[0].scrollHeight,
+ "textarea should not have a scroll bar");
+ form.destroy();
+ });
+
+ QUnit.test('text fields in edit mode, no vertical resize', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="txt"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var $textarea = form.$('textarea:first');
+
+ assert.strictEqual($textarea.css('resize'), 'none',
+ "should not have vertical resize");
+
+ form.destroy();
+ });
+
+ QUnit.test('text fields should have correct height after onchange', async function (assert) {
+ assert.expect(2);
+
+ const damnLongText = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec est massa, gravida eget dapibus ac, eleifend eget libero.
+ Suspendisse feugiat sed massa eleifend vestibulum. Sed tincidunt
+ velit sed lacinia lacinia. Nunc in fermentum nunc. Vestibulum ante
+ ipsum primis in faucibus orci luctus et ultrices posuere cubilia
+ Curae; Nullam ut nisi a est ornare molestie non vulputate orci.
+ Nunc pharetra porta semper. Mauris dictum eu nulla a pulvinar. Duis
+ eleifend odio id ligula congue sollicitudin. Curabitur quis aliquet
+ nunc, ut aliquet enim. Suspendisse malesuada felis non metus
+ efficitur aliquet.`;
+
+ this.data.partner.records[0].txt = damnLongText;
+ this.data.partner.records[0].bar = false;
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.txt = damnLongText;
+ },
+ };
+ const form = await createView({
+ arch: `
+ <form string="Partners">
+ <field name="bar"/>
+ <field name="txt" attrs="{'invisible': [('bar', '=', True)]}"/>
+ </form>`,
+ data: this.data,
+ model: 'partner',
+ res_id: 1,
+ View: FormView,
+ viewOptions: { mode: 'edit' },
+ });
+
+ const textarea = form.el.querySelector('textarea[name="txt"]');
+ const initialHeight = textarea.offsetHeight;
+
+ await testUtils.fields.editInput($(textarea), 'Short value');
+
+ assert.ok(textarea.offsetHeight < initialHeight,
+ "Textarea height should have shrank");
+
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+
+ assert.strictEqual(textarea.offsetHeight, initialHeight,
+ "Textarea height should be reset");
+
+ form.destroy();
+ });
+
+ QUnit.test('text fields in editable list have correct height', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].txt = "a\nb\nc\nd\ne\nf";
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<list editable="top">' +
+ '<field name="foo"/>' +
+ '<field name="txt"/>' +
+ '</list>',
+ });
+
+ // Click to enter edit: in this test we specifically do not set
+ // the focus on the textarea by clicking on another column.
+ // The main goal is to test the resize is actually triggered in this
+ // particular case.
+ await testUtils.dom.click(list.$('.o_data_cell:first'));
+ var $textarea = list.$('textarea:first');
+
+ // make sure the correct data is there
+ assert.strictEqual($textarea.val(), this.data.partner.records[0].txt);
+
+ // make sure there is no scroll bar
+ assert.strictEqual($textarea[0].clientHeight, $textarea[0].scrollHeight,
+ "textarea should not have a scroll bar");
+
+ list.destroy();
+ });
+
+ QUnit.test('text fields in edit mode should resize on reset', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.type = 'text';
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.foo = 'a\nb\nc\nd\ne\nf';
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="bar"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // edit the form
+ // trigger a textarea reset (through onchange) by clicking the box
+ // then check there is no scroll bar
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('div[name="bar"] input'));
+
+ var $textarea = form.$('textarea:first');
+ assert.strictEqual($textarea.innerHeight(), $textarea[0].scrollHeight,
+ "textarea should not have a scroll bar");
+
+ form.destroy();
+ });
+
+ QUnit.test('text field translatable', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.txt.translate = true;
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="txt"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === "/web/dataset/call_button" && args.method === 'translate_fields') {
+ assert.deepEqual(args.args, ["partner",1,"txt"], 'should call "call_button" route');
+ return Promise.resolve({
+ domain: [],
+ context: {search_default_name: 'partnes,foo'},
+ });
+ }
+ if (route === "/web/dataset/call_kw/res.lang/get_installed") {
+ return Promise.resolve([["en_US"], ["fr_BE"]]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ var $button = form.$('textarea + .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button");
+ await testUtils.dom.click($button);
+ assert.containsOnce($(document), '.modal', 'there should be a translation modal');
+ form.destroy();
+ _t.database.multi_lang = multiLang;
+ });
+
+ QUnit.test('text field translatable in create mode', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.txt.translate = true;
+
+ var multiLang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="txt"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+ var $button = form.$('textarea + .o_field_translate');
+ assert.strictEqual($button.length, 1, "should have a translate button in create mode");
+ form.destroy();
+
+ _t.database.multi_lang = multiLang;
+ });
+
+ QUnit.test('go to next line (and not the next row) when pressing enter', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.foo.type = 'text';
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<list editable="top">' +
+ '<field name="int_field"/>' +
+ '<field name="foo"/>' +
+ '<field name="qux"/>' +
+ '</list>',
+ });
+
+ await testUtils.dom.click(list.$('tbody tr:first .o_list_text'));
+ var $textarea = list.$('textarea.o_field_text');
+ assert.strictEqual($textarea.length, 1, "should have a text area");
+ assert.strictEqual($textarea.val(), 'yop', 'should still be "yop" in edit');
+
+ assert.strictEqual(list.$('textarea').get(0), document.activeElement,
+ "text area should have the focus");
+
+ // click on enter
+ list.$('textarea')
+ .trigger({type: "keydown", which: $.ui.keyCode.ENTER})
+ .trigger({type: "keyup", which: $.ui.keyCode.ENTER});
+
+ assert.strictEqual(list.$('textarea').first().get(0), document.activeElement,
+ "text area should still have the focus");
+
+ list.destroy();
+ });
+
+ // Firefox-specific
+ // Copying from <div style="white-space:pre-wrap"> does not keep line breaks
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115
+ QUnit.test('copying text fields in RO mode should preserve line breaks', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="txt"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // Copying from a div tag with white-space:pre-wrap doesn't work in Firefox
+ assert.strictEqual(form.$('[name="txt"]').prop("tagName").toLowerCase(), 'span',
+ "the field contents should be surrounded by a span tag");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldBinary');
+
+ QUnit.test('binary fields are correctly rendered', async function (assert) {
+ assert.expect(16);
+
+ // save the session function
+ var oldGetFile = session.get_file;
+ session.get_file = function (option) {
+ assert.strictEqual(option.data.field, 'document',
+ "we should download the field document");
+ assert.strictEqual(option.data.data, 'coucou==\n',
+ "we should download the correct data");
+ option.complete();
+ return Promise.resolve();
+ };
+
+ this.data.partner.records[0].foo = 'coucou.txt';
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" filename="foo"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download',
+ "the binary field should be rendered as a downloadable link in readonly");
+ assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), 'coucou.txt',
+ "the binary field should display the name of the file in the link");
+ assert.strictEqual(form.$('.o_field_char').text(), 'coucou.txt',
+ "the filename field should have the file name as value");
+
+ await testUtils.dom.click(form.$('a.o_field_widget[name="document"]'));
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download',
+ "the binary field should not be rendered as a downloadable link in edit");
+ assert.strictEqual(form.$('div.o_field_binary_file[name="document"] > input').val(), 'coucou.txt',
+ "the binary field should display the file name in the input edit mode");
+ assert.hasAttrValue(form.$('.o_field_binary_file > input'), 'readonly', 'readonly',
+ "the input should be readonly");
+ assert.containsOnce(form, '.o_field_binary_file > .o_clear_file_button',
+ "there shoud be a button to clear the file");
+ assert.strictEqual(form.$('input.o_field_char').val(), 'coucou.txt',
+ "the filename field should have the file name as value");
+
+
+ await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button'));
+
+ assert.isNotVisible(form.$('.o_field_binary_file > input'),
+ "the input should be hidden");
+ assert.strictEqual(form.$('.o_field_binary_file > .o_select_file_button:not(.o_hidden)').length, 1,
+ "there shoud be a button to upload the file");
+ assert.strictEqual(form.$('input.o_field_char').val(), '',
+ "the filename field should be empty since we removed the file");
+
+ await testUtils.form.clickSave(form);
+ assert.containsNone(form, 'a.o_field_widget[name="document"] > .fa-download',
+ "the binary field should not render as a downloadable link since we removed the file");
+ assert.strictEqual(form.$('a.o_field_widget[name="document"]').text().trim(), '',
+ "the binary field should not display a filename in the link since we removed the file");
+ assert.strictEqual(form.$('.o_field_char').text().trim(), '',
+ "the filename field should be empty since we removed the file");
+
+ form.destroy();
+
+ // restore the session function
+ session.get_file = oldGetFile;
+ });
+
+ QUnit.test('binary fields: option accepted_file_extensions', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <field name="document" widget="binary" options="{'accepted_file_extensions': '.dat,.bin'}"/>
+ </form>`
+ });
+ assert.strictEqual(form.$('input.o_input_file').attr('accept'), '.dat,.bin',
+ "the input should have the correct ``accept`` attribute");
+ form.destroy();
+ });
+
+ QUnit.test('binary fields that are readonly in create mode do not download', async function (assert) {
+ assert.expect(2);
+
+ // save the session function
+ var oldGetFile = session.get_file;
+ session.get_file = function (option) {
+ assert.step('We shouldn\'t be getting the file.');
+ return oldGetFile.bind(session)(option);
+ };
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ obj.document = "onchange==\n";
+ },
+ };
+
+ this.data.partner.fields.document.readonly = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id"/>' +
+ '<field name="document" filename="\'yooo\'"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickCreate(form);
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ assert.containsOnce(form, 'a.o_field_widget[name="document"] > .fa-download',
+ 'The link to download the binary should be present');
+
+ testUtils.dom.click(form.$('a.o_field_widget[name="document"]'));
+
+ assert.verifySteps([]); // We shouldn't have passed through steps
+
+ form.destroy();
+ session.get_file = oldGetFile;
+ });
+
+ QUnit.module('FieldPdfViewer');
+
+ QUnit.test("pdf_viewer without data", async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="document" widget="pdf_viewer"/>' +
+ '</form>',
+ });
+
+ assert.hasClass(form.$('.o_field_widget'), 'o_field_pdfviewer');
+ assert.strictEqual(form.$('.o_select_file_button:not(.o_hidden)').length, 1,
+ "there should be a visible 'Upload' button");
+ assert.isNotVisible(form.$('.o_field_widget iframe.o_pdfview_iframe'),
+ "there should be an invisible iframe");
+ assert.containsOnce(form, 'input[type="file"]',
+ "there should be one input");
+
+ form.destroy();
+ });
+
+ QUnit.test("pdf_viewer: basic rendering", async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ res_id: 1,
+ arch:
+ '<form>' +
+ '<field name="document" widget="pdf_viewer"/>' +
+ '</form>',
+ mockRPC: function (route) {
+ if (route.indexOf('/web/static/lib/pdfjs/web/viewer.html') !== -1) {
+ return Promise.resolve();
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.hasClass(form.$('.o_field_widget'), 'o_field_pdfviewer');
+ assert.strictEqual(form.$('.o_select_file_button:not(.o_hidden)').length, 0,
+ "there should not be a any visible 'Upload' button");
+ assert.isVisible(form.$('.o_field_widget iframe.o_pdfview_iframe'),
+ "there should be an visible iframe");
+ assert.hasAttrValue(form.$('.o_field_widget iframe.o_pdfview_iframe'), 'data-src',
+ '/web/static/lib/pdfjs/web/viewer.html?file=%2Fweb%2Fcontent%3Fmodel%3Dpartner%26field%3Ddocument%26id%3D1#page=1',
+ "the src attribute should be correctly set on the iframe");
+
+ form.destroy();
+ });
+
+ QUnit.test("pdf_viewer: upload rendering", async function (assert) {
+ assert.expect(6);
+
+ testUtils.mock.patch(field_registry.map.pdf_viewer, {
+ on_file_change: function (ev) {
+ ev.target = {files: [new Blob()]};
+ this._super.apply(this, arguments);
+ },
+ _getURI: function (fileURI) {
+ this._super.apply(this, arguments);
+ assert.step('_getURI');
+ assert.ok(_.str.startsWith(fileURI, 'blob:'));
+ this.PDFViewerApplication = {
+ open: function (URI) {
+ assert.step('open');
+ assert.ok(_.str.startsWith(URI, 'blob:'));
+ },
+ };
+ return 'about:blank';
+ },
+ });
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="document" widget="pdf_viewer"/>' +
+ '</form>',
+ });
+
+ // first upload initialize iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI']);
+ // second upload call pdfjs method inside iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['open']);
+
+ testUtils.mock.unpatch(field_registry.map.pdf_viewer);
+ form.destroy();
+ });
+
+ QUnit.test('text field rendering in list view', async function (assert) {
+ assert.expect(1);
+
+ var data = {
+ foo: {
+ fields: {foo: {string: "F", type: "text"}},
+ records: [{id: 1, foo: "some text"}]
+ },
+ };
+ var list = await createView({
+ View: ListView,
+ model: 'foo',
+ data: data,
+ arch: '<tree><field name="foo"/></tree>',
+ });
+
+ assert.strictEqual(list.$('tbody td.o_list_text:contains(some text)').length, 1,
+ "should have a td with the .o_list_text class");
+ list.destroy();
+ });
+
+ QUnit.test("binary fields input value is empty whean clearing after uploading", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" filename="foo"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // // We need to convert the input type since we can't programmatically set the value of a file input
+ form.$('.o_input_file').attr('type', 'text').val('coucou.txt');
+
+ assert.strictEqual(form.$('.o_input_file').val(), 'coucou.txt',
+ "input value should be changed to \"coucou.txt\"");
+
+ await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button'));
+
+ assert.strictEqual(form.$('.o_input_file').val(), '',
+ "input value should be empty");
+
+ form.destroy();
+ });
+
+ QUnit.test('field text in editable list view', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.foo.type = 'text';
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Phonecalls" editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>',
+ });
+
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
+
+ assert.strictEqual(list.$('textarea').first().get(0), document.activeElement,
+ "text area should have the focus");
+ list.destroy();
+ });
+
+ QUnit.module('FieldImage');
+
+ QUnit.test('image fields are correctly rendered', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
+ this.data.partner.records[0].document = MY_IMAGE;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" widget="image" options="{\'size\': [90, 90]}"/> ' +
+ '</form>',
+ res_id: 1,
+ async mockRPC(route, args) {
+ if (route === '/web/dataset/call_kw/partner/read') {
+ assert.deepEqual(args.args[1], ['document', '__last_update', 'display_name'], "The fields document, display_name and __last_update should be present when reading an image");
+ }
+ if (route === `data:image/png;base64,${MY_IMAGE}`) {
+ assert.ok(true, "should called the correct route");
+ return 'wow';
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.hasClass(form.$('div[name="document"]'),'o_field_image',
+ "the widget should have the correct class");
+ assert.containsOnce(form, 'div[name="document"] > img',
+ "the widget should contain an image");
+ assert.hasClass(form.$('div[name="document"] > img'),'img-fluid',
+ "the image should have the correct class");
+ assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90",
+ "the image should correctly set its attributes");
+ assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px",
+ "the image should correctly set its attributes");
+ form.destroy();
+ });
+
+ QUnit.test('image fields are correctly replaced when given an incorrect value', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
+ this.data.partner.records[0].document = 'incorrect_base64_value';
+
+ testUtils.mock.patch(basicFields.FieldBinaryImage, {
+ // Delay the _render function: this will ensure that the error triggered
+ // by the incorrect base64 value is dispatched before the src is replaced
+ // (see test_utils_mock.removeSrcAttribute), since that function is called
+ // when the element is inserted into the DOM.
+ async _render() {
+ const result = this._super.apply(this, arguments);
+ await concurrency.delay(100);
+ return result;
+ },
+ });
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form string="Partners">
+ <field name="document" widget="image" options="{'size': [90, 90]}"/>
+ </form>`,
+ res_id: 1,
+ async mockRPC(route, args) {
+ const _super = this._super;
+ if (route === '/web/static/src/img/placeholder.png') {
+ assert.step('call placeholder route');
+ }
+ return _super.apply(this, arguments);
+ },
+ });
+
+ assert.hasClass(form.$('div[name="document"]'),'o_field_image',
+ "the widget should have the correct class");
+ assert.containsOnce(form, 'div[name="document"] > img',
+ "the widget should contain an image");
+ assert.hasClass(form.$('div[name="document"] > img'), 'img-fluid',
+ "the image should have the correct class");
+ assert.hasAttrValue(form.$('div[name="document"] > img'), 'width', "90",
+ "the image should correctly set its attributes");
+ assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px",
+ "the image should correctly set its attributes");
+
+ assert.verifySteps(['call placeholder route']);
+
+ form.destroy();
+ testUtils.mock.unpatch(basicFields.FieldBinaryImage);
+ });
+
+ QUnit.test('image: option accepted_file_extensions', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <field name="document" widget="image" options="{'accepted_file_extensions': '.png,.jpeg'}"/>
+ </form>`
+ });
+ assert.strictEqual(form.$('input.o_input_file').attr('accept'), '.png,.jpeg',
+ "the input should have the correct ``accept`` attribute");
+ form.destroy();
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <field name="document" widget="image"/>
+ </form>`
+ });
+ assert.strictEqual(form.$('input.o_input_file').attr('accept'), 'image/*',
+ 'the default value for the attribute "accept" on the "image" widget must be "image/*"');
+ form.destroy();
+ });
+
+ QUnit.test('image fields in subviews are loaded correctly', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
+ this.data.partner.records[0].document = MY_IMAGE;
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = PRODUCT_IMAGE;
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" widget="image" options="{\'size\': [90, 90]}"/>' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="image" widget="image"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ async mockRPC(route) {
+ if (route === `data:image/png;base64,${MY_IMAGE}`) {
+ assert.step("The view's image should have been fetched");
+ return 'wow';
+ }
+ if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) {
+ assert.step("The dialog's image should have been fetched");
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.verifySteps(["The view's image should have been fetched"]);
+
+ assert.containsOnce(form, 'tr.o_data_row',
+ 'There should be one record in the many2many');
+
+ // Actual flow: click on an element of the m2m to get its form view
+ await testUtils.dom.click(form.$('tbody td:contains(gold)'));
+ assert.strictEqual($('.modal').length, 1,
+ 'The modal should have opened');
+ assert.verifySteps(["The dialog's image should have been fetched"]);
+
+ form.destroy();
+ });
+
+ QUnit.test('image fields in x2many list are loaded correctly', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = PRODUCT_IMAGE;
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="image" widget="image"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ async mockRPC(route) {
+ if (route === `data:image/gif;base64,${PRODUCT_IMAGE}`) {
+ assert.ok(true, "The list's image should have been fetched");
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsOnce(form, 'tr.o_data_row',
+ 'There should be one record in the many2many');
+
+ form.destroy();
+ });
+
+ QUnit.test('image fields with required attribute', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" required="1" widget="image"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ throw new Error("Should not do a create RPC with unset required image field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickSave(form);
+
+ assert.hasClass(form.$('.o_form_view'),'o_form_editable',
+ "form view should still be editable");
+ assert.hasClass(form.$('.o_field_widget'),'o_field_invalid',
+ "image field should be displayed as invalid");
+
+ form.destroy();
+ });
+
+ /**
+ * Same tests than for Image fields, but for Char fields with image_url widget.
+ */
+ QUnit.module('FieldChar-ImageUrlWidget', {
+ beforeEach: function () {
+ // specific sixth partner data for image_url widget tests
+ this.data.partner.records.push({id: 6, bar: false, foo: FR_FLAG_URL, int_field: 5, qux: 0.0, timmy: []});
+ },
+ });
+
+ QUnit.test('image fields are correctly rendered', async function (assert) {
+ assert.expect(6);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/> ' +
+ '</form>',
+ res_id: 6,
+ async mockRPC(route, args) {
+ if (route === FR_FLAG_URL) {
+ assert.ok(true, "the correct route should have been called.");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.hasClass(form.$('div[name="foo"]'), 'o_field_image',
+ "the widget should have the correct class");
+ assert.containsOnce(form, 'div[name="foo"] > img',
+ "the widget should contain an image");
+ assert.hasClass(form.$('div[name="foo"] > img'), 'img-fluid',
+ "the image should have the correct class");
+ assert.hasAttrValue(form.$('div[name="foo"] > img'), 'width', "90",
+ "the image should correctly set its attributes");
+ assert.strictEqual(form.$('div[name="foo"] > img').css('max-width'), "90px",
+ "the image should correctly set its attributes");
+ form.destroy();
+ });
+
+ QUnit.test('image_url widget in subviews are loaded correctly', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'char'};
+ this.data.partner_type.records[0].image = EN_FLAG_URL;
+ this.data.partner.records[5].timmy = [12];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" widget="image_url" options="{\'size\': [90, 90]}"/>' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="image" widget="image_url"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 6,
+ async mockRPC(route) {
+ if (route === FR_FLAG_URL) {
+ assert.step("The view's image should have been fetched");
+ return 'wow';
+ }
+ if (route === EN_FLAG_URL) {
+ assert.step("The dialog's image should have been fetched");
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.verifySteps(["The view's image should have been fetched"]);
+
+ assert.containsOnce(form, 'tr.o_data_row',
+ 'There should be one record in the many2many');
+
+ // Actual flow: click on an element of the m2m to get its form view
+ await testUtils.dom.click(form.$('tbody td:contains(gold)'));
+ assert.strictEqual($('.modal').length, 1,
+ 'The modal should have opened');
+ assert.verifySteps(["The dialog's image should have been fetched"]);
+
+ form.destroy();
+ });
+
+ QUnit.test('image fields in x2many list are loaded correctly', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'char'};
+ this.data.partner_type.records[0].image = EN_FLAG_URL;
+ this.data.partner.records[5].timmy = [12];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="image" widget="image_url"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 6,
+ async mockRPC(route) {
+ if (route === EN_FLAG_URL) {
+ assert.ok(true, "The list's image should have been fetched");
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsOnce(form, 'tr.o_data_row',
+ 'There should be one record in the many2many');
+
+ form.destroy();
+ });
+
+ QUnit.module('JournalDashboardGraph', {
+ beforeEach: function () {
+ _.extend(this.data.partner.fields, {
+ graph_data: { string: "Graph Data", type: "text" },
+ graph_type: {
+ string: "Graph Type",
+ type: "selection",
+ selection: [['line', 'Line'], ['bar', 'Bar']]
+ },
+ });
+ this.data.partner.records[0].graph_type = "bar";
+ this.data.partner.records[1].graph_type = "line";
+ var graph_values = [
+ {'value': 300, 'label': '5-11 Dec'},
+ {'value': 500, 'label': '12-18 Dec'},
+ {'value': 100, 'label': '19-25 Dec'},
+ ];
+ this.data.partner.records[0].graph_data = JSON.stringify([{
+ color: 'red',
+ title: 'Partner 0',
+ values: graph_values,
+ key: 'A key',
+ area: true,
+ }]);
+ this.data.partner.records[1].graph_data = JSON.stringify([{
+ color: 'blue',
+ title: 'Partner 1',
+ values: graph_values,
+ key: 'A key',
+ area: true,
+ }]);
+ },
+ });
+
+ QUnit.test('graph dashboard widget attach/detach callbacks', async function (assert) {
+ // This widget is rendered with Chart.js.
+ var done = assert.async();
+ assert.expect(6);
+
+ testUtils.mock.patch(JournalDashboardGraph, {
+ on_attach_callback: function () {
+ assert.step('on_attach_callback');
+ },
+ on_detach_callback: function () {
+ assert.step('on_detach_callback');
+ },
+ });
+
+ createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="graph_type"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates></kanban>',
+ domain: [['id', 'in', [1, 2]]],
+ }).then(function (kanban) {
+ assert.verifySteps([
+ 'on_attach_callback',
+ 'on_attach_callback'
+ ]);
+
+ kanban.on_detach_callback();
+
+ assert.verifySteps([
+ 'on_detach_callback',
+ 'on_detach_callback'
+ ]);
+
+ kanban.destroy();
+ testUtils.mock.unpatch(JournalDashboardGraph);
+ done();
+ });
+ });
+
+ QUnit.test('graph dashboard widget is rendered correctly', async function (assert) {
+ var done = assert.async();
+ assert.expect(3);
+
+ createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="graph_type"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates></kanban>',
+ domain: [['id', 'in', [1, 2]]],
+ }).then(function (kanban) {
+ concurrency.delay(0).then(function () {
+ assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1,
+ "graph of first record should be a barchart");
+ assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_dashboard_graph').length, 1,
+ "graph of second record should be a linechart");
+
+ // force a re-rendering of the first record (to check if the
+ // previous rendered graph is correctly removed from the DOM)
+ var firstRecordState = kanban.model.get(kanban.handle).data[0];
+ return kanban.renderer.updateRecord(firstRecordState);
+ }).then(function () {
+ return concurrency.delay(0);
+ }).then(function () {
+ assert.strictEqual(kanban.$('.o_kanban_record:first() canvas').length, 1,
+ "there should be only one rendered graph by record");
+
+ kanban.destroy();
+ done();
+ });
+ });
+ });
+
+ QUnit.test('rendering of a field with dashboard_graph widget in an updated kanban view (ungrouped)', async function (assert) {
+
+ var done = assert.async();
+ assert.expect(2);
+
+ createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="graph_type"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates></kanban>',
+ domain: [['id', 'in', [1, 2]]],
+ }).then(function (kanban) {
+ concurrency.delay(0).then(function () {
+ assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered");
+ return kanban.update({});
+ }).then(function () {
+ return concurrency.delay(0); // one graph is re-rendered
+ }).then(function () {
+ assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be one graph rendered");
+ kanban.destroy();
+ done();
+ });
+ });
+ });
+
+ QUnit.test('rendering of a field with dashboard_graph widget in an updated kanban view (grouped)', async function (assert) {
+
+ var done = assert.async();
+ assert.expect(2);
+
+ createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<field name="graph_type"/>' +
+ '<templates><t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates></kanban>',
+ domain: [['id', 'in', [1, 2]]],
+ }).then(function (kanban) {
+ concurrency.delay(0).then(function () {
+ assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered");
+ return kanban.update({groupBy: ['selection'], domain: [['int_field', '=', 10]]});
+ }).then(function () {
+ assert.containsOnce(kanban, '.o_dashboard_graph canvas', "there should be one graph rendered");
+ kanban.destroy();
+ done();
+ });
+ });
+ });
+
+ QUnit.module('AceEditor');
+
+ QUnit.test('ace widget on text fields works', async function (assert) {
+ assert.expect(2);
+ var done = assert.async();
+
+ this.data.partner.fields.foo.type = 'text';
+ testUtils.createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" widget="ace"/>' +
+ '</form>',
+ res_id: 1,
+ }).then(function (form) {
+ assert.ok('ace' in window, "the ace library should be loaded");
+ assert.ok(form.$('div.ace_content').length, "should have rendered something with ace editor");
+ form.destroy();
+ done();
+ });
+ });
+
+ QUnit.module('HandleWidget');
+
+ QUnit.test('handle widget in x2m', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="sequence" widget="handle"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('td span.o_row_handle').text(), "",
+ "handle should not have any content");
+
+ assert.notOk(form.$('td span.o_row_handle').is(':visible'),
+ "handle should be invisible in readonly mode");
+
+ assert.containsN(form, 'span.o_row_handle', 2, "should have 2 handles");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('td:first'),'o_handle_cell',
+ "column widget should be displayed in css class");
+
+ assert.ok(form.$('td span.o_row_handle').is(':visible'),
+ "handle should be visible in readonly mode");
+
+ testUtils.dom.click(form.$('td').eq(1));
+ assert.containsOnce(form, 'td:first span.o_row_handle',
+ "content of the cell should have been replaced");
+ form.destroy();
+ });
+
+ QUnit.test('handle widget with falsy values', async function (assert) {
+ assert.expect(1);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree>' +
+ '<field name="sequence" widget="handle"/>' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ });
+
+ assert.containsN(list, '.o_row_handle:visible', this.data.partner.records.length,
+ 'there should be a visible handle for each record');
+ list.destroy();
+ });
+
+
+ QUnit.module('FieldDateRange');
+
+ QUnit.test('Datetime field [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(20);
+
+ this.data.partner.fields.datetime_end = {string: 'Datetime End', type: 'datetime'};
+ this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="datetime" widget="daterange" options="{\'related_end_date\': \'datetime_end\'}"/>' +
+ '<field name="datetime_end" widget="daterange" options="{\'related_start_date\': \'datetime\'}"/>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ });
+
+ // Check date display correctly in readonly
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 15:30:00',
+ "the start date should be correctly displayed in readonly");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/13/2017 05:30:00',
+ "the end date should be correctly displayed in readonly");
+
+ // Edit
+ await testUtils.form.clickEdit(form);
+
+ // Check date range picker initialization
+ assert.containsN(document.body, '.daterangepicker', 2,
+ "should initialize 2 date range picker");
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
+ "date range picker should be opened initially");
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed initially");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .active.start-date').text(), '8',
+ "active start date should be '8' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .hourselect').val(), '15',
+ "active start date hour should be '15' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.left .minuteselect').val(), '30',
+ "active start date minute should be '30' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .active.end-date').text(), '13',
+ "active end date should be '13' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .hourselect').val(), '5',
+ "active end date hour should be '5' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .drp-calendar.right .minuteselect').val(), '30',
+ "active end date minute should be '30' in date range picker");
+ assert.containsN($('.daterangepicker:first .drp-calendar.left .minuteselect'), 'option', 12,
+ "minute selection should contain 12 options (1 for each 5 minutes)");
+
+ // Close picker
+ await testUtils.dom.click($('.daterangepicker:first .cancelBtn'));
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'none',
+ "date range picker should be closed");
+
+ // Try to check with end date
+ await testUtils.dom.click(form.$('.o_field_date_range:last'));
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'block',
+ "date range picker should be opened");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .active.start-date').text(), '8',
+ "active start date should be '8' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .hourselect').val(), '15',
+ "active start date hour should be '15' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.left .minuteselect').val(), '30',
+ "active start date minute should be '30' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .active.end-date').text(), '13',
+ "active end date should be '13' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .hourselect').val(), '5',
+ "active end date hour should be '5' in date range picker");
+ assert.strictEqual($('.daterangepicker:last .drp-calendar.right .minuteselect').val(), '30',
+ "active end date minute should be '30' in date range picker");
+
+ form.destroy();
+ });
+
+ QUnit.test('Date field [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(18);
+
+ this.data.partner.fields.date_end = {string: 'Date End', type: 'date'};
+ this.data.partner.records[0].date_end = '2017-02-08';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="date" widget="daterange" options="{\'related_end_date\': \'date_end\'}"/>' +
+ '<field name="date_end" widget="daterange" options="{\'related_start_date\': \'date\'}"/>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ });
+
+ // Check date display correctly in readonly
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/03/2017',
+ "the start date should be correctly displayed in readonly");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '02/08/2017',
+ "the end date should be correctly displayed in readonly");
+
+ // Edit
+ await testUtils.form.clickEdit(form);
+
+ // Check date range picker initialization
+ assert.containsN(document.body, '.daterangepicker', 2,
+ "should initialize 2 date range picker");
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'block',
+ "date range picker should be opened initially");
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed initially");
+ assert.strictEqual($('.daterangepicker:first .active.start-date').text(), '3',
+ "active start date should be '3' in date range picker");
+ assert.strictEqual($('.daterangepicker:first .active.end-date').text(), '8',
+ "active end date should be '8' in date range picker");
+
+ // Change date
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.left .available:contains("16")'), 'mousedown');
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:first .drp-calendar.right .available:contains("12")'), 'mousedown');
+ await testUtils.dom.click($('.daterangepicker:first .applyBtn'));
+
+ // Check date after change
+ assert.strictEqual($('.daterangepicker:first').css('display'), 'none',
+ "date range picker should be closed");
+ assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/16/2017',
+ "the date should be '02/16/2017'");
+ assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/12/2017',
+ "'the date should be '03/12/2017'");
+
+ // Try to change range with end date
+ await testUtils.dom.click(form.$('.o_field_date_range:last'));
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'block',
+ "date range picker should be opened");
+ assert.strictEqual($('.daterangepicker:last .active.start-date').text(), '16',
+ "start date should be a 16 in date range picker");
+ assert.strictEqual($('.daterangepicker:last .active.end-date').text(), '12',
+ "end date should be a 12 in date range picker");
+
+ // Change date
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.left .available:contains("13")'), 'mousedown');
+ await testUtils.dom.triggerMouseEvent($('.daterangepicker:last .drp-calendar.right .available:contains("18")'), 'mousedown');
+ await testUtils.dom.click($('.daterangepicker:last .applyBtn'));
+
+ // Check date after change
+ assert.strictEqual($('.daterangepicker:last').css('display'), 'none',
+ "date range picker should be closed");
+ assert.strictEqual(form.$('.o_field_date_range:first').val(), '02/13/2017',
+ "the start date should be '02/13/2017'");
+ assert.strictEqual(form.$('.o_field_date_range:last').val(), '03/18/2017',
+ "the end date should be '03/18/2017'");
+
+ // Save
+ await testUtils.form.clickSave(form);
+
+ // Check date after save
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/13/2017',
+ "the start date should be '02/13/2017' after save");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/18/2017',
+ "the end date should be '03/18/2017' after save");
+
+ form.destroy();
+ });
+
+ QUnit.test('daterangepicker should disappear on scrolling outside of it', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.datetime_end = {string: 'Datetime End', type: 'datetime'};
+ this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00';
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="datetime" widget="daterange" options="{'related_end_date': 'datetime_end'}"/>
+ <field name="datetime_end" widget="daterange" options="{'related_start_date': 'datetime'}"/>
+ </form>`,
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_date_range:first'));
+
+ assert.isVisible($('.daterangepicker:first'), "date range picker should be opened");
+
+ form.el.dispatchEvent(new Event('scroll'));
+ assert.isNotVisible($('.daterangepicker:first'), "date range picker should be closed");
+
+ form.destroy();
+ });
+
+ QUnit.test('Datetime field manually input value should send utc value to server', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.datetime_end = { string: 'Datetime End', type: 'datetime' };
+ this.data.partner.records[0].datetime_end = '2017-03-13 00:00:00';
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="datetime" widget="daterange" options="{'related_end_date': 'datetime_end'}"/>
+ <field name="datetime_end" widget="daterange" options="{'related_start_date': 'datetime'}"/>
+ </form>`,
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 330;
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], { datetime: '2017-02-08 06:00:00' });
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ // check date display correctly in readonly
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 15:30:00',
+ "the start date should be correctly displayed in readonly");
+ assert.strictEqual(form.$('.o_field_date_range:last').text(), '03/13/2017 05:30:00',
+ "the end date should be correctly displayed in readonly");
+
+ // edit form
+ await testUtils.form.clickEdit(form);
+ // update input for Datetime
+ await testUtils.fields.editInput(form.$('.o_field_date_range:first'), '02/08/2017 11:30:00');
+ // save form
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_date_range:first').text(), '02/08/2017 11:30:00',
+ "the start date should be correctly displayed in readonly after manual update");
+
+ form.destroy();
+ });
+
+ QUnit.test('Daterange field manually input wrong value should show toaster', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.date_end = { string: 'Date End', type: 'date' };
+ this.data.partner.records[0].date_end = '2017-02-08';
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="date" widget="daterange" options="{'related_end_date': 'date_end'}"/>
+ <field name="date_end" widget="daterange" options="{'related_start_date': 'date'}"/>
+ </form>`,
+ interceptsPropagate: {
+ call_service: function (ev) {
+ if (ev.data.service === 'notification') {
+ assert.strictEqual(ev.data.method, 'notify');
+ assert.strictEqual(ev.data.args[0].title, 'Invalid fields:');
+ assert.strictEqual(ev.data.args[0].message, '<ul><li>A date</li></ul>');
+ }
+ }
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('.o_field_date_range:first'), 'blabla');
+ // click outside daterange field
+ await testUtils.dom.click(form.$el);
+ assert.hasClass(form.$('input[name=date]'), 'o_field_invalid',
+ "date field should be displayed as invalid");
+ // update input date with right value
+ await testUtils.fields.editInput(form.$('.o_field_date_range:first'), '02/08/2017');
+ assert.doesNotHaveClass(form.$('input[name=date]'), 'o_field_invalid',
+ "date field should not be displayed as invalid now");
+
+ // again enter wrong value and try to save should raise invalid fields value
+ await testUtils.fields.editInput(form.$('.o_field_date_range:first'), 'blabla');
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldDate');
+
+ QUnit.test('date field: toggle datepicker [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form><field name="foo"/><field name="date"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ });
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should be closed initially");
+
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
+ "datepicker should be opened");
+
+ // focus another field
+ await testUtils.dom.click(form.$('.o_field_widget[name=foo]').focus().mouseenter());
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should close itself when the user clicks outside");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field: toggle datepicker far in the future', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records = [{
+ id: 1,
+ date: "9999-12-30",
+ foo: "yop",
+ }]
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form><field name="foo"/><field name="date"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should be closed initially");
+
+ testUtils.openDatepicker(form.$('.o_datepicker'));
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 1,
+ "datepicker should be opened");
+
+ // focus another field
+ form.$('.o_field_widget[name=foo]').click().focus();
+
+ assert.strictEqual($('.bootstrap-datetimepicker-widget:visible').length, 0,
+ "datepicker should close itself when the user clicks outside");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field is empty if no date is set', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 4,
+ });
+ var $span = form.$('span.o_field_widget');
+ assert.strictEqual($span.length, 1, "should have one span in the form view");
+ assert.strictEqual($span.text(), "", "and it should be empty");
+ form.destroy();
+ });
+
+ QUnit.test('date field: set an invalid date when the field is already set', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var $input = form.$('.o_field_widget[name=date] input');
+
+ assert.strictEqual($input.val(), "02/03/2017");
+
+ $input.val('mmmh').trigger('change');
+ assert.strictEqual($input.val(), "02/03/2017", "should have reset the original value");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field: set an invalid date when the field is not set yet', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="date"/></form>',
+ res_id: 4,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var $input = form.$('.o_field_widget[name=date] input');
+
+ assert.strictEqual($input.text(), "");
+
+ $input.val('mmmh').trigger('change');
+ assert.strictEqual($input.text(), "", "The date field should be empty");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field value should not set on first click', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 4,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // open datepicker and select a date
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '', "date field's input should be empty on first click");
+ testUtils.dom.click($('.day:contains(22)'));
+
+ // re-open datepicker
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual($('.day.active').text(), '22',
+ "datepicker should be highlight with 22nd day of month");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field in form view (with positive time zone offset)', async function (assert) {
+ assert.expect(8);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].date, '2017-02-22', 'the correct value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120; // Should be ignored by date fields
+ },
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017',
+ 'the date should be correctly displayed in readonly');
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ // open datepicker and select another value
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ assert.strictEqual($('.day.active').data('day'), '02/03/2017', 'datepicker should be highlight February 3');
+ testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)').first());
+ testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1));
+ testUtils.dom.click($('.day:contains(22)'));
+ assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/22/2017',
+ 'the selected date should be displayed in the input');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_date').text(), '02/22/2017',
+ 'the selected date should be displayed after saving');
+ form.destroy();
+ });
+
+ QUnit.test('date field in form view (with negative time zone offset)', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ session: {
+ getTZOffset: function () {
+ return -120; // Should be ignored by date fields
+ },
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017',
+ 'the date should be correctly displayed in readonly');
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ form.destroy();
+ });
+
+ QUnit.test('date field dropdown disappears on scroll', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<div class="scrollable" style="height: 2000px;">' +
+ '<field name="date"/>' +
+ '</div>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget', "datepicker should be opened");
+
+ form.el.dispatchEvent(new Event('wheel'));
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget', "datepicker should be closed");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field with warn_future option', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="date" options="{\'datepicker\': {\'warn_future\': true}}"/>' +
+ '</form>',
+ res_id: 4,
+ });
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ // open datepicker and select another value
+ await testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year').eq(11));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(11));
+ await testUtils.dom.click($('.day:contains(31)'));
+
+ var $warn = form.$('.o_datepicker_warning:visible');
+ assert.strictEqual($warn.length, 1, "should have a warning in the form view");
+
+ await testUtils.fields.editSelect(form.$('.o_field_widget[name=date] input'), ''); // remove the value
+
+ $warn = form.$('.o_datepicker_warning:visible');
+ assert.strictEqual($warn.length, 0, "the warning in the form view should be hidden");
+
+ form.destroy();
+ });
+
+ QUnit.test('date field with warn_future option: do not overwrite datepicker option', async function (assert) {
+ assert.expect(2);
+
+ // Making sure we don't have a legit default value
+ // or any onchange that would set the value
+ this.data.partner.fields.date.default = undefined;
+ this.data.partner.onchanges = {};
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo" />' + // Do not let the date field get the focus in the first place
+ '<field name="date" options="{\'datepicker\': {\'warn_future\': true}}"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name="date"]').val(), '02/03/2017',
+ 'The existing record should have a value for the date field');
+
+ // save with no changes
+ await testUtils.form.clickSave(form);
+
+ //Create a new record
+ await testUtils.form.clickCreate(form);
+
+ assert.notOk(form.$('input[name="date"]').val(),
+ 'The new record should not have a value that the framework would have set');
+
+ form.destroy();
+ });
+
+ QUnit.test('date field in editable list view', async function (assert) {
+ assert.expect(8);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="date"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ session: {
+ getTZOffset: function () {
+ return 0;
+ },
+ },
+ });
+
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ assert.strictEqual($cell.text(), '02/03/2017',
+ 'the date should be displayed correctly in readonly');
+ await testUtils.dom.click($cell);
+
+ assert.containsOnce(list, 'input.o_datepicker_input',
+ "the view should have a date input for editable mode");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement,
+ "date input should have the focus");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ // open datepicker and select another value
+ await testUtils.dom.openDatepicker(list.$('.o_datepicker'));
+ assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1));
+ await testUtils.dom.click($('.day:contains(22)'));
+ assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+ assert.strictEqual(list.$('.o_datepicker_input').val(), '02/22/2017',
+ 'the selected date should be displayed in the input');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), '02/22/2017',
+ 'the selected date should be displayed after saving');
+
+ list.destroy();
+ });
+
+ QUnit.test('date field remove value', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].date, false, 'the correct value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ });
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
+ 'the date should be correct in edit mode');
+
+ await testUtils.fields.editAndTrigger(form.$('.o_datepicker_input'), '', ['input', 'change', 'focusout']);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '',
+ 'should have correctly removed the value');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_date').text(), '',
+ 'the selected date should be displayed after saving');
+
+ form.destroy();
+ });
+
+ QUnit.test('do not trigger a field_changed for datetime field with date widget', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="datetime" widget="date"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017',
+ 'the date should be correct');
+
+ testUtils.fields.editAndTrigger(form.$('input[name="datetime"]'),'02/08/2017', ['input', 'change', 'focusout']);
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['read']); // should not have save as nothing changed
+
+ form.destroy();
+ });
+
+ QUnit.test('field date should select its content onclick when there is one', async function (assert) {
+ assert.expect(2);
+ var done = assert.async();
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="date"/></form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$el.on({
+ 'show.datetimepicker': function () {
+ assert.ok($('.bootstrap-datetimepicker-widget').is(':visible'),
+ 'bootstrap-datetimepicker is visible');
+ assert.strictEqual(window.getSelection().toString(), "02/03/2017",
+ 'The whole input of the date field should have been selected');
+ done();
+ }
+ });
+
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+
+ form.destroy();
+ });
+
+ QUnit.test('date field support internalization', async function (assert) {
+ assert.expect(2);
+
+ var originalLocale = moment.locale();
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {date_format: '%d. %b %Y', time_format: '%H:%M:%S'});
+ moment.defineLocale('norvegianForTest', {
+ monthsShort: 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'),
+ monthsParseExact: true,
+ dayOfMonthOrdinalParse: /\d{1,2}\./,
+ ordinal: '%d.',
+ });
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ });
+
+ var dateViewForm = form.$('.o_field_date').text();
+ await testUtils.dom.click(form.$buttons.find('.o_form_button_edit'));
+ await testUtils.openDatepicker(form.$('.o_datepicker'));
+ assert.strictEqual(form.$('.o_datepicker_input').val(), dateViewForm,
+ "input date field should be the same as it was in the view form");
+
+ await testUtils.dom.click($('.day:contains(30)'));
+ var dateEditForm = form.$('.o_datepicker_input').val();
+ await testUtils.dom.click(form.$buttons.find('.o_form_button_save'));
+ assert.strictEqual(form.$('.o_field_date').text(), dateEditForm,
+ "date field should be the same as the one selected in the view form");
+
+ moment.locale(originalLocale);
+ moment.updateLocale('norvegianForTest', null);
+ core._t.database.parameters = originalParameters;
+
+ form.destroy();
+ });
+
+ QUnit.test('date field: hit enter should update value', async function (assert) {
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="date"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ const year = (new Date()).getFullYear();
+
+ await testUtils.fields.editInput(form.el.querySelector('input[name="date"]'), '01/08');
+ await testUtils.fields.triggerKeydown(form.el.querySelector('input[name="date"]'), 'enter');
+ assert.strictEqual(form.el.querySelector('input[name="date"]').value, '01/08/' + year);
+
+ await testUtils.fields.editInput(form.el.querySelector('input[name="date"]'), '08/01');
+ await testUtils.fields.triggerKeydown(form.el.querySelector('input[name="date"]'), 'enter');
+ assert.strictEqual(form.el.querySelector('input[name="date"]').value, '08/01/' + year);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldDatetime');
+
+ QUnit.test('datetime field in form view', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners"><field name="datetime"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ var expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
+ assert.strictEqual(form.$('.o_field_date').text(), expectedDateString,
+ 'the datetime should be correctly displayed in readonly');
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
+ 'the datetime should be correct in edit mode');
+
+ // datepicker should not open on focus
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
+
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
+
+ // select 22 February at 8:23:33
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(3));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day:contains(22)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .fa-clock-o'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-hour'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .hour:contains(08)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-minute'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .minute:contains(25)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-second'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)'));
+ assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+
+ var newExpectedDateString = "04/22/2017 08:25:35";
+ assert.strictEqual(form.$('.o_datepicker_input').val(), newExpectedDateString,
+ 'the selected date should be displayed in the input');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_date').text(), newExpectedDateString,
+ 'the selected date should be displayed after saving');
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime fields do not trigger fieldChange before datetime completly picked', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges = {
+ datetime: function () {},
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="datetime"/></form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step('onchange');
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
+
+ // select a date and time
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(3));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day:contains(22)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .fa-clock-o'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-hour'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .hour:contains(08)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-minute'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .minute:contains(25)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-second'));
+ assert.verifySteps([], "should not have done any onchange yet");
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)'));
+
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
+ assert.strictEqual(form.$('.o_datepicker_input').val(), "04/22/2017 08:25:35");
+ assert.verifySteps(['onchange'], "should have done only one onchange");
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field not visible in form view should not capture the focus on keyboard navigation', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="txt"/>' +
+ '<field name="datetime" invisible="True"/></form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$el.find('textarea[name=txt]').trigger($.Event('keydown', {
+ which: $.ui.keyCode.TAB,
+ keyCode: $.ui.keyCode.TAB,
+ }));
+ assert.strictEqual(document.activeElement, form.$buttons.find('.o_form_button_save')[0],
+ "the save button should be selected, because the datepicker did not capture the focus");
+ form.destroy();
+ });
+
+ QUnit.test('datetime field with datetime formatted without second', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
+ this.data.partner.fields.datetime.required = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="datetime"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M',
+ },
+ });
+
+ var expectedDateString = "08/02/2017 12:00"; // 10:00:00 without timezone
+ assert.strictEqual(form.$('.o_field_date input').val(), expectedDateString,
+ 'the datetime should be correctly displayed in readonly');
+
+ await testUtils.form.clickDiscard(form);
+
+ assert.strictEqual($('.modal').length, 0,
+ "there should not be a Warning dialog");
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field in editable list view', async function (assert) {
+ assert.expect(9);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="datetime"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ var expectedDateString = "02/08/2017 12:00:00"; // 10:00:00 without timezone
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ assert.strictEqual($cell.text(), expectedDateString,
+ 'the datetime should be correctly displayed in readonly');
+
+ // switch to edit mode
+ await testUtils.dom.click($cell);
+ assert.containsOnce(list, 'input.o_datepicker_input',
+ "the view should have a date input for editable mode");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').get(0), document.activeElement,
+ "date input should have the focus");
+
+ assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
+ 'the date should be correct in edit mode');
+
+ assert.containsNone($('body'), '.bootstrap-datetimepicker-widget');
+ testUtils.dom.openDatepicker(list.$('.o_datepicker'));
+ assert.containsOnce($('body'), '.bootstrap-datetimepicker-widget');
+
+ // select 22 February at 8:23:33
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(3));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .day:contains(22)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .fa-clock-o'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-hour'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .hour:contains(08)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-minute'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .minute:contains(25)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .timepicker-second'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .second:contains(35)'));
+ assert.ok(!$('.bootstrap-datetimepicker-widget').length, 'datepicker should be closed');
+
+ var newExpectedDateString = "04/22/2017 08:25:35";
+ assert.strictEqual(list.$('.o_datepicker_input').val(), newExpectedDateString,
+ 'the selected datetime should be displayed in the input');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector)').text(), newExpectedDateString,
+ 'the selected datetime should be displayed after saving');
+
+ list.destroy();
+ });
+
+ QUnit.test('multi edition of datetime field in list view: edit date in input', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree multi_edit="1">' +
+ '<field name="datetime"/>' +
+ '</tree>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ // select two records and edit them
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first .o_data_cell'));
+ assert.containsOnce(list, 'input.o_datepicker_input');
+ list.$('.o_datepicker_input').val("10/02/2019 09:00:00");
+ await testUtils.dom.triggerEvents(list.$('.o_datepicker_input'), ['change']);
+
+ assert.containsOnce(document.body, '.modal');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(list.$('.o_data_row:first .o_data_cell').text(), "10/02/2019 09:00:00");
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), "10/02/2019 09:00:00");
+
+ list.destroy();
+ });
+
+ QUnit.test('datetime field remove value', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="datetime"/></form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].datetime, false, 'the correct value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017 12:00:00',
+ 'the date time should be correct in edit mode');
+
+ await testUtils.fields.editAndTrigger($('.o_datepicker_input'), '', ['input', 'change', 'focusout']);
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '',
+ "should have an empty input");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_date').text(), '',
+ 'the selected date should be displayed after saving');
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field with date/datetime widget (with day change)', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="datetime" widget="date"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return -240;
+ },
+ },
+ });
+
+ var expectedDateString = "02/07/2017 22:00:00"; // local time zone
+ assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
+ 'the datetime (datetime widget) should be correctly displayed in tree view');
+
+ // switch to form view
+ await testUtils.dom.click(form.$('.o_field_widget[name=p] .o_data_row'));
+ assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/07/2017',
+ 'the datetime (date widget) should be correctly displayed in form view');
+
+ form.destroy();
+ });
+
+ QUnit.test('datetime field with date/datetime widget (without day change)', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="datetime" widget="date"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ session: {
+ getTZOffset: function () {
+ return -240;
+ },
+ },
+ });
+
+ var expectedDateString = "02/08/2017 06:00:00"; // with timezone
+ assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
+ 'the datetime (datetime widget) should be correctly displayed in tree view');
+
+ // switch to form view
+ await testUtils.dom.click(form.$('.o_field_widget[name=p] .o_data_row'));
+ assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/08/2017',
+ 'the datetime (date widget) should be correctly displayed in form view');
+
+ form.destroy();
+ });
+
+ QUnit.test('datepicker option: daysOfWeekDisabled', async function (assert) {
+ assert.expect(42);
+
+ this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
+ this.data.partner.fields.datetime.required = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="datetime" ' +
+ 'options=\'{"datepicker": {"daysOfWeekDisabled": [0, 6]}}\'/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickCreate(form);
+ testUtils.dom.openDatepicker(form.$('.o_datepicker'));
+ $.each($('.day:last-child,.day:nth-child(2)'), function (index, value) {
+ assert.hasClass(value, 'disabled', 'first and last days must be disabled');
+ });
+ // the assertions below could be replaced by a single hasClass classic on the jQuery set using the idea
+ // All not <=> not Exists. But we want to be sure that the set is non empty. We don't have an helper
+ // function for that.
+ $.each($('.day:not(:last-child):not(:nth-child(2))'), function (index, value) {
+ assert.doesNotHaveClass(value, 'disabled', 'other days must stay clickable');
+ });
+ form.destroy();
+ });
+
+ QUnit.module('RemainingDays');
+
+ QUnit.test('remaining_days widget on a date field in list view', async function (assert) {
+ assert.expect(16);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
+ this.data.partner.records = [
+ { id: 1, date: '2017-10-08' }, // today
+ { id: 2, date: '2017-10-09' }, // tomorrow
+ { id: 3, date: '2017-10-07' }, // yesterday
+ { id: 4, date: '2017-10-10' }, // + 2 days
+ { id: 5, date: '2017-10-05' }, // - 3 days
+ { id: 6, date: '2018-02-08' }, // + 4 months (diff >= 100 days)
+ { id: 7, date: '2017-06-08' }, // - 4 months (diff >= 100 days)
+ { id: 8, date: false },
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="date" widget="remaining_days"/></tree>',
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday');
+ assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days');
+ assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago');
+ assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018');
+ assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017');
+ assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), '');
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017');
+
+ assert.hasClass(list.$('.o_data_cell:nth(0) div'), 'text-bf text-warning');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'text-bf text-danger');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'text-bf text-danger');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'text-bf text-danger');
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a date field in form view', async function (assert) {
+ assert.expect(4);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
+ this.data.partner.records = [
+ { id: 1, date: '2017-10-08' }, // today
+ ];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="date" widget="remaining_days"/></form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Today');
+ assert.hasClass(form.$('.o_field_widget'), 'text-bf text-warning');
+
+ // in edit mode, this widget should not be editable.
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+ assert.containsOnce(form, 'div.o_field_widget[name=date]');
+
+ form.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a datetime field in list view in UTC', async function (assert) {
+ assert.expect(16);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
+ this.data.partner.records = [
+ { id: 1, datetime: '2017-10-08 20:00:00' }, // today
+ { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow
+ { id: 3, datetime: '2017-10-07 18:00:00' }, // yesterday
+ { id: 4, datetime: '2017-10-10 22:00:00' }, // + 2 days
+ { id: 5, datetime: '2017-10-05 04:00:00' }, // - 3 days
+ { id: 6, datetime: '2018-02-08 04:00:00' }, // + 4 months (diff >= 100 days)
+ { id: 7, datetime: '2017-06-08 04:00:00' }, // - 4 months (diff >= 100 days)
+ { id: 8, datetime: false },
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="datetime" widget="remaining_days"/></tree>',
+ session: {
+ getTZOffset: () => 0,
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday');
+ assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days');
+ assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago');
+ assert.strictEqual(list.$('.o_data_cell:nth(5)').text(), '02/08/2018');
+ assert.strictEqual(list.$('.o_data_cell:nth(6)').text(), '06/08/2017');
+ assert.strictEqual(list.$('.o_data_cell:nth(7)').text(), '');
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017');
+
+ assert.hasClass(list.$('.o_data_cell:nth(0) div'), 'text-bf text-warning');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(1) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(2) div'), 'text-bf text-danger');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(3) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(4) div'), 'text-bf text-danger');
+ assert.doesNotHaveClass(list.$('.o_data_cell:nth(5) div'), 'text-bf text-warning text-danger');
+ assert.hasClass(list.$('.o_data_cell:nth(6) div'), 'text-bf text-danger');
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a datetime field in list view in UTC+6', async function (assert) {
+ assert.expect(6);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC+6
+ this.data.partner.records = [
+ { id: 1, datetime: '2017-10-08 20:00:00' }, // tomorrow
+ { id: 2, datetime: '2017-10-09 08:00:00' }, // tomorrow
+ { id: 3, datetime: '2017-10-07 18:30:00' }, // today
+ { id: 4, datetime: '2017-10-07 12:00:00' }, // yesterday
+ { id: 5, datetime: '2017-10-09 20:00:00' }, // + 2 days
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="datetime" widget="remaining_days"/></tree>',
+ session: {
+ getTZOffset: () => 360,
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday');
+ assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), 'In 2 days');
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/09/2017');
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a date field in list view in UTC-6', async function (assert) {
+ assert.expect(6);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11
+ this.data.partner.records = [
+ { id: 1, date: '2017-10-08' }, // today
+ { id: 2, date: '2017-10-09' }, // tomorrow
+ { id: 3, date: '2017-10-07' }, // yesterday
+ { id: 4, date: '2017-10-10' }, // + 2 days
+ { id: 5, date: '2017-10-05' }, // - 3 days
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="date" widget="remaining_days"/></tree>',
+ session: {
+ getTZOffset: () => -360,
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Yesterday');
+ assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'In 2 days');
+ assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '3 days ago');
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0) .o_field_widget').attr('title'), '10/08/2017');
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.test('remaining_days widget on a datetime field in list view in UTC-8', async function (assert) {
+ assert.expect(5);
+
+ const unpatchDate = patchDate(2017, 9, 8, 15, 35, 11); // October 8 2017, 15:35:11, UTC-8
+ this.data.partner.records = [
+ { id: 1, datetime: '2017-10-08 20:00:00' }, // today
+ { id: 2, datetime: '2017-10-09 07:00:00' }, // today
+ { id: 3, datetime: '2017-10-09 10:00:00' }, // tomorrow
+ { id: 4, datetime: '2017-10-08 06:00:00' }, // yesterday
+ { id: 5, datetime: '2017-10-07 02:00:00' }, // - 2 days
+ ];
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="datetime" widget="remaining_days"/></tree>',
+ session: {
+ getTZOffset: () => -560,
+ },
+ });
+
+ assert.strictEqual(list.$('.o_data_cell:nth(0)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(1)').text(), 'Today');
+ assert.strictEqual(list.$('.o_data_cell:nth(2)').text(), 'Tomorrow');
+ assert.strictEqual(list.$('.o_data_cell:nth(3)').text(), 'Yesterday');
+ assert.strictEqual(list.$('.o_data_cell:nth(4)').text(), '2 days ago');
+
+ list.destroy();
+ unpatchDate();
+ });
+
+ QUnit.module('FieldMonetary');
+
+ QUnit.test('monetary field in form view', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a09.10',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '9.10',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ await testUtils.fields.editInput(form.$('.o_field_monetary input'), '108.2458938598598');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.form.clickSave(form);
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0108.25',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field rounding using formula in form view', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Test computation and rounding
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('.o_field_monetary input'), '=100/3');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a033.33',
+ 'The new value should be calculated and rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field with currency symbol after', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.00\u00a0€',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '0.00',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), '€',
+ 'The input should be followed by a span containing the currency symbol.');
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '108.2458938598598');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '108.2458938598598',
+ 'The value should not be formated yet.');
+
+ await testUtils.form.clickSave(form);
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '108.25\u00a0€',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field with currency digits != 2', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [{
+ id: 1,
+ bar: false,
+ foo: "pouet",
+ int_field: 68,
+ qux: 99.1234,
+ currency_id: 1,
+ }];
+ this.data.currency.records = [{
+ id: 1,
+ display_name: "VEF",
+ symbol: "Bs.F",
+ position: "after",
+ digits: [16, 4],
+ }];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1234\u00a0Bs.F',
+ 'The value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.1234',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').parent().children().eq(1).text(), 'Bs.F',
+ 'The input should be followed by a span containing the currency symbol.');
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=qux] input'), '99.111111111');
+ assert.strictEqual(form.$('.o_field_widget[name=qux] input').val(), '99.111111111',
+ 'The value should not be formated yet.');
+
+ await testUtils.form.clickSave(form);
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '99.1111\u00a0Bs.F',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field in editable list view', async function (assert) {
+ assert.expect(9);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ var dollarValues = list.$('td').filter(function () {return _.str.include($(this).text(), '$');});
+ assert.strictEqual(dollarValues.length, 1,
+ 'Only one line has dollar as a currency.');
+
+ var euroValues = list.$('td').filter(function () {return _.str.include($(this).text(), '€');});
+ assert.strictEqual(euroValues.length, 1,
+ 'One one line has euro as a currency.');
+
+ var zeroValues = list.$('td.o_data_cell').filter(function () {return $(this).text() === '';});
+ assert.strictEqual(zeroValues.length, 1,
+ 'Unset float values should be rendered as empty strings.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)');
+ await testUtils.dom.click($cell);
+
+ assert.strictEqual($cell.children().length, 1,
+ 'The cell td should only contain the special div of monetary widget.');
+ assert.containsOnce(list, '[name="qux"] input',
+ 'The view should have 1 input for editable monetary float.');
+ assert.strictEqual(list.$('[name="qux"] input').val(), '9.10',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(list.$('[name="qux"] input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ await testUtils.fields.editInput(list.$('[name="qux"] input'), '108.2458938598598');
+ assert.strictEqual(list.$('[name="qux"] input').val(), '108.2458938598598',
+ 'The typed value should be correctly displayed.');
+
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('tr.o_data_row td:not(.o_list_record_selector):contains($)').text(), '$\u00a0108.25',
+ 'The new value should be rounded properly.');
+
+ list.destroy();
+ });
+
+ QUnit.test('monetary field with real monetary field in model', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.qux.type = "monetary";
+ this.data.partner.fields.quux = {
+ string: "Quux", type: "monetary", digits: [16,1], searchable: true, readonly: true,
+ };
+
+ (_.find(this.data.partner.records, function (record) { return record.id === 5; })).quux = 4.2;
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.qux = obj.bar ? 100 : obj.qux;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux"/>' +
+ '<field name="quux"/>' +
+ '<field name="currency_id"/>' +
+ '<field name="bar"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_monetary').first().html(), "$&nbsp;9.10",
+ "readonly value should contain the currency");
+ assert.strictEqual(form.$('.o_field_monetary').first().next().html(), "$&nbsp;4.20",
+ "readonly value should contain the currency");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_monetary > input').val(), "9.10",
+ "input value in edition should only contain the value, without the currency");
+
+ await testUtils.dom.click(form.$('input[type="checkbox"]'));
+ assert.containsOnce(form, '.o_field_monetary > input',
+ "After the onchange, the monetary <input/> should not have been duplicated");
+ assert.containsOnce(form, '.o_field_monetary[name=quux]',
+ "After the onchange, the monetary readonly field should not have been duplicated");
+
+ await testUtils.fields.many2one.clickOpenDropdown('currency_id');
+ await testUtils.fields.many2one.clickItem('currency_id','€');
+ assert.strictEqual(form.$('.o_field_monetary > span').html(), "€",
+ "After currency change, the monetary field currency should have been updated");
+ assert.strictEqual(form.$('.o_field_monetary').first().next().html(), "4.20&nbsp;€",
+ "readonly value should contain the updated currency");
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field with monetary field given in options', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.qux.type = "monetary";
+ this.data.partner.fields.company_currency_id = {
+ string: "Company Currency", type: "many2one", relation: "currency",
+ };
+ this.data.partner.records[4].company_currency_id = 2;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" options="{\'currency_field\': \'company_currency_id\'}"/>' +
+ '<field name="company_currency_id"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 5,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_monetary').html(), "9.10&nbsp;€",
+ "field monetary should be formatted with correct currency");
+
+ form.destroy();
+ });
+
+ QUnit.test('should keep the focus when being edited in x2many lists', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.fields.currency_id.default = 1;
+ this.data.partner.fields.m2m = {
+ string: "m2m", type: "many2many", relation: 'partner', default: [[6, false, [2]]],
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="p"/>' +
+ '<field name="m2m"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree editable="bottom">' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ },
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // test the monetary field inside the one2many
+ var $o2m = form.$('.o_field_widget[name=p]');
+ await testUtils.dom.click($o2m.find('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput($o2m.find('.o_field_widget input'), "22");
+
+ assert.strictEqual($o2m.find('.o_field_widget input').get(0), document.activeElement,
+ "the focus should still be on the input");
+ assert.strictEqual($o2m.find('.o_field_widget input').val(), "22",
+ "the value should not have been formatted yet");
+
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual($o2m.find('.o_field_widget[name=qux]').html(), "$&nbsp;22.00",
+ "the value should have been formatted after losing the focus");
+
+ // test the monetary field inside the many2many
+ var $m2m = form.$('.o_field_widget[name=m2m]');
+ await testUtils.dom.click($m2m.find('.o_data_row td:first'));
+ await testUtils.fields.editInput($m2m.find('.o_field_widget input'), "22");
+
+ assert.strictEqual($m2m.find('.o_field_widget input').get(0), document.activeElement,
+ "the focus should still be on the input");
+ assert.strictEqual($m2m.find('.o_field_widget input').val(), "22",
+ "the value should not have been formatted yet");
+
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual($m2m.find('.o_field_widget[name=qux]').html(), "22.00&nbsp;€",
+ "the value should have been formatted after losing the focus");
+
+ form.destroy();
+ });
+
+ QUnit.test('monetary field with currency set by an onchange',async function (assert) {
+ // this test ensures that the monetary field can be re-rendered with and
+ // without currency (which can happen as the currency can be set by an
+ // onchange)
+ assert.expect(8);
+
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.currency_id = obj.int_field ? 2 : null;
+ },
+ };
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="int_field"/>' +
+ '<field name="qux" widget="monetary"/>' +
+ '<field name="currency_id" invisible="1"/>' +
+ '</tree>',
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
+ assert.containsOnce(list, 'div.o_field_widget[name=qux] input',
+ "monetary field should have been rendered correctly (without currency)");
+ assert.containsNone(list, '.o_field_widget[name=qux] span',
+ "monetary field should have been rendered correctly (without currency)");
+
+ // set a value for int_field -> should set the currency and re-render qux
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'),'7');
+ assert.containsOnce(list, 'div.o_field_widget[name=qux] input',
+ "monetary field should have been re-rendered correctly (with currency)");
+ assert.strictEqual(list.$('.o_field_widget[name=qux] span:contains(€)').length, 1,
+ "monetary field should have been re-rendered correctly (with currency)");
+ var $quxInput = list.$('.o_field_widget[name=qux] input');
+ await testUtils.dom.click($quxInput);
+ assert.strictEqual(document.activeElement, $quxInput[0],
+ "focus should be on the qux field's input");
+
+ // unset the value of int_field -> should unset the currency and re-render qux
+ await testUtils.dom.click(list.$('.o_field_widget[name=int_field]'));
+ await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'),'0');
+ $quxInput = list.$('div.o_field_widget[name=qux] input');
+ assert.strictEqual($quxInput.length, 1,
+ "monetary field should have been re-rendered correctly (without currency)");
+ assert.containsNone(list, '.o_field_widget[name=qux] span',
+ "monetary field should have been re-rendered correctly (without currency)");
+ await testUtils.dom.click($quxInput);
+ assert.strictEqual(document.activeElement, $quxInput[0],
+ "focus should be on the qux field's input");
+
+ list.destroy();
+ });
+
+ QUnit.module('FieldInteger');
+
+ QUnit.test('integer field when unset', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="int_field"/></form>',
+ res_id: 4,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Non-set integer field should be recognized as 0.');
+ assert.strictEqual(form.$('.o_field_widget').text(), "0",
+ 'Non-set integer field should be recognized as 0.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field in form view', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="int_field"/></form>',
+ res_id: 2,
+ });
+
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ 'Integer field should be considered set for value 0.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=int_field]').val(), '0',
+ 'The value should be rendered correctly in edit mode.');
+
+ await testUtils.fields.editInput(form.$('input[name=int_field]'), '-18');
+ assert.strictEqual(form.$('input[name=int_field]').val(), '-18',
+ 'The value should be correctly displayed in the input.');
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '-18',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field rounding using formula in form view', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="int_field"/></form>',
+ res_id: 2,
+ });
+
+ // Test computation and rounding
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.editInput(form.$('input[name=int_field]'), '=100/3');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '33',
+ 'The new value should be calculated properly.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field in form view with virtual id', async function (assert) {
+ assert.expect(1);
+ var params = {
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="id"/></form>',
+ };
+
+ params.res_id = this.data.partner.records[1].id = "2-20170808020000";
+ var form = await createView(params);
+ assert.strictEqual(form.$('.o_field_widget').text(), "2-20170808020000",
+ "Should display virtual id");
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field in editable list view', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="int_field"/>' +
+ '</tree>',
+ });
+
+ var zeroValues = list.$('td').filter(function () {return $(this).text() === '0';});
+ assert.strictEqual(zeroValues.length, 1,
+ 'Unset integer values should not be rendered as zeros.');
+
+ // switch to edit mode
+ var $cell = list.$('tr.o_data_row td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+
+ assert.containsOnce(list, 'input[name="int_field"]',
+ 'The view should have 1 input for editable integer.');
+
+ await testUtils.fields.editInput(list.$('input[name="int_field"]'), '-28');
+ assert.strictEqual(list.$('input[name="int_field"]').val(), '-28',
+ 'The value should be displayed properly in the input.');
+
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('td:not(.o_list_record_selector)').first().text(), '-28',
+ 'The new value should be saved and displayed properly.');
+
+ list.destroy();
+ });
+
+ QUnit.test('integer field with type number option', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field" options="{\'type\': \'number\'}"/>' +
+ '</form>',
+ res_id: 4,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.ok(form.$('.o_field_widget')[0].hasAttribute('type'),
+ 'Integer field with option type must have a type attribute.');
+ assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'number',
+ 'Integer field with option type must have a type attribute equals to "number".');
+
+ await testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget').val(), '1234567890',
+ 'Integer value must be not formatted if input type is number.');
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').text(), '1,234,567,890',
+ 'Integer value must be formatted in readonly view even if the input type is number.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field without type number option', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '</form>',
+ res_id: 4,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.hasAttrValue(form.$('.o_field_widget'), 'type', 'text',
+ 'Integer field without option type must have a text type (default type).');
+
+ await testUtils.fields.editInput(form.$('input[name=int_field]'), '1234567890');
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget').val(), '1,234,567,890',
+ 'Integer value must be formatted if input type isn\'t number.');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field without formatting', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records = [{
+ 'id': 999,
+ 'int_field': 8069,
+ }];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field" options="{\'format\': \'false\'}"/>' +
+ '</form>',
+ res_id: 999,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+ assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), '8069',
+ 'Integer value must not be formatted');
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget').val(), '8069',
+ 'Integer value must not be formatted');
+
+ form.destroy();
+ });
+
+ QUnit.test('integer field is formatted by default', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records = [{
+ 'id': 999,
+ 'int_field': 8069,
+ }];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field" />' +
+ '</form>',
+ res_id: 999,
+ translateParameters: {
+ thousands_sep: ",",
+ grouping: [3, 0],
+ },
+ });
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+ assert.strictEqual(form.$('.o_field_widget[name=int_field]').text(), '8,069',
+ 'Integer value must be formatted by default');
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget').val(), '8,069',
+ 'Integer value must be formatted by default');
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldFloatTime');
+
+ QUnit.test('float_time field in form view', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float_time"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 48 / 60 = 0.8
+ assert.strictEqual(args.args[1].qux, -11.8, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 5,
+ });
+
+ // 9 + 0.1 * 60 = 9.06
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '09:06',
+ 'The formatted time value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=qux]').val(), '09:06',
+ 'The value should be rendered correctly in the input.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '-11:48');
+ assert.strictEqual(form.$('input[name=qux]').val(), '-11:48',
+ 'The new value should be displayed properly in the input.');
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '-11:48',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldFloatFactor');
+
+ QUnit.test('float_factor field in form view', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float_factor" options="{\'factor\': 0.5}" digits="[16,2]"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 16.4 / 2 = 8.2
+ assert.strictEqual(args.args[1].qux, 4.6, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 5,
+ });
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '4.55', // 9.1 / 0.5
+ 'The formatted value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('input[name=qux]').val(), '4.55',
+ 'The value should be rendered correctly in the input.');
+
+ await testUtils.fields.editInput(form.$('input[name=qux]'), '2.3');
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '2.30',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldFloatToggle');
+
+ QUnit.test('float_toggle field in form view', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="qux" widget="float_toggle" options="{\'factor\': 0.125, \'range\': [0, 1, 0.75, 0.5, 0.25]}" digits="[5,3]"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ // 1.000 / 0.125 = 8
+ assert.strictEqual(args.args[1].qux, 8, 'the correct float value should be saved');
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '0.056',
+ 'The formatted time value should be displayed properly.');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('button.o_field_float_toggle').text(), '0.056',
+ 'The value should be rendered correctly on the button.');
+
+ await testUtils.dom.click(form.$('button.o_field_float_toggle'));
+
+ assert.strictEqual(form.$('button.o_field_float_toggle').text(), '1.000',
+ 'The value should be rendered correctly on the button.');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '1.000',
+ 'The new value should be saved and displayed properly.');
+
+ form.destroy();
+ });
+
+
+ QUnit.module('PhoneWidget');
+
+ QUnit.test('phone field in form view on normal screens', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ config: {
+ device: {
+ size_class: config.device.SIZES.LG,
+ },
+ },
+ });
+
+ var $phone = form.$('a.o_field_widget.o_form_uri');
+ assert.strictEqual($phone.length, 1,
+ "should have rendered the phone number as a link with correct classes");
+ assert.strictEqual($phone.text(), 'yop',
+ "value should be displayed properly");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'input[type="text"].o_field_widget',
+ "should have an input for the phone field");
+ assert.strictEqual(form.$('input[type="text"].o_field_widget').val(), 'yop',
+ "input should contain field value in edit mode");
+
+ // change value in edit mode
+ await testUtils.fields.editInput(form.$('input[type="text"].o_field_widget'), 'new');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('a.o_field_widget.o_form_uri').text(), 'new',
+ "new value should be displayed properly");
+
+ form.destroy();
+ });
+
+ QUnit.test('phone field in editable list view on normal screens', async function (assert) {
+ assert.expect(8);
+ var doActionCount = 0;
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="foo" widget="phone"/></tree>',
+ config: {
+ device: {
+ size_class: config.device.SIZES.LG,
+ },
+ },
+ });
+
+ assert.containsN(list, 'tbody td:not(.o_list_record_selector)', 5);
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'yop',
+ "value should be displayed properly with a link to send SMS");
+
+ assert.containsN(list, 'a.o_field_widget.o_form_uri', 5,
+ "should have the correct classnames");
+
+ // Edit a line and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row', 'should be set as edit mode');
+ assert.strictEqual($cell.find('input').val(), 'yop',
+ 'should have the corect value in internal input');
+ await testUtils.fields.editInput($cell.find('input'), 'new');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row', 'should not be in edit mode anymore');
+ assert.strictEqual(list.$('tbody td:not(.o_list_record_selector) a').first().text(), 'new',
+ "value should be properly updated");
+ assert.containsN(list, 'a.o_field_widget.o_form_uri', 5,
+ "should still have links with correct classes");
+
+ list.destroy();
+ });
+
+ QUnit.test('use TAB to navigate to a phone field', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="display_name"/>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ testUtils.dom.click(form.$('input[name=display_name]'));
+ assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
+ "display_name should be focused");
+ form.$('input[name="display_name"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ "foo should be focused");
+
+ form.destroy();
+ });
+
+ QUnit.module('PriorityWidget');
+
+ QUnit.test('priority widget when not set', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="priority"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget.o_priority:not(.o_field_empty)').length, 1,
+ "widget should be considered set, even though there is no value for this field");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 0,
+ "should have no full star since there is no value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 2,
+ "should have two empty stars since there is no value");
+
+ form.destroy();
+ });
+
+ QUnit.test('priority widget in form view', async function (assert) {
+ assert.expect(22);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="priority"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget.o_priority:not(.o_field_empty)').length, 1,
+ "widget should be considered set");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // hover last star
+ form.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').last().trigger('mouseover');
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should temporary have two full stars since we are hovering the third value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should temporary have no empty star since we are hovering the third value");
+
+ // Here we should test with mouseout, but currently the effect associated with it
+ // occurs in a setTimeout after 200ms so it's not trivial to test it here.
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // switch to edit mode to check that the new value was properly written
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 1,
+ "should still have one full star since the value is the second value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 1,
+ "should still have one empty star since the value is the second value");
+
+ // click on the second star in edit mode
+ await testUtils.dom.click(form.$('.o_field_widget.o_priority a.o_priority_star.fa-star-o').last());
+
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(form.$('.o_field_widget.o_priority').find('a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ form.destroy();
+ });
+
+ QUnit.test('priority widget in editable list view', async function (assert) {
+ assert.expect(25);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="selection" widget="priority"/></tree>',
+ });
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority:not(.o_field_empty)').length, 1,
+ "widget should be considered set");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // Here we should test with mouseout, but currently the effect associated with it
+ // occurs in a setTimeout after 200ms so it's not trivial to test it here.
+
+ // switch to edit mode and check the result
+ var $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 1,
+ "should have one full star since the value is the second value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 1,
+ "should have one empty star since the value is the second value");
+
+ // hover last star
+ list.$('.o_data_row .o_priority a.o_priority_star.fa-star-o').first().trigger('mouseenter');
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should have two stars for representing each possible value: no star, one star and two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star').length, 2,
+ "should temporary have two full stars since we are hovering the third value");
+ assert.strictEqual(list.$('.o_data_row').first().find('a.o_priority_star.fa-star-o').length, 0,
+ "should temporary have no empty star since we are hovering the third value");
+
+ // click on the first star in readonly mode
+ await testUtils.dom.click(list.$('.o_priority a.o_priority_star.fa-star').first());
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0,
+ "should now have no full star since the value is the first value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2,
+ "should now have two empty stars since the value is the first value");
+
+ // re-enter edit mode to force re-rendering the widget to check if the value was correctly saved
+ $cell = list.$('tbody td:not(.o_list_record_selector)').first();
+ await testUtils.dom.click($cell);
+
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star').length, 0,
+ "should now only have no full star since the value is the first value");
+ assert.strictEqual(list.$('.o_data_row').first().find('.o_priority a.o_priority_star.fa-star-o').length, 2,
+ "should now have two empty stars since the value is the first value");
+
+ // Click on second star in edit mode
+ await testUtils.dom.click(list.$('.o_priority a.o_priority_star.fa-star-o').last());
+
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star').length, 2,
+ "should still have two stars");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star').length, 2,
+ "should now have two full stars since the value is the third value");
+ assert.strictEqual(list.$('.o_data_row').last().find('.o_priority a.o_priority_star.fa-star-o').length, 0,
+ "should now have no empty star since the value is the third value");
+
+ list.destroy();
+ });
+
+ QUnit.test('priority widget with readonly attribute', async function (assert) {
+ assert.expect(1);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="selection" widget="priority" readonly="1"/>
+ </form>`,
+ res_id: 2,
+ });
+
+ assert.containsN(form, '.o_field_widget.o_priority span', 2,
+ "stars of priority widget should rendered with span tag if readonly");
+
+ form.destroy();
+ });
+
+ QUnit.module('StateSelection Widget');
+
+ QUnit.test('state_selection widget in form view', async function (assert) {
+ assert.expect(21);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="state_selection"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ disable_autofocus: true,
+ },
+ });
+
+ assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_red',
+ "should have one red status since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green',
+ "should not have one green status since selection is the second, blocked state");
+ assert.containsNone(form, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ await testUtils.dom.click(form.$('.o_field_widget.o_selection .o_status').first());
+ assert.containsOnce(form, '.dropdown-menu.state:visible',
+ "there should be a dropdown");
+ assert.containsN(form, '.dropdown-menu.state:visible .dropdown-item', 2,
+ "there should be two options in the dropdown");
+
+ // Click on the first option, "Normal"
+ await testUtils.dom.click(form.$('.dropdown-menu.state:visible .dropdown-item').first());
+ assert.containsNone(form, '.dropdown-menu.state:visible',
+ "there should not be a dropdown anymore");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red',
+ "should not have one red status since selection is the first, normal state");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green',
+ "should not have one green status since selection is the first, normal state");
+ assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status',
+ "should have one grey status since selection is the first, normal state");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsNone(form, '.dropdown-menu.state:visible',
+ "there should still not be a dropdown");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red',
+ "should still not have one red status since selection is the first, normal state");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_green',
+ "should still not have one green status since selection is the first, normal state");
+ assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status',
+ "should still have one grey status since selection is the first, normal state");
+
+ // Click on the status button to make the dropdown appear
+ await testUtils.dom.click(form.$('.o_field_widget.o_selection .o_status').first());
+ assert.containsOnce(form, '.dropdown-menu.state:visible',
+ "there should be a dropdown");
+ assert.containsN(form, '.dropdown-menu.state:visible .dropdown-item', 2,
+ "there should be two options in the dropdown");
+
+ // Click on the last option, "Done"
+ await testUtils.dom.click(form.$('.dropdown-menu.state:visible .dropdown-item').last());
+ assert.containsNone(form, '.dropdown-menu.state:visible',
+ "there should not be a dropdown anymore");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red',
+ "should not have one red status since selection is the third, done state");
+ assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_green',
+ "should have one green status since selection is the third, done state");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsNone(form, '.dropdown-menu.state:visible',
+ "there should still not be a dropdown anymore");
+ assert.containsNone(form, '.o_field_widget.o_selection > a span.o_status.o_status_red',
+ "should still not have one red status since selection is the third, done state");
+ assert.containsOnce(form, '.o_field_widget.o_selection > a span.o_status.o_status_green',
+ "should still have one green status since selection is the third, done state");
+
+ form.destroy();
+ });
+
+ QUnit.test('state_selection widget with readonly modifier', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="selection" widget="state_selection" readonly="1"/></form>',
+ res_id: 1,
+ });
+
+ assert.hasClass(form.$('.o_selection'), 'o_readonly_modifier');
+ assert.hasClass(form.$('.o_selection > a'), 'disabled');
+ assert.isNotVisible(form.$('.dropdown-menu.state'));
+
+ await testUtils.dom.click(form.$('.o_selection > a'));
+ assert.isNotVisible(form.$('.dropdown-menu.state'));
+
+ form.destroy();
+ });
+
+ QUnit.test('state_selection widget in editable list view', async function (assert) {
+ assert.expect(32);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="selection" widget="state_selection"/>' +
+ '</tree>',
+ });
+
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5,
+ "should have five status selection widgets");
+ assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red',
+ "should have one red status");
+ assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green',
+ "should have one green status");
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ var $cell = list.$('tbody td.o_state_selection_cell').first();
+ await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').first());
+ assert.doesNotHaveClass($cell.parent(), 'o_selected_row',
+ 'should not be in edit mode since we clicked on the state selection widget');
+ assert.containsOnce(list, '.dropdown-menu.state:visible',
+ "there should be a dropdown");
+ assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2,
+ "there should be two options in the dropdown");
+
+ // Click on the first option, "Normal"
+ await testUtils.dom.click(list.$('.dropdown-menu.state:visible .dropdown-item').first());
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5,
+ "should still have five status selection widgets");
+ assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red',
+ "should now have no red status");
+ assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green',
+ "should still have one green status");
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ // switch to edit mode and check the result
+ $cell = list.$('tbody td.o_state_selection_cell').first();
+ await testUtils.dom.click($cell);
+ assert.hasClass($cell.parent(),'o_selected_row',
+ 'should now be in edit mode');
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5,
+ "should still have five status selection widgets");
+ assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red',
+ "should now have no red status");
+ assert.containsOnce(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green',
+ "should still have one green status");
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ // Click on the status button to make the dropdown appear
+ await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').first());
+ assert.containsOnce(list, '.dropdown-menu.state:visible',
+ "there should be a dropdown");
+ assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2,
+ "there should be two options in the dropdown");
+
+ // Click on another row
+ var $lastCell = list.$('tbody td.o_state_selection_cell').last();
+ await testUtils.dom.click($lastCell);
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown anymore");
+ var $firstCell = list.$('tbody td.o_state_selection_cell').first();
+ assert.doesNotHaveClass($firstCell.parent(), 'o_selected_row',
+ 'first row should not be in edit mode anymore');
+ assert.hasClass($lastCell.parent(),'o_selected_row',
+ 'last row should be in edit mode');
+
+ // Click on the last status button to make the dropdown appear
+ await testUtils.dom.click(list.$('.o_state_selection_cell .o_selection > a span.o_status').last());
+ assert.containsOnce(list, '.dropdown-menu.state:visible',
+ "there should be a dropdown");
+ assert.containsN(list, '.dropdown-menu.state:visible .dropdown-item', 2,
+ "there should be two options in the dropdown");
+
+ // Click on the last option, "Done"
+ await testUtils.dom.click(list.$('.dropdown-menu.state:visible .dropdown-item').last());
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown anymore");
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5,
+ "should still have five status selection widgets");
+ assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red',
+ "should still have no red status");
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', 2,
+ "should now have two green status");
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status', 5,
+ "should have five status selection widgets");
+ assert.containsNone(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_red',
+ "should have no red status");
+ assert.containsN(list, '.o_state_selection_cell .o_selection > a span.o_status.o_status_green', 2,
+ "should have two green status");
+ assert.containsNone(list, '.dropdown-menu.state:visible',
+ "there should not be a dropdown");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('FavoriteWidget');
+
+ QUnit.test('favorite widget in kanban view', async function (assert) {
+ assert.expect(4);
+
+ var kanban = await createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '<kanban class="o_kanban_test">' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="bar" widget="boolean_favorite" />' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ domain: [['id', '=', 1]],
+ });
+
+ assert.containsOnce(kanban, '.o_kanban_record .o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+ assert.strictEqual(kanban.$('.o_kanban_record .o_field_widget.o_favorite > a').text(), ' Remove from Favorites',
+ 'the label should say "Remove from Favorites"');
+
+ // click on favorite
+ await testUtils.dom.click(kanban.$('.o_field_widget.o_favorite'));
+ assert.containsNone(kanban, '.o_kanban_record .o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should not be favorite');
+ assert.strictEqual(kanban.$('.o_kanban_record .o_field_widget.o_favorite > a').text(), ' Add to Favorites',
+ 'the label should say "Add to Favorites"');
+
+ kanban.destroy();
+ });
+
+ QUnit.test('favorite widget in form view', async function (assert) {
+ assert.expect(10);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="bar" widget="boolean_favorite" />' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+ assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites',
+ 'the label should say "Remove from Favorites"');
+
+ // click on favorite
+ await testUtils.dom.click(form.$('.o_field_widget.o_favorite'));
+ assert.containsNone(form, '.o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should not be favorite');
+ assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Add to Favorites',
+ 'the label should say "Add to Favorites"');
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star-o',
+ 'should not be favorite');
+ assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Add to Favorites',
+ 'the label should say "Add to Favorites"');
+
+ // click on favorite
+ await testUtils.dom.click(form.$('.o_field_widget.o_favorite'));
+ assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+ assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites',
+ 'the label should say "Remove from Favorites"');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+ assert.strictEqual(form.$('.o_field_widget.o_favorite > a').text(), ' Remove from Favorites',
+ 'the label should say "Remove from Favorites"');
+
+ form.destroy();
+ });
+
+ QUnit.test('favorite widget in editable list view without label', async function (assert) {
+ assert.expect(4);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="bar" widget="boolean_favorite" nolabel="1" />' +
+ '</tree>',
+ });
+
+ assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+
+ // switch to edit mode
+ await testUtils.dom.click(list.$('tbody td:not(.o_list_record_selector)').first());
+ assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should be favorite');
+
+ // click on favorite
+ await testUtils.dom.click(list.$('.o_data_row:first .o_field_widget.o_favorite'));
+ assert.containsNone(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star',
+ 'should not be favorite');
+
+ // save
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.containsOnce(list, '.o_data_row:first .o_field_widget.o_favorite > a i.fa.fa-star-o',
+ 'should not be favorite');
+
+ list.destroy();
+ });
+
+
+ QUnit.module('LabelSelectionWidget');
+
+ QUnit.test('label_selection widget in form view', async function (assert) {
+ assert.expect(12);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="selection" widget="label_selection" ' +
+ ' options="{\'classes\': {\'normal\': \'secondary\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_widget.badge.badge-warning',
+ "should have a warning status label since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-secondary',
+ "should not have a default status since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-success',
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ // // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_widget.badge.badge-warning',
+ "should have a warning status label since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-secondary',
+ "should not have a default status since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-success',
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_widget.badge.badge-warning',
+ "should have a warning status label since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-secondary',
+ "should not have a default status since selection is the second, blocked state");
+ assert.containsNone(form, '.o_field_widget.badge.badge-success',
+ "should not have a success status since selection is the second, blocked state");
+ assert.strictEqual(form.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the label should say 'Blocked' since this is the label value for that state");
+
+ form.destroy();
+ });
+
+ QUnit.test('label_selection widget in editable list view', async function (assert) {
+ assert.expect(21);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="selection" widget="label_selection"' +
+ ' options="{\'classes\': {\'normal\': \'secondary\', \'blocked\': \'warning\',\'done\': \'success\'}}"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-warning',
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-secondary',
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-success',
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ // switch to edit mode and check the result
+ await testUtils.dom.clickFirst(list.$('tbody td:not(.o_list_record_selector)'));
+ assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-warning',
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-secondary',
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-success',
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ // save and check the result
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_save'));
+ assert.strictEqual(list.$('.o_field_widget.badge:not(:empty)').length, 3,
+ "should have three visible status labels");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-warning',
+ "should have one warning status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-warning').text(), 'Blocked',
+ "the warning label should read 'Blocked'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-secondary',
+ "should have one default status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-secondary').text(), 'Normal',
+ "the default label should read 'Normal'");
+ assert.containsOnce(list, '.o_field_widget.badge.badge-success',
+ "should have one success status label");
+ assert.strictEqual(list.$('.o_field_widget.badge.badge-success').text(), 'Done',
+ "the success label should read 'Done'");
+
+ list.destroy();
+ });
+
+
+ QUnit.module('StatInfo');
+
+ QUnit.test('statinfo widget formats decimal precision', async function (assert) {
+ // sometimes the round method can return numbers such as 14.000001
+ // when asked to round a number to 2 decimals, as such is the behaviour of floats.
+ // we check that even in that eventuality, only two decimals are displayed
+ assert.expect(2);
+
+ this.data.partner.fields.monetary = {string: "Monetary", type: 'monetary'};
+ this.data.partner.records[0].monetary = 9.999999;
+ this.data.partner.records[0].currency_id = 1;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<button class="oe_stat_button" name="items" icon="fa-gear">' +
+ '<field name="qux" widget="statinfo"/>' +
+ '</button>' +
+ '<button class="oe_stat_button" name="money" icon="fa-money">' +
+ '<field name="monetary" widget="statinfo"/>' +
+ '</button>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // formatFloat renders according to this.field.digits
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(0).text(),
+ '0.4', "Default precision should be [16,1]");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').eq(1).text(),
+ '10.00', "Currency decimal precision should be 2");
+
+ form.destroy();
+ });
+
+ QUnit.test('statinfo widget in form view', async function (assert) {
+ assert.expect(9);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field name="int_field" widget="statinfo"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'int_field', "should have 'int_field' as text");
+
+ form.destroy();
+ });
+
+ QUnit.test('statinfo widget in form view with specific label_field', async function (assert) {
+ assert.expect(9);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field string="Useful stat button" name="int_field" widget="statinfo" ' +
+ 'options="{\'label_field\': \'foo\'}"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ 'yop', "should have 'yop' as text, since it is the value of field foo");
+
+ form.destroy();
+ });
+
+ QUnit.test('statinfo widget in form view with no label', async function (assert) {
+ assert.expect(9);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<div class="oe_button_box" name="button_box">' +
+ '<button class="oe_stat_button" name="items" type="object" icon="fa-gear">' +
+ '<field string="Useful stat button" name="int_field" widget="statinfo" nolabel="1"/>' +
+ '</button>' +
+ '</div>' +
+ '<group>' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should still have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should still have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.oe_stat_button .o_field_widget.o_stat_info',
+ "should have one stat button");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_value').text(),
+ '10', "should have 10 as value");
+ assert.strictEqual(form.$('.oe_stat_button .o_field_widget.o_stat_info .o_stat_text').text(),
+ '', "should not have any label");
+
+ form.destroy();
+ });
+
+
+ QUnit.module('PercentPie');
+
+ QUnit.test('percentpie widget in form view with value < 50%', async function (assert) {
+ assert.expect(12);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field" widget="percentpie"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '10%', "should have 10% as pie value since int_field=10");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(180deg);'), "left mask should be covering the whole left side of the pie");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last().attr('style'),
+ 'transform: rotate(36deg);'), "right mask should be rotated from 360*(10/100) = 36 degrees");
+
+ form.destroy();
+ });
+
+ QUnit.test('percentpie widget in form view with value > 50%', async function (assert) {
+ assert.expect(12);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field" widget="percentpie"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 3,
+ });
+
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full',
+ "right mask should be hidden since the value > 50%");
+
+ // switch to edit mode and check the result
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full',
+ "right mask should be hidden since the value > 50%");
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_percent_pie.o_field_widget .o_pie',
+ "should have a pie chart");
+ assert.strictEqual(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_pie_value').text(),
+ '80%', "should have 80% as pie value since int_field=80");
+ assert.ok(_.str.include(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').first().attr('style'),
+ 'transform: rotate(288deg);'), "left mask should be rotated from 360*(80/100) = 288 degrees");
+ assert.hasClass(form.$('.o_field_percent_pie.o_field_widget .o_pie .o_mask').last(),'o_full',
+ "right mask should be hidden since the value > 50%");
+
+ form.destroy();
+ });
+
+ // TODO: This test would pass without any issue since all the classes and
+ // custom style attributes are correctly set on the widget in list
+ // view, but since the scss itself for this widget currently only
+ // applies inside the form view, the widget is unusable. This test can
+ // be uncommented when we refactor the scss files so that this widget
+ // stylesheet applies in both form and list view.
+ // QUnit.test('percentpie widget in editable list view', async function(assert) {
+ // assert.expect(10);
+ //
+ // var list = await createView({
+ // View: ListView,
+ // model: 'partner',
+ // data: this.data,
+ // arch: '<tree editable="bottom">' +
+ // '<field name="foo"/>' +
+ // '<field name="int_field" widget="percentpie"/>' +
+ // '</tree>',
+ // });
+ //
+ // assert.containsN(list, '.o_field_percent_pie .o_pie', 5,
+ // "should have five pie charts");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole left side of the pie");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // // switch to edit mode and check the result
+// testUtils.dom.click( list.$('tbody td:not(.o_list_record_selector)').first());
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // // save
+// testUtils.dom.click( list.$buttons.find('.o_list_button_save'));
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_pie_value').first().text(),
+ // '10%', "should have 10% as pie value since int_field=10");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').first().attr('style'),
+ // 'transform: rotate(180deg);', "left mask should be covering the whole right side of the pie");
+ // assert.strictEqual(list.$('.o_field_percent_pie:first .o_pie .o_mask').last().attr('style'),
+ // 'transform: rotate(36deg);', "right mask should be rotated from 360*(10/100) = 36 degrees");
+ //
+ // list.destroy();
+ // });
+
+
+ QUnit.module('FieldDomain');
+
+ QUnit.test('The domain editor should not crash the view when given a dynamic filter', async function (assert) {
+ //dynamic filters (containing variables, such as uid, parent or today)
+ //are not handled by the domain editor, but it shouldn't crash the view
+ assert.expect(1);
+
+ this.data.partner.records[0].foo = '[["int_field", "=", uid]]';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' +
+ '<field name="int_field" invisible="1"/>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ user_context: {uid: 14},
+ },
+ });
+
+ assert.strictEqual(form.$('.o_read_mode').text(), "This domain is not supported.",
+ "The widget should not crash the view, but gracefully admit its failure.");
+ form.destroy();
+ });
+
+ QUnit.test('basic domain field usage is ok', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].foo = "[]";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ // As the domain is empty, there should be a button to add the first
+ // domain part
+ var $domain = form.$(".o_field_domain");
+ var $domainAddFirstNodeButton = $domain.find(".o_domain_add_first_node_button");
+ assert.equal($domainAddFirstNodeButton.length, 1,
+ "there should be a button to create first domain element");
+
+ // Clicking on the button should add the [["id", "=", "1"]] domain, so
+ // there should be a field selector in the DOM
+ await testUtils.dom.click($domainAddFirstNodeButton);
+ var $fieldSelector = $domain.find(".o_field_selector");
+ assert.equal($fieldSelector.length, 1,
+ "there should be a field selector");
+
+ // Focusing the field selector input should open the field selector
+ // popover
+ await testUtils.dom.triggerEvents($fieldSelector, 'focus');
+ var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ assert.containsOnce($fieldSelectorPopover, '.o_field_selector_search input',
+ "field selector popover should contain a search input");
+
+ // The popover should contain the list of partner_type fields and so
+ // there should be the "Color index" field
+ var $lis = $fieldSelectorPopover.find("li");
+ var $colorIndex = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Color index") >= 0) {
+ $colorIndex = $li;
+ }
+ });
+ assert.equal($colorIndex.length, 1,
+ "field selector popover should contain 'Color index' field");
+
+ // Clicking on this field should close the popover, then changing the
+ // associated value should reveal one matched record
+ await testUtils.dom.click($colorIndex);
+ await testUtils.fields.editAndTrigger($('.o_domain_leaf_value_input'), 2, ['change']);
+ assert.equal($domain.find(".o_domain_show_selection_button").text().trim().substr(0, 2), "1 ",
+ "changing color value to 2 should reveal only one record");
+
+ // Saving the form view should show a readonly domain containing the
+ // "color" field
+ await testUtils.form.clickSave(form);
+ $domain = form.$(".o_field_domain");
+ assert.ok($domain.html().indexOf("Color index") >= 0,
+ "field selector readonly value should now contain 'Color index'");
+ form.destroy();
+ });
+
+ QUnit.test('domain field is correctly reset on every view change', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].foo = '[["id","=",1]]';
+ this.data.partner.fields.bar.type = "char";
+ this.data.partner.records[0].bar = "product";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="bar"/>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ // As the domain is equal to [["id", "=", 1]] there should be a field
+ // selector to change this
+ var $domain = form.$(".o_field_domain");
+ var $fieldSelector = $domain.find(".o_field_selector");
+ assert.equal($fieldSelector.length, 1,
+ "there should be a field selector");
+
+ // Focusing its input should open the field selector popover
+ await testUtils.dom.triggerEvents($fieldSelector, 'focus');
+ var $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // As the value of the "bar" field is "product", the field selector
+ // popover should contain the list of "product" fields
+ var $lis = $fieldSelectorPopover.find("li");
+ var $sampleLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Product Name") >= 0) {
+ $sampleLi = $li;
+ }
+ });
+ assert.strictEqual($lis.length, 1,
+ "field selector popover should contain only one field");
+ assert.strictEqual($sampleLi.length, 1,
+ "field selector popover should contain 'Product Name' field");
+
+ // Now change the value of the "bar" field to "partner_type"
+ await testUtils.dom.click(form.$("input.o_field_widget"));
+ await testUtils.fields.editInput(form.$("input.o_field_widget"), "partner_type");
+
+ // Refocusing the field selector input should open the popover again
+ $fieldSelector = form.$(".o_field_selector");
+ $fieldSelector.trigger('focusin');
+ $fieldSelectorPopover = $fieldSelector.find(".o_field_selector_popover");
+ assert.ok($fieldSelectorPopover.is(":visible"),
+ "field selector popover should be visible");
+
+ // Now the list of fields should be the ones of the "partner_type" model
+ $lis = $fieldSelectorPopover.find("li");
+ $sampleLi = $();
+ $lis.each(function () {
+ var $li = $(this);
+ if ($li.html().indexOf("Color index") >= 0) {
+ $sampleLi = $li;
+ }
+ });
+ assert.strictEqual($lis.length, 2,
+ "field selector popover should contain two fields");
+ assert.strictEqual($sampleLi.length, 1,
+ "field selector popover should contain 'Color index' field");
+ form.destroy();
+ });
+
+ QUnit.test('domain field can be reset with a new domain (from onchange)', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].foo = '[]';
+ this.data.partner.onchanges = {
+ display_name: function (obj) {
+ obj.foo = '[["id", "=", 1]]';
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="display_name"/>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '5 record(s)',
+ "the domain being empty, there should be 5 records");
+
+ // update display_name to trigger the onchange and reset foo
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'new value');
+
+ assert.equal(form.$('.o_domain_show_selection_button').text().trim(), '1 record(s)',
+ "the domain has changed, there should be only 1 record");
+
+ form.destroy();
+ });
+
+ QUnit.test('domain field: handle false domain as []', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].foo = false;
+ this.data.partner.fields.bar.type = "char";
+ this.data.partner.records[0].bar = "product";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="bar"/>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'bar\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_count') {
+ assert.deepEqual(args.args[0], [], "should send a valid domain");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=foo]:not(.o_field_empty)').length, 1,
+ "there should be a domain field, not considered empty");
+
+ await testUtils.form.clickEdit(form);
+
+ var $warning = form.$('.o_field_widget[name=foo] .text-warning');
+ assert.strictEqual($warning.length, 0, "should not display that the domain is invalid");
+
+ form.destroy();
+ });
+
+ QUnit.test('basic domain field: show the selection', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].foo = "[]";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo" widget="domain" options="{\'model\': \'partner_type\'}"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="name" string="Name"/></search>',
+ },
+ res_id: 1,
+ });
+
+ assert.equal(form.$(".o_domain_show_selection_button").text().trim().substr(0, 2), "2 ",
+ "selection should contain 2 records");
+
+ // open the selection
+ await testUtils.dom.click(form.$(".o_domain_show_selection_button"));
+ assert.strictEqual($('.modal .o_list_view .o_data_row').length, 2,
+ "should have open a list view with 2 records in a dialog");
+
+ // click on a record -> should not open the record
+ // we don't actually check that it doesn't open the record because even
+ // if it tries to, it will crash as we don't define an arch in this test
+ await testUtils.dom.click($('.modal .o_list_view .o_data_row:first .o_data_cell'));
+
+ form.destroy();
+ });
+
+ QUnit.test('field context is propagated when opening selection', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].foo = "[]";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="foo" widget="domain" options="{'model': 'partner_type'}" context="{'tree_view_ref': 3}"/>
+ </form>
+ `,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,3,list': '<tree><field name="id"/></tree>',
+ 'partner_type,false,search': '<search><field name="name" string="Name"/></search>',
+ },
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$(".o_domain_show_selection_button"));
+
+ assert.strictEqual($('.modal .o_data_row').text(), '1214',
+ "should have picked the correct list view");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldProgressBar');
+
+ QUnit.test('Field ProgressBar: max_value should update', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records = this.data.partner.records.slice(0,1);
+ this.data.partner.records[0].qux = 2;
+
+ this.data.partner.onchanges = {
+ display_name: function (obj) {
+ obj.int_field = 999;
+ obj.qux = 5;
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="display_name" />' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'current_value\': \'int_field\', \'max_value\': \'qux\'}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(
+ args.args[1],
+ {int_field: 999, qux: 5, display_name: 'new name'},
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '10 / 2',
+ 'The initial value of the progress bar should be correct');
+
+ // trigger the onchange
+ await testUtils.fields.editInput(form.$('.o_input[name=display_name]'), 'new name');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '999 / 5',
+ 'The value of the progress bar should be correct after the update');
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should not update in readonly mode when sliding the bar', async function (assert) {
+ assert.expect(4);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct')
+
+ var $progressBarEl = form.$('.o_progress');
+ var top = $progressBarEl.offset().top + 5;
+ var left = $progressBarEl.offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "click");
+ } catch (e) {
+ form.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'New value should be different than initial after click');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ $view.remove();
+ });
+
+ QUnit.test('Field ProgressBar: value should not update in edit mode when sliding the bar', async function (assert) {
+ assert.expect(6);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+ var $view = $('#qunit-fixture').contents();
+ $view.prependTo('body'); // => select with click position
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct')
+
+ var $progressBarEl = form.$('.o_progress');
+ var top = $progressBarEl.offset().top + 5;
+ var left = $progressBarEl.offset().left + 5;
+ try {
+ testUtils.triggerPositionalMouseEvent(left, top, "click");
+ } catch (e) {
+ form.destroy();
+ $view.remove();
+ throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
+ }
+ assert.strictEqual(form.$('.o_progressbar_value.o_input').val(), "99",
+ 'Value of input is not changed');
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'New value should be different than initial after click');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ $view.remove();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in edit mode when typing in input', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69%',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in edit mode when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\'}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69 / 0',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: max value should update in edit mode when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].qux, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: Standard readonly mode is readonly', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ assert.containsNone(form, '.o_progressbar_value.o_input', 'no input in readonly mode');
+
+ assert.verifySteps(["/web/dataset/call_kw/partner/read"]);
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: max value should update in readonly mode with right parameter when typing in input with field max value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'max_value\': \'qux\', \'edit_max_value\': true, \'editable_readonly\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].qux, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 0',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "0.44444", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69', ['input', 'blur']);
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99 / 69',
+ 'New value should be different than initial after changing it');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: value should update in readonly mode with right parameter when typing in input with field value', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true, \'editable_readonly\': true}" />' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 69,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_readonly'), 'Form in readonly mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), "99", 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '69.6', ['input', 'blur']);
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '69%',
+ 'New value should be different than initial after changing it');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: write float instead of int works, in locale', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ translateParameters: {
+ thousands_sep: "#",
+ decimal_point: ":",
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].int_field, 1037,
+ 'New value of progress bar saved');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, '1#037:9', ['input', 'blur']);
+
+ await testUtilsDom.click(form.$buttons.find('.o_form_button_save'));
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '1k%',
+ 'New value should be different than initial after click');
+
+ form.destroy();
+ });
+
+ QUnit.test('Field ProgressBar: write gibbrish instead of int throws warning', async function (assert) {
+ assert.expect(5);
+ this.data.partner.records[0].int_field = 99;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field" widget="progressbar" options="{\'editable\': true}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ interceptsPropagate: {
+ call_service: function (ev) {
+ if (ev.data.service === 'notification') {
+ assert.strictEqual(ev.data.method, 'notify');
+ assert.strictEqual(
+ ev.data.args[0].message,
+ "Please enter a numerical value"
+ );
+ }
+ }
+ },
+ });
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'), 'Form in edit mode');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '99%',
+ 'Initial value should be correct');
+
+ await testUtilsDom.click(form.$('.o_progress'));
+
+ var $valInput = form.$('.o_progressbar_value.o_input');
+ assert.strictEqual($valInput.val(), '99', 'Initial value in input is correct');
+
+ await testUtils.fields.editAndTrigger($valInput, 'trente sept virgule neuf', ['input']);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldColor', {
+ before: function () {
+ return ajax.loadXML('/web/static/src/xml/colorpicker.xml', core.qweb);
+ },
+ });
+
+ QUnit.test('Field Color: default widget state', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="hex_color" widget="color" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_color'));
+ assert.containsOnce($, '.modal');
+ assert.containsNone($('.modal'), '.o_opacity_slider',
+ "Opacity slider should not be present");
+ assert.containsNone($('.modal'), '.o_opacity_input',
+ "Opacity input should not be present");
+
+ await testUtils.dom.click($('.modal .btn:contains("Discard")'));
+
+ assert.strictEqual(document.activeElement, form.$('.o_field_color')[0],
+ "Focus should go back to the color field");
+
+ form.destroy();
+ });
+
+ QUnit.test('Field Color: behaviour in different views', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [4, 2];
+ this.data.partner.records[1].hex_color = '#ff0080';
+
+ const form = await createView({
+ arch: '<form>' +
+ '<field name="hex_color" widget="color"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '<field name="hex_color" widget="color"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ data: this.data,
+ model: 'partner',
+ res_id: 1,
+ View: FormView,
+ });
+
+ await testUtils.dom.click(form.$('.o_field_color:first()'));
+ assert.containsNone($(document.body), '.modal',
+ "Color field in readonly shouldn't be editable");
+
+ const rowInitialHeight = form.$('.o_data_row:first()').height();
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_row:first() .o_data_cell:first()'));
+
+ assert.strictEqual(rowInitialHeight, form.$('.o_data_row:first()').height(),
+ "Color field shouldn't change the color height when edited");
+
+ form.destroy();
+ });
+
+ QUnit.test('Field Color: pick and reset colors', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="hex_color" widget="color" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(255, 0, 0)',
+ "Background of the color field should be initially red");
+
+ await testUtils.dom.click(form.$('.o_field_color'));
+ await testUtils.fields.editAndTrigger($('.modal .o_hex_input'), '#00ff00', ['change']);
+ await testUtils.dom.click($('.modal .btn:contains("Choose")'));
+
+ assert.strictEqual($('.o_field_color').css('backgroundColor'), 'rgb(0, 255, 0)',
+ "Background of the color field should be updated to green");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldColorPicker');
+
+ QUnit.test('FieldColorPicker: can navigate away with TAB', async function (assert) {
+ assert.expect(1);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form string="Partners">
+ <field name="int_field" widget="color_picker"/>
+ <field name="foo" />
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$el.find('a.oe_kanban_color_1')[0].focus();
+
+ form.$el.find('a.oe_kanban_color_1').trigger($.Event('keydown', {
+ which: $.ui.keyCode.TAB,
+ keyCode: $.ui.keyCode.TAB,
+ }));
+ assert.strictEqual(document.activeElement, form.$el.find('input[name="foo"]')[0],
+ "foo field should be focused");
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldBadge');
+
+ QUnit.test('FieldBadge component on a char field in list view', async function (assert) {
+ assert.expect(3);
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: `<list><field name="display_name" widget="badge"/></list>`,
+ });
+
+ assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(first record)');
+ assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(second record)');
+ assert.containsOnce(list, '.o_field_badge[name="display_name"]:contains(aaa)');
+
+ list.destroy();
+ });
+
+ QUnit.test('FieldBadge component on a selection field in list view', async function (assert) {
+ assert.expect(3);
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: `<list><field name="selection" widget="badge"/></list>`,
+ });
+
+ assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Blocked)');
+ assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Normal)');
+ assert.containsOnce(list, '.o_field_badge[name="selection"]:contains(Done)');
+
+ list.destroy();
+ });
+
+ QUnit.test('FieldBadge component on a many2one field in list view', async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: `<list><field name="trululu" widget="badge"/></list>`,
+ });
+
+ assert.containsOnce(list, '.o_field_badge[name="trululu"]:contains(first record)');
+ assert.containsOnce(list, '.o_field_badge[name="trululu"]:contains(aaa)');
+
+ list.destroy();
+ });
+
+ QUnit.test('FieldBadge component with decoration-xxx attributes', async function (assert) {
+ assert.expect(6);
+
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <list>
+ <field name="selection"/>
+ <field name="foo" widget="badge" decoration-danger="selection == 'done'" decoration-warning="selection == 'blocked'"/>
+ </list>`,
+ });
+
+ assert.containsN(list, '.o_field_badge[name="foo"]', 5);
+ assert.containsOnce(list, '.o_field_badge[name="foo"].bg-danger-light');
+ assert.containsOnce(list, '.o_field_badge[name="foo"].bg-warning-light');
+
+ await list.reload();
+
+ assert.containsN(list, '.o_field_badge[name="foo"]', 5);
+ assert.containsOnce(list, '.o_field_badge[name="foo"].bg-danger-light');
+ assert.containsOnce(list, '.o_field_badge[name="foo"].bg-warning-light');
+
+ list.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
new file mode 100644
index 00000000..08c52817
--- /dev/null
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -0,0 +1,437 @@
+odoo.define('web.field_utils_tests', function (require) {
+"use strict";
+
+var core = require('web.core');
+var session = require('web.session');
+var fieldUtils = require('web.field_utils');
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('field_utils');
+
+QUnit.test('format integer', function(assert) {
+ assert.expect(5);
+
+ var originalGrouping = core._t.database.parameters.grouping;
+
+ core._t.database.parameters.grouping = [3, 3, 3, 3];
+ assert.strictEqual(fieldUtils.format.integer(1000000), '1,000,000');
+
+ core._t.database.parameters.grouping = [3, 2, -1];
+ assert.strictEqual(fieldUtils.format.integer(106500), '1,06,500');
+
+ core._t.database.parameters.grouping = [1, 2, -1];
+ assert.strictEqual(fieldUtils.format.integer(106500), '106,50,0');
+
+ assert.strictEqual(fieldUtils.format.integer(0), "0");
+ assert.strictEqual(fieldUtils.format.integer(false), "");
+
+ core._t.database.parameters.grouping = originalGrouping;
+});
+
+QUnit.test('format float', function(assert) {
+ assert.expect(5);
+
+ var originalParameters = $.extend(true, {}, core._t.database.parameters);
+
+ core._t.database.parameters.grouping = [3, 3, 3, 3];
+ assert.strictEqual(fieldUtils.format.float(1000000), '1,000,000.00');
+
+ core._t.database.parameters.grouping = [3, 2, -1];
+ assert.strictEqual(fieldUtils.format.float(106500), '1,06,500.00');
+
+ core._t.database.parameters.grouping = [1, 2, -1];
+ assert.strictEqual(fieldUtils.format.float(106500), '106,50,0.00');
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+ assert.strictEqual(fieldUtils.format.float(6000), '6.000,00');
+ assert.strictEqual(fieldUtils.format.float(false), '');
+
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test("format_datetime", function (assert) {
+ assert.expect(1);
+
+ var date_string = "2009-05-04 12:34:23";
+ var date = fieldUtils.parse.datetime(date_string, {}, {timezone: false});
+ var str = fieldUtils.format.datetime(date, {}, {timezone: false});
+ assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss"));
+});
+
+QUnit.test("format_datetime (with different timezone offset)", function (assert) {
+ assert.expect(2);
+
+ // mock the date format to avoid issues due to localisation
+ var dateFormat = core._t.database.parameters.date_format;
+ core._t.database.parameters.date_format = '%m/%d/%Y';
+ session.getTZOffset = function (date) {
+ // simulate daylight saving time
+ var startDate = new Date(2017, 2, 26);
+ var endDate = new Date(2017, 9, 29);
+ if (startDate < date && date < endDate) {
+ return 120; // UTC+2
+ } else {
+ return 60; // UTC+1
+ }
+ };
+
+ var str = fieldUtils.format.datetime(moment.utc('2017-01-01T10:00:00Z'));
+ assert.strictEqual(str, '01/01/2017 11:00:00');
+ str = fieldUtils.format.datetime(moment.utc('2017-06-01T10:00:00Z'));
+ assert.strictEqual(str, '06/01/2017 12:00:00');
+
+ core._t.database.parameters.date_format = dateFormat;
+});
+
+QUnit.test("format_many2one", function (assert) {
+ assert.expect(2);
+
+ assert.strictEqual('', fieldUtils.format.many2one(null));
+ assert.strictEqual('A M2O value', fieldUtils.format.many2one({
+ data: { display_name: 'A M2O value' },
+ }));
+});
+
+QUnit.test('format monetary', function(assert) {
+ assert.expect(1);
+
+ assert.strictEqual(fieldUtils.format.monetary(false), '');
+});
+
+QUnit.test('format char', function(assert) {
+ assert.expect(1);
+
+ assert.strictEqual(fieldUtils.format.char(), '',
+ "undefined char should be formatted as an empty string");
+});
+
+QUnit.test('format many2many', function(assert) {
+ assert.expect(3);
+
+ assert.strictEqual(fieldUtils.format.many2many({data: []}), 'No records');
+ assert.strictEqual(fieldUtils.format.many2many({data: [1]}), '1 record');
+ assert.strictEqual(fieldUtils.format.many2many({data: [1, 2]}), '2 records');
+});
+
+QUnit.test('format one2many', function(assert) {
+ assert.expect(3);
+
+ assert.strictEqual(fieldUtils.format.one2many({data: []}), 'No records');
+ assert.strictEqual(fieldUtils.format.one2many({data: [1]}), '1 record');
+ assert.strictEqual(fieldUtils.format.one2many({data: [1, 2]}), '2 records');
+});
+
+QUnit.test('format binary', function (assert) {
+ assert.expect(1);
+
+ // base64 estimated size (bytes) = value.length / 1.37 (http://en.wikipedia.org/wiki/Base64#MIME)
+ // Here: 4 / 1.37 = 2.91970800 => 2.92 (rounded 2 decimals by utils.human_size)
+ assert.strictEqual(fieldUtils.format.binary('Cg=='), '2.92 Bytes');
+});
+
+QUnit.test('format percentage', function (assert) {
+ assert.expect(12);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ assert.strictEqual(fieldUtils.format.percentage(0), '0%');
+ assert.strictEqual(fieldUtils.format.percentage(0.5), '50%');
+ assert.strictEqual(fieldUtils.format.percentage(1), '100%');
+
+ assert.strictEqual(fieldUtils.format.percentage(-0.2), '-20%');
+ assert.strictEqual(fieldUtils.format.percentage(2.5), '250%');
+
+ assert.strictEqual(fieldUtils.format.percentage(0.125), '12.5%');
+ assert.strictEqual(fieldUtils.format.percentage(0.666666), '66.67%');
+
+ assert.strictEqual(fieldUtils.format.percentage(false), '0%');
+ assert.strictEqual(fieldUtils.format.percentage(50, null,
+ {humanReadable: function (val) {return true;}}), '5k%'
+ );
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+ assert.strictEqual(fieldUtils.format.percentage(0.125), '12,5%');
+ assert.strictEqual(fieldUtils.format.percentage(0.666666), '66,67%');
+ assert.strictEqual(fieldUtils.format.percentage(0.5, null, { noSymbol: true }), '50');
+
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test('format float time', function (assert) {
+ assert.expect(7);
+
+ assert.strictEqual(fieldUtils.format.float_time(2), '02:00');
+ assert.strictEqual(fieldUtils.format.float_time(3.5), '03:30');
+ assert.strictEqual(fieldUtils.format.float_time(0.25), '00:15');
+
+ assert.strictEqual(fieldUtils.format.float_time(-0.5), '-00:30');
+
+ const options = {
+ noLeadingZeroHour: true,
+ };
+ assert.strictEqual(fieldUtils.format.float_time(2, null, options), '2:00');
+ assert.strictEqual(fieldUtils.format.float_time(3.5, null, options), '3:30');
+ assert.strictEqual(fieldUtils.format.float_time(-0.5, null, options), '-0:30');
+});
+
+QUnit.test('parse float', function(assert) {
+ assert.expect(10);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: '.',
+ thousands_sep: ','
+ });
+
+ assert.strictEqual(fieldUtils.parse.float(""), 0);
+ assert.strictEqual(fieldUtils.parse.float("0"), 0);
+ assert.strictEqual(fieldUtils.parse.float("100.00"), 100);
+ assert.strictEqual(fieldUtils.parse.float("-100.00"), -100);
+ assert.strictEqual(fieldUtils.parse.float("1,000.00"), 1000);
+ assert.strictEqual(fieldUtils.parse.float("1,000,000.00"), 1000000);
+ assert.strictEqual(fieldUtils.parse.float('1,234.567'), 1234.567);
+ assert.throws(function () {
+ fieldUtils.parse.float("1.000.000");
+ }, "Throw an exception if it's not a valid number");
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+
+ assert.strictEqual(fieldUtils.parse.float('1.234,567'), 1234.567);
+ assert.throws(function () {
+ fieldUtils.parse.float("1,000,000");
+ }, "Throw an exception if it's not a valid number");
+
+ _.extend(core._t.database.parameters, originalParameters);
+});
+
+QUnit.test('parse integer', function(assert) {
+ assert.expect(11);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: '.',
+ thousands_sep: ','
+ });
+
+ assert.strictEqual(fieldUtils.parse.integer(""), 0);
+ assert.strictEqual(fieldUtils.parse.integer("0"), 0);
+ assert.strictEqual(fieldUtils.parse.integer("100"), 100);
+ assert.strictEqual(fieldUtils.parse.integer("-100"), -100);
+ assert.strictEqual(fieldUtils.parse.integer("1,000"), 1000);
+ assert.strictEqual(fieldUtils.parse.integer("1,000,000"), 1000000);
+ assert.throws(function () {
+ fieldUtils.parse.integer("1.000.000");
+ }, "Throw an exception if it's not a valid number");
+ assert.throws(function () {
+ fieldUtils.parse.integer("1,234.567");
+ }, "Throw an exception if the number is a float");
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+
+ assert.strictEqual(fieldUtils.parse.integer("1.000.000"), 1000000);
+ assert.throws(function () {
+ fieldUtils.parse.integer("1,000,000");
+ }, "Throw an exception if it's not a valid number");
+ assert.throws(function () {
+ fieldUtils.parse.integer("1.234,567");
+ }, "Throw an exception if the number is a float");
+
+ _.extend(core._t.database.parameters, originalParameters);
+});
+
+QUnit.test('parse monetary', function(assert) {
+ assert.expect(11);
+ var originalCurrencies = session.currencies;
+ session.currencies = {
+ 1: {
+ digits: [69, 2],
+ position: "after",
+ symbol: "€"
+ },
+ 3: {
+ digits: [69, 2],
+ position: "before",
+ symbol: "$"
+ }
+ };
+
+ assert.strictEqual(fieldUtils.parse.monetary(""), 0);
+ assert.strictEqual(fieldUtils.parse.monetary("0"), 0);
+ assert.strictEqual(fieldUtils.parse.monetary("100.00"), 100);
+ assert.strictEqual(fieldUtils.parse.monetary("-100.00"), -100);
+ assert.strictEqual(fieldUtils.parse.monetary("1,000.00"), 1000);
+ assert.strictEqual(fieldUtils.parse.monetary("1,000,000.00"), 1000000);
+ assert.strictEqual(fieldUtils.parse.monetary("$&nbsp;125.00", {}, {currency_id: 3}), 125);
+ assert.strictEqual(fieldUtils.parse.monetary("1,000.00&nbsp;€", {}, {currency_id: 1}), 1000);
+ assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 3})}, /is not a correct/);
+ assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00", {}, {currency_id: 1})}, /is not a correct/);
+ assert.throws(function() {fieldUtils.parse.monetary("$&nbsp;12.00&nbsp;34", {}, {currency_id: 3})}, /is not a correct/);
+
+ session.currencies = originalCurrencies;
+});
+
+QUnit.test('parse percentage', function(assert) {
+ assert.expect(7);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ assert.strictEqual(fieldUtils.parse.percentage(""), 0);
+ assert.strictEqual(fieldUtils.parse.percentage("0"), 0);
+ assert.strictEqual(fieldUtils.parse.percentage("0.5"), 0.005);
+ assert.strictEqual(fieldUtils.parse.percentage("1"), 0.01);
+ assert.strictEqual(fieldUtils.parse.percentage("100"), 1);
+
+ _.extend(core._t.database.parameters, {
+ grouping: [3, 0],
+ decimal_point: ',',
+ thousands_sep: '.'
+ });
+
+ assert.strictEqual(fieldUtils.parse.percentage("1.234,56"), 12.3456);
+ assert.strictEqual(fieldUtils.parse.percentage("6,02"), 0.0602);
+
+ core._t.database.parameters = originalParameters;
+
+});
+
+QUnit.test('parse datetime', function (assert) {
+ assert.expect(7);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+ var originalLocale = moment.locale();
+ var dateStr, date1, date2;
+
+ moment.defineLocale('englishForTest', {
+ dayOfMonthOrdinalParse: /\d{1,2}(st|nd|rd|th)/,
+ ordinal: function (number) {
+ var b = number % 10,
+ output = (~~(number % 100 / 10) === 1) ? 'th' :
+ (b === 1) ? 'st' :
+ (b === 2) ? 'nd' :
+ (b === 3) ? 'rd' : 'th';
+ return number + output;
+ },
+ });
+
+ moment.defineLocale('norvegianForTest', {
+ monthsShort: 'jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.'.split('_'),
+ monthsParseExact: true,
+ dayOfMonthOrdinalParse: /\d{1,2}\./,
+ ordinal: '%d.',
+ });
+
+ moment.locale('englishForTest');
+ _.extend(core._t.database.parameters, {date_format: '%m/%d/%Y', time_format: '%H:%M:%S'});
+ assert.throws(function () {
+ fieldUtils.parse.datetime("13/01/2019 12:00:00", {}, {});
+ }, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.throws(function () {
+ fieldUtils.parse.datetime("10000-01-01 12:00:00", {}, {});
+ }, /is not a correct/, "Dates after 9999 should be invalid");
+ assert.throws(function () {
+ fieldUtils.parse.datetime("999-01-01 12:00:00", {}, {});
+ }, /is not a correct/, "Dates before 1000 should be invalid");
+
+ dateStr = '01/13/2019 10:05:45';
+ date1 = fieldUtils.parse.datetime(dateStr);
+ date2 = moment.utc(dateStr, ['MM/DD/YYYY HH:mm:ss'], true);
+ assert.equal(date1.format(), date2.format(), "Date with leading 0");
+
+ dateStr = '1/14/2019 10:5:45';
+ date1 = fieldUtils.parse.datetime(dateStr);
+ date2 = moment.utc(dateStr, ['M/D/YYYY H:m:s'], true);
+ assert.equal(date1.format(), date2.format(), "Date without leading 0");
+
+ dateStr = '01/01/1000 10:15:45';
+ date1 = fieldUtils.parse.datetime(dateStr);
+ date2 = moment.utc(dateStr, ['MM/DD/YYYY HH:mm:ss'], true);
+ assert.equal(date1.format(), date2.format(), "can parse dates of year 1");
+
+ moment.locale('norvegianForTest');
+ _.extend(core._t.database.parameters, {date_format: '%d. %b %Y', time_format: '%H:%M:%S'});
+ dateStr = '16. jan. 2019 10:05:45';
+ date1 = fieldUtils.parse.datetime(dateStr);
+ date2 = moment.utc(dateStr, ['DD. MMM YYYY HH:mm:ss'], true);
+ assert.equal(date1.format(), date2.format(), "Day/month inverted + month i18n");
+
+ moment.locale(originalLocale);
+ moment.updateLocale("englishForTest", null);
+ moment.updateLocale("norvegianForTest", null);
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test('parse date without separator', function (assert) {
+ assert.expect(8);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {date_format: '%d.%m/%Y'});
+ var dateFormat = "DD.MM/YYYY";
+
+ assert.throws(function () {fieldUtils.parse.date("1197")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.throws(function () {fieldUtils.parse.date("0131")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.throws(function () {fieldUtils.parse.date("970131")}, /is not a correct/, "Wrongly formated dates should be invalid");
+ assert.equal(fieldUtils.parse.date("3101").format(dateFormat), "31.01/" + moment.utc().year());
+ assert.equal(fieldUtils.parse.date("31.01").format(dateFormat), "31.01/" + moment.utc().year());
+ assert.equal(fieldUtils.parse.date("310197").format(dateFormat), "31.01/1997");
+ assert.equal(fieldUtils.parse.date("310117").format(dateFormat), "31.01/2017");
+ assert.equal(fieldUtils.parse.date("31011985").format(dateFormat), "31.01/1985");
+
+ core._t.database.parameters = originalParameters;
+});
+
+QUnit.test('parse datetime without separator', function (assert) {
+ assert.expect(3);
+
+ var originalParameters = _.clone(core._t.database.parameters);
+
+ _.extend(core._t.database.parameters, {date_format: '%d.%m/%Y', time_format: '%H:%M/%S'});
+ var dateTimeFormat = "DD.MM/YYYY HH:mm/ss";
+
+ assert.equal(fieldUtils.parse.datetime("3101198508").format(dateTimeFormat), "31.01/1985 08:00/00");
+ assert.equal(fieldUtils.parse.datetime("310119850833").format(dateTimeFormat), "31.01/1985 08:33/00");
+ assert.equal(fieldUtils.parse.datetime("31/01/1985 08").format(dateTimeFormat), "31.01/1985 08:00/00");
+
+ core._t.database.parameters = originalParameters;
+});
+});
+
+QUnit.test('parse smart date input', function (assert) {
+ assert.expect(10);
+
+ const format = "DD MM YYYY";
+ assert.strictEqual(fieldUtils.parse.date("+1d").format(format), moment().add(1, 'days').format(format));
+ assert.strictEqual(fieldUtils.parse.datetime("+2w").format(format), moment().add(2, 'weeks').format(format));
+ assert.strictEqual(fieldUtils.parse.date("+3m").format(format), moment().add(3, 'months').format(format));
+ assert.strictEqual(fieldUtils.parse.datetime("+4y").format(format), moment().add(4, 'years').format(format));
+
+ assert.strictEqual(fieldUtils.parse.date("+5").format(format), moment().add(5, 'days').format(format));
+ assert.strictEqual(fieldUtils.parse.datetime("-5").format(format), moment().subtract(5, 'days').format(format));
+
+ assert.strictEqual(fieldUtils.parse.date("-4y").format(format), moment().subtract(4, 'years').format(format));
+ assert.strictEqual(fieldUtils.parse.datetime("-3m").format(format), moment().subtract(3, 'months').format(format));
+ assert.strictEqual(fieldUtils.parse.date("-2w").format(format), moment().subtract(2, 'weeks').format(format));
+ assert.strictEqual(fieldUtils.parse.datetime("-1d").format(format), moment().subtract(1, 'days').format(format));
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
new file mode 100644
index 00000000..bececc25
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields/field_many2many_tests.js
@@ -0,0 +1,1809 @@
+odoo.define('web.field_many_to_many_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+const cpHelpers = testUtils.controlPanel;
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+ QUnit.module('relational_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: { string: "Foo", type: "char", default: "My little Foo Value" },
+ int_field: { string: "int_field", type: "integer", sortable: true },
+ turtles: { string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu' },
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type' },
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ string: "Color",
+ },
+ user_id: { string: "User", type: 'many2one', relation: 'user' },
+ reference: {
+ string: "Reference Field", type: 'reference', selection: [
+ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ foo: "yop",
+ int_field: 10,
+ turtles: [2],
+ timmy: [],
+ user_id: 17,
+ reference: 'product,37',
+ }, {
+ id: 2,
+ display_name: "second record",
+ foo: "blip",
+ int_field: 9,
+ timmy: [],
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: { string: "Product Name", type: "char" }
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: { string: "Partner Type", type: "char" },
+ color: { string: "Color index", type: "integer" },
+ },
+ records: [
+ { id: 12, display_name: "gold", color: 2 },
+ { id: 14, display_name: "silver", color: 5 },
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: { string: "Foo", type: "char" },
+ turtle_bar: { string: "Bar", type: "boolean", default: true },
+ partner_ids: { string: "Partner", type: "many2many", relation: 'partner' },
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_foo: "blip",
+ partner_ids: [2, 4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ turtle_foo: "kawa",
+ partner_ids: [],
+ }],
+ onchanges: {},
+ },
+ user: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ },
+ }, function () {
+ QUnit.module('FieldMany2Many');
+
+ QUnit.test('many2many kanban: edition', async function (assert) {
+ assert.expect(33);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "red", color: 6 });
+ this.data.partner_type.records.push({ id: 18, display_name: "yellow", color: 4 });
+ this.data.partner_type.records.push({ id: 21, display_name: "blue", color: 1 });
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,form': '<form string="Types"><field name="display_name"/></form>',
+ 'partner_type,false,list': '<tree string="Types"><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search string="Types">' +
+ '<field name="name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner_type/write') {
+ assert.strictEqual(args.args[1].display_name, "new name", "should write 'new_name'");
+ }
+ if (route === '/web/dataset/call_kw/partner_type/create') {
+ assert.strictEqual(args.args[0].display_name, "A new type", "should create 'A new type'");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ // get the created type's id
+ var createdType = _.findWhere(this.data.partner_type.records, {
+ display_name: "A new type"
+ });
+ var ids = _.sortBy([12, 15, 18].concat(createdType.id), _.identity.bind(_));
+ assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), ids),
+ "new value should be " + ids);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // the SelectCreateDialog requests the session, so intercept its custom
+ // event to specify a fake session to prevent it from crashing
+ testUtils.mock.intercept(form, 'get_session', function (event) {
+ event.data.callback({ user_context: {} });
+ });
+
+ assert.ok(!form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_many2many .o-kanban-button-new').length,
+ '"Add" button should not be visible in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'gold',
+ 'display_name of subrecord should be the one in DB');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_field_many2many .o-kanban-button-new').length,
+ '"Add" button should be visible in edit');
+ assert.strictEqual(form.$('.o_field_many2many .o-kanban-button-new').text().trim(), "Add",
+ 'Create button should have "Add" label');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.oe_kanban_global_click:first()'));
+
+ await testUtils.fields.editInput($('.modal .o_form_view input'), 'new name');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_kanban_record:first() span').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add subrecords
+ // -> single select
+ await testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ assert.ok($('.modal .o_list_view').length, "should have opened a list view in a modal");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 3,
+ "list view should contain 3 records");
+ await testUtils.dom.click($('.modal .o_list_view tbody tr:contains(red)'));
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3,
+ 'kanban should now contain 3 records');
+ assert.ok(form.$('.o_kanban_record:contains(red)').length,
+ 'record "red" should be in the kanban');
+
+ // -> multiple select
+ await testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ assert.ok($('.modal .o_select_button').prop('disabled'), "select button should be disabled");
+ assert.strictEqual($('.modal .o_list_view tbody .o_list_record_selector').length, 2,
+ "list view should contain 2 records");
+ await testUtils.dom.click($('.modal .o_list_view thead .o_list_record_selector input'));
+ await testUtils.dom.click($('.modal .o_select_button'));
+ assert.ok(!$('.modal .o_select_button').prop('disabled'), "select button should be enabled");
+ assert.ok(!$('.modal .o_list_view').length, "should have closed the modal");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'kanban should now contain 5 records');
+ // -> created record
+ await testUtils.dom.click(form.$('.o_field_many2many .o-kanban-button-new'));
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)'));
+ assert.ok($('.modal .o_form_view.o_form_editable').length,
+ "should have opened a form view in edit mode, in a modal");
+ await testUtils.fields.editInput($('.modal .o_form_view input'), 'A new type');
+ await testUtils.dom.click($('.modal:nth(1) footer .btn-primary:first()'));
+ assert.ok(!$('.modal').length, "should have closed both modals");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 6,
+ 'kanban should now contain 6 records');
+ assert.ok(form.$('.o_kanban_record:contains(A new type)').length,
+ 'the newly created type should be in the kanban');
+
+ // delete subrecords
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1,
+ 'There should be a modal having Remove Button');
+ await testUtils.dom.click($('.modal .modal-footer .o_btn_remove'));
+ assert.containsNone($('.o_modal'), "modal should have been closed");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 5,
+ 'should contain 5 records');
+ assert.ok(!form.$('.o_kanban_record:contains(silver)').length,
+ 'the removed record should not be in kanban anymore');
+
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(blue) .delete_icon'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ 'should contain 4 records');
+ assert.ok(!form.$('.o_kanban_record:contains(blue)').length,
+ 'the removed record should not be in kanban anymore');
+
+ // save the record
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban(editable): properly handle create_text node option', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" options="{\'create_text\': \'Add timmy\'}" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_details">' +
+ '<field name="display_name"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_many2many[name="timmy"] .o-kanban-button-new').text().trim(),
+ "Add timmy", "In M2M Kanban, Add button should have 'Add timmy' label");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban: create action disabled', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<kanban create="0">' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ res_id: 1,
+ session: { user_context: {} },
+ });
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.ok(form.$('.o-kanban-button-new').length,
+ '"Add" button should be available in edit');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ assert.strictEqual($('.modal .modal-footer .btn-primary').length, 1, // only button 'Select'
+ '"Create" button should not be available in the modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many kanban: conditional create/delete actions', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
+ <kanban>
+ <field name="display_name"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click">
+ <span><t t-esc="record.display_name.value"/></span>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,form': '<form><field name="name"/></form>',
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red
+ assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available');
+
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove',
+ 'remove button should be visible in modal');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+ assert.containsOnce(form, '.o-kanban-button-new',
+ '"Add" button should still be available even after color field changed');
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ // only select and cancel button should be available, create
+ // button should be removed based on color field condition
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ '"Create" button should not be available in the modal after color field changed');
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ await testUtils.dom.click(form.$('.o_kanban_record:contains(silver)'));
+ assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove',
+ 'remove button should be visible in modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (non editable): edition', async function (assert) {
+ assert.expect(29);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
+ this.data.partner_type.fields.float_field = { string: 'Float', type: 'float' };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')));
+ }
+ if (args.method === 'write' && args.model === 'partner') {
+ assert.deepEqual(args.args[1].timmy, [
+ [6, false, [12, 15]],
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.containsNone(form.$('.o_list_record_remove'),
+ 'delete icon should not be visible in readonly');
+ assert.containsNone(form.$('.o_field_x2many_list_row_add'),
+ '"Add an item" should not be visible in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_remove').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should be visible in edit');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.o_list_view tbody tr:first()'));
+
+ assert.containsNone($('.modal .modal-footer .o_btn_remove'),
+ 'there should not be a "Remove" button in the modal footer');
+
+ await testUtils.fields.editInput($('.modal .o_form_view input'), 'new name');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // add new subrecords
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsNone($('.modal .modal-footer .o_btn_remove'),
+ 'there should not be a "Remove" button in the modal footer');
+ assert.strictEqual($('.modal .o_list_view').length, 1,
+ "a modal should be open");
+ assert.strictEqual($('.modal .o_list_view .o_data_row').length, 1,
+ "the list should contain one row");
+ await testUtils.dom.click($('.modal .o_list_view .o_data_row'));
+ assert.strictEqual($('.modal .o_list_view').length, 0,
+ "the modal should be closed");
+ assert.containsN(form, '.o_list_view td.o_list_number', 3,
+ 'should contain 3 subrecords');
+
+ // remove subrecords
+ await testUtils.dom.click(form.$('.o_list_record_remove:nth(1)'));
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(), 'new name',
+ 'the updated row still has the correct values');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ assert.verifySteps([
+ 'read', // main record
+ 'read', // relational field
+ 'read', // relational record in dialog
+ 'write', // save relational record from dialog
+ 'read', // relational field (updated)
+ 'search_read', // list view in dialog
+ 'read', // relational field (updated)
+ 'write', // save main record
+ 'read', // main record
+ 'read', // relational field
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list (editable): edition', async function (assert) {
+ assert.expect(31);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({ id: 15, display_name: "bronze", color: 6 });
+ this.data.partner_type.fields.float_field = { string: 'Float', type: 'float' };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/><field name="float_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')));
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].timmy, [
+ [6, false, [12, 15]],
+ [1, 12, { display_name: 'new name' }],
+ ]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_list_record_remove').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'gold',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_remove').length,
+ 'delete icon should be visible in edit');
+ assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-times',
+ "should have X icons to remove (unlink) records");
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not visible in edit');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.o_list_view tbody td:first()'));
+ assert.ok(!$('.modal').length,
+ 'in edit, clicking on a subrecord should not open a dialog');
+ assert.hasClass(form.$('.o_list_view tbody tr:first()'),'o_selected_row',
+ 'first row should be in edition');
+ await testUtils.fields.editInput(form.$('.o_list_view input:first()'), 'new name');
+ assert.hasClass(form.$('.o_list_view .o_data_row:first'),'o_selected_row',
+ 'first row should still be in edition');
+ assert.strictEqual(form.$('.o_list_view input[name=display_name]').get(0),
+ document.activeElement, 'edited field should still have the focus');
+ await testUtils.dom.click(form.$el);
+ assert.doesNotHaveClass(form.$('.o_list_view tbody tr:first'), 'o_selected_row',
+ 'first row should not be in edition anymore');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+ assert.verifySteps(['read', 'read']);
+
+ // add new subrecords
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_list_view').length, 1,
+ "a modal should be open");
+ assert.strictEqual($('.modal .o_list_view .o_data_row').length, 1,
+ "the list should contain one row");
+ await testUtils.dom.click($('.modal .o_list_view .o_data_row'));
+ assert.strictEqual($('.modal .o_list_view').length, 0,
+ "the modal should be closed");
+ assert.containsN(form, '.o_list_view td.o_list_number', 3,
+ 'should contain 3 subrecords');
+
+ // remove subrecords
+ await testUtils.dom.click(form.$('.o_list_record_remove:nth(1)'));
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 subrecords');
+ assert.strictEqual(form.$('.o_list_view .o_data_row td:first').text(),
+ 'new name', 'the updated row still has the correct values');
+
+ assert.verifySteps([
+ 'search_read', // list view in dialog
+ 'read', // relational field (updated)
+ 'write', // save main record
+ 'read', // main record
+ 'read', // relational field
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many: create & delete attributes', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="true" delete="true">' +
+ '<field name="color"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_field_x2many_list_row_add', "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2, "should have the 'Add an item' link");
+
+ form.destroy();
+
+ form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="false" delete="false">' +
+ '<field name="color"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_field_x2many_list_row_add', "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2, "each record should have the 'Remove Item' link");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: create action disabled', async function (assert) {
+ assert.expect(2);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy">' +
+ '<tree create="0">' +
+ '<field name="name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should not be available in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should be available in edit');
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many list comodel not writable', async function (assert) {
+ /**
+ * Many2Many List should behave as the m2m_tags
+ * that is, the relation can be altered even if the comodel itself is not CRUD-able
+ * This can happen when someone has read access alone on the comodel
+ * and full CRUD on the current model
+ */
+ assert.expect(12);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:`<form string="Partners">
+ <field name="timmy" widget="many2many" can_create="false" can_write="false"/>
+ </form>`,
+ archs:{
+ 'partner_type,false,list': `<tree create="false" delete="false" edit="false">
+ <field name="display_name"/>
+ </tree>`,
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/create') {
+ assert.deepEqual(args.args[0], {timmy: [[6, false, [12]]]});
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.deepEqual(args.args[1], {timmy: [[6, false, []]]});
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.containsOnce(form, '.o_field_many2many .o_field_x2many_list_row_add');
+ await testUtils.dom.click(form.$('.o_field_many2many .o_field_x2many_list_row_add a'));
+ assert.containsOnce(document.body, '.modal');
+
+ assert.containsN($('.modal-footer'), 'button', 2);
+ assert.containsOnce($('.modal-footer'), 'button.o_select_button');
+ assert.containsOnce($('.modal-footer'), 'button.o_form_button_cancel');
+
+ await testUtils.dom.click($('.modal .o_list_view .o_data_cell:first()'));
+ assert.containsNone(document.body, '.modal');
+
+ assert.containsOnce(form, '.o_field_many2many .o_data_row');
+ assert.equal($('.o_field_many2many .o_data_row').text(), 'gold');
+ assert.containsOnce(form, '.o_field_many2many .o_field_x2many_list_row_add');
+
+ await testUtils.form.clickSave(form);
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_field_many2many .o_data_row .o_list_record_remove');
+ await testUtils.dom.click(form.$('.o_field_many2many .o_data_row .o_list_record_remove'));
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: conditional create/delete actions', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'create': [('color', '=', 'red')], 'delete': [('color', '=', 'red')]}">
+ <tree>
+ <field name="name"/>
+ </tree>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> create and delete actions are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should have two remove icons");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> create and delete actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ // add a line and remove icon should still be there as they don't create/delete records,
+ // but rather add/remove links
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ '"Add a line" button should still be available even after color field changed');
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should still have remove icon even after color field changed");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ '"Create" button should not be available in the modal after color field changed');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link/unlink options (list)', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'link': [('color', '=', 'red')], 'unlink': [('color', '=', 'red')]}">
+ <tree>
+ <field name="name"/>
+ </tree>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> link and unlink actions are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should have two remove icons");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal (Create action is available)');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add a line" should no longer be available after color field changed');
+ assert.containsNone(form, '.o_list_record_remove',
+ "should no longer have remove icon after color field changed");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link/unlink options (list, create="0")', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'link': [('color', '=', 'red')], 'unlink': [('color', '=', 'red')]}">
+ <tree create="0">
+ <field name="name"/>
+ </tree>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> link and unlink actions are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ "should have the 'Add an item' link");
+ assert.containsN(form, '.o_list_record_remove', 2,
+ "should have two remove icons");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ 'there should be 2 buttons available in the modal (Create action is not available)');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add a line" should no longer be available after color field changed');
+ assert.containsNone(form, '.o_list_record_remove',
+ "should no longer have remove icon after color field changed");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link option (kanban)', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'link': [('color', '=', 'red')]}">
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> link and unlink actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button");
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons available in the modal (Create action is available');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" should no longer be available after color field changed');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many field with link option (kanban, create="0")', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="color"/>
+ <field name="timmy" options="{'link': [('color', '=', 'red')]}">
+ <kanban create="0">
+ <templates>
+ <t t-name="kanban-box">
+ <div><field name="name"/></div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </form>`,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="name"/></tree>',
+ 'partner_type,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // color is red -> link and unlink actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', "should have the 'Add' button");
+
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ 'there should be 2 buttons available in the modal (Create action is not available');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set color to black -> link and unlink actions are no longer available
+ await testUtils.fields.editSelect(form.$('select[name="color"]'), '"black"');
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" should no longer be available after color field changed');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list: list of id as default value', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.turtles.default = [2, 3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').text(), "blipkawa",
+ "should have loaded default data");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many checkboxes with default values', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.turtles.default = [3];
+ this.data.partner.fields.turtles.type = "many2many";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" widget="many2many_checkboxes">' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].turtles, [[6, false, [1]]],
+ "correct values should have been sent to create");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.ok(form.$('.o_form_view .custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should be checked");
+
+ await testUtils.dom.click(form.$('.o_form_view .custom-checkbox input:checked'));
+ await testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
+ await testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
+ await testUtils.dom.click(form.$('.o_form_view .custom-checkbox input').first());
+
+ assert.ok(form.$('.o_form_view .custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+ assert.notOk(form.$('.o_form_view .custom-checkbox input').eq(2).prop('checked'),
+ "third checkbox should not be checked");
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list with x2many: add a record', async function (assert) {
+ assert.expect(18);
+
+ this.data.partner_type.fields.m2m = {
+ string: "M2M", type: "many2many", relation: 'turtle',
+ };
+ this.data.partner_type.records[0].m2m = [1, 2];
+ this.data.partner_type.records[1].m2m = [2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="m2m" widget="many2many_tags"/>' +
+ '</tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method !== 'load_views') {
+ assert.step(_.last(route.split('/')) + ' on ' + args.model);
+ }
+ if (args.model === 'turtle') {
+ assert.step(JSON.stringify(args.args[0])); // the read ids
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal .o_data_row:first'));
+
+ assert.containsOnce(form, '.o_data_row',
+ "the record should have been added to the relation");
+ assert.strictEqual(form.$('.o_data_row:first .o_badge_text').text(), 'leonardodonatello',
+ "inner m2m should have been fetched and correctly displayed");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal .o_data_row:first'));
+
+ assert.containsN(form, '.o_data_row', 2,
+ "the second record should have been added to the relation");
+ assert.strictEqual(form.$('.o_data_row:nth(1) .o_badge_text').text(), 'donatelloraphael',
+ "inner m2m should have been fetched and correctly displayed");
+
+ assert.verifySteps([
+ 'read on partner',
+ 'search_read on partner_type',
+ 'read on turtle',
+ '[1,2,3]',
+ 'read on partner_type',
+ 'read on turtle',
+ '[1,2]',
+ 'search_read on partner_type',
+ 'read on turtle',
+ '[2,3]',
+ 'read on partner_type',
+ 'read on turtle',
+ '[2,3]',
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many with a domain', async function (assert) {
+ // The domain specified on the field should not be replaced by the potential
+ // domain the user writes in the dialog, they should rather be concatenated
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" domain="[[\'display_name\', \'=\', \'gold\']]"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ 'partner_type,false,search': '<search>' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "should contain only one row (gold)");
+
+ await cpHelpers.editSearch('.modal', 's');
+ await cpHelpers.validateSearch('.modal');
+
+ assert.strictEqual($('.modal .o_data_row').length, 0, "should contain no row");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list with onchange and edition of a record', async function (assert) {
+ assert.expect(8);
+
+ this.data.partner.fields.turtles.type = "many2many";
+ this.data.partner.onchanges.turtles = function () { };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'turtle,false,form': '<form string="Turtle Power"><field name="turtle_bar"/></form>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('td.o_data_cell:first'));
+
+ await testUtils.dom.click($('.modal-body input[type="checkbox"]'));
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary').first());
+
+ // there is nothing left to save -> should not do a 'write' RPC
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps([
+ 'read', // read initial record (on partner)
+ 'read', // read many2many turtles
+ 'load_views', // load arch of turtles form view
+ 'read', // read missing field when opening record in modal form view
+ 'write', // when saving the modal
+ 'onchange', // onchange should be triggered on partner
+ 'read', // reload many2many
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange with 40+ commands for a many2many', async function (assert) {
+ // this test ensures that the basic_model correctly handles more LINK_TO
+ // commands than the limit of the dataPoint (40 for x2many kanban)
+ assert.expect(24);
+
+ // create a lot of partner_types that will be linked by the onchange
+ var commands = [[5]];
+ for (var i = 0; i < 45; i++) {
+ var id = 100 + i;
+ this.data.partner_type.records.push({ id: id, display_name: "type " + id });
+ commands.push([4, id]);
+ }
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.timmy = commands;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="timmy">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><t t-esc="record.display_name.value"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].timmy[0][0], 6,
+ "should send a command 6");
+ assert.strictEqual(args.args[1].timmy[0][2].length, 45,
+ "should replace with 45 ids");
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.verifySteps(['read']);
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange');
+
+ assert.verifySteps(['onchange', 'read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
+ assert.verifySteps(['read']);
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records displayed on page 2');
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=timmy] .o_pager_next'));
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records displayed on page 1');
+
+ assert.verifySteps(['write', 'read', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('default_get, onchange, onchange on m2m', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.int_field = function (obj) {
+ if (obj.int_field === 2) {
+ assert.deepEqual(obj.timmy, [
+ [6, false, [12]],
+ [1, 12, { display_name: 'gold' }]
+ ]);
+ }
+ obj.timmy = [
+ [5],
+ [1, 12, { display_name: 'gold' }]
+ ];
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags', async function (assert) {
+ assert.expect(1);
+ this.data.turtle.records[0].partner_ids = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch: '<form string="Turtles">' +
+ '<sheet>' +
+ '<field name="display_name"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.deepEqual(
+ form.$('.o_field_many2manytags.o_field_widget .badge .o_badge_text').attr('title'),
+ 'second record', 'the title should be filled in'
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many tags widget: select multiple records', async function (assert) {
+ assert.expect(5);
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner_type.records.push({ id: 100 + i, display_name: "Partner" + i});
+ }
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ });
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickItem('timmy','Search More');
+ assert.ok($('.modal .o_list_view'), "should have open the modal");
+
+ // + 1 for the select all
+ assert.containsN($(document),'.modal .o_list_view .o_list_record_selector input', this.data.partner_type.records.length + 1,
+ "Should have record selector checkboxes to select multiple records");
+ //multiple select tag
+ await testUtils.dom.click($('.modal .o_list_view thead .o_list_record_selector input'));
+ assert.ok(!$('.modal .o_select_button').prop('disabled'), "select button should be enabled");
+ await testUtils.dom.click($('.o_select_button'));
+ assert.containsNone($(document),'.modal .o_list_view', "should have closed the modal");
+ assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge', this.data.partner_type.records.length,
+ "many2many tag should now contain 12 records");
+ form.destroy();
+ });
+
+ QUnit.test("many2many tags widget: select multiple records doesn't show already added tags", async function (assert) {
+ assert.expect(5);
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner_type.records.push({ id: 100 + i, display_name: "Partner" + i});
+ }
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickItem('timmy','Partner1');
+
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickItem('timmy','Search More');
+ assert.ok($('.modal .o_list_view'), "should have open the modal");
+
+ // -1 for the one that is already on the form & +1 for the select all,
+ assert.containsN($(document), '.modal .o_list_view .o_list_record_selector input', this.data.partner_type.records.length - 1 + 1,
+ "Should have record selector checkboxes to select multiple records");
+ //multiple select tag
+ await testUtils.dom.click($('.modal .o_list_view thead .o_list_record_selector input'));
+ assert.ok(!$('.modal .o_select_button').prop('disabled'), "select button should be enabled");
+ await testUtils.dom.click($('.o_select_button'));
+ assert.containsNone($(document),'.modal .o_list_view', "should have closed the modal");
+ assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge', this.data.partner_type.records.length,
+ "many2many tag should now contain 12 records");
+ form.destroy();
+ });
+
+ QUnit.test("many2many tags widget: save&new in edit mode doesn't close edit window", async function (assert) {
+ assert.expect(5);
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner_type.records.push({ id: 100 + i, display_name: "Partner" + i});
+ }
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/></search>',
+ 'partner_type,false,form': '<form><field name="display_name"/></form>'
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.createAndEdit('timmy',"Ralts");
+ assert.containsOnce($(document), '.modal .o_form_view', "should have opened the modal");
+
+ // Create multiple records with save & new
+ await testUtils.fields.editInput($('.modal input:first'), 'Ralts');
+ await testUtils.dom.click($('.modal .btn-primary:nth-child(2)'));
+ assert.containsOnce($(document), '.modal .o_form_view', "modal should still be open");
+ assert.equal($('.modal input:first')[0].value, '', "input should be empty")
+
+ // Create another record and click save & close
+ await testUtils.fields.editInput($('.modal input:first'), 'Pikachu');
+ await testUtils.dom.click($('.modal .btn-primary:first'));
+ assert.containsNone($(document),'.modal .o_list_view', "should have closed the modal");
+ assert.containsN(form, '.o_field_many2manytags[name="timmy"] .badge', 2, "many2many tag should now contain 2 records");
+
+ form.destroy();
+ });
+
+ QUnit.test("many2many tags widget: make tag name input field blank on Save&New", async function (assert) {
+ assert.expect(4);
+
+ let onchangeCalls = 0;
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
+ archs: {
+ 'partner_type,false,form': '<form><field name="name"/></form>'
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ if (onchangeCalls === 0) {
+ assert.deepEqual(args.kwargs.context, { default_name: 'hello' },
+ "context should have default_name with 'hello' as value");
+ }
+ if (onchangeCalls === 1) {
+ assert.deepEqual(args.kwargs.context, {},
+ "context should have default_name with false as value");
+ }
+ onchangeCalls++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.editInput($('.o_field_widget input'), 'hello');
+ await testUtils.fields.many2one.clickItem('timmy', 'Create and Edit');
+ assert.strictEqual(document.querySelector('.modal .o_form_view input').value, "hello",
+ "should contain the 'hello' in the tag name input field");
+
+ // Create record with save & new
+ await testUtils.dom.click(document.querySelector('.modal .btn-primary:nth-child(2)'));
+ assert.strictEqual(document.querySelector('.modal .o_form_view input').value, "",
+ "should display the blank value in the tag name input field");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list add *many* records, remove, re-add', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.timmy.domain = [['color', '=', 2]];
+ this.data.partner.fields.timmy.onChange = true;
+ this.data.partner_type.fields.product_ids = { string: "Product", type: "many2many", relation: 'product' };
+
+ for (var i = 0; i < 50; i++) {
+ var new_record_partner_type = { id: 100 + i, display_name: "batch" + i, color: 2 };
+ this.data.partner_type.records.push(new_record_partner_type);
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="product_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // First round: add 51 records in batch
+ await testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_edit'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ var $modal = $('.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ await testUtils.dom.click($modal.find('thead input[type=checkbox]'));
+
+ await testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
+
+ assert.strictEqual(form.$('.o_data_row').length, 51,
+ 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold'
+
+ await testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_save'));
+
+ // Secound round: remove one record
+ await testUtils.dom.click(form.$buttons.find('.btn.btn-primary.o_form_button_edit'));
+ var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_remove');
+
+ await testUtils.dom.click(trash_buttons.first());
+
+ var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit');
+ assert.equal(pager_limit.text(), '50',
+ 'We should have 50 records in the m2m field');
+
+ // Third round: re-add 1 records
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ $modal = $('.modal-lg');
+
+ assert.equal($modal.length, 1,
+ 'There should be one modal');
+
+ await testUtils.dom.click($modal.find('thead input[type=checkbox]'));
+
+ await testUtils.dom.click($modal.find('.btn.btn-primary.o_select_button'));
+
+ assert.strictEqual(form.$('.o_data_row').length, 51,
+ 'We should have 51 records in the m2m field');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many_tags widget: conditional create/delete actions', async function (assert) {
+ assert.expect(10);
+
+ this.data.turtle.records[0].partner_ids = [2];
+ for (var i = 1; i <= 10; i++) {
+ this.data.partner.records.push({ id: 100 + i, display_name: "Partner" + i });
+ }
+
+ const form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="display_name"/>
+ <field name="turtle_bar"/>
+ <field name="partner_ids" options="{'create': [('turtle_bar', '=', True)], 'delete': [('turtle_bar', '=', True)]}" widget="many2many_tags"/>
+ </form>`,
+ archs: {
+ 'partner,false,list': '<tree><field name="name"/></tree>',
+ 'partner,false,search': '<search/>',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // turtle_bar is true -> create and delete actions are available
+ assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete',
+ 'X icon on badges should not be available');
+
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+
+ const $dropdown1 = form.$('.o_field_many2one input').autocomplete('widget');
+ assert.containsOnce($dropdown1, 'li.o_m2o_start_typing:contains(Start typing...)',
+ 'autocomplete should contain Start typing...');
+
+ await testUtils.fields.many2one.clickItem('partner_ids', 'Search More');
+
+ assert.containsN(document.body, '.modal .modal-footer button', 3,
+ 'there should be 3 buttons (Select, Create and Cancel) available in the modal footer');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // type something that doesn't exist
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'Something that does not exist', 'keydown');
+ // await testUtils.nextTick();
+ assert.containsN(form.$('.o_field_many2one input').autocomplete('widget'), 'li.o_m2o_dropdown_option', 2,
+ 'autocomplete should contain Create and Create and Edit... options');
+
+ // set turtle_bar false -> create and delete actions are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="turtle_bar"] input').first());
+
+ // remove icon should still be there as it doesn't delete records but rather remove links
+ assert.containsOnce(form, '.o_field_many2manytags.o_field_widget .badge .o_delete',
+ 'X icon on badge should still be there even after turtle_bar is not checked');
+
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ const $dropdown2 = form.$('.o_field_many2one input').autocomplete('widget');
+
+ // only Search More option should be available
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option',
+ 'autocomplete should contain only one option');
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)',
+ 'autocomplete option should be Search More');
+
+ await testUtils.fields.many2one.clickItem('partner_ids', 'Search More');
+
+ assert.containsN(document.body, '.modal .modal-footer button', 2,
+ 'there should be 2 buttons (Select and Cancel) available in the modal footer');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // type something that doesn't exist
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'Something that does not exist', 'keyup');
+ // await testUtils.nextTick();
+
+ // only Search More option should be available
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option',
+ 'autocomplete should contain only one option');
+ assert.containsOnce($dropdown2, 'li.o_m2o_dropdown_option:contains(Search More)',
+ 'autocomplete option should be Search More');
+
+ form.destroy();
+ });
+
+ QUnit.test('failing many2one quick create in a many2many_tags', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="timmy" widget="many2many_tags"/></form>',
+ mockRPC(route, args) {
+ if (args.method === 'name_create') {
+ return Promise.reject();
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ color: 8,
+ name: 'new partner',
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'partner_type,false,form': `
+ <form>
+ <field name="name"/>
+ <field name="color"/>
+ </form>`,
+ },
+ });
+
+ assert.containsNone(form, '.o_field_many2manytags .badge');
+
+ // try to quick create a record
+ await testUtils.dom.triggerEvent(form.$('.o_field_many2one input'), 'focus');
+ await testUtils.fields.many2one.searchAndClickItem('timmy', {
+ search: 'new partner',
+ item: 'Create'
+ });
+
+ // as the quick create failed, a dialog should be open to 'slow create' the record
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.strictEqual($('.modal .o_field_widget[name=name]').val(), 'new partner');
+
+ await testUtils.fields.editInput($('.modal .o_field_widget[name=color]'), 8);
+ await testUtils.modal.clickButton('Save & Close');
+
+ assert.containsOnce(form, '.o_field_many2manytags .badge');
+
+ form.destroy();
+ });
+ });
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
new file mode 100644
index 00000000..e8db0df1
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields/field_many2one_tests.js
@@ -0,0 +1,3565 @@
+odoo.define('web.field_many_to_one_tests', function (require) {
+"use strict";
+
+var BasicModel = require('web.BasicModel');
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var relationalFields = require('web.relational_fields');
+var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
+var testUtils = require('web.test_utils');
+var Widget = require('web.Widget');
+
+const cpHelpers = testUtils.controlPanel;
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+ QUnit.module('relational_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: { string: "Foo", type: "char", default: "My little Foo Value" },
+ bar: { string: "Bar", type: "boolean", default: true },
+ int_field: { string: "int_field", type: "integer", sortable: true },
+ p: { string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu' },
+ turtles: { string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu' },
+ trululu: { string: "Trululu", type: "many2one", relation: 'partner' },
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type' },
+ product_id: { string: "Product", type: "many2one", relation: 'product' },
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ string: "Color",
+ },
+ date: { string: "Some Date", type: "date" },
+ datetime: { string: "Datetime Field", type: 'datetime' },
+ user_id: { string: "User", type: 'many2one', relation: 'user' },
+ reference: {
+ string: "Reference Field", type: 'reference', selection: [
+ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ p: [],
+ turtles: [2],
+ timmy: [],
+ trululu: 4,
+ user_id: 17,
+ reference: 'product,37',
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ product_id: 37,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ bar: false,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: { string: "Product Name", type: "char" }
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ display_name: { string: "Partner Type", type: "char" },
+ name: { string: "Partner Type", type: "char" },
+ color: { string: "Color index", type: "integer" },
+ },
+ records: [
+ { id: 12, display_name: "gold", color: 2 },
+ { id: 14, display_name: "silver", color: 5 },
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: { string: "Foo", type: "char" },
+ turtle_bar: { string: "Bar", type: "boolean", default: true },
+ turtle_int: { string: "int", type: "integer", sortable: true },
+ turtle_trululu: { string: "Trululu", type: "many2one", relation: 'partner' },
+ turtle_ref: {
+ string: "Reference", type: 'reference', selection: [
+ ["product", "Product"], ["partner", "Partner"]]
+ },
+ product_id: { string: "Product", type: "many2one", relation: 'product', required: true },
+ partner_ids: { string: "Partner", type: "many2many", relation: 'partner' },
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_bar: true,
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_bar: true,
+ turtle_foo: "blip",
+ turtle_int: 9,
+ partner_ids: [2, 4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ product_id: 37,
+ turtle_bar: false,
+ turtle_foo: "kawa",
+ turtle_int: 21,
+ partner_ids: [],
+ turtle_ref: 'product,37',
+ }],
+ onchanges: {},
+ },
+ user: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ partner_ids: { string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id' },
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ partner_ids: [1, 2],
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ },
+ }, function () {
+ QUnit.module('FieldMany2One');
+
+ QUnit.test('many2ones in form views', async function (assert) {
+ assert.expect(5);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu" string="custom label"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="display_name"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_action') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_action with correct id");
+ return Promise.resolve({
+ res_id: 17,
+ type: 'ir.actions.act_window',
+ target: 'current',
+ res_model: 'res.partner'
+ });
+ }
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_id with correct id");
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ testUtils.mock.intercept(form, 'do_action', function (event) {
+ assert.strictEqual(event.data.action.res_id, 17,
+ "should do a do_action with correct parameters");
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri:contains(aaa)').length, 1,
+ "should contain a link");
+ await testUtils.dom.click(form.$('a.o_form_uri'));
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('.o_external_button'));
+ assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label',
+ "dialog title should display the custom string label");
+
+ // TODO: test that we can edit the record in the dialog, and that
+ // the value is correctly updated on close
+ form.destroy();
+ });
+
+ QUnit.test('editing a many2one, but not changing anything', async function (assert) {
+ assert.expect(2);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="trululu"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="display_name"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [4], "should call get_formview_id with correct id");
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ ids: [1, 2],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // click on the external button (should do an RPC)
+ await testUtils.dom.click(form.$('.o_external_button'));
+ // save and close modal
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ // save form
+ await testUtils.form.clickSave(form);
+ // click next on pager
+ await testUtils.dom.click(form.el.querySelector('.o_pager .o_pager_next'));
+
+ // this checks that the view did not ask for confirmation that the
+ // record is dirty
+ assert.strictEqual(form.el.querySelector('.o_pager').innerText.trim(), '2 / 2',
+ 'pager should be at second page');
+ form.destroy();
+ });
+
+ QUnit.test('context in many2one and default get', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.int_field.default = 14;
+ this.data.partner.fields.trululu.default = 2;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="trululu" context="{\'blip\':int_field}" options=\'{"always_reload": True}\'/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.strictEqual(args.kwargs.context.blip, 14,
+ 'context should have been properly sent to the nameget rpc');
+ }
+ return this._super(route, args);
+ },
+ });
+ form.destroy();
+ });
+
+ QUnit.test('editing a many2one (with form view opened with external button)', async function (assert) {
+ assert.expect(1);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="trululu"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ ids: [1, 2],
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // click on the external button (should do an RPC)
+ await testUtils.dom.click(form.$('.o_external_button'));
+
+ await testUtils.fields.editInput($('.modal input[name="foo"]'), 'brandon');
+
+ // save and close modal
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ // save form
+ await testUtils.form.clickSave(form);
+ // click next on pager
+ await testUtils.dom.click(form.el.querySelector('.o_pager .o_pager_next'));
+
+ // this checks that the view did not ask for confirmation that the
+ // record is dirty
+ assert.strictEqual(form.el.querySelector('.o_pager').innerText.trim(), '2 / 2',
+ 'pager should be at second page');
+ form.destroy();
+ });
+
+ QUnit.test('many2ones in form views with show_address', async function (assert) {
+ assert.expect(4);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field ' +
+ 'name="trululu" ' +
+ 'string="custom label" ' +
+ 'context="{\'show_address\': 1}" ' +
+ 'options="{\'always_reload\': True}"' +
+ '/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ return this._super(route, args).then(function (result) {
+ result[0][1] += '\nStreet\nCity ZIP';
+ return result;
+ });
+ }
+ return this._super(route, args);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri').html(), '<span>aaa</span><br><span>Street</span><br><span>City ZIP</span>',
+ "input should have a multi-line content in readonly due to show_address");
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'button.o_external_button:visible',
+ "should have an open record button");
+
+ testUtils.dom.click(form.$('input.o_input'));
+
+ assert.containsOnce(form, 'button.o_external_button:visible',
+ "should still have an open record button");
+ form.$('input.o_input').trigger('focusout');
+ assert.strictEqual($('.modal button:contains(Create and edit)').length, 0,
+ "there should not be a quick create modal");
+
+ form.destroy();
+ });
+
+ QUnit.test('show_address works in a view embedded in a view of another type', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records[1].turtle_trululu = 2;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="turtles"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="T">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ "turtle,false,list": '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ return this._super(route, args).then(function (result) {
+ if (args.model === 'partner' && args.kwargs.context.show_address) {
+ result[0][1] += '\nrue morgue\nparis 75013';
+ }
+ return result;
+ });
+ }
+ return this._super(route, args);
+ },
+ });
+ // click the turtle field, opens a modal with the turtle form view
+ await testUtils.dom.click(form.$('.o_data_row:first td.o_data_cell'));
+
+ assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013",
+ "The partner's address should be displayed");
+ form.destroy();
+ });
+
+ QUnit.test('many2one data is reloaded if there is a context to take into account', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records[1].turtle_trululu = 2;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="turtles"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="T">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ "turtle,false,list": '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ return this._super(route, args).then(function (result) {
+ if (args.model === 'partner' && args.kwargs.context.show_address) {
+ result[0][1] += '\nrue morgue\nparis 75013';
+ }
+ return result;
+ });
+ }
+ return this._super(route, args);
+ },
+ });
+ // click the turtle field, opens a modal with the turtle form view
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013",
+ "The partner's address should be displayed");
+ form.destroy();
+ });
+
+ QUnit.test('many2ones in form views with search more', async function (assert) {
+ assert.expect(3);
+ this.data.partner.records.push({
+ id: 5,
+ display_name: "Partner 4",
+ }, {
+ id: 6,
+ display_name: "Partner 5",
+ }, {
+ id: 7,
+ display_name: "Partner 6",
+ }, {
+ id: 8,
+ display_name: "Partner 7",
+ }, {
+ id: 9,
+ display_name: "Partner 8",
+ }, {
+ id: 10,
+ display_name: "Partner 9",
+ });
+ this.data.partner.fields.datetime.searchable = true;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner,false,search': '<search><field name="datetime"/></search>',
+ },
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickItem('trululu', 'Search');
+
+ assert.strictEqual($('tr.o_data_row').length, 9, "should display 9 records");
+
+ await cpHelpers.toggleFilterMenu('.modal');
+ await cpHelpers.toggleAddCustomFilter('.modal');
+ assert.strictEqual(document.querySelector('.modal .o_generator_menu_field').value, 'datetime',
+ "datetime field should be selected");
+ await cpHelpers.applyFilter('.modal');
+
+ assert.strictEqual($('tr.o_data_row').length, 0, "should display 0 records");
+ form.destroy();
+ });
+
+ QUnit.test('onchanges on many2ones trigger when editing record in form view', async function (assert) {
+ assert.expect(10);
+
+ this.data.partner.onchanges.user_id = function () { };
+ this.data.user.fields.other_field = { string: "Other Field", type: "char" };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="user_id"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'user,false,form': '<form string="Users"><field name="other_field"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'get_formview_id') {
+ return Promise.resolve(false);
+ }
+ if (args.method === 'onchange') {
+ assert.strictEqual(args.args[1].user_id, 17,
+ "onchange is triggered with correct user_id");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // open the many2one in form view and change something
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_external_button'));
+ await testUtils.fields.editInput($('.modal-body input[name="other_field"]'), 'wood');
+
+ // save the modal and make sure an onchange is triggered
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary').first());
+ assert.verifySteps(['read', 'get_formview_id', 'load_views', 'read', 'write', 'read', 'onchange']);
+
+ // save the main record, and check that no extra rpcs are done (record
+ // is not dirty, only a related record was modified)
+ await testUtils.form.clickSave(form);
+ assert.verifySteps([]);
+ form.destroy();
+ });
+
+ QUnit.test("many2one doesn't trigger field_change when being emptied", async function (assert) {
+ assert.expect(2);
+
+ const list = await createView({
+ arch: `
+ <tree multi_edit="1">
+ <field name="trululu"/>
+ </tree>`,
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const $input = list.$('.o_field_widget[name=trululu] input');
+
+ await testUtils.fields.editInput($input, "");
+ await testUtils.dom.triggerEvents($input, ['keyup']);
+
+ assert.containsNone(document.body, '.modal',
+ "No save should be triggered when removing value");
+
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+
+ assert.containsOnce(document.body, '.modal',
+ "Saving should be triggered when selecting a value");
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ list.destroy();
+ });
+
+ QUnit.test("focus tracking on a many2one in a list", async function (assert) {
+ assert.expect(4);
+
+ const list = await createView({
+ arch: '<tree editable="top"><field name="trululu"/></tree>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
+ },
+ data: this.data,
+ model: 'partner',
+ View: ListView,
+ });
+
+ // Select two records
+ await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
+ await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
+
+ await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
+
+ const input = list.$('.o_data_row:first() .o_data_cell:first() input')[0];
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused when activated");
+
+ await testUtils.fields.many2one.createAndEdit('trululu', "ABC");
+
+ // At this point, if the focus is correctly registered by the m2o, there
+ // should be only one modal (the "Create" one) and none for saving changes.
+ assert.containsOnce(document.body, '.modal', "There should be only one modal");
+
+ await testUtils.dom.click($('.modal .btn:not(.btn-primary)'));
+
+ assert.strictEqual(document.activeElement, input, "Input should be focused after dialog closes");
+ assert.strictEqual(input.value, "", "Input should be empty after discard");
+
+ list.destroy();
+ });
+
+ QUnit.test('many2one fields with option "no_open"', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu" options="{&quot;no_open&quot;: True}" />' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'span.o_field_widget[name=trululu]',
+ "should be displayed inside a span (sanity check)");
+ assert.containsNone(form, 'span.o_form_uri', "should not have an anchor");
+
+ await testUtils.form.clickEdit(form);
+ assert.containsNone(form, '.o_field_widget[name=trululu] .o_external_button', "should not have the button to open the record");
+
+ form.destroy();
+ });
+
+ QUnit.test('empty many2one field', async function (assert) {
+ assert.expect(4);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <sheet>
+ <group>
+ <field name="trululu"/>
+ </group>
+ </sheet>
+ </form>`,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ const $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ assert.containsNone($dropdown, 'li.o_m2o_dropdown_option',
+ 'autocomplete should not contains dropdown options');
+ assert.containsOnce($dropdown, 'li.o_m2o_start_typing',
+ 'autocomplete should contains start typing option');
+
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="trululu"] input'),
+ 'abc', 'keydown');
+ await testUtils.nextTick();
+ assert.containsN($dropdown, 'li.o_m2o_dropdown_option', 2,
+ 'autocomplete should contains 2 dropdown options');
+ assert.containsNone($dropdown, 'li.o_m2o_start_typing',
+ 'autocomplete should not contains start typing option');
+
+ form.destroy();
+ });
+
+ QUnit.test('empty many2one field with node options', async function (assert) {
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <sheet>
+ <group>
+ <field name="trululu" options="{'no_create_edit': 1}"/>
+ <field name="product_id" options="{'no_create_edit': 1, 'no_quick_create': 1}"/>
+ </group>
+ </sheet>
+ </form>`,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ const $dropdownTrululu = form.$('.o_field_many2one[name="trululu"] input').autocomplete('widget');
+ const $dropdownProduct = form.$('.o_field_many2one[name="product_id"] input').autocomplete('widget');
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ assert.containsOnce($dropdownTrululu, 'li.o_m2o_start_typing',
+ 'autocomplete should contains start typing option');
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ assert.containsNone($dropdownProduct, 'li.o_m2o_start_typing',
+ 'autocomplete should contains start typing option');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one in edit mode', async function (assert) {
+ assert.expect(17);
+
+ // create 10 partners to have the 'Search More' option in the autocomplete dropdown
+ for (var i = 0; i < 10; i++) {
+ var id = 20 + i;
+ this.data.partner.records.push({ id: id, display_name: "Partner " + id });
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree string="Partners"><field name="display_name"/></tree>',
+ 'partner,false,search': '<search string="Partners">' +
+ '<field name="display_name" string="Name"/>' +
+ '</search>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[1].trululu, 20, "should write the correct id");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // the SelectCreateDialog requests the session, so intercept its custom
+ // event to specify a fake session to prevent it from crashing
+ testUtils.mock.intercept(form, 'get_session', function (event) {
+ event.data.callback({ user_context: {} });
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ assert.ok($dropdown.is(':visible'),
+ 'clicking on the m2o input should open the dropdown if it is not open yet');
+ assert.strictEqual($dropdown.find('li:not(.o_m2o_dropdown_option)').length, 7,
+ 'autocomplete should contains 8 suggestions');
+ assert.strictEqual($dropdown.find('li.o_m2o_dropdown_option').length, 1,
+ 'autocomplete should contain "Search More"');
+ assert.containsNone($dropdown, 'li.o_m2o_start_typing',
+ 'autocomplete should not contains start typing option if value is available');
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ assert.ok(!$dropdown.is(':visible'),
+ 'clicking on the m2o input should close the dropdown if it is open');
+
+ // change the value of the m2o with a suggestion of the dropdown
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+ assert.ok(!$dropdown.is(':visible'), 'clicking on a value should close the dropdown');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'first record',
+ 'value of the m2o should have been correctly updated');
+
+ // change the value of the m2o with a record in the 'Search More' modal
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ // click on 'Search More' (mouseenter required by ui-autocomplete)
+ await testUtils.fields.many2one.clickItem('trululu', 'Search');
+ assert.ok($('.modal .o_list_view').length, "should have opened a list view in a modal");
+ assert.ok(!$('.modal .o_list_view .o_list_record_selector').length,
+ "there should be no record selector in the list view");
+ assert.ok(!$('.modal .modal-footer .o_select_button').length,
+ "there should be no 'Select' button in the footer");
+ assert.ok($('.modal tbody tr').length > 10, "list should contain more than 10 records");
+ await cpHelpers.editSearch('.modal', "P");
+ await cpHelpers.validateSearch('.modal');
+ assert.strictEqual($('.modal tbody tr').length, 10,
+ "list should be restricted to records containing a P (10 records)");
+ // choose a record
+ await testUtils.dom.click($('.modal tbody tr:contains(Partner 20)'));
+ assert.ok(!$('.modal').length, "should have closed the modal");
+ assert.ok(!$dropdown.is(':visible'), 'should have closed the dropdown');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'Partner 20',
+ 'value of the m2o should have been correctly updated');
+
+ // save
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('a.o_form_uri').text(), 'Partner 20',
+ "should display correct value after save");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one in non edit mode', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'a.o_form_uri',
+ "should display 1 m2o link in form");
+ assert.hasAttrValue(form.$('a.o_form_uri'), 'href', "#id=4&model=partner",
+ "href should contain id and model");
+
+ // Remove value from many2one and then save, there should not have href with id and model on m2o anchor
+ await testUtils.form.clickEdit(form);
+ form.$('.o_field_many2one input').val('').trigger('keyup').trigger('focusout');
+ await testUtils.form.clickSave(form);
+
+ assert.hasAttrValue(form.$('a.o_form_uri'), 'href', "#",
+ "href should have #");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one with co-model whose name field is a many2one', async function (assert) {
+ assert.expect(4);
+
+ this.data.product.fields.name = {
+ string: 'User Name',
+ type: 'many2one',
+ relation: 'user',
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="product_id"/></form>',
+ archs: {
+ 'product,false,form': '<form><field name="name"/></form>',
+ },
+ });
+
+ await testUtils.fields.many2one.createAndEdit('product_id', "ABC");
+ assert.containsOnce(document.body, '.modal .o_form_view');
+
+ // quick create 'new value'
+ await testUtils.fields.many2one.searchAndClickItem('name', {search: 'new value'});
+ assert.strictEqual($('.modal .o_field_many2one input').val(), 'new value');
+
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary')); // save in modal
+ assert.containsNone(document.body, '.modal .o_form_view');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'new value');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one searches with correct value', async function (assert) {
+ assert.expect(6);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="trululu"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.step('search: ' + args.kwargs.name);
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'aaa',
+ "should be initially set to 'aaa'");
+
+ await testUtils.dom.click(form.$('.o_field_many2one input'));
+ // unset the many2one -> should search again with ''
+ form.$('.o_field_many2one input').val('').trigger('keydown');
+ await testUtils.nextTick();
+ form.$('.o_field_many2one input').val('p').trigger('keydown').trigger('keyup');
+ await testUtils.nextTick();
+
+ // close and re-open the dropdown -> should search with 'p' again
+ await testUtils.dom.click(form.$('.o_field_many2one input'));
+ await testUtils.dom.click(form.$('.o_field_many2one input'));
+
+ assert.verifySteps(['search: ', 'search: ', 'search: p', 'search: p']);
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('many2one search with trailing and leading spaces', async function (assert) {
+ assert.expect(10);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form><field name="trululu"/></form>`,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.step('search: ' + args.kwargs.name);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ const $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ assert.isVisible($dropdown);
+ assert.containsN($dropdown, 'li:not(.o_m2o_dropdown_option)', 4,
+ 'autocomplete should contains 4 suggestions');
+
+ // search with leading spaces
+ form.$('.o_field_many2one input').val(' first').trigger('keydown').trigger('keyup');
+ await testUtils.nextTick();
+ assert.containsOnce($dropdown, 'li:not(.o_m2o_dropdown_option)',
+ 'autocomplete should contains 1 suggestion');
+
+ // search with trailing spaces
+ form.$('.o_field_many2one input').val('first ').trigger('keydown').trigger('keyup');
+ await testUtils.nextTick();
+ assert.containsOnce($dropdown, 'li:not(.o_m2o_dropdown_option)',
+ 'autocomplete should contains 1 suggestion');
+
+ // search with leading and trailing spaces
+ form.$('.o_field_many2one input').val(' first ').trigger('keydown').trigger('keyup');
+ await testUtils.nextTick();
+ assert.containsOnce($dropdown, 'li:not(.o_m2o_dropdown_option)',
+ 'autocomplete should contains 1 suggestion');
+
+ assert.verifySteps(['search: ', 'search: first', 'search: first', 'search: first']);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one field with option always_reload', async function (assert) {
+ assert.expect(4);
+ var count = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ count++;
+ return Promise.resolve([[1, "first record\nand some address"]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(count, 1, "an extra name_get should have been done");
+ assert.ok(form.$('a:contains(and some address)').length,
+ "should display additional result");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), "first record",
+ "actual field value should be displayed to be edited");
+
+ await testUtils.form.clickSave(form);
+
+ assert.ok(form.$('a:contains(and some address)').length,
+ "should still display additional result");
+ form.destroy();
+ });
+
+ QUnit.test('many2one field and list navigation', async function (assert) {
+ assert.expect(3);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="trululu"/></tree>',
+ });
+
+ // edit first input, to trigger autocomplete
+ await testUtils.dom.click(list.$('.o_data_row .o_data_cell').first());
+ await testUtils.fields.editInput(list.$('.o_data_cell input'), '');
+
+ // press keydown, to select first choice
+ await testUtils.fields.triggerKeydown(list.$('.o_data_cell input').focus(), 'down');
+
+ // we now check that the dropdown is open (and that the focus did not go
+ // to the next line)
+ var $dropdown = list.$('.o_field_many2one input').autocomplete('widget');
+ assert.ok($dropdown.is(':visible'), "dropdown should be visible");
+ assert.hasClass(list.$('.o_data_row:eq(0)'),'o_selected_row',
+ 'first data row should still be selected');
+ assert.doesNotHaveClass(list.$('.o_data_row:eq(1)'), 'o_selected_row',
+ 'second data row should not be selected');
+
+ list.destroy();
+ });
+
+ QUnit.test('standalone many2one field', async function (assert) {
+ assert.expect(4);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var fixture = $('#qunit-fixture');
+ var self = this;
+
+ var model = await testUtils.createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+ var record;
+ model.makeRecord('coucou', [{
+ name: 'partner_id',
+ relation: 'partner',
+ type: 'many2one',
+ value: [1, 'first partner'],
+ }]).then(function (recordID) {
+ record = model.get(recordID);
+ });
+ await testUtils.nextTick();
+ // create a new widget that uses the StandaloneFieldManagerMixin
+ var StandaloneWidget = Widget.extend(StandaloneFieldManagerMixin, {
+ init: function (parent) {
+ this._super.apply(this, arguments);
+ StandaloneFieldManagerMixin.init.call(this, parent);
+ },
+ });
+ var parent = new StandaloneWidget(model);
+ model.setParent(parent);
+ await testUtils.mock.addMockEnvironment(parent, {
+ data: self.data,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ var relField = new relationalFields.FieldMany2One(parent, 'partner_id', record, {
+ mode: 'edit',
+ noOpen: true,
+ });
+
+ relField.appendTo(fixture);
+ await testUtils.nextTick();
+ await testUtils.fields.editInput($('input.o_input'), 'xyzzrot');
+
+ await testUtils.fields.many2one.clickItem('partner_id', 'Create');
+
+ assert.containsNone(relField, '.o_external_button',
+ "should not have the button to open the record");
+ assert.verifySteps(['name_search', 'name_create']);
+
+ parent.destroy();
+ model.destroy();
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ });
+
+ // QUnit.test('onchange on a many2one to a different model', async function (assert) {
+ // This test is commented because the mock server does not give the correct response.
+ // It should return a couple [id, display_name], but I don't know the logic used
+ // by the server, so it's hard to emulate it correctly
+ // assert.expect(2);
+
+ // this.data.partner.records[0].product_id = 41;
+ // this.data.partner.onchanges = {
+ // foo: function(obj) {
+ // obj.product_id = 37;
+ // },
+ // };
+
+ // var form = await createView({
+ // View: FormView,
+ // model: 'partner',
+ // data: this.data,
+ // arch: '<form>' +
+ // '<field name="foo"/>' +
+ // '<field name="product_id"/>' +
+ // '</form>',
+ // res_id: 1,
+ // });
+ // await testUtils.form.clickEdit(form);
+ // assert.strictEqual(form.$('input').eq(1).val(), 'xpad', "initial product_id val should be xpad");
+
+ // testUtils.fields.editInput(form.$('input').eq(0), "let us trigger an onchange");
+
+ // assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied");
+ // });
+
+ QUnit.test('form: quick create then save directly', async function (assert) {
+ assert.expect(5);
+
+ var prom = testUtils.makeTestPromise();
+ var newRecordID;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return prom.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {search: 'b'});
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ await prom.resolve();
+ await testUtils.nextTick();
+
+ assert.verifySteps(['create']);
+ form.destroy();
+ });
+
+ QUnit.test('form: quick create for field that returns false after name_create call', async function (assert) {
+ assert.expect(3);
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/></form>',
+ mockRPC: function (route, args) {
+ const result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ // Resolve the name_create call to false. This is possible if
+ // _rec_name for the model of the field is unassigned.
+ return Promise.resolve(false);
+ }
+ return result;
+ },
+ });
+ await testUtils.fields.many2one.searchAndClickItem('trululu', { search: 'beam' });
+ assert.verifySteps(['name_create'], 'attempt to name_create');
+ assert.strictEqual(form.$(".o_input_dropdown input").val(), "",
+ "the input should contain no text after search and click")
+ form.destroy();
+ });
+
+ QUnit.test('list: quick create then save directly', async function (assert) {
+ assert.expect(8);
+
+ var prom = testUtils.makeTestPromise();
+ var newRecordID;
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="trululu"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return prom.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
+
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {search:'b'});
+ list.$buttons.find('.o_list_button_add').show();
+ testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+ assert.containsN(list, '.o_data_row', 4,
+ "should wait for the name_create before adding the new row");
+
+ await prom.resolve();
+ await testUtils.nextTick();
+
+ assert.verifySteps(['create']);
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), 'b',
+ "created row should have the correct m2o value");
+ assert.containsN(list, '.o_data_row', 5, "should have added the fifth row");
+
+ list.destroy();
+ });
+
+ QUnit.test('list in form: quick create then save directly', async function (assert) {
+ assert.expect(6);
+
+ var prom = testUtils.makeTestPromise();
+ var newRecordID;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return prom.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].p[0][2].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {search: 'b'});
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ await prom.resolve();
+ await testUtils.nextTick();
+
+ assert.verifySteps(['create']);
+ assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
+ "first row should have the correct m2o value");
+ form.destroy();
+ });
+
+ QUnit.test('list in form: quick create then add a new line directly', async function (assert) {
+ // required many2one inside a one2many list: directly after quick creating
+ // a new many2one value (before the name_create returns), click on add an item:
+ // at this moment, the many2one has still no value, and as it is required,
+ // the row is discarded if a saveLine is requested. However, it should
+ // wait for the name_create to return before trying to save the line.
+ assert.expect(8);
+
+ this.data.partner.onchanges = {
+ trululu: function () { },
+ };
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var prom = testUtils.makeTestPromise();
+ var newRecordID;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ return prom.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].p[0][2].trululu, newRecordID);
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'b', 'keydown');
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should still be only one row");
+ assert.hasClass(form.$('.o_data_row'),'o_selected_row',
+ "the row should still be in edition");
+
+ await prom.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
+ "first row should have the correct m2o value");
+ assert.containsN(form, '.o_data_row', 2,
+ "there should now be 2 rows");
+ assert.hasClass(form.$('.o_data_row:nth(1)'),'o_selected_row',
+ "the second row should be in edition");
+
+ await testUtils.form.clickSave(form);
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should be 1 row saved (the second one was empty and invalid)");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'b',
+ "should have the correct m2o value");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('list in form: create with one2many with many2one', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [[0, 0, { display_name: 'new record', p: [] }]];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ throw new Error('Nameget should not be called');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
+ "should have created the new record in the o2m with the correct name");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: create with one2many with many2one (version 2)', async function (assert) {
+ // This test simulates the exact same scenario as the previous one,
+ // except that the value for the many2one is explicitely set to false,
+ // which is stupid, but this happens, so we have to handle it
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [
+ [0, 0, { display_name: 'new record', trululu: false, p: [] }]
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ throw new Error('Nameget should not be called');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
+ "should have created the new record in the o2m with the correct name");
+
+ form.destroy();
+ });
+
+ QUnit.test('item not dropped on discard with empty required field (default_get)', async function (assert) {
+ // This test simulates discarding a record that has been created with
+ // one of its required field that is empty. When we discard the changes
+ // on this empty field, it should not assume that this record should be
+ // abandonned, since it has been added (even though it is a new record).
+ assert.expect(8);
+
+ this.data.partner.fields.p.default = [
+ [0, 0, { display_name: 'new record', trululu: false, p: [] }]
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ assert.strictEqual($('tr.o_data_row').length, 1,
+ "should have created the new record in the o2m");
+ assert.strictEqual($('td.o_data_cell').first().text(), "new record",
+ "should have the correct displayed name");
+
+ var requiredElement = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredElement.length, 1,
+ "should have a required field on this record");
+ assert.strictEqual(requiredElement.text(), "",
+ "should have empty string in the required field on this record");
+
+ testUtils.dom.click(requiredElement);
+ // discard by clicking on body
+ testUtils.dom.click($('body'));
+
+ assert.strictEqual($('tr.o_data_row').length, 1,
+ "should still have the record in the o2m");
+ assert.strictEqual($('td.o_data_cell').first().text(), "new record",
+ "should still have the correct displayed name");
+
+ // update selector of required field element
+ requiredElement = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredElement.length, 1,
+ "should still have the required field on this record");
+ assert.strictEqual(requiredElement.text(), "",
+ "should still have empty string in the required field on this record");
+ form.destroy();
+ });
+
+ QUnit.test('list in form: name_get with unique ids (default_get)', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].display_name = "MyTrululu";
+ this.data.partner.fields.p.default = [
+ [0, 0, { trululu: 1, p: [] }],
+ [0, 0, { trululu: 1, p: [] }]
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ throw new Error('should not call name_get');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').text(), "MyTrululuMyTrululu",
+ "both records should have the correct display_name for trululu field");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: show name of many2one fields in multi-page (default_get)', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.p.default = [
+ [0, 0, { display_name: 'record1', trululu: 1, p: [] }],
+ [0, 0, { display_name: 'record2', trululu: 2, p: [] }]
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom" limit="1">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record1", "should show display_name of 1st record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "first record", "should show display_name of trululu of 1st record");
+
+ await testUtils.dom.click(form.$('button.o_pager_next'));
+
+ assert.strictEqual(form.$('td.o_data_cell').first().text(),
+ "record2", "should show display_name of 2nd record");
+ assert.strictEqual(form.$('td.o_data_cell').first().next().text(),
+ "second record", "should show display_name of trululu of 2nd record");
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: item not dropped on discard with empty required field (onchange in default_get)', async function (assert) {
+ // variant of the test "list in form: discard newly added element with
+ // empty required field (default_get)", in which the `default_get`
+ // performs an `onchange` at the same time. This `onchange` may create
+ // some records, which should not be abandoned on discard, similarly
+ // to records created directly by `default_get`
+ assert.expect(7);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.fields.product_id.default = 37;
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ if (obj.product_id === 37) {
+ obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
+ }
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // check that there is a record in the editable list with empty string as required field
+ assert.containsOnce(form, '.o_data_row',
+ "should have a row in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should have the correct displayed name");
+ var requiredField = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field on this record");
+ assert.strictEqual(requiredField.text(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ testUtils.dom.click(requiredField);
+ // click off so that the required field still stay empty
+ testUtils.dom.click($('body'));
+
+ // record should not be dropped
+ assert.containsOnce(form, '.o_data_row',
+ "should not have dropped record in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should still have the correct displayed name");
+ assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
+ "should still have empty string in the required field");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('list in form: item not dropped on discard with empty required field (onchange on list after default_get)', async function (assert) {
+ // discarding a record from an `onchange` in a `default_get` should not
+ // abandon the record. This should not be the case for following
+ // `onchange`, except if an onchange make some changes on the list:
+ // in particular, if an onchange make changes on the list such that
+ // a record is added, this record should not be dropped on discard
+ assert.expect(8);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges = {
+ product_id: function (obj) {
+ if (obj.product_id === 37) {
+ obj.p = [[0, 0, { display_name: "entry", trululu: false }]];
+ }
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // check no record in list
+ assert.containsNone(form, '.o_data_row',
+ "should have no row in the editable list");
+
+ // select product_id to force on_change in editable list
+ await testUtils.dom.click(form.$('.o_field_widget[name="product_id"] .o_input'));
+ await testUtils.dom.click($('.ui-menu-item').first());
+
+ // check that there is a record in the editable list with empty string as required field
+ assert.containsOnce(form, '.o_data_row',
+ "should have a row in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should have the correct displayed name");
+ var requiredField = $('td.o_data_cell.o_required_modifier');
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field on this record");
+ assert.strictEqual(requiredField.text(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ await testUtils.dom.click(requiredField);
+ // click off so that the required field still stay empty
+ await testUtils.dom.click($('body'));
+
+ // record should not be dropped
+ assert.containsOnce(form, '.o_data_row',
+ "should not have dropped record in the editable list");
+ assert.strictEqual($('td.o_data_cell').first().text(), "entry",
+ "should still have the correct displayed name");
+ assert.strictEqual($('td.o_data_cell.o_required_modifier').text(), "",
+ "should still have empty string in the required field");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('item dropped on discard with empty required field with "Add an item" (invalid on "ADD")', async function (assert) {
+ // when a record in a list is added with "Add an item", it should
+ // always be dropped on discard if some required field are empty
+ // at the record creation.
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // Click on "Add an item"
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ var charField = form.$('.o_field_widget.o_field_char[name="display_name"]');
+ var requiredField = form.$('.o_field_widget.o_required_modifier[name="trululu"]');
+ charField.val("some text");
+ assert.strictEqual(charField.length, 1,
+ "should have a char field 'display_name' on this record");
+ assert.doesNotHaveClass(charField, 'o_required_modifier',
+ "the char field should not be required on this record");
+ assert.strictEqual(charField.val(), "some text",
+ "should have entered text in the char field on this record");
+ assert.strictEqual(requiredField.length, 1,
+ "should have a required field 'trululu' on this record");
+ assert.strictEqual(requiredField.val().trim(), "",
+ "should have empty string in the required field on this record");
+
+ // click on empty required field in editable list record
+ await testUtils.dom.click(requiredField);
+ // click off so that the required field still stay empty
+ await testUtils.dom.click($('body'));
+
+ // record should be dropped
+ assert.containsNone(form, '.o_data_row',
+ "should have dropped record in the editable list");
+
+ form.destroy();
+ });
+
+ QUnit.test('item not dropped on discard with empty required field with "Add an item" (invalid on "UPDATE")', async function (assert) {
+ // when a record in a list is added with "Add an item", it should
+ // be temporarily added to the list when it is valid (e.g. required
+ // fields are non-empty). If the record is updated so that the required
+ // field is empty, and it is discarded, then the record should not be
+ // dropped.
+ assert.expect(8);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.containsNone(form, '.o_data_row',
+ "should initially not have any record in the list");
+
+ // Click on "Add an item"
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsOnce(form, '.o_data_row',
+ "should have a temporary record in the list");
+
+ var $inputEditMode = form.$('.o_field_widget.o_required_modifier[name="trululu"] input');
+ assert.strictEqual($inputEditMode.length, 1,
+ "should have a required field 'trululu' on this record");
+ assert.strictEqual($inputEditMode.val(), "",
+ "should have empty string in the required field on this record");
+
+ // add something to required field and leave edit mode of the record
+ await testUtils.dom.click($inputEditMode);
+ await testUtils.dom.click($('li.ui-menu-item').first());
+ await testUtils.dom.click($('body'));
+
+ var $inputReadonlyMode = form.$('.o_data_cell.o_required_modifier');
+ assert.containsOnce(form, '.o_data_row',
+ "should not have dropped valid record when leaving edit mode");
+ assert.strictEqual($inputReadonlyMode.text(), "first record",
+ "should have put some content in the required field on this record");
+
+ // remove the required field and leave edit mode of the record
+ await testUtils.dom.click($('.o_data_row'));
+ assert.containsOnce(form, '.o_data_row',
+ "should not have dropped record in the list on discard (invalid on UPDATE)");
+ assert.strictEqual($inputReadonlyMode.text(), "first record",
+ "should keep previous valid required field content on this record");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('list in form: default_get with x2many create', async function (assert) {
+ assert.expect(3);
+ this.data.partner.fields.timmy.default = [
+ [0, 0, { display_name: 'brandon is the new timmy', name: 'brandon' }]
+ ];
+ var displayName = 'brandon is the new timmy';
+ this.data.partner.onchanges.timmy = function (obj) {
+ obj.int_field = obj.timmy.length;
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="timmy">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ int_field: 2,
+ timmy: [
+ [6, false, []],
+ // LPE TODO 1 taskid-2261084: remove this entire comment including code snippet
+ // when the change in behavior has been thoroughly tested.
+ // We can't distinguish a value coming from a default_get
+ // from one coming from the onchange, and so we can either store and
+ // send it all the time, or never.
+ // [0, args.args[0].timmy[1][1], { display_name: displayName, name: 'brandon' }],
+ [0, args.args[0].timmy[1][1], { display_name: displayName }],
+ ],
+ }, "should send the correct values to create");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual($('td.o_data_cell:first').text(), 'brandon is the new timmy',
+ "should have created the new record in the m2m with the correct name");
+ assert.strictEqual($('input.o_field_integer').val(), '1',
+ "should have called and executed the onchange properly");
+
+ // edit the subrecord and save
+ displayName = 'new value';
+ await testUtils.dom.click(form.$('.o_data_cell'));
+ await testUtils.fields.editInput(form.$('.o_data_cell input'), displayName);
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: default_get with x2many create and onchange', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.turtles.default = [[6, 0, [2, 3]]];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].turtles, [
+ [4, 2, false],
+ [4, 3, false],
+ ], 'should send proper commands to create method');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('list in form: call button in sub view', async function (assert) {
+ assert.expect(11);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/get_formview_id') {
+ return Promise.resolve(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ intercepts: {
+ execute_action: function (event) {
+ assert.strictEqual(event.data.env.model, 'product',
+ 'should call with correct model in env');
+ assert.strictEqual(event.data.env.currentID, 37,
+ 'should call with correct currentID in env');
+ assert.deepEqual(event.data.env.resIDs, [37],
+ 'should call with correct resIDs in env');
+ assert.step(event.data.action_data.name);
+ },
+ },
+ archs: {
+ 'product,false,form': '<form string="Partners">' +
+ '<header>' +
+ '<button name="action" type="action" string="Just do it !"/>' +
+ '<button name="object" type="object" string="Just don\'t do it !"/>' +
+ '<field name="display_name"/>' +
+ '</header>' +
+ '</form>',
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('td.o_data_cell:first'));
+ await testUtils.dom.click(form.$('.o_external_button'));
+ await testUtils.dom.click($('button:contains("Just do it !")'));
+ assert.verifySteps(['action']);
+ await testUtils.dom.click($('button:contains("Just don\'t do it !")'));
+ assert.verifySteps([]); // the second button is disabled, it can't be clicked
+
+ await testUtils.dom.click($('.modal .btn-secondary:contains(Discard)'));
+ await testUtils.dom.click(form.$('.o_external_button'));
+ await testUtils.dom.click($('button:contains("Just don\'t do it !")'));
+ assert.verifySteps(['object']);
+ form.destroy();
+ });
+
+ QUnit.test('X2Many sequence list in modal', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.sequence = { string: 'Sequence', type: 'integer' };
+ this.data.partner.records[0].sequence = 1;
+ this.data.partner.records[1].sequence = 2;
+ this.data.partner.onchanges = {
+ sequence: function (obj) {
+ if (obj.id === 2) {
+ obj.sequence = 1;
+ assert.step('onchange sequence');
+ }
+ },
+ };
+
+ this.data.product.fields.turtle_ids = { string: 'Turtles', type: 'one2many', relation: 'turtle' };
+ this.data.product.records[0].turtle_ids = [1];
+
+ this.data.turtle.fields.partner_types_ids = { string: "Partner", type: "one2many", relation: 'partner' };
+ this.data.turtle.fields.type_id = { string: "Partner Type", type: "many2one", relation: 'partner_type' };
+
+ this.data.partner_type.fields.partner_ids = { string: "Partner", type: "one2many", relation: 'partner' };
+ this.data.partner_type.records[0].partner_ids = [1, 2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'product',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="turtle_ids" widget="one2many">' +
+ '<tree string="Turtles" editable="bottom">' +
+ '<field name="type_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,form': '<form><field name="partner_ids"/></form>',
+ 'partner,false,list': '<tree string="Vendors">' +
+ '<field name="display_name"/>' +
+ '<field name="sequence" widget="handle"/>' +
+ '</tree>',
+ },
+ res_id: 37,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/read') {
+ return Promise.resolve([{ id: 37, name: 'xphone', display_name: 'leonardo', turtle_ids: [1] }]);
+ }
+ if (route === '/web/dataset/call_kw/turtle/read') {
+ return Promise.resolve([{ id: 1, type_id: [12, 'gold'] }]);
+ }
+ if (route === '/web/dataset/call_kw/partner_type/get_formview_id') {
+ return Promise.resolve(false);
+ }
+ if (route === '/web/dataset/call_kw/partner_type/read') {
+ return Promise.resolve([{ id: 12, partner_ids: [1, 2], display_name: 'gold' }]);
+ }
+ if (route === '/web/dataset/call_kw/partner_type/write') {
+ assert.step('partner_type write');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_cell'));
+ await testUtils.dom.click(form.$('.o_external_button'));
+
+ var $modal = $('.modal');
+ assert.equal($modal.length, 1,
+ 'There should be 1 modal opened');
+
+ var $handles = $modal.find('.ui-sortable-handle');
+ assert.equal($handles.length, 2,
+ 'There should be 2 sequence handlers');
+
+ await testUtils.dom.dragAndDrop($handles.eq(1),
+ $modal.find('tbody tr').first(), { position: 'top' });
+
+ // Saving the modal and then the original model
+ await testUtils.dom.click($modal.find('.modal-footer .btn-primary'));
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['onchange sequence', 'partner_type write']);
+
+ form.destroy();
+ });
+
+ QUnit.test('autocompletion in a many2one, in form view with a domain', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ domain: [['trululu', '=', 4]]
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [], "should not have a domain");
+ }
+ return this._super(route, args);
+ }
+ });
+ await testUtils.form.clickEdit(form);
+
+ testUtils.dom.click(form.$('.o_field_widget[name=product_id] input'));
+ form.destroy();
+ });
+
+ QUnit.test('autocompletion in a many2one, in form view with a date field', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="date"/>' +
+ '<field name="trululu" domain="[(\'bar\',\'=\',True)]"/>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [["bar", "=", true]], "should not have a domain");
+ }
+ return this._super(route, args);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+ form.destroy();
+ });
+
+ QUnit.test('creating record with many2one with option always_reload', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.trululu.default = 1;
+ this.data.partner.onchanges = {
+ trululu: function (obj) {
+ obj.trululu = 2; //[2, "second record"];
+ },
+ };
+
+ var count = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'always_reload\': True}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ count++;
+ if (args.method === 'name_get' && args.args[0] === 2) {
+ return Promise.resolve([[2, "hello world\nso much noise"]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(count, 2, "should have done 2 rpcs (onchange and name_get)");
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'hello world',
+ "should have taken the correct display name");
+ form.destroy();
+ });
+
+ QUnit.test('selecting a many2one, then discarding', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('a[name=product_id]').text(), '', 'the tag a should be empty');
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickItem('product_id','xphone');
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), "xphone", "should have selected xphone");
+
+ await testUtils.form.clickDiscard(form);
+ assert.strictEqual(form.$('a[name=product_id]').text(), '', 'the tag a should be empty');
+ form.destroy();
+ });
+
+ QUnit.test('domain and context are correctly used when doing a name_search in a m2o', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="product_id" ' +
+ 'domain="[[\'foo\', \'=\', \'bar\'], [\'foo\', \'=\', foo]]" ' +
+ 'context="{\'hello\': \'world\', \'test\': foo}"/>' +
+ '<field name="foo"/>' +
+ '<field name="trululu" context="{\'timmy\': timmy}" domain="[[\'id\', \'in\', timmy]]"/>' +
+ '<field name="timmy" widget="many2many_tags" invisible="1"/>' +
+ '</form>',
+ res_id: 1,
+ session: { user_context: { hey: "ho" } },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search' && args.model === 'product') {
+ assert.deepEqual(
+ args.kwargs.args,
+ [['foo', '=', 'bar'], ['foo', '=', 'yop']],
+ 'the field attr domain should have been used for the RPC (and evaluated)');
+ assert.deepEqual(
+ args.kwargs.context,
+ { hey: "ho", hello: "world", test: "yop" },
+ 'the field attr context should have been used for the ' +
+ 'RPC (evaluated and merged with the session one)');
+ return Promise.resolve([]);
+ }
+ if (args.method === 'name_search' && args.model === 'partner') {
+ assert.deepEqual(args.kwargs.args, [['id', 'in', [12]]],
+ 'the field attr domain should have been used for the RPC (and evaluated)');
+ assert.deepEqual(args.kwargs.context, { hey: 'ho', timmy: [[6, false, [12]]] },
+ 'the field attr context should have been used for the RPC (and evaluated)');
+ return Promise.resolve([]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ testUtils.dom.click(form.$('.o_field_widget[name=product_id] input'));
+
+ testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('quick create on a many2one', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id"/>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/product/name_create') {
+ assert.strictEqual(args.args[0], 'new partner',
+ "should name create a new product");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.triggerEvent(form.$('.o_field_many2one input'),'focus');
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'new partner', ['keyup', 'blur']);
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary').first());
+ assert.strictEqual($('.modal .modal-body').text().trim(), "Do you want to create new partner as a new Product?");
+
+ form.destroy();
+ });
+
+ QUnit.test('failing quick create on a many2one', async function (assert) {
+ assert.expect(4);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="product_id"/></form>',
+ archs: {
+ 'product,false,form': '<form><field name="name"/></form>',
+ },
+ mockRPC(route, args) {
+ if (args.method === 'name_create') {
+ return Promise.reject();
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], { name: 'xyz' });
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ await testUtils.fields.many2one.searchAndClickItem('product_id', {
+ search: 'abcd',
+ item: 'Create "abcd"',
+ });
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.strictEqual($('.o_field_widget[name=name]').val(), 'abcd');
+
+ await testUtils.fields.editInput($('.modal .o_field_widget[name=name]'), 'xyz');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xyz');
+
+ form.destroy();
+ });
+
+ QUnit.test('failing quick create on a many2one inside a one2many', async function (assert) {
+ assert.expect(4);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="p"/></form>',
+ archs: {
+ 'partner,false,list': '<tree editable="bottom"><field name="product_id"/></tree>',
+ 'product,false,form': '<form><field name="name"/></form>',
+ },
+ mockRPC(route, args) {
+ if (args.method === 'name_create') {
+ return Promise.reject();
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], { name: 'xyz' });
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.many2one.searchAndClickItem('product_id', {
+ search: 'abcd',
+ item: 'Create "abcd"',
+ });
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.strictEqual($('.o_field_widget[name=name]').val(), 'abcd');
+
+ await testUtils.fields.editInput($('.modal .o_field_widget[name=name]'), 'xyz');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xyz');
+
+ form.destroy();
+ });
+
+ QUnit.test('slow create on a many2one', async function (assert) {
+ assert.expect(11);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<sheet>' +
+ '<field name="product_id" options="{\'quick_create\': False}"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'product,false,form':
+ '<form>' +
+ '<field name="name"/>' +
+ '</form>',
+ },
+ });
+
+ // cancel the many2one creation with Cancel button
+ form.$('.o_field_many2one input').focus().val('new product').trigger('keyup').trigger('blur');
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal').length, 1, "there should be one opened modal");
+
+ await testUtils.dom.click($('.modal .modal-footer .btn:contains(Cancel)'));
+ assert.strictEqual($('.modal').length, 0, "the modal should be closed");
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "",
+ 'the many2one should not set a value as its creation has been cancelled (with Cancel button)');
+
+ // cancel the many2one creation with Close button
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'new product', ['keyup', 'blur']);
+ assert.strictEqual($('.modal').length, 1, "there should be one opened modal");
+ await testUtils.dom.click($('.modal .modal-header button'));
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "",
+ 'the many2one should not set a value as its creation has been cancelled (with Close button)');
+ assert.strictEqual($('.modal').length, 0, "the modal should be closed");
+
+ // select a new value then cancel the creation of the new one --> restore the previous
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickItem('product_id','o');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "xphone", "should have selected xphone");
+
+ form.$('.o_field_many2one input').focus().val('new product').trigger('keyup').trigger('blur');
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal').length, 1, "there should be one opened modal");
+
+ await testUtils.dom.click($('.modal .modal-footer .btn:contains(Cancel)'));
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "xphone",
+ 'should have restored the many2one with its previous selected value (xphone)');
+
+ // confirm the many2one creation
+ form.$('.o_field_many2one input').focus().val('new partner').trigger('keyup').trigger('blur');
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal').length, 1, "there should be one opened modal");
+
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:contains(Create and edit)'));
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal .o_form_view').length, 1,
+ 'a new modal should be opened and contain a form view');
+
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ form.destroy();
+ });
+
+ QUnit.test('no_create option on a many2one', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id" options="{\'no_create\': True}"/>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'new partner', ['keyup', 'focusout']);
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal').length, 0, "should not display the create modal");
+ form.destroy();
+ });
+
+ QUnit.test('can_create and can_write option on a many2one', async function (assert) {
+ assert.expect(5);
+
+ this.data.product.options = {
+ can_create: "false",
+ can_write: "false",
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="product_id" can_create="false" can_write="false"/>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'product,false,form': '<form string="Products"><field name="display_name"/></form>',
+ },
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/product/get_formview_id') {
+ return Promise.resolve(false);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_many2one input'));
+ assert.strictEqual($('.ui-autocomplete .o_m2o_dropdown_option:contains(Create)').length, 0,
+ "there shouldn't be any option to search and create");
+
+ await testUtils.dom.click($('.ui-autocomplete li:contains(xpad)').mouseenter());
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "xpad",
+ "the correct record should be selected");
+ assert.containsOnce(form, '.o_field_many2one .o_external_button',
+ "there should be an external button displayed");
+
+ await testUtils.dom.click(form.$('.o_field_many2one .o_external_button'));
+ assert.strictEqual($('.modal .o_form_view.o_form_readonly').length, 1,
+ "there should be a readonly form view opened");
+
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'new product', ['keyup', 'focusout']);
+ assert.strictEqual($('.modal').length, 0, "should not display the create modal");
+ form.destroy();
+ });
+
+ QUnit.test('pressing enter in a m2o in an editable list', async function (assert) {
+ assert.expect(9);
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="product_id"/></tree>',
+ });
+
+ await testUtils.dom.click(list.$('td.o_data_cell:first'));
+ assert.containsOnce(list, '.o_selected_row',
+ "should have a row in edit mode");
+
+ // we now write 'a' and press enter to check that the selection is
+ // working, and prevent the navigation
+ await testUtils.fields.editInput(list.$('td.o_data_cell input:first'), 'a');
+ var $input = list.$('td.o_data_cell input:first');
+ var $dropdown = $input.autocomplete('widget');
+ assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible");
+
+ // we now trigger ENTER to select first choice
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ assert.strictEqual($input[0], document.activeElement,
+ "input should still be focused");
+
+ // we now trigger again ENTER to make sure we can move to next line
+ await testUtils.fields.triggerKeydown($input, 'enter');
+
+ assert.notOk(document.contains($input[0]),
+ "input should no longer be in dom");
+ assert.hasClass(list.$('tr.o_data_row:eq(1)'),'o_selected_row',
+ "second row should now be selected");
+
+ // we now write again 'a' in the cell to select xpad. We will now
+ // test with the tab key
+ await testUtils.fields.editInput(list.$('td.o_data_cell input:first'), 'a');
+ var $input = list.$('td.o_data_cell input:first');
+ var $dropdown = $input.autocomplete('widget');
+ assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible");
+ await testUtils.fields.triggerKeydown($input, 'tab');
+ assert.strictEqual($input[0], document.activeElement,
+ "input should still be focused");
+
+ // we now trigger again ENTER to make sure we can move to next line
+ await testUtils.fields.triggerKeydown($input, 'tab');
+
+ assert.notOk(document.contains($input[0]),
+ "input should no longer be in dom");
+ assert.hasClass(list.$('tr.o_data_row:eq(2)'),'o_selected_row',
+ "third row should now be selected");
+ list.destroy();
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ });
+
+ QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should open a M2ODialog', async function (assert) {
+ assert.expect(2);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'no_quick_create\': True}"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="display_name"/></form>',
+ },
+ });
+
+ var $input = form.$('.o_field_many2one input');
+ await testUtils.fields.editInput($input, "Something that does not exist");
+ $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter');
+ await testUtils.nextTick();
+ await testUtils.fields.triggerKey('down', $input, 'enter')
+ await testUtils.fields.triggerKey('press', $input, 'enter')
+ await testUtils.fields.triggerKey('up', $input, 'enter')
+ $input.blur();
+ assert.strictEqual($('.modal').length, 1,
+ "should have one modal in body");
+ // Check that discarding clears $input
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+ assert.strictEqual($input.val(), '',
+ "the field should be empty");
+ form.destroy();
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ });
+
+ QUnit.test('select a value by pressing TAB on a many2one with onchange', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges.trululu = function () { };
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+ var prom = testUtils.makeTestPromise();
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu"/>' +
+ '<field name="display_name"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return prom.then(_.constant(result));
+ }
+ return result;
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var $input = form.$('.o_field_many2one input');
+ await testUtils.fields.editInput($input, "first");
+ await testUtils.fields.triggerKey('down', $input, 'tab');
+ await testUtils.fields.triggerKey('press', $input, 'tab');
+ await testUtils.fields.triggerKey('up', $input, 'tab');
+
+ // simulate a focusout (e.g. because the user clicks outside)
+ // before the onchange returns
+ form.$('.o_field_char').focus();
+
+ assert.strictEqual($('.modal').length, 0,
+ "there shouldn't be any modal in body");
+
+ // unlock the onchange
+ prom.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual($input.val(), 'first record',
+ "first record should have been selected");
+ assert.strictEqual($('.modal').length, 0,
+ "there shouldn't be any modal in body");
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(6);
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges.product_id = function (obj) {
+ obj.int_field = obj.product_id || 0;
+ };
+
+ var prom = testUtils.makeTestPromise();
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="product_id"/><field name="int_field"/></tree>',
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return prom.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(list.$('td.o_data_cell:first'));
+ await testUtils.fields.editInput(list.$('td.o_data_cell input:first'), 'a');
+ var $input = list.$('td.o_data_cell input:first');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.fields.triggerKey('up', $input, 'enter');
+ prom.resolve();
+ await testUtils.nextTick();
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ assert.strictEqual($('.modal').length, 0, "should not have any modal in DOM");
+ assert.verifySteps(['name_search', 'onchange', 'write', 'read']);
+ list.destroy();
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ });
+
+ QUnit.test('many2one in editable list + onchange, with enter, part 2 [REQUIRE FOCUS]', async function (assert) {
+ // this is the same test as the previous one, but the onchange is just
+ // resolved slightly later
+ assert.expect(6);
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges.product_id = function (obj) {
+ obj.int_field = obj.product_id || 0;
+ };
+
+ var prom = testUtils.makeTestPromise();
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="product_id"/><field name="int_field"/></tree>',
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return prom.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(list.$('td.o_data_cell:first'));
+ await testUtils.fields.editInput(list.$('td.o_data_cell input:first'), 'a');
+ var $input = list.$('td.o_data_cell input:first');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ await testUtils.fields.triggerKey('up', $input, 'enter');
+ await testUtils.fields.triggerKeydown($input, 'enter');
+ prom.resolve();
+ await testUtils.nextTick();
+ assert.strictEqual($('.modal').length, 0, "should not have any modal in DOM");
+ assert.verifySteps(['name_search', 'onchange', 'write', 'read']);
+ list.destroy();
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ });
+
+ QUnit.test('many2one: domain updated by an onchange', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ int_field: function () { },
+ };
+
+ var domain = [];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return Promise.resolve({
+ domain: {
+ trululu: domain,
+ unexisting_field: domain,
+ }
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // trigger a name_search (domain should be [])
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+ // close the dropdown
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+ // trigger an onchange that will update the domain
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+ // trigger a name_search (domain should be [['id', 'in', [10]]])
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one in one2many: domain updated by an onchange', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ trululu: function () { },
+ };
+
+ var domain = [];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ domain: {
+ trululu: domain,
+ },
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // add a first row with a specific domain for the m2o
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+
+ // add a second row with another domain for the m2o
+ domain = [['id', 'in', [5]]]; // domain for subrecord 2
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+
+ // check again the first row to ensure that the domain hasn't change
+ domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
+ await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell'));
+ await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('search more in many2one: no text in input', async function (assert) {
+ // when the user clicks on 'Search More...' in a many2one dropdown, and there is no text
+ // in the input (i.e. no value to search on), we bypass the name_search that is meant to
+ // return a list of preselected ids to filter on in the list view (opened in a dialog)
+ assert.expect(6);
+
+ for (var i = 0; i < 8; i++) {
+ this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/></form>',
+ archs: {
+ 'partner,false,list': '<list><field name="display_name"/></list>',
+ 'partner,false,search': '<search></search>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [],
+ "should not preselect ids as there as nothing in the m2o input");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {
+ item: 'Search More',
+ search: '',
+ });
+
+ assert.verifySteps([
+ 'onchange',
+ 'name_search', // to display results in the dropdown
+ 'load_views', // list view in dialog
+ '/web/dataset/search_read', // to display results in the dialog
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('search more in many2one: text in input', async function (assert) {
+ // when the user clicks on 'Search More...' in a many2one dropdown, and there is some
+ // text in the input, we perform a name_search to get a (limited) list of preselected
+ // ids and we add a dynamic filter (with those ids) to the search view in the dialog, so
+ // that the user can remove this filter to bypass the limit
+ assert.expect(12);
+
+ for (var i = 0; i < 8; i++) {
+ this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
+ }
+
+ var expectedDomain;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/></form>',
+ archs: {
+ 'partner,false,list': '<list><field name="display_name"/></list>',
+ 'partner,false,search': '<search></search>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, expectedDomain);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ expectedDomain = [['id', 'in', [100, 101, 102, 103, 104, 105, 106, 107]]];
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {
+ item: 'Search More',
+ search: 'test',
+ });
+
+ assert.containsOnce(document.body, '.modal .o_list_view');
+ assert.containsOnce(document.body, '.modal .o_cp_searchview .o_facet_values',
+ "should have a special facet for the pre-selected ids");
+
+ // remove the filter on ids
+ expectedDomain = [];
+ await testUtils.dom.click($('.modal .o_cp_searchview .o_facet_remove'));
+
+ assert.verifySteps([
+ 'onchange',
+ 'name_search', // empty search, triggered when the user clicks in the input
+ 'name_search', // to display results in the dropdown
+ 'name_search', // to get preselected ids matching the search
+ 'load_views', // list view in dialog
+ '/web/dataset/search_read', // to display results in the dialog
+ '/web/dataset/search_read', // after removal of dynamic filter
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('search more in many2one: dropdown click', async function (assert) {
+ assert.expect(8);
+
+ for (let i = 0; i < 8; i++) {
+ this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
+ }
+
+ // simulate modal-like element rendered by the field html
+ const $fakeDialog = $(`<div>
+ <div class="pouet">
+ <div class="modal"></div>
+ </div>
+ </div>`);
+ $('body').append($fakeDialog);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/></form>',
+ archs: {
+ 'partner,false,list': '<list><field name="display_name"/></list>',
+ 'partner,false,search': '<search></search>',
+ },
+ });
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {
+ item: 'Search More',
+ search: 'test',
+ });
+
+ // dropdown selector
+ let filterMenuCss = '.o_search_options > .o_filter_menu';
+ let groupByMenuCss = '.o_search_options > .o_group_by_menu';
+
+ await testUtils.dom.click(document.querySelector(`${filterMenuCss} > .o_dropdown_toggler_btn`));
+
+ assert.hasClass(document.querySelector(filterMenuCss), 'show');
+ assert.isVisible(document.querySelector(`${filterMenuCss} > .dropdown-menu`),
+ "the filter dropdown menu should be visible");
+ assert.doesNotHaveClass(document.querySelector(groupByMenuCss), 'show');
+ assert.isNotVisible(document.querySelector(`${groupByMenuCss} > .dropdown-menu`),
+ "the Group by dropdown menu should be not visible");
+
+ await testUtils.dom.click(document.querySelector(`${groupByMenuCss} > .o_dropdown_toggler_btn`));
+ assert.hasClass(document.querySelector(groupByMenuCss), 'show');
+ assert.isVisible(document.querySelector(`${groupByMenuCss} > .dropdown-menu`),
+ "the group by dropdown menu should be visible");
+ assert.doesNotHaveClass(document.querySelector(filterMenuCss), 'show');
+ assert.isNotVisible(document.querySelector(`${filterMenuCss} > .dropdown-menu`),
+ "the filter dropdown menu should be not visible");
+
+ $fakeDialog.remove();
+ form.destroy();
+ });
+
+ QUnit.test('updating a many2one from a many2many', async function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.records[1].turtle_trululu = 1;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form string="Trululu"><field name="display_name"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id");
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // Opening the modal
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_row td:contains(first record)'));
+ await testUtils.dom.click(form.$('.o_external_button'));
+ assert.strictEqual($('.modal').length, 1,
+ "should have one modal in body");
+
+ // Changing the 'trululu' value
+ await testUtils.fields.editInput($('.modal input[name="display_name"]'), 'test');
+ await testUtils.dom.click($('.modal button.btn-primary'));
+
+ // Test whether the value has changed
+ assert.strictEqual($('.modal').length, 0,
+ "the modal should be closed");
+ assert.equal(form.$('.o_data_cell:contains(test)').text(), 'test',
+ "the partner name should have been updated to 'test'");
+
+ form.destroy();
+ });
+
+ QUnit.test('search more in many2one: resequence inside dialog', async function (assert) {
+ // when the user clicks on 'Search More...' in a many2one dropdown, resequencing inside
+ // the dialog works
+ assert.expect(10);
+
+ this.data.partner.fields.sequence = { string: 'Sequence', type: 'integer' };
+ for (var i = 0; i < 8; i++) {
+ this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/></form>',
+ archs: {
+ 'partner,false,list': '<list>' +
+ '<field name="sequence" widget="handle"/>' +
+ '<field name="display_name"/>' +
+ '</list>',
+ 'partner,false,search': '<search></search>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (route === '/web/dataset/search_read') {
+ assert.deepEqual(args.domain, [],
+ "should not preselect ids as there as nothing in the m2o input");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {
+ item: 'Search More',
+ search: '',
+ });
+
+ var $modal = $('.modal');
+ assert.equal($modal.length, 1,
+ 'There should be 1 modal opened');
+
+ var $handles = $modal.find('.ui-sortable-handle');
+ assert.equal($handles.length, 11,
+ 'There should be 11 sequence handlers');
+
+ await testUtils.dom.dragAndDrop($handles.eq(1),
+ $modal.find('tbody tr').first(), { position: 'top' });
+
+ assert.verifySteps([
+ 'onchange',
+ 'name_search', // to display results in the dropdown
+ 'load_views', // list view in dialog
+ '/web/dataset/search_read', // to display results in the dialog
+ '/web/dataset/resequence', // resequencing lines
+ 'read',
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one dropdown disappears on scroll', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<div style="height: 2000px;">' +
+ '<field name="trululu"/>' +
+ '</div>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var $input = form.$('.o_field_many2one input');
+
+ await testUtils.dom.click($input);
+ assert.isVisible($input.autocomplete('widget'), "dropdown should be opened");
+
+ form.el.dispatchEvent(new Event('scroll'));
+ assert.isNotVisible($input.autocomplete('widget'), "dropdown should be closed");
+
+ form.destroy();
+ });
+
+ QUnit.test('x2many list sorted by many2one', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [1, 2, 4];
+ this.data.partner.fields.trululu.sortable = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="id"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '124',
+ "should have correct order initially");
+
+ await testUtils.dom.click(form.$('.o_list_view thead th:nth(1)'));
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '412',
+ "should have correct order (ASC)");
+
+ await testUtils.dom.click(form.$('.o_list_view thead th:nth(1)'));
+
+ assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '214',
+ "should have correct order (DESC)");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with extra field from server not in form', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form>' +
+ '<field name="display_name"/>' +
+ '</form>'},
+ mockRPC: function(route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ args.args[1].p[0][2].datetime = '2018-04-05 12:00:00';
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a record in the list
+ await testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a'));
+
+ var modal = $('.modal-lg');
+
+ var nameInput = modal.find('input.o_input[name=display_name]');
+ await testUtils.fields.editInput(nameInput, 'michelangelo');
+
+ // Save the record in the modal (though it is still virtual)
+ await testUtils.dom.click(modal.find('.btn-primary').first());
+
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ var newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '',
+ 'The create_date field should be empty');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ // Save the whole thing
+ await testUtils.form.clickSave(form);
+
+ x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Redo asserts in RO mode after saving
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00',
+ 'The create_date field should have the right value');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with extra field from server not in (inline) form', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a record in the list
+ await testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a'));
+
+ var modal = $('.modal-lg');
+
+ var nameInput = modal.find('input.o_input[name=display_name]');
+ await testUtils.fields.editInput(nameInput, 'michelangelo');
+
+ // Save the record in the modal (though it is still virtual)
+ await testUtils.dom.click(modal.find('.btn-primary').first());
+
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with extra X2many field from server not in inline form', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="turtles"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a first record in the list
+ await testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a'));
+
+ // Save & New
+ await testUtils.dom.click($('.modal-lg').find('.btn-primary').eq(1));
+
+ // Save & Close
+ await testUtils.dom.click($('.modal-lg').find('.btn-primary').eq(0));
+
+ assert.equal(x2mList.find('.o_data_row').length, 2,
+ 'There should be 2 records in the x2m list');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many invisible depends on parent field', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.test('one2many column visiblity depends on onchange of parent field', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[0].bar = false;
+
+ this.data.partner.onchanges.p = function (obj) {
+ // set bar to true when line is added
+ if (obj.p.length > 1 && obj.p[1][2].foo === 'New line') {
+ obj.bar = true;
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form>' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // bar is false so there should be 1 column
+ assert.containsOnce(form, 'th',
+ "should be only 1 column ('foo') in the one2many");
+ assert.containsOnce(form, '.o_list_view .o_data_row', "should contain one row");
+
+ await testUtils.form.clickEdit(form);
+
+ // add a new o2m record
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ form.$('.o_field_one2many input:first').focus();
+ await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line');
+ await testUtils.dom.click(form.$el);
+
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns('foo' + 'int_field')");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many column_invisible on view not inline', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>',
+ },
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.module('Many2OneAvatar');
+
+ QUnit.test('many2one_avatar widget in form view', async function (assert) {
+ assert.expect(10);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="user_id" widget="many2one_avatar"/></form>',
+ res_id: 1,
+ });
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_readonly');
+ assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Aline');
+ assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+ assert.containsOnce(form, '.o_input_dropdown');
+ assert.strictEqual(form.$('.o_input_dropdown input').val(), 'Aline');
+ assert.containsOnce(form, '.o_external_button');
+
+ await testUtils.fields.many2one.clickOpenDropdown("user_id");
+ await testUtils.fields.many2one.clickItem("user_id", "Christine");
+ await testUtils.form.clickSave(form);
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_readonly');
+ assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Christine');
+ assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one_avatar widget in form view, with onchange', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ if (obj.int_field === 1) {
+ obj.user_id = [19, 'Christine'];
+ } else if (obj.int_field === 2) {
+ obj.user_id = false;
+ } else {
+ obj.user_id = [17, 'Aline']; // default value
+ }
+ },
+ };
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="int_field"/>
+ <field name="user_id" widget="many2one_avatar" readonly="1"/>
+ </form>`,
+ });
+
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+ assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Aline');
+ assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]');
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 1);
+
+ assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), 'Christine');
+ assert.containsOnce(form, 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]');
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+
+ assert.strictEqual(form.$('.o_field_widget[name=user_id]').text().trim(), '');
+ assert.containsNone(form, 'img.o_m2o_avatar');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one_avatar widget in list view', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records = [
+ { id: 1, user_id: 17, },
+ { id: 2, user_id: 19, },
+ { id: 3, user_id: 17, },
+ { id: 4, user_id: false, },
+ ];
+ const list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree><field name="user_id" widget="many2one_avatar"/></tree>',
+ });
+
+ assert.strictEqual(list.$('.o_data_cell span').text(), 'AlineChristineAline');
+ assert.containsOnce(list.$('.o_data_cell:nth(0)'), 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]');
+ assert.containsOnce(list.$('.o_data_cell:nth(1)'), 'img.o_m2o_avatar[data-src="/web/image/user/19/image_128"]');
+ assert.containsOnce(list.$('.o_data_cell:nth(2)'), 'img.o_m2o_avatar[data-src="/web/image/user/17/image_128"]');
+ assert.containsNone(list.$('.o_data_cell:nth(3)'), 'img.o_m2o_avatar');
+
+ list.destroy();
+ });
+ });
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
new file mode 100644
index 00000000..a2531f54
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
@@ -0,0 +1,9959 @@
+odoo.define('web.field_one_to_many_tests', function (require) {
+"use strict";
+
+var AbstractField = require('web.AbstractField');
+var AbstractStorageService = require('web.AbstractStorageService');
+const ControlPanel = require('web.ControlPanel');
+const fieldRegistry = require('web.field_registry');
+var FormView = require('web.FormView');
+var KanbanRecord = require('web.KanbanRecord');
+var ListRenderer = require('web.ListRenderer');
+var NotificationService = require('web.NotificationService');
+var RamStorage = require('web.RamStorage');
+var relationalFields = require('web.relational_fields');
+var testUtils = require('web.test_utils');
+var fieldUtils = require('web.field_utils');
+
+const cpHelpers = testUtils.controlPanel;
+var createView = testUtils.createView;
+const { FieldOne2Many } = relationalFields;
+
+QUnit.module('fields', {}, function () {
+
+ QUnit.module('relational_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: { string: "Foo", type: "char", default: "My little Foo Value" },
+ bar: { string: "Bar", type: "boolean", default: true },
+ int_field: { string: "int_field", type: "integer", sortable: true },
+ qux: { string: "Qux", type: "float", digits: [16, 1] },
+ p: { string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu' },
+ turtles: { string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu' },
+ trululu: { string: "Trululu", type: "many2one", relation: 'partner' },
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type' },
+ product_id: { string: "Product", type: "many2one", relation: 'product' },
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ string: "Color",
+ },
+ date: { string: "Some Date", type: "date" },
+ datetime: { string: "Datetime Field", type: 'datetime' },
+ user_id: { string: "User", type: 'many2one', relation: 'user' },
+ reference: {
+ string: "Reference Field", type: 'reference', selection: [
+ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]
+ },
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44,
+ p: [],
+ turtles: [2],
+ timmy: [],
+ trululu: 4,
+ user_id: 17,
+ reference: 'product,37',
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ qux: 13,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ product_id: 37,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ bar: false,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: { string: "Product Name", type: "char" }
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: { string: "Partner Type", type: "char" },
+ color: { string: "Color index", type: "integer" },
+ },
+ records: [
+ { id: 12, display_name: "gold", color: 2 },
+ { id: 14, display_name: "silver", color: 5 },
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: { string: "Foo", type: "char" },
+ turtle_bar: { string: "Bar", type: "boolean", default: true },
+ turtle_int: { string: "int", type: "integer", sortable: true },
+ turtle_qux: { string: "Qux", type: "float", digits: [16, 1], required: true, default: 1.5 },
+ turtle_description: { string: "Description", type: "text" },
+ turtle_trululu: { string: "Trululu", type: "many2one", relation: 'partner' },
+ turtle_ref: {
+ string: "Reference", type: 'reference', selection: [
+ ["product", "Product"], ["partner", "Partner"]]
+ },
+ product_id: { string: "Product", type: "many2one", relation: 'product', required: true },
+ partner_ids: { string: "Partner", type: "many2many", relation: 'partner' },
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_bar: true,
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_bar: true,
+ turtle_foo: "blip",
+ turtle_int: 9,
+ partner_ids: [2, 4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ product_id: 37,
+ turtle_bar: false,
+ turtle_foo: "kawa",
+ turtle_int: 21,
+ turtle_qux: 9.8,
+ partner_ids: [],
+ turtle_ref: 'product,37',
+ }],
+ onchanges: {},
+ },
+ user: {
+ fields: {
+ name: { string: "Name", type: "char" },
+ partner_ids: { string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id' },
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ partner_ids: [1, 2],
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ }
+ }, function () {
+ QUnit.module('FieldOne2Many');
+
+ QUnit.test('New record with a o2m also with 2 new records, ordered, and resequenced', async function (assert) {
+ assert.expect(2);
+
+ // Needed to have two new records in a single stroke
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.p = [
+ [5],
+ [0, 0, { trululu: false }],
+ [0, 0, { trululu: false }],
+ ];
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo" />' +
+ '<field name="p">' +
+ '<tree editable="bottom" default_order="int_field">' +
+ '<field name="int_field" widget="handle"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ viewOptions: {
+ mode: 'create',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method + ' ' + args.model);
+ return this._super(route, args);
+ },
+ });
+
+ // change the int_field through drag and drop
+ // that way, we'll trigger the sorting and the name_get
+ // of the lines of "p"
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.verifySteps(['onchange partner']);
+
+ form.destroy();
+ });
+
+ QUnit.test('O2M List with pager, decoration and default_order: add and cancel adding', async function (assert) {
+ assert.expect(3);
+
+ // The decoration on the list implies that its condition will be evaluated
+ // against the data of the field (actual records *displayed*)
+ // If one data is wrongly formed, it will crash
+ // This test adds then cancels a record in a paged, ordered, and decorated list
+ // That implies prefetching of records for sorting
+ // and evaluation of the decoration against *visible records*
+
+ this.data.partner.records[0].p = [2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom" limit="1" decoration-muted="foo != False" default_order="display_name">' +
+ '<field name="foo" invisible="1"/>' +
+ '<field name="display_name" />' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list .o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_field_x2many_list .o_data_row', 2,
+ 'There should be 2 rows');
+
+ var $expectedSelectedRow = form.$('.o_field_x2many_list .o_data_row').eq(1);
+ var $actualSelectedRow = form.$('.o_selected_row');
+ assert.equal($actualSelectedRow[0], $expectedSelectedRow[0],
+ 'The selected row should be the new one');
+
+ // Cancel Creation
+ await testUtils.fields.triggerKeydown($actualSelectedRow.find('input'), 'escape');
+ assert.containsOnce(form, '.o_field_x2many_list .o_data_row',
+ 'There should be 1 row');
+
+ form.destroy();
+ });
+
+ QUnit.test('O2M with parented m2o and domain on parent.m2o', async function (assert) {
+ assert.expect(4);
+
+ /* records in an o2m can have a m2o pointing to themselves
+ * in that case, a domain evaluation on that field followed by name_search
+ * shouldn't send virtual_ids to the server
+ */
+
+ this.data.turtle.fields.parent_id = { string: "Parent", type: "many2one", relation: 'turtle' };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="parent_id" />' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'turtle,false,form': '<form><field name="parent_id" domain="[(\'id\', \'in\', parent.turtles)]"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/turtle/name_search') {
+ // We are going to pass twice here
+ // First time, we really have nothing
+ // Second time, a virtual_id has been created
+ assert.deepEqual(args.kwargs.args, [['id', 'in', []]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list[name=turtles] .o_field_x2many_list_row_add a'));
+
+ await testUtils.fields.many2one.createAndEdit('parent_id');
+
+ var $modal = $('.modal-content');
+
+ await testUtils.dom.click($modal.eq(1).find('.modal-footer .btn-primary').eq(0));
+ await testUtils.dom.click($modal.eq(0).find('.modal-footer .btn-primary').eq(1));
+
+ assert.containsOnce(form, '.o_data_row',
+ 'The main record should have the new record in its o2m');
+
+ $modal = $('.modal-content');
+ await testUtils.dom.click($modal.find('.o_field_many2one input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable with cell readonly modifier', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].turtles = [1, 2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="turtles" invisible="1"/>' +
+ '<field name="foo" attrs="{&quot;readonly&quot; : [(&quot;turtles&quot;, &quot;!=&quot;, [])] }"/>' +
+ '<field name="qux" attrs="{&quot;readonly&quot; : [(&quot;turtles&quot;, &quot;!=&quot;, [])] }"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.deepEqual(args.args[1].p[1][2], { foo: 'ff', qux: 99 },
+ 'The right values should be written');
+ }
+ return this._super(route, args);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ var $targetInput = $('.o_selected_row .o_input[name=foo]');
+ assert.equal($targetInput[0], document.activeElement,
+ 'The first input of the line should have the focus');
+
+ // Simulating hitting the 'f' key twice
+ await testUtils.fields.editInput($targetInput, 'f');
+ await testUtils.fields.editInput($targetInput, $targetInput.val() + 'f');
+
+ assert.equal($targetInput[0], document.activeElement,
+ 'The first input of the line should still have the focus');
+
+ // Simulating a TAB key
+ await testUtils.fields.triggerKeydown($targetInput, 'tab');
+
+ var $secondTarget = $('.o_selected_row .o_input[name=qux]');
+
+ assert.equal($secondTarget[0], document.activeElement,
+ 'The second input of the line should have the focus after the TAB press');
+
+
+ await testUtils.fields.editInput($secondTarget, 9);
+ await testUtils.fields.editInput($secondTarget, $secondTarget.val() + 9);
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many basic properties', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ load_filters: function (event) {
+ throw new Error('Should not load filters');
+ },
+ },
+ });
+
+
+ assert.containsNone(form, 'td.o_list_record_selector',
+ "embedded one2many should not have a selector");
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ "embedded one2many should not be editable");
+ assert.ok(!form.$('td.o_list_record_remove').length,
+ "embedded one2many records should not have a remove icon");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ "embedded one2many should now be editable");
+
+ assert.hasAttrValue(form.$('.o_field_x2many_list_row_add'), 'colspan', "2",
+ "should have colspan 2 (one for field foo, one for being below remove icon)");
+
+ assert.ok(form.$('td.o_list_record_remove').length,
+ "embedded one2many records should have a remove icon");
+ form.destroy();
+ });
+
+ QUnit.test('transferring class attributes in one2many sub fields', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo" class="hey"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'td.hey',
+ 'should have a td with the desired class');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, 'td.hey',
+ 'should have a td with the desired class');
+
+ await testUtils.dom.click(form.$('td.o_data_cell'));
+
+ assert.containsOnce(form, 'input[name="turtle_foo"].hey',
+ 'should have an input with the desired class');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with date and datetime', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="date"/>' +
+ '<field name="datetime"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ session: {
+ getTZOffset: function () {
+ return 120;
+ },
+ },
+ });
+ assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017",
+ "should have formatted the date");
+ assert.strictEqual(form.$('td:eq(1)').text(), "12/12/2016 12:55:05",
+ "should have formatted the datetime");
+ form.destroy();
+ });
+
+ QUnit.test('rendering with embedded one2many', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('th:contains(Foo)').length, 1,
+ "embedded one2many should have a column titled according to foo");
+ assert.strictEqual(form.$('td:contains(blip)').length, 1,
+ "embedded one2many should have a cell with relational value");
+ form.destroy();
+ });
+
+ QUnit.test('use the limit attribute in arch (in field o2m inline tree view)', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'turtle') {
+ assert.deepEqual(args.args[0], [1, 2],
+ 'should only load first 2 records');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2,
+ 'should display 2 data rows');
+ form.destroy();
+ });
+
+ QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles"/>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree limit="2"><field name="turtle_foo"/></tree>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.model === 'turtle' && args.method === 'read') {
+ assert.deepEqual(args.args[0], [1, 2],
+ 'should only load first 2 records');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2,
+ 'should display 2 data rows');
+ form.destroy();
+ });
+
+ QUnit.test('one2many with default_order on view not inline', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Turtles">' +
+ '<field name="turtles"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree default_order="turtle_foo">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>',
+ },
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('.o_field_one2many .o_list_view .o_data_row').text(), "9blip21kawa0yop",
+ "the default order should be correctly applied");
+ form.destroy();
+ });
+
+ QUnit.test('embedded one2many with widget', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="int_field" widget="handle"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, 'span.o_row_handle', "should have 1 handles");
+ form.destroy();
+ });
+
+ QUnit.test('embedded one2many with handle widget', async function (assert) {
+ assert.expect(10);
+
+ var nbConfirmChange = 0;
+ testUtils.mock.patch(ListRenderer, {
+ confirmChange: function () {
+ nbConfirmChange++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="turtles">' +
+ '<tree default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.mock.intercept(form, "field_changed", function (event) {
+ assert.step(event.data.changes.turtles.data.turtle_int.toString());
+ }, true);
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should have the 3 rows in the correct order");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should still have the 3 rows in the correct order");
+ assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet");
+
+ // Drag and drop the second line in first position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.strictEqual(nbConfirmChange, 1, "should have confirmed changes only once");
+ assert.verifySteps(["0", "1"],
+ "sequences values should be incremental starting from the previous minimum one");
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
+ "should have the 3 rows in the new order");
+
+ await testUtils.form.clickSave(form);
+
+ assert.deepEqual(_.map(this.data.turtle.records, function (turtle) {
+ return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int');
+ }), [
+ { id: 1, turtle_foo: "yop", turtle_int: 1 },
+ { id: 2, turtle_foo: "blip", turtle_int: 0 },
+ { id: 3, turtle_foo: "kawa", turtle_int: 21 }
+ ], "should have save the changed sequence");
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
+ "should still have the 3 rows in the new order");
+
+ testUtils.mock.unpatch(ListRenderer);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange for embedded one2many in a one2many with a second page', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.fields.partner_ids.type = 'one2many';
+ this.data.turtle.records[0].partner_ids = [1];
+ // we need a second page, so we set two records and only display one per page
+ this.data.partner.records[0].turtles = [1, 2];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, {
+ turtle_foo: "hop",
+ partner_ids: [[5], [4, 1]],
+ }],
+ [1, 2, {
+ turtle_foo: "blip",
+ partner_ids: [[5], [4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="1">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var expectedResultTurtles = [
+ [1, 1, {
+ turtle_foo: "hop",
+ }],
+ [1, 2, {
+ partner_ids: [[4, 2, false], [4, 4, false]],
+ turtle_foo: "blip",
+ }],
+ ];
+ assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
+ "the right values should be written");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_cell').eq(1));
+ var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
+ await testUtils.fields.editSelect($cell, "hop");
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange for embedded one2many in a one2many updated by server', async function (assert) {
+ // here we test that after an onchange, the embedded one2many field has
+ // been updated by a new list of ids by the server response, to this new
+ // list should be correctly sent back at save time
+ assert.expect(3);
+
+ this.data.turtle.fields.partner_ids.type = 'one2many';
+ this.data.partner.records[0].turtles = [2];
+ this.data.turtle.records[1].partner_ids = [2];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 2, {
+ turtle_foo: "hop",
+ partner_ids: [[5], [4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var expectedResultTurtles = [
+ [1, 2, {
+ partner_ids: [[4, 2, false], [4, 4, false]],
+ turtle_foo: "hop",
+ }],
+ ];
+ assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
+ 'The right values should be written');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim(), "second record",
+ "the partner_ids should be as specified at initialization");
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_cell').eq(1));
+ var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
+ await testUtils.fields.editSelect($cell, "hop");
+ await testUtils.form.clickSave(form);
+
+ assert.deepEqual(form.$('.o_data_cell.o_many2many_tags_cell').text().trim().split(/\s+/),
+ ["second", "record", "aaa"],
+ 'The partner_ids should have been updated');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange for embedded one2many with handle widget', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var partnerOnchange = 0;
+ this.data.partner.onchanges = {
+ turtles: function () {
+ partnerOnchange++;
+ },
+ };
+ var turtleOnchange = 0;
+ this.data.turtle.onchanges = {
+ turtle_int: function () {
+ turtleOnchange++;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="turtles">' +
+ '<tree default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // Drag and drop the second line in first position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.strictEqual(turtleOnchange, 2, "should trigger one onchange per line updated");
+ assert.strictEqual(partnerOnchange, 1, "should trigger only one onchange on the parent");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange for embedded one2many with handle widget using same sequence', async function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.records[0].turtle_int = 1;
+ this.data.turtle.records[1].turtle_int = 1;
+ this.data.turtle.records[2].turtle_int = 1;
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var turtleOnchange = 0;
+ this.data.turtle.onchanges = {
+ turtle_int: function () {
+ turtleOnchange++;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="turtles">' +
+ '<tree default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles, [[1, 2, { "turtle_int": 1 }], [1, 1, { "turtle_int": 2 }], [1, 3, { "turtle_int": 3 }]],
+ "should change all lines that have changed (the first one doesn't change because it has the same sequence)");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should have the 3 rows in the correct order");
+
+ // Drag and drop the second line in first position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
+ "should still have the 3 rows in the correct order");
+ assert.strictEqual(turtleOnchange, 3, "should update all lines");
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('onchange (with command 5) for embedded one2many with handle widget', async function (assert) {
+ assert.expect(3);
+
+ var ids = [];
+ for (var i = 10; i < 50; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.turtle.records.push({
+ id: id,
+ turtle_int: 0,
+ turtle_foo: "#" + id,
+ });
+ }
+ ids.push(1, 2, 3);
+ this.data.partner.records[0].turtles = ids;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
+ "should have the 3 rows in the correct order");
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp');
+
+ // Drag and drop the third line in second position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ { position: 'top' }
+ );
+
+ assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order");
+
+ await testUtils.form.clickSave(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip",
+ "should display to record in 'turtle_int' order");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange with modifiers for embedded one2many on the second page', async function (assert) {
+ assert.expect(7);
+
+ var data = this.data;
+ var ids = [];
+ for (var i = 10; i < 60; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ data.turtle.records.push({
+ id: id,
+ turtle_int: 0,
+ turtle_foo: "#" + id,
+ });
+ }
+ ids.push(1, 2, 3);
+ data.partner.records[0].turtles = ids;
+ data.partner.onchanges = {
+ turtles: function (obj) {
+ // TODO: make this test more 'difficult'
+ // For now, the server only returns UPDATE commands (no LINK TO)
+ // even though it should do it (for performance reasons)
+ // var turtles = obj.turtles.splice(0, 20);
+
+ var turtles = [];
+ turtles.unshift([5]);
+ // create UPDATE commands for each records (this is the server
+ // usual answer for onchange)
+ for (var k in obj.turtles) {
+ var change = obj.turtles[k];
+ var record = _.findWhere(data.turtle.records, { id: change[1] });
+ if (change[0] === 1) {
+ _.extend(record, change[2]);
+ }
+ turtles.push([1, record.id, record]);
+ }
+ obj.turtles = turtles;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" default_order="turtle_int" limit="10">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_qux" attrs="{\'readonly\': [(\'turtle_foo\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should display the records in order");
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp');
+
+ // the domain fail if the widget does not use the allready loaded data.
+ await testUtils.form.clickDiscard(form);
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "blurp#21#22#23#24#25#26#27#28#29",
+ "should display the records in order with the changes");
+
+ await testUtils.dom.click($('.modal .modal-footer button:first'));
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should cancel changes and display the records in order");
+
+ await testUtils.form.clickEdit(form);
+
+ // Drag and drop the third line in second position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ { position: 'top' }
+ );
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#30#31#32#33#34#35#36#37#38",
+ "should display the records in order after resequence (display record with turtle_int=0)");
+
+ // Drag and drop the third line in second position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ { position: 'top' }
+ );
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47",
+ "should display the records in order after resequence (display record with turtle_int=0)");
+
+ await testUtils.form.clickDiscard(form);
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#39#40#41#42#43#44#45#46#47",
+ "should display the records in order after resequence");
+
+ await testUtils.dom.click($('.modal .modal-footer button:first'));
+
+ assert.equal(form.$('.o_field_one2many .o_list_char').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should cancel changes and display the records in order");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange followed by edition on the second page', async function (assert) {
+ assert.expect(12);
+
+ var ids = [];
+ for (var i = 1; i < 85; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.turtle.records.push({
+ id: id,
+ turtle_int: id / 3 | 0,
+ turtle_foo: "#" + i,
+ });
+ }
+ ids.splice(41, 0, 1, 2, 3);
+ this.data.partner.records[0].turtles = ids;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top" default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1');
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2');
+
+ assert.containsN(form, '.o_data_row', 40, "should display 40 records");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line");
+ assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position");
+ assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line");
+
+ await testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3');
+
+ assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position after onchange");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line");
+ assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange followed by edition on the second page (part 2)', async function (assert) {
+ assert.expect(8);
+
+ var ids = [];
+ for (var i = 1; i < 85; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.turtle.records.push({
+ id: id,
+ turtle_int: id / 3 | 0,
+ turtle_foo: "#" + i,
+ });
+ }
+ ids.splice(41, 0, 1, 2, 3);
+ this.data.partner.records[0].turtles = ids;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
+
+ // bottom order
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1');
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2');
+
+ assert.containsN(form, '.o_data_row', 40, "should display 40 records");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 41, "should display 41 records and the create line");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
+ assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position");
+
+ await testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3');
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 42, "should display 42 records and the create line");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line");
+ assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange returning a command 6 for an x2many', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.turtles = [[6, false, [1, 2, 3]]];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should be one record in the relation");
+
+ // change the value of foo to trigger the onchange
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value');
+
+ assert.containsN(form, '.o_data_row', 3,
+ "there should be three records in the relation");
+
+ form.destroy();
+ });
+
+ QUnit.test('x2many fields inside x2manys are fetched after an onchange', async function (assert) {
+ assert.expect(6);
+
+ this.data.turtle.records[0].partner_ids = [1];
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.turtles = [[5], [4, 1], [4, 2], [4, 3]];
+ },
+ };
+
+ var checkRPC = false;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (checkRPC && args.method === 'read' && args.model === 'partner') {
+ assert.deepEqual(args.args[1], ['display_name'],
+ "should only read the display_name for the m2m tags");
+ assert.deepEqual(args.args[0], [1],
+ "should only read the display_name of the unknown record");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should be one record in the relation");
+ assert.strictEqual(form.$('.o_data_row .o_field_widget[name=partner_ids]').text().replace(/\s/g, ''),
+ 'secondrecordaaa', "many2many_tags should be correctly displayed");
+
+ // change the value of foo to trigger the onchange
+ checkRPC = true; // enable flag to check read RPC for the m2m field
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value');
+
+ assert.containsN(form, '.o_data_row', 3,
+ "there should be three records in the relation");
+ assert.strictEqual(form.$('.o_data_row:first .o_field_widget[name=partner_ids]').text().trim(),
+ 'first record', "many2many_tags should be correctly displayed");
+
+ form.destroy();
+ });
+
+ QUnit.test('reference fields inside x2manys are fetched after an onchange', async function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.records[1].turtle_ref = 'product,41';
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.turtles = [[5], [4, 1], [4, 2], [4, 3]];
+ },
+ };
+
+ var checkRPC = false;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_ref" class="ref_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (checkRPC && args.method === 'name_get') {
+ assert.deepEqual(args.args[0], [37],
+ "should only fetch the name_get of the unknown record");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should be one record in the relation");
+ assert.strictEqual(form.$('.ref_field').text().trim(), 'xpad',
+ "reference field should be correctly displayed");
+
+ // change the value of foo to trigger the onchange
+ checkRPC = true; // enable flag to check read RPC for reference field
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value');
+
+ assert.containsN(form, '.o_data_row', 3,
+ "there should be three records in the relation");
+ assert.strictEqual(form.$('.ref_field').text().trim(), 'xpadxphone',
+ "reference fields should be correctly displayed");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange on one2many containing x2many in form view', async function (assert) {
+ assert.expect(16);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.turtles = [[0, false, { turtle_foo: 'new record' }]];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="partner_ids">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="foo"/></tree>',
+ 'partner,false,search': '<search></search>',
+ },
+ });
+
+
+ assert.containsOnce(form, '.o_data_row',
+ "the onchange should have created one record in the relation");
+
+ // open the created o2m record in a form view, and add a m2m subrecord
+ // in its relation
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 0,
+ "there should be no record in the one2many in the dialog");
+
+ // add a many2many subrecord
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.modal').length, 2,
+ "should have opened a second dialog");
+
+ // select a many2many subrecord
+ await testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first'));
+
+ assert.strictEqual($('.modal').length, 1,
+ "second dialog should be closed");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+ assert.containsNone($('.modal'), '.o_x2m_control_panel .o_pager',
+ 'm2m pager should be hidden');
+
+ // click on 'Save & Close'
+ await testUtils.dom.click($('.modal-footer .btn-primary:first'));
+
+ assert.strictEqual($('.modal').length, 0, "dialog should be closed");
+
+ // reopen o2m record, and another m2m subrecord in its relation, but
+ // discard the changes
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+
+ // add another m2m subrecord
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.modal').length, 2,
+ "should have opened a second dialog");
+
+ await testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first'));
+
+ assert.strictEqual($('.modal').length, 1,
+ "second dialog should be closed");
+ assert.strictEqual($('.modal .o_data_row').length, 2,
+ "there should be two records in the one2many in the dialog");
+
+ // click on 'Discard'
+ await testUtils.dom.click($('.modal-footer .btn-secondary'));
+
+ assert.strictEqual($('.modal').length, 0, "dialog should be closed");
+
+ // reopen o2m record to check that second changes have properly been discarded
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.strictEqual($('.modal').length, 1, "should have opened a dialog");
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "there should be one record in the one2many in the dialog");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange on one2many with x2many in list (no widget) and form view (list)', async function (assert) {
+ assert.expect(6);
+
+ this.data.turtle.fields.turtle_foo.default = "a default value";
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: 'hello'}]] }]];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="turtles"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ });
+
+
+ assert.containsOnce(form, '.o_data_row',
+ "the onchange should have created one record in the relation");
+
+ // open the created o2m record in a form view
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal', "should have opened a dialog");
+ assert.containsOnce(document.body, '.modal .o_data_row');
+ assert.strictEqual($('.modal .o_data_row').text(), 'hello');
+
+ // add a one2many subrecord and check if the default value is correctly applied
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .o_data_row', 2);
+ assert.strictEqual($('.modal .o_data_row:first .o_field_widget[name=turtle_foo]').val(),
+ 'a default value');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange on one2many with x2many in list (many2many_tags) and form view (list)', async function (assert) {
+ assert.expect(6);
+
+ this.data.turtle.fields.turtle_foo.default = "a default value";
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.p = [[0, false, { turtles: [[0, false, { turtle_foo: 'hello'}]] }]];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="turtles" widget="many2many_tags"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ });
+
+
+ assert.containsOnce(form, '.o_data_row',
+ "the onchange should have created one record in the relation");
+
+ // open the created o2m record in a form view
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal', "should have opened a dialog");
+ assert.containsOnce(document.body, '.modal .o_data_row');
+ assert.strictEqual($('.modal .o_data_row').text(), 'hello');
+
+ // add a one2many subrecord and check if the default value is correctly applied
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ assert.containsN(document.body, '.modal .o_data_row', 2);
+ assert.strictEqual($('.modal .o_data_row:first .o_field_widget[name=turtle_foo]').val(),
+ 'a default value');
+
+ form.destroy();
+ });
+
+ QUnit.test('embedded one2many with handle widget with minimum setValue calls', async function (assert) {
+ var done = assert.async();
+ assert.expect(20);
+
+
+ this.data.turtle.records[0].turtle_int = 6;
+ this.data.turtle.records.push({
+ id: 4,
+ turtle_int: 20,
+ turtle_foo: "a1",
+ }, {
+ id: 5,
+ turtle_int: 9,
+ turtle_foo: "a2",
+ }, {
+ id: 6,
+ turtle_int: 2,
+ turtle_foo: "a3",
+ }, {
+ id: 7,
+ turtle_int: 11,
+ turtle_foo: "a4",
+ });
+ this.data.partner.records[0].turtles = [1, 2, 3, 4, 5, 6, 7];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="turtles">' +
+ '<tree default_order="turtle_int">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.mock.intercept(form, "field_changed", function (event) {
+ assert.step(String(form.model.get(event.data.changes.turtles.id).res_id));
+ }, true);
+
+ await testUtils.form.clickEdit(form);
+
+ var positions = [
+ [6, 0, 'top', ['3', '6', '1', '2', '5', '7', '4']], // move the last to the first line
+ [5, 1, 'top', ['7', '6', '1', '2', '5']], // move the penultimate to the second line
+ [2, 5, 'bottom', ['1', '2', '5', '6']], // move the third to the penultimate line
+ ];
+ async function dragAndDrop() {
+ var pos = positions.shift();
+
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(pos[0]),
+ form.$('tbody tr').eq(pos[1]),
+ { position: pos[2] }
+ );
+
+ assert.verifySteps(pos[3],
+ "sequences values should be apply from the begin index to the drop index");
+
+ if (positions.length) {
+
+ setTimeout(dragAndDrop, 10);
+
+ } else {
+
+ assert.deepEqual(_.pluck(form.model.get(form.handle).data.turtles.data, 'data'), [
+ { id: 3, turtle_foo: "kawa", turtle_int: 2 },
+ { id: 7, turtle_foo: "a4", turtle_int: 3 },
+ { id: 1, turtle_foo: "yop", turtle_int: 4 },
+ { id: 2, turtle_foo: "blip", turtle_int: 5 },
+ { id: 5, turtle_foo: "a2", turtle_int: 6 },
+ { id: 6, turtle_foo: "a3", turtle_int: 7 },
+ { id: 4, turtle_foo: "a1", turtle_int: 8 }
+ ], "sequences must be apply correctly");
+
+ form.destroy();
+ done();
+ }
+ }
+
+ dragAndDrop();
+ });
+
+ QUnit.test('embedded one2many (editable list) with handle widget', async function (assert) {
+ assert.expect(8);
+
+ this.data.partner.records[0].p = [1, 2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="P page">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="int_field" widget="handle"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ testUtils.mock.intercept(form, "field_changed", function (event) {
+ assert.step(event.data.changes.p.data.int_field.toString());
+ }, true);
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
+ "should have the 3 rows in the correct order");
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
+ "should still have the 3 rows in the correct order");
+
+ // Drag and drop the second line in first position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.verifySteps(["0", "1"],
+ "sequences values should be incremental starting from the previous minimum one");
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
+ "should have the 3 rows in the new order");
+
+ await testUtils.dom.click(form.$('tbody tr:first td:first'));
+
+ assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip",
+ "should edit the correct row");
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
+ "should still have the 3 rows in the new order");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many field when using the pager', async function (assert) {
+ assert.expect(13);
+
+ var ids = [];
+ for (var i = 0; i < 45; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.partner.records.push({
+ id: id,
+ display_name: "relational record " + id,
+ });
+ }
+ this.data.partner.records[0].p = ids.slice(0, 42);
+ this.data.partner.records[1].p = ids.slice(42);
+
+ var count = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><t t-esc="record.display_name"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ mockRPC: function () {
+ count++;
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ // we are on record 1, which has 90 related record (first 40 should be
+ // displayed), 2 RPCs (read) should have been done, one on the main record
+ // and one for the O2M
+ assert.strictEqual(count, 2, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'one2many kanban should contain 40 cards for record 1');
+
+ // move to record 2, which has 3 related records (and shouldn't contain the
+ // related records of record 1 anymore). Two additional RPCs should have
+ // been done
+ await cpHelpers.pagerNext(form);
+ assert.strictEqual(count, 4, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3,
+ 'one2many kanban should contain 3 cards for record 2');
+
+ // move back to record 1, which should contain again its first 40 related
+ // records
+ await cpHelpers.pagerPrevious(form);
+ assert.strictEqual(count, 6, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'one2many kanban should contain 40 cards for record 1');
+
+ // move to the second page of the o2m: 1 RPC should have been done to fetch
+ // the 2 subrecords of page 2, and those records should now be displayed
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next'));
+ assert.strictEqual(count, 7, 'one RPC should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2,
+ 'one2many kanban should contain 2 cards for record 1 at page 2');
+
+ // move to record 2 again and check that everything is correctly updated
+ await cpHelpers.pagerNext(form);
+ assert.strictEqual(count, 9, 'two RPCs should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3,
+ 'one2many kanban should contain 3 cards for record 2');
+
+ // move back to record 1 and move to page 2 again: all data should have
+ // been correctly reloaded
+ await cpHelpers.pagerPrevious(form);
+ assert.strictEqual(count, 11, 'two RPCs should have been done');
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next'));
+ assert.strictEqual(count, 12, 'one RPC should have been done');
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2,
+ 'one2many kanban should contain 2 cards for record 1 at page 2');
+ form.destroy();
+ });
+
+ QUnit.test('edition of one2many field with pager', async function (assert) {
+ assert.expect(31);
+
+ var ids = [];
+ for (var i = 0; i < 45; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.partner.records.push({
+ id: id,
+ display_name: "relational record " + id,
+ });
+ }
+ this.data.partner.records[0].p = ids;
+
+ var saveCount = 0;
+ var checkRead = false;
+ var readIDs;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="display_name"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && checkRead) {
+ readIDs = args.args[0];
+ checkRead = false;
+ }
+ if (args.method === 'write') {
+ saveCount++;
+ var nbCommands = args.args[1].p.length;
+ var nbLinkCommands = _.filter(args.args[1].p, function (command) {
+ return command[0] === 4;
+ }).length;
+ switch (saveCount) {
+ case 1:
+ assert.strictEqual(nbCommands, 46,
+ "should send 46 commands (one for each record)");
+ assert.strictEqual(nbLinkCommands, 45,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[45], [0, args.args[1].p[45][1], {
+ display_name: 'new record',
+ }], "should sent a CREATE command for the new record");
+ break;
+ case 2:
+ assert.strictEqual(nbCommands, 46,
+ "should send 46 commands");
+ assert.strictEqual(nbLinkCommands, 45,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[45], [2, 10, false],
+ "should sent a DELETE command for the deleted record");
+ break;
+ case 3:
+ assert.strictEqual(nbCommands, 47,
+ "should send 47 commands");
+ assert.strictEqual(nbLinkCommands, 43,
+ "should send a LINK_TO command for each existing record");
+ assert.deepEqual(args.args[1].p[43],
+ [0, args.args[1].p[43][1], { display_name: 'new record page 1' }],
+ "should sent correct CREATE command");
+ assert.deepEqual(args.args[1].p[44],
+ [0, args.args[1].p[44][1], { display_name: 'new record page 2' }],
+ "should sent correct CREATE command");
+ assert.deepEqual(args.args[1].p[45],
+ [2, 11, false],
+ "should sent correct DELETE command");
+ assert.deepEqual(args.args[1].p[46],
+ [2, 52, false],
+ "should sent correct DELETE command");
+ break;
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager range should be correct");
+
+ // add a record on page one
+ checkRead = true;
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ await testUtils.fields.editInput($('.modal input'), 'new record');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ // checks
+ assert.strictEqual(readIDs, undefined, "should not have read any record");
+ assert.strictEqual(form.$('span:contains(new record)').length, 0,
+ "new record should be on page 2");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 46', "pager range should be correct");
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length,
+ 0, 'new record should not be on page 1');
+ // save
+ await testUtils.form.clickSave(form);
+
+ // delete a record on page one
+ checkRead = true;
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length,
+ 1, 'first record should be the one with id 10 (next checks rely on that)');
+ await testUtils.dom.click(form.$('.delete_icon:first'));
+ // checks
+ assert.deepEqual(readIDs, [50],
+ "should have read a record (to display 40 records on page 1)");
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40,
+ 'there should be 40 records on page 1');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '1-40 / 45', "pager range should be correct");
+ // save
+ await testUtils.form.clickSave(form);
+
+ // add and delete records in both pages
+ await testUtils.form.clickEdit(form);
+ checkRead = true;
+ readIDs = undefined;
+ // add and delete a record in page 1
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ await testUtils.fields.editInput($('.modal input'), 'new record page 1');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length,
+ 1, 'first record should be the one with id 11 (next checks rely on that)');
+ await testUtils.dom.click(form.$('.delete_icon:first'));
+ assert.deepEqual(readIDs, [51],
+ "should have read a record (to display 40 records on page 1)");
+ // add and delete a record in page 2
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next'));
+ assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length,
+ 1, 'first record should be the one with id 52 (next checks rely on that)');
+ checkRead = true;
+ readIDs = undefined;
+ await testUtils.dom.click(form.$('.delete_icon:first'));
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ await testUtils.fields.editInput($('.modal input'), 'new record page 2');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ assert.strictEqual(readIDs, undefined, "should not have read any record");
+ // checks
+ assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5,
+ 'there should be 5 records on page 2');
+ assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(),
+ '41-45 / 45', "pager range should be correct");
+ assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length,
+ 1, 'new records should be on page 2');
+ assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length,
+ 1, 'new records should be on page 2');
+ // save
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('edition of one2many field, with onchange and not inline sub view', async function (assert) {
+ assert.expect(2);
+
+ this.data.turtle.onchanges.turtle_int = function (obj) {
+ obj.turtle_foo = String(obj.turtle_int);
+ };
+ this.data.partner.onchanges.turtles = function () { };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles"/>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="turtle_foo"/></tree>',
+ 'turtle,false,form': '<form><group><field name="turtle_foo"/><field name="turtle_int"/></group></form>',
+ },
+ mockRPC: function () {
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput($('input[name="turtle_int"]'), '5');
+ await testUtils.dom.click($('.modal-footer button.btn-primary').first());
+ assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '5',
+ 'should display 5 in the foo field');
+ await testUtils.dom.click(form.$('tbody tr:eq(1) td.o_data_cell'));
+
+ await testUtils.fields.editInput($('input[name="turtle_int"]'), '3');
+ await testUtils.dom.click($('.modal-footer button.btn-primary').first());
+ assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '3',
+ 'should now display 3 in the foo field');
+ form.destroy();
+ });
+
+ QUnit.test('sorting one2many fields', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.foo.sortable = true;
+ this.data.partner.records.push({ id: 23, foo: "abc" });
+ this.data.partner.records.push({ id: 24, foo: "xyz" });
+ this.data.partner.records.push({ id: 25, foo: "def" });
+ this.data.partner.records[0].p = [23, 24, 25];
+
+ var rpcCount = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function () {
+ rpcCount++;
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ rpcCount = 0;
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(def)').length,
+ "the 3rd record is the one with 'def' value");
+ form.renderer._render = function () {
+ throw "should not render the whole form";
+ };
+
+ await testUtils.dom.click(form.$('table thead th:contains(Foo)'));
+ assert.strictEqual(rpcCount, 0,
+ 'sort should be in memory, no extra RPCs should have been done');
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(xyz)').length,
+ "the 3rd record is the one with 'xyz' value");
+
+ await testUtils.dom.click(form.$('table thead th:contains(Foo)'));
+ assert.ok(form.$('table tbody tr:eq(2) td:contains(abc)').length,
+ "the 3rd record is the one with 'abc' value");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list field edition', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records.push({
+ id: 3,
+ display_name: "relational record 1",
+ });
+ this.data.partner.records[1].p = [3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit the first line of the o2m
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1',
+ "display name of first record in o2m list should be 'relational record 1'");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_one2many tbody td').first());
+ assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row',
+ "first row of o2m should be in edition");
+ await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value");
+ assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row',
+ "first row of o2m should still be in edition");
+
+ // // leave o2m edition
+ await testUtils.dom.click(form.$el);
+ assert.doesNotHaveClass(form.$('.o_field_one2many tbody td').first().parent(), 'o_selected_row',
+ "first row of o2m should be readonly again");
+
+ // discard changes
+ await testUtils.form.clickDiscard(form);
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
+ "changes shouldn't have been discarded yet, waiting for user confirmation");
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1',
+ "display name of first record in o2m list should be 'relational record 1'");
+
+ // edit again and save
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_one2many tbody td').first());
+ await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value");
+ await testUtils.dom.click(form.$el);
+ await testUtils.form.clickSave(form);
+ // FIXME: this next test doesn't pass as the save of updates of
+ // relational data is temporarily disabled
+ // assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value',
+ // "display name of first record in o2m list should be 'new value'");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: create action disabled', async function (assert) {
+ assert.expect(2);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree create="0">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: conditional create/delete actions', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2, 4];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p" options="{'create': [('bar', '=', True)], 'delete': [('bar', '=', True)]}">
+ <tree>
+ <field name="display_name"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // bar is true -> create and delete action are available
+ assert.containsOnce(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should be available');
+ assert.hasClass(form.$('td.o_list_record_remove button').first(), 'fa fa-trash-o',
+ "should have trash bin icons");
+
+ // set bar to false -> create and delete action are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first());
+
+ assert.containsNone(form, '.o_field_x2many_list_row_add',
+ '"Add an item" link should not be available if bar field is False');
+ assert.containsNone(form, 'td.o_list_record_remove button',
+ "should not have trash bin icons if bar field is False");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: unlink two records', async function (assert) {
+ assert.expect(8);
+ this.data.partner.records[0].p = [1, 2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" widget="many2many">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].p;
+ assert.strictEqual(commands.length, 3,
+ 'should have generated three commands');
+ assert.ok(commands[0][0] === 4 && commands[0][1] === 2,
+ 'should have generated the command 4 (LINK_TO) with id 4');
+ assert.ok(commands[1][0] === 4 && commands[1][1] === 4,
+ 'should have generated the command 4 (LINK_TO) with id 4');
+ assert.ok(commands[2][0] === 3 && commands[2][1] === 1,
+ 'should have generated the command 3 (UNLINK) with id 1');
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner"><field name="display_name"/></form>',
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, 'td.o_list_record_remove button', 3,
+ "should have 3 remove buttons");
+
+ assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-times',
+ "should have X icons to remove (unlink) records");
+
+ await testUtils.dom.click(form.$('td.o_list_record_remove button').first());
+
+ assert.containsN(form, 'td.o_list_record_remove button', 2,
+ "should have 2 remove buttons (a record is supposed to have been unlinked)");
+
+ await testUtils.dom.click(form.$('tr.o_data_row').first());
+ assert.containsNone($('.modal .modal-footer .o_btn_remove'),
+ 'there should not be a modal having Remove Button');
+
+ await testUtils.dom.click($('.modal .btn-secondary'))
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many list: deleting one records', async function (assert) {
+ assert.expect(7);
+ this.data.partner.records[0].p = [1, 2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].p;
+ assert.strictEqual(commands.length, 3,
+ 'should have generated three commands');
+ assert.ok(commands[0][0] === 4 && commands[0][1] === 2,
+ 'should have generated the command 4 (LINK_TO) with id 2');
+ assert.ok(commands[1][0] === 4 && commands[1][1] === 4,
+ 'should have generated the command 2 (LINK_TO) with id 1');
+ assert.ok(commands[2][0] === 2 && commands[2][1] === 1,
+ 'should have generated the command 2 (DELETE) with id 2');
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner"><field name="display_name"/></form>',
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, 'td.o_list_record_remove button', 3,
+ "should have 3 remove buttons");
+
+ assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-trash-o',
+ "should have trash bin icons to remove (delete) records");
+
+ await testUtils.dom.click(form.$('td.o_list_record_remove button').first());
+
+ assert.containsN(form, 'td.o_list_record_remove button', 2,
+ "should have 2 remove buttons");
+
+ // save and check that the correct command has been generated
+ await testUtils.form.clickSave(form);
+
+ // FIXME: it would be nice to test that the view is re-rendered correctly,
+ // but as the relational data isn't re-fetched, the rendering is ok even
+ // if the changes haven't been saved
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban: edition', async function (assert) {
+ assert.expect(23);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ // color will be in the kanban but not in the form
+ '<field name="color"/>' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '<span><t t-esc="record.color.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ // foo will be in the form but not in the kanban
+ '<field name="foo"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].p;
+ assert.strictEqual(commands.length, 2,
+ 'should have generated two commands');
+ assert.strictEqual(commands[0][0], 0,
+ 'generated command should be ADD WITH VALUES');
+ assert.strictEqual(commands[0][2].display_name, "new subrecord 3",
+ 'value of newly created subrecord should be "new subrecord 3"');
+ assert.strictEqual(commands[1][0], 2,
+ 'generated command should be REMOVE AND DELETE');
+ assert.strictEqual(commands[1][1], 2,
+ 'deleted record id should be 2');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_one2many .o-kanban-button-new').length,
+ '"Create" button should not be visible in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ 'should contain 1 record');
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'second record',
+ 'display_name of subrecord should be the one in DB');
+ assert.strictEqual(form.$('.o_kanban_record span:nth(1)').text(), 'Red',
+ 'color of subrecord should be the one in DB');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ assert.ok(form.$('.o_field_one2many .o-kanban-button-new').length,
+ '"Create" button should be visible in edit');
+ assert.hasClass(form.$('.o_field_one2many .o-kanban-button-new'),'btn-secondary',
+ "'Create' button should have className 'btn-secondary'");
+ assert.strictEqual(form.$('.o_field_one2many .o-kanban-button-new').text().trim(), "Add",
+ 'Create button should have "Add" label');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.oe_kanban_global_click'));
+
+ await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new name');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // create a new subrecord
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 1');
+ await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1Red',
+ 'value of newly created subrecord should be "new subrecord 1"');
+
+ // create two new subrecords
+ await testUtils.dom.click(form.$('.o-kanban-button-new'));
+ await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 2');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)'));
+ await testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 3');
+ await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4,
+ 'should contain 4 records');
+
+ // delete subrecords
+ await testUtils.dom.click(form.$('.oe_kanban_global_click').first());
+ assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1,
+ 'There should be a modal having Remove Button');
+ await testUtils.dom.click($('.modal .modal-footer .o_btn_remove'));
+ assert.containsNone($('.o_modal'), "modal should have been closed");
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3,
+ 'should contain 3 records');
+ await testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()'));
+ await testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()'));
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ 'should contain 1 records');
+ assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new subrecord 3',
+ 'the remaining subrecord should be "new subrecord 3"');
+
+ // save and check that the correct command has been generated
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban (editable): properly handle create_text node option', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" options="{\'create_text\': \'Add turtle\'}" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_details">' +
+ '<field name="display_name"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o-kanban-button-new').text().trim(),
+ "Add turtle", "In O2M Kanban, Add button should have 'Add turtle' label");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban: create action disabled', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban create="0">' +
+ '<field name="display_name"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_global_click">' +
+ '<a t-if="!read_only_mode" type="delete" class="fa fa-times float-right delete_icon"/>' +
+ '<span><t t-esc="record.display_name.value"/></span>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.ok(!form.$('.o-kanban-button-new').length,
+ '"Add" button should not be available in edit');
+ assert.ok(form.$('.o_kanban_view .delete_icon').length,
+ 'delete icon should be visible in edit');
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban: conditional create/delete actions', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2, 4];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p" options="{'create': [('bar', '=', True)], 'delete': [('bar', '=', True)]}">
+ <kanban>
+ <field name="display_name"/>
+ <templates>
+ <t t-name="kanban-box">
+ <div class="oe_kanban_global_click">
+ <span><t t-esc="record.display_name.value"/></span>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ <form>
+ <field name="display_name"/>
+ <field name="foo"/>
+ </form>
+ </field>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // bar is initially true -> create and delete actions are available
+ assert.containsOnce(form, '.o-kanban-button-new', '"Add" button should be available');
+
+ await testUtils.dom.click(form.$('.oe_kanban_global_click').first());
+
+ assert.containsOnce(document.body, '.modal .modal-footer .o_btn_remove',
+ 'There should be a Remove Button inside modal');
+
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // set bar false -> create and delete actions are no longer available
+ await testUtils.dom.click(form.$('.o_field_widget[name="bar"] input').first());
+
+ assert.containsNone(form, '.o-kanban-button-new',
+ '"Add" button should not be available as bar is False');
+
+ await testUtils.dom.click(form.$('.oe_kanban_global_click').first());
+
+ assert.containsNone(document.body, '.modal .modal-footer .o_btn_remove',
+ 'There should not be a Remove Button as bar field is False');
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, pager is updated', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' });
+ this.data.partner.records[0].turtles = [1, 2, 3, 4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, then click in form view to confirm it
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
+ "pager should display the correct total");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (non editable): edition', async function (assert) {
+ assert.expect(12);
+
+ var nbWrite = 0;
+ this.data.partner.records[0].p = [2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/><field name="qux"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ nbWrite++;
+ assert.deepEqual(args.args[1], {
+ p: [[1, 2, { display_name: 'new name' }], [2, 4, false]]
+ }, "should have sent the correct commands");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(!form.$('.o_list_record_remove').length,
+ 'remove icon should not be visible in readonly');
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not be visible in readonly');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_list_view td.o_list_number', 2,
+ 'should contain 2 records');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'second record',
+ 'display_name of first subrecord should be the one in DB');
+ assert.ok(form.$('.o_list_record_remove').length,
+ 'remove icon should be visible in edit');
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" should not visible in edit');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.o_list_view tbody tr:first() td:eq(1)'));
+
+ await testUtils.fields.editInput($('.modal .o_form_view input'), 'new name');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'value of subrecord should have been updated');
+ assert.strictEqual(nbWrite, 0, "should not have write anything in DB");
+
+ // create new subrecords
+ // TODO when 'Add an item' will be implemented
+
+ // remove subrecords
+ await testUtils.dom.click(form.$('.o_list_record_remove:nth(1)'));
+ assert.containsOnce(form, '.o_list_view td.o_list_number',
+ 'should contain 1 subrecord');
+ assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name',
+ 'the remaining subrecord should be "new name"');
+
+ await testUtils.form.clickSave(form); // save the record
+ assert.strictEqual(nbWrite, 1, "should have write the changes in DB");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].p = [2, 4];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/><field name="qux"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.ok(!form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should not be available in readonly');
+
+ await testUtils.dom.click(form.$('.o_list_view tbody td:first()'));
+ assert.ok($('.modal .o_form_readonly').length,
+ 'in readonly, clicking on a subrecord should open it in readonly in a dialog');
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ await testUtils.form.clickEdit(form);
+
+ assert.ok(form.$('.o_field_x2many_list_row_add').length,
+ '"Add an item" link should be available in edit');
+
+ // edit existing subrecord
+ await testUtils.dom.click(form.$('.o_list_view tbody td:first()'));
+ assert.strictEqual($('.modal').length, 0,
+ 'in edit, clicking on a subrecord should not open a dialog');
+ assert.hasClass(form.$('.o_list_view tbody tr:first()'),'o_selected_row',
+ 'first row should be in edition');
+ await testUtils.fields.editInput(form.$('.o_list_view input:first()'), 'new name');
+
+ await testUtils.dom.click(form.$('.o_list_view tbody tr:nth(1) td:first'));
+ assert.doesNotHaveClass(form.$('.o_list_view tbody tr:first'), 'o_selected_row',
+ 'first row should not be in edition anymore');
+ assert.strictEqual(form.$('.o_list_view tbody td:first').text(), 'new name',
+ 'value of subrecord should have been updated');
+
+ // create new subrecords
+ // TODO when 'Add an item' will be implemented
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 2', async function (assert) {
+ assert.expect(8);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].p[0][0], 0,
+ "should send a 0 command for field p");
+ assert.strictEqual(args.args[1].p[1][0], 0,
+ "should send a second 0 command for field p");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item and enter a value
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'kartoffel');
+
+ // click again on Add an item
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
+ "should have one td with the new value");
+ assert.containsOnce(form, '.o_selected_row > td input',
+ "should have one other new td");
+ assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows");
+
+ // enter another value and save
+ await testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'gemuse');
+ await testUtils.form.clickSave(form);
+ assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows");
+ assert.strictEqual(form.$('td:contains(kartoffel)').length, 1,
+ "should have one td with the new value");
+ assert.strictEqual(form.$('td:contains(gemuse)').length, 1,
+ "should have one td with the new value");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 3', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // edit mode, then click on Add an item 2 times
+ assert.containsOnce(form, 'tr.o_data_row',
+ "should have 1 data rows");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsN(form, 'tr.o_data_row', 3,
+ "should have 3 data rows");
+
+ // cancel the edition
+ await testUtils.form.clickDiscard(form);
+ await testUtils.dom.click($('.modal-footer button.btn-primary').first());
+ assert.containsOnce(form, 'tr.o_data_row',
+ "should have 1 data rows");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): edition, part 4', async function (assert) {
+ assert.expect(3);
+ var i = 0;
+
+ this.data.turtle.onchanges = {
+ turtle_trululu: function (obj) {
+ if (i) {
+ obj.turtle_description = "Some Description";
+ }
+ i++;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_trululu"/>' +
+ '<field name="turtle_description"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit mode, then click on Add an item
+ assert.containsNone(form, 'tr.o_data_row',
+ "should have 0 data rows");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('textarea').val(), "",
+ "field turtle_description should be empty");
+
+ // add a value in the turtle_trululu field to trigger an onchange
+ await testUtils.fields.many2one.clickOpenDropdown('turtle_trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('turtle_trululu');
+ assert.strictEqual(form.$('textarea').val(), "Some Description",
+ "field turtle_description should be set to the result of the onchange");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): discarding required empty data', async function (assert) {
+ assert.expect(7);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item, then click elsewhere
+ assert.containsNone(form, 'tr.o_data_row',
+ "should have 0 data rows");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('label.o_form_label').first());
+ assert.containsNone(form, 'tr.o_data_row',
+ "should still have 0 data rows");
+
+ // click on Add an item again, then click on save
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.form.clickSave(form);
+ assert.containsNone(form, 'tr.o_data_row',
+ "should still have 0 data rows");
+
+ assert.verifySteps(['read', 'onchange', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, adding line when only one page', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, to reach the page size limit
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ // the record currently being added should not count in the pager
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+
+ // unselect the row
+ await testUtils.dom.click(form.$el);
+ assert.containsNone(form, '.o_selected_row');
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_widget[name=turtles] .o_pager');
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text(), "1-3 / 4");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, adding line, then discarding', async function (assert) {
+ assert.expect(2);
+
+ this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' });
+ this.data.partner.records[0].turtles = [1, 2, 3, 4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record, then discard
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.form.clickDiscard(form);
+
+ // confirm the discard operation
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.isVisible(form.$('.o_field_widget[name=turtles] .o_pager'));
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-3 / 4',
+ "pager should display correct values");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, required field and pager', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' });
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.records[0].turtles = [1, 2, 3, 4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a (empty) record
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ // go on next page. The new record is not valid and should be discarded
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+ assert.containsOnce(form, 'tr.o_data_row');
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, required field, pager and confirm discard', async function (assert) {
+ assert.expect(3);
+
+ this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' });
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.records[0].turtles = [1, 2, 3, 4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add a record with a dirty state, but not valid
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), 4321);
+
+ // go to next page. The new record is not valid, but dirty. we should
+ // see a confirm dialog
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
+ "pager should still display the correct total");
+
+ // click on cancel
+ await testUtils.dom.click($('.modal .modal-footer .btn-secondary'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
+ "pager should again display the correct total");
+ assert.containsOnce(form, '.o_field_one2many input.o_field_invalid',
+ "there should be an invalid input in the one2many");
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list, adding, discarding, and pager', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // add 4 records (to have more records than the limit)
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, 'tr.o_data_row', 5);
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+
+ // discard
+ await testUtils.form.clickDiscard(form);
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.containsOnce(form, 'tr.o_data_row');
+ assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
+
+ form.destroy();
+ });
+
+ QUnit.test('unselecting a line with missing required data', async function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ // edit mode, then click on Add an item, then click elsewhere
+ assert.containsNone(form, 'tr.o_data_row',
+ "should have 0 data rows");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsOnce(form, 'tr.o_data_row',
+ "should have 1 data rows");
+
+ // adding a value in the non required field, so it is dirty, but with
+ // a missing required field
+ await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '12345');
+
+ // click elsewhere,
+ await testUtils.dom.click(form.$('label.o_form_label'));
+ assert.strictEqual($('.modal').length, 1,
+ 'a confirmation model should be opened');
+
+ // click on cancel, the line should still be selected
+ await testUtils.dom.click($('.modal .modal-footer button.btn-secondary'));
+ assert.containsOnce(form, 'tr.o_data_row.o_selected_row',
+ "should still have 1 selected data row");
+
+ // click elsewhere, and click on ok (on the confirmation dialog)
+ await testUtils.dom.click(form.$('label.o_form_label'));
+ await testUtils.dom.click($('.modal .modal-footer button.btn-primary'));
+ assert.containsNone(form, 'tr.o_data_row',
+ "should have 0 data rows (invalid line has been discarded");
+
+ form.destroy();
+ });
+
+ QUnit.test('pressing enter in a o2m with a required empty field', async function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item, then press enter
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'enter');
+ assert.hasClass(form.$('input[name="turtle_foo"]'), 'o_field_invalid',
+ "input should be marked invalid");
+ assert.verifySteps(['read', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('editing a o2m, with required field and onchange', async function (assert) {
+ assert.expect(11);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ delete this.data.turtle.fields.turtle_foo.default;
+ this.data.turtle.onchanges = {
+ turtle_foo: function (obj) {
+ obj.turtle_int = obj.turtle_foo.length;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method) {
+ assert.step(args.method);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit mode, then click on Add an item
+ assert.containsNone(form, 'tr.o_data_row',
+ "should have 0 data rows");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ // input some text in required turtle_foo field
+ await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'aubergine');
+ assert.strictEqual(form.$('input[name="turtle_int"]').val(), "9",
+ "onchange should have been triggered");
+
+ // save and check everything is fine
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1,
+ "should have one row with turtle_foo value");
+ assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1,
+ "should have one row with turtle_int value");
+
+ assert.verifySteps(['read', 'onchange', 'onchange', 'write', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('editable o2m, pressing ESC discard current changes', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsOnce(form, 'tr.o_data_row',
+ "there should be one data row");
+
+ await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape');
+ assert.containsNone(form, 'tr.o_data_row',
+ "data row should have been discarded");
+ assert.verifySteps(['read', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('editable o2m with required field, pressing ESC discard current changes', async function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsOnce(form, 'tr.o_data_row',
+ "there should be one data row");
+
+ await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'escape');
+ assert.containsNone(form, 'tr.o_data_row',
+ "data row should have been discarded");
+ assert.verifySteps(['read', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('pressing escape in editable o2m list in dialog', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "partner,false,form": '<form>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.modal .o_data_row.o_selected_row').length, 1,
+ "there should be a row in edition in the dialog");
+
+ await testUtils.fields.triggerKeydown($('.modal .o_data_cell input'), 'escape');
+
+ assert.strictEqual($('.modal').length, 1,
+ "dialog should still be open");
+ assert.strictEqual($('.modal .o_data_row').length, 0,
+ "the row should have been removed");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable o2m with onchange and required field: delete an invalid line', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ turtles: function () { },
+ };
+ this.data.partner.records[0].turtles = [1];
+ this.data.turtle.records[0].product_id = 37;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_data_cell:first'));
+ form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup');
+ assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid');
+ await testUtils.dom.click(form.$('.o_list_record_remove'));
+ assert.verifySteps(['onchange'], 'onchange should have been done');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange in a one2many', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records.push({
+ id: 3,
+ foo: "relational record 1",
+ });
+ this.data.partner.records[1].p = [3];
+ this.data.partner.onchanges = { p: true };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ p: [
+ [5], // delete all
+ [0, 0, { foo: "from onchange" }], // create new
+ ]
+ }
+ });
+ }
+ return this._super(route, args);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_one2many tbody td').first());
+ await testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value");
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange',
+ "display name of first record in o2m list should be 'new value'");
+ form.destroy();
+ });
+
+ QUnit.test('one2many, default_get and onchange (basic)', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [
+ [6, 0, []], // replace with zero ids
+ ];
+ this.data.partner.onchanges = { p: true };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ p: [
+ [5], // delete all
+ [0, 0, { foo: "from onchange" }], // create new
+ ]
+ }
+ });
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.ok(form.$('td:contains(from onchange)').length,
+ "should have 'from onchange' value in one2many");
+ form.destroy();
+ });
+
+ QUnit.test('one2many and default_get (with date)', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.p.default = [
+ [0, false, { date: '2017-10-08', p: [] }],
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="date"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('.o_data_cell').text(), '10/08/2017',
+ "should correctly display the date");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many and onchange (with integer)', async function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.onchanges = {
+ turtle_int: function () { }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('td:contains(9)'));
+ await testUtils.fields.editInput(form.$('td input[name="turtle_int"]'), "3");
+
+ // the 'change' event is triggered on the input when we focus somewhere
+ // else, for example by clicking in the body. However, if we try to
+ // programmatically click in the body, it does not trigger a change
+ // event, so we simply trigger it directly instead.
+ form.$('td input[name="turtle_int"]').trigger('change');
+
+ assert.verifySteps(['read', 'read', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('one2many and onchange (with date)', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.onchanges = {
+ date: function () { }
+ };
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="date"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('td:contains(01/25/2017)'));
+ await testUtils.dom.click(form.$('.o_datepicker_input'));
+ await testUtils.nextTick();
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first());
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)'));
+ await testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1));
+ await testUtils.dom.click($('.day:contains(22)'));
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('one2many and onchange (with command DELETE_ALL)', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ foo: function (obj) {
+ obj.p = [[5]];
+ },
+ p: function () { }, // dummy onchange on the o2m to execute _isX2ManyValid()
+ };
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (method, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [
+ [0, args.args[1].p[0][1], { display_name: 'z' }],
+ [2, 2, false],
+ ], "correct commands should be sent");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "o2m should contain one row");
+
+ // empty o2m by triggering the onchange
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange');
+
+ assert.containsNone(form, '.o_data_row',
+ "rows of the o2m should have been deleted");
+
+ // add two new subrecords
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'x');
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'y');
+
+ assert.containsN(form, '.o_data_row', 2,
+ "o2m should contain two rows");
+
+ // empty o2m by triggering the onchange
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange again');
+
+ assert.containsNone(form, '.o_data_row',
+ "rows of the o2m should have been deleted");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'z');
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many and onchange only write modified field', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5], // delete all
+ [1, 3, { // the server returns all fields
+ display_name: "coucou",
+ product_id: [37, "xphone"],
+ turtle_bar: false,
+ turtle_foo: "has changed",
+ turtle_int: 42,
+ turtle_qux: 9.8,
+ partner_ids: [],
+ turtle_ref: 'product,37',
+ }],
+ ];
+ },
+ };
+
+ this.data.partner.records[0].turtles = [3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="product_id"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_ref"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (method, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles, [
+ [1, 3, { display_name: 'coucou', turtle_foo: 'has changed', turtle_int: 42 }],
+ ], "correct commands should be sent (only send changed values)");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "o2m should contain one row");
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first'));
+ await testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp');
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many with CREATE onchanges correctly refreshed', async function (assert) {
+ assert.expect(5);
+
+ var delta = 0;
+ testUtils.mock.patch(AbstractField, {
+ init: function () {
+ delta++;
+ this._super.apply(this, arguments);
+ },
+ destroy: function () {
+ delta--;
+ this._super.apply(this, arguments);
+ },
+ });
+
+ var deactiveOnchange = true;
+
+ this.data.partner.records[0].turtles = [];
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ if (deactiveOnchange) { return; }
+ // the onchange will either:
+ // - create a second line if there is only one line
+ // - edit the second line if there are two lines
+ if (obj.turtles.length === 1) {
+ obj.turtles = [
+ [5], // delete all
+ [0, obj.turtles[0][1], {
+ display_name: "first",
+ turtle_int: obj.turtles[0][2].turtle_int,
+ }],
+ [0, 0, {
+ display_name: "second",
+ turtle_int: -obj.turtles[0][2].turtle_int,
+ }],
+ ];
+ } else if (obj.turtles.length === 2) {
+ obj.turtles = [
+ [5], // delete all
+ [0, obj.turtles[0][1], {
+ display_name: "first",
+ turtle_int: obj.turtles[0][2].turtle_int,
+ }],
+ [0, obj.turtles[1][1], {
+ display_name: "second",
+ turtle_int: -obj.turtles[0][2].turtle_int,
+ }],
+ ];
+ }
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name" widget="char"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsNone(form, '.o_data_row',
+ "o2m shouldn't contain any row");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ // trigger the first onchange
+ deactiveOnchange = false;
+ await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '10');
+ // put the list back in non edit mode
+ await testUtils.dom.click(form.$('input[name="foo"]'));
+ assert.strictEqual(form.$('.o_data_row').text(), "first10second-10",
+ "should correctly refresh the records");
+
+ // trigger the second onchange
+ await testUtils.dom.click(form.$('.o_field_x2many_list tbody tr:first td:first'));
+ await testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '20');
+
+ await testUtils.dom.click(form.$('input[name="foo"]'));
+ assert.strictEqual(form.$('.o_data_row').text(), "first20second-20",
+ "should correctly refresh the records");
+
+ assert.containsN(form, '.o_field_widget', delta,
+ "all (non visible) field widgets should have been destroyed");
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_data_row').text(), "first20second-20",
+ "should correctly refresh the records after save");
+
+ form.destroy();
+ testUtils.mock.unpatch(AbstractField);
+ });
+
+ QUnit.test('editable one2many with sub widgets are rendered in readonly', async function (assert) {
+ assert.expect(2);
+
+ var editableWidgets = 0;
+ testUtils.mock.patch(AbstractField, {
+ init: function () {
+ this._super.apply(this, arguments);
+ if (this.mode === 'edit') {
+ editableWidgets++;
+ }
+ },
+ });
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo" widget="char" attrs="{\'readonly\': [(\'turtle_int\', \'==\', 11111)]}"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(editableWidgets, 1,
+ "o2m is only widget in edit mode");
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual(editableWidgets, 3,
+ "3 widgets currently in edit mode");
+
+ form.destroy();
+ testUtils.mock.unpatch(AbstractField);
+ });
+
+ QUnit.test('one2many editable list with onchange keeps the order', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [1, 2, 4];
+ this.data.partner.onchanges = {
+ p: function () { },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_cell').text(), 'first recordsecond recordaaa',
+ "records should be display in the correct order");
+
+ await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell'));
+ await testUtils.fields.editInput(form.$('.o_selected_row .o_field_widget[name=display_name]'), 'new');
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual(form.$('.o_data_cell').text(), 'newsecond recordaaa',
+ "records should be display in the correct order");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list (editable): readonly domain is evaluated', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2, 4];
+ this.data.partner.records[1].product_id = false;
+ this.data.partner.records[2].product_id = 37;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="display_name" attrs=\'{"readonly": [["product_id", "=", false]]}\'/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_list_view tbody tr:eq(0) td:first'),'o_readonly_modifier',
+ "first record should have display_name in readonly mode");
+
+ assert.doesNotHaveClass(form.$('.o_list_view tbody tr:eq(1) td:first'), 'o_readonly_modifier',
+ "second record should not have display_name in readonly mode");
+ form.destroy();
+ });
+
+ QUnit.test('pager of one2many field in new record', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.containsNone(form, '.o_x2m_control_panel .o_pager',
+ 'o2m pager should be hidden');
+
+ // click to create a subrecord
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ assert.containsOnce(form, 'tr.o_data_row');
+
+ assert.containsNone(form, '.o_x2m_control_panel .o_pager',
+ 'o2m pager should be hidden');
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with a many2one', async function (assert) {
+ assert.expect(5);
+
+ let checkOnchange = false;
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+ this.data.partner.onchanges.p = function (obj) {
+ obj.p = [
+ [5], // delete all
+ [1, 2, { product_id: [37, "xphone"] }], // update existing record
+ [0, 0, { product_id: [41, "xpad"] }]
+ ];
+ //
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form':
+ '<form string="Partner"><field name="product_id"/></form>',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange' && checkOnchange) {
+ assert.deepEqual(args.args[1].p, [[4, 2, false], [0, args.args[1].p[1][1], { product_id: 41 }]],
+ "should trigger onchange with correct parameters");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should have properly fetched the many2one nameget");
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 0,
+ "should not display 'xpad' anywhere");
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+
+ checkOnchange = true;
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ testUtils.fields.many2one.clickItem('product_id', 'xpad');
+
+ await testUtils.dom.click($('.modal .modal-footer button:eq(0)'));
+
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1,
+ "should display 'xpad' on a td");
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should still display xphone");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with inline form view', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].p = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<field name="product_id"/>' +
+ '<field name="int_field"/>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '<field name="foo"/>' + // don't remove this, it is
+ // useful to make sure the foo fieldwidget
+ // does not crash because the foo field
+ // is not in the form view
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], {
+ foo: "My little Foo Value", int_field: 123, product_id: 41,
+ }]]);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+
+ // write in the many2one field, value = 37 (xphone)
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ // write in the integer field
+ await testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '123');
+
+ // save and close
+ await testUtils.dom.click($('.modal .modal-footer button:eq(0)'));
+
+ assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1,
+ "should display 'xphone' in a td");
+
+ // reopen the record in form view
+ await testUtils.dom.click(form.$('tbody td:contains(xphone)'));
+
+ assert.strictEqual($('.modal .modal-body input').val(), "xphone",
+ "should display 'xphone' in an input");
+
+ await testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '456');
+
+ // discard
+ await testUtils.dom.click($('.modal .modal-footer span:contains(Discard)'));
+
+ // reopen the record in form view
+ await testUtils.dom.click(form.$('tbody td:contains(xphone)'));
+
+ assert.strictEqual($('.modal .modal-body input.o_field_widget').val(), "123",
+ "should display 123 (previous change has been discarded)");
+
+ // write in the many2one field, value = 41 (xpad)
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ testUtils.fields.many2one.clickItem('product_id', 'xpad');
+
+ // save and close
+ await testUtils.dom.click($('.modal .modal-footer button:eq(0)'));
+
+ assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1,
+ "should display 'xpad' in a td");
+
+ // save the record
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with inline form view with context with parent key', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[0].product_id = 41;
+ this.data.partner.records[1].product_id = 37;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="product_id"/>' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<field name="product_id" context="{\'partner_foo\':parent.foo, \'lalala\': parent.product_id}"/>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.kwargs.context.partner_foo, "yop",
+ "should have correctly evaluated parent foo field");
+ assert.strictEqual(args.kwargs.context.lalala, 41,
+ "should have correctly evaluated parent product_id field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ // open a modal
+ await testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)'));
+
+ // write in the many2one field
+ await testUtils.dom.click($('.modal .o_field_many2one input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('value of invisible x2many fields is correctly evaluated in context', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner.records[0].p = [2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="product_id" context="{\'p\': p, \'timmy\': timmy}"/>' +
+ '<field name="p" invisible="1"/>' +
+ '<field name="timmy" invisible="1"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(
+ args.kwargs.context, {
+ p: [[4, 2, false], [4, 3, false]],
+ timmy: [[6, false, [12]]],
+ }, 'values of x2manys should have been correctly evaluated in context');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name=product_id] input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list, editable, with many2one and with context with parent key', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="product_id" context="{\'partner_foo\':parent.foo}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.kwargs.context.partner_foo, "yop",
+ "should have correctly evaluated parent foo field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)'));
+
+ // trigger a name search
+ await testUtils.dom.click(form.$('table td input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list, editable, with a date in the context', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].product_id = 37;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="date"/>' +
+ '<field name="p" context="{\'date\':date}">' +
+ '<tree editable="top">' +
+ '<field name="date"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.strictEqual(args.kwargs.context.date, '2017-01-25',
+ "should have properly evaluated date key in context");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many field with context', async function (assert) {
+ assert.expect(2);
+
+ var counter = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles" context="{\'turtles\':turtles}">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var expected = counter === 0 ?
+ [[4, 2, false]] :
+ [[4, 2, false], [0, args.kwargs.context.turtles[1][1], { turtle_foo: 'hammer' }]];
+ assert.deepEqual(args.kwargs.context.turtles, expected,
+ "should have properly evaluated turtles key in context");
+ counter++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'hammer');
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ form.destroy();
+ });
+
+ QUnit.test('one2many list edition, some basic functionality', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.foo.default = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+
+ assert.containsOnce(form, 'td input.o_field_widget',
+ "should have created a row in edit mode");
+
+ await testUtils.fields.editInput(form.$('td input.o_field_widget'), 'a');
+
+ assert.containsOnce(form, 'td input.o_field_widget',
+ "should not have unselected the row after edition");
+
+ await testUtils.fields.editInput(form.$('td input.o_field_widget'), 'abc');
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('td:contains(abc)').length, 1,
+ "should have a row with the correct value");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list, the context is properly evaluated and sent', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p" context="{\'hello\': \'world\', \'abc\': int_field}">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var context = args.kwargs.context;
+ assert.strictEqual(context.hello, "world");
+ assert.strictEqual(context.abc, 10);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many widget: create', async function (assert) {
+ assert.expect(10);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" widget="many2many">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '<form>' +
+ '<group>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="display_name"/><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></tree>',
+ 'turtle,false,search': '<search><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></search>',
+ },
+ session: {},
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/turtle/create') {
+ assert.ok(args.args, "should write on the turtle record");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1");
+ assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.modal .o_data_row').length, 2,
+ "should have 2 records in the select view (the last one is not displayed because it is already selected)");
+
+ await testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input'));
+ await testUtils.dom.click($('.modal .o_select_button'));
+ await testUtils.dom.click($('.o_form_button_save'));
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "should have 1 record in the select view");
+
+ await testUtils.dom.click($('.modal-footer button:eq(1)'));
+ await testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_foo"]'), 'tototo');
+ await testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_int"]'), 50);
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ await testUtils.dom.click($('.modal-footer button:contains(&):first'));
+
+ assert.strictEqual($('.modal').length, 0, "should close the modals");
+
+ assert.containsN(form, '.o_data_row', 3,
+ "should have 3 records in one2many list");
+ assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.50tototo1.550xphone",
+ "should display the record values in one2many list");
+
+ await testUtils.dom.click($('.o_form_button_save'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many widget: edition', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles" widget="many2many">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="product_id"/>' +
+ '</tree>' +
+ '<form>' +
+ '<group>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_trululu"/>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'turtle,false,list': '<tree><field name="display_name"/><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></tree>',
+ 'turtle,false,search': '<search><field name="turtle_foo"/><field name="turtle_bar"/><field name="product_id"/></search>',
+ },
+ session: {},
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/turtle/write') {
+ assert.strictEqual(args.args[0].length, 1, "should write on the turtle record");
+ assert.deepEqual(args.args[1], { "product_id": 37 }, "should write only the product_id on the turtle record");
+ }
+ if (route === '/web/dataset/call_kw/partner/write') {
+ assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1");
+ assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+ assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: one2many turtle field',
+ "modal should use the python field string as title");
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+ await testUtils.form.clickEdit(form);
+
+ // edit the first one2many record
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+ await testUtils.dom.click($('.modal-footer button:first'));
+
+ await testUtils.dom.click($('.o_form_button_save'));
+
+ // add a one2many record
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input'));
+ await testUtils.dom.click($('.modal .o_select_button'));
+
+ // edit the second one2many record
+ await testUtils.dom.click(form.$('.o_data_row:eq(1)'));
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+ await testUtils.dom.click($('.modal-footer button:first'));
+
+ await testUtils.dom.click($('.o_form_button_save'));
+
+ form.destroy();
+ });
+
+ QUnit.test('new record, the context is properly evaluated and sent', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.int_field.default = 17;
+ var n = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p" context="{\'hello\': \'world\', \'abc\': int_field}">' +
+ '<tree editable="top">' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ n++;
+ if (n === 2) {
+ var context = args.kwargs.context;
+ assert.strictEqual(context.hello, "world");
+ assert.strictEqual(context.abc, 17);
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ form.destroy();
+ });
+
+ QUnit.test('parent data is properly sent on an onchange rpc', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = { bar: function () { } };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ var fieldValues = args.args[1];
+ assert.strictEqual(fieldValues.trululu.foo, "yop",
+ "should have properly sent the parent foo value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have an x2many field with a boolean field
+ // (written in owl), so when we add a line, we sequentially render the list itself
+ // (including the boolean field), so we have to wait for the next animation frame, and
+ // then we render the control panel (also in owl), so we have to wait again for the
+ // next animation frame
+ await testUtils.owlCompatibilityNextTick();
+ form.destroy();
+ });
+
+ QUnit.test('parent data is properly sent on an onchange rpc (existing x2many record)', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ display_name: function () {},
+ };
+ this.data.partner.records[0].p = [1];
+ this.data.partner.records[0].turtles = [2];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="foo"/>
+ <field name="p">
+ <tree editable="top">
+ <field name="display_name"/>
+ <field name="turtles" widget="many2many_tags"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ mockRPC(route, args) {
+ if (args.method === 'onchange') {
+ const fieldValues = args.args[1];
+ assert.strictEqual(fieldValues.trululu.foo, "yop");
+ // we only send fields that changed inside the reverse many2one
+ assert.deepEqual(fieldValues.trululu.p, [
+ [1, 1, { display_name: 'new val' }],
+ ]);
+ }
+ return this._super(...arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row');
+
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first'));
+
+ assert.containsOnce(form, '.o_data_row.o_selected_row');
+ await testUtils.fields.editInput(form.$('.o_selected_row .o_field_widget[name=display_name]'), "new val");
+
+ form.destroy();
+ });
+
+ QUnit.test('parent data is properly sent on an onchange rpc, new record', async function (assert) {
+ assert.expect(4);
+
+ this.data.turtle.onchanges = { turtle_bar: function () { } };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'onchange' && args.model === 'turtle') {
+ var fieldValues = args.args[1];
+ assert.strictEqual(fieldValues.turtle_trululu.foo, "My little Foo Value",
+ "should have properly sent the parent foo value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have an x2many field with a boolean field
+ // (written in owl), so when we add a line, we sequentially render the list itself
+ // (including the boolean field), so we have to wait for the next animation frame, and
+ // then we render the control panel (also in owl), so we have to wait again for the
+ // next animation frame
+ await testUtils.owlCompatibilityNextTick();
+ assert.verifySteps(['onchange', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('id in one2many obtained in onchange is properly set', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.turtles = function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 3, { turtle_foo: "kawa" }]
+ ];
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="id"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.strictEqual(form.$('tr.o_data_row').text(), '3kawa',
+ "should have properly displayed id and foo field");
+ form.destroy();
+ });
+
+ QUnit.test('id field in one2many in a new record', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="id" invisible="1"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ var virtualID = args.args[0].turtles[0][1];
+ assert.deepEqual(args.args[0].turtles,
+ [[0, virtualID, { turtle_foo: "cat" }]],
+ 'should send proper commands');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.dom.click(form.$('td.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('td input[name="turtle_foo"]'), 'cat');
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('sub form view with a required field', async function (assert) {
+ assert.expect(2);
+ this.data.partner.fields.foo.required = true;
+ this.data.partner.fields.foo.default = null;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<form string="Partner">' +
+ '<group><field name="foo"/></group>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal-footer button.btn-primary').first());
+
+ assert.strictEqual($('.modal').length, 1, "should still have an open modal");
+ assert.strictEqual($('.modal tbody label.o_field_invalid').length, 1,
+ "should have displayed invalid fields");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with action button', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '<button name="method_name" type="object" icon="fa-plus"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ execute_action: function (event) {
+ assert.deepEqual(event.data.env.currentID, 2,
+ 'should call with correct id');
+ assert.strictEqual(event.data.env.model, 'partner',
+ 'should call with correct model');
+ assert.strictEqual(event.data.action_data.name, 'method_name',
+ "should call correct method");
+ assert.strictEqual(event.data.action_data.type, 'object',
+ 'should have correct type');
+ },
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_list_button button'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban with action button', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<kanban>' +
+ '<field name="foo"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<span><t t-esc="record.foo.value"/></span>' +
+ '<button name="method_name" type="object" class="fa fa-plus"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ intercepts: {
+ execute_action: function (event) {
+ assert.deepEqual(event.data.env.currentID, 2,
+ 'should call with correct id');
+ assert.strictEqual(event.data.env.model, 'partner',
+ 'should call with correct model');
+ assert.strictEqual(event.data.action_data.name, 'method_name',
+ "should call correct method");
+ assert.strictEqual(event.data.action_data.type, 'object',
+ 'should have correct type');
+ },
+ },
+ });
+
+ await testUtils.dom.click(form.$('.oe_kanban_action_button'));
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban with edit type action and domain widget (widget using SpecialData)', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.fields.model_name = { string: "Domain Condition Model", type: "char" };
+ this.data.turtle.fields.condition = { string: "Domain Condition", type: "char" };
+ _.each(this.data.turtle.records, function (record) {
+ record.model_name = 'partner';
+ record.condition = '[]';
+ });
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><field name="display_name"/></div>' +
+ '<div><field name="turtle_foo"/></div>' +
+ // field without Widget in the list
+ '<div><field name="condition"/></div>' +
+ '<div> <a type="edit"> Edit </a> </div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '<form>' +
+ '<field name="product_id" widget="statusbar"/>' +
+ '<field name="model_name"/>' +
+ // field with Widget requiring specialData in the form
+ '<field name="condition" widget="domain" options="{\'model\': \'model_name\'}"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$('.oe_kanban_action:eq(0)'));
+ assert.strictEqual($('.o_domain_selector').length, 1, "should add domain selector widget");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list with onchange and domain widget (widget using SpecialData)', async function (assert) {
+ assert.expect(3);
+
+ this.data.turtle.fields.model_name = { string: "Domain Condition Model", type: "char" };
+ this.data.turtle.fields.condition = { string: "Domain Condition", type: "char" };
+ _.each(this.data.turtle.records, function (record) {
+ record.model_name = 'partner';
+ record.condition = '[]';
+ });
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ var virtualID = obj.turtles[1][1];
+ obj.turtles = [
+ [5], // delete all
+ [0, virtualID, {
+ display_name: "coucou",
+ product_id: [37, "xphone"],
+ turtle_bar: false,
+ turtle_foo: "has changed",
+ turtle_int: 42,
+ turtle_qux: 9.8,
+ partner_ids: [],
+ turtle_ref: 'product,37',
+ model_name: 'partner',
+ condition: '[]',
+ }],
+ ];
+ },
+ };
+ var nbFetchSpecialDomain = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles" mode="tree">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_foo"/>' +
+ // field without Widget in the list
+ '<field name="condition"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="model_name"/>' +
+ // field with Widget requiring specialData in the form
+ '<field name="condition" widget="domain" options="{\'model\': \'model_name\'}"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route) {
+ if (route === '/web/dataset/call_kw/partner/search_count') {
+ nbFetchSpecialDomain++;
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal').length, 1, "form view dialog should be opened");
+ await testUtils.fields.editInput($('.modal-body input[name="model_name"]'), 'partner');
+ await testUtils.dom.click($('.modal-footer button:first'));
+
+ assert.strictEqual(form.$('.o_field_one2many tbody tr:first').text(), "coucouhas changed[]",
+ "the onchange should create one new record and remove the existing");
+
+ await testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(0) td:first'));
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(nbFetchSpecialDomain, 1,
+ "should only fetch special domain once");
+ form.destroy();
+ });
+
+ QUnit.test('one2many without inline tree arch', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="p" widget="many2many_tags"/>' + // check if the view don not call load view (widget without useSubview)
+ '<field name="turtles"/>' +
+ '<field name="timmy" invisible="1"/>' + // check if the view don not call load view in invisible
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,list": '<tree string="Turtles"><field name="turtle_bar"/><field name="display_name"/><field name="partner_ids"/></tree>',
+ }
+ });
+
+ assert.containsOnce(form, '.o_field_widget[name="turtles"] .o_list_view',
+ 'should display one2many list view in the modal');
+
+ assert.containsN(form, '.o_data_row', 2,
+ 'should display the 2 turtles');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2one and many2many in one2many', async function (assert) {
+ assert.expect(11);
+
+ this.data.turtle.records[1].product_id = 37;
+ this.data.partner.records[0].turtles = [2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<form string="Turtles">' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '</form>' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '<field name="product_id"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands.length, 2,
+ "should have generated 2 commands");
+ assert.deepEqual(commands[0], [1, 2, {
+ partner_ids: [[6, false, [2, 1]]],
+ product_id: 41,
+ }], "generated commands should be correct");
+ assert.deepEqual(commands[1], [4, 3, false],
+ "generated commands should be correct");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2,
+ 'should display the 2 turtles');
+ assert.strictEqual(form.$('.o_data_row:first td:nth(1)').text(), 'xphone',
+ "should correctly display the m2o");
+ assert.strictEqual(form.$('.o_data_row:first td:nth(2) .badge').length, 2,
+ "m2m should contain two tags");
+ assert.strictEqual(form.$('.o_data_row:first td:nth(2) .badge:first span').text(),
+ 'second record', "m2m values should have been correctly fetched");
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.strictEqual($('.modal .o_field_widget').text(), "xphone",
+ 'should display the form view dialog with the many2one value');
+ await testUtils.dom.click($('.modal-footer button'));
+
+ await testUtils.form.clickEdit(form);
+
+ // edit the m2m of first row
+ await testUtils.dom.click(form.$('.o_list_view tbody td:first()'));
+ // remove a tag
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge:contains(aaa) .o_delete'));
+ assert.strictEqual(form.$('.o_selected_row .o_field_many2manytags .o_badge_text:contains(aaa)').length, 0,
+ "tag should have been correctly removed");
+ // add a tag
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
+ assert.strictEqual(form.$('.o_selected_row .o_field_many2manytags .o_badge_text:contains(first record)').length, 1,
+ "tag should have been correctly added");
+
+ // edit the m2o of first row
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickItem('product_id', 'xpad');
+ assert.strictEqual(form.$('.o_selected_row .o_field_many2one:first input').val(), 'xpad',
+ "m2o value should have been updated");
+
+ // save (should correctly generate the commands)
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2manytag in one2many, onchange, some modifiers, and more than one page', async function (assert) {
+ assert.expect(9);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ this.data.partner.onchanges.turtles = function () { };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top" limit="2">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags" attrs="{\'readonly\': [(\'turtle_foo\', \'=\', \'a\')]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: { mode: 'edit' },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.containsN(form, '.o_data_row', 2,
+ 'there should be only 2 rows displayed');
+ await testUtils.dom.clickFirst(form.$('.o_list_record_remove'));
+ await testUtils.dom.clickFirst(form.$('.o_list_record_remove'));
+
+ assert.containsOnce(form, '.o_data_row',
+ 'there should be just one remaining row');
+
+ assert.verifySteps([
+ "read", // initial read on partner
+ "read", // initial read on turtle
+ "read", // batched read on partner (field partner_ids)
+ "read", // after first delete, read on turtle (to fetch 3rd record)
+ "onchange", // after first delete, onchange on field turtles
+ "onchange" // onchange after second delete
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange many2many in one2many list editable', async function (assert) {
+ assert.expect(14);
+
+ this.data.product.records.push({
+ id: 1,
+ display_name: "xenomorphe",
+ });
+
+ this.data.turtle.onchanges = {
+ product_id: function (rec) {
+ if (rec.product_id) {
+ rec.partner_ids = [
+ [5],
+ [4, rec.product_id === 41 ? 1 : 2]
+ ];
+ }
+ },
+ };
+ var partnerOnchange = function (rec) {
+ if (!rec.int_field || !rec.turtles.length) {
+ return;
+ }
+ rec.turtles = [
+ [5],
+ [0, 0, {
+ display_name: 'new line',
+ product_id: [37, 'xphone'],
+ partner_ids: [
+ [5],
+ [4, 1]
+ ]
+ }],
+ [0, rec.turtles[0][1], {
+ display_name: rec.turtles[0][2].display_name,
+ product_id: [1, 'xenomorphe'],
+ partner_ids: [
+ [5],
+ [4, 2]
+ ]
+ }],
+ ];
+ };
+
+ this.data.partner.onchanges = {
+ int_field: partnerOnchange,
+ turtles: partnerOnchange,
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="product_id"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ });
+
+ // add new line (first, xpad)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('input[name="display_name"]'), 'first');
+ await testUtils.dom.click(form.$('div[name="product_id"] input'));
+ // the onchange won't be generated
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+
+ assert.containsOnce(form, '.o_field_many2manytags.o_input',
+ 'should display the line in editable mode');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "xpad",
+ 'should display the product xpad');
+ assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "first record",
+ 'should display the tag from the onchange');
+
+ await testUtils.dom.click(form.$('input.o_field_integer[name="int_field"]'));
+
+ assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xpad",
+ 'should display the product xpad');
+ assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first record",
+ 'should display the tag in readonly');
+
+ // enable the many2many onchange and generate it
+ await testUtils.fields.editInput(form.$('input.o_field_integer[name="int_field"]'), '10');
+
+ assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
+ 'should display the product xphone and xenomorphe');
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
+ 'should display the name, one2many and many2many value');
+
+ // disable the many2many onchange
+ await testUtils.fields.editInput(form.$('input.o_field_integer[name="int_field"]'), '0');
+
+ // remove and start over
+ await testUtils.dom.click(form.$('.o_list_record_remove:first button'));
+ await testUtils.dom.click(form.$('.o_list_record_remove:first button'));
+
+ // enable the many2many onchange
+ await testUtils.fields.editInput(form.$('input.o_field_integer[name="int_field"]'), '10');
+
+ // add new line (first, xenomorphe)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('input[name="display_name"]'), 'first');
+ await testUtils.dom.click(form.$('div[name="product_id"] input'));
+ // generate the onchange
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xenomorphe)').trigger('mouseenter'));
+
+ assert.containsOnce(form, '.o_field_many2manytags.o_input',
+ 'should display the line in editable mode');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), "xenomorphe",
+ 'should display the product xenomorphe');
+ assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "second record",
+ 'should display the tag from the onchange');
+
+ // put list in readonly mode
+ await testUtils.dom.click(form.$('input.o_field_integer[name="int_field"]'));
+
+ assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
+ 'should display the product xphone and xenomorphe');
+ assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "second recordfirst record",
+ 'should display the tag in readonly (first record and second record)');
+
+ await testUtils.fields.editInput(form.$('input.o_field_integer[name="int_field"]'), '10');
+
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
+ 'should display the name, one2many and many2many value');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
+ 'should display the name, one2many and many2many value after save');
+
+ form.destroy();
+ });
+
+ QUnit.test('load view for x2many in one2many', async function (assert) {
+ assert.expect(2);
+
+ this.data.turtle.records[1].product_id = 37;
+ this.data.partner.records[0].turtles = [2, 3];
+ this.data.partner.records[2].turtles = [1, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<form string="Turtles">' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '<field name="partner_ids"/>' +
+ '</group>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "partner,false,list": '<tree string="Partners"><field name="display_name"/></tree>',
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2,
+ 'should display the 2 turtles');
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.strictEqual($('.modal .o_field_widget[name="partner_ids"] .o_list_view').length, 1,
+ 'should display many2many list view in the modal');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many (who contains a one2many) with tree view and without form view', async function (assert) {
+ assert.expect(1);
+
+ // avoid error in _postprocess
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="partner_ids"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="Turtles"><field name="turtle_foo"/></form>',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').text(), 'blip',
+ 'should open the modal and display the form field');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with x2many in form view (but not in list view)', async function (assert) {
+ assert.expect(1);
+
+ // avoid error when saving the edited related record (because the
+ // related x2m field is unknown in the inline list view)
+ // also ensure that the changes are correctly saved
+
+ this.data.turtle.fields.o2m = { string: "o2m", type: "one2many", relation: 'user' };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="Turtles">' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</form>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles, [[1, 2, {
+ partner_ids: [[6, false, [2, 4, 1]]],
+ }]]);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_data_row:first')); // edit first record
+
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
+
+ // add a many2many tag and save
+ await testUtils.dom.click($('.modal .o_field_many2manytags input'));
+ await testUtils.fields.editInput($('.modal .o_field_many2manytags input'), 'test');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary')); // save
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many list in a one2many opened by a many2one', async function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.records[1].turtle_trululu = 2;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "partner,false,form": '<form string="P">' +
+ '<field name="timmy"/>' +
+ '</form>',
+ "partner_type,false,list": '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ "partner_type,false,search": '<search>' +
+ '</search>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/get_formview_id') {
+ return Promise.resolve(false);
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].timmy, [[6, false, [12]]],
+ 'should properly write ids');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // edit the first partner in the one2many partner form view
+ await testUtils.dom.click(form.$('.o_data_row:first td.o_data_cell'));
+ // open form view for many2one
+ await testUtils.dom.click(form.$('.o_external_button'));
+
+ // click on add, to add a new partner in the m2m
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+
+ // select the partner_type 'gold' (this closes the 2nd modal)
+ await testUtils.dom.click($('.modal td:contains(gold)'));
+
+ // confirm the changes in the modal
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('nested x2many default values', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.turtles.default = [
+ [0, 0, { partner_ids: [[6, 0, [4]]] }],
+ [0, 0, { partner_ids: [[6, 0, [1]]] }],
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.containsN(form, '.o_list_view .o_data_row', 2,
+ "one2many list should contain 2 rows");
+ assert.containsN(form, '.o_list_view .o_field_many2manytags[name="partner_ids"] .badge', 2,
+ "m2mtags should contain two tags");
+ assert.strictEqual(form.$('.o_list_view .o_field_many2manytags[name="partner_ids"] .o_badge_text').text(),
+ 'aaafirst record', "tag names should have been correctly loaded");
+
+ form.destroy();
+ });
+
+ QUnit.test('nested x2many (inline form view) and onchanges', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges.bar = function (obj) {
+ if (!obj.bar) {
+ obj.p = [[5], [0, 0, {
+ turtles: [[0, 0, {
+ turtle_foo: 'new turtle',
+ }]],
+ }]];
+ }
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="bar"/>
+ <field name="p">
+ <tree>
+ <field name="turtles"/>
+ </tree>
+ <form>
+ <field name="turtles">
+ <tree>
+ <field name="turtle_foo"/>
+ </tree>
+ </field>
+ </form>
+ </field>
+ </form>`,
+ });
+
+ assert.containsNone(form, '.o_data_row');
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar] input'));
+ assert.containsOnce(form, '.o_data_row');
+ assert.strictEqual(form.$('.o_data_row').text(), '1 record');
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsOnce(document.body, '.modal .o_form_view .o_data_row');
+ assert.strictEqual($('.modal .o_form_view .o_data_row').text(), 'new turtle');
+
+ form.destroy();
+ });
+
+ QUnit.test('nested x2many (non inline form view) and onchanges', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges.bar = function (obj) {
+ if (!obj.bar) {
+ obj.p = [[5], [0, 0, {
+ turtles: [[0, 0, {
+ turtle_foo: 'new turtle',
+ }]],
+ }]];
+ }
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p">
+ <tree>
+ <field name="turtles"/>
+ </tree>
+ </field>
+ </form>`,
+ archs: {
+ 'partner,false,form': `
+ <form>
+ <field name="turtles">
+ <tree>
+ <field name="turtle_foo"/>
+ </tree>
+ </field>
+ </form>`,
+ },
+ });
+
+ assert.containsNone(form, '.o_data_row');
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar] input'));
+ assert.containsOnce(form, '.o_data_row');
+ assert.strictEqual(form.$('.o_data_row').text(), '1 record');
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsOnce(document.body, '.modal .o_form_view .o_data_row');
+ assert.strictEqual($('.modal .o_form_view .o_data_row').text(), 'new turtle');
+
+ form.destroy();
+ });
+
+ QUnit.test('nested x2many (non inline views and no widget on inner x2many in list)', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].p = [1];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="p"/></form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="turtles"/></tree>',
+ 'partner,false,form': '<form><field name="turtles" widget="many2many_tags"/></form>',
+ },
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_data_row');
+ assert.strictEqual(form.$('.o_data_row').text(), '1 record');
+
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsOnce(document.body, '.modal .o_form_view .o_field_many2manytags .badge');
+ assert.strictEqual($('.modal .o_field_many2manytags').text().trim(), 'donatello');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many (who contains display_name) with tree view and without form view', async function (assert) {
+ assert.expect(1);
+
+ // avoid error in _fetchX2Manys
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ "turtle,false,form": '<form string="Turtles"><field name="turtle_foo"/></form>',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_data_row:first'));
+
+ assert.strictEqual($('.modal .o_field_widget[name="turtle_foo"]').text(), 'blip',
+ 'should open the modal and display the form field');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many field with virtual ids', async function (assert) {
+ assert.expect(11);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<notebook>' +
+ '<page>' +
+ '<field name="p" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="oe_kanban_details">' +
+ '<div class="o_test_id">' +
+ '<field name="id"/>' +
+ '</div>' +
+ '<div class="o_test_foo">' +
+ '<field name="foo"/>' +
+ '</div>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Associated partners">' +
+ '<field name="foo"/>' +
+ '</form>',
+ },
+ res_id: 4,
+ });
+
+ assert.containsOnce(form, '.o_field_widget .o_kanban_view',
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 0,
+ "should not have kanban records yet");
+
+ // // switch to edit mode and create a new kanban record
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget .o-kanban-button-new'));
+
+ // save & close the modal
+ assert.strictEqual($('.modal-content input.o_field_widget').val(), 'My little Foo Value',
+ "should already have the default value for field foo");
+ await testUtils.dom.click($('.modal-content .btn-primary').first());
+
+ assert.containsOnce(form, '.o_field_widget .o_kanban_view',
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ "should now have one kanban record");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_id').text(),
+ '', "should not have a value for the id field");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_foo').text(),
+ 'My little Foo Value', "should have a value for the foo field");
+
+ // save the view to force a create of the new record in the one2many
+ await testUtils.form.clickSave(form);
+ assert.containsOnce(form, '.o_field_widget .o_kanban_view',
+ "should have one inner kanban view for the one2many field");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ "should now have one kanban record");
+ assert.notEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_id').text(),
+ '', "should now have a value for the id field");
+ assert.strictEqual(form.$('.o_field_widget .o_kanban_view .o_kanban_record:not(.o_kanban_ghost) .o_test_foo').text(),
+ 'My little Foo Value', "should still have a value for the foo field");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many field with virtual ids with kanban button', async function (assert) {
+ assert.expect(25);
+
+ testUtils.mock.patch(KanbanRecord, {
+ init: function () {
+ this._super.apply(this, arguments);
+ this._onKanbanActionClicked = this.__proto__._onKanbanActionClicked;
+ },
+ });
+
+ this.data.partner.records[0].p = [4];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p" mode="kanban">' +
+ '<kanban>' +
+ '<templates>' +
+ '<field name="foo"/>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<span><t t-esc="record.foo.value"/></span>' +
+ '<button type="object" class="btn btn-link fa fa-shopping-cart" name="button_warn" string="button_warn" warn="warn" />' +
+ '<button type="object" class="btn btn-link fa fa-shopping-cart" name="button_disabled" string="button_disabled" />' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="foo"/></form>',
+ },
+ res_id: 1,
+ services: {
+ notification: NotificationService.extend({
+ notify: function (params) {
+ assert.step(params.type);
+ }
+ }),
+ },
+ intercepts: {
+ execute_action: function (event) {
+ assert.step(event.data.action_data.name + '_' + event.data.env.model + '_' + event.data.env.currentID);
+ event.data.on_success();
+ },
+ },
+ });
+
+ // 1. Define all css selector
+ var oKanbanView = '.o_field_widget .o_kanban_view';
+ var oKanbanRecordActive = oKanbanView + ' .o_kanban_record:not(.o_kanban_ghost)';
+ var oAllKanbanButton = oKanbanRecordActive + ' button[data-type="object"]';
+ var btn1 = oKanbanRecordActive + ':nth-child(1) button[data-type="object"]';
+ var btn2 = oKanbanRecordActive + ':nth-child(2) button[data-type="object"]';
+ var btn1Warn = btn1 + '[data-name="button_warn"]';
+ var btn1Disabled = btn1 + '[data-name="button_disabled"]';
+ var btn2Warn = btn2 + '[data-name="button_warn"]';
+ var btn2Disabled = btn2 + '[data-name="button_disabled"]';
+
+ // check if we already have one kanban card
+ assert.containsOnce(form, oKanbanView, "should have one inner kanban view for the one2many field");
+ assert.containsOnce(form, oKanbanRecordActive, "should have one kanban records yet");
+
+ // we have 2 buttons
+ assert.containsN(form, oAllKanbanButton, 2, "should have 2 buttons type object");
+
+ // disabled ?
+ assert.containsNone(form, oAllKanbanButton + '[disabled]', "should not have button type object disabled");
+
+ // click on the button
+ await testUtils.dom.click(form.$(btn1Disabled));
+ await testUtils.dom.click(form.$(btn1Warn));
+
+ // switch to edit mode
+ await testUtils.form.clickEdit(form);
+
+ // click on existing buttons
+ await testUtils.dom.click(form.$(btn1Disabled));
+ await testUtils.dom.click(form.$(btn1Warn));
+
+ // create new kanban
+ await testUtils.dom.click(form.$('.o_field_widget .o-kanban-button-new'));
+
+ // save & close the modal
+ assert.strictEqual($('.modal-content input.o_field_widget').val(), 'My little Foo Value',
+ "should already have the default value for field foo");
+ await testUtils.dom.click($('.modal-content .btn-primary').first());
+
+ // check new item
+ assert.containsN(form, oAllKanbanButton, 4, "should have 4 buttons type object");
+ assert.containsN(form, btn1, 2, "should have 2 buttons type object in area 1");
+ assert.containsN(form, btn2, 2, "should have 2 buttons type object in area 2");
+ assert.containsOnce(form, oAllKanbanButton + '[disabled]', "should have 1 button type object disabled");
+
+ assert.strictEqual(form.$(btn2Disabled).attr('disabled'), 'disabled', 'Should have a button type object disabled in area 2');
+ assert.strictEqual(form.$(btn2Warn).attr('disabled'), undefined, 'Should have a button type object not disabled in area 2');
+ assert.strictEqual(form.$(btn2Warn).attr('warn'), 'warn', 'Should have a button type object with warn attr in area 2');
+
+ // click all buttons
+ await testUtils.dom.click(form.$(btn1Disabled));
+ await testUtils.dom.click(form.$(btn1Warn));
+ await testUtils.dom.click(form.$(btn2Disabled));
+ await testUtils.dom.click(form.$(btn2Warn));
+
+ // save the form
+ await testUtils.form.clickSave(form);
+
+ assert.containsNone(form, oAllKanbanButton + '[disabled]', "should not have button type object disabled after save");
+
+ // click all buttons
+ await testUtils.dom.click(form.$(btn1Disabled));
+ await testUtils.dom.click(form.$(btn1Warn));
+ await testUtils.dom.click(form.$(btn2Disabled));
+ await testUtils.dom.click(form.$(btn2Warn));
+
+ assert.verifySteps([
+ "button_disabled_partner_4",
+ "button_warn_partner_4",
+
+ "button_disabled_partner_4",
+ "button_warn_partner_4",
+
+ "button_disabled_partner_4",
+ "button_warn_partner_4",
+ "danger", // warn btn8
+
+ "button_disabled_partner_4",
+ "button_warn_partner_4",
+ "button_disabled_partner_5",
+ "button_warn_partner_5"
+ ], "should have triggered theses 11 clicks event");
+
+ testUtils.mock.unpatch(KanbanRecord);
+ form.destroy();
+ });
+
+ QUnit.test('focusing fields in one2many list', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('.o_data_row:first td:first'));
+ assert.strictEqual(form.$('input[name="turtle_foo"]')[0], document.activeElement,
+ "turtle foo field should have focus");
+
+ await testUtils.fields.triggerKeydown(form.$('input[name="turtle_foo"]'), 'tab');
+ assert.strictEqual(form.$('input[name="turtle_int"]')[0], document.activeElement,
+ "turtle int field should have focus");
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable = top', async function (assert) {
+ assert.expect(6);
+
+ this.data.turtle.fields.turtle_foo.default = "default foo turtle";
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands[0][0], 0,
+ "first command is a create");
+ assert.strictEqual(commands[1][0], 4,
+ "second command is a link to");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_data_row',
+ "should start with one data row");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 2,
+ "should have 2 data rows");
+ assert.strictEqual(form.$('tr.o_data_row:first input').val(), 'default foo turtle',
+ "first row should be the new value");
+ assert.hasClass(form.$('tr.o_data_row:first'),'o_selected_row',
+ "first row should be selected");
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable = bottom', async function (assert) {
+ assert.expect(6);
+ this.data.turtle.fields.turtle_foo.default = "default foo turtle";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var commands = args.args[1].turtles;
+ assert.strictEqual(commands[0][0], 4,
+ "first command is a link to");
+ assert.strictEqual(commands[1][0], 0,
+ "second command is a create");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_data_row',
+ "should start with one data row");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, '.o_data_row', 2,
+ "should have 2 data rows");
+ assert.strictEqual(form.$('tr.o_data_row:eq(1) input').val(), 'default foo turtle',
+ "second row should be the new value");
+ assert.hasClass(form.$('tr.o_data_row:eq(1)'),'o_selected_row',
+ "second row should be selected");
+
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('one2many list edition, no "Remove" button in modal', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.foo.default = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a'));
+ assert.containsOnce($(document), $('.modal'), 'there should be a modal opened');
+ assert.containsNone($('.modal .modal-footer .o_btn_remove'),
+ 'modal should not contain a "Remove" button');
+
+ // Discard a modal
+ await testUtils.dom.click($('.modal-footer .btn-secondary'));
+
+ await testUtils.form.clickDiscard(form);
+ form.destroy();
+ });
+
+ QUnit.test('x2many fields use their "mode" attribute', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<group>' +
+ '<field mode="kanban" name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<field name="turtle_int"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_one2many .o_kanban_view',
+ "should have rendered a kanban view");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable, onchange and required field', async function (assert) {
+ assert.expect(8);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.int_field = obj.turtles.length;
+ },
+ };
+ this.data.partner.records[0].int_field = 0;
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
+ "int_field should start with value 0");
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
+ "int_field should still be 0 (no onchange should have been done yet");
+
+ assert.verifySteps(['read', 'onchange']);
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text");
+ assert.verifySteps(['onchange']);
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1",
+ "int_field should now be 1 (the onchange should have been done");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable: trigger onchange when row is valid', async function (assert) {
+ // should omit require fields that aren't in the view as they (obviously)
+ // have no value, when checking the validity of required fields
+ // shouldn't consider numerical fields with value 0 as unset
+ assert.expect(13);
+
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.turtle.fields.turtle_qux.required = true; // required field not in the view
+ this.data.turtle.fields.turtle_bar.required = true; // required boolean field with no default
+ delete this.data.turtle.fields.turtle_bar.default;
+ this.data.turtle.fields.turtle_int.required = true; // required int field (default 0)
+ this.data.turtle.fields.turtle_int.default = 0;
+ this.data.turtle.fields.partner_ids.required = true; // required many2many
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.int_field = obj.turtles.length;
+ },
+ };
+ this.data.partner.records[0].int_field = 0;
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="turtles"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'turtle,false,list': '<tree editable="top">' +
+ '<field name="turtle_qux"/>' +
+ '<field name="turtle_bar"/>' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>',
+ },
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
+ "int_field should start with value 0");
+
+ // add a new row (which is invalid at first)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
+ "int_field should still be 0 (no onchange should have been done yet)");
+ assert.verifySteps(['load_views', 'read', 'onchange']);
+
+ // fill turtle_foo field
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text");
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "0",
+ "int_field should still be 0 (no onchange should have been done yet)");
+ assert.verifySteps([], "no onchange should have been applied");
+
+ // fill partner_ids field with a tag (all required fields will then be set)
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1",
+ "int_field should now be 1 (the onchange should have been done");
+ assert.verifySteps(['name_search', 'read', 'onchange']);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable: \'required\' modifiers is properly working', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.int_field = obj.turtles.length;
+ },
+ };
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10",
+ "int_field should start with value 10");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10",
+ "int_field should still be 10 (no onchange, because line is not valid)");
+
+ // fill turtle_foo field
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_foo"]'), "some text");
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1",
+ "int_field should be 1 (onchange triggered, because line is now valid)");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable: \'required\' modifiers is properly working, part 2', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.int_field = obj.turtles.length;
+ },
+ };
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo" attrs=\'{"required": [["turtle_int", "=", 0]]}\'/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10",
+ "int_field should start with value 10");
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "10",
+ "int_field should still be 10 (no onchange, because line is not valid)");
+
+ // fill turtle_int field
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtle_int"]'), "1");
+
+ assert.strictEqual(form.$('.o_field_widget[name="int_field"]').val(), "1",
+ "int_field should be 1 (onchange triggered, because line is now valid)");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many list editable: add new line before onchange returns', async function (assert) {
+ // If the user adds a new row (with a required field with onchange), selects
+ // a value for that field, then adds another row before the onchange returns,
+ // the editable list must wait for the onchange to return before trying to
+ // unselect the first row, otherwise it will be detected as invalid.
+ assert.expect(7);
+
+ this.data.turtle.onchanges = {
+ turtle_trululu: function () { },
+ };
+
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // add a first line but hold the onchange back
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ prom = testUtils.makeTestPromise();
+ assert.containsOnce(form, '.o_data_row',
+ "should have created the first row immediately");
+ await testUtils.fields.many2one.clickOpenDropdown('turtle_trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('turtle_trululu');
+
+ // try to add a second line and check that it is correctly waiting
+ // for the onchange to return
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal').length, 0, "no modal should be displayed");
+ assert.strictEqual($('.o_field_invalid').length, 0,
+ "no field should be marked as invalid");
+ assert.containsOnce(form, '.o_data_row',
+ "should wait for the onchange to create the second row");
+ assert.hasClass(form.$('.o_data_row'),'o_selected_row',
+ "first row should still be in edition");
+
+ // resolve the onchange promise
+ prom.resolve();
+ await testUtils.nextTick();
+ assert.containsN(form, '.o_data_row', 2,
+ "second row should now have been created");
+ assert.doesNotHaveClass(form.$('.o_data_row:first'), 'o_selected_row',
+ "first row should no more be in edition");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: multiple clicks on Add an item do not create invalid rows', async function (assert) {
+ assert.expect(3);
+
+ this.data.turtle.onchanges = {
+ turtle_trululu: function () { },
+ };
+
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_trululu" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+ prom = testUtils.makeTestPromise();
+ // click twice to add a new line
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsNone(form, '.o_data_row',
+ "no row should have been created yet (waiting for the onchange)");
+
+ // resolve the onchange promise
+ prom.resolve();
+ await testUtils.nextTick();
+ assert.containsOnce(form, '.o_data_row',
+ "only one row should have been created");
+ assert.hasClass(form.$('.o_data_row:first'),'o_selected_row',
+ "the created row should be in edition");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: value reset by an onchange', async function (assert) {
+ // this test reproduces a subtle behavior that may occur in a form view:
+ // the user adds a record in a one2many field, and directly clicks on a
+ // datetime field of the form view which has an onchange, which totally
+ // overrides the value of the one2many (commands 5 and 0). The handler
+ // that switches the edited row to readonly is then called after the
+ // new value of the one2many field is applied (the one returned by the
+ // onchange), so the row that must go to readonly doesn't exist anymore.
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ datetime: function (obj) {
+ obj.turtles = [[5], [0, 0, { display_name: 'new' }]];
+ },
+ };
+
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="datetime"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // trigger the two onchanges
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_data_row .o_field_widget'), 'a name');
+ prom = testUtils.makeTestPromise();
+ await testUtils.dom.click(form.$('.o_datepicker_input'));
+ var dateTimeVal = fieldUtils.format.datetime(moment(), { timezone: false });
+ await testUtils.fields.editSelect(form.$('.o_datepicker_input'), dateTimeVal);
+
+ // resolve the onchange promise
+ prom.resolve();
+ await testUtils.nextTick();
+
+ assert.containsOnce(form, '.o_data_row',
+ "should have one record in the o2m");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new',
+ "should be the record created by the onchange");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: onchange that returns a warning', async function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.onchanges = {
+ display_name: function () { },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step(args.method);
+ return Promise.resolve({
+ value: {},
+ warning: {
+ title: "Warning",
+ message: "You must first select a partner"
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ intercepts: {
+ warning: function () {
+ assert.step('warning');
+ },
+ },
+ });
+
+ // add a line (this should trigger an onchange and a warning)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ // check if 'Add an item' still works (this should trigger an onchange
+ // and a warning again)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.verifySteps(['onchange', 'warning', 'onchange', 'warning']);
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: contexts are correctly sent', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="timmy" context="{\'key\': parent.foo}">' +
+ '<tree editable="top">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'partner') {
+ assert.deepEqual(args.kwargs.context, {
+ active_field: 2,
+ bin_size: true,
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ if (args.method === 'read' && args.model === 'partner_type') {
+ assert.deepEqual(args.kwargs.context, {
+ key: 'yop',
+ active_field: 2,
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.kwargs.context, {
+ active_field: 2,
+ someKey: 'some value',
+ }, "sent context should be correct");
+ }
+ return this._super.apply(this, arguments);
+ },
+ session: {
+ user_context: { someKey: 'some value' },
+ },
+ viewOptions: {
+ mode: 'edit',
+ context: { active_field: 2 },
+ },
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$('.o_data_cell:first'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'abc');
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('resetting invisible one2manys', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].turtles = [];
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.turtles = [[5], [4, 1]];
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '<field name="turtles" invisible="1"/>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('input[name="foo"]'), 'abcd');
+ assert.verifySteps(['read', 'onchange']);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many: onchange that returns unknown field in list, but not in form', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.onchanges = {
+ name: function () { },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="name"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form string="Partners">' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ p: [[5], [0, 0, { display_name: 'new', timmy: [[5], [4, 12]] }]],
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "the one2many should contain one row");
+ assert.containsNone(form, '.o_field_widget[name="timmy"]',
+ "timmy should not be displayed in the list view");
+
+ await testUtils.dom.click(form.$('.o_data_row td:first'));
+
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"]').length, 1,
+ "timmy should be displayed in the form view");
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .badge').length, 1,
+ "m2mtags should contain one tag");
+ assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .o_badge_text').text(),
+ 'gold', "tag name should have been correctly loaded");
+
+ form.destroy();
+ });
+
+ QUnit.test('multi level of nested x2manys, onchange and rawChanges', async function (assert) {
+ assert.expect(8);
+
+ this.data.partner.records[0].p = [1];
+ this.data.partner.onchanges = {
+ name: function () { },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="name"/>
+ <field name="p">
+ <tree><field name="display_name"/></tree>
+ <form>
+ <field name="display_name"/>
+ <field name="p">
+ <tree><field name="display_name"/></tree>
+ <form><field name="display_name"/></form>
+ </field>
+ </form>
+ </field>
+ </form>`,
+ mockRPC(route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].p[0][2], {
+ p: [[1, 1, { display_name: 'new name' }]],
+ });
+ }
+ return this._super(...arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_data_row', "the one2many should contain one row");
+
+ // open the o2m record in readonly first
+ await testUtils.dom.click(form.$('.o_data_row td:first'));
+ assert.containsOnce(document.body, ".modal .o_form_readonly");
+ await testUtils.dom.click($('.modal .modal-footer .o_form_button_cancel'));
+
+ // switch to edit mode and open it again
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_row td:first'));
+
+ assert.containsOnce(document.body, ".modal .o_form_editable");
+ assert.containsOnce(document.body, '.modal .o_data_row', "the one2many should contain one row");
+
+ // open the o2m again, in the dialog
+ await testUtils.dom.click($('.modal .o_data_row td:first'));
+
+ assert.containsN(document.body, ".modal .o_form_editable", 2);
+
+ // edit the name and click save modal that is on top
+ await testUtils.fields.editInput($('.modal:nth(1) .o_field_widget[name=display_name]'), 'new name');
+ await testUtils.dom.click($('.modal:nth(1) .modal-footer .btn-primary'));
+
+ assert.containsOnce(document.body, ".modal .o_form_editable");
+
+ // click save on the other modal
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.containsNone(document.body, ".modal");
+
+ // save the main record
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange and required fields with override in arch', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ turtles: function () { }
+ };
+ this.data.turtle.fields.turtle_foo.required = true;
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo" required="0"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+ await testUtils.form.clickEdit(form);
+
+ // triggers an onchange on partner, because the new record is valid
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.verifySteps(['read', 'onchange', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('onchange on a one2many containing a one2many', async function (assert) {
+ // the purpose of this test is to ensure that the onchange specs are
+ // correctly and recursively computed
+ assert.expect(1);
+
+ this.data.partner.onchanges = {
+ p: function () { }
+ };
+ var checkOnchange = false;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree><field name="display_name"/></tree>' +
+ '<form>' +
+ '<field name="display_name"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom"><field name="display_name"/></tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange' && checkOnchange) {
+ assert.strictEqual(args.args[3]['p.p.display_name'], '',
+ "onchange specs should be computed recursively");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput($('.modal .o_data_cell input'), 'new record');
+ checkOnchange = true;
+ await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary'));
+
+ form.destroy();
+ });
+
+ QUnit.test('editing tabbed one2many (editable=bottom)', async function (assert) {
+ assert.expect(12);
+
+ this.data.partner.records[0].turtles = [];
+ for (var i = 0; i < 42; i++) {
+ var id = 100 + i;
+ this.data.turtle.records.push({ id: id, turtle_foo: 'turtle' + (id - 99) });
+ this.data.partner.records[0].turtles.push(id);
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].turtles[40][0], 0, 'should send a create command');
+ assert.deepEqual(args.args[1].turtles[40][2], { turtle_foo: 'rainbow dash' });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, 'tr.o_data_row', 41);
+ assert.hasClass(form.$('tr.o_data_row').last(), 'o_selected_row');
+
+ await testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
+ await testUtils.form.clickSave(form);
+
+ assert.containsN(form, 'tr.o_data_row', 40);
+
+ assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('editing tabbed one2many (editable=bottom), again...', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+ for (var i = 0; i < 9; i++) {
+ var id = 100 + i;
+ this.data.turtle.records.push({ id: id, turtle_foo: 'turtle' + (id - 99) });
+ this.data.partner.records[0].turtles.push(id);
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="3">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+
+ await testUtils.form.clickEdit(form);
+ // add a new record page 1 (this increases the limit to 4)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); // page 2: 4 records
+ await testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); // page 3: 2 records
+
+ assert.containsN(form, 'tr.o_data_row', 2,
+ "should have 2 data rows on the current page");
+ form.destroy();
+ });
+
+ QUnit.test('editing tabbed one2many (editable=top)', async function (assert) {
+ assert.expect(15);
+
+ this.data.partner.records[0].turtles = [];
+ this.data.turtle.fields.turtle_foo.default = "default foo";
+ for (var i = 0; i < 42; i++) {
+ var id = 100 + i;
+ this.data.turtle.records.push({ id: id, turtle_foo: 'turtle' + (id - 99) });
+ this.data.partner.records[0].turtles.push(id);
+ }
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].turtles[40][0], 0);
+ assert.deepEqual(args.args[1].turtles[40][2], { turtle_foo: 'rainbow dash' });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ assert.containsN(form, 'tr.o_data_row', 2);
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsN(form, 'tr.o_data_row', 3);
+
+ assert.hasClass(form.$('tr.o_data_row').first(), 'o_selected_row');
+
+ assert.strictEqual(form.$('tr.o_data_row input').val(), 'default foo',
+ "selected input should have correct string");
+
+ await testUtils.fields.editInput(form.$('.o_data_row input[name="turtle_foo"]'), 'rainbow dash');
+ await testUtils.form.clickSave(form);
+
+ assert.containsN(form, 'tr.o_data_row', 40);
+
+ assert.verifySteps(['read', 'read', 'read', 'onchange', 'write', 'read', 'read']);
+ form.destroy();
+ });
+
+ QUnit.test('one2many field: change value before pending onchange returns', async function (assert) {
+ assert.expect(2);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ this.data.partner.onchanges = {
+ int_field: function () { }
+ };
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="int_field"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ // delay the onchange RPC
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ prom = testUtils.makeTestPromise();
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), '44');
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ // set trululu before onchange
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one input'),
+ 'first', ['keydown', 'keyup']);
+ // complete the onchange
+ prom.resolve();
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'first',
+ 'should have kept the new value');
+ await testUtils.nextTick();
+ // check name_search result
+ assert.strictEqual($dropdown.find('li:not(.o_m2o_dropdown_option)').length, 1,
+ 'autocomplete should contains 1 suggestion');
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('focus is correctly reset after an onchange in an x2many', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ int_field: function () { }
+ };
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="int_field"/>' +
+ '<button string="hello"/>' +
+ '<field name="qux"/>' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ // delay the onchange RPC
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ prom = testUtils.makeTestPromise();
+ await testUtils.fields.editAndTrigger(form.$('.o_field_widget[name=int_field]'), '44',
+ ['input', { type: 'keydown', which: $.ui.keyCode.TAB }]);
+ prom.resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(document.activeElement, form.$('.o_field_widget[name=qux]')[0],
+ "qux field should have the focus");
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickHighlightedItem('trululu');
+ assert.strictEqual(form.$('.o_field_many2one input').val(), 'first record',
+ "the one2many field should have the expected value");
+
+ form.destroy();
+ });
+
+ QUnit.test('checkbox in an x2many that triggers an onchange', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = {
+ bar: function () { }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have a boolean field (owl) inside the
+ // x2many, so an update of the x2many requires to wait for 2 animation frames: one
+ // for the list to be re-rendered (with the boolean field) and one for the control
+ // panel.
+ await testUtils.owlCompatibilityNextTick();
+ await testUtils.dom.click(form.$('.o_field_widget[name=bar] input'));
+ assert.notOk(form.$('.o_field_widget[name=bar] input').prop('checked'),
+ "the checkbox should be unticked");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with default value: edit line to make it invalid', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.p.default = [
+ [0, false, { foo: "coucou", int_field: 5, p: [] }],
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // edit the line and enter an invalid value for int_field
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell:nth(1)'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 'e');
+ await testUtils.dom.click(form.$el);
+
+ assert.containsOnce(form, '.o_data_row.o_selected_row',
+ "line should not have been removed and should still be in edition");
+ assert.strictEqual($('.modal').length, 1,
+ "a confirmation dialog should be opened");
+ assert.hasClass(form.$('.o_field_widget[name=int_field]'),'o_field_invalid',
+ "should indicate that int_field is invalid");
+
+ form.destroy();
+ });
+
+ QUnit.test('default value for nested one2manys (coming from onchange)', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges.p = function (obj) {
+ obj.p = [
+ [5],
+ [0, 0, { turtles: [[5], [4, 1]] }], // link record 1 by default
+ ];
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree><field name="turtles"/></tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.strictEqual(args.args[0].p[0][0], 0,
+ "should send a command 0 (CREATE) for p");
+ assert.deepEqual(args.args[0].p[0][2], { turtles: [[4, 1, false]] },
+ "should send the correct values");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_cell').text(), '1 record',
+ "should correctly display the value of the inner o2m");
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('display correct value after validation error', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges.turtles = function () { };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ if (args.args[1].turtles[0][2].turtle_foo === 'pinky') {
+ // we simulate a validation error. In the 'real' web client,
+ // the server error will be used by the session to display
+ // an error dialog. From the point of view of the basic
+ // model, the promise is just rejected.
+ return Promise.reject();
+ }
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles[0], [1, 2, { turtle_foo: 'foo' }],
+ 'should send the "good" value');
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: { mode: 'edit' },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'blip',
+ "initial text should be correct");
+
+ // click and edit value to 'foo', which will trigger onchange
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell:nth(0)'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=turtle_foo]'), 'foo');
+ await testUtils.dom.click(form.$el);
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo',
+ "field should have been changed to foo");
+
+ // click and edit value to 'pinky', which trigger a failed onchange
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell:nth(0)'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=turtle_foo]'), 'pinky');
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo',
+ "turtle_foo text should now be set back to foo");
+
+ // we make sure here that when we save, the values are the current
+ // values displayed in the field.
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('propagate context to sub views without default_* keys', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.strictEqual(args.kwargs.context.flutter, 'shy',
+ 'view context key should be used for every rpcs');
+ if (args.method === 'onchange') {
+ if (args.model === 'partner') {
+ assert.strictEqual(args.kwargs.context.default_flutter, 'why',
+ "should have default_* values in context for form view RPCs");
+ } else if (args.model === 'turtle') {
+ assert.notOk(args.kwargs.context.default_flutter,
+ "should not have default_* values in context for subview RPCs");
+ }
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ context: {
+ flutter: 'shy',
+ default_flutter: 'why',
+ },
+ },
+ });
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'pinky pie');
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('nested one2manys with no widget in list and as invisible list in form', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [1];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="p">
+ <tree><field name="turtles"/></tree>
+ <form><field name="turtles" invisible="1"/></form>
+ </field>
+ </form>`,
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_data_row');
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), '1 record');
+
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsNone(document.body, '.modal .o_form_view .o_field_one2many');
+
+ // Test possible caching issues
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsNone(document.body, '.modal .o_form_view .o_field_one2many');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange on nested one2manys', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges.display_name = function (obj) {
+ if (obj.display_name) {
+ obj.p = [
+ [5],
+ [0, 0, {
+ display_name: 'test',
+ turtles: [[5], [0, 0, { display_name: 'test nested' }]],
+ }],
+ ];
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="display_name"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="turtles">' +
+ '<tree><field name="display_name"/></tree>' +
+ '</field>' +
+ '</form>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.strictEqual(args.args[0].p[0][0], 0,
+ "should send a command 0 (CREATE) for p");
+ assert.strictEqual(args.args[0].p[0][2].display_name, 'test',
+ "should send the correct values");
+ assert.strictEqual(args.args[0].p[0][2].turtles[0][0], 0,
+ "should send a command 0 (CREATE) for turtles");
+ assert.deepEqual(args.args[0].p[0][2].turtles[0][2], { display_name: 'test nested' },
+ "should send the correct values");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'trigger onchange');
+
+ assert.strictEqual(form.$('.o_data_cell').text(), 'test',
+ "should have added the new row to the one2many");
+
+ // open the new subrecord to check the value of the nested o2m, and to
+ // ensure that it will be saved
+ await testUtils.dom.click(form.$('.o_data_cell:first'));
+ assert.strictEqual($('.modal .o_data_cell').text(), 'test nested',
+ "should have added the new row to the nested one2many");
+ await testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary'));
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with multiple pages and sequence field', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+ this.data.partner.onchanges.turtles = function () { };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" invisible="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ turtles: [
+ [5],
+ [1, 1, { turtle_foo: "from onchange", partner_ids: [[5]] }],
+ ]
+ }
+ });
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ await testUtils.dom.click(form.$('.o_list_record_remove:first button'));
+ assert.strictEqual(form.$('.o_data_row').text(), 'from onchange',
+ 'onchange has been properly applied');
+ form.destroy();
+ });
+
+ QUnit.test('one2many with multiple pages and sequence field, part2', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+ this.data.partner.onchanges.turtles = function () { };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" invisible="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ return Promise.resolve({
+ value: {
+ turtles: [
+ [5],
+ [1, 1, { turtle_foo: "from onchange id2", partner_ids: [[5]] }],
+ [1, 3, { turtle_foo: "from onchange id3", partner_ids: [[5]] }],
+ ]
+ }
+ });
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ await testUtils.dom.click(form.$('.o_list_record_remove:first button'));
+ assert.strictEqual(form.$('.o_data_row').text(), 'from onchange id2from onchange id3',
+ 'onchange has been properly applied');
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, override default_get, bottom when inline', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+
+ this.data.turtle.fields.turtle_int.default = 10;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // starting condition
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
+
+ // click add a new line
+ // save the record
+ // check line is at the correct place
+
+ var inputText = 'ninja';
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText);
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, override default_get, top when inline', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+
+ this.data.turtle.fields.turtle_int.default = 10;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // starting condition
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
+
+ // click add a new line
+ // save the record
+ // check line is at the correct place
+
+ var inputText = 'ninja';
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText);
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual($('.o_data_cell').text(), inputText + "blipyopkawa");
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, override default_get, bottom when popup', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+
+ this.data.turtle.fields.turtle_int.default = 10;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '<form>' +
+ // NOTE: at some point we want to fix this in the framework so that an invisible field is not required.
+ '<field name="turtle_int" invisible="1"/>' +
+ '<field name="turtle_foo"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // starting condition
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa");
+
+ // click add a new line
+ // save the record
+ // check line is at the correct place
+
+ var inputText = 'ninja';
+ await testUtils.dom.click($('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput($('.o_input[name="turtle_foo"]'), inputText);
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
+
+ await testUtils.dom.click($('.o_form_button_save'));
+ assert.strictEqual($('.o_data_cell').text(), "blipyopkawa" + inputText);
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, override default_get, not last page', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+
+ this.data.turtle.fields.turtle_int.default = 10;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="turtle_int"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // click add a new line
+ // check turtle_int for new is the current max of the page
+ await testUtils.dom.click($('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_input[name="turtle_int"]').val(), '10');
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, override default_get, last page', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [3, 2, 1];
+
+ this.data.turtle.fields.turtle_int.default = 10;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="4">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="turtle_int"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // click add a new line
+ // check turtle_int for new is the current max of the page +1
+ await testUtils.dom.click($('.o_field_x2many_list_row_add a'));
+ assert.strictEqual($('.modal .o_input[name="turtle_int"]').val(), '22');
+ form.destroy();
+ });
+
+ QUnit.test('one2many with sequence field, fetch name_get from empty list, field text', async function (assert) {
+ // There was a bug where a RPC would fail because no route was set.
+ // The scenario is:
+ // - create a new parent model, which has a one2many
+ // - add at least 2 one2many lines which have:
+ // - a handle field
+ // - a many2one, which is not required, and we will leave it empty
+ // - reorder the lines with the handle
+ // -> This will call a resequence, which calls a name_get.
+ // -> With the bug that would fail, if it's ok the test will pass.
+
+ // This test will also make sure lists with
+ // FieldText (turtle_description) can be reordered with a handle.
+ // More specifically this will trigger a reset on a FieldText
+ // while the field is not in editable mode.
+ assert.expect(4);
+
+ this.data.turtle.fields.turtle_int.default = 10;
+ this.data.turtle.fields.product_id.default = 37;
+ this.data.turtle.fields.not_required_product_id = {
+ string: "Product",
+ type: "many2one",
+ relation: 'product'
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="not_required_product_id"/>' +
+ '<field name="turtle_description" widget="text"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // starting condition
+ assert.strictEqual($('.o_data_cell:nth-child(2)').text(), "");
+
+ var inputText1 = 'relax';
+ var inputText2 = 'max';
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText1);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), inputText2);
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual($('.o_data_cell:nth-child(2)').text(), inputText1 + inputText2);
+
+ var $handles = form.$('.ui-sortable-handle');
+
+ assert.equal($handles.length, 3, 'There should be 3 sequence handlers');
+
+ await testUtils.dom.dragAndDrop($handles.eq(1),
+ form.$('tbody tr').first(),
+ { position: 'top' }
+ );
+
+ assert.strictEqual($('.o_data_cell:nth-child(2)').text(), inputText2 + inputText1);
+
+ form.destroy();
+ });
+
+ QUnit.skip('one2many with several pages, onchange and default order', async function (assert) {
+ // This test reproduces a specific scenario where a one2many is displayed
+ // over several pages, and has a default order such that a record that
+ // would normally be on page 1 is actually on another page. Moreover,
+ // there is an onchange on that one2many which converts all commands 4
+ // (LINK_TO) into commands 1 (UPDATE), which is standard in the ORM.
+ // This test ensures that the record displayed on page 2 is never fully
+ // read.
+ assert.expect(8);
+
+ var data = this.data;
+ data.partner.records[0].turtles = [1, 2, 3];
+ data.turtle.records[0].partner_ids = [1];
+ data.partner.onchanges = {
+ turtles: function (obj) {
+ var res = _.map(obj.turtles, function (command) {
+ if (command[0] === 1) { // already an UPDATE command: do nothing
+ return command;
+ }
+ // convert LINK_TO commands to UPDATE commands
+ var id = command[1];
+ var record = _.findWhere(data.turtle.records, { id: id });
+ return [1, id, _.pick(record, ['turtle_int', 'turtle_foo', 'partner_ids'])];
+ });
+ obj.turtles = [[5]].concat(res);
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="top" limit="2" default_order="turtle_foo">' +
+ '<field name="turtle_int"/>' +
+ '<field name="turtle_foo" class="foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var ids = args.method === 'read' ? ' [' + args.args[0] + ']' : '';
+ assert.step(args.method + ids);
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
+ "should display two records out of three, in the correct order");
+
+ // edit turtle_int field of first row
+ await testUtils.dom.click(form.$('.o_data_cell:first'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=turtle_int]'), 3);
+ await testUtils.dom.click(form.$el);
+
+ assert.strictEqual(form.$('.o_data_cell.foo').text(), 'blipkawa',
+ "should still display the same two records");
+
+ assert.verifySteps([
+ 'read [1]', // main record
+ 'read [1,2,3]', // one2many (turtle_foo, all records)
+ 'read [2,3]', // one2many (all fields in view, records of first page)
+ 'read [2,4]', // many2many inside one2many (partner_ids), first page only
+ 'onchange',
+ 'read [1]', // AAB FIXME 4 (draft fixing taskid-2323491):
+ // this test's purpose is to assert that this rpc isn't
+ // done, but yet it is. Actually, it wasn't before because mockOnChange
+ // returned [1] as command list, instead of [[6, false, [1]]], so basically
+ // this value was ignored. Now that mockOnChange properly works, the value
+ // is taken into account but the basicmodel doesn't care it concerns a
+ // record of the second page, and does the read. I don't think we
+ // introduced a regression here, this test was simply wrong...
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('new record, with one2many with more default values than limit', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="2">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ context: { default_turtles: [1, 2, 3] },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
+ 'data has been properly loaded');
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
+ 'data has been properly saved');
+ form.destroy();
+ });
+
+ QUnit.test('add a new line after limit is reached should behave nicely', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, { turtle_foo: "yop" }],
+ [1, 2, { turtle_foo: "blip" }],
+ [1, 3, { turtle_foo: "kawa" }],
+ [0, obj.turtles[3][2], { turtle_foo: "abc" }],
+ ];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="3" editable="bottom">' +
+ '<field name="turtle_foo" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsN(form, '.o_data_row', 4, 'should have 4 data rows');
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), 'a');
+ assert.containsN(form, '.o_data_row', 4,
+ 'should still have 4 data rows (the limit is increased to 4)');
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange in a one2many with non inline view on an existing record', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.fields.sequence = { string: 'Sequence', type: 'integer' };
+ this.data.partner.records[0].sequence = 1;
+ this.data.partner.records[1].sequence = 2;
+ this.data.partner.onchanges = { sequence: function () { } };
+
+ this.data.partner_type.fields.partner_ids = { string: "Partner", type: "one2many", relation: 'partner' };
+ this.data.partner_type.records[0].partner_ids = [1, 2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner_type',
+ data: this.data,
+ arch: '<form><field name="partner_ids"/></form>',
+ archs: {
+ 'partner,false,list': '<tree string="Vendors">' +
+ '<field name="sequence" widget="handle"/>' +
+ '<field name="display_name"/>' +
+ '</tree>',
+ },
+ res_id: 12,
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: { mode: 'edit' },
+ });
+
+ // swap 2 lines in the one2many
+ await testUtils.dom.dragAndDrop(form.$('.ui-sortable-handle:eq(1)'), form.$('tbody tr').first(),
+ { position: 'top' });
+ assert.verifySteps(['load_views', 'read', 'read', 'onchange', 'onchange']);
+ form.destroy();
+ });
+
+ QUnit.test('onchange in a one2many with non inline view on a new record', async function (assert) {
+ assert.expect(6);
+
+ this.data.turtle.onchanges = {
+ display_name: function (obj) {
+ if (obj.display_name) {
+ obj.turtle_int = 44;
+ }
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="turtles"/></form>',
+ archs: {
+ 'turtle,false,list': '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // add a row and trigger the onchange
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_data_row .o_field_widget[name=display_name]'), 'a name');
+
+ assert.strictEqual(form.$('.o_data_row .o_field_widget[name=turtle_int]').val(), "44",
+ "should have triggered the onchange");
+
+ assert.verifySteps([
+ 'load_views', // load sub list
+ 'onchange', // main record
+ 'onchange', // sub record
+ 'onchange', // edition of display_name of sub record
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('add a line, edit it and "Save & New"', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree><field name="display_name"/></tree>' +
+ '<form><field name="display_name"/></form>' +
+ '</field>' +
+ '</form>',
+ });
+
+ assert.containsNone(form, '.o_data_row',
+ "there should be no record in the relation");
+
+ // add a new record
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput($('.modal .o_field_widget'), 'new record');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new record',
+ "should display the new record");
+
+ // reopen freshly added record and edit it
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell'));
+ await testUtils.fields.editInput($('.modal .o_field_widget'), 'new record edited');
+
+ // save it, and choose to directly create another record
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)'));
+
+ assert.strictEqual($('.modal').length, 1,
+ "the model should still be open");
+ assert.strictEqual($('.modal .o_field_widget').text(), '',
+ "should have cleared the input");
+
+ await testUtils.fields.editInput($('.modal .o_field_widget'), 'another new record');
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(),
+ 'new record editedanother new record', "should display the two records");
+
+ form.destroy();
+ });
+
+ QUnit.test('o2m add a line custom control create editable', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<control>' +
+ '<create string="Add food" context="" />' +
+ '<create string="Add pizza" context="{\'default_display_name\': \'pizza\'}"/>' +
+ '</control>' +
+
+ '<control>' +
+ '<create string="Add pasta" context="{\'default_display_name\': \'pasta\'}"/>' +
+ '</control>' +
+
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // new controls correctly added
+ var $td = form.$('.o_field_x2many_list_row_add');
+ assert.strictEqual($td.length, 1);
+ assert.strictEqual($td.closest('tr').find('td').length, 1);
+ assert.strictEqual($td.text(), "Add foodAdd pizzaAdd pasta");
+
+ // click add food
+ // check it's empty
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(0)'));
+ assert.strictEqual($('.o_data_cell').text(), "");
+
+ // click add pizza
+ // save the modal
+ // check it's pizza
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(1)'));
+ // click add pasta
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(2)'));
+ await testUtils.form.clickSave(form);
+ assert.strictEqual($('.o_data_cell').text(), "pizzapasta");
+
+ form.destroy();
+ });
+
+ QUnit.test('o2m add a line custom control create non-editable', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<control>' +
+ '<create string="Add food" context="" />' +
+ '<create string="Add pizza" context="{\'default_display_name\': \'pizza\'}" />' +
+ '</control>' +
+
+ '<control>' +
+ '<create string="Add pasta" context="{\'default_display_name\': \'pasta\'}" />' +
+ '</control>' +
+
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="display_name"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // new controls correctly added
+ var $td = form.$('.o_field_x2many_list_row_add');
+ assert.strictEqual($td.length, 1);
+ assert.strictEqual($td.closest('tr').find('td').length, 1);
+ assert.strictEqual($td.text(), "Add foodAdd pizzaAdd pasta");
+
+ // click add food
+ // check it's empty
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(0)'));
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ assert.strictEqual($('.o_data_cell').text(), "");
+
+ // click add pizza
+ // save the modal
+ // check it's pizza
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(1)'));
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ assert.strictEqual($('.o_data_cell').text(), "pizza");
+
+ // click add pasta
+ // save the whole record
+ // check it's pizzapasta
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(2)'));
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary:first'));
+ assert.strictEqual($('.o_data_cell').text(), "pizzapasta");
+
+ form.destroy();
+ });
+
+ QUnit.test('o2m add a line custom control create align with handle', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="int_field" widget="handle"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // controls correctly added, at one column offset when handle is present
+ var $tr = form.$('.o_field_x2many_list_row_add').closest('tr');
+ assert.strictEqual($tr.find('td').length, 2);
+ assert.strictEqual($tr.find('td:eq(0)').text(), "");
+ assert.strictEqual($tr.find('td:eq(1)').text(), "Add a line");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many form view with action button', async function (assert) {
+ // once the action button is clicked, the record is reloaded (via the
+ // on_close handler, executed because the python method does not return
+ // any action, or an ir.action.act_window_close) ; this test ensures that
+ // it reloads the fields of the opened view (i.e. the form in this case).
+ // See https://github.com/odoo/odoo/issues/24189
+ assert.expect(7);
+
+ var data = this.data;
+ data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ res_id: 1,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree><field name="display_name"/></tree>' +
+ '<form>' +
+ '<button type="action" string="Set Timmy"/>' +
+ '<field name="timmy"/>' +
+ '</form>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner_type,false,list': '<tree><field name="display_name"/></tree>',
+ },
+ intercepts: {
+ execute_action: function (ev) {
+ data.partner.records[1].display_name = 'new name';
+ data.partner.records[1].timmy = [12];
+ ev.data.on_closed();
+ },
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row',
+ "there should be one record in the one2many");
+ assert.strictEqual(form.$('.o_data_cell').text(), 'second record',
+ "initial display_name of o2m record should be correct");
+
+ // open one2many record in form view
+ await testUtils.dom.click(form.$('.o_data_cell:first'));
+ assert.strictEqual($('.modal .o_form_view').length, 1,
+ "should have opened the form view in a dialog");
+ assert.strictEqual($('.modal .o_form_view .o_data_row').length, 0,
+ "there should be no record in the many2many");
+
+ // click on the action button
+ await testUtils.dom.click($('.modal .o_form_view button'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ "fields in the o2m form view should have been read");
+ assert.strictEqual($('.modal .o_data_cell').text(), 'gold',
+ "many2many subrecord should have been fetched");
+
+ // save the dialog
+ await testUtils.dom.click($('.modal .modal-footer .btn-primary'));
+
+ assert.strictEqual(form.$('.o_data_cell').text(), 'new name',
+ "fields in the o2m list view should have been read as well");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange affecting inline unopened list view', async function (assert) {
+ // when we got onchange result for fields of record that were not
+ // already available because they were in a inline view not already
+ // opened, in a given configuration the change were applied ignoring
+ // existing data, thus a line of a one2many field inside a one2many
+ // field could be duplicated unexplectedly
+ assert.expect(5);
+
+ var numUserOnchange = 0;
+
+ this.data.user.onchanges = {
+ partner_ids: function (obj) {
+ if (numUserOnchange === 0) {
+ // simulate proper server onchange after save of modal with new record
+ obj.partner_ids = [
+ [5],
+ [1, 1, {
+ display_name: 'first record',
+ turtles: [
+ [5],
+ [1, 2, { 'display_name': 'donatello' }],
+ ],
+ }],
+ [1, 2, {
+ display_name: 'second record',
+ turtles: [
+ [5],
+ obj.partner_ids[1][2].turtles[0],
+ ],
+ }],
+ ];
+ }
+ numUserOnchange++;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'user',
+ data: this.data,
+ arch: '<form><sheet><group>' +
+ '<field name="partner_ids">' +
+ '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group></sheet></form>',
+ res_id: 17,
+ });
+
+ // add a turtle on second partner
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_row:eq(1)'));
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+ $('.modal input[name="display_name"]').val('michelangelo').change();
+ await testUtils.dom.click($('.modal .btn-primary'));
+ // open first partner so changes from previous action are applied
+ await testUtils.dom.click(form.$('.o_data_row:eq(0)'));
+ await testUtils.dom.click($('.modal .btn-primary'));
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(numUserOnchange, 2,
+ 'there should 2 and only 2 onchange from closing the partner modal');
+
+ await testUtils.dom.click(form.$('.o_data_row:eq(0)'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for first partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'donatello',
+ 'first partner turtle is donatello');
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ await testUtils.dom.click(form.$('.o_data_row:eq(1)'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for second partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'michelangelo',
+ 'second partner turtle is michelangelo');
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ form.destroy();
+ });
+
+ QUnit.test('click on URL should not open the record', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="display_name" widget="email"/>' +
+ '<field name="turtle_foo" widget="url"/>' +
+ '</tree>' +
+ '<form></form>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$('.o_email_cell a'));
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+
+ await testUtils.dom.click(form.$('.o_url_cell a'));
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+ form.destroy();
+ });
+
+ QUnit.test('create and edit on m2o in o2m, and press ESCAPE', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="display_name"/></form>',
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsOnce(form, '.o_selected_row',
+ "should have create a new row in edition");
+
+ await testUtils.fields.many2one.createAndEdit('turtle_trululu', "ABC");
+
+ assert.strictEqual($('.modal .o_form_view').length, 1,
+ "should have opened a form view in a dialog");
+
+ await testUtils.fields.triggerKeydown($('.modal .o_form_view .o_field_widget[name=display_name]'), 'escape');
+
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ "should have closed the dialog");
+ assert.containsOnce(form, '.o_selected_row',
+ "new row should still be present");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many add a line should not crash if orderedResIDs is not set', async function (assert) {
+ // There is no assertion, the code will just crash before the bugfix.
+ assert.expect(0);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<header>' +
+ '<button name="post" type="object" string="Validate" class="oe_highlight"/>' +
+ '</header>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ intercepts: {
+ execute_action: function (event) {
+ event.data.on_fail();
+ },
+ },
+ });
+
+ await testUtils.dom.click($('button[name="post"]'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ form.destroy();
+ });
+
+ QUnit.test('one2many shortcut tab should not crash when there is no input widget', async function (assert) {
+ assert.expect(2);
+
+ // create a one2many view which has no input (only 1 textarea in this case)
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo" widget="text"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ // add a row, fill it, then trigger the tab shortcut
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_input[name="turtle_foo"]'), 'ninja');
+ await testUtils.fields.triggerKeydown(form.$('.o_input[name="turtle_foo"]'), 'tab');
+
+ assert.strictEqual(form.$('.o_field_text').text(), 'blipninja',
+ 'current line should be saved');
+ assert.containsOnce(form, 'textarea.o_field_text',
+ 'new line should be created');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with onchange, required field, shortcut enter', async function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.onchanges = {
+ turtle_foo: function () { },
+ };
+
+ var prom;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo" required="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return Promise.resolve(prom).then(_.constant(result));
+ }
+ return result;
+ },
+ // simulate what happens in the client:
+ // the new value isn't notified directly to the model
+ fieldDebounce: 5000,
+ });
+
+ var value = "hello";
+
+ // add a new line
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ // we want to add a delay to simulate an onchange
+ prom = testUtils.makeTestPromise();
+
+ // write something in the field
+ var $input = form.$('input[name="turtle_foo"]');
+ await testUtils.fields.editInput($input, value);
+ await testUtils.fields.triggerKeydown($input, 'enter');
+
+ // check that nothing changed before the onchange finished
+ assert.strictEqual($input.val(), value, "input content shouldn't change");
+ assert.containsOnce(form, '.o_data_row',
+ "should still contain only one row");
+
+ // unlock onchange
+ prom.resolve();
+ await testUtils.nextTick();
+
+ // check the current line is added with the correct content and a new line is editable
+ assert.strictEqual(form.$('td.o_data_cell').text(), value);
+ assert.strictEqual(form.$('input[name="turtle_foo"]').val(), '');
+ assert.containsN(form, '.o_data_row', 2,
+ "should now contain two rows");
+
+ form.destroy();
+ });
+
+ QUnit.test('no deadlock when leaving a one2many line with uncommitted changes', async function (assert) {
+ // Before unselecting a o2m line, field widgets are asked to commit their changes (new values
+ // that they wouldn't have sent to the model yet). This test is added alongside a bug fix
+ // ensuring that we don't end up in a deadlock when a widget actually has some changes to
+ // commit at that moment.
+ assert.expect(9);
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ // we set a fieldDebounce to precisely mock the behavior of the webclient: changes are
+ // not sent to the model at keystrokes, but when the input is left
+ fieldDebounce: 5000,
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=turtles] input'), 'some foo value');
+
+ // click to add a second row to unselect the current one, then save
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.form.clickSave(form);
+
+ assert.containsOnce(form, '.o_form_readonly',
+ "form view should be in readonly");
+ assert.strictEqual(form.$('.o_data_row').text(), 'some foo value',
+ "foo field should have correct value");
+ assert.verifySteps([
+ 'onchange', // main record
+ 'onchange', // line 1
+ 'onchange', // line 2
+ 'create',
+ 'read', // main record
+ 'read', // line 1
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with extra field from server not in form', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form>' +
+ '<field name="display_name"/>' +
+ '</form>'
+ },
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ args.args[1].p[0][2].datetime = '2018-04-05 12:00:00';
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a record in the list
+ await testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a'));
+
+ var modal = $('.modal-lg');
+
+ var nameInput = modal.find('input.o_input[name=display_name]');
+ await testUtils.fields.editInput(nameInput, 'michelangelo');
+
+ // Save the record in the modal (though it is still virtual)
+ await testUtils.dom.click(modal.find('.btn-primary').first());
+
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ var newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '',
+ 'The create_date field should be empty');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ // Save the whole thing
+ await testUtils.form.clickSave(form);
+
+ x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Redo asserts in RO mode after saving
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00',
+ 'The create_date field should have the right value');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many invisible depends on parent field', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.test('column_invisible attrs on a button in a one2many list', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [2];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="product_id"/>
+ <field name="p">
+ <tree>
+ <field name="foo"/>
+ <button name="abc" string="Do it" class="some_button" attrs="{'column_invisible': [('parent.product_id', '=', False)]}"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), '');
+ assert.containsN(form, '.o_list_table th', 2); // foo + trash bin
+ assert.containsNone(form, '.some_button');
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone');
+ assert.containsN(form, '.o_list_table th', 3); // foo + button + trash bin
+ assert.containsOnce(form, '.some_button');
+
+ form.destroy();
+ });
+
+ QUnit.test('column_invisible attrs on adjacent buttons', async function (assert) {
+ assert.expect(14);
+
+ this.data.partner.records[0].p = [2];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="product_id"/>
+ <field name="trululu"/>
+ <field name="p">
+ <tree>
+ <button name="abc1" string="Do it 1" class="some_button1"/>
+ <button name="abc2" string="Do it 2" class="some_button2" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
+ <field name="foo"/>
+ <button name="abc3" string="Do it 3" class="some_button3" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}"/>
+ <button name="abc4" string="Do it 4" class="some_button4" attrs="{'column_invisible': [('parent.trululu', '!=', False)]}"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), '');
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa');
+ assert.containsN(form, '.o_list_table th', 4); // button group 1 + foo + button group 2 + trash bin
+ assert.containsOnce(form, '.some_button1');
+ assert.containsOnce(form, '.some_button2');
+ assert.containsOnce(form, '.some_button3');
+ assert.containsNone(form, '.some_button4');
+
+ await testUtils.fields.many2one.clickOpenDropdown('product_id');
+ await testUtils.fields.many2one.clickHighlightedItem('product_id');
+
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone');
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'aaa');
+ assert.containsN(form, '.o_list_table th', 3); // button group 1 + foo + trash bin
+ assert.containsOnce(form, '.some_button1');
+ assert.containsNone(form, '.some_button2');
+ assert.containsNone(form, '.some_button3');
+ assert.containsNone(form, '.some_button4');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many column visiblity depends on onchange of parent field', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[0].bar = false;
+
+ this.data.partner.onchanges.p = function (obj) {
+ // set bar to true when line is added
+ if (obj.p.length > 1 && obj.p[1][2].foo === 'New line') {
+ obj.bar = true;
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // bar is false so there should be 1 column
+ assert.containsOnce(form, 'th',
+ "should be only 1 column ('foo') in the one2many");
+ assert.containsOnce(form, '.o_list_view .o_data_row', "should contain one row");
+
+ await testUtils.form.clickEdit(form);
+
+ // add a new o2m record
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ form.$('.o_field_one2many input:first').focus();
+ await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line');
+ await testUtils.dom.click(form.$el);
+
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2, "should be 2 columns('foo' + 'int_field')");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many column_invisible on view not inline', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>',
+ },
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
+ await testUtils.dom.click($('li.ui-menu-item a:contains(xpad)').trigger('mouseenter'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.test('field context is correctly passed to x2m subviews', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles" context="{\'some_key\': 1}">' +
+ '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div>' +
+ '<t t-if="context.some_key">' +
+ '<field name="turtle_foo"/>' +
+ '</t>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1,
+ "should have a record in the relation");
+ assert.strictEqual(form.$('.o_kanban_record span:contains(blip)').length, 1,
+ "condition in the kanban template should have been correctly evaluated");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many kanban with widget handle', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<kanban>' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div><field name="turtle_foo"/></div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {
+ turtles: [
+ [1, 2, {turtle_int: 0}],
+ [1, 3, {turtle_int: 1}],
+ [1, 1, {turtle_int: 2}],
+ ],
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'yopblipkawa');
+ assert.doesNotHaveClass(form.$('.o_field_one2many .o_kanban_view'), 'ui-sortable');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.hasClass(form.$('.o_field_one2many .o_kanban_view'), 'ui-sortable');
+
+ var $record = form.$('.o_field_one2many[name=turtles] .o_kanban_view .o_kanban_record:first');
+ var $to = form.$('.o_field_one2many[name=turtles] .o_kanban_view .o_kanban_record:nth-child(3)');
+ await testUtils.dom.dragAndDrop($record, $to, {position: "bottom"});
+
+ assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').text(), 'blipkawayop');
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many editable list: edit and click on add a line', async function (assert) {
+ assert.expect(9);
+
+ this.data.turtle.onchanges = {
+ turtle_int: function () {},
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom"><field name="turtle_int"/></tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step('onchange');
+ }
+ return this._super.apply(this, arguments);
+ },
+ // in this test, we want to to accurately mock what really happens, that is, input
+ // fields only trigger their changes on 'change' event, not on 'input'
+ fieldDebounce: 100000,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsOnce(form, '.o_data_row');
+
+ // edit first row
+ await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell:first'));
+ assert.hasClass(form.$('.o_data_row:first'), 'o_selected_row');
+ await testUtils.fields.editInput(form.$('.o_selected_row input[name=turtle_int]'), '44');
+
+ assert.verifySteps([]);
+ // simulate a long click on 'Add a line' (mousedown [delay] mouseup and click events)
+ var $addLine = form.$('.o_field_x2many_list_row_add a');
+ testUtils.dom.triggerEvents($addLine, 'mousedown');
+ // mousedown is supposed to trigger the change event on the edited input, but it doesn't
+ // in the test environment, for an unknown reason, so we trigger it manually to reproduce
+ // what really happens
+ testUtils.dom.triggerEvents(form.$('.o_selected_row input[name=turtle_int]'), 'change');
+ await testUtils.nextTick();
+
+ // release the click
+ await testUtils.dom.triggerEvents($addLine, ['mouseup', 'click']);
+ assert.verifySteps(['onchange', 'onchange']);
+
+ assert.containsN(form, '.o_data_row', 2);
+ assert.strictEqual(form.$('.o_data_row:first').text(), '44');
+ assert.hasClass(form.$('.o_data_row:nth(1)'), 'o_selected_row');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2manys inside a one2many are fetched in batch after onchange', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, {
+ turtle_foo: "leonardo",
+ partner_ids: [[4, 2]],
+ }],
+ [1, 2, {
+ turtle_foo: "donatello",
+ partner_ids: [[4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ enableBasicModelBachedRPCs: true,
+ mockRPC: function (route, args) {
+ assert.step(args.method || route);
+ if (args.method === 'read') {
+ assert.deepEqual(args.args[0], [2, 4],
+ 'should read the partner_ids once, batched');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsN(form, '.o_data_row', 2);
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordsecondrecordaaa");
+
+ assert.verifySteps(['onchange', 'read']);
+
+ form.destroy();
+ });
+
+ QUnit.test('two one2many fields with same relation and onchanges', async function (assert) {
+ // this test simulates the presence of two one2many fields with onchanges, such that
+ // changes to the first o2m are repercuted on the second one
+ assert.expect(6);
+
+ this.data.partner.fields.turtles2 = {
+ string: "Turtles 2",
+ type: "one2many",
+ relation: 'turtle',
+ relation_field: 'turtle_trululu',
+ };
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ // when we add a line to turtles, add same line to turtles2
+ if (obj.turtles.length) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ obj.turtles2 = obj.turtles;
+ }
+ },
+ turtles2: function (obj) {
+ // simulate an onchange on turtles2 as well
+ if (obj.turtles2.length) {
+ obj.turtles2 = [[5]].concat(obj.turtles2);
+ }
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom"><field name="name" required="1"/></tree>' +
+ '</field>' +
+ '<field name="turtles2">' +
+ '<tree editable="bottom"><field name="name" required="1"/></tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ // trigger first onchange by adding a line in turtles field (should add a line in turtles2)
+ await testUtils.dom.click(form.$('.o_field_widget[name="turtles"] .o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtles"] .o_field_widget[name="name"]'), 'ABC');
+
+ assert.containsOnce(form, '.o_field_widget[name="turtles"] .o_data_row',
+ 'line of first o2m should have been created');
+ assert.containsOnce(form, '.o_field_widget[name="turtles2"] .o_data_row',
+ 'line of second o2m should have been created');
+
+ // add a line in turtles2
+ await testUtils.dom.click(form.$('.o_field_widget[name="turtles2"] .o_field_x2many_list_row_add a'));
+ await testUtils.fields.editInput(form.$('.o_field_widget[name="turtles2"] .o_field_widget[name="name"]'), 'DEF');
+
+ assert.containsOnce(form, '.o_field_widget[name="turtles"] .o_data_row',
+ 'we should still have 1 line in turtles');
+ assert.containsN(form, '.o_field_widget[name="turtles2"] .o_data_row', 2,
+ 'we should have 2 lines in turtles2');
+ assert.hasClass(form.$('.o_field_widget[name="turtles2"] .o_data_row:nth(1)'), 'o_selected_row',
+ 'second row should be in edition');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_widget[name="turtles2"] .o_data_row').text(), 'ABCDEF');
+
+ form.destroy();
+ });
+
+ QUnit.test('column widths are kept when adding first record in o2m', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="date"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ var width = form.$('th[data-name="date"]')[0].offsetWidth;
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.containsOnce(form, '.o_data_row');
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ form.destroy();
+ });
+
+ QUnit.test('column widths are kept when editing a record in o2m', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="date"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var width = form.$('th[data-name="date"]')[0].offsetWidth;
+
+ await testUtils.dom.click(form.$('.o_data_row .o_data_cell:first'));
+
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ var longVal = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed blandit, ' +
+ 'justo nec tincidunt feugiat, mi justo suscipit libero, sit amet tempus ipsum ' +
+ 'purus bibendum est.';
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), longVal);
+
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ form.destroy();
+ });
+
+ QUnit.test('column widths are kept when remove last record in o2m', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="date"/>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var width = form.$('th[data-name="date"]')[0].offsetWidth;
+
+ await testUtils.dom.click(form.$('.o_data_row .o_list_record_remove'));
+
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ form.destroy();
+ });
+
+ QUnit.test('column widths are correct after toggling optional fields', async function (assert) {
+ assert.expect(2);
+
+ var RamStorageService = AbstractStorageService.extend({
+ storage: new RamStorage(),
+ });
+
+ this.data.partner.records[0].p = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p">' +
+ '<tree editable="top">' +
+ '<field name="date" required="1"/>' + // we want the list to remain empty
+ '<field name="foo"/>' +
+ '<field name="int_field" optional="1"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ services: {
+ local_storage: RamStorageService,
+ },
+ });
+
+ // date fields have an hardcoded width, which apply when there is no
+ // record, and should be kept afterwards
+ let width = form.$('th[data-name="date"]')[0].offsetWidth;
+
+ // create a record to store the current widths, but discard it directly to keep
+ // the list empty (otherwise, the browser automatically computes the optimal widths)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ await testUtils.dom.click(form.$('.o_optional_columns_dropdown_toggle'));
+ await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item input'));
+
+ assert.strictEqual(form.$('th[data-name="date"]')[0].offsetWidth, width);
+
+ form.destroy();
+ });
+
+ QUnit.test('editable one2many list with oe_read_only button', async function (assert) {
+ assert.expect(9);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="turtles">
+ <tree editable="bottom">
+ <field name="turtle_foo"/>
+ <button name="do_it" type="object" class="oe_read_only"/>
+ </tree>
+ </field>
+ </form>`,
+ res_id: 1,
+ });
+
+ assert.containsN(form, '.o_list_view thead th:visible', 2);
+ assert.containsN(form, '.o_list_view tbody .o_data_row td:visible', 2);
+ assert.containsN(form, '.o_list_view tfoot td:visible', 2);
+ assert.containsNone(form, '.o_list_record_remove_header');
+
+ await testUtils.form.clickEdit(form);
+
+ // should have two visible columns in edit: foo + trash
+ assert.hasClass(form.$('.o_form_view'), 'o_form_editable');
+ assert.containsN(form, '.o_list_view thead th:visible', 2);
+ assert.containsN(form, '.o_list_view tbody .o_data_row td:visible', 2);
+ assert.containsN(form, '.o_list_view tfoot td:visible', 2);
+ assert.containsOnce(form, '.o_list_record_remove_header');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many reset by onchange (of another field) while being edited', async function (assert) {
+ // In this test, we have a many2one and a one2many. The many2one has an onchange that
+ // updates the value of the one2many. We set a new value to the many2one (name_create)
+ // such that the onchange is delayed. During the name_create, we click to add a new row
+ // to the one2many. After a while, we unlock the name_create, which triggers the onchange
+ // and resets the one2many. At the end, we want the row to be in edition.
+ assert.expect(3);
+
+ const prom = testUtils.makeTestPromise();
+ this.data.partner.onchanges = {
+ trululu: obj => {
+ obj.p = [[5]].concat(obj.p);
+ },
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="trululu"/>
+ <field name="p">
+ <tree editable="top"><field name="product_id" required="1"/></tree>
+ </field>
+ </form>`,
+ mockRPC: function (route, args) {
+ const result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ return prom.then(() => result);
+ }
+ return result;
+ },
+ });
+
+ // set a new value for trululu (will delay the onchange)
+ await testUtils.fields.many2one.searchAndClickItem('trululu', {search: 'new value'});
+
+ // add a row in p
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ assert.containsNone(form, '.o_data_row');
+
+ // resolve the name_create to trigger the onchange, and the reset of p
+ prom.resolve();
+ await testUtils.nextTick();
+ // use of owlCompatibilityNextTick because we have two sequential updates of the
+ // fieldX2Many: one because of the onchange, and one because of the click on add a line.
+ // As an update requires an update of the ControlPanel, which is an Owl Component, and
+ // waits for it, we need to wait for two animation frames before seeing the new line in
+ // the DOM
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, '.o_data_row');
+ assert.hasClass(form.$('.o_data_row'), 'o_selected_row');
+
+ form.destroy();
+ });
+
+ QUnit.skip('one2many with many2many_tags in list and list in form with a limit', async function (assert) {
+ // This test is skipped for now, as it doesn't work, and it can't be fixed in the current
+ // architecture (without large changes). However, this is unlikely to happen as the default
+ // limit is 80, and it would be useless to display so many records with a many2many_tags
+ // widget. So it would be nice if we could make it work in the future, but it's no big
+ // deal for now.
+ assert.expect(6);
+
+ this.data.partner.records[0].p = [1];
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p">
+ <tree>
+ <field name="turtles" widget="many2many_tags"/>
+ </tree>
+ <form>
+ <field name="turtles">
+ <tree limit="2"><field name="display_name"/></tree>
+ </field>
+ </form>
+ </field>
+ </form>`,
+ res_id: 1,
+ });
+
+ assert.containsOnce(form, '.o_field_widget[name=p] .o_data_row');
+ assert.containsN(form, '.o_data_row .o_field_many2manytags .badge', 3);
+
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsN(document.body, '.modal .o_field_widget[name=turtles] .o_data_row', 2);
+ assert.isVisible($('.modal .o_field_x2many_list .o_pager'));
+ assert.strictEqual($(".modal .o_field_x2many_list .o_pager").text().trim(), '1-2 / 3');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many_tags in list and list in form, and onchange', async function (assert) {
+ assert.expect(8);
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.p = [
+ [5],
+ [0, 0, {
+ turtles: [
+ [5],
+ [0, 0, {
+ display_name: 'new turtle',
+ }]
+ ],
+ }]
+ ];
+ },
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p">
+ <tree>
+ <field name="turtles" widget="many2many_tags"/>
+ </tree>
+ <form>
+ <field name="turtles">
+ <tree editable="bottom"><field name="display_name"/></tree>
+ </field>
+ </form>
+ </field>
+ </form>`,
+ });
+
+ assert.containsOnce(form, '.o_field_widget[name=p] .o_data_row');
+ assert.containsOnce(form, '.o_data_row .o_field_many2manytags .badge');
+
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+ assert.containsOnce(document.body, '.modal .o_field_widget[name=turtles] .o_data_row');
+ assert.strictEqual($('.modal .o_field_widget[name=turtles] .o_data_row').text(), 'new turtle');
+
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+ assert.containsN(document.body, '.modal .o_field_widget[name=turtles] .o_data_row', 2);
+ assert.strictEqual($('.modal .o_field_widget[name=turtles] .o_data_row:first').text(), 'new turtle');
+ assert.hasClass($('.modal .o_field_widget[name=turtles] .o_data_row:nth(1)'), 'o_selected_row');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many with many2many_tags in list and list in form, and onchange (2)', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.p = [
+ [5],
+ [0, 0, {
+ turtles: [
+ [5],
+ [0, 0, {
+ display_name: 'new turtle',
+ }]
+ ],
+ }]
+ ];
+ },
+ };
+ this.data.turtle.onchanges = {
+ turtle_foo: function (obj) {
+ obj.display_name = obj.turtle_foo;
+ },
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p">
+ <tree>
+ <field name="turtles" widget="many2many_tags"/>
+ </tree>
+ <form>
+ <field name="turtles">
+ <tree editable="bottom">
+ <field name="turtle_foo" required="1"/>
+ </tree>
+ </field>
+ </form>
+ </field>
+ </form>`,
+ });
+
+ assert.containsOnce(form, '.o_field_widget[name=p] .o_data_row');
+
+ await testUtils.dom.click(form.$('.o_data_row'));
+
+ assert.containsOnce(document.body, '.modal .o_form_view');
+
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+ assert.containsN(document.body, '.modal .o_field_widget[name=turtles] .o_data_row', 2);
+
+ await testUtils.fields.editInput($('.modal .o_selected_row input'), 'another one');
+ await testUtils.modal.clickButton('Save & Close');
+
+ assert.containsNone(document.body, '.modal');
+
+ assert.containsOnce(form, '.o_field_widget[name=p] .o_data_row');
+ assert.containsN(form, '.o_data_row .o_field_many2manytags .badge', 2);
+ assert.strictEqual(form.$('.o_data_row .o_field_many2manytags .o_badge_text').text(),
+ 'new turtleanother one');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many value returned by onchange with unknown fields', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ obj.p = [
+ [5],
+ [0, 0, {
+ bar: true,
+ display_name: "coucou",
+ trululu: [2, 'second record'],
+ turtles: [[5], [0, 0, {turtle_int: 4}]],
+ }]
+ ];
+ },
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="bar"/>
+ <field name="p" widget="many2many_tags"/>
+ </form>`,
+ mockRPC(route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0].p[0][2], {
+ bar: true,
+ display_name: "coucou",
+ trululu: 2,
+ turtles: [[5], [0, 0, {turtle_int: 4}]],
+ });
+ }
+ return this._super(...arguments);
+ },
+ });
+
+ assert.containsOnce(form, '.o_field_many2manytags .badge');
+ assert.strictEqual(form.$('.o_field_many2manytags .o_badge_text').text(), 'coucou');
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('mounted is called only once for x2many control panel', async function (assert) {
+ // This test could be removed as soon as the field widgets will be converted in owl.
+ // It comes with a fix for a bug that occurred because in some circonstances, 'mounted'
+ // is called twice for the x2many control panel.
+ // Specifically, this occurs when there is 'pad' widget in the form view, because this
+ // widget does a 'setValue' in its 'start', which thus resets the field x2many.
+ assert.expect(5);
+
+ const PadLikeWidget = fieldRegistry.get('char').extend({
+ start() {
+ this._setValue("some value");
+ }
+ });
+ fieldRegistry.add('pad_like', PadLikeWidget);
+
+ let resolveCP;
+ const prom = new Promise(r => {
+ resolveCP = r;
+ });
+ ControlPanel.patch('cp_patch_mock', T =>
+ class extends T {
+ constructor() {
+ super(...arguments);
+ owl.hooks.onMounted(() => {
+ assert.step('mounted');
+ });
+ owl.hooks.onWillUnmount(() => {
+ assert.step('willUnmount');
+ });
+ }
+ async update() {
+ // the issue is a race condition, so we manually delay the update to turn it deterministic
+ await prom;
+ super.update(...arguments);
+ }
+ }
+ );
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="foo" widget="pad_like"/>
+ <field name="p">
+ <tree><field name="display_name"/></tree>
+ </field>
+ </form>`,
+ viewOptions: {
+ withControlPanel: false, // s.t. there is only one CP: the one of the x2many
+ },
+ });
+
+ assert.verifySteps(['mounted']);
+
+ resolveCP();
+ await testUtils.nextTick();
+
+ assert.verifySteps([]);
+
+ ControlPanel.unpatch('cp_patch_mock');
+ delete fieldRegistry.map.pad_like;
+ form.destroy();
+
+ assert.verifySteps(["willUnmount"]);
+ });
+
+ QUnit.test('one2many: internal state is updated after another field changes', async function (assert) {
+ // The FieldOne2Many is configured such that it is reset at any field change.
+ // The MatrixProductConfigurator feature relies on that, and requires that its
+ // internal state is correctly updated. This white-box test artificially checks that.
+ assert.expect(2);
+
+ let o2m;
+ testUtils.patch(FieldOne2Many, {
+ init() {
+ this._super(...arguments);
+ o2m = this;
+ },
+ });
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="display_name"/>
+ <field name="p">
+ <tree><field name="display_name"/></tree>
+ </field>
+ </form>`,
+ });
+
+ assert.strictEqual(o2m.recordData.display_name, false);
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'val');
+
+ assert.strictEqual(o2m.recordData.display_name, "val");
+
+ form.destroy();
+ testUtils.unpatch(FieldOne2Many);
+ });
+
+ QUnit.test('nested one2many, onchange, no command value', async function (assert) {
+ // This test ensures that we always send all values to onchange rpcs for nested
+ // one2manys, even if some field hasn't changed. In this particular test case,
+ // a first onchange returns a value for the inner one2many, and a second onchange
+ // removes it, thus restoring the field to its initial empty value. From this point,
+ // the nested one2many value must still be sent to onchange rpcs (on the main record),
+ // as it might be used to compute other fields (so the fact that the nested o2m is empty
+ // must be explicit).
+ assert.expect(3);
+
+ this.data.turtle.fields.o2m = {
+ string: "o2m", type: "one2many", relation: 'partner', relation_field: 'trululu',
+ };
+ this.data.turtle.fields.turtle_bar.default = true;
+ this.data.partner.onchanges.turtles = function (obj) {};
+ this.data.turtle.onchanges.turtle_bar = function (obj) {
+ if (obj.turtle_bar) {
+ obj.o2m = [[5], [0, false, { display_name: "default" }]];
+ } else {
+ obj.o2m = [[5]];
+ }
+ };
+
+ let step = 1;
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="turtles">
+ <tree editable="bottom">
+ <field name="o2m"/>
+ <field name="turtle_bar"/>
+ </tree>
+ </field>
+ </form>`,
+ async mockRPC(route, args) {
+ if (step === 3 && args.method === 'onchange' && args.model === 'partner') {
+ assert.deepEqual(args.args[1].turtles[0][2], {
+ turtle_bar: false,
+ o2m: [], // we must send a value for this field
+ });
+ }
+ const result = await this._super(...arguments);
+ if (args.model === 'turtle') {
+ // sanity checks; this is what the onchanges on turtle must return
+ if (step === 2) {
+ assert.deepEqual(result.value, {
+ o2m: [[5], [0, false, { display_name: "default" }]],
+ turtle_bar: true,
+ });
+ }
+ if (step === 3) {
+ assert.deepEqual(result.value, {
+ o2m: [[5]],
+ });
+ }
+ }
+ return result;
+ },
+ });
+
+ step = 2;
+ await testUtils.dom.click(form.$('.o_field_x2many_list .o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because we have an x2many field with a boolean field
+ // (written in owl), so when we add a line, we sequentially render the list itself
+ // (including the boolean field), so we have to wait for the next animation frame, and
+ // then we render the control panel (also in owl), so we have to wait again for the
+ // next animation frame
+ await testUtils.owlCompatibilityNextTick();
+ step = 3;
+ await testUtils.dom.click(form.$('.o_data_row .o_field_boolean input'));
+
+ form.destroy();
+ });
+
+ QUnit.test('update a one2many from a custom field widget', async function (assert) {
+ // In this test, we define a custom field widget to render/update a one2many
+ // field. For the update part, we ensure that updating primitive fields of a sub
+ // record works. There is no guarantee that updating a relational field on the sub
+ // record would work. Deleting a sub record works as well. However, creating sub
+ // records isn't supported. There are obviously a lot of limitations, but the code
+ // hasn't been designed to support all this. This test simply encodes what can be
+ // done, and this comment explains what can't (and won't be implemented in stable
+ // versions).
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [1, 2];
+ const MyRelationalField = AbstractField.extend({
+ events: {
+ 'click .update': '_onUpdate',
+ 'click .delete': '_onDelete',
+ },
+ async _render() {
+ const records = await this._rpc({
+ method: 'read',
+ model: 'partner',
+ args: [this.value.res_ids],
+ });
+ this.$el.text(records.map(r => `${r.display_name}/${r.int_field}`).join(', '));
+ this.$el.append($('<button class="update fa fa-edit">'));
+ this.$el.append($('<button class="delete fa fa-trash">'));
+ },
+ _onUpdate() {
+ this._setValue({
+ operation: 'UPDATE',
+ id: this.value.data[0].id,
+ data: {
+ display_name: 'new name',
+ int_field: 44,
+ },
+ });
+ },
+ _onDelete() {
+ this._setValue({
+ operation: 'DELETE',
+ ids: [this.value.data[0].id],
+ });
+ },
+ });
+ fieldRegistry.add('my_relational_field', MyRelationalField);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <field name="p" widget="my_relational_field"/>
+ </form>`,
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=p]').text(), 'first record/10, second record/9');
+
+ await testUtils.dom.click(form.$('button.update'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=p]').text(), 'new name/44, second record/9');
+
+ await testUtils.dom.click(form.$('button.delete'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=p]').text(), 'second record/9');
+
+ form.destroy();
+ delete fieldRegistry.map.my_relational_field;
+ });
+
+ QUnit.test('reordering embedded one2many with handle widget starting with same sequence', async function (assert) {
+ assert.expect(3);
+
+ this.data.turtle = {
+ fields: {turtle_int: {string: "int", type: "integer", sortable: true}},
+ records: [
+ {id: 1, turtle_int: 1},
+ {id: 2, turtle_int: 1},
+ {id: 3, turtle_int: 1},
+ {id: 4, turtle_int: 2},
+ {id: 5, turtle_int: 3},
+ {id: 6, turtle_int: 4},
+ ],
+ };
+ this.data.partner.records[0].turtles = [1, 2, 3, 4, 5, 6];
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form string="Partners">
+ <sheet>
+ <notebook>
+ <page string="P page">
+ <field name="turtles">
+ <tree default_order="turtle_int">
+ <field name="turtle_int" widget="handle"/>
+ <field name="id"/>
+ </tree>
+ </field>
+ </page>
+ </notebook>
+ </sheet>
+ </form>`,
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "123456", "default should be sorted by id");
+
+ // Drag and drop the fourth line in first position
+ await testUtils.dom.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(3),
+ form.$('tbody tr').first(),
+ {position: 'top'}
+ );
+ assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "412356", "should still have the 6 rows in the correct order");
+
+ await testUtils.form.clickSave(form);
+
+ assert.deepEqual(_.map(this.data.turtle.records, function (turtle) {
+ return _.pick(turtle, 'id', 'turtle_int');
+ }), [
+ {id: 1, turtle_int: 2},
+ {id: 2, turtle_int: 3},
+ {id: 3, turtle_int: 4},
+ {id: 4, turtle_int: 1},
+ {id: 5, turtle_int: 5},
+ {id: 6, turtle_int: 6},
+ ], "should have saved the updated turtle_int sequence");
+
+ form.destroy();
+ });
+ });
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields_mobile_tests.js b/addons/web/static/tests/fields/relational_fields_mobile_tests.js
new file mode 100644
index 00000000..cf46aa2c
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields_mobile_tests.js
@@ -0,0 +1,66 @@
+odoo.define("web.relational_fields_mobile_tests", function (require) {
+"use strict";
+
+const FormView = require("web.FormView");
+const testUtils = require("web.test_utils");
+
+QUnit.module("fields", {}, function () {
+ QUnit.module("relational_fields", {
+ beforeEach() {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ p: {string: "one2many field", type: "one2many", relation: "partner", relation_field: "trululu"},
+ trululu: {string: "Trululu", type: "many2one", relation: "partner"},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ p: [2, 4],
+ trululu: 4,
+ }, {
+ id: 2,
+ display_name: "second record",
+ p: [],
+ trululu: 1,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ }],
+ },
+ };
+ },
+ }, function () {
+ QUnit.module("FieldOne2Many");
+
+ QUnit.test("one2many on mobile: display list if present without kanban view", async function (assert) {
+ assert.expect(2);
+
+ const form = await testUtils.createView({
+ View: FormView,
+ model: "partner",
+ data: this.data,
+ arch: `
+ <form>
+ <field name="p">
+ <tree>
+ <field name="display_name"/>
+ </tree>
+ </field>
+ </form>
+ `,
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, ".o_field_x2many_list",
+ "should display one2many's list");
+ assert.containsN(form, ".o_field_x2many_list .o_data_row", 2,
+ "should display 2 records in one2many's list");
+
+ form.destroy();
+ });
+ });
+});
+});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
new file mode 100644
index 00000000..7091cff0
--- /dev/null
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -0,0 +1,3679 @@
+odoo.define('web.relational_fields_tests', function (require) {
+"use strict";
+
+var AbstractStorageService = require('web.AbstractStorageService');
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var RamStorage = require('web.RamStorage');
+var relationalFields = require('web.relational_fields');
+var testUtils = require('web.test_utils');
+
+const cpHelpers = testUtils.controlPanel;
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('relational_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ bar: {string: "Bar", type: "boolean", default: true},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1] },
+ p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'},
+ turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu'},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
+ product_id: {string: "Product", type: "many2one", relation: 'product'},
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ string: "Color",
+ },
+ date: {string: "Some Date", type: "date"},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ user_id: {string: "User", type: 'many2one', relation: 'user'},
+ reference: {string: "Reference Field", type: 'reference', selection: [
+ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44,
+ p: [],
+ turtles: [2],
+ timmy: [],
+ trululu: 4,
+ user_id: 17,
+ reference: 'product,37',
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ qux: 13,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ product_id: 37,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ bar: false,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char"},
+ color: {string: "Color index", type: "integer"},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: {string: "Foo", type: "char"},
+ turtle_bar: {string: "Bar", type: "boolean", default: true},
+ turtle_int: {string: "int", type: "integer", sortable: true},
+ turtle_description: {string: "Description", type: "text"},
+ turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ turtle_ref: {string: "Reference", type: 'reference', selection: [
+ ["product", "Product"], ["partner", "Partner"]]},
+ product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
+ partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_bar: true,
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_bar: true,
+ turtle_foo: "blip",
+ turtle_int: 9,
+ partner_ids: [2,4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ product_id: 37,
+ turtle_bar: false,
+ turtle_foo: "kawa",
+ turtle_int: 21,
+ partner_ids: [],
+ turtle_ref: 'product,37',
+ }],
+ onchanges: {},
+ },
+ user: {
+ fields: {
+ name: {string: "Name", type: "char"},
+ partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'},
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ partner_ids: [1, 2],
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ },
+}, function () {
+
+ QUnit.test('search more pager is reset when doing a new search', async function (assert) {
+ assert.expect(6);
+
+ this.data.partner.records.push(
+ ...new Array(170).fill().map((_, i) => ({ id: i + 10, name: "Partner " + i }))
+ );
+ this.data.partner.fields.datetime.searchable = true;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="display_name"/></tree>',
+ 'partner,false,search': '<search><field name="datetime"/><field name="display_name"/></search>',
+ },
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickItem('trululu','Search');
+ await testUtils.dom.click($('.modal .o_pager_next'));
+
+ assert.strictEqual($('.o_pager_limit').text(), "1173", "there should be 173 records");
+ assert.strictEqual($('.o_pager_value').text(), "181-160", "should display the second page");
+ assert.strictEqual($('tr.o_data_row').length, 80, "should display 80 record");
+
+ await cpHelpers.editSearch('.modal', "first");
+ await cpHelpers.validateSearch('.modal');
+
+ assert.strictEqual($('.o_pager_limit').text(), "11", "there should be 1 record");
+ assert.strictEqual($('.o_pager_value').text(), "11-1", "should display the first page");
+ assert.strictEqual($('tr.o_data_row').length, 1, "should display 1 record");
+ form.destroy();
+ });
+
+ QUnit.test('do not call name_get if display_name already known', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.product_id.default = 37;
+ this.data.partner.onchanges = {
+ trululu: function (obj) {
+ obj.trululu = 1;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="trululu"/><field name="product_id"/></form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method + ' on ' + args.model);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=trululu] input').val(), 'first record');
+ assert.strictEqual(form.$('.o_field_widget[name=product_id] input').val(), 'xphone');
+ assert.verifySteps(['onchange on partner']);
+
+ form.destroy();
+ });
+
+ QUnit.test('x2many default_order multiple fields', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records = [
+ {int_field: 10, id: 1, display_name: "record1"},
+ {int_field: 12, id: 2, display_name: "record2"},
+ {int_field: 11, id: 3, display_name: "record3"},
+ {int_field: 12, id: 4, display_name: "record4"},
+ {int_field: 10, id: 5, display_name: "record5"},
+ {int_field: 10, id: 6, display_name: "record6"},
+ {int_field: 11, id: 7, display_name: "record7"},
+ ];
+
+ this.data.partner.records[0].p = [1, 7, 4, 5, 2, 6, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p" >' +
+ '<tree default_order="int_field,id">' +
+ '<field name="id"/>' +
+ '<field name="int_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $recordList = form.$('.o_field_x2many_list .o_data_row');
+ var expectedOrderId = ['1', '5', '6', '3', '7', '2', '4'];
+
+ _.each($recordList, function(record, index) {
+ var $record = $(record);
+ assert.strictEqual($record.find('.o_data_cell').eq(0).text(), expectedOrderId[index],
+ 'The record should be the right place. Index: ' + index);
+ });
+
+ form.destroy();
+ });
+
+ QUnit.test('focus when closing many2one modal in many2one modal', async function (assert) {
+ assert.expect(12);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu"/>' +
+ '</form>',
+ res_id: 2,
+ archs: {
+ 'partner,false,form': '<form><field name="trululu"/></form>'
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_id') {
+ return Promise.resolve(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // Open many2one modal
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_external_button'));
+
+ var $originalModal = $('.modal');
+ var $focusedModal = $(document.activeElement).closest('.modal');
+
+ assert.equal($originalModal.length, 1, 'There should be one modal');
+ assert.equal($originalModal[0], $focusedModal[0], 'Modal is focused');
+ assert.ok($('body').hasClass('modal-open'), 'Modal is said opened');
+
+ // Open many2one modal of field in many2one modal
+ await testUtils.dom.click($originalModal.find('.o_external_button'));
+ var $modals = $('.modal');
+ $focusedModal = $(document.activeElement).closest('.modal');
+
+ assert.equal($modals.length, 2, 'There should be two modals');
+ assert.equal($modals[1], $focusedModal[0], 'Last modal is focused');
+ assert.ok($('body').hasClass('modal-open'), 'Modal is said opened');
+
+ // Close second modal
+ await testUtils.dom.click($modals.last().find('button[class="close"]'));
+ var $modal = $('.modal');
+ $focusedModal = $(document.activeElement).closest('.modal');
+
+ assert.equal($modal.length, 1, 'There should be one modal');
+ assert.equal($modal[0], $originalModal[0], 'First modal is still opened');
+ assert.equal($modal[0], $focusedModal[0], 'Modal is focused');
+ assert.ok($('body').hasClass('modal-open'), 'Modal is said opened');
+
+ // Close first modal
+ await testUtils.dom.click($modal.find('button[class="close"]'));
+ $modal = $('.modal-dialog.modal-lg');
+
+ assert.equal($modal.length, 0, 'There should be no modal');
+ assert.notOk($('body').hasClass('modal-open'), 'Modal is not said opened');
+
+ form.destroy();
+ });
+
+
+ QUnit.test('one2many from a model that has been sorted', async function (assert) {
+ assert.expect(1);
+
+ /* On a standard list view, sort your records by a field
+ * Click on a record which contains a x2m with multiple records in it
+ * The x2m shouldn't take the orderedBy of the parent record (the one on the form)
+ */
+
+ this.data.partner.records[0].turtles = [3, 2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ context: {
+ orderedBy: [{
+ name: 'foo',
+ asc: false,
+ }]
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_one2many[name=turtles] tbody').text().trim(), "kawablip",
+ 'The o2m should not have been sorted.');
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_checkboxes in a subview', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<notebook>' +
+ '<page string="Turtles">' +
+ '<field name="turtles" mode="tree">' +
+ '<tree>' +
+ '<field name="id"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'turtle,false,form': '<form>' +
+ '<field name="partner_ids" widget="many2many_checkboxes"/>' +
+ '</form>',
+ },
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_cell'));
+ // edit the partner_ids field by (un)checking boxes on the widget
+ var $firstCheckbox = $('.modal .custom-control-input').first();
+ await testUtils.dom.click($firstCheckbox);
+ assert.ok($firstCheckbox.prop('checked'), "the checkbox should be ticked");
+ var $secondCheckbox = $('.modal .custom-control-input').eq(1);
+ await testUtils.dom.click($secondCheckbox);
+ assert.notOk($secondCheckbox.prop('checked'), "the checkbox should be unticked");
+ form.destroy();
+ });
+
+ QUnit.test('embedded readonly one2many with handle widget', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles" readonly="1">' +
+ '<tree editable="top">' +
+ '<field name="turtle_int" widget="handle"/>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_row_handle').length, 3,
+ "there should be 3 handles (one for each row)");
+ assert.strictEqual(form.$('.o_row_handle:visible').length, 0,
+ "the handles should be hidden in readonly mode");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.strictEqual(form.$('.o_row_handle').length, 3,
+ "the handles should still be there");
+ assert.strictEqual(form.$('.o_row_handle:visible').length, 0,
+ "the handles should still be hidden (on readonly fields)");
+
+ form.destroy();
+ });
+
+ QUnit.test('delete a record while adding another one in a multipage', async function (assert) {
+ // in a many2one with at least 2 pages, add a new line. Delete the line above it.
+ // (the onchange makes it so that the virtualID is inserted in the middle of the currentResIDs.)
+ // it should load the next line to display it on the page.
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [2, 3];
+ this.data.partner.onchanges.turtles = function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="1" decoration-muted="turtle_bar == False">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ // add a line (virtual record)
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
+ await testUtils.fields.editInput(form.$('.o_input'), 'pi');
+ // delete the line above it
+ await testUtils.dom.click(form.$('.o_list_record_remove').first());
+ await testUtils.owlCompatibilityNextTick();
+ // the next line should be displayed below the newly added one
+ assert.strictEqual(form.$('.o_data_row').length, 2, "should have 2 records");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:first-child').text(), 'pikawa',
+ "should display the correct records on page 1");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many, onchange, edition and multipage...', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ }
+ };
+
+ this.data.partner.records[0].turtles = [1,2,3];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom" limit="2">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(args.method + ' ' + args.model);
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+
+ assert.verifySteps([
+ 'read partner',
+ 'read turtle',
+ 'onchange turtle',
+ 'onchange partner',
+ 'onchange turtle',
+ 'onchange partner',
+ ]);
+ form.destroy();
+ });
+
+ QUnit.test('onchange on unloaded record clearing posterious change', async function (assert) {
+ // when we got onchange result for fields of record that were not
+ // already available because they were in a inline view not already
+ // opened, in a given configuration the change were applied ignoring
+ // posteriously changed data, thus an added/removed/modified line could
+ // be reset to the original onchange data
+ assert.expect(5);
+
+ var numUserOnchange = 0;
+
+ this.data.user.onchanges = {
+ partner_ids: function (obj) {
+ // simulate actual server onchange after save of modal with new record
+ if (numUserOnchange === 0) {
+ obj.partner_ids = _.clone(obj.partner_ids);
+ obj.partner_ids.unshift([5]);
+ obj.partner_ids[1][2].turtles.unshift([5]);
+ obj.partner_ids[2] = [1, 2, {
+ display_name: 'second record',
+ trululu: 1,
+ turtles: [[5]],
+ }];
+ } else if (numUserOnchange === 1) {
+ obj.partner_ids = _.clone(obj.partner_ids);
+ obj.partner_ids.unshift([5]);
+ obj.partner_ids[1][2].turtles.unshift([5]);
+ obj.partner_ids[2][2].turtles.unshift([5]);
+ }
+ numUserOnchange++;
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'user',
+ data: this.data,
+ arch: '<form><sheet><group>' +
+ '<field name="partner_ids">' +
+ '<form>'+
+ '<field name="trululu"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</group></sheet></form>',
+ res_id: 17,
+ });
+
+ // open first partner and change turtle name
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_data_row:eq(0)'));
+ await testUtils.dom.click($('.modal .o_data_cell:eq(0)'));
+ await testUtils.fields.editAndTrigger($('.modal input[name="display_name"]'),
+ 'Donatello', 'change');
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ await testUtils.dom.click(form.$('.o_data_row:eq(1)'));
+ await testUtils.dom.click($('.modal .o_field_x2many_list_row_add a'));
+ await testUtils.fields.editAndTrigger($('.modal input[name="display_name"]'),
+ 'Michelangelo', 'change');
+ await testUtils.dom.click($('.modal .btn-primary'));
+
+ assert.strictEqual(numUserOnchange, 2,
+ 'there should 2 and only 2 onchange from closing the partner modal');
+
+ // check first record still has change
+ await testUtils.dom.click(form.$('.o_data_row:eq(0)'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for first partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'Donatello',
+ 'first partner turtle is Donatello');
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ // check second record still has changes
+ await testUtils.dom.click(form.$('.o_data_row:eq(1)'));
+ assert.strictEqual($('.modal .o_data_row').length, 1,
+ 'only 1 turtle for second partner');
+ assert.strictEqual($('.modal .o_data_row').text(), 'Michelangelo',
+ 'second partner turtle is Michelangelo');
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ form.destroy();
+ });
+
+ QUnit.test('quickly switch between pages in one2many list', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1, 2, 3];
+
+ var readDefs = [Promise.resolve(), testUtils.makeTestPromise(), testUtils.makeTestPromise()];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree limit="1">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'read') {
+ var recordID = args.args[0][0];
+ return Promise.resolve(readDefs[recordID - 1]).then(_.constant(result));
+ }
+ return result;
+ },
+ res_id: 1,
+ });
+
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+ await testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next'));
+
+ readDefs[1].resolve();
+ await testUtils.nextTick();
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'donatello');
+
+ readDefs[2].resolve();
+ await testUtils.nextTick();
+
+ assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_data_cell').text(), 'raphael');
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many read, field context is properly sent', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.timmy.context = {hello: 'world'};
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'partner_type') {
+ assert.step(args.kwargs.context.hello);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.verifySteps(['world']);
+
+ await testUtils.form.clickEdit(form);
+ var $m2mInput = form.$('.o_field_many2manytags input');
+ $m2mInput.click();
+ await testUtils.nextTick();
+ $m2mInput.autocomplete('widget').find('li:first()').click();
+ await testUtils.nextTick();
+ assert.verifySteps(['world']);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldStatus');
+
+ QUnit.test('static statusbar widget on many2one field', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+ this.data.partner.records[1].bar = false;
+
+ var count = 0;
+ var nb_fields_fetched;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ // the following field seem useless, but its presence was the
+ // cause of a crash when evaluating the field domain.
+ '<field name="timmy" invisible="1"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ count++;
+ nb_fields_fetched = args.kwargs.fields.length;
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ assert.strictEqual(count, 1, 'once search_read should have been done to fetch the relational values');
+ assert.strictEqual(nb_fields_fetched, 1, 'search_read should only fetch field id');
+ assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 2);
+ assert.containsN(form, '.o_statusbar_status button:disabled', 2);
+ assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'btn-primary');
+ form.destroy();
+ });
+
+ QUnit.test('static statusbar widget on many2one field with domain', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" domain="[(\'user_id\',\'=\',uid)]" widget="statusbar"/></header>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ assert.deepEqual(args.kwargs.domain, ['|', ['id', '=', 4], ['user_id', '=', 17]],
+ "search_read should sent the correct domain");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ session: {user_context: {uid: 17}},
+ });
+
+ form.destroy();
+ });
+
+ QUnit.test('clickable statusbar widget on many2one field', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" options=\'{"clickable": "1"}\'/></header>' +
+ '</form>',
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+
+ assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'btn-primary');
+ assert.hasClass(form.$('.o_statusbar_status button[data-value="4"]'), 'disabled');
+
+ assert.containsN(form, '.o_statusbar_status button.btn-secondary:not(.dropdown-toggle):not(:disabled)', 2);
+
+ var $clickable = form.$('.o_statusbar_status button.btn-secondary:not(.dropdown-toggle):not(:disabled)');
+ await testUtils.dom.click($clickable.last()); // (last is visually the first here (css))
+
+ assert.hasClass(form.$('.o_statusbar_status button[data-value="1"]'), "btn-primary");
+ assert.hasClass(form.$('.o_statusbar_status button[data-value="1"]'), "disabled");
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with no status', async function (assert) {
+ assert.expect(2);
+
+ this.data.product.records = [];
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <header><field name="product_id" widget="statusbar"/></header>
+ </form>`,
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ assert.doesNotHaveClass(form.$('.o_statusbar_status'), 'o_field_empty');
+ assert.strictEqual(form.$('.o_statusbar_status').children().length, 0,
+ 'statusbar widget should be empty');
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with required modifier', async function (assert) {
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form string="Partners">
+ <header><field name="product_id" widget="statusbar" required="1"/></header>
+ </form>`,
+ config: {device: {isMobile: false}},
+ });
+ testUtils.intercept(form, 'call_service', function (ev) {
+ assert.strictEqual(ev.data.service, 'notification',
+ "should display an 'invalid fields' notification");
+ }, true);
+
+ testUtils.form.clickSave(form);
+
+ assert.containsOnce(form, '.o_form_editable', 'view should still be in edit');
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with no value in readonly', async function (assert) {
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `
+ <form>
+ <header><field name="product_id" widget="statusbar"/></header>
+ </form>`,
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ assert.doesNotHaveClass(form.$('.o_statusbar_status'), 'o_field_empty');
+ assert.containsN(form, '.o_statusbar_status button:visible', 2);
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with domain but no value (create mode)', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ '</form>',
+ config: {device: {isMobile: false}},
+ });
+
+ assert.containsN(form, '.o_statusbar_status button:disabled', 2);
+ form.destroy();
+ });
+
+ QUnit.test('clickable statusbar should change m2o fetching domain in edit mode', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.trululu.domain = "[('bar', '=', True)]";
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" options=\'{"clickable": "1"}\'/></header>' +
+ '</form>',
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 3);
+ await testUtils.dom.click(form.$('.o_statusbar_status button:not(.dropdown-toggle)').last());
+ assert.containsN(form, '.o_statusbar_status button:not(.dropdown-toggle)', 2);
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar fold_field option and statusbar_visible attribute', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].bar = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar" options="{\'fold_field\': \'bar\'}"/>' +
+ '<field name="color" widget="statusbar" statusbar_visible="red"/></header>' +
+ '</form>',
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_statusbar_status:first .dropdown-menu button.disabled');
+ assert.containsOnce(form, '.o_statusbar_status:last button.disabled');
+
+ form.destroy();
+ });
+
+ QUnit.test('statusbar with dynamic domain', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.trululu.domain = "[('int_field', '>', qux)]";
+ this.data.partner.records[2].int_field = 0;
+
+ var rpcCount = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form string="Partners">' +
+ '<header><field name="trululu" widget="statusbar"/></header>' +
+ '<field name="qux"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'search_read') {
+ rpcCount++;
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ config: {device: {isMobile: false}},
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_statusbar_status button.disabled', 3);
+ assert.strictEqual(rpcCount, 1, "should have done 1 search_read rpc");
+ await testUtils.fields.editInput(form.$('input[name=qux]'), 9.5);
+ assert.containsN(form, '.o_statusbar_status button.disabled', 2);
+ assert.strictEqual(rpcCount, 2, "should have done 1 more search_read rpc");
+ await testUtils.fields.editInput(form.$('input[name=qux]'), "hey");
+ assert.strictEqual(rpcCount, 2, "should not have done 1 more search_read rpc");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldSelection');
+
+ QUnit.test('widget selection in a list view', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records.forEach(function (r) {
+ r.color = 'red';
+ });
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Colors" editable="top">' +
+ '<field name="color"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('td:contains(Red)').length, 3,
+ "should have 3 rows with correct value");
+ await testUtils.dom.click(list.$('td:contains(Red):first'));
+
+ var $td = list.$('tbody tr.o_selected_row td:not(.o_list_record_selector)');
+
+ assert.strictEqual($td.find('select').length, 1, "td should have a child 'select'");
+ assert.strictEqual($td.contents().length, 1, "select tag should be only child of td");
+ list.destroy();
+ });
+
+ QUnit.test('widget selection, edition and on many2one field', async function (assert) {
+ assert.expect(21);
+
+ this.data.partner.onchanges = {product_id: function () {}};
+ this.data.partner.records[0].product_id = 37;
+ this.data.partner.records[0].trululu = false;
+
+ var count = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="product_id" widget="selection"/>' +
+ '<field name="trululu" widget="selection"/>' +
+ '<field name="color" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ count++;
+ assert.step(args.method);
+ return this._super(route, args);
+ },
+ });
+
+ assert.containsNone(form.$('.o_form_view'), 'select');
+ assert.strictEqual(form.$('.o_field_widget[name=product_id]').text(), 'xphone',
+ "should have rendered the many2one field correctly");
+ assert.strictEqual(form.$('.o_field_widget[name=product_id]').attr('raw-value'), '37',
+ "should have set the raw-value attr for many2one field correctly");
+ assert.strictEqual(form.$('.o_field_widget[name=trululu]').text(), '',
+ "should have rendered the unset many2one field correctly");
+ assert.strictEqual(form.$('.o_field_widget[name=color]').text(), 'Red',
+ "should have rendered the selection field correctly");
+ assert.strictEqual(form.$('.o_field_widget[name=color]').attr('raw-value'), 'red',
+ "should have set the raw-value attr for selection field correctly");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form.$('.o_form_view'), 'select', 3);
+ assert.containsOnce(form, 'select[name="product_id"] option:contains(xphone)',
+ "should have fetched xphone option");
+ assert.containsOnce(form, 'select[name="product_id"] option:contains(xpad)',
+ "should have fetched xpad option");
+ assert.strictEqual(form.$('select[name="product_id"]').val(), "37",
+ "should have correct product_id value");
+ assert.strictEqual(form.$('select[name="trululu"]').val(), "false",
+ "should not have any value in trululu field");
+ await testUtils.fields.editSelect(form.$('select[name="product_id"]'), 41);
+
+ assert.strictEqual(form.$('select[name="product_id"]').val(), "41",
+ "should have a value of xphone");
+
+ assert.strictEqual(form.$('select[name="color"]').val(), "\"red\"",
+ "should have correct value in color field");
+
+ assert.verifySteps(['read', 'name_search', 'name_search', 'onchange']);
+ count = 0;
+ await form.reload();
+ assert.strictEqual(count, 1, "should not reload product_id relation");
+ assert.verifySteps(['read']);
+
+ form.destroy();
+ });
+
+ QUnit.test('unset selection field with 0 as key', async function (assert) {
+ // The server doesn't make a distinction between false value (the field
+ // is unset), and selection 0, as in that case the value it returns is
+ // false. So the client must convert false to value 0 if it exists.
+ assert.expect(2);
+
+ this.data.partner.fields.selection = {
+ type: "selection",
+ selection: [[0, "Value O"], [1, "Value 1"]],
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="selection"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Value O',
+ "the displayed value should be 'Value O'");
+ assert.doesNotHaveClass(form.$('.o_field_widget'), 'o_field_empty',
+ "should not have class o_field_empty");
+
+ form.destroy();
+ });
+
+ QUnit.test('unset selection field with string keys', async function (assert) {
+ // The server doesn't make a distinction between false value (the field
+ // is unset), and selection 0, as in that case the value it returns is
+ // false. So the client must convert false to value 0 if it exists. In
+ // this test, it doesn't exist as keys are strings.
+ assert.expect(2);
+
+ this.data.partner.fields.selection = {
+ type: "selection",
+ selection: [['0', "Value O"], ['1', "Value 1"]],
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="selection"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget').text(), '',
+ "there should be no displayed value");
+ assert.hasClass(form.$('.o_field_widget'),'o_field_empty',
+ "should have class o_field_empty");
+
+ form.destroy();
+ });
+
+ QUnit.test('unset selection on a many2one field', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].trululu, false,
+ "should send 'false' as trululu value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ await testUtils.fields.editSelect(form.$('.o_form_view select'), 'false');
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('field selection with many2ones and special characters', async function (assert) {
+ assert.expect(1);
+
+ // edit the partner with id=4
+ this.data.partner.records[2].display_name = '<span>hey</span>';
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ });
+ assert.strictEqual(form.$('select option[value="4"]').text(), '<span>hey</span>');
+
+ form.destroy();
+ });
+
+ QUnit.test('widget selection on a many2one: domain updated by an onchange', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ int_field: function () {},
+ };
+
+ var domain = [];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu" widget="selection"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return Promise.resolve({
+ domain: {
+ trululu: domain,
+ }
+ });
+ }
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.args[1], domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsN(form, '.o_field_widget[name=trululu] option', 4,
+ "should be 4 options in the selection");
+
+ // trigger an onchange that will update the domain
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+
+ assert.containsOnce(form, '.o_field_widget[name=trululu] option',
+ "should be 1 option in the selection");
+
+ form.destroy();
+ });
+
+ QUnit.test('required selection widget should not have blank option', async function (assert) {
+ assert.expect(12);
+
+ this.data.partner.fields.feedback_value = {
+ type: "selection",
+ required: true,
+ selection : [['good', 'Good'], ['bad', 'Bad']],
+ default: 'good',
+ string: 'Good'
+ };
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="feedback_value"/>' +
+ '<field name="color" attrs="{\'required\': [(\'feedback_value\', \'=\', \'bad\')]}"/>' +
+ '</form>',
+ res_id: 1
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var $colorField = form.$('.o_field_widget[name=color]');
+ assert.containsN($colorField, 'option', 3, "Three options in non required field");
+
+ assert.hasAttrValue($colorField.find('option:first()'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
+
+ const $requiredSelect = form.$('.o_field_widget[name=feedback_value]');
+
+ assert.containsN($requiredSelect, 'option', 3, "Three options in required field");
+ assert.hasAttrValue($requiredSelect.find('option:first()'), 'style', "display: none",
+ "Should have display=none");
+ assert.hasAttrValue($requiredSelect.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($requiredSelect.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
+
+ // change value to update widget modifier values
+ await testUtils.fields.editSelect($requiredSelect, '"bad"');
+ $colorField = form.$('.o_field_widget[name=color]');
+
+ assert.containsN($colorField, 'option', 3, "Three options in required field");
+ assert.hasAttrValue($colorField.find('option:first()'), 'style', "display: none",
+ "Should have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(1)'), 'style', "",
+ "Should not have display=none");
+ assert.hasAttrValue($colorField.find('option:eq(2)'), 'style', "",
+ "Should not have display=none");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2ManyTags');
+
+ QUnit.test('fieldmany2many tags with and without color', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'};
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="partner_ids" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method ==='read' && args.model === 'partner_type') {
+ assert.deepEqual(args.args , [[12], ['display_name']], "should not read any color field");
+ } else if (args.method ==='read' && args.model === 'partner') {
+ assert.deepEqual(args.args , [[1], ['display_name', 'color']], "should read color field");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ // add a tag on field partner_ids
+ await testUtils.fields.many2one.clickOpenDropdown('partner_ids');
+ await testUtils.fields.many2one.clickHighlightedItem('partner_ids');
+
+ // add a tag on field timmy
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ var $input = form.$('.o_field_many2manytags[name="timmy"] input');
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 3,
+ "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')");
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+ assert.containsOnce(form, '.o_field_many2manytags[name="timmy"] .badge',
+ "should contain 1 tag");
+ assert.containsOnce(form, '.o_field_many2manytags[name="timmy"] .badge:contains("gold")',
+ "should contain newly added tag 'gold'");
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags with color: rendering and edition', async function (assert) {
+ assert.expect(28);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records.push({id: 13, display_name: "red", color: 8});
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\', \'no_create_edit\': True}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var commands = args.args[1].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ assert.ok(_.isEqual(_.sortBy(commands[0][2], _.identity.bind(_)), [12, 13]),
+ "new value should be [12, 13]");
+ }
+ if (args.method ==='read' && args.model === 'partner_type') {
+ assert.deepEqual(args.args[1], ['display_name', 'color'], "should read the color field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2,
+ "should contain 2 tags");
+ assert.ok(form.$('.badge .dropdown-toggle:contains(gold)').length,
+ 'should have fetched and rendered gold partner tag');
+ assert.ok(form.$('.badge .dropdown-toggle:contains(silver)').length,
+ 'should have fetched and rendered silver partner tag');
+ assert.strictEqual(form.$('.badge:first()').data('color'), 2,
+ 'should have correctly fetched the color');
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2,
+ "should still contain 2 tags in edit mode");
+ assert.ok(form.$('.o_tag_color_2 .o_badge_text:contains(gold)').length,
+ 'first tag should still contain "gold" and be color 2 in edit mode');
+ assert.containsN(form, '.o_field_many2manytags .o_delete', 2,
+ "tags should contain a delete button");
+
+ // add an other existing tag
+ var $input = form.$('.o_field_many2manytags input');
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 2,
+ "autocomplete dropdown should have 2 entry");
+ assert.strictEqual($input.autocomplete('widget').find('li a:contains("red")').length, 1,
+ "autocomplete dropdown should contain 'red'");
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+ assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 3,
+ "should contain 3 tags");
+ assert.ok(form.$('.o_field_many2manytags .badge .dropdown-toggle:contains("red")').length,
+ "should contain newly added tag 'red'");
+ assert.ok(form.$('.o_field_many2manytags .badge[data-color=8] .dropdown-toggle:contains("red")').length,
+ "should have fetched the color of added tag");
+
+ // remove tag with id 14
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge[data-id=14] .o_delete'));
+ assert.containsN(form, '.o_field_many2manytags .badge .dropdown-toggle', 2,
+ "should contain 2 tags");
+ assert.ok(!form.$('.o_field_many2manytags .badge .dropdown-toggle:contains("silver")').length,
+ "should not contain tag 'silver' anymore");
+
+ // save the record (should do the write RPC with the correct commands)
+ await testUtils.form.clickSave(form);
+
+ // checkbox 'Hide in Kanban'
+ $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // selects 'red' tag
+ await testUtils.dom.click($input);
+ var $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input');
+ assert.strictEqual($checkBox.length, 1, "should have a checkbox in the colorpicker dropdown menu");
+ assert.notOk($checkBox.is(':checked'), "should have unticked checkbox in colorpicker dropdown menu");
+
+ await testUtils.fields.editAndTrigger($checkBox, null,['mouseenter','mousedown']);
+
+ $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // refresh
+ await testUtils.dom.click($input);
+ $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input'); // refresh
+ assert.equal($input.parent().data('color'), "0", "should become transparent when toggling on checkbox");
+ assert.ok($checkBox.is(':checked'), "should have a ticked checkbox in colorpicker dropdown menu after mousedown");
+
+ await testUtils.fields.editAndTrigger($checkBox, null,['mouseenter','mousedown']);
+
+ $input = form.$('.o_field_many2manytags .badge[data-id=13] .dropdown-toggle'); // refresh
+ await testUtils.dom.click($input);
+ $checkBox = form.$('.o_field_many2manytags .badge[data-id=13] .custom-checkbox input'); // refresh
+ assert.equal($input.parent().data('color'), "8", "should revert to old color when toggling off checkbox");
+ assert.notOk($checkBox.is(':checked'), "should have an unticked checkbox in colorpicker dropdown menu after 2nd click");
+
+ // TODO: it would be nice to test the behaviors of the autocomplete dropdown
+ // (like refining the research, creating new tags...), but ui-autocomplete
+ // makes it difficult to test
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags in tree view', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
+ '</tree>',
+ });
+ assert.containsN(list, '.o_field_many2manytags .badge', 2, "there should be 2 tags");
+ assert.containsNone(list, '.badge.dropdown-toggle', "the tags should not be dropdowns");
+
+ testUtils.intercept(list, 'switch_view', function (event) {
+ assert.strictEqual(event.data.view_type, "form", "should switch to form view");
+ });
+ // click on the tag: should do nothing and open the form view
+ testUtils.dom.click(list.$('.o_field_many2manytags .badge:first'));
+
+ list.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags view a domain', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.fields.timmy.domain = [['id', '<', 50]];
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner_type.records.push({id: 99, display_name: "red", color: 8});
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'no_create_edit\': True}"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'name_search') {
+ assert.deepEqual(args.kwargs.args, [['id', '<', 50], ['id', 'not in', [12]]],
+ "domain sent to name_search should be correct");
+ return Promise.resolve([[14, 'silver']]);
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.containsOnce(form, '.o_field_many2manytags .badge',
+ "should contain 1 tag");
+ assert.ok(form.$('.badge:contains(gold)').length,
+ 'should have fetched and rendered gold partner tag');
+
+ await testUtils.form.clickEdit(form);
+
+ // add an other existing tag
+ var $input = form.$('.o_field_many2manytags input');
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ assert.strictEqual($input.autocomplete('widget').find('li').length, 2,
+ "autocomplete dropdown should have 2 entry");
+ assert.strictEqual($input.autocomplete('widget').find('li a:contains("silver")').length, 1,
+ "autocomplete dropdown should contain 'silver'");
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+ assert.containsN(form, '.o_field_many2manytags .badge', 2,
+ "should contain 2 tags");
+ assert.ok(form.$('.o_field_many2manytags .badge:contains("silver")').length,
+ "should contain newly added tag 'silver'");
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags in a new record', async function (assert) {
+ assert.expect(7);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/create') {
+ var commands = args.args[0].timmy;
+ assert.strictEqual(commands.length, 1, "should have generated one command");
+ assert.strictEqual(commands[0][0], 6, "generated command should be REPLACE WITH");
+ assert.ok(_.isEqual(commands[0][2], [12]), "new value should be [12]");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+ assert.hasClass(form.$('.o_form_view'),'o_form_editable', "form should be in edit mode");
+
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ assert.strictEqual(form.$('.o_field_many2manytags input').autocomplete('widget').find('li').length, 3,
+ "autocomplete dropdown should have 3 entries (2 values + 'Search and Edit...')");
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+
+ assert.containsOnce(form, '.o_field_many2manytags .badge',
+ "should contain 1 tag");
+ assert.ok(form.$('.o_field_many2manytags .badge:contains("gold")').length,
+ "should contain newly added tag 'gold'");
+
+ // save the record (should do the write RPC with the correct commands)
+ await testUtils.form.clickSave(form);
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags: update color', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].timmy = [12, 14];
+ this.data.partner_type.records[0].color = 0;
+
+ var color;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1], {color: color},
+ "shoud write the new color");
+ }
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ });
+
+ // First checks that default color 0 is rendered as 0 color
+ assert.ok(form.$('.badge.dropdown:first()').is('.o_tag_color_0'),
+ 'first tag color should be 0');
+
+ // Update the color in readonly
+ color = 1;
+ await testUtils.dom.click(form.$('.badge:first() .dropdown-toggle'));
+ await testUtils.dom.triggerEvents($('.o_colorpicker a[data-color="' + color + '"]'), ['mousedown']);
+ await testUtils.nextTick();
+ assert.strictEqual(form.$('.badge:first()').data('color'), color,
+ 'should have correctly updated the color (in readonly)');
+
+ // Update the color in edit
+ color = 6;
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.badge:first() .dropdown-toggle'));
+ await testUtils.dom.triggerEvents($('.o_colorpicker a[data-color="' + color + '"]'), ['mousedown']); // choose color 6
+ await testUtils.nextTick();
+ assert.strictEqual(form.$('.badge:first()').data('color'), color,
+ 'should have correctly updated the color (in edit)');
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags with no_edit_color option', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\', \'no_edit_color\': 1}"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // Click to try to open colorpicker
+ await testUtils.dom.click(form.$('.badge:first() .dropdown-toggle'));
+ assert.containsNone(document.body, '.o_colorpicker');
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags in editable list', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].timmy = [12];
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ context: {take: 'five'},
+ arch:'<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ if (args.method === 'read' && args.model === 'partner_type') {
+ assert.deepEqual(args.kwargs.context, {take: 'five'},
+ 'The context should be passed to the RPC');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.containsOnce(list, '.o_data_row:first .o_field_many2manytags .badge',
+ "m2m field should contain one tag");
+
+ // edit first row
+ await testUtils.dom.click(list.$('.o_data_row:first td:nth(2)'));
+
+ var $m2o = list.$('.o_data_row:first .o_field_many2manytags .o_field_many2one');
+ assert.strictEqual($m2o.length, 1, "a many2one widget should have been instantiated");
+
+ // add a tag
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+
+ assert.containsN(list, '.o_data_row:first .o_field_many2manytags .badge', 2,
+ "m2m field should contain 2 tags");
+
+ // leave edition
+ await testUtils.dom.click(list.$('.o_data_row:nth(1) td:nth(2)'));
+
+ assert.containsN(list, '.o_data_row:first .o_field_many2manytags .badge', 2,
+ "m2m field should contain 2 tags");
+
+ list.destroy();
+ });
+
+ QUnit.test('search more in many2one: group and use the pager', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records.push({
+ id: 5,
+ display_name: "Partner 4",
+ }, {
+ id: 6,
+ display_name: "Partner 5",
+ }, {
+ id: 7,
+ display_name: "Partner 6",
+ }, {
+ id: 8,
+ display_name: "Partner 7",
+ }, {
+ id: 9,
+ display_name: "Partner 8",
+ }, {
+ id: 10,
+ display_name: "Partner 9",
+ });
+ this.data.partner.fields.datetime.searchable = true;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="trululu"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree limit="7"><field name="display_name"/></tree>',
+ 'partner,false,search': '<search><group>' +
+ ' <filter name="bar" string="Bar" context="{\'group_by\': \'bar\'}"/>' +
+ '</group></search>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ await testUtils.fields.many2one.clickOpenDropdown('trululu');
+ await testUtils.fields.many2one.clickItem('trululu', 'Search');
+ await cpHelpers.toggleGroupByMenu('.modal');
+ await cpHelpers.toggleMenuItem('.modal', "Bar");
+
+ await testUtils.dom.click($('.modal .o_group_header:first'));
+
+ assert.strictEqual($('.modal tbody:nth(1) .o_data_row').length, 7,
+ "should display 7 records in the first page");
+ await testUtils.dom.click($('.modal .o_group_header:first .o_pager_next'));
+ assert.strictEqual($('.modal tbody:nth(1) .o_data_row').length, 1,
+ "should display 1 record in the second page");
+
+ form.destroy();
+ });
+
+ QUnit.test('many2many_tags can load more than 40 records', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'};
+ this.data.partner.records[0].partner_ids = [];
+ for (var i = 15; i < 115; i++) {
+ this.data.partner.records.push({id: i, display_name: 'walter' + i});
+ this.data.partner.records[0].partner_ids.push(i);
+ }
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.containsN(form, '.o_field_widget[name="partner_ids"] .badge', 100,
+ 'should have rendered 100 tags');
+ form.destroy();
+ });
+
+ QUnit.test('many2many_tags loads records according to limit defined on widget prototype', async function (assert) {
+ assert.expect(1);
+
+ const M2M_LIMIT = relationalFields.FieldMany2ManyTags.prototype.limit;
+ relationalFields.FieldMany2ManyTags.prototype.limit = 30;
+ this.data.partner.fields.partner_ids = {string: "Partner", type: "many2many", relation: 'partner'};
+ this.data.partner.records[0].partner_ids = [];
+ for (var i = 15; i < 50; i++) {
+ this.data.partner.records.push({id: i, display_name: 'walter' + i});
+ this.data.partner.records[0].partner_ids.push(i);
+ }
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="partner_ids" widget="many2many_tags"/></form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name="partner_ids"] .badge').length, 30,
+ 'should have rendered 30 tags even though 35 records linked');
+
+ relationalFields.FieldMany2ManyTags.prototype.limit = M2M_LIMIT;
+ form.destroy();
+ });
+
+ QUnit.test('field many2many_tags keeps focus when being edited', async function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner.onchanges.foo = function (obj) {
+ obj.timmy = [[5]]; // DELETE command
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="timmy" widget="many2many_tags"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, '.o_field_many2manytags .badge',
+ "should contain one tag");
+
+ // update foo, which will trigger an onchange and update timmy
+ // -> m2mtags input should not have taken the focus
+ form.$('input[name=foo]').focus();
+ await testUtils.fields.editInput(form.$('input[name=foo]'), 'trigger onchange');
+ assert.containsNone(form, '.o_field_many2manytags .badge',
+ "should contain no tags");
+ assert.strictEqual(form.$('input[name=foo]').get(0), document.activeElement,
+ "foo input should have kept the focus");
+
+ // add a tag -> m2mtags input should still have the focus
+ await testUtils.fields.many2one.clickOpenDropdown('timmy');
+ await testUtils.fields.many2one.clickHighlightedItem('timmy');
+
+
+ assert.containsOnce(form, '.o_field_many2manytags .badge',
+ "should contain a tag");
+ assert.strictEqual(form.$('.o_field_many2manytags input').get(0), document.activeElement,
+ "m2m tags input should have kept the focus");
+
+ // remove a tag -> m2mtags input should still have the focus
+ await testUtils.dom.click(form.$('.o_field_many2manytags .o_delete'));
+ assert.containsNone(form, '.o_field_many2manytags .badge',
+ "should contain no tags");
+ assert.strictEqual(form.$('.o_field_many2manytags input').get(0), document.activeElement,
+ "m2m tags input should have kept the focus");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags in one2many with display_name', async function (assert) {
+ assert.expect(4);
+ this.data.turtle.records[0].partner_ids = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' + // will use display_name
+ '</tree>' +
+ '<form>' +
+ '<sheet>' +
+ '<field name="partner_ids"/>' +
+ '</sheet>' +
+ '</form>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'partner,false,list': '<tree><field name="foo"/></tree>',
+ },
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should be correctly rendered");
+
+ // open the x2m form view
+ await testUtils.dom.click(form.$('.o_field_one2many[name="turtles"] .o_list_view td.o_data_cell:first'));
+ assert.strictEqual($('.modal .o_form_view .o_field_many2many[name="partner_ids"] .o_list_view .o_data_cell').text(),
+ "blipMy little Foo Value", "the list view should be correctly rendered with foo");
+
+ await testUtils.dom.click($('.modal button.o_form_button_cancel'));
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should still be correctly rendered");
+
+ await testUtils.form.clickEdit(form);
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_list_view .o_field_many2manytags[name="partner_ids"]').text().replace(/\s/g, ''),
+ "secondrecordaaa", "the tags should still be correctly rendered");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags: tags title attribute', async function (assert) {
+ assert.expect(1);
+ this.data.turtle.records[0].partner_ids = [2];
+
+ var form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch:'<form string="Turtles">' +
+ '<sheet>' +
+ '<field name="display_name"/>' +
+ '<field name="partner_ids" widget="many2many_tags"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.deepEqual(
+ form.$('.o_field_many2manytags.o_field_widget .badge .o_badge_text').attr('title'),
+ 'second record', 'the title should be filled in'
+ );
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags: toggle colorpicker multiple times', async function (assert) {
+ assert.expect(11);
+
+ this.data.partner.records[0].timmy = [12];
+ this.data.partner_type.records[0].color = 0;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="timmy" widget="many2many_tags" options="{\'color_field\': \'color\'}"/>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual($('.o_field_many2manytags .badge').length, 1,
+ "should have one tag");
+ assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0,
+ "tag should have color 0");
+ assert.strictEqual($('.o_colorpicker:visible').length, 0,
+ "colorpicker should be closed");
+
+ // click on the badge to open colorpicker
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle'));
+
+ assert.strictEqual($('.o_colorpicker:visible').length, 1,
+ "colorpicker should be open");
+
+ // click on the badge again to close colorpicker
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle'));
+
+ assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0,
+ "tag should still have color 0");
+ assert.strictEqual($('.o_colorpicker:visible').length, 0,
+ "colorpicker should be closed");
+
+ // click on the badge to open colorpicker
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle'));
+
+ assert.strictEqual($('.o_colorpicker:visible').length, 1,
+ "colorpicker should be open");
+
+ // click on the colorpicker, but not on a color
+ await testUtils.dom.click(form.$('.o_colorpicker'));
+
+ assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 0,
+ "tag should still have color 0");
+ assert.strictEqual($('.o_colorpicker:visible').length, 0,
+ "colorpicker should be closed");
+
+ // click on the badge to open colorpicker
+ await testUtils.dom.click(form.$('.o_field_many2manytags .badge .dropdown-toggle'));
+
+ // click on a color in the colorpicker
+ await testUtils.dom.triggerEvents(form.$('.o_colorpicker .o_tag_color_2'),['mousedown']);
+
+ assert.strictEqual($('.o_field_many2manytags .badge').data('color'), 2,
+ "tag should have color 2");
+ assert.strictEqual($('.o_colorpicker:visible').length, 0,
+ "colorpicker should be closed");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_tags_avatar', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="partner_ids" widget="many2many_tags_avatar"/>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 2,
+ });
+
+ assert.containsN(form, '.o_field_many2manytags.avatar.o_field_widget .badge', 2, "should have 2 records");
+ assert.strictEqual(form.$('.o_field_many2manytags.avatar.o_field_widget .badge:first img').data('src'), '/web/image/partner/2/image_128',
+ "should have correct avatar image");
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldmany2many tags: quick create a new record', async function (assert) {
+ assert.expect(3);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form><field name="timmy" widget="many2many_tags"/></form>`,
+ });
+
+ assert.containsNone(form, '.o_field_many2manytags .badge');
+
+ await testUtils.fields.many2one.searchAndClickItem('timmy', {search: 'new value'});
+
+ assert.containsOnce(form, '.o_field_many2manytags .badge');
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.el.querySelector('.o_field_many2manytags').innerText.trim(), "new value");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldRadio');
+
+ QUnit.test('fieldradio widget on a many2one in a new record', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id" widget="radio"/>' +
+ '</form>',
+ });
+
+ assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div");
+ assert.containsN(form, 'input.o_radio_input', 2, "should have 2 possible choices");
+ assert.ok(form.$('label.o_form_label:contains(xphone)').length, "one of them should be xphone");
+ assert.containsNone(form, 'input:checked', "none of the input should be checked");
+
+ await testUtils.dom.click(form.$("input.o_radio_input:first"));
+
+ assert.containsOnce(form, 'input:checked', "one of the input should be checked");
+
+ await testUtils.form.clickSave(form);
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.test('fieldradio change value by onchange', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {bar: function (obj) {
+ obj.product_id = obj.bar ? 41 : 37;
+ obj.color = obj.bar ? 'red' : 'black';
+ }};
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="product_id" widget="radio"/>' +
+ '<field name="color" widget="radio"/>' +
+ '</form>',
+ });
+
+ await testUtils.dom.click(form.$("input[type='checkbox']"));
+ assert.containsOnce(form, 'input.o_radio_input[data-value="37"]:checked', "one of the input should be checked");
+ assert.containsOnce(form, 'input.o_radio_input[data-value="black"]:checked', "the other of the input should be checked");
+ await testUtils.dom.click(form.$("input[type='checkbox']"));
+ assert.containsOnce(form, 'input.o_radio_input[data-value="41"]:checked', "the other of the input should be checked");
+ assert.containsOnce(form, 'input.o_radio_input[data-value="red"]:checked', "one of the input should be checked");
+
+ form.destroy();
+ });
+
+ QUnit.test('fieldradio widget on a selection in a new record', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="color" widget="radio"/>' +
+ '</form>',
+ });
+
+
+ assert.ok(form.$('div.o_radio_item').length, "should have rendered outer div");
+ assert.containsN(form, 'input.o_radio_input', 2, "should have 2 possible choices");
+ assert.ok(form.$('label.o_form_label:contains(Red)').length, "one of them should be Red");
+
+ // click on 2nd option
+ await testUtils.dom.click(form.$("input.o_radio_input").eq(1));
+
+ await testUtils.form.clickSave(form);
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.color, 'black', "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.test('fieldradio widget has o_horizontal or o_vertical class', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.color2 = this.data.partner.fields.color;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="color" widget="radio"/>' +
+ '<field name="color2" widget="radio" options="{\'horizontal\': True}"/>' +
+ '</group>' +
+ '</form>',
+ });
+
+ var btn1 = form.$('div.o_field_radio.o_vertical');
+ var btn2 = form.$('div.o_field_radio.o_horizontal');
+
+ assert.strictEqual(btn1.length, 1, "should have o_vertical class");
+ assert.strictEqual(btn2.length, 1, "should have o_horizontal class");
+ form.destroy();
+ });
+
+ QUnit.test('fieldradio widget with numerical keys encoded as strings', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.selection = {
+ type: 'selection',
+ selection: [['0', "Red"], ['1', "Black"]],
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="selection" widget="radio"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].selection, '1',
+ "should write correct value");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+
+ assert.strictEqual(form.$('.o_field_widget').text(), '',
+ "field should be unset");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsNone(form, '.o_radio_input:checked',
+ "no value should be checked");
+
+ await testUtils.dom.click(form.$("input.o_radio_input:nth(1)"));
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('.o_field_widget').text(), 'Black',
+ "value should be 'Black'");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, '.o_radio_input[data-index=1]:checked',
+ "'Black' should be checked");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget radio on a many2one: domain updated by an onchange', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges = {
+ int_field: function () {},
+ };
+
+ var domain = [];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="int_field"/>' +
+ '<field name="trululu" widget="radio"/>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ domain = [['id', 'in', [10]]];
+ return Promise.resolve({
+ value: {
+ trululu: false,
+ },
+ domain: {
+ trululu: domain,
+ },
+ });
+ }
+ if (args.method === 'search_read') {
+ assert.deepEqual(args.kwargs.domain, domain,
+ "sent domain should be correct");
+ }
+ return this._super(route, args);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsN(form, '.o_field_widget[name=trululu] .o_radio_item', 3,
+ "should be 3 radio buttons");
+
+ // trigger an onchange that will update the domain
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 2);
+ assert.containsNone(form, '.o_field_widget[name=trululu] .o_radio_item',
+ "should be no more radio button");
+
+ form.destroy();
+ });
+
+
+ QUnit.module('FieldSelectionBadge');
+
+ QUnit.test('FieldSelectionBadge widget on a many2one in a new record', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id" widget="selection_badge"/>' +
+ '</form>',
+ });
+
+ assert.ok(form.$('span.o_selection_badge').length, "should have rendered outer div");
+ assert.containsN(form, 'span.o_selection_badge', 2, "should have 2 possible choices");
+ assert.ok(form.$('span.o_selection_badge:contains(xphone)').length, "one of them should be xphone");
+ assert.containsNone(form, 'span.active', "none of the input should be checked");
+
+ await testUtils.dom.click($("span.o_selection_badge:first"));
+
+ assert.containsOnce(form, 'span.active', "one of the input should be checked");
+
+ await testUtils.form.clickSave(form);
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.product_id, 37, "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.test('FieldSelectionBadge widget on a selection in a new record', async function (assert) {
+ assert.expect(4);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="color" widget="selection_badge"/>' +
+ '</form>',
+ });
+
+ assert.ok(form.$('span.o_selection_badge').length, "should have rendered outer div");
+ assert.containsN(form, 'span.o_selection_badge', 2, "should have 2 possible choices");
+ assert.ok(form.$('span.o_selection_badge:contains(Red)').length, "one of them should be Red");
+
+ // click on 2nd option
+ await testUtils.dom.click(form.$("span.o_selection_badge").eq(1));
+
+ await testUtils.form.clickSave(form);
+
+ var newRecord = _.last(this.data.partner.records);
+ assert.strictEqual(newRecord.color, 'black', "should have saved record with correct value");
+ form.destroy();
+ });
+
+ QUnit.test('FieldSelectionBadge widget on a selection in a readonly mode', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="color" widget="selection_badge" readonly="1"/>' +
+ '</form>',
+ });
+
+ assert.containsOnce(form, 'span.o_readonly_modifier', "should have 1 possible value in readonly mode");
+ form.destroy();
+ });
+
+ QUnit.module('FieldSelectionFont');
+
+ QUnit.test('FieldSelectionFont displays the correct fonts on options', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.fields.fonts = {
+ type: "selection",
+ selection: [['Lato', "Lato"], ['Oswald', "Oswald"]],
+ default: 'Lato',
+ string: "Fonts",
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="fonts" widget="font"/>' +
+ '</form>',
+ });
+ var options = form.$('.o_field_widget[name="fonts"] > option');
+
+ assert.strictEqual(form.$('.o_field_widget[name="fonts"]').css('fontFamily'), 'Lato',
+ "Widget font should be default (Lato)");
+ assert.strictEqual($(options[0]).css('fontFamily'), 'Lato',
+ "Option 0 should have the correct font (Lato)");
+ assert.strictEqual($(options[1]).css('fontFamily'), 'Oswald',
+ "Option 1 should have the correct font (Oswald)");
+
+ await testUtils.fields.editSelect(form.$('.o_field_widget[name="fonts"]'), '"Oswald"');
+ assert.strictEqual(form.$('.o_field_widget[name="fonts"]').css('fontFamily'), 'Oswald',
+ "Widget font should be updated (Oswald)");
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2ManyCheckBoxes');
+
+ QUnit.test('widget many2many_checkboxes', async function (assert) {
+ assert.expect(10);
+
+ this.data.partner.records[0].timmy = [12];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<group><field name="timmy" widget="many2many_checkboxes"/></group>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.containsN(form, 'div.o_field_widget div.custom-checkbox', 2,
+ "should have fetched and displayed the 2 values of the many2many");
+
+ assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ assert.ok(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'),
+ "the checkboxes should be disabled");
+
+ await testUtils.form.clickEdit(form);
+
+ assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').prop('disabled'),
+ "the checkboxes should not be disabled");
+
+ // add a m2m value by clicking on input
+ await testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(1));
+ await testUtils.form.clickSave(form);
+ assert.deepEqual(this.data.partner.records[0].timmy, [12, 14],
+ "should have added the second element to the many2many");
+ assert.containsN(form, 'input:checked', 2,
+ "both checkboxes should be checked");
+
+ // remove a m2m value by clinking on label
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox > label').eq(0));
+ await testUtils.form.clickSave(form);
+ assert.deepEqual(this.data.partner.records[0].timmy, [14],
+ "should have removed the first element to the many2many");
+ assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should be checked");
+ assert.ok(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_checkboxes: start non empty, then remove twice', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].timmy = [12,14];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<group><field name="timmy" widget="many2many_checkboxes"/></group>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ });
+
+ await testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(0));
+ await testUtils.dom.click(form.$('div.o_field_widget div.custom-checkbox input').eq(1));
+ await testUtils.form.clickSave(form);
+ assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('div.o_field_widget div.custom-checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_checkboxes: values are updated when domain changes', async function (assert) {
+ assert.expect(5);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="int_field"/>
+ <field name="timmy" widget="many2many_checkboxes" domain="[['id', '>', int_field]]"/>
+ </form>`,
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_widget[name=int_field]').val(), '10');
+ assert.containsN(form, '.o_field_widget[name=timmy] .custom-checkbox', 2);
+ assert.strictEqual(form.$('.o_field_widget[name=timmy] .o_form_label').text(), 'goldsilver');
+
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 13);
+
+ assert.containsOnce(form, '.o_field_widget[name=timmy] .custom-checkbox');
+ assert.strictEqual(form.$('.o_field_widget[name=timmy] .o_form_label').text(), 'silver');
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_checkboxes with 40+ values', async function (assert) {
+ // 40 is the default limit for x2many fields. However, the many2many_checkboxes is a
+ // special field that fetches its data through the fetchSpecialData mechanism, and it
+ // uses the name_search server-side limit of 100. This test comes with a fix for a bug
+ // that occurred when the user (un)selected a checkbox that wasn't in the 40 first checkboxes,
+ // because the piece of data corresponding to that checkbox hadn't been processed by the
+ // BasicModel, whereas the code handling the change assumed it had.
+ assert.expect(3);
+
+ const records = [];
+ for (let id = 1; id <= 90; id++) {
+ records.push({
+ id,
+ display_name: `type ${id}`,
+ color: id % 7,
+ });
+ }
+ this.data.partner_type.records = records;
+ this.data.partner.records[0].timmy = records.map((r) => r.id);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="timmy" widget="many2many_checkboxes"/></form>',
+ res_id: 1,
+ async mockRPC(route, args) {
+ if (args.method === 'write') {
+ const expectedIds = records.map((r) => r.id);
+ expectedIds.pop();
+ assert.deepEqual(args.args[1].timmy, [[6, false, expectedIds]]);
+ }
+ return this._super(...arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsN(form, '.o_field_widget[name=timmy] input[type=checkbox]:checked', 90);
+
+ // toggle the last value
+ await testUtils.dom.click(form.$('.o_field_widget[name=timmy] input[type=checkbox]:last'));
+ assert.notOk(form.$('.o_field_widget[name=timmy] input[type=checkbox]:last').is(':checked'));
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('widget many2many_checkboxes with 100+ values', async function (assert) {
+ // The many2many_checkboxes widget limits the displayed values to 100 (this is the
+ // server-side name_search limit). This test encodes a scenario where there are more than
+ // 100 records in the co-model, and all values in the many2many relationship aren't
+ // displayed in the widget (due to the limit). If the user (un)selects a checkbox, we don't
+ // want to remove all values that aren't displayed from the relation.
+ assert.expect(5);
+
+ const records = [];
+ for (let id = 1; id < 150; id++) {
+ records.push({
+ id,
+ display_name: `type ${id}`,
+ color: id % 7,
+ });
+ }
+ this.data.partner_type.records = records;
+ this.data.partner.records[0].timmy = records.map((r) => r.id);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="timmy" widget="many2many_checkboxes"/></form>',
+ res_id: 1,
+ async mockRPC(route, args) {
+ if (args.method === 'write') {
+ const expectedIds = records.map((r) => r.id);
+ expectedIds.shift();
+ assert.deepEqual(args.args[1].timmy, [[6, false, expectedIds]]);
+ }
+ const result = await this._super(...arguments);
+ if (args.method === 'name_search') {
+ assert.strictEqual(result.length, 100,
+ "sanity check: name_search automatically sets the limit to 100");
+ }
+ return result;
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.containsN(form, '.o_field_widget[name=timmy] input[type=checkbox]', 100,
+ "should only display 100 checkboxes");
+ assert.ok(form.$('.o_field_widget[name=timmy] input[type=checkbox]:first').is(':checked'));
+
+ // toggle the first value
+ await testUtils.dom.click(form.$('.o_field_widget[name=timmy] input[type=checkbox]:first'));
+ assert.notOk(form.$('.o_field_widget[name=timmy] input[type=checkbox]:first').is(':checked'));
+
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldMany2ManyBinaryMultiFiles');
+
+ QUnit.test('widget many2many_binary', async function (assert) {
+ assert.expect(16);
+ this.data['ir.attachment'] = {
+ fields: {
+ name: {string:"Name", type: "char"},
+ mimetype: {string: "Mimetype", type: "char"},
+ },
+ records: [{
+ id: 17,
+ name: 'Marley&Me.jpg',
+ mimetype: 'jpg',
+ }],
+ };
+ this.data.turtle.fields.picture_ids = {
+ string: "Pictures",
+ type: "many2many",
+ relation: 'ir.attachment',
+ };
+ this.data.turtle.records[0].picture_ids = [17];
+
+ var form = await createView({
+ View: FormView,
+ model: 'turtle',
+ data: this.data,
+ arch:'<form string="Turtles">' +
+ '<group><field name="picture_ids" widget="many2many_binary" options="{\'accepted_file_extensions\': \'image/*\'}"/></group>' +
+ '</form>',
+ archs: {
+ 'ir.attachment,false,list': '<tree string="Pictures"><field name="name"/></tree>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ assert.step(route);
+ if (route === '/web/dataset/call_kw/ir.attachment/read') {
+ assert.deepEqual(args.args[1], ['name', 'mimetype']);
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.containsOnce(form, 'div.o_field_widget.oe_fileupload',
+ "there should be the attachment widget");
+ assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .o_attachments').children().length, 1,
+ "there should be no attachment");
+ assert.containsNone(form, 'div.o_field_widget.oe_fileupload .o_attach',
+ "there should not be an Add button (readonly)");
+ assert.containsNone(form, 'div.o_field_widget.oe_fileupload .o_attachment .o_attachment_delete',
+ "there should not be a Delete button (readonly)");
+
+ // to edit mode
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'div.o_field_widget.oe_fileupload .o_attach',
+ "there should be an Add button");
+ assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .o_attach').text().trim(), "Pictures",
+ "the button should be correctly named");
+ assert.containsOnce(form, 'div.o_field_widget.oe_fileupload .o_hidden_input_file form',
+ "there should be a hidden form to upload attachments");
+
+ assert.strictEqual(form.$('input.o_input_file').attr('accept'), 'image/*',
+ "there should be an attribute \"accept\" on the input")
+
+ // TODO: add an attachment
+ // no idea how to test this
+
+ // delete the attachment
+ await testUtils.dom.click(form.$('div.o_field_widget.oe_fileupload .o_attachment .o_attachment_delete'));
+
+ assert.verifySteps([
+ '/web/dataset/call_kw/turtle/read',
+ '/web/dataset/call_kw/ir.attachment/read',
+ ]);
+
+ await testUtils.form.clickSave(form);
+
+ assert.strictEqual(form.$('div.o_field_widget.oe_fileupload .o_attachments').children().length, 0,
+ "there should be no attachment");
+
+ assert.verifySteps([
+ '/web/dataset/call_kw/turtle/write',
+ '/web/dataset/call_kw/turtle/read',
+ ]);
+
+ form.destroy();
+ });
+
+ QUnit.test('name_create in form dialog', async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '<form>' +
+ '<field name="product_id"/>' +
+ '</form>' +
+ '</field>' +
+ '</group>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ await testUtils.owlCompatibilityNextTick();
+ await testUtils.fields.many2one.searchAndClickItem('product_id',
+ {selector: '.modal', search: 'new record'});
+
+ assert.verifySteps(['name_create']);
+
+ form.destroy();
+ });
+
+ QUnit.module('FieldReference');
+
+ QUnit.test('Reference field can quick create models', async function (assert) {
+ assert.expect(8);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form><field name="reference"/></form>`,
+ mockRPC(route, args) {
+ assert.step(args.method || route);
+ return this._super(...arguments);
+ },
+ });
+
+ await testUtils.fields.editSelect(form.$('select'), 'partner');
+ await testUtils.fields.many2one.searchAndClickItem('reference', {search: 'new partner'});
+ await testUtils.form.clickSave(form);
+
+ assert.verifySteps([
+ 'onchange',
+ 'name_search', // for the select
+ 'name_search', // for the spawned many2one
+ 'name_create',
+ 'create',
+ 'read',
+ 'name_get'
+ ], "The name_create method should have been called");
+
+ form.destroy();
+ });
+
+ QUnit.test('Reference field in modal readonly mode', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].trululu = 1;
+ this.data.partner.records[1].reference = 'product,41';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="reference"/>' +
+ '<field name="p"/>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="reference"/></form>',
+ 'partner,false,list': '<tree><field name="display_name"/></tree>',
+ },
+ res_id: 1,
+ });
+
+ // Current Form
+ assert.equal(form.$('.o_form_uri.o_field_widget[name=reference]').text(), 'xphone',
+ 'the field reference of the form should have the right value');
+
+ var $cell_o2m = form.$('.o_data_cell');
+ assert.equal($cell_o2m.text(), 'second record',
+ 'the list should have one record');
+
+ await testUtils.dom.click($cell_o2m);
+
+ // In modal
+ var $modal = $('.modal-lg');
+ assert.equal($modal.length, 1,
+ 'there should be one modal opened');
+
+ assert.equal($modal.find('.o_form_uri.o_field_widget[name=reference]').text(), 'xpad',
+ 'The field reference in the modal should have the right value');
+
+ await testUtils.dom.click($modal.find('.o_form_button_cancel'));
+
+ form.destroy();
+ });
+
+ QUnit.test('Reference field in modal write mode', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[1].trululu = 1;
+ this.data.partner.records[1].reference = 'product,41';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="reference"/>' +
+ '<field name="p"/>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form><field name="reference"/></form>',
+ 'partner,false,list': '<tree><field name="display_name"/></tree>',
+ },
+ res_id: 1,
+ });
+
+ // current form
+ await testUtils.form.clickEdit(form);
+
+ var $fieldRef = form.$('.o_field_widget.o_field_many2one[name=reference]');
+ assert.equal($fieldRef.find('option:selected').text(), 'Product',
+ 'The reference field\'s model should be Product');
+ assert.equal($fieldRef.find('.o_input.ui-autocomplete-input').val(), 'xphone',
+ 'The reference field\'s record should be xphone');
+
+ await testUtils.dom.click(form.$('.o_data_cell'));
+
+ // In modal
+ var $modal = $('.modal-lg');
+ assert.equal($modal.length, 1,
+ 'there should be one modal opened');
+
+ var $fieldRefModal = $modal.find('.o_field_widget.o_field_many2one[name=reference]');
+
+ assert.equal($fieldRefModal.find('option:selected').text(), 'Product',
+ 'The reference field\'s model should be Product');
+ assert.equal($fieldRefModal.find('.o_input.ui-autocomplete-input').val(), 'xpad',
+ 'The reference field\'s record should be xpad');
+
+ form.destroy();
+ });
+
+ QUnit.test('reference in form view', async function (assert) {
+ assert.expect(15);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="reference" string="custom label"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ archs: {
+ 'product,false,form': '<form string="Product"><field name="display_name"/></form>',
+ },
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'get_formview_action') {
+ assert.deepEqual(args.args[0], [37], "should call get_formview_action with correct id");
+ return Promise.resolve({
+ res_id: 17,
+ type: 'ir.actions.act_window',
+ target: 'current',
+ res_model: 'res.partner'
+ });
+ }
+ if (args.method === 'get_formview_id') {
+ assert.deepEqual(args.args[0], [37], "should call get_formview_id with correct id");
+ return Promise.resolve(false);
+ }
+ if (args.method === 'name_search') {
+ assert.strictEqual(args.model, 'partner_type',
+ "the name_search should be done on the newly set model");
+ }
+ if (args.method === 'write') {
+ assert.strictEqual(args.model, 'partner',
+ "should write on the current model");
+ assert.deepEqual(args.args, [[1], {reference: 'partner_type,12'}],
+ "should write the correct value");
+ }
+ return this._super(route, args);
+ },
+ });
+
+ testUtils.mock.intercept(form, 'do_action', function (event) {
+ assert.strictEqual(event.data.action.res_id, 17,
+ "should do a do_action with correct parameters");
+ });
+
+ assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1,
+ "should contain a link");
+ await testUtils.dom.click(form.$('a.o_form_uri'));
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsN(form, '.o_field_widget', 2,
+ "should contain two field widgets (selection and many2one)");
+ assert.containsOnce(form, '.o_field_many2one',
+ "should contain one many2one");
+ assert.strictEqual(form.$('.o_field_widget select').val(), "product",
+ "widget should contain one select with the model");
+ assert.strictEqual(form.$('.o_field_widget input').val(), "xphone",
+ "widget should contain one input with the record");
+
+ var options = _.map(form.$('.o_field_widget select > option'), function (el) {
+ return $(el).val();
+ });
+ assert.deepEqual(options, ['', 'product', 'partner_type', 'partner'],
+ "the options should be correctly set");
+
+ await testUtils.dom.click(form.$('.o_external_button'));
+
+ assert.strictEqual($('.modal .modal-title').text().trim(), 'Open: custom label',
+ "dialog title should display the custom string label");
+ await testUtils.dom.click($('.modal .o_form_button_cancel'));
+
+ await testUtils.fields.editSelect(form.$('.o_field_widget select'), 'partner_type');
+ assert.strictEqual(form.$('.o_field_widget input').val(), "",
+ "many2one value should be reset after model change");
+
+ await testUtils.fields.many2one.clickOpenDropdown('reference');
+ await testUtils.fields.many2one.clickHighlightedItem('reference');
+
+
+ await testUtils.form.clickSave(form);
+ assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1,
+ "should contain a link with the new value");
+
+ form.destroy();
+ });
+
+ QUnit.test('interact with reference field changed by onchange', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ bar: function (obj) {
+ if (!obj.bar) {
+ obj.reference = 'partner,1';
+ }
+ },
+ };
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: `<form>
+ <field name="bar"/>
+ <field name="reference"/>
+ </form>`,
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {
+ bar: false,
+ reference: 'partner,4',
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // trigger the onchange to set a value for the reference field
+ await testUtils.dom.click(form.$('.o_field_boolean input'));
+
+ assert.strictEqual(form.$('.o_field_widget[name=reference] select').val(), 'partner');
+
+ // manually update reference field
+ await testUtils.fields.many2one.searchAndClickItem('reference', {search: 'aaa'});
+
+ // save
+ await testUtils.form.clickSave(form);
+
+ form.destroy();
+ });
+
+ QUnit.test('default_get and onchange with a reference field', async function (assert) {
+ assert.expect(8);
+
+ this.data.partner.fields.reference.default = 'product,37';
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ if (obj.int_field) {
+ obj.reference = 'partner_type,' + obj.int_field;
+ }
+ },
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="reference"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_get') {
+ assert.step(args.model);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.verifySteps(['product'], "the first name_get should have been done");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "product",
+ "reference field model should be correctly set");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "xphone",
+ "reference field value should be correctly set");
+
+ // trigger onchange
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 12);
+
+ assert.verifySteps(['partner_type'], "the second name_get should have been done");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "partner_type",
+ "reference field model should be correctly set");
+ assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "gold",
+ "reference field value should be correctly set");
+ form.destroy();
+ });
+
+ QUnit.test('default_get a reference field in a x2m', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.turtles.default = [
+ [0, false, {turtle_ref: 'product,37'}]
+ ];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_ref"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ archs: {
+ 'turtle,false,form': '<form><field name="display_name"/><field name="turtle_ref"/></form>',
+ },
+ });
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_data_row:first').text(), "xphone",
+ "the default value should be correctly handled");
+ form.destroy();
+ });
+
+ QUnit.test('widget reference on char field, reset by onchange', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].foo = 'product,37';
+ this.data.partner.onchanges = {
+ int_field: function (obj) {
+ obj.foo = 'product,' + obj.int_field;
+ },
+ };
+
+ var nbNameGet = 0;
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="int_field"/>' +
+ '<field name="foo" widget="reference" readonly="1"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.model === 'product' && args.method === 'name_get') {
+ nbNameGet++;
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.strictEqual(nbNameGet, 1,
+ "the first name_get should have been done");
+ assert.strictEqual(form.$('a[name="foo"]').text(), "xphone",
+ "foo field should be correctly set");
+
+ // trigger onchange
+ await testUtils.fields.editInput(form.$('.o_field_widget[name=int_field]'), 41);
+
+ assert.strictEqual(nbNameGet, 2,
+ "the second name_get should have been done");
+ assert.strictEqual(form.$('a[name="foo"]').text(), "xpad",
+ "foo field should have been updated");
+ form.destroy();
+ });
+
+ QUnit.test('reference and list navigation', async function (assert) {
+ assert.expect(2);
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="reference"/></tree>',
+ });
+
+ // edit first row
+ await testUtils.dom.click(list.$('.o_data_row .o_data_cell').first());
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_field_widget[name="reference"] input')[0], document.activeElement,
+ 'input of first data row should be selected');
+
+ // press TAB to go to next line
+ await testUtils.dom.triggerEvents(list.$('.o_data_row:eq(0) input:eq(1)'),[$.Event('keydown', {
+ which: $.ui.keyCode.TAB,
+ keyCode: $.ui.keyCode.TAB,
+ })]);
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_widget[name="reference"] select')[0], document.activeElement,
+ 'select of second data row should be selected');
+
+ list.destroy();
+ });
+
+ QUnit.test('one2many with extra field from server not in form', async function (assert) {
+ assert.expect(6);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p" >' +
+ '<tree>' +
+ '<field name="datetime"/>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form>' +
+ '<field name="display_name"/>' +
+ '</form>'},
+ mockRPC: function(route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ args.args[1].p[0][2].datetime = '2018-04-05 12:00:00';
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ var x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Add a record in the list
+ await testUtils.dom.click(x2mList.find('.o_field_x2many_list_row_add a'));
+
+ var modal = $('.modal-lg');
+
+ var nameInput = modal.find('input.o_input[name=display_name]');
+ await testUtils.fields.editInput(nameInput, 'michelangelo');
+
+ // Save the record in the modal (though it is still virtual)
+ await testUtils.dom.click(modal.find('.btn-primary').first());
+
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ var newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '',
+ 'The create_date field should be empty');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ // Save the whole thing
+ await testUtils.form.clickSave(form);
+
+ x2mList = form.$('.o_field_x2many_list[name=p]');
+
+ // Redo asserts in RO mode after saving
+ assert.equal(x2mList.find('.o_data_row').length, 1,
+ 'There should be 1 records in the x2m list');
+
+ newlyAdded = x2mList.find('.o_data_row').eq(0);
+
+ assert.equal(newlyAdded.find('.o_data_cell').first().text(), '04/05/2018 12:00:00',
+ 'The create_date field should have the right value');
+ assert.equal(newlyAdded.find('.o_data_cell').eq(1).text(), 'michelangelo',
+ 'The display name field should have the right value');
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many invisible depends on parent field', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.fields.many2one.clickOpenDropdown("product_id");
+ await testUtils.fields.many2one.clickHighlightedItem("product_id");
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.test('one2many column visiblity depends on onchange of parent field', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].p = [2];
+ this.data.partner.records[0].bar = false;
+
+ this.data.partner.onchanges.p = function (obj) {
+ // set bar to true when line is added
+ if (obj.p.length > 1 && obj.p[1][2].foo === 'New line') {
+ obj.bar = true;
+ }
+ };
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="bar"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="foo"/>' +
+ '<field name="int_field" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ // bar is false so there should be 1 column
+ assert.containsOnce(form, 'th',
+ "should be only 1 column ('foo') in the one2many");
+ assert.containsOnce(form, '.o_list_view .o_data_row', "should contain one row");
+
+ await testUtils.form.clickEdit(form);
+
+ // add a new o2m record
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ form.$('.o_field_one2many input:first').focus();
+ await testUtils.fields.editInput(form.$('.o_field_one2many input:first'), 'New line');
+ await testUtils.dom.click(form.$el);
+
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns('foo' + 'int_field')");
+
+ form.destroy();
+ });
+
+ QUnit.test('one2many column_invisible on view not inline', async function (assert) {
+ assert.expect(4);
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="product_id"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="bar"/>' +
+ '<field name="p"/>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree>' +
+ '<field name="foo" attrs="{\'column_invisible\': [(\'parent.product_id\', \'!=\', False)]}"/>' +
+ '<field name="bar" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
+ '</tree>',
+ },
+ });
+ assert.containsN(form, 'th', 2,
+ "should be 2 columns in the one2many");
+ await testUtils.form.clickEdit(form);
+ await testUtils.dom.click(form.$('.o_field_many2one[name="product_id"] input'));
+ await testUtils.fields.many2one.clickHighlightedItem("product_id");
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column when the product_id is set");
+ await testUtils.fields.editAndTrigger(form.$('.o_field_many2one[name="product_id"] input'),
+ '', 'keyup');
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form, 'th:not(.o_list_record_remove_header)', 2,
+ "should be 2 columns in the one2many when product_id is not set");
+ await testUtils.dom.click(form.$('.o_field_boolean[name="bar"] input'));
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsOnce(form, 'th:not(.o_list_record_remove_header)',
+ "should be 1 column after the value change");
+ form.destroy();
+ });
+
+ QUnit.test('one2many field in edit mode with optional fields and trash icon', async function (assert) {
+ assert.expect(13);
+
+ var RamStorageService = AbstractStorageService.extend({
+ storage: new RamStorage(),
+ });
+
+ this.data.partner.records[0].p = [2];
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,list': '<tree editable="top">' +
+ '<field name="foo" optional="show"/>' +
+ '<field name="bar" optional="hide"/>' +
+ '</tree>',
+ },
+ services: {
+ local_storage: RamStorageService,
+ },
+ });
+
+ // should have 2 columns 1 for foo and 1 for advanced dropdown
+ assert.containsN(form.$('.o_field_one2many'), 'th', 1,
+ "should be 1 th in the one2many in readonly mode");
+ assert.containsOnce(form.$('.o_field_one2many table'), '.o_optional_columns_dropdown_toggle',
+ "should have the optional columns dropdown toggle inside the table");
+ await testUtils.form.clickEdit(form);
+ // should have 2 columns 1 for foo and 1 for trash icon, dropdown is displayed
+ // on trash icon cell, no separate cell created for trash icon and advanced field dropdown
+ assert.containsN(form.$('.o_field_one2many'), 'th', 2,
+ "should be 2 th in the one2many edit mode");
+ assert.containsN(form.$('.o_field_one2many'), '.o_data_row:first > td', 2,
+ "should be 2 cells in the one2many in edit mode");
+
+ await testUtils.dom.click(form.$('.o_field_one2many table .o_optional_columns_dropdown_toggle'));
+ assert.containsN(form.$('.o_field_one2many'), 'div.o_optional_columns div.dropdown-item:visible', 2,
+ "dropdown have 2 advanced field foo with checked and bar with unchecked");
+ await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item:eq(1) input'));
+ assert.containsN(form.$('.o_field_one2many'), 'th', 3,
+ "should be 3 th in the one2many after enabling bar column from advanced dropdown");
+
+ await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item:first input'));
+ assert.containsN(form.$('.o_field_one2many'), 'th', 2,
+ "should be 2 th in the one2many after disabling foo column from advanced dropdown");
+
+ assert.containsN(form.$('.o_field_one2many'), 'div.o_optional_columns div.dropdown-item:visible', 2,
+ "dropdown is still open");
+ await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
+ // use of owlCompatibilityNextTick because the x2many field is reset, meaning that
+ // 1) its list renderer is updated (updateState is called): this is async and as it
+ // contains a FieldBoolean, which is written in Owl, it completes in the nextAnimationFrame
+ // 2) when this is done, the control panel is updated: as it is written in owl, this is
+ // done in the nextAnimationFrame
+ // -> we need to wait for 2 nextAnimationFrame to ensure that everything is fine
+ await testUtils.owlCompatibilityNextTick();
+ assert.containsN(form.$('.o_field_one2many'), 'div.o_optional_columns div.dropdown-item:visible', 0,
+ "dropdown is closed");
+ var $selectedRow = form.$('.o_field_one2many tr.o_selected_row');
+ assert.strictEqual($selectedRow.length, 1, "should have selected row i.e. edition mode");
+
+ await testUtils.dom.click(form.$('.o_field_one2many table .o_optional_columns_dropdown_toggle'));
+ await testUtils.dom.click(form.$('div.o_optional_columns div.dropdown-item:first input'));
+ $selectedRow = form.$('.o_field_one2many tr.o_selected_row');
+ assert.strictEqual($selectedRow.length, 0,
+ "current edition mode discarded when selecting advanced field");
+ assert.containsN(form.$('.o_field_one2many'), 'th', 3,
+ "should be 3 th in the one2many after re-enabling foo column from advanced dropdown");
+
+ // check after form reload advanced column hidden or shown are still preserved
+ await form.reload();
+ assert.containsN(form.$('.o_field_one2many .o_list_view'), 'th', 3,
+ "should still have 3 th in the one2many after reloading whole form view");
+
+ form.destroy();
+ });
+
+ QUnit.module('TabNavigation');
+ QUnit.test('when Navigating to a many2one with tabs, it receives the focus and adds a new line', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$el.find('input[name="qux"]')[0],
+ document.activeElement,
+ "initially, the focus should be on the 'qux' field because it is the first input");
+ await testUtils.fields.triggerKeydown(form.$el.find('input[name="qux"]'), 'tab');
+ assert.strictEqual(assert.strictEqual(form.$el.find('input[name="turtle_foo"]')[0],
+ document.activeElement,
+ "after tab, the focus should be on the many2one on the first input of the newly added line"));
+
+ form.destroy();
+ });
+
+ QUnit.test('when Navigating to a many to one with tabs, it places the focus on the first visible field', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_bar" invisible="1"/>'+
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$el.find('input[name="qux"]')[0],
+ document.activeElement,
+ "initially, the focus should be on the 'qux' field because it is the first input");
+ form.$el.find('input[name="qux"]').trigger($.Event('keydown', {
+ which: $.ui.keyCode.TAB,
+ keyCode: $.ui.keyCode.TAB,
+ }));
+ await testUtils.owlCompatibilityNextTick();
+ await testUtils.dom.click(document.activeElement);
+ assert.strictEqual(assert.strictEqual(form.$el.find('input[name="turtle_foo"]')[0],
+ document.activeElement,
+ "after tab, the focus should be on the many2one"));
+
+ form.destroy();
+ });
+
+ QUnit.test('when Navigating to a many2one with tabs, not filling any field and hitting tab,' +
+ ' we should not add a first line but navigate to the next control', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_description"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$el.find('input[name="qux"]')[0],
+ document.activeElement,
+ "initially, the focus should be on the 'qux' field because it is the first input");
+ await testUtils.fields.triggerKeydown(form.$el.find('input[name="qux"]'), 'tab');
+
+ // skips the first field of the one2many
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'tab');
+ // skips the second (and last) field of the one2many
+ await testUtils.fields.triggerKeydown($(document.activeElement), 'tab');
+ assert.strictEqual(assert.strictEqual(form.$el.find('input[name="foo"]')[0],
+ document.activeElement,
+ "after tab, the focus should be on the many2one"));
+
+ form.destroy();
+ });
+
+ QUnit.test('when Navigating to a many to one with tabs, editing in a popup, the popup should receive the focus then give it back', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="qux"/>' +
+ '</group>' +
+ '<notebook>' +
+ '<page string="Partner page">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_description"/>' +
+ '</tree>' +
+ '</field>' +
+ '</page>' +
+ '</notebook>' +
+ '<group>' +
+ '<field name="foo"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'turtle,false,form': '<form><group><field name="turtle_foo"/><field name="turtle_int"/></group></form>',
+ },
+ });
+
+ assert.strictEqual(form.$el.find('input[name="qux"]')[0],
+ document.activeElement,
+ "initially, the focus should be on the 'qux' field because it is the first input");
+ await testUtils.fields.triggerKeydown(form.$el.find('input[name="qux"]'), 'tab');
+ assert.strictEqual($.find('input[name="turtle_foo"]')[0],
+ document.activeElement,
+ "when the one2many received the focus, the popup should open because it automatically adds a new line");
+
+ await testUtils.fields.triggerKeydown($('input[name="turtle_foo"]'), 'escape');
+ assert.strictEqual(form.$el.find('.o_field_x2many_list_row_add a')[0],
+ document.activeElement,
+ "after escape, the focus should be back on the add new line link");
+
+ form.destroy();
+ });
+
+ QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', async function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="top">' +
+ '<field name="turtle_foo"/>' +
+ '<field name="turtle_trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'partner,false,form': '<form><group><field name="foo"/><field name="bar"/></group></form>'
+ },
+ });
+
+ // add a new line
+ await testUtils.dom.click(form.$el.find('.o_field_x2many_list_row_add>a'));
+
+ // open the field turtle_trululu (one2many)
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+ await testUtils.dom.click(form.$el.find('.o_input_dropdown>input'));
+
+ await testUtils.fields.editInput(form.$('.o_field_many2one input'), 'ABC');
+ // click create and edit
+ await testUtils.dom.click($('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter'));
+
+ // hit escape immediately
+ var escapeKey = $.ui.keyCode.ESCAPE;
+ $(document.activeElement).trigger(
+ $.Event('keydown', {which: escapeKey, keyCode: escapeKey}));
+
+ assert.ok('did not crash');
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ });
+
+ QUnit.test('navigating through an editable list with custom controls [REQUIRE FOCUS]', async function (assert) {
+ assert.expect(5);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '<form>' +
+ '<field name="display_name"/>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<control>' +
+ '<create string="Custom 1" context="{\'default_foo\': \'1\'}"/>' +
+ '<create string="Custom 2" context="{\'default_foo\': \'2\'}"/>' +
+ '</control>' +
+ '<field name="foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="display_name"]')[0],
+ "first input should be focused by default");
+
+ // press tab to navigate to the list
+ await testUtils.fields.triggerKeydown(
+ form.$('.o_field_widget[name="display_name"]'), 'tab');
+ // press ESC to cancel 1st control click (create)
+ await testUtils.fields.triggerKeydown(
+ form.$('.o_data_cell input'), 'escape');
+ assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:first')[0],
+ "first editable list control should now have the focus");
+
+ // press right to focus the second control
+ await testUtils.fields.triggerKeydown(
+ form.$('.o_field_x2many_list_row_add a:first'), 'right');
+ assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:nth(1)')[0],
+ "second editable list control should now have the focus");
+
+ // press left to come back to first control
+ await testUtils.fields.triggerKeydown(
+ form.$('.o_field_x2many_list_row_add a:nth(1)'), 'left');
+ assert.strictEqual(document.activeElement, form.$('.o_field_x2many_list_row_add a:first')[0],
+ "first editable list control should now have the focus");
+
+ // press tab to leave the list
+ await testUtils.fields.triggerKeydown(
+ form.$('.o_field_x2many_list_row_add a:first'), 'tab');
+ assert.strictEqual(document.activeElement, form.$('.o_field_widget[name="int_field"]')[0],
+ "last input should now be focused");
+
+ form.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/fields/signature_tests.js b/addons/web/static/tests/fields/signature_tests.js
new file mode 100644
index 00000000..088c6d70
--- /dev/null
+++ b/addons/web/static/tests/fields/signature_tests.js
@@ -0,0 +1,217 @@
+odoo.define('web.signature_field_tests', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('signature', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: {string: "Name", type: "char" },
+ product_id: {string: "Product Name", type: "many2one", relation: 'product'},
+ sign: {string: "Signature", type: "binary"},
+ },
+ records: [{
+ id: 1,
+ display_name: "Pop's Chock'lit",
+ product_id: 7,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 7,
+ display_name: "Veggie Burger",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('Signature Field', {
+ before: function () {
+ return ajax.loadXML('/web/static/src/xml/name_and_signature.xml', core.qweb);
+ },
+ });
+
+ QUnit.test('Set simple field in "full_name" node option', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<field name="display_name"/>' +
+ '<field name="sign" widget="signature" options="{\'full_name\': \'display_name\'}" />' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/sign/get_fonts/') {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, 'div[name=sign] div.o_signature svg',
+ "should have a valid signature widget");
+ // Click on the widget to open signature modal
+ await testUtils.dom.click(form.$('div[name=sign] div.o_signature'));
+ assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
+ 'should open a modal with "Auto" button');
+ assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Pop's Chock'lit",
+ 'Correct Value should be set in the input for auto drawing the signature');
+
+ form.destroy();
+ });
+
+ QUnit.test('Set m2o field in "full_name" node option', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id"/>' +
+ '<field name="sign" widget="signature" options="{\'full_name\': \'product_id\'}" />' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/sign/get_fonts/') {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ assert.containsOnce(form, 'div[name=sign] div.o_signature svg',
+ "should have a valid signature widget");
+ // Click on the widget to open signature modal
+ await testUtils.dom.click(form.$('div[name=sign] div.o_signature'));
+ assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
+ 'should open a modal with "Auto" button');
+ assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Veggie Burger",
+ 'Correct Value should be set in the input for auto drawing the signature');
+
+ form.destroy();
+ });
+
+ QUnit.module('Signature Widget');
+
+ QUnit.test('Signature widget renders a Sign button', async function (assert) {
+ assert.expect(3);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<header>' +
+ '<widget name="signature" string="Sign"/>' +
+ '</header>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/sign/get_fonts/') {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.containsOnce(form, 'button.o_sign_button.o_widget',
+ "Should have a signature widget button");
+ assert.strictEqual($('.modal-dialog').length, 0,
+ "Should not have any modal");
+ // Clicks on the sign button to open the sign modal.
+ await testUtils.dom.click(form.$('span.o_sign_label'));
+ assert.strictEqual($('.modal-dialog').length, 1,
+ "Should have one modal opened");
+
+ form.destroy();
+ });
+
+ QUnit.test('Signature widget: full_name option', async function (assert) {
+ assert.expect(2);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<header>' +
+ '<widget name="signature" string="Sign" full_name="display_name"/>' +
+ '</header>' +
+ '<field name="display_name"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/sign/get_fonts/') {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ // Clicks on the sign button to open the sign modal.
+ await testUtils.dom.click(form.$('span.o_sign_label'));
+ assert.strictEqual($('.modal .modal-body a.o_web_sign_auto_button').length, 1,
+ "Should open a modal with \"Auto\" button");
+ assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), "Pop's Chock'lit",
+ "Correct Value should be set in the input for auto drawing the signature");
+
+ form.destroy();
+ });
+
+ QUnit.test('Signature widget: highlight option', async function (assert) {
+ assert.expect(3);
+
+ const form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<header>' +
+ '<widget name="signature" string="Sign" highlight="1"/>' +
+ '</header>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (route === '/web/sign/get_fonts/') {
+ return Promise.resolve();
+ }
+ return this._super(route, args);
+ },
+ });
+
+ assert.hasClass(form.$('button.o_sign_button.o_widget'), 'btn-primary',
+ "The button must have the 'btn-primary' class as \"highlight=1\"");
+ // Clicks on the sign button to open the sign modal.
+ await testUtils.dom.click(form.$('span.o_sign_label'));
+ assert.isNotVisible($('.modal .modal-body a.o_web_sign_auto_button'),
+ "\"Auto\" button must be invisible");
+ assert.strictEqual($('.modal .modal-body .o_web_sign_name_input').val(), '',
+ "No value should be set in the input for auto drawing the signature");
+
+ form.destroy();
+ });
+});
+});
+});
diff --git a/addons/web/static/tests/fields/special_fields_tests.js b/addons/web/static/tests/fields/special_fields_tests.js
new file mode 100644
index 00000000..6f0ea650
--- /dev/null
+++ b/addons/web/static/tests/fields/special_fields_tests.js
@@ -0,0 +1,365 @@
+odoo.define('web.special_fields_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var ListView = require('web.ListView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('special_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ bar: {string: "Bar", type: "boolean", default: true},
+ int_field: {string: "int_field", type: "integer", sortable: true},
+ qux: {string: "Qux", type: "float", digits: [16,1] },
+ p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'},
+ turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle'},
+ trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
+ product_id: {string: "Product", type: "many2one", relation: 'product'},
+ color: {
+ type: "selection",
+ selection: [['red', "Red"], ['black', "Black"]],
+ default: 'red',
+ },
+ date: {string: "Some Date", type: "date"},
+ datetime: {string: "Datetime Field", type: 'datetime'},
+ user_id: {string: "User", type: 'many2one', relation: 'user'},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ bar: true,
+ foo: "yop",
+ int_field: 10,
+ qux: 0.44,
+ p: [],
+ turtles: [2],
+ timmy: [],
+ trululu: 4,
+ user_id: 17,
+ }, {
+ id: 2,
+ display_name: "second record",
+ bar: true,
+ foo: "blip",
+ int_field: 9,
+ qux: 13,
+ p: [],
+ timmy: [],
+ trululu: 1,
+ product_id: 37,
+ date: "2017-01-25",
+ datetime: "2016-12-12 10:55:05",
+ user_id: 17,
+ }, {
+ id: 4,
+ display_name: "aaa",
+ bar: false,
+ }],
+ onchanges: {},
+ },
+ product: {
+ fields: {
+ name: {string: "Product Name", type: "char"}
+ },
+ records: [{
+ id: 37,
+ display_name: "xphone",
+ }, {
+ id: 41,
+ display_name: "xpad",
+ }]
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char"},
+ color: {string: "Color index", type: "integer"},
+ },
+ records: [
+ {id: 12, display_name: "gold", color: 2},
+ {id: 14, display_name: "silver", color: 5},
+ ]
+ },
+ turtle: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ turtle_foo: {string: "Foo", type: "char", default: "My little Foo Value"},
+ turtle_bar: {string: "Bar", type: "boolean", default: true},
+ turtle_int: {string: "int", type: "integer", sortable: true},
+ turtle_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5},
+ turtle_description: {string: "Description", type: "text"},
+ turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'},
+ product_id: {string: "Product", type: "many2one", relation: 'product', required: true},
+ partner_ids: {string: "Partner", type: "many2many", relation: 'partner'},
+ },
+ records: [{
+ id: 1,
+ display_name: "leonardo",
+ turtle_bar: true,
+ turtle_foo: "yop",
+ partner_ids: [],
+ }, {
+ id: 2,
+ display_name: "donatello",
+ turtle_bar: true,
+ turtle_foo: "blip",
+ turtle_int: 9,
+ partner_ids: [2,4],
+ }, {
+ id: 3,
+ display_name: "raphael",
+ turtle_bar: false,
+ turtle_foo: "kawa",
+ turtle_int: 21,
+ turtle_qux: 9.8,
+ partner_ids: [],
+ }],
+ },
+ user: {
+ fields: {
+ name: {string: "Name", type: "char"}
+ },
+ records: [{
+ id: 17,
+ name: "Aline",
+ }, {
+ id: 19,
+ name: "Christine",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('FieldTimezoneMismatch');
+
+ QUnit.test('widget timezone_mismatch in a list view', async function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.tz_offset = {
+ string: "tz_offset",
+ type: "char"
+ };
+ this.data.partner.records.forEach(function (r) {
+ r.color = 'red';
+ r.tz_offset = 0;
+ });
+ this.data.partner.onchanges = {
+ color: function (r) {
+ r.tz_offset = '+4800'; // make sur we have a mismatch
+ }
+ };
+
+ var list = await createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree string="Colors" editable="top">' +
+ '<field name="tz_offset" invisible="True"/>' +
+ '<field name="color" widget="timezone_mismatch"/>' +
+ '</tree>',
+ });
+
+ assert.strictEqual(list.$('td:contains(Red)').length, 3,
+ "should have 3 rows with correct value");
+ await testUtils.dom.click(list.$('td:contains(Red):first'));
+
+ var $td = list.$('tbody tr.o_selected_row td:not(.o_list_record_selector)');
+
+ assert.strictEqual($td.find('select').length, 1, "td should have a child 'select'");
+ assert.strictEqual($td.contents().length, 1, "select tag should be only child of td");
+
+ await testUtils.fields.editSelect($td.find('select'), '"black"');
+
+ assert.strictEqual($td.find('.o_tz_warning').length, 1, "Should display icon alert");
+ assert.ok($td.find('select option:selected').text().match(/Black\s+\([0-9]+\/[0-9]+\/[0-9]+ [0-9]+:[0-9]+:[0-9]+\)/), "Should display the datetime in the selected timezone");
+ list.destroy();
+ });
+
+ QUnit.test('widget timezone_mismatch in a form view', async function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.tz_offset = {
+ string: "tz_offset",
+ type: "char"
+ };
+ this.data.partner.fields.tz = {
+ type: "selection",
+ selection: [['Europe/Brussels', "Europe/Brussels"], ['America/Los_Angeles', "America/Los_Angeles"]],
+ };
+ this.data.partner.records[0].tz = false;
+ this.data.partner.records[0].tz_offset = '+4800';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<field name="tz_offset" invisible="True"/>' +
+ '<field name="tz" widget="timezone_mismatch"/>' +
+ '</form>',
+ });
+ await testUtils.form.clickEdit(form);
+ assert.containsOnce(form, 'select[name=tz]');
+
+ var $timezoneMismatch = form.$('.o_tz_warning');
+ assert.strictEqual($timezoneMismatch.length, 1, "warning class should be there.");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget timezone_mismatch in a form view edit mode with mismatch', async function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.tz_offset = {
+ string: "tz_offset",
+ type: "char"
+ };
+ this.data.partner.fields.tz = {
+ type: "selection",
+ selection: [['Europe/Brussels', "Europe/Brussels"], ['America/Los_Angeles', "America/Los_Angeles"]],
+ };
+ this.data.partner.records[0].tz = 'America/Los_Angeles';
+ this.data.partner.records[0].tz_offset = '+4800';
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ res_id: 1,
+ data: this.data,
+ arch: '<form>' +
+ '<field name="tz_offset" invisible="True"/>' +
+ '<field name="tz" widget="timezone_mismatch" options="{\'tz_offset_field\': \'tz_offset\'}"/>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ var $timezoneEl = form.$('select[name="tz"]');
+ assert.strictEqual($timezoneEl.children().length, 3,
+ 'The select element should have 3 children');
+
+ var $timezoneMismatch = form.$('.o_tz_warning');
+ assert.strictEqual($timezoneMismatch.length, 1,
+ 'timezone mismatch is present');
+
+ assert.notOk($timezoneMismatch.children().length,
+ 'The mismatch element should not have children');
+ form.destroy();
+ });
+
+ QUnit.module('FieldReportLayout');
+
+ QUnit.test('report_layout widget in form view', async function (assert) {
+ assert.expect(3);
+
+ this.data['report.layout'] = {
+ fields: {
+ view_id: {string: "Document Template", type: "many2one", relation: "product"},
+ image: {string: "Preview image src", type: "char"},
+ pdf: {string: "Preview pdf src", type: "char"}
+ },
+ records: [{
+ id: 1,
+ view_id: 37,
+ image: "/web/static/toto.png",
+ pdf: "/web/static/toto.pdf",
+ }, {
+ id: 2,
+ view_id: 41,
+ image: "/web/static/tata.png",
+ pdf: "/web/static/tata.pdf",
+ }],
+ };
+ this.data.partner.records[1].product_id = false;
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="product_id" widget="report_layout"/> '+
+ '</form>',
+ res_id: 2,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.img.img-fluid').length, 2,
+ "Two images should be rendered");
+ assert.strictEqual(form.$('.img.btn-info').length, 0,
+ "No image should be selected");
+
+ // select first image
+ await testUtils.dom.click(form.$(".img.img-fluid:first"));
+ assert.ok(form.$(".img.img-fluid:first").hasClass('btn-info'),
+ "First image should be selected");
+
+ form.destroy();
+ });
+
+ QUnit.module('IframeWrapper');
+
+ QUnit.test('iframe_wrapper widget in form view', async function (assert) {
+
+ assert.expect(2);
+
+ this.data = {
+ report: {
+ fields: {
+ report_content: {string: "Content of report", type: "html"}
+ },
+ records: [{
+ id: 1,
+ report_content:
+ `<html>
+ <head>
+ <style>
+ body { color : rgb(255, 0, 0); }
+ </style>
+ <head>
+ <body>
+ <div class="nice_div"><p>Some content</p></div>
+ </body>
+ </html>`
+ }]
+ }
+ };
+
+ const form = await createView({
+ View: FormView,
+ model: 'report',
+ data: this.data,
+ arch: `<form><field name="report_content" widget="iframe_wrapper"/></form>`,
+ res_id: 1,
+ });
+
+ const $iframe = form.$('iframe');
+ await $iframe.data('ready');
+ const doc = $iframe.contents()[0];
+
+ assert.strictEqual($(doc).find('.nice_div').html(), '<p>Some content</p>',
+ "should have rendered a div with correct content");
+
+ assert.strictEqual($(doc).find('.nice_div p').css('color'), 'rgb(255, 0, 0)',
+ "head tag style should have been applied");
+
+ form.destroy();
+
+ });
+
+
+});
+});
+});
diff --git a/addons/web/static/tests/fields/upgrade_fields_tests.js b/addons/web/static/tests/fields/upgrade_fields_tests.js
new file mode 100644
index 00000000..d908fd48
--- /dev/null
+++ b/addons/web/static/tests/fields/upgrade_fields_tests.js
@@ -0,0 +1,66 @@
+odoo.define('web.upgrade_fields_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {}, function () {
+
+QUnit.module('upgrade_fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ bar: {string: "Bar", type: "boolean"},
+ },
+ }
+ };
+ },
+}, function () {
+
+ QUnit.module('UpgradeBoolean');
+
+ QUnit.test('widget upgrade_boolean in a form view', async function (assert) {
+ assert.expect(1);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><field name="bar" widget="upgrade_boolean"/></form>',
+ });
+
+ await testUtils.dom.click(form.$('input:checkbox'));
+ assert.strictEqual($('.modal').length, 1,
+ "the 'Upgrade to Enterprise' dialog should be opened");
+
+ form.destroy();
+ });
+
+ QUnit.test('widget upgrade_boolean in a form view', async function (assert) {
+ assert.expect(3);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<div class="o_field"><field name="bar" widget="upgrade_boolean"/></div>' +
+ '<div class="o_label"><label for="bar"/><div>Coucou</div></div>' +
+ '</form>',
+ });
+
+ assert.containsNone(form, '.o_field .badge',
+ "the upgrade badge shouldn't be inside the field section");
+ assert.containsOnce(form, '.o_label .badge',
+ "the upgrade badge should be inside the label section");
+ assert.strictEqual(form.$('.o_label').text(), "Bar EnterpriseCoucou",
+ "the upgrade label should be inside the label section");
+ form.destroy();
+ });
+
+});
+});
+});