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: '
' + '' + '' + '' + '' + '' + '
', archs: { 'partner,false,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: '
' + '' + '' + '' + '
', archs: { 'partner,false,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: '
' + '' + '' + '', 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: '
' + '' + '' + '' + '
', archs: { 'partner,false,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: '
' + '' + '' + '' + '' + '' + '
', 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(), 'aaa
Street
City ZIP', "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: '
' + '' + '' + '', res_id: 1, archs: { "turtle,false,form": '
' + '' + '' + '', "turtle,false,list": '' + '' + '', }, 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: '
' + '' + '' + '', res_id: 1, archs: { "turtle,false,form": '
' + '' + '' + '', "turtle,false,list": '' + '' + '' + '', }, 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: '
' + '' + '' + '' + '' + '' + '
', archs: { 'partner,false,list': '', 'partner,false,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: '
' + '' + '' + '' + '' + '' + '
', archs: { 'user,false,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: ` `, 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: '', archs: { 'partner,false,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: '
' + '' + '' + '' + '' + '' + '
', 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: `
`, 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: `
`, 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: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,list': '', 'partner,false,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: '
' + '' + '', 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: '
', archs: { 'product,false,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: '
' + '' + '' + '' + '
', 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: `
`, 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: '
' + '' + '', 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: '', }); // 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: '
' + // '' + // '' + // '', // 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: '
' + '' + '', 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: '
', 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: '' + '' + '', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', }); 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: '
' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', }); 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: '
' + '' + '' + '' + '' + '' + '' + '' + '', }); // 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: '
' + '' + '' + '' + '' + '' + '' + '' + '', }); // 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: '
' + '' + '' + '' + '' + '' + '' + '
', }); // 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: '
' + '' + '' + '' + '' + '' + '' + '
', }); 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '
', 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': '
' + '
' + '
' + '
', }, }); 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: '
' + '' + '' + '' + '' + '' + '' + '', archs: { 'partner_type,false,form': '
', 'partner,false,list': '' + '' + '' + '', }, 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: '
' + '' + '', 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: '
' + '' + '' + '' + '', 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: '
' + '' + '', 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: '
' + '' + '', 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: '
' + '' + '' + '' + '' + '', 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: '
' + '' + '' + '' + '
', 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: '
', archs: { 'product,false,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: '
', archs: { 'partner,false,list': '', 'product,false,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: '
' + '' + '' + '' + '
', archs: { 'product,false,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: '
' + '' + '' + '' + '
', }); 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: '
' + '' + '' + '' + '
', archs: { 'product,false,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: '', }); 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: '
' + '' + '' + '', archs: { 'partner,false,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: '
' + '' + '' + '', 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: '', 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: '', 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: '
' + '' + '' + '', 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: '
' + '' + '' + '' + '' + '' + '
', 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: '
', archs: { 'partner,false,list': '', 'partner,false,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: '
', archs: { 'partner,false,list': '', 'partner,false,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 = $(`
`); $('body').append($fakeDialog); const form = await createView({ View: FormView, model: 'partner', data: this.data, arch: '
', archs: { 'partner,false,list': '', 'partner,false,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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,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: '
', archs: { 'partner,false,list': '' + '' + '' + '', 'partner,false,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: '
' + '
' + '' + '
' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '
', 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: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', 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:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', 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:'
' + '' + '' + '' + '' + '' + '' + '' + '', 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:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,list': '' + '' + '' + '', }, }); 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: '
', 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: `
`, }); 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: '', }); 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(); }); }); }); });