flectra.define('web.relational_fields_tests', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); var BasicModel = require('web.BasicModel'); var concurrency = require('web.concurrency'); var FormView = require('web.FormView'); var ListRenderer = require('web.ListRenderer'); 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'); var fieldUtils = require('web.field_utils'); var createView = testUtils.createView; QUnit.module('fields', {}, function () { QUnit.module('relational_fields', { beforeEach: function () { this.data = { partner: { fields: { display_name: { string: "Displayed name", type: "char" }, foo: {string: "Foo", type: "char", default: "My little Foo Value"}, bar: {string: "Bar", type: "boolean", default: true}, int_field: {string: "int_field", type: "integer", sortable: true}, qux: {string: "Qux", type: "float", digits: [16,1] }, p: {string: "one2many field", type: "one2many", relation: 'partner', relation_field: 'trululu'}, turtles: {string: "one2many turtle field", type: "one2many", relation: 'turtle', relation_field: 'turtle_trululu'}, trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'}, product_id: {string: "Product", type: "many2one", relation: 'product'}, color: { type: "selection", selection: [['red', "Red"], ['black', "Black"]], default: 'red', string: "Color", }, date: {string: "Some Date", type: "date"}, datetime: {string: "Datetime Field", type: 'datetime'}, user_id: {string: "User", type: 'many2one', relation: 'user'}, reference: {string: "Reference Field", type: 'reference', selection: [ ["product", "Product"], ["partner_type", "Partner Type"], ["partner", "Partner"]]}, }, records: [{ id: 1, display_name: "first record", bar: true, foo: "yop", int_field: 10, qux: 0.44, p: [], turtles: [2], timmy: [], trululu: 4, user_id: 17, reference: 'product,37', }, { id: 2, display_name: "second record", bar: true, foo: "blip", int_field: 9, qux: 13, p: [], timmy: [], trululu: 1, product_id: 37, date: "2017-01-25", datetime: "2016-12-12 10:55:05", user_id: 17, }, { id: 4, display_name: "aaa", bar: false, }], onchanges: {}, }, product: { fields: { name: {string: "Product Name", type: "char"} }, records: [{ id: 37, display_name: "xphone", }, { id: 41, display_name: "xpad", }] }, partner_type: { fields: { name: {string: "Partner Type", type: "char"}, color: {string: "Color index", type: "integer"}, }, records: [ {id: 12, display_name: "gold", color: 2}, {id: 14, display_name: "silver", color: 5}, ] }, turtle: { fields: { display_name: { string: "Displayed name", type: "char" }, turtle_foo: {string: "Foo", type: "char"}, turtle_bar: {string: "Bar", type: "boolean", default: true}, turtle_int: {string: "int", type: "integer", sortable: true}, turtle_qux: {string: "Qux", type: "float", digits: [16,1], required: true, default: 1.5}, turtle_description: {string: "Description", type: "text"}, turtle_trululu: {string: "Trululu", type: "many2one", relation: 'partner'}, turtle_ref: {string: "Reference", type: 'reference', selection: [ ["product", "Product"], ["partner", "Partner"]]}, product_id: {string: "Product", type: "many2one", relation: 'product', required: true}, partner_ids: {string: "Partner", type: "many2many", relation: 'partner'}, }, records: [{ id: 1, display_name: "leonardo", turtle_bar: true, turtle_foo: "yop", partner_ids: [], }, { id: 2, display_name: "donatello", turtle_bar: true, turtle_foo: "blip", turtle_int: 9, partner_ids: [2,4], }, { id: 3, display_name: "raphael", product_id: 37, turtle_bar: false, turtle_foo: "kawa", turtle_int: 21, turtle_qux: 9.8, partner_ids: [], turtle_ref: 'product,37', }], onchanges: {}, }, user: { fields: { name: {string: "Name", type: "char"}, partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'}, }, records: [{ id: 17, name: "Aline", partner_ids: [1, 2], }, { id: 19, name: "Christine", }] }, }; } }, function () { QUnit.module('FieldMany2One'); QUnit.test('many2ones in form views', function (assert) { assert.expect(5); var form = 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 $.when({ 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 $.when(false); } return this._super(route, args); }, }); testUtils.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"); form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action) form.$buttons.find('.o_form_button_edit').click(); form.$('.o_external_button').click(); // click on the external button (should do an RPC) 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', function (assert) { assert.expect(2); var form = 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 $.when(false); } return this._super(route, args); }, viewOptions: { ids: [1, 2], }, }); form.$buttons.find('.o_form_button_edit').click(); // click on the external button (should do an RPC) form.$('.o_external_button').click(); // save and close modal $('.modal .modal-footer .btn-primary:first').click(); // save form form.$buttons.find('.o_form_button_save').click(); // click next on pager form.pager.$('.o_pager_next').click(); // this checks that the view did not ask for confirmation that the // record is dirty assert.strictEqual(form.pager.$el.text().trim(), '2 / 2', 'pager should be at second page'); form.destroy(); }); QUnit.test('context in many2one and default get', function (assert) { assert.expect(1); this.data.partner.fields.int_field.default = 14; this.data.partner.fields.trululu.default = 2; var form = 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)', function (assert) { assert.expect(1); var form = 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 $.when(false); } return this._super(route, args); }, viewOptions: { ids: [1, 2], }, }); form.$buttons.find('.o_form_button_edit').click(); // click on the external button (should do an RPC) form.$('.o_external_button').click(); $('.modal input[name="foo"]').val('brandon').trigger('input'); // save and close modal $('.modal .modal-footer .btn-primary:first').click(); // save form form.$buttons.find('.o_form_button_save').click(); // click next on pager form.pager.$('.o_pager_next').click(); // this checks that the view did not ask for confirmation that the // record is dirty assert.strictEqual(form.pager.$el.text().trim(), '2 / 2', 'pager should be at second page'); form.destroy(); }); QUnit.test('many2ones in form views with show_adress', function (assert) { assert.expect(4); var form = 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($('a.o_form_uri').html(), 'aaa
Street
City ZIP', "input should have a multi-line content in readonly due to show_address"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('button.o_external_button:visible').length, 1, "should have an open record button"); form.$('input.o_input').click(); assert.strictEqual(form.$('button.o_external_button:visible').length, 1, "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('many2ones in form views with search more', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', archs: { 'partner,false,list': '', 'partner,false,search': '', }, res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').click(); $dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click(); // Open Search More assert.strictEqual($('tr.o_data_row').length, 9, "should display 9 records"); $('.o_searchview_more').click(); // Magnifying class for more filters $('button:contains(Filters)').click(); $('.o_add_filter').click(); // Add a custom filter, datetime field is selected assert.strictEqual($('li.o_filter_condition select.o_searchview_extended_prop_field').val(), 'datetime', "datetime field should be selected"); $('.o_apply_filter').click(); 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', function (assert) { assert.expect(9); this.data.partner.onchanges.user_id = function () {}; this.data.user.fields.other_field = {string: "Other Field", type: "char"}; var form = 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 $.when(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 form.$buttons.find('.o_form_button_edit').click(); form.$('.o_external_button').click(); $('.modal-body input[name="other_field"]').val('wood').trigger('input'); // save the modal and make sure an onchange is triggered $('.modal .modal-footer .btn-primary').first().click(); assert.verifySteps(['read', 'get_formview_id', 'read', 'write', 'onchange', 'read']); // save the main record, and check that no extra rpcs are done (record // is not dirty, only a related record was modified) form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['read', 'get_formview_id', 'read', 'write', 'onchange', 'read']); form.destroy(); }); QUnit.test('many2one readonly fields with option "no_open"', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('a.o_form_uri').length, 0, "should not have an anchor"); form.destroy(); }); QUnit.test('many2one in edit mode', function (assert) { assert.expect(16); // 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 = 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.intercept(form, 'get_session', function (event) { event.data.callback({user_context: {}}); }); form.$buttons.find('.o_form_button_edit').click(); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').click(); 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 7 suggestions'); assert.strictEqual($dropdown.find('li.o_m2o_dropdown_option').length, 2, 'autocomplete should contain "Search More" and Create and Edit..."'); form.$('.o_field_many2one input').click(); 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 form.$('.o_field_many2one input').click(); $dropdown.find('li:first()').click(); 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 form.$('.o_field_many2one input').click(); // click on 'Search More' (mouseenter required by ui-autocomplete) $dropdown.find('.o_m2o_dropdown_option:contains(Search)').mouseenter().click(); 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"); // filter the list using the searchview $('.modal .o_searchview_input').trigger({type: 'keypress', which: 80}); // P $('.modal .o_searchview_input').trigger({type: 'keydown', which: 13}); // enter assert.strictEqual($('.modal tbody tr').length, 10, "list should be restricted to records containing a P (10 records)"); // choose a record $('.modal tbody tr:contains(Partner 20)').click(); // choose record '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 form.$buttons.find('.o_form_button_save').click(); 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', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.strictEqual(form.$('a.o_form_uri').length, 1, "should display 1 m2o link in form"); assert.strictEqual(form.$('a.o_form_uri').attr('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 form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_many2one input').val('').trigger('keyup').trigger('focusout'); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('a.o_form_uri').attr('href'), "#", "href should have #"); form.destroy(); }); QUnit.test('many2one searches with correct value', function (assert) { var done = assert.async(); assert.expect(6); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var form = 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'"); form.$('.o_field_many2one input').click(); // should search with '' // unset the many2one -> should search again with '' form.$('.o_field_many2one input').val('').trigger('keydown'); concurrency.delay(0).then(function () { // write 'p' -> should search with 'p' form.$('.o_field_many2one input').val('p').trigger('keydown').trigger('keyup'); return concurrency.delay(0); }).then(function () { // close and re-open the dropdown -> should search with 'p' again form.$('.o_field_many2one input').click(); form.$('.o_field_many2one input').click(); assert.verifySteps(['search: ', 'search: ', 'search: p', 'search: p']); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; form.destroy(); done(); }); }); QUnit.test('many2one field with option always_reload', function (assert) { assert.expect(4); var count = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 2, mockRPC: function (route, args) { if (args.method === 'name_get') { count++; return $.when([[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"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('input').val(), "first record", "actual field value should be displayed to be edited"); form.$buttons.find('.o_form_button_save').click(); assert.ok(form.$('a:contains(and some address)').length, "should still display additional result"); form.destroy(); }); QUnit.test('many2one field and list navigation', function (assert) { assert.expect(3); var list = createView({ View: ListView, model: 'partner', data: this.data, arch: '', }); // edit first input, to trigger autocomplete list.$('.o_data_row .o_data_cell').first().click(); list.$('.o_data_cell input').val('').trigger('input'); // press keydown, to select first choice list.$('.o_data_cell input').focus().trigger($.Event('keydown', { which: $.ui.keyCode.DOWN, keyCode: $.ui.keyCode.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.ok(list.$('.o_data_row:eq(0)').hasClass('o_selected_row'), 'first data row should still be selected'); assert.ok(!list.$('.o_data_row:eq(1)').hasClass('o_selected_row'), 'second data row should not be selected'); list.destroy(); }); QUnit.test('standalone many2one field', function (assert) { assert.expect(3); var done = assert.async(); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var fixture = $('#qunit-fixture'); var self = this; var model = testUtils.createModel({ Model: BasicModel, data: this.data, }); model.makeRecord('coucou', [{ name: 'partner_id', relation: 'partner', type: 'many2one', value: [1, 'first partner'], }], { partner_id: { options: { no_open: true, }, }, }).then(function (recordID) { var record = model.get(recordID); // 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); testUtils.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', }); relField.appendTo(fixture); $('input.o_input').val('xyzzrot').trigger('input'); concurrency.delay(0).then(function () { var $dropdown = $('input.o_input').autocomplete('widget'); $dropdown.find('.o_m2o_dropdown_option:contains(Create)') .first().mouseenter().click(); assert.verifySteps(['name_search', 'name_create']); parent.destroy(); model.destroy(); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; done(); }); }); }); // QUnit.test('onchange on a many2one to a different model', 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 = createView({ // View: FormView, // model: 'partner', // data: this.data, // arch: '
' + // '' + // '' + // '', // res_id: 1, // }); // form.$buttons.find('.o_form_button_edit').click(); // assert.strictEqual(form.$('input').eq(1).val(), 'xpad', "initial product_id val should be xpad"); // form.$('input').eq(0).val("let us trigger an onchange").trigger('input'); // assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied"); // }); QUnit.test('form: quick create then save directly', function (assert) { var done = assert.async(); assert.expect(5); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var def = $.Deferred(); var newRecordID; var form = 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 def.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; }, }); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').val('b').trigger('keydown'); concurrency.delay(0).then(function () { $dropdown.find('li:first()').click(); // quick create 'b' form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['name_create'], "should wait for the name_create before creating the record"); def.resolve(); assert.verifySteps(['name_create', 'create']); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; form.destroy(); done(); }); }); QUnit.test('list: quick create then save directly', function (assert) { var done = assert.async(); assert.expect(8); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var def = $.Deferred(); var newRecordID; var list = 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 def.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; }, }); list.$buttons.find('.o_list_button_add').click(); var $dropdown = list.$('.o_field_many2one input').autocomplete('widget'); list.$('.o_field_many2one input').val('b').trigger('keydown'); concurrency.delay(0).then(function () { $dropdown.find('li:first()').click(); // quick create 'b' list.$buttons.find('.o_list_button_add').click(); assert.verifySteps(['name_create'], "should wait for the name_create before creating the record"); assert.strictEqual(list.$('.o_data_row').length, 4, "should wait for the name_create before adding the new row"); def.resolve(); assert.verifySteps(['name_create', 'create']); assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), 'b', "created row should have the correct m2o value"); assert.strictEqual(list.$('.o_data_row').length, 5, "should have added the fifth row"); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; list.destroy(); done(); }); }); QUnit.test('list in form: quick create then save directly', function (assert) { var done = assert.async(); assert.expect(6); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var def = $.Deferred(); var newRecordID; var form = 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 def.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; }, }); form.$('.o_field_x2many_list_row_add a').click(); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').val('b').trigger('keydown'); concurrency.delay(0).then(function () { $dropdown.find('li:first()').click(); // quick create 'b' form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['name_create'], "should wait for the name_create before creating the record"); def.resolve(); assert.verifySteps(['name_create', 'create']); assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b', "first row should have the correct m2o value"); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; form.destroy(); done(); }); }); QUnit.test('list in form: quick create then add a new line directly', 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. var done = assert.async(); assert.expect(8); this.data.partner.onchanges = { trululu: function () {}, }; var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var def = $.Deferred(); var newRecordID; var form = 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 def.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; }, }); form.$('.o_field_x2many_list_row_add a').click(); var $dropdown = form.$('.o_field_many2one input').autocomplete('widget'); form.$('.o_field_many2one input').val('b').trigger('keydown'); concurrency.delay(0).then(function () { $dropdown.find('li:first()').click(); // quick create 'b' form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 1, "there should still be only one row"); assert.ok(form.$('.o_data_row').hasClass('o_selected_row'), "the row should still be in edition"); def.resolve(); assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b', "first row should have the correct m2o value"); assert.strictEqual(form.$('.o_data_row').length, 2, "there should now be 2 rows"); assert.ok(form.$('.o_data_row:nth(1)').hasClass('o_selected_row'), "the second row should be in edition"); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_data_row').length, 1, "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(); done(); }); }); QUnit.test('list in form: create with one2many with many2one', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({p: [[0, 0, {display_name: 'new record'}]]}); } else if (args.method === 'name_get') { // This should not be called at all and thus is not accounted for // in the assert.expect. If this is called, you broke this test. assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'), "should not call name_get for the m2o inside o2m which has no value"); } 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)', 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); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({p: [[0, 0, {display_name: 'new record', trululu: false}]]}); } else if (args.method === 'name_get') { // This should not be called at all and thus is not accounted for // in the assert.expect. If this is called, you broke this test. assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'), "should not call name_get for the m2o inside o2m which has no value"); } 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)', 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); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({ p: [[0, 0, { display_name: 'new record', trululu: false }]] }); } return this._super.apply(this, arguments); }, }); 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"); requiredElement.click(); // discard by clicking on body $('body').click(); 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)', function (assert) { assert.expect(2); this.data.partner.records[0].display_name = "MyTrululu"; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({ p: [ [0, 0, { trululu: 1 }], [0, 0, { trululu: 1 }] ] }); } if (args.method === 'name_get') { assert.deepEqual(args.args[0], _.uniq(args.args[0]), "should not have duplicates in name_get rpc"); } 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)', function (assert) { assert.expect(4); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({ p: [ [0, 0, { display_name: 'record1', trululu: 1 }], [0, 0, { display_name: 'record2', trululu: 2 }] ] }); } return this._super.apply(this, arguments); }, }); 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"); form.$('button.o_pager_next').click(); 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)', 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.onchanges = { product_id: function (obj) { if (obj.product_id === 37) { obj.p = [[0, 0, { display_name: "entry", trululu: false }]]; } }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({ product_id: 37, }); } return this._super.apply(this, arguments); }, }); // check that there is a record in the editable list with empty string as required field assert.strictEqual(form.$('.o_data_row').length, 1, "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 requiredField.click(); // click off so that the required field still stay empty $('body').click(); // record should not be dropped assert.strictEqual(form.$('.o_data_row').length, 1, "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)', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', }); // check no record in list assert.strictEqual(form.$('.o_data_row').length, 0, "should have no row in the editable list"); // select product_id to force on_change in editable list form.$('.o_field_widget[name="product_id"] .o_input').click(); $('.ui-menu-item').first().click(); // check that there is a record in the editable list with empty string as required field assert.strictEqual(form.$('.o_data_row').length, 1, "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 requiredField.click(); // click off so that the required field still stay empty $('body').click(); // record should not be dropped assert.strictEqual(form.$('.o_data_row').length, 1, "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")', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', }); // Click on "Add an item" form.$('.o_field_x2many_list_row_add a').click(); 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.notOk(charField.hasClass('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 requiredField.click(); // click off so that the required field still stay empty $('body').click(); // record should be dropped assert.strictEqual(form.$('.o_data_row').length, 0, "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")', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('.o_data_row').length, 0, "should initially not have any record in the list"); // Click on "Add an item" form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 1, "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 $inputEditMode.click(); $('li.ui-menu-item').first().click(); $('body').click(); // leave edit mode on the line var $inputReadonlyMode = form.$('.o_data_cell.o_required_modifier'); assert.strictEqual(form.$('.o_data_row').length, 1, "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 $('.o_data_row').click(); // enter edit mode on the line $inputEditMode.click(); $inputEditMode.val(""); $('body').click(); assert.strictEqual(form.$('.o_data_row').length, 1, "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', function (assert) { assert.expect(5); var displayName = 'brandon is the new timmy'; this.data.partner.onchanges.timmy = function (obj) { assert.deepEqual( obj.timmy, [ [6, false, []], [0, obj.timmy[1][1], {display_name: displayName, name: 'brandon'}] ], "should have properly created the x2many command list"); obj.int_field = obj.timmy.length; }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({timmy: [[0, 0, {display_name: 'brandon is the new timmy', name: 'brandon'}]]}); } if (args.method === 'create') { assert.deepEqual(args.args[0], { int_field: 2, timmy: [ [6, false, []], [0, args.args[0].timmy[1][1], {display_name: displayName, name: 'brandon'}], ], }, "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(), '2', "should have called and executed the onchange properly"); // edit the subrecord and save displayName = 'new value'; form.$('.o_data_cell').click(); form.$('.o_data_cell input').val(displayName).trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('list in form: default_get with x2many create and onchange', function (assert) { assert.expect(2); this.data.partner.onchanges.turtles = function (obj) { assert.deepEqual( obj.turtles, [ [4, 2, false], [1, 2, {turtle_foo: 'blip'}], [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], "should have properly created the x2many command list"); }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'default_get') { return $.when({turtles: [[6, 0, [2,3]]]}); } if (args.method === 'create') { // it would be even better if we did not send the current // unchanged state with the command 1, but this seems more // difficult. assert.deepEqual(args.args[0].turtles, [ [4, 2, false], [1, 2, {turtle_foo: 'blip'}], [4, 3, false], [1, 3, {turtle_foo: 'kawa'}] ], 'should send proper commands to create method'); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('list in form: call button in sub view', function (assert) { assert.expect(6); this.data.partner.records[0].p = [2]; var form = 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 $.when(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'); }, }, archs: { 'product,false,form': '
' + '
' + '
' + '
', }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('td.o_data_cell:first').click(); // edit first one2many line form.$('.o_external_button').click(); // open product sub view in modal $('button:contains("Just do it !")').click(); // click on action button $('button:contains("Just don\'t do it !")').click(); // click on object button form.destroy(); }); QUnit.test('X2Many sequence list in modal', 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]; testUtils.createAsyncView({ 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 $.when([{id: 37, name: 'xphone', display_name: 'leonardo', turtle_ids: [1]}]); } if (route === '/web/dataset/call_kw/turtle/read') { return $.when([{id: 1, type_id: [12, 'gold']}]); } if (route === '/web/dataset/call_kw/partner_type/get_formview_id') { return $.when(false) } if (route === '/web/dataset/call_kw/partner_type/read') { return $.when([{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); }, }).then(function(form) { form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_cell').click(); form.$('.o_external_button').click(); var $modal = $('.modal-dialog'); 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'); testUtils.dragAndDrop($handles.eq(1), $modal.find('tbody tr').first()); // Saving the modal and then the original model $modal.find('.modal-footer .btn-primary').click(); form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['onchange sequence', 'partner_type write']); form.destroy(); }); }); QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) { assert.expect(1); var form = 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); } }); form.$buttons.find('.o_form_button_edit').click(); form.$('input').click(); form.destroy(); }); QUnit.test('autocompletion in a many2one, in form view with a date field', function (assert) { assert.expect(1); var form = 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); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('input:eq(2)').click(); form.destroy(); }); QUnit.test('creating record with many2one with option always_reload', 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 = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', mockRPC: function (route, args) { count++; if (args.method === 'name_get' && args.args[0][0] === 2) { return $.when([[2, "hello world\nso much noise"]]); } return this._super(route, args); }, }); assert.strictEqual(count, 3, "should have done 3 rpcs (default_get, onchange, name_get)"); assert.strictEqual(form.$('input').val(), 'hello world', "should have taken the correct display name"); form.destroy(); }); QUnit.test('selecting a many2one, then discarding', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', res_id: 1, }); assert.strictEqual(form.$('a').text(), '', 'the tag a should be empty'); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_many2one input').click(); form.$('.o_field_many2one input').autocomplete('widget').find('a').first().click(); assert.strictEqual(form.$('input').val(), "xphone", "should have selected xphone"); form.$buttons.find('.o_form_button_cancel').click(); assert.strictEqual(form.$('a').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', function (assert) { assert.expect(4); this.data.partner.records[0].timmy = [12]; var form = 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 $.when([]); } 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 $.when([]); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_widget[name=product_id] input').click(); form.$('.o_field_widget[name=trululu] input').click(); form.destroy(); }); QUnit.test('quick create on a many2one', function (assert) { assert.expect(1); var form = 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); }, }); form.$('.o_field_many2one input').focus(); form.$('.o_field_many2one input').val('new partner').trigger('keyup').trigger('focusout'); $('.modal .modal-footer .btn-primary').first().click(); form.destroy(); }); QUnit.test('slow create on a many2one', function (assert) { assert.expect(7); var form = 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'); assert.strictEqual($('.modal').length, 1, "there should be one opened modal"); $('.modal .modal-footer .btn:contains(Cancel)').click(); 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 form.$('.o_field_many2one input').focus().val('new product').trigger('keyup').trigger('blur'); $('.modal .modal-header button').click(); assert.strictEqual(form.$('.o_field_many2one input').val(), "", 'the many2one should not set a value as its creation has been cancelled (with Close button)'); // select a new value then cancel the creation of the new one --> restore the previous form.$('.o_field_many2one input').click(); form.$('.o_field_many2one input').autocomplete('widget').find('a').first().click(); assert.strictEqual(form.$('input').val(), "xphone", "should have selected xphone"); form.$('.o_field_many2one input').focus().val('new product').trigger('keyup').trigger('blur'); assert.strictEqual($('.modal').length, 1, "there should be one opened modal"); $('.modal .modal-footer .btn:contains(Cancel)').click(); 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(); form.$('.o_field_many2one input').val('new partner').trigger('keyup').trigger('blur'); $('.modal .modal-footer .btn-primary').click(); assert.strictEqual($('.modal .o_form_view').length, 1, 'a new modal should be opened and contain a form view'); form.destroy(); }); QUnit.test('no_create option on a many2one', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '
', }); form.$('.o_field_many2one input').focus(); form.$('.o_field_many2one input').val('new partner').trigger('keyup').trigger('focusout'); assert.strictEqual($('.modal').length, 0, "should not display the create modal"); form.destroy(); }); QUnit.test('can_create and can_write option on a many2one', function (assert) { assert.expect(5); this.data.product.options = { can_create: "false", can_write: "false", }; var form = 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 $.when(false); } return this._super.apply(this, arguments); }, }); form.$('.o_field_many2one input').click(); assert.strictEqual($('.ui-autocomplete .o_m2o_dropdown_option:contains(Create)').length, 0, "there shouldn't be any option to search and create"); $('.ui-autocomplete li:contains(xpad)').mouseenter().click(); assert.strictEqual(form.$('.o_field_many2one input').val(), "xpad", "the correct record should be selected"); assert.strictEqual(form.$('.o_field_many2one .o_external_button').length, 1, "there should be an external button displayed"); form.$('.o_field_many2one .o_external_button').click(); assert.strictEqual($('.modal .o_form_view.o_form_readonly').length, 1, "there should be a readonly form view opened"); $('.modal .o_form_button_cancel').click(); form.$('.o_field_many2one input').val('new product').trigger('keyup').trigger('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', function (assert) { assert.expect(9); var done = assert.async(); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var list = createView({ View: ListView, model: 'partner', data: this.data, arch: '', }); list.$('td.o_data_cell:first').click(); assert.strictEqual(list.$('.o_selected_row').length, 1, "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 list.$('td.o_data_cell input:first').val('a').trigger('input'); concurrency.delay(0).then(function () { 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 $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.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 $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); assert.notOk(document.contains($input[0]), "input should no longer be in dom"); assert.ok(list.$('tr.o_data_row:eq(1)').hasClass('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 list.$('td.o_data_cell input:first').val('a').trigger('input'); return concurrency.delay(0); }).then(function () { var $input = list.$('td.o_data_cell input:first'); var $dropdown = $input.autocomplete('widget'); assert.ok($dropdown.is(':visible'), "autocomplete dropdown should be visible"); $input.trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.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 $input.trigger($.Event('keydown', { which: $.ui.keyCode.TAB, keyCode: $.ui.keyCode.TAB, })); assert.notOk(document.contains($input[0]), "input should no longer be in dom"); assert.ok(list.$('tr.o_data_row:eq(2)').hasClass('o_selected_row'), "third row should now be selected"); list.destroy(); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; done(); }); }); QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should not trigger M2ODialog', function (assert) { var done = assert.async(); assert.expect(2); var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '', archs: { 'partner,false,form': '
', }, }); var $input = form.$('.o_field_many2one input'); $input.val("Something that does not exist").trigger('input'); $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter'); concurrency.delay(0).then(function() { $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); $input.trigger($.Event('keypress', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); $input.trigger($.Event('keyup', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); concurrency.delay(0).then(function() { $input.blur(); assert.strictEqual($('.modal').length, 1, "should have one modal in body"); // Check that discarding clears $input $('.modal .o_form_button_cancel').click(); assert.strictEqual($input.val(), '', "the field should be empty"); form.destroy(); relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; done(); }); }); }); QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) { assert.expect(6); var done = assert.async(); 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 def = $.Deferred(); var list = 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 def.then(_.constant(result)); } return result; }, }); list.$('td.o_data_cell:first').click(); list.$('td.o_data_cell input:first').val('a').trigger('input'); concurrency.delay(0).then(function () { var $input = list.$('td.o_data_cell input:first'); $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); $input.trigger($.Event('keyup', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); def.resolve(); $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); assert.strictEqual($('div.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; done(); }); }); QUnit.test('many2one in editable list + onchange, with enter, part 2 [REQUIRE FOCUS]', function (assert) { // this is the same test as the previous one, but the onchange is just // resolved slightly later assert.expect(6); var done = assert.async(); 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 def = $.Deferred(); var list = 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 def.then(_.constant(result)); } return result; }, }); list.$('td.o_data_cell:first').click(); list.$('td.o_data_cell input:first').val('a').trigger('input'); concurrency.delay(0).then(function () { var $input = list.$('td.o_data_cell input:first'); $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); $input.trigger($.Event('keyup', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); $input.trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); def.resolve(); assert.strictEqual($('div.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; done(); }); }); QUnit.test('many2one: domain updated by an onchange', function (assert) { assert.expect(2); this.data.partner.onchanges = { int_field: function () {}, }; var domain = []; var form = 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 $.when({ 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 []) form.$('.o_field_widget[name=trululu] input').click(); // close the dropdown form.$('.o_field_widget[name=trululu] input').click(); // trigger an onchange that will update the domain form.$('.o_field_widget[name=int_field]').val(2).trigger('input'); // trigger a name_search (domain should be [['id', 'in', [10]]]) form.$('.o_field_widget[name=trululu] input').click(); form.destroy(); }); QUnit.test('many2one in one2many: domain updated by an onchange', function (assert) { assert.expect(3); this.data.partner.onchanges = { trululu: function () {}, }; var domain = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { return $.when({ 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 form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search // add a second row with another domain for the m2o domain = [['id', 'in', [5]]]; // domain for subrecord 2 form.$('.o_field_x2many_list_row_add a').click(); // triggers the onchange form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search // 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 form.$('.o_data_row:first .o_data_cell').click(); form.$('.o_field_widget[name=trululu] input').click(); // triggers the name_search form.destroy(); }); QUnit.test('updating a many2one from a many2many', function (assert) { assert.expect(4); this.data.turtle.records[1].turtle_trululu = 1; var form = 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 $.when(false); } return this._super(route, args); }, }); // Opening the modal form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_row td:contains(first record)').click(); form.$('.o_external_button').click(); assert.strictEqual($('.modal').length, 1, "should have one modal in body"); // Changing the 'trululu' value $('.modal input[name="display_name"]').val('test').trigger('input'); $('.modal button.btn-primary').click(); // 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('x2many list sorted by many2one', function (assert) { assert.expect(3); this.data.partner.records[0].p = [1, 2, 4]; this.data.partner.fields.trululu.sortable = true; var form = 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"); form.$('.o_list_view thead th:nth(1)').click(); assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '412', "should have correct order (ASC)"); form.$('.o_list_view thead th:nth(1)').click(); assert.strictEqual(form.$('.o_data_row .o_list_number').text(), '214', "should have correct order (DESC)"); form.destroy(); }); QUnit.test('many2many list add *many* records, remove, re-add', function (assert) { assert.expect(5); this.data.partner.fields.timmy.domain = [['color', '=', 2]]; this.data.partner.fields.timmy.onChange = true; this.data.partner_type.fields.product_ids = {string: "Product", type: "many2many", relation: 'product'}; for (var i=0; i<50; i++) { var new_record_partner_type = {id: 100+i, display_name: "batch" + i, color: 2}; this.data.partner_type.records.push(new_record_partner_type); } var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner_type,false,list': '', 'partner_type,false,search': '', }, mockRPC: function (route, args) { if (args.method === 'get_formview_id') { assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id"); return $.when(false); } return this._super(route, args); }, }); // First round: add 51 records in batch form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); var $modal = $('.modal-dialog.modal-lg'); assert.equal($modal.length, 1, 'There should be one modal'); $modal.find('thead input[type=checkbox]').click(); //select all the records we created in batch + 'gold' $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)'); assert.equal(m2m_records.length, 51, 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold' form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_save').click(); // Secound round: remove one record form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_delete'); trash_buttons.first().click(); var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); assert.equal(pager_limit.text(), '50', 'We should have 50 records in the m2m field'); // Third round: re-add 1 records form.$('.o_field_x2many_list_row_add a').click(); $modal = $('.modal-dialog.modal-lg'); assert.equal($modal.length, 1, 'There should be one modal'); $modal.find('thead input[type=checkbox]').click(); $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); assert.equal(pager_limit.text(), '51', 'We should have 51 records in the m2m field'); form.destroy(); }); QUnit.module('FieldOne2Many'); QUnit.test('New record with a o2m also with 2 new records, ordered, and resequenced', function (assert) { assert.expect(3); // Needed to have two new records in a single stroke this.data.partner.onchanges = { foo: function(obj) { obj.p = [ [5], [0, 0, {trululu: false}], [0, 0, {trululu: false}], ] } }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '', viewOptions: { mode: 'create', }, mockRPC: function (route, args) { assert.step(args.method + ' ' + args.model) return this._super(route, args); }, }); // change the int_field through drag and drop // that way, we'll trigger the sorting and the name_get // of the lines of "p" testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), {position: 'top'} ); // Only those two should have been called // name_get on trululu would trigger an traceback assert.verifySteps(['default_get partner', 'onchange partner']); form.destroy(); }); QUnit.test('O2M List with pager, decoration and default_order: add and cancel adding', function (assert) { assert.expect(3); // The decoration on the list implies that its condition will be evaluated // against the data of the field (actual records *displayed*) // If one data is wrongly formed, it will crash // This test adds then cancels a record in a paged, ordered, and decorated list // That implies prefetching of records for sorting // and evaluation of the decoration against *visible records* this.data.partner.records[0].p = [2,4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); form.$('.o_field_x2many_list .o_field_x2many_list_row_add a').click(); var $dataRows = form.$('.o_field_x2many_list .o_data_row'); assert.equal($dataRows.length, 2, 'There should be 2 rows'); var $expectedSelectedRow = $dataRows.eq(1); var $actualSelectedRow = form.$('.o_selected_row'); assert.equal($actualSelectedRow[0], $expectedSelectedRow[0], 'The selected row should be the new one'); // Cancel Creation var escapeKey = $.ui.keyCode.ESCAPE; $actualSelectedRow.find('input').trigger( $.Event('keydown', {which: escapeKey, keyCode: escapeKey})); $dataRows = form.$('.o_field_x2many_list .o_data_row'); assert.equal($dataRows.length, 1, 'There should be 1 row'); form.destroy(); }); QUnit.test('O2M with parented m2o and domain on parent.m2o', function (assert) { assert.expect(3); /* records in an o2m can have a m2o pointing to themselves * in that case, a domain evaluation on that field followed by name_search * shouldn't send virtual_ids to the server */ this.data.turtle.fields.parent_id = {string: "Parent", type: "many2one", relation: 'turtle'}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', archs: { 'turtle,false,form': '
', }, mockRPC: function(route, args) { if (route === '/web/dataset/call_kw/turtle/name_search') { // We are going to pass twice here // First time, we really have nothing // Second time, a virtual_id has been created assert.deepEqual(args.kwargs.args, [['id', 'in', []]]); } return this._super(route, args); } }); form.$('.o_field_x2many_list[name=turtles] .o_field_x2many_list_row_add a').click(); var $modal = $('.modal-content'); var $turtleParent = $modal.find('.o_field_many2one input'); var $dropdown = $turtleParent.autocomplete('widget'); $turtleParent.click(); $dropdown.find('li.o_m2o_dropdown_option:contains(Create)').first().mouseenter().click(); $modal = $('.modal-content'); $modal.eq(1).find('.modal-footer .btn-primary').eq(0).click(); // Confirm new Record $modal.eq(0).find('.modal-footer .btn-primary').eq(1).click(); // Save & New assert.equal(form.$('.o_data_row').length, 1, 'The main record should have the new record in its o2m'); $modal = $('.modal-content'); $modal.find('.o_field_many2one input').click(); form.destroy(); }); QUnit.test('one2many list editable with cell readonly modifier', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; this.data.partner.records[1].turtles = [1,2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { assert.deepEqual(args.args[1].p[1][2], {foo: 'ff', qux: 99}, 'The right values should be written'); } return this._super(route, args); } }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); var $targetInput = $('.o_selected_row .o_input[name=foo]'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should have the focus'); // Simulating hitting the 'f' key twice $targetInput.val('f').trigger('input'); $targetInput.val($targetInput.val() + 'f').trigger('input'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should still have the focus'); // Simulating a TAB key $targetInput.trigger($.Event('keydown', {which: 9, keyCode:9})); var $secondTarget = $('.o_selected_row .o_input[name=qux]'); assert.equal($secondTarget[0], document.activeElement, 'The second input of the line should have the focus after the TAB press'); $secondTarget.val(9).trigger('input'); $secondTarget.val($secondTarget.val() + 9).trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many basic properties', function (assert) { assert.expect(6); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('td.o_list_record_selector').length, 0, "embedded one2many should not have a selector"); assert.ok(!form.$('.o_field_x2many_list_row_add').length, "embedded one2many should not be editable"); assert.ok(!form.$('td.o_list_record_delete').length, "embedded one2many records should not have a trash icon"); form.$buttons.find('.o_form_button_edit').click(); assert.ok(form.$('.o_field_x2many_list_row_add').length, "embedded one2many should now be editable"); assert.strictEqual(form.$('.o_field_x2many_list_row_add').attr('colspan'), "2", "should have colspan 2 (one for field foo, one for being below trash icon)"); assert.ok(form.$('td.o_list_record_delete').length, "embedded one2many records should have a trash icon"); form.destroy(); }); QUnit.test('transferring class attributes in one2many sub fields', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('td.hey').length, 1, 'should have a td with the desired class'); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.hey').length, 1, 'should have a td with the desired class'); form.$('td.o_data_cell').click(); assert.strictEqual(form.$('input[name="turtle_foo"].hey').length, 1, 'should have an input with the desired class'); form.destroy(); }); QUnit.test('one2many with date and datetime', function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, session: { getTZOffset: function () { return 120; }, }, }); assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017", "should have formatted the date"); assert.strictEqual(form.$('td:eq(1)').text(), "12/12/2016 12:55:05", "should have formatted the datetime"); form.destroy(); }); QUnit.test('rendering with embedded one2many', function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('th:contains(Foo)').length, 1, "embedded one2many should have a column titled according to foo"); assert.strictEqual(form.$('td:contains(blip)').length, 1, "embedded one2many should have a cell with relational value"); form.destroy(); }); QUnit.test('use the limit attribute in arch (in field o2m inline tree view)', function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1,2,3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.model === 'turtle') { assert.deepEqual(args.args[0], [1,2], 'should only load first 2 records'); } return this._super.apply(this, arguments); }, }); assert.strictEqual(form.$('.o_data_row').length, 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1,2,3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '', archs: { 'turtle,false,list': '', }, res_id: 1, mockRPC: function (route, args) { if (args.model === 'turtle') { assert.deepEqual(args.args[0], [1,2], 'should only load first 2 records'); } return this._super.apply(this, arguments); }, }); assert.strictEqual(form.$('.o_data_row').length, 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('embedded one2many with widget', function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.strictEqual(form.$('span.o_row_handle').length, 1, "should have 1 handles"); form.destroy(); }); QUnit.test('embedded one2many with handle widget', function (assert) { assert.expect(10); var nbConfirmChange = 0; testUtils.patch(ListRenderer, { confirmChange: function () { nbConfirmChange++; return this._super.apply(this, arguments); }, }); this.data.partner.records[0].turtles = [1, 2, 3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.turtles.data.turtle_int.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet"); // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), {position: 'top'} ); assert.strictEqual(nbConfirmChange, 1, "should have confirmed changes only once"); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should have the 3 rows in the new order"); form.$buttons.find('.o_form_button_save').click(); assert.deepEqual(_.map(this.data.turtle.records, function(turtle) { return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int'); }), [ {id: 1, turtle_foo: "yop", turtle_int: 1}, {id: 2, turtle_foo: "blip", turtle_int:0}, {id: 3, turtle_foo: "kawa", turtle_int:21} ], "should have save the changed sequence"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the new order"); testUtils.unpatch(ListRenderer); form.destroy(); }); QUnit.test('onchange for embedded one2many in a one2many with a second page', function (assert) { assert.expect(1); this.data.turtle.fields.partner_ids.type = 'one2many'; this.data.turtle.records[0].partner_ids = [1]; // we need a second page, so we set two records and only display one per page this.data.partner.records[0].turtles = [1, 2]; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], [1, 1, { turtle_foo: "hop", partner_ids: [[5], [4, 1]], }], [1, 2, { turtle_foo: "blip", partner_ids: [[5], [4, 2], [4, 4]], }], ]; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { var expectedResultTurtles = [ [1, 1, { turtle_foo: "hop", }], [1, 2, { partner_ids: [[4, 2, false], [4, 4, false]], turtle_foo: "blip", }], ]; assert.deepEqual(args.args[1].turtles, expectedResultTurtles, "the right values should be written"); } return this._super.apply(this, arguments); } }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_cell').eq(1).click(); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); $cell.val("hop").trigger('change'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('onchange for embedded one2many in a one2many updated by server', function (assert) { // here we test that after an onchange, the embedded one2many field has // been updated by a new list of ids by the server response, to this new // list should be correctly sent back at save time assert.expect(3); this.data.turtle.fields.partner_ids.type = 'one2many'; this.data.partner.records[0].turtles = [2]; this.data.turtle.records[1].partner_ids = [2]; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], [1, 2, { turtle_foo: "hop", partner_ids: [[5], [4, 2], [4, 4]], }], ]; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var expectedResultTurtles = [ [1, 2, { partner_ids: [[4, 2, false], [4, 4, false]], turtle_foo: "hop", }], ]; assert.deepEqual(args.args[1].turtles, expectedResultTurtles, 'The right values should be written'); } return this._super.apply(this, arguments); } }); assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record", "the partner_ids should be as specified at initialization"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_data_cell').eq(1).click(); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); $cell.val("hop").trigger("change"); form.$buttons.find('.o_form_button_save').click(); assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/), [ "second", "record", "aaa" ], 'The partner_ids should have been updated'); form.destroy(); }); QUnit.test('onchange for embedded one2many with handle widget', function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1, 2, 3]; var partnerOnchange = 0; this.data.partner.onchanges = { turtles: function (obj) { partnerOnchange++; }, }; var turtleOnchange = 0; this.data.turtle.onchanges = { turtle_int: function (obj) { turtleOnchange++; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), {position: 'top'} ); assert.strictEqual(turtleOnchange, 2, "should trigger one onchange per line updated"); assert.strictEqual(partnerOnchange, 1, "should trigger only one onchange on the parent"); form.destroy(); }); QUnit.test('onchange for embedded one2many with handle widget using same sequence', function (assert) { assert.expect(4); this.data.turtle.records[0].turtle_int = 1; this.data.turtle.records[1].turtle_int = 1; this.data.turtle.records[2].turtle_int = 1; this.data.partner.records[0].turtles = [1, 2, 3]; var turtleOnchange = 0; this.data.turtle.onchanges = { turtle_int: function () { turtleOnchange++; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].turtles, [[4,2,false],[1,1,{"turtle_int":2}],[1,3,{"turtle_int":3}]], "should change all lines that have changed (the first one doesn't change because it has the same sequence)"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), {position: 'top'} ); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(turtleOnchange, 3, "should update all lines"); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('onchange (with command 5) for embedded one2many with handle widget', function (assert) { assert.expect(3); var ids = []; for (var i=10; i<50; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$('.o_pager_next').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input'); // Drag and drop the third line in second position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), {position: 'top'} ); assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); form.$buttons.find('.o_form_button_save').click(); form.$('.o_pager_next').click(); assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); form.destroy(); }); QUnit.test('onchange with modifiers for embedded one2many on the second page', function (assert) { assert.expect(7); var data = this.data; var ids = []; for (var i=10; i<60; i++) { var id = 10 + i; ids.push(id); data.turtle.records.push({ id: id, turtle_int: 0, turtle_foo: "#" + id, }); } ids.push(1, 2, 3); data.partner.records[0].turtles = ids; data.partner.onchanges = { turtles: function (obj) { // TODO: make this test more 'difficult' // For now, the server only returns UPDATE commands (no LINK TO) // even though it should do it (for performance reasons) // var turtles = obj.turtles.splice(0, 20); var turtles = []; turtles.unshift([5]); // create UPDATE commands for each records (this is the server // usual answer for onchange) for (var k in obj.turtles) { var change = obj.turtles[k]; var record = _.findWhere(data.turtle.records, {id: change[1]}); if (change[0] === 1) { _.extend(record, change[2]); } turtles.push([1, record.id, record]); } obj.turtles = turtles; }, }; var form = createView({ View: FormView, model: 'partner', data: data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29", "should display the records in order"); form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input'); // the domain fail if the widget does not use the allready loaded data. form.$buttons.find('.o_form_button_cancel').click(); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "blurp#21#22#23#24#25#26#27#28#29", "should display the records in order with the changes"); $('.modal .modal-footer button:first').click(); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); form.$buttons.find('.o_form_button_edit').click(); // Drag and drop the third line in second position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), {position: 'top'} ); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#30#31#32#33#34#35#36#37#38", "should display the records in order after resequence (display record with turtle_int=0)"); // Drag and drop the third line in second position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), {position: 'top'} ); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence (display record with turtle_int=0)"); form.$buttons.find('.o_form_button_cancel').click(); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47", "should display the records in order after resequence"); $('.modal .modal-footer button:first').click(); assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29", "should cancel changes and display the records in order"); form.destroy(); }); QUnit.test('onchange followed by edition on the second page', function (assert) { assert.expect(12); var ids = []; for (var i=1; i<85; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: id/3|0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_pager_next').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input'); form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line"); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line"); form.$('.o_data_row input:first').val('value 3').trigger('input'); assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position after onchange"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange"); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line"); form.destroy(); }); QUnit.test('onchange followed by edition on the second page (part 2)', function (assert) { assert.expect(8); var ids = []; for (var i=1; i<85; i++) { var id = 10 + i; ids.push(id); this.data.turtle.records.push({ id: id, turtle_int: id/3|0, turtle_foo: "#" + i, }); } ids.splice(41, 0, 1, 2, 3); this.data.partner.records[0].turtles = ids; this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [[5]].concat(obj.turtles); }, }; // bottom order var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_pager_next').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input'); form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line"); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 41, "should display 41 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position"); form.$('.o_data_row input:first').val('value 3').trigger('input'); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('.o_data_row').length, 42, "should display 42 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position"); form.destroy(); }); QUnit.test('onchange returning a command 6 for an x2many', function (assert) { assert.expect(2); this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[6, false, [1, 2, 3]]]; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "there should be one record in the relation"); // change the value of foo to trigger the onchange form.$('.o_field_widget[name=foo]').val('some value').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 3, "there should be three records in the relation"); form.destroy(); }); QUnit.test('x2many fields inside x2manys are fetched after an onchange', function (assert) { assert.expect(6); this.data.turtle.records[0].partner_ids = [1]; this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[5], [4, 1], [4, 2], [4, 3]]; }, }; var checkRPC = false; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (checkRPC && args.method === 'read' && args.model === 'partner') { assert.deepEqual(args.args[1], ['display_name'], "should only read the display_name for the m2m tags"); assert.deepEqual(args.args[0], [1], "should only read the display_name of the unknown record"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "there should be one record in the relation"); assert.strictEqual(form.$('.o_data_row .o_field_widget[name=partner_ids]').text().replace(/\s/g, ''), 'secondrecordaaa', "many2many_tags should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for the m2m field form.$('.o_field_widget[name=foo]').val('some value').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 3, "there should be three records in the relation"); assert.strictEqual(form.$('.o_data_row:first .o_field_widget[name=partner_ids]').text().trim(), 'first record', "many2many_tags should be correctly displayed"); form.destroy(); }); QUnit.test('reference fields inside x2manys are fetched after an onchange', function (assert) { assert.expect(5); this.data.turtle.records[1].turtle_ref = 'product,41'; this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[5], [4, 1], [4, 2], [4, 3]]; }, }; var checkRPC = false; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (checkRPC && args.method === 'name_get') { assert.deepEqual(args.args[0], [37], "should only fetch the name_get of the unknown record"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "there should be one record in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpad', "reference field should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for reference field form.$('.o_field_widget[name=foo]').val('some value').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 3, "there should be three records in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpadxphone', "reference fields should be correctly displayed"); form.destroy(); }); QUnit.test('onchange on one2many containing x2many in form view', function (assert) { assert.expect(16); this.data.partner.onchanges = { foo: function (obj) { obj.turtles = [[0, false, {turtle_foo: 'new record'}]]; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', archs: { 'partner,false,list': '', 'partner,false,search': '', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "the onchange should have created one record in the relation"); // open the created o2m record in a form view, and add a m2m subrecord // in its relation form.$('.o_data_row').click(); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 0, "there should be no record in the one2many in the dialog"); // add a many2many subrecord $('.modal .o_field_x2many_list_row_add a').click(); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); // select a many2many subrecord $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'), 'm2m pager should be hidden'); // click on 'Save & Close' $('.modal .modal-footer .btn-primary:first').click(); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record, and another m2m subrecord in its relation, but // discard the changes form.$('.o_data_row').click(); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); // add another m2m subrecord $('.modal .o_field_x2many_list_row_add a').click(); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); $('.modal:nth(1) .o_list_view .o_data_cell:first').click(); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 2, "there should be two records in the one2many in the dialog"); // click on 'Discard' $('.modal .modal-footer .btn-default').click(); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record to check that second changes have properly been discarded form.$('.o_data_row').click(); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); form.destroy(); }); QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) { var done = assert.async(); assert.expect(20); this.data.turtle.records[0].turtle_int = 6; this.data.turtle.records.push({ id: 4, turtle_int: 20, turtle_foo: "a1", }, { id: 5, turtle_int: 9, turtle_foo: "a2", }, { id: 6, turtle_int: 2, turtle_foo: "a3", }, { id: 7, turtle_int: 11, turtle_foo: "a4", }); this.data.partner.records[0].turtles = [1, 2, 3, 4, 5, 6, 7]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.intercept(form, "field_changed", function (event) { assert.step(form.model.get(event.data.changes.turtles.id).res_id); }, true); form.$buttons.find('.o_form_button_edit').click(); var steps = []; var positions = [ [6, 0, 'top', [3, 6, 1, 2, 5, 7, 4]], // move the last to the first line [5, 1, 'top', [7, 6, 1, 2, 5]], // move the penultimate to the second line [2, 5, 'center', [1, 2, 5, 6]], // move the third to the penultimate line ]; function dragAndDrop() { var pos = positions.shift(); testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(pos[0]), form.$('tbody tr').eq(pos[1]), {position: pos[2]} ); steps = steps.concat(pos[3]); assert.verifySteps(steps, "sequences values should be apply from the begin index to the drop index"); if (positions.length) { setTimeout(dragAndDrop, 10); } else { assert.deepEqual(_.pluck(form.model.get(form.handle).data.turtles.data, 'data'), [ { id: 3, turtle_foo: "kawa", turtle_int: 2 }, { id: 7, turtle_foo: "a4", turtle_int: 3 }, { id: 1, turtle_foo: "yop", turtle_int: 4 }, { id: 2, turtle_foo: "blip", turtle_int: 5 }, { id: 5, turtle_foo: "a2", turtle_int: 6 }, { id: 6, turtle_foo: "a3", turtle_int: 7 }, { id: 4, turtle_foo: "a1", turtle_int: 8 } ], "sequences must be apply correctly"); form.destroy(); done(); } } dragAndDrop(); }); QUnit.test('embedded one2many (editable list) with handle widget', function (assert) { assert.expect(8); this.data.partner.records[0].p = [1, 2, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.p.data.int_field.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should have the 3 rows in the correct order"); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should still have the 3 rows in the correct order"); // Drag and drop the second line in first position testUtils.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), {position: 'top'} ); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should have the 3 rows in the new order"); form.$('tbody tr:first td:first').click(); assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip", "should edit the correct row"); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should still have the 3 rows in the new order"); form.destroy(); }); QUnit.test('one2many field when using the pager', function (assert) { assert.expect(13); var ids = []; for (var i=0; i<45; i++) { var id = 10 + i; ids.push(id); this.data.partner.records.push({ id: id, display_name: "relational record " + id, }); } this.data.partner.records[0].p = ids.slice(0, 42); this.data.partner.records[1].p = ids.slice(42); var count = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
', viewOptions: { ids: [1, 2], index: 0, }, mockRPC: function () { count++; return this._super.apply(this, arguments); }, res_id: 1, }); // we are on record 1, which has 90 related record (first 40 should be // displayed), 2 RPCs (read) should have been done, one on the main record // and one for the O2M assert.strictEqual(count, 2, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'one2many kanban should contain 40 cards for record 1'); // move to record 2, which has 3 related records (and shouldn't contain the // related records of record 1 anymore). Two additional RPCs should have // been done form.pager.next(); assert.strictEqual(count, 4, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3, 'one2many kanban should contain 3 cards for record 2'); // move back to record 1, which should contain again its first 40 related // records form.pager.previous(); assert.strictEqual(count, 6, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'one2many kanban should contain 40 cards for record 1'); // move to the second page of the o2m: 1 RPC should have been done to fetch // the 2 subrecords of page 2, and those records should now be displayed form.$('.o_x2m_control_panel .o_pager_next').click(); assert.strictEqual(count, 7, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); // move to record 2 again and check that everything is correctly updated form.pager.next(); assert.strictEqual(count, 9, 'two RPCs should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 3, 'one2many kanban should contain 3 cards for record 2'); // move back to record 1 and move to page 2 again: all data should have // been correctly reloaded form.pager.previous(); assert.strictEqual(count, 11, 'two RPCs should have been done'); form.$('.o_x2m_control_panel .o_pager_next').click(); assert.strictEqual(count, 12, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); form.destroy(); }); QUnit.test('edition of one2many field with pager', function (assert) { assert.expect(31); var ids = []; for (var i = 0; i < 45; i++) { var id = 10 + i; ids.push(id); this.data.partner.records.push({ id: id, display_name: "relational record " + id, }); } this.data.partner.records[0].p = ids; var saveCount = 0; var checkRead = false; var readIDs; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', archs: { 'partner,false,form': '
', }, mockRPC: function (route, args) { if (args.method === 'read' && checkRead) { readIDs = args.args[0]; checkRead = false; } if (args.method === 'write') { saveCount++; var nbCommands = args.args[1].p.length; var nbLinkCommands = _.filter(args.args[1].p, function (command) { return command[0] === 4; }).length; switch(saveCount) { case 1: assert.strictEqual(nbCommands, 46, "should send 46 commands (one for each record)"); assert.strictEqual(nbLinkCommands, 45, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[45], [0, args.args[1].p[45][1], { display_name: 'new record', }], "should sent a CREATE command for the new record"); break; case 2: assert.strictEqual(nbCommands, 46, "should send 46 commands"); assert.strictEqual(nbLinkCommands, 45, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[45], [2, 10, false], "should sent a DELETE command for the deleted record"); break; case 3: assert.strictEqual(nbCommands, 47, "should send 47 commands"); assert.strictEqual(nbLinkCommands, 43, "should send a LINK_TO command for each existing record"); assert.deepEqual(args.args[1].p[43], [0, args.args[1].p[43][1], {display_name: 'new record page 1'}], "should sent correct CREATE command"); assert.deepEqual(args.args[1].p[44], [0, args.args[1].p[44][1], {display_name: 'new record page 2'}], "should sent correct CREATE command"); assert.deepEqual(args.args[1].p[45], [2, 11, false], "should sent correct DELETE command"); assert.deepEqual(args.args[1].p[46], [2, 52, false], "should sent correct DELETE command"); break; } } return this._super.apply(this, arguments); }, res_id: 1, }); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 45', "pager range should be correct"); // add a record on page one checkRead = true; form.$buttons.find('.o_form_button_edit').click(); form.$('.o-kanban-button-new').click(); $('.modal input').val('new record').trigger('input'); $('.modal .modal-footer .btn-primary:first').click(); // save and close // checks assert.strictEqual(readIDs, undefined, "should not have read any record"); assert.strictEqual(form.$('span:contains(new record)').length, 0, "new record should be on page 2"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 46', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length, 0, 'new record should not be on page 1'); // save form.$buttons.find('.o_form_button_save').click(); // delete a record on page one checkRead = true; form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length, 1, 'first record should be the one with id 10 (next checks rely on that)'); form.$('.delete_icon:first').click(); // checks assert.deepEqual(readIDs, [50], "should have read a record (to display 40 records on page 1)"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 45', "pager range should be correct"); // save form.$buttons.find('.o_form_button_save').click(); // add and delete records in both pages form.$buttons.find('.o_form_button_edit').click(); checkRead = true; readIDs = undefined; // add and delete a record in page 1 form.$('.o-kanban-button-new').click(); $('.modal input').val('new record page 1').trigger('input'); $('.modal .modal-footer .btn-primary:first').click(); // save and close assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length, 1, 'first record should be the one with id 11 (next checks rely on that)'); form.$('.delete_icon:first').click(); assert.deepEqual(readIDs, [51], "should have read a record (to display 40 records on page 1)"); // add and delete a record in page 2 form.$('.o_x2m_control_panel .o_pager_next').click(); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length, 1, 'first record should be the one with id 52 (next checks rely on that)'); checkRead = true; readIDs = undefined; form.$('.delete_icon:first').click(); form.$('.o-kanban-button-new').click(); $('.modal input').val('new record page 2').trigger('input'); $('.modal .modal-footer .btn-primary:first').click(); // save and close assert.strictEqual(readIDs, undefined, "should not have read any record"); // checks assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5, 'there should be 5 records on page 2'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '41-45 / 45', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length, 1, 'new records should be on page 2'); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length, 1, 'new records should be on page 2'); // save form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('edition of one2many field, with onchange and not inline sub view', function (assert) { assert.expect(2); this.data.turtle.onchanges.turtle_int = function (obj) { obj.turtle_foo = String(obj.turtle_int); }; this.data.partner.onchanges.turtles = function () {}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '', archs: { 'turtle,false,list': '', 'turtle,false,form': '
', }, mockRPC: function (route, args) { return this._super.apply(this, arguments); }, res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); $('input[name="turtle_int"]').val('5').trigger('input'); $('.modal-footer button.btn-primary').first().click(); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '5', 'should display 5 in the foo field'); form.$('tbody tr:eq(1) td.o_data_cell').click(); $('input[name="turtle_int"]').val('3').trigger('input'); $('.modal-footer button.btn-primary').first().click(); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '3', 'should now display 3 in the foo field'); form.destroy(); }); QUnit.test('sorting one2many fields', function (assert) { assert.expect(4); this.data.partner.fields.foo.sortable = true; this.data.partner.records.push({id: 23, foo: "abc"}); this.data.partner.records.push({id: 24, foo: "xyz"}); this.data.partner.records.push({id: 25, foo: "def"}); this.data.partner.records[0].p = [23,24,25]; var rpcCount = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function () { rpcCount++; return this._super.apply(this, arguments); }, }); rpcCount = 0; assert.ok(form.$('table tbody tr:eq(2) td:contains(def)').length, "the 3rd record is the one with 'def' value"); form.renderer._render = function () { throw "should not render the whole form"; }; form.$('table thead th:contains(Foo)').click(); assert.strictEqual(rpcCount, 0, 'sort should be in memory, no extra RPCs should have been done'); assert.ok(form.$('table tbody tr:eq(2) td:contains(xyz)').length, "the 3rd record is the one with 'xyz' value"); form.$('table thead th:contains(Foo)').click(); assert.ok(form.$('table tbody tr:eq(2) td:contains(abc)').length, "the 3rd record is the one with 'abc' value"); form.destroy(); }); QUnit.test('one2many list field edition', function (assert) { assert.expect(6); this.data.partner.records.push({ id: 3, display_name: "relational record 1", }); this.data.partner.records[1].p = [3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit the first line of the o2m assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_one2many tbody td').first().click(); assert.ok(form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'), "first row of o2m should be in edition"); form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input'); assert.ok(form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'), "first row of o2m should still be in edition"); // // leave o2m edition form.$el.click(); assert.ok(!form.$('.o_field_one2many tbody td').first().parent().hasClass('o_selected_row'), "first row of o2m should be readonly again"); // discard changes form.$buttons.find('.o_form_button_cancel').click(); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', "changes shouldn't have been discarded yet, waiting for user confirmation"); $('.modal .modal-footer .btn-primary').click(); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); // edit again and save form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_one2many tbody td').first().click(); form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input'); form.$el.click(); form.$buttons.find('.o_form_button_save').click(); // FIXME: this next test doesn't pass as the save of updates of // relational data is temporarily disabled // assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', // "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many list: create action disabled', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); form.$buttons.find('.o_form_button_edit').click(); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); form.destroy(); }); QUnit.test('one2many list: unlink one record', function (assert) { assert.expect(5); this.data.partner.records[0].p = [2, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; assert.strictEqual(commands.length, 2, 'should have generated two commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 4, 'should have generated the command 4 (LINK_TO) with id 4'); assert.ok(commands[1][0] === 3 && commands[1][1] === 2, 'should have generated the command 3 (UNLINK) with id 2'); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_list_record_delete button').length, 2, "should have 2 delete buttons"); form.$('td.o_list_record_delete button').first().click(); assert.strictEqual(form.$('td.o_list_record_delete button').length, 1, "should have 1 delete button (a record is supposed to have been unlinked)"); // save and check that the correct command has been generated form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many list: deleting one record', function (assert) { assert.expect(5); this.data.partner.records[0].p = [2, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/partner/write') { var commands = args.args[1].p; assert.strictEqual(commands.length, 2, 'should have generated two commands'); assert.ok(commands[0][0] === 4 && commands[0][1] === 4, 'should have generated the command 4 (LINK_TO) with id 4'); assert.ok(commands[1][0] === 2 && commands[1][1] === 2, 'should have generated the command 2 (DELETE) with id 2'); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('td.o_list_record_delete button').length, 2, "should have 2 delete buttons"); form.$('td.o_list_record_delete button').first().click(); assert.strictEqual(form.$('td.o_list_record_delete button').length, 1, "should have 1 delete button (a record is supposed to have been deleted)"); // save and check that the correct command has been generated form.$buttons.find('.o_form_button_save').click(); // FIXME: it would be nice to test that the view is re-rendered correctly, // but as the relational data isn't re-fetched, the rendering is ok even // if the changes haven't been saved form.destroy(); }); QUnit.test('one2many kanban: edition', function (assert) { assert.expect(14); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + // color will be in the kanban but not in the form '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + // foo will be in the form but not in the kanban '' + '' + '' + '', res_id: 1, }); assert.ok(!form.$('.o_kanban_view .delete_icon').length, 'delete icon should not be visible in readonly'); assert.ok(!form.$('.o_field_one2many .o-kanban-button-new').length, '"Create" button should not be visible in readonly'); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 record'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'second record', 'display_name of subrecord should be the one in DB'); assert.strictEqual(form.$('.o_kanban_record span:nth(1)').text(), 'Red', 'color of subrecord should be the one in DB'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); assert.ok(form.$('.o_field_one2many .o-kanban-button-new').length, '"Create" button should be visible in edit'); // edit existing subrecord form.$('.oe_kanban_global_click').click(); $('.modal .o_form_view input').val('new name').trigger('input'); $('.modal .modal-footer .btn-primary').click(); // save assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new name', 'value of subrecord should have been updated'); // create a new subrecord form.$('.o-kanban-button-new').click(); $('.modal .o_form_view input').val('new subrecord 1').trigger('input'); $('.modal .modal-footer .btn-primary').click(); // save and close assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1', 'value of newly created subrecord should be "new subrecord 1"'); // create two new subrecords form.$('.o-kanban-button-new').click(); $('.modal .o_form_view input').val('new subrecord 2').trigger('input'); $('.modal .modal-footer .btn-primary:nth(1)').click(); // save and new $('.modal .o_form_view input').val('new subrecord 3').trigger('input'); $('.modal .modal-footer .btn-primary').click(); // save and close assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4, 'should contain 4 records'); // delete subrecords form.$('.o_kanban_view .delete_icon:first()').click(); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3, 'should contain 3 records'); form.$('.o_kanban_view .delete_icon:first()').click(); form.$('.o_kanban_view .delete_icon:first()').click(); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 records'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new subrecord 3', 'the remaining subrecord should be "new subrecord 3"'); form.destroy(); }); QUnit.test('one2many kanban: create action disabled', function (assert) { assert.expect(3); this.data.partner.records[0].p = [4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, }); assert.ok(!form.$('.o-kanban-button-new').length, '"Add" button should not be available in readonly'); form.$buttons.find('.o_form_button_edit').click(); assert.ok(!form.$('.o-kanban-button-new').length, '"Add" button should not be available in edit'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); form.destroy(); }); QUnit.test('editable one2many list, pager is updated', function (assert) { assert.expect(1); this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); this.data.partner.records[0].turtles = [1,2,3,4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, then click in form view to confirm it form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$el.click(); assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5', "pager should display the correct total"); form.destroy(); }); QUnit.test('one2many list (non editable): edition', function (assert) { assert.expect(12); var nbWrite = 0; this.data.partner.records[0].p = [2, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { nbWrite++; assert.deepEqual(args.args[1], { p: [[1, 2, {display_name: 'new name'}], [2, 4, false]] }, "should have sent the correct commands"); } return this._super.apply(this, arguments); }, }); assert.ok(!form.$('.o_list_record_delete').length, 'delete icon should not be visible in readonly'); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not be visible in readonly'); form.$buttons.find('.o_form_button_edit').click(); assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'second record', 'display_name of first subrecord should be the one in DB'); assert.ok(form.$('.o_list_record_delete').length, 'delete icon should be visible in edit'); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not visible in edit'); // edit existing subrecord form.$('.o_list_view tbody tr:first() td:eq(1)').click(); $('.modal .o_form_view input').val('new name').trigger('input'); $('.modal .modal-footer .btn-primary').click(); // save assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'value of subrecord should have been updated'); assert.strictEqual(nbWrite, 0, "should not have write anything in DB"); // create new subrecords // TODO when 'Add an item' will be implemented // delete subrecords form.$('.o_list_record_delete:nth(1)').click(); assert.strictEqual(form.$('.o_list_view td.o_list_number').length, 1, 'should contain 1 subrecord'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'the remaining subrecord should be "new name"'); form.$buttons.find('.o_form_button_save').click(); // save the record assert.strictEqual(nbWrite, 1, "should have write the changes in DB"); form.destroy(); }); QUnit.test('one2many list (editable): edition', function (assert) { assert.expect(7); this.data.partner.records[0].p = [2, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, }); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); form.$('.o_list_view tbody td:first()').click(); assert.ok($('.modal .o_form_readonly').length, 'in readonly, clicking on a subrecord should open it in readonly in a dialog'); $('.modal .o_form_button_cancel').click(); // close the dialog form.$buttons.find('.o_form_button_edit').click(); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should be available in edit'); // edit existing subrecord form.$('.o_list_view tbody td:first()').click(); assert.strictEqual($('.modal').length, 0, 'in edit, clicking on a subrecord should not open a dialog'); assert.ok(form.$('.o_list_view tbody tr:first()').hasClass('o_selected_row'), 'first row should be in edition'); form.$('.o_list_view input:first()').val('new name').trigger('input'); form.$('.o_list_view tbody tr:nth(1) td:first').click(); // click on second record to validate the first one assert.ok(!form.$('.o_list_view tbody tr:first').hasClass('o_selected_row'), 'first row should not be in edition anymore'); assert.strictEqual(form.$('.o_list_view tbody td:first').text(), 'new name', 'value of subrecord should have been updated'); // create new subrecords // TODO when 'Add an item' will be implemented form.destroy(); }); QUnit.test('one2many list (editable): edition, part 2', function (assert) { assert.expect(8); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'write') { assert.strictEqual(args.args[1].p[0][0], 0, "should send a 0 command for field p"); assert.strictEqual(args.args[1].p[1][0], 0, "should send a second 0 command for field p"); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item and enter a value form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_selected_row > td input').val('kartoffel').trigger('input'); // click again on Add an item form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.strictEqual(form.$('.o_selected_row > td input').length, 1, "should have one other new td"); assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows"); // enter another value and save form.$('.o_selected_row > td input').val('gemuse').trigger('input'); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('tr.o_data_row').length, 2, "should have 2 data rows"); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.strictEqual(form.$('td:contains(gemuse)').length, 1, "should have one td with the new value"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 3', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // edit mode, then click on Add an item 2 times assert.strictEqual(form.$('tr.o_data_row').length, 1, "should have 1 data rows"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('tr.o_data_row').length, 3, "should have 3 data rows"); // cancel the edition form.$buttons.find('.o_form_button_cancel').click(); $('.modal-footer button.btn-primary').first().click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "should have 1 data rows"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 4', function (assert) { assert.expect(3); var i = 0; this.data.turtle.onchanges = { turtle_trululu: function (obj) { if (i) { obj.turtle_description = "Some Description"; } i++; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit mode, then click on Add an item assert.strictEqual(form.$('tr.o_data_row').length, 0, "should have 0 data rows"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('textarea').val(), "", "field turtle_description should be empty"); // add a value in the turtle_trululu field to trigger an onchange var $dropdown = form.$('.o_field_many2one[name=turtle_trululu] input') .autocomplete('widget'); form.$('.o_field_many2one[name=turtle_trululu] input').click(); $dropdown.find('a:contains(first record)').mouseenter().click(); assert.strictEqual(form.$('textarea').val(), "Some Description", "field turtle_description should be set to the result of the onchange"); form.destroy(); }); QUnit.test('one2many list (editable): discarding required empty data', function (assert) { assert.expect(7); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method) { assert.step(args.method); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item, then click elsewhere assert.strictEqual(form.$('tr.o_data_row').length, 0, "should have 0 data rows"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('label.o_form_label').first().click(); assert.strictEqual(form.$('tr.o_data_row').length, 0, "should still have 0 data rows"); // click on Add an item again, then click on save form.$('.o_field_x2many_list_row_add a').click(); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('tr.o_data_row').length, 0, "should still have 0 data rows"); assert.verifySteps(['read', 'default_get', 'default_get']); form.destroy(); }); QUnit.test('editable one2many list, adding line when only one page', function (assert) { assert.expect(1); this.data.partner.records[0].turtles=[1,2,3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, to reach the page size limit form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); // the record currently being added should not count in the pager assert.ok(!form.$('.o_cp_pager').is(':visible'), "pager should not be visible"); form.destroy(); }); QUnit.test('editable one2many list, adding line, then discarding', function (assert) { assert.expect(1); this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); this.data.partner.records[0].turtles = [1,2,3,4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, then discard form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$buttons.find('.o_form_button_cancel').click(); // confirm the discard operation $('.modal .modal-footer .btn-primary').click(); // click on confirm assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', "pager should still be visible"); form.destroy(); }); QUnit.test('editable one2many list, required field and pager', function (assert) { assert.expect(1); this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); this.data.turtle.fields.turtle_foo.required = true; this.data.partner.records[0].turtles = [1,2,3,4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a (empty) record form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); // go on next page. The new record is not valid and should be discarded form.$('.o_pager_next').click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "should have 1 data rows"); form.destroy(); }); QUnit.test('editable one2many list, required field, pager and confirm discard', function (assert) { assert.expect(3); this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); this.data.turtle.fields.turtle_foo.required = true; this.data.partner.records[0].turtles = [1,2,3,4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record with a dirty state, but not valid form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('input[name="turtle_int"]').val(4321).trigger('input'); // go to next page. The new record is not valid, but dirty. we should // see a confirm dialog form.$('.o_pager_next').click(); assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', "pager should still display the correct total"); // click on cancel $('.modal .modal-footer .btn-default').click(); // click on cancel assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', "pager should again display the correct total"); assert.strictEqual(form.$('.o_field_one2many input.o_field_invalid').length, 1, "there should be an invalid input in the one2many"); form.destroy(); }); QUnit.test('editable one2many list, adding, discarding, and pager', function (assert) { assert.expect(2); this.data.partner.records[0].turtles = [1]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a 4 records record (to make the pager appear) form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_x2many_list_row_add a').click(); // go on next page form.$('.o_pager_next').click(); // discard form.$buttons.find('.o_form_button_cancel').click(); $('.modal .modal-footer .btn-primary').click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "should have 1 data row"); assert.ok(!form.$('.o_cp_pager').is(':visible'), "pager should not be visible"); form.destroy(); }); QUnit.test('unselecting a line with missing required data', function (assert) { assert.expect(5); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, }); // edit mode, then click on Add an item, then click elsewhere assert.strictEqual(form.$('tr.o_data_row').length, 0, "should have 0 data rows"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "should have 1 data rows"); // adding a value in the non required field, so it is dirty, but with // a missing required field form.$('input[name="turtle_int"]').val('12345').trigger('input'); // click elsewhere, form.$('label.o_form_label').click(); assert.strictEqual($('.modal').length, 1, 'a confirmation model should be opened'); // click on cancel, the line should still be selected $('.modal .modal-footer button.btn-default').click(); assert.strictEqual(form.$('tr.o_data_row.o_selected_row').length, 1, "should still have 1 selected data row"); // click elsewhere, and click on ok (on the confirmation dialog) form.$('label.o_form_label').click(); $('.modal .modal-footer button.btn-primary').click(); assert.strictEqual(form.$('tr.o_data_row').length, 0, "should have 0 data rows (invalid line has been discarded"); form.destroy(); }); QUnit.test('pressing enter in a o2m with a required empty m2o', function (assert) { assert.expect(4); this.data.turtle.fields.turtle_foo.required = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item, then click elsewhere form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('input[name="turtle_foo"]').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); assert.ok(form.$('input[name="turtle_foo"]').hasClass('o_field_invalid'), "input should be marked invalid"); assert.verifySteps(['read', 'default_get']); form.destroy(); }); QUnit.test('editing a o2m, with required field and onchange', function (assert) { assert.expect(12); this.data.turtle.fields.turtle_foo.required = true; delete this.data.turtle.fields.turtle_foo.default; this.data.turtle.onchanges = { turtle_foo: function (obj) { obj.turtle_int = obj.turtle_foo.length; }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method) { assert.step(args.method); } return this._super.apply(this, arguments); }, }); // edit mode, then click on Add an item assert.strictEqual(form.$('tr.o_data_row').length, 0, "should have 0 data rows"); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); // input some text in required turtle_foo field form.$('input[name="turtle_foo"]').val('aubergine').trigger('input'); assert.strictEqual(form.$('input[name="turtle_int"]').val(), "9", "onchange should have been triggered"); // save and check everything is fine form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1, "should have one row with turtle_foo value"); assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1, "should have one row with turtle_int value"); assert.verifySteps(['read', 'default_get', 'onchange', 'onchange', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('editable o2m, pressing ESC discard current changes', function (assert) { assert.expect(5); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "there should be one data row"); form.$('input[name="turtle_foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); assert.strictEqual(form.$('tr.o_data_row').length, 0, "data row should have been discarded"); assert.verifySteps(['read', 'default_get']); form.destroy(); }); QUnit.test('editable o2m with required field, pressing ESC discard current changes', function (assert) { assert.expect(5); this.data.turtle.fields.turtle_foo.required = true; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('tr.o_data_row').length, 1, "there should be one data row"); form.$('input[name="turtle_foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); assert.strictEqual(form.$('tr.o_data_row').length, 0, "data row should have been discarded"); assert.verifySteps(['read', 'default_get']); form.destroy(); }); QUnit.test('pressing escape in editable o2m list in dialog', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { "partner,false,form": '
' + '' + '' + '' + '' + '' + '
', }, viewOptions: { mode: 'edit', }, }); form.$('.o_field_x2many_list_row_add a').click(); $('.modal .o_field_x2many_list_row_add a').click(); assert.strictEqual($('.modal .o_data_row.o_selected_row').length, 1, "there should be a row in edition in the dialog"); // trigger keydown ESCAPE in the edited row $('.modal .o_data_cell input').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE}); assert.strictEqual($('.modal').length, 1, "dialog should still be open"); assert.strictEqual($('.modal .o_data_row').length, 0, "the row should have been removed"); form.destroy(); }); QUnit.test('editable o2m with onchange and required field: delete an invalid line', function (assert) { assert.expect(5); this.data.partner.onchanges = { turtles: function () {}, }; this.data.partner.records[0].turtles = [1]; this.data.turtle.records[0].product_id = 37; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, viewOptions: { mode: 'edit', }, }); form.$('.o_data_cell:first').click(); form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup'); assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid'); form.$('.o_list_record_delete').click(); assert.verifySteps(['read', 'read', 'onchange'], 'onchange should have been done'); form.destroy(); }); QUnit.test('onchange in a one2many', function (assert) { assert.expect(1); this.data.partner.records.push({ id: 3, foo: "relational record 1", }); this.data.partner.records[1].p = [3]; this.data.partner.onchanges = {p: true}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method === 'onchange') { return $.when({value: { p: [ [5], // delete all [0, 0, {foo: "from onchange"}], // create new ]}}); } return this._super(route, args); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_one2many tbody td').first().click(); form.$('.o_field_one2many tbody td').first().find('input').val("new value").trigger('input'); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange', "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many, default_get and onchange (basic)', function (assert) { assert.expect(1); this.data.partner.fields.p.default = [ [6, 0, []], // replace with zero ids ]; this.data.partner.onchanges = {p: true}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'onchange') { return $.when({value: { p: [ [5], // delete all [0, 0, {foo: "from onchange"}], // create new ]}}); } return this._super(route, args); }, }); assert.ok(form.$('td:contains(from onchange)').length, "should have 'from onchange' value in one2many"); form.destroy(); }); QUnit.test('one2many and default_get (with date)', function (assert) { assert.expect(1); this.data.partner.fields.p.default = [ [0, false, {date: '2017-10-08'}], ]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('.o_data_cell').text(), '10/08/2017', "should correctly display the date"); form.destroy(); }); QUnit.test('one2many and onchange (with integer)', function (assert) { assert.expect(4); this.data.turtle.onchanges = { turtle_int: function (obj) {} }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('td:contains(9)').click(); form.$('td input[name="turtle_int"]').val("3").trigger('input'); // the 'change' event is triggered on the input when we focus somewhere // else, for example by clicking in the body. However, if we try to // programmatically click in the body, it does not trigger a change // event, so we simply trigger it directly instead. form.$('td input[name="turtle_int"]').trigger('change'); assert.verifySteps(['read', 'read', 'onchange']); form.destroy(); }); QUnit.test('one2many and onchange (with date)', function (assert) { assert.expect(7); this.data.partner.onchanges = { date: function (obj) {} }; this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { assert.step(args.method); return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('td:contains(01/25/2017)').click(); form.$('.o_datepicker_input').click(); $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection $('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection $('.bootstrap-datetimepicker-widget .year:contains(2017)').click(); $('.bootstrap-datetimepicker-widget .month').eq(1).click(); // February $('.day:contains(22)').click(); // select the 22 February form.$buttons.find('.o_form_button_save').click(); assert.verifySteps(['read', 'read', 'onchange', 'write', 'read', 'read']); form.destroy(); }); QUnit.test('one2many and onchange (with command DELETE_ALL)', function (assert) { assert.expect(5); this.data.partner.onchanges = { foo: function (obj) { obj.p = [[5]]; }, p: function () {}, // dummy onchange on the o2m to execute _isX2ManyValid() }; this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (method, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].p, [ [0, args.args[1].p[0][1], {display_name: 'z'}], [2, 2, false], ], "correct commands should be sent"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "o2m should contain one row"); // empty o2m by triggering the onchange form.$('.o_field_widget[name=foo]').val('trigger onchange').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 0, "rows of the o2m should have been deleted"); // add two new subrecords form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_widget[name=display_name]').val('x').trigger('input'); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_widget[name=display_name]').val('y').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 2, "o2m should contain two rows"); // empty o2m by triggering the onchange form.$('.o_field_widget[name=foo]').val('trigger onchange again').trigger('input'); assert.strictEqual(form.$('.o_data_row').length, 0, "rows of the o2m should have been deleted"); form.$('.o_field_x2many_list_row_add a').click(); form.$('.o_field_widget[name=display_name]').val('z').trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many and onchange only write modified field', function (assert) { assert.expect(2); this.data.partner.onchanges = { turtles: function (obj) { obj.turtles = [ [5], // delete all [1, 3, { // the server returns all fields display_name: "coucou", product_id: [37, "xphone"], turtle_bar: false, turtle_foo: "has changed", turtle_int: 42, turtle_qux: 9.8, partner_ids: [], turtle_ref: 'product,37', }], ]; }, }; this.data.partner.records[0].turtles = [3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (method, args) { if (args.method === 'write') { assert.deepEqual(args.args[1].turtles, [ [1, 3, {display_name: 'coucou', turtle_foo: 'has changed', turtle_int: 42}], ], "correct commands should be sent (only send changed values)"); } return this._super.apply(this, arguments); }, res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 1, "o2m should contain one row"); form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click(); form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many with CREATE onchanges correctly refreshed', function (assert) { assert.expect(5); var delta = 0; testUtils.patch(AbstractField, { init: function () { delta++; this._super.apply(this, arguments); }, destroy: function () { delta--; this._super.apply(this, arguments); }, }); var deactiveOnchange = true; this.data.partner.records[0].turtles = []; this.data.partner.onchanges = { turtles: function (obj) { if (deactiveOnchange) { return; } // the onchange will either: // - create a second line if there is only one line // - edit the second line if there are two lines if (obj.turtles.length === 1) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, 0, { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } else if (obj.turtles.length === 2) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, obj.turtles[1][1], { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_row').length, 0, "o2m shouldn't contain any row"); form.$('.o_field_x2many_list_row_add a').click(); // trigger the first onchange deactiveOnchange = false; form.$('input[name="turtle_int"]').val('10').trigger('input'); // put the list back in non edit mode form.$('input[name="foo"]').click(); assert.strictEqual(form.$('.o_data_row').text(), "first10second-10", "should correctly refresh the records"); // trigger the second onchange form.$('.o_field_x2many_list tbody tr:first td:first').click(); form.$('input[name="turtle_int"]').val('20').trigger('input'); form.$('input[name="foo"]').click(); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records"); assert.strictEqual(form.$('.o_field_widget').length, delta, "all (non visible) field widgets should have been destroyed"); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records after save"); form.destroy(); testUtils.unpatch(AbstractField); }); QUnit.test('editable one2many with sub widgets are rendered in readonly', function (assert) { assert.expect(2); var editableWidgets = 0; testUtils.patch(AbstractField, { init: function () { this._super.apply(this, arguments); if (this.mode === 'edit') { editableWidgets++; } }, }); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(editableWidgets, 1, "o2m is only widget in edit mode"); form.$('tbody td.o_field_x2many_list_row_add a').click(); assert.strictEqual(editableWidgets, 3, "3 widgets currently in edit mode"); form.destroy(); testUtils.unpatch(AbstractField); }); QUnit.test('one2many editable list with onchange keeps the order', function (assert) { assert.expect(2); this.data.partner.records[0].p = [1, 2, 4]; this.data.partner.onchanges = { p: function () {}, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '
', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.strictEqual(form.$('.o_data_cell').text(), 'first recordsecond recordaaa', "records should be display in the correct order"); form.$('.o_data_row:first .o_data_cell').click(); form.$('.o_selected_row .o_field_widget[name=display_name]').val('new').trigger('input'); form.$el.click(); // click outside to validate the row assert.strictEqual(form.$('.o_data_cell').text(), 'newsecond recordaaa', "records should be display in the correct order"); form.destroy(); }); QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) { assert.expect(2); this.data.partner.records[0].p = [2, 4]; this.data.partner.records[1].product_id = false; this.data.partner.records[2].product_id = 37; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); assert.ok(form.$('.o_list_view tbody tr:eq(0) td:first').hasClass('o_readonly_modifier'), "first record should have display_name in readonly mode"); assert.notOk(form.$('.o_list_view tbody tr:eq(1) td:first').hasClass('o_readonly_modifier'), "second record should not have display_name in readonly mode"); form.destroy(); }); QUnit.test('pager of one2many field in new record', function (assert) { assert.expect(2); this.data.partner.records[0].p = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', archs: { 'partner,false,form': '
', }, }); assert.ok(!form.$('.o_x2m_control_panel .o_cp_pager div').is(':visible'), 'o2m pager should be hidden'); // click to create a subrecord form.$('tbody td.o_field_x2many_list_row_add a').click(); $('.modal input').val('new record').trigger('input'); $('.modal .modal-footer button:eq(0)').click(); // save and close assert.ok(!form.$('.o_x2m_control_panel .o_cp_pager div').is(':visible'), 'o2m pager should be hidden'); form.destroy(); }); QUnit.test('one2many list with a many2one', function (assert) { assert.expect(5); this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; this.data.partner.onchanges.p = function (obj) { obj.p = [ [5], // delete all [1, 2, {product_id: [37, "xphone"]}], // update existing record [0, 0, {product_id: [41, "xpad"]}] ]; // }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, archs: { 'partner,false,form': '
', }, mockRPC: function (route, args) { if (args.method === 'onchange') { assert.deepEqual(args.args[1].p, [[4, 2, false], [0, args.args[1].p[1][1], {product_id: 41}]], "should trigger onchange with correct parameters"); } return this._super.apply(this, arguments); } }); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should have properly fetched the many2one nameget"); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 0, "should not display 'xpad' anywhere"); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); $('.modal .o_field_many2one input').click(); var $dropdown = $('.modal .o_field_many2one input').autocomplete('widget'); $dropdown.find('li:eq(1) a').mouseenter(); $dropdown.find('li:eq(1) a').click(); $('.modal .modal-footer button:eq(0)').click(); // save and close assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' on a td"); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should still display xphone"); form.destroy(); }); QUnit.test('one2many list with inline form view', function (assert) { assert.expect(5); this.data.partner.records[0].p = []; var rpcCount = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + // don't remove this, it is // useful to make sure the foo fieldwidget // does not crash because the foo field // is not in the form view '' + '' + '', res_id: 1, mockRPC: function (route, args) { rpcCount++; if (args.method === 'write') { assert.deepEqual(args.args[1].p, [[0, args.args[1].p[0][1], { foo: false, int_field: 123, product_id: 41, }]]); } return this._super(route, args); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); // write in the many2one field, value = 37 (xphone) $('.modal .o_field_many2one input').click(); var $dropdown = $('.modal .o_field_many2one input').autocomplete('widget'); $dropdown.find('li:eq(0) a').mouseenter(); $dropdown.find('li:eq(0) a').click(); // write in the integer field $('.modal .modal-body input.o_field_widget').val('123').trigger('input'); // save and close $('.modal .modal-footer button:eq(0)').click(); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should display 'xphone' in a td"); // reopen the record in form view form.$('tbody td:contains(xphone)').click(); assert.strictEqual($('.modal .modal-body input').val(), "xphone", "should display 'xphone' in an input"); $('.modal .modal-body input.o_field_widget').val('456').trigger('input'); // discard $('.modal .modal-footer span:contains(Discard)').click(); // reopen the record in form view form.$('tbody td:contains(xphone)').click(); assert.strictEqual($('.modal .modal-body input.o_field_widget').val(), "123", "should display 123 (previous change has been discarded)"); // write in the many2one field, value = 41 (xpad) $('.modal .o_field_many2one input').click(); $dropdown = $('.modal .o_field_many2one input').autocomplete('widget'); $dropdown.find('li:eq(1) a').mouseenter(); $dropdown.find('li:eq(1) a').click(); // save and close $('.modal .modal-footer button:eq(0)').click(); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' in a td"); // save the record form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many list with inline form view with context with parent key', function (assert) { assert.expect(2); this.data.partner.records[0].p = [2]; this.data.partner.records[0].product_id = 41; this.data.partner.records[1].product_id = 37; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.strictEqual(args.kwargs.context.partner_foo, "yop", "should have correctly evaluated parent foo field"); assert.strictEqual(args.kwargs.context.lalala, 41, "should have correctly evaluated parent product_id field"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); // open a modal form.$('tr.o_data_row:eq(0) td:contains(xphone)').click(); // write in the many2one field $('.modal .o_field_many2one input').click(); form.destroy(); }); QUnit.test('value of invisible x2many fields is correctly evaluated in context', function (assert) { assert.expect(1); this.data.partner.records[0].timmy = [12]; this.data.partner.records[0].p = [2, 3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.deepEqual( args.kwargs.context, { p: [[4, 2, false], [4, 3, false]], timmy: [[6, false, [12]]], }, 'values of x2manys should have been correctly evaluated in context'); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_widget[name=product_id] input').click(); form.destroy(); }); QUnit.test('one2many list, editable, with many2one and with context with parent key', function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'name_search') { assert.strictEqual(args.kwargs.context.partner_foo, "yop", "should have correctly evaluated parent foo field"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tr.o_data_row:eq(0) td:contains(xphone)').click(); // trigger a name search form.$('table td input').click(); form.destroy(); }); QUnit.test('one2many list, editable, with a date in the context', function (assert) { assert.expect(1); this.data.partner.records[0].p = [2]; this.data.partner.records[1].product_id = 37; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 2, mockRPC: function (route, args) { if (args.method === 'default_get') { assert.strictEqual(args.kwargs.context.date, '2017-01-25', "should have properly evaluated date key in context"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('one2many field with context', function (assert) { assert.expect(2); var counter = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, mockRPC: function (route, args) { if (args.method === 'default_get') { var expected = counter === 0 ? [[4, 2, false]] : [[4, 2, false], [0, args.kwargs.context.turtles[1][1], {turtle_foo: 'hammer'}]]; assert.deepEqual(args.kwargs.context.turtles, expected, "should have properly evaluated turtles key in context"); counter++; } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); form.$('input[name="turtle_foo"]').val('hammer').trigger('input'); form.$('.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('one2many list edition, some basic functionality', function (assert) { assert.expect(3); this.data.partner.fields.foo.default = false; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); assert.strictEqual(form.$('td input.o_field_widget').length, 1, "should have created a row in edit mode"); form.$('td input.o_field_widget').val('a').trigger('input'); assert.strictEqual(form.$('td input.o_field_widget').length, 1, "should not have unselected the row after edition"); form.$('td input.o_field_widget').val('abc').trigger('input'); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('td:contains(abc)').length, 1, "should have a row with the correct value"); form.destroy(); }); QUnit.test('one2many list, the context is properly evaluated and sent', function (assert) { assert.expect(2); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'default_get') { var context = args.kwargs.context; assert.strictEqual(context.hello, "world"); assert.strictEqual(context.abc, 10); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('one2many with many2many widget: create', function (assert) { assert.expect(10); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', archs: { 'turtle,false,list': '', 'turtle,false,search': '', }, session: {}, res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/turtle/create') { assert.ok(args.args, "should write on the turtle record"); } if (route === '/web/dataset/call_kw/partner/write') { assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1"); assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual($('.modal .o_data_row').length, 2, "sould have 2 records in the select view (the last one is not displayed because it is already selected)"); $('.modal .o_data_row:first .o_list_record_selector input').click(); $('.modal .o_select_button').click(); $('.o_form_button_save').click(); form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); assert.strictEqual($('.modal .o_data_row').length, 1, "sould have 1 record in the select view"); $('.modal-footer button:eq(1)').click(); $('.modal input.o_field_widget[name="turtle_foo"]').val('tototo').trigger('input'); $('.modal input.o_field_widget[name="turtle_int"]').val(50).trigger('input'); var $many2one = $('.modal [name="product_id"] input').click(); var $dropdown = $many2one.autocomplete('widget'); $dropdown.find('li:first a').mouseenter(); $dropdown.find('li:first a').click(); $('.modal-footer button:contains(&):first').click(); assert.strictEqual($('.modal').length, 0, "sould close the modals"); assert.strictEqual(form.$('.o_data_row').length, 3, "sould have 3 records in one2many list"); assert.strictEqual(form.$('.o_data_row').text(), "blip1.59yop1.50tototo1.550xphone", "should display the record values in one2many list"); $('.o_form_button_save').click(); form.destroy(); }); QUnit.test('one2many with many2many widget: edition', function (assert) { assert.expect(7); var form = createView({ View: FormView, model: 'partner', data: this.data, arch:'
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '', archs: { 'turtle,false,list': '', 'turtle,false,search': '', }, session: {}, res_id: 1, mockRPC: function (route, args) { if (route === '/web/dataset/call_kw/turtle/write') { assert.strictEqual(args.args[0].length, 1, "should write on the turtle record"); assert.deepEqual(args.args[1], {"product_id":37}, "should write only the product_id on the turtle record"); } if (route === '/web/dataset/call_kw/partner/write') { assert.strictEqual(args.args[0][0], 1, "should write on the partner record 1"); assert.strictEqual(args.args[1].turtles[0][0], 6, "should send only a 'replace with' command"); } return this._super.apply(this, arguments); }, }); form.$('.o_data_row:first').click(); assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: one2many turtle field', "modal should use the python field string as title"); $('.modal .o_form_button_cancel').click(); form.$buttons.find('.o_form_button_edit').click(); // edit the first one2many record form.$('.o_data_row:first').click(); var $many2one = $('.modal [name="product_id"] input').click(); var $dropdown = $many2one.autocomplete('widget'); $dropdown.find('li:first a').mouseenter(); $dropdown.find('li:first a').click(); $('.modal-footer button:first').click(); $('.o_form_button_save').click(); // don't save anything because the one2many does not change // add a one2many record form.$buttons.find('.o_form_button_edit').click(); form.$('.o_field_x2many_list_row_add a').click(); $('.modal .o_data_row:first .o_list_record_selector input').click(); $('.modal .o_select_button').click(); // edit the second one2many record form.$('.o_data_row:eq(1)').click(); $many2one = $('.modal [name="product_id"] input').click(); $dropdown = $many2one.autocomplete('widget'); $dropdown.find('li:first a').mouseenter(); $dropdown.find('li:first a').click(); $('.modal-footer button:first').click(); $('.o_form_button_save').click(); form.destroy(); }); QUnit.test('new record, the context is properly evaluated and sent', function (assert) { assert.expect(2); this.data.partner.fields.int_field.default = 17; var n = 0; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (route, args) { if (args.method === 'default_get') { n++; if (n === 2) { var context = args.kwargs.context; assert.strictEqual(context.hello, "world"); assert.strictEqual(context.abc, 17); } } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc', function (assert) { assert.expect(1); this.data.partner.onchanges = {bar: function () {}}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', res_id: 1, mockRPC: function (route, args) { if (args.method === 'onchange') { var fieldValues = args.args[1]; assert.strictEqual(fieldValues.trululu.foo, "yop", "should have properly sent the parent foo value"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc, new record', function (assert) { assert.expect(6); this.data.turtle.onchanges = {turtle_bar: function () {}}; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '', mockRPC: function (route, args) { assert.step(args.method); if (args.method === 'onchange' && args.model === 'turtle') { var fieldValues = args.args[1]; assert.strictEqual(fieldValues.turtle_trululu.foo, "My little Foo Value", "should have properly sent the parent foo value"); } return this._super.apply(this, arguments); }, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); assert.verifySteps(['default_get', 'onchange', 'default_get', 'onchange']); form.destroy(); }); QUnit.test('id in one2many obtained in onchange is properly set', function (assert) { assert.expect(1); this.data.partner.onchanges.turtles = function (obj) { obj.turtles = [ [5], [1, 3, {turtle_foo: "kawa"}] ]; }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', }); assert.strictEqual(form.$('tr.o_data_row').text(), '3kawa', "should have properly displayed id and foo field"); form.destroy(); }); QUnit.test('id field in one2many in a new record', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '
', mockRPC: function (route, args) { if (args.method === 'create') { var virtualID = args.args[0].turtles[0][1]; assert.deepEqual(args.args[0].turtles, [[0, virtualID, {turtle_foo: "cat"}]], 'should send proper commands'); } return this._super.apply(this, arguments); }, }); form.$('td.o_field_x2many_list_row_add a').click(); form.$('td input[name="turtle_foo"]').val('cat').trigger('input'); form.$buttons.find('.o_form_button_save').click(); form.destroy(); }); QUnit.test('sub form view with a required field', function (assert) { assert.expect(2); this.data.partner.fields.foo.required = true; this.data.partner.fields.foo.default = null; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '
' + '', res_id: 1, }); form.$buttons.find('.o_form_button_edit').click(); form.$('tbody td.o_field_x2many_list_row_add a').click(); $('.modal-footer button.btn-primary').first().click(); assert.strictEqual($('.modal').length, 1, "should still have an open modal"); assert.strictEqual($('.modal tbody label.o_field_invalid').length, 1, "should have displayed invalid fields"); form.destroy(); }); QUnit.test('one2many list with action button', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '