odoo.define('web.field_one_to_many_tests', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); var concurrency = require('web.concurrency'); var FormView = require('web.FormView'); var ListRenderer = require('web.ListRenderer'); var relationalFields = require('web.relational_fields'); var testUtils = require('web.test_utils'); 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('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.dom.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', }, }); testUtils.dom.click(form.$('.o_field_x2many_list .o_field_x2many_list_row_add a')); 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); }, }); testUtils.dom.click(form.$('.o_field_x2many_list[name=turtles] .o_field_x2many_list_row_add a')); testUtils.fields.many2one.clickOpenDropdown('parent_id'); testUtils.fields.many2one.clickHighlightedItem('parent_id'); var $modal = $('.modal-content'); testUtils.dom.click($modal.eq(1).find('.modal-footer .btn-primary').eq(0)); testUtils.dom.click($modal.eq(0).find('.modal-footer .btn-primary').eq(1)); assert.equal(form.$('.o_data_row').length, 1, 'The main record should have the new record in its o2m'); $modal = $('.modal-content'); testUtils.dom.click($modal.find('.o_field_many2one input')); 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); } }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); var $targetInput = $('.o_selected_row .o_input[name=foo]'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should have the focus'); // Simulating hitting the 'f' key twice testUtils.fields.editInput($targetInput, 'f'); testUtils.fields.editInput($targetInput, $targetInput.val() + 'f'); assert.equal($targetInput[0], document.activeElement, 'The first input of the line should still have the focus'); // Simulating a TAB key $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'); testUtils.fields.editInput($secondTarget, 9); testUtils.fields.editInput($secondTarget, $secondTarget.val() + 9); testUtils.form.clickSave(form); 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, intercepts: { load_filters: function (event) { throw new Error('Should not load filters'); }, }, }); assert.containsNone(form, 'td.o_list_record_selector', "embedded one2many should not have a selector"); assert.ok(!form.$('.o_field_x2many_list_row_add').length, "embedded one2many should not be editable"); assert.ok(!form.$('td.o_list_record_remove').length, "embedded one2many records should not have a remove icon"); testUtils.form.clickEdit(form); assert.ok(form.$('.o_field_x2many_list_row_add').length, "embedded one2many should now be editable"); assert.hasAttrValue(form.$('.o_field_x2many_list_row_add'), 'colspan', "2", "should have colspan 2 (one for field foo, one for being below remove icon)"); assert.ok(form.$('td.o_list_record_remove').length, "embedded one2many records should have a remove icon"); form.destroy(); }); QUnit.test('transferring class attributes in one2many sub fields', function (assert) { assert.expect(3); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); assert.containsOnce(form, 'td.hey', 'should have a td with the desired class'); testUtils.form.clickEdit(form); assert.containsOnce(form, 'td.hey', 'should have a td with the desired class'); testUtils.dom.click(form.$('td.o_data_cell')); assert.containsOnce(form, 'input[name="turtle_foo"].hey', 'should have an input with the desired class'); form.destroy(); }); QUnit.test('one2many with date and datetime', 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.containsN(form, '.o_data_row', 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', 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' && args.method === 'read') { assert.deepEqual(args.args[0], [1, 2], 'should only load first 2 records'); } return this._super.apply(this, arguments); }, }); assert.containsN(form, '.o_data_row', 2, 'should display 2 data rows'); form.destroy(); }); QUnit.test('one2many with default_order on view not inline', 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: '
' + '' + '' + '' + '' + '' + '' + '' + '
', archs: { 'turtle,false,list': '' + '' + '' + '', }, res_id: 1, }); assert.strictEqual(form.$('.o_field_one2many .o_list_view .o_data_row').text(), "9blip21kawa0yop", "the default order should be correctly applied"); form.destroy(); }); QUnit.test('embedded one2many with widget', 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.containsOnce(form, 'span.o_row_handle', "should have 1 handles"); form.destroy(); }); QUnit.test('embedded one2many with handle widget', function (assert) { assert.expect(10); var nbConfirmChange = 0; testUtils.mock.patch(ListRenderer, { confirmChange: function () { nbConfirmChange++; return this._super.apply(this, arguments); }, }); this.data.partner.records[0].turtles = [1, 2, 3]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
', res_id: 1, }); testUtils.mock.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.turtles.data.turtle_int.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet"); // Drag and drop the second line in first position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(nbConfirmChange, 1, "should have confirmed changes only once"); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should have the 3 rows in the new order"); testUtils.form.clickSave(form); assert.deepEqual(_.map(this.data.turtle.records, function (turtle) { return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int'); }), [ { id: 1, turtle_foo: "yop", turtle_int: 1 }, { id: 2, turtle_foo: "blip", turtle_int: 0 }, { id: 3, turtle_foo: "kawa", turtle_int: 21 } ], "should have save the changed sequence"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the new order"); testUtils.mock.unpatch(ListRenderer); form.destroy(); }); QUnit.test('onchange for embedded one2many in a one2many with a second page', 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); } }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_data_cell').eq(1)); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); testUtils.fields.editSelect($cell, "hop"); testUtils.form.clickSave(form); 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"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_data_cell').eq(1)); var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]'); testUtils.fields.editSelect($cell, "hop"); testUtils.form.clickSave(form); 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, }); testUtils.form.clickEdit(form); // Drag and drop the second line in first position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(turtleOnchange, 2, "should trigger one onchange per line updated"); assert.strictEqual(partnerOnchange, 1, "should trigger only one onchange on the parent"); form.destroy(); }); QUnit.test('onchange for embedded one2many with handle widget using same sequence', 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); }, }); testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); // Drag and drop the second line in first position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa", "should still have the 3 rows in the correct order"); assert.strictEqual(turtleOnchange, 3, "should update all lines"); testUtils.form.clickSave(form); 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, }); testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa", "should have the 3 rows in the correct order"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); // Drag and drop the third line in second position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), { position: 'top' } ); assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); testUtils.form.clickSave(form); testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip", "should display to record in 'turtle_int' order"); form.destroy(); }); QUnit.test('onchange with modifiers for embedded one2many on the second page', 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, }); testUtils.form.clickEdit(form); 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"); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); // the domain fail if the widget does not use the allready loaded data. testUtils.form.clickDiscard(form); 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"); testUtils.dom.click($('.modal .modal-footer button:first')); 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"); testUtils.form.clickEdit(form); // Drag and drop the third line in second position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(2), form.$('.o_field_one2many tbody tr').eq(1), { position: 'top' } ); assert.equal(form.$('.o_field_one2many 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.dom.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)"); testUtils.form.clickDiscard(form); 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"); testUtils.dom.click($('.modal .modal-footer button:first')); 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, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1'); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2'); assert.containsN(form, '.o_data_row', 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line"); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line"); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line"); testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3'); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position after onchange"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange"); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 40, "should display 39 records and the create line"); assert.containsOnce(form, '.o_data_row:first .o_field_char', "should display the create line in first position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line"); form.destroy(); }); QUnit.test('onchange followed by edition on the second page (part 2)', 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, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first'), 'value 1'); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first'), 'value 2'); assert.containsN(form, '.o_data_row', 40, "should display 40 records"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line"); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 41, "should display 41 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position"); testUtils.fields.editInput(form.$('.o_data_row input:first'), 'value 3'); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, '.o_data_row', 42, "should display 42 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position"); form.destroy(); }); QUnit.test('onchange returning a command 6 for an x2many', 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.containsOnce(form, '.o_data_row', "there should be one record in the relation"); // change the value of foo to trigger the onchange testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); form.destroy(); }); QUnit.test('x2many fields inside x2manys are fetched after an onchange', 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.containsOnce(form, '.o_data_row', "there should be one record in the relation"); assert.strictEqual(form.$('.o_data_row .o_field_widget[name=partner_ids]').text().replace(/\s/g, ''), 'secondrecordaaa', "many2many_tags should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for the m2m field testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); assert.strictEqual(form.$('.o_data_row:first .o_field_widget[name=partner_ids]').text().trim(), 'first record', "many2many_tags should be correctly displayed"); form.destroy(); }); QUnit.test('reference fields inside x2manys are fetched after an onchange', 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.containsOnce(form, '.o_data_row', "there should be one record in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpad', "reference field should be correctly displayed"); // change the value of foo to trigger the onchange checkRPC = true; // enable flag to check read RPC for reference field testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'some value'); assert.containsN(form, '.o_data_row', 3, "there should be three records in the relation"); assert.strictEqual(form.$('.ref_field').text().trim(), 'xpadxphone', "reference fields should be correctly displayed"); form.destroy(); }); QUnit.test('onchange on one2many containing x2many in form view', 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.containsOnce(form, '.o_data_row', "the onchange should have created one record in the relation"); // open the created o2m record in a form view, and add a m2m subrecord // in its relation testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 0, "there should be no record in the one2many in the dialog"); // add a many2many subrecord testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); // select a many2many subrecord testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first')); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); assert.notOk($('.modal .o_x2m_control_panel .o_cp_pager div').is(':visible'), 'm2m pager should be hidden'); // click on 'Save & Close' testUtils.dom.click($('.modal-footer .btn-primary:first')); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record, and another m2m subrecord in its relation, but // discard the changes testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); // add another m2m subrecord testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal').length, 2, "should have opened a second dialog"); testUtils.dom.click($('.modal:nth(1) .o_list_view .o_data_cell:first')); assert.strictEqual($('.modal').length, 1, "second dialog should be closed"); assert.strictEqual($('.modal .o_data_row').length, 2, "there should be two records in the one2many in the dialog"); // click on 'Discard' testUtils.dom.clickFirst($('.modal-footer .btn-secondary')); assert.strictEqual($('.modal').length, 0, "dialog should be closed"); // reopen o2m record to check that second changes have properly been discarded testUtils.dom.click(form.$('.o_data_row')); assert.strictEqual($('.modal').length, 1, "should have opened a dialog"); assert.strictEqual($('.modal .o_data_row').length, 1, "there should be one record in the one2many in the dialog"); form.destroy(); }); QUnit.test('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.mock.intercept(form, "field_changed", function (event) { assert.step(form.model.get(event.data.changes.turtles.id).res_id); }, true); testUtils.form.clickEdit(form); 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.dom.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.mock.intercept(form, "field_changed", function (event) { assert.step(event.data.changes.p.data.int_field.toString()); }, true); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should have the 3 rows in the correct order"); testUtils.form.clickEdit(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop", "should still have the 3 rows in the correct order"); // Drag and drop the second line in first position testUtils.dom.dragAndDrop( form.$('.ui-sortable-handle').eq(1), form.$('tbody tr').first(), { position: 'top' } ); assert.verifySteps(["0", "1"], "sequences values should be incremental starting from the previous minimum one"); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should have the 3 rows in the new order"); testUtils.dom.click(form.$('tbody tr:first td:first')); assert.strictEqual(form.$('tbody tr:first td.o_data_cell:not(.o_handle_cell) input').val(), "blip", "should edit the correct row"); testUtils.form.clickSave(form); assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop", "should still have the 3 rows in the new order"); form.destroy(); }); QUnit.test('one2many field when using the pager', 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 testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(count, 7, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); // move to record 2 again and check that everything is correctly updated 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'); testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(count, 12, 'one RPC should have been done'); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 2, 'one2many kanban should contain 2 cards for record 1 at page 2'); form.destroy(); }); QUnit.test('edition of one2many field with pager', 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; testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o-kanban-button-new')); testUtils.fields.editInput($('.modal input'), 'new record'); testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); // checks assert.strictEqual(readIDs, undefined, "should not have read any record"); assert.strictEqual(form.$('span:contains(new record)').length, 0, "new record should be on page 2"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 46', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record:first span:contains(new record)').length, 0, 'new record should not be on page 1'); // save testUtils.form.clickSave(form); // delete a record on page one checkRead = true; testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 10)').length, 1, 'first record should be the one with id 10 (next checks rely on that)'); testUtils.dom.click(form.$('.delete_icon:first')); // checks assert.deepEqual(readIDs, [50], "should have read a record (to display 40 records on page 1)"); assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 40, 'there should be 40 records on page 1'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '1-40 / 45', "pager range should be correct"); // save testUtils.form.clickSave(form); // add and delete records in both pages testUtils.form.clickEdit(form); checkRead = true; readIDs = undefined; // add and delete a record in page 1 testUtils.dom.click(form.$('.o-kanban-button-new')); testUtils.fields.editInput($('.modal input'), 'new record page 1'); testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 11)').length, 1, 'first record should be the one with id 11 (next checks rely on that)'); testUtils.dom.click(form.$('.delete_icon:first')); assert.deepEqual(readIDs, [51], "should have read a record (to display 40 records on page 1)"); // add and delete a record in page 2 testUtils.dom.click(form.$('.o_x2m_control_panel .o_pager_next')); assert.strictEqual(form.$('.o_kanban_record:first span:contains(relational record 52)').length, 1, 'first record should be the one with id 52 (next checks rely on that)'); checkRead = true; readIDs = undefined; testUtils.dom.click(form.$('.delete_icon:first')); testUtils.dom.click(form.$('.o-kanban-button-new')); testUtils.fields.editInput($('.modal input'), 'new record page 2'); testUtils.dom.click($('.modal .modal-footer .btn-primary:first')); assert.strictEqual(readIDs, undefined, "should not have read any record"); // checks assert.strictEqual(form.$('.o_kanban_record:not(".o_kanban_ghost")').length, 5, 'there should be 5 records on page 2'); assert.strictEqual(form.$('.o_x2m_control_panel .o_pager_counter').text().trim(), '41-45 / 45', "pager range should be correct"); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 1)').length, 1, 'new records should be on page 2'); assert.strictEqual(form.$('.o_kanban_record span:contains(new record page 2)').length, 1, 'new records should be on page 2'); // save testUtils.form.clickSave(form); 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, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput($('input[name="turtle_int"]'), '5'); testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '5', 'should display 5 in the foo field'); testUtils.dom.click(form.$('tbody tr:eq(1) td.o_data_cell')); testUtils.fields.editInput($('input[name="turtle_int"]'), '3'); testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual(form.$('tbody tr:eq(1) td.o_data_cell').text(), '3', 'should now display 3 in the foo field'); form.destroy(); }); QUnit.test('sorting one2many fields', 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"; }; testUtils.dom.click(form.$('table thead th:contains(Foo)')); assert.strictEqual(rpcCount, 0, 'sort should be in memory, no extra RPCs should have been done'); assert.ok(form.$('table tbody tr:eq(2) td:contains(xyz)').length, "the 3rd record is the one with 'xyz' value"); testUtils.dom.click(form.$('table thead th:contains(Foo)')); assert.ok(form.$('table tbody tr:eq(2) td:contains(abc)').length, "the 3rd record is the one with 'abc' value"); form.destroy(); }); QUnit.test('one2many list field edition', 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'"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row', "first row of o2m should be in edition"); testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); assert.hasClass(form.$('.o_field_one2many tbody td').first().parent(),'o_selected_row', "first row of o2m should still be in edition"); // // leave o2m edition testUtils.dom.click(form.$el); assert.doesNotHaveClass(form.$('.o_field_one2many tbody td').first().parent(), 'o_selected_row', "first row of o2m should be readonly again"); // discard changes testUtils.form.clickDiscard(form); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', "changes shouldn't have been discarded yet, waiting for user confirmation"); testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'relational record 1', "display name of first record in o2m list should be 'relational record 1'"); // edit again and save testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); testUtils.dom.click(form.$el); testUtils.form.clickSave(form); // FIXME: this next test doesn't pass as the save of updates of // relational data is temporarily disabled // assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'new value', // "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many list: create action disabled', 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'); testUtils.form.clickEdit(form); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should not be available in readonly'); form.destroy(); }); QUnit.test('one2many list: unlink two records', function (assert) { assert.expect(9); this.data.partner.records[0].p = [1, 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, 3, 'should have generated three 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] === 1, 'should have generated the command 3 (UNLINK) with id 1'); assert.ok(commands[2][0] === 3 && commands[2][1] === 2, 'should have generated the command 3 (UNLINK) with id 2'); } return this._super.apply(this, arguments); }, archs: { 'partner,false,form': '
', }, }); testUtils.form.clickEdit(form); assert.containsN(form, 'td.o_list_record_remove button', 3, "should have 3 remove buttons"); assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-times', "should have X icons to remove (unlink) records"); testUtils.dom.click(form.$('td.o_list_record_remove button').first()); assert.containsN(form, 'td.o_list_record_remove button', 2, "should have 2 remove buttons (a record is supposed to have been unlinked)"); testUtils.dom.click(form.$('tr.o_data_row').first()); assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, 'there should be a modal having Remove Button'); testUtils.dom.click($('.modal .modal-footer .o_btn_remove')); assert.containsOnce(form, 'td.o_list_record_remove button', "should have 1 delete button (another record is supposed to have been unlinked)"); // save and check that the correct command has been generated testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many list: deleting two records', function (assert) { assert.expect(9); this.data.partner.records[0].p = [1, 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, 3, 'should have generated three 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] === 1, 'should have generated the command 2 (DELETE) with id 1'); assert.ok(commands[2][0] === 2 && commands[2][1] === 2, 'should have generated the command 2 (DELETE) with id 2'); } return this._super.apply(this, arguments); }, archs: { 'partner,false,form': '
', }, }); testUtils.form.clickEdit(form); assert.containsN(form, 'td.o_list_record_remove button', 3, "should have 3 remove buttons"); assert.hasClass(form.$('td.o_list_record_remove button').first(),'fa fa-trash-o', "should have trash bin icons to remove (delete) records"); testUtils.dom.click(form.$('td.o_list_record_remove button').first()); assert.containsN(form, 'td.o_list_record_remove button', 2, "should have 2 remove buttons (a record is supposed to have been deleted)"); testUtils.dom.click(form.$('tr.o_data_row').first()); assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, 'there should be a modal having Remove Button'); testUtils.dom.click($('.modal .modal-footer .o_btn_remove')); assert.containsOnce(form, 'td.o_list_record_remove button', "should have 1 remove button (another record is supposed to have been deleted)"); // save and check that the correct command has been generated testUtils.form.clickSave(form); // FIXME: it would be nice to test that the view is re-rendered correctly, // but as the relational data isn't re-fetched, the rendering is ok even // if the changes haven't been saved form.destroy(); }); QUnit.test('one2many kanban: edition', function (assert) { assert.expect(17); 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'); testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 record'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'second record', 'display_name of subrecord should be the one in DB'); assert.strictEqual(form.$('.o_kanban_record span:nth(1)').text(), 'Red', 'color of subrecord should be the one in DB'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); assert.ok(form.$('.o_field_one2many .o-kanban-button-new').length, '"Create" button should be visible in edit'); assert.hasClass(form.$('.o_field_one2many .o-kanban-button-new'),'btn-secondary', "'Create' button should have className 'btn-secondary'"); assert.strictEqual(form.$('.o_field_one2many .o-kanban-button-new').text().trim(), "Add", 'Create button should have "Add" label'); // edit existing subrecord testUtils.dom.click(form.$('.oe_kanban_global_click')); testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new name'); testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new name', 'value of subrecord should have been updated'); // create a new subrecord testUtils.dom.click(form.$('.o-kanban-button-new')); testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 1'); testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_kanban_record:nth(1) span').text(), 'new subrecord 1', 'value of newly created subrecord should be "new subrecord 1"'); // create two new subrecords testUtils.dom.click(form.$('.o-kanban-button-new')); testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 2'); testUtils.dom.click($('.modal .modal-footer .btn-primary:nth(1)')); testUtils.fields.editInput($('.modal .o_form_view input').first(), 'new subrecord 3'); testUtils.dom.clickFirst($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 4, 'should contain 4 records'); // delete subrecords testUtils.dom.click(form.$('.oe_kanban_global_click').first()); assert.strictEqual($('.modal .modal-footer .o_btn_remove').length, 1, 'There should be a modal having Remove Button'); testUtils.dom.click($('.modal .modal-footer .o_btn_remove')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 3, 'should contain 3 records'); testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()')); testUtils.dom.click(form.$('.o_kanban_view .delete_icon:first()')); assert.strictEqual(form.$('.o_kanban_record:not(.o_kanban_ghost)').length, 1, 'should contain 1 records'); assert.strictEqual(form.$('.o_kanban_record span:first').text(), 'new subrecord 3', 'the remaining subrecord should be "new subrecord 3"'); form.destroy(); }); QUnit.test('one2many kanban (editable): properly handle create_text node option', function (assert) { assert.expect(1); var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '
' + '' + '
' + '
' + '
' + '
' + '
' + '
', res_id: 1, }); testUtils.form.clickEdit(form); assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o-kanban-button-new').text().trim(), "Add turtle", "In O2M Kanban, Add button should have 'Add turtle' label"); form.destroy(); }); QUnit.test('one2many kanban: create action disabled', 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'); testUtils.form.clickEdit(form); assert.ok(!form.$('.o-kanban-button-new').length, '"Add" button should not be available in edit'); assert.ok(form.$('.o_kanban_view .delete_icon').length, 'delete icon should be visible in edit'); form.destroy(); }); QUnit.test('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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$el); assert.strictEqual(form.$('.o_field_widget[name=turtles] .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_remove').length, 'remove icon should not be visible in readonly'); assert.ok(!form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not be visible in readonly'); testUtils.form.clickEdit(form); assert.containsN(form, '.o_list_view td.o_list_number', 2, 'should contain 2 records'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'second record', 'display_name of first subrecord should be the one in DB'); assert.ok(form.$('.o_list_record_remove').length, 'remove icon should be visible in edit'); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" should not visible in edit'); // edit existing subrecord testUtils.dom.click(form.$('.o_list_view tbody tr:first() td:eq(1)')); testUtils.fields.editInput($('.modal .o_form_view input'), 'new name'); testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'value of subrecord should have been updated'); assert.strictEqual(nbWrite, 0, "should not have write anything in DB"); // create new subrecords // TODO when 'Add an item' will be implemented // remove subrecords testUtils.dom.click(form.$('.o_list_record_remove:nth(1)')); assert.containsOnce(form, '.o_list_view td.o_list_number', 'should contain 1 subrecord'); assert.strictEqual(form.$('.o_list_view tbody td:first()').text(), 'new name', 'the remaining subrecord should be "new name"'); testUtils.form.clickSave(form); // save the record assert.strictEqual(nbWrite, 1, "should have write the changes in DB"); form.destroy(); }); QUnit.test('one2many list (editable): edition', 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'); testUtils.dom.click(form.$('.o_list_view tbody td:first()')); assert.ok($('.modal .o_form_readonly').length, 'in readonly, clicking on a subrecord should open it in readonly in a dialog'); testUtils.dom.click($('.modal .o_form_button_cancel')); testUtils.form.clickEdit(form); assert.ok(form.$('.o_field_x2many_list_row_add').length, '"Add an item" link should be available in edit'); // edit existing subrecord testUtils.dom.click(form.$('.o_list_view tbody td:first()')); assert.strictEqual($('.modal').length, 0, 'in edit, clicking on a subrecord should not open a dialog'); assert.hasClass(form.$('.o_list_view tbody tr:first()'),'o_selected_row', 'first row should be in edition'); testUtils.fields.editInput(form.$('.o_list_view input:first()'), 'new name'); testUtils.dom.click(form.$('.o_list_view tbody tr:nth(1) td:first')); assert.doesNotHaveClass(form.$('.o_list_view tbody tr:first'), 'o_selected_row', 'first row should not be in edition anymore'); assert.strictEqual(form.$('.o_list_view tbody td:first').text(), 'new name', 'value of subrecord should have been updated'); // create new subrecords // TODO when 'Add an item' will be implemented form.destroy(); }); QUnit.test('one2many list (editable): edition, part 2', 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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'kartoffel'); // click again on Add an item testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.containsOnce(form, '.o_selected_row > td input', "should have one other new td"); assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows"); // enter another value and save testUtils.fields.editInput(form.$('.o_selected_row > td input'), 'gemuse'); testUtils.form.clickSave(form); assert.containsN(form, 'tr.o_data_row', 2, "should have 2 data rows"); assert.strictEqual(form.$('td:contains(kartoffel)').length, 1, "should have one td with the new value"); assert.strictEqual(form.$('td:contains(gemuse)').length, 1, "should have one td with the new value"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 3', 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.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsN(form, 'tr.o_data_row', 3, "should have 3 data rows"); // cancel the edition testUtils.form.clickDiscard(form); testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); form.destroy(); }); QUnit.test('one2many list (editable): edition, part 4', 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.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual(form.$('textarea').val(), "", "field turtle_description should be empty"); // add a value in the turtle_trululu field to trigger an onchange testUtils.fields.many2one.clickOpenDropdown('turtle_trululu'); testUtils.fields.many2one.clickHighlightedItem('turtle_trululu'); assert.strictEqual(form.$('textarea').val(), "Some Description", "field turtle_description should be set to the result of the onchange"); form.destroy(); }); QUnit.test('one2many list (editable): discarding required empty data', 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.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$('label.o_form_label').first()); assert.containsNone(form, 'tr.o_data_row', "should still have 0 data rows"); // click on Add an item again, then click on save testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.form.clickSave(form); assert.containsNone(form, 'tr.o_data_row', "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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // the record currently being added should not count in the pager assert.isNotVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager')); form.destroy(); }); QUnit.test('editable one2many list, adding line, then discarding', function (assert) { assert.expect(2); this.data.turtle.records.push({ id: 4, turtle_foo: 'stephen hawking' }); this.data.partner.records[0].turtles = [1, 2, 3, 4]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', res_id: 1, }); // add a record, then discard testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.form.clickDiscard(form); // confirm the discard operation testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.isVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4', "pager should display correct values"); 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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // go on next page. The new record is not valid and should be discarded testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.containsOnce(form, 'tr.o_data_row'); form.destroy(); }); QUnit.test('editable one2many list, required field, pager and confirm discard', 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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('input[name="turtle_int"]'), 4321); // go to next page. The new record is not valid, but dirty. we should // see a confirm dialog testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4', "pager should still display the correct total"); // click on cancel testUtils.dom.click($('.modal .modal-footer .btn-secondary')); assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_cp_pager').text().trim(), '1-3 / 4', "pager should again display the correct total"); assert.containsOnce(form, '.o_field_one2many input.o_field_invalid', "there should be an invalid input in the one2many"); form.destroy(); }); QUnit.test('editable one2many list, adding, discarding, and pager', 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) testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // go on next page testUtils.dom.click(form.$('.o_field_widget[name=turtles] .o_pager_next')); // discard testUtils.form.clickDiscard(form); testUtils.dom.click($('.modal .modal-footer .btn-primary')); assert.containsOnce(form, 'tr.o_data_row'); assert.isNotVisible(form.$('.o_field_widget[name=turtles] .o_cp_pager')); 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.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "should have 1 data rows"); // adding a value in the non required field, so it is dirty, but with // a missing required field testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '12345'); // click elsewhere, testUtils.dom.click(form.$('label.o_form_label')); assert.strictEqual($('.modal').length, 1, 'a confirmation model should be opened'); // click on cancel, the line should still be selected testUtils.dom.click($('.modal .modal-footer button.btn-secondary')); assert.containsOnce(form, 'tr.o_data_row.o_selected_row', "should still have 1 selected data row"); // click elsewhere, and click on ok (on the confirmation dialog) testUtils.dom.click(form.$('label.o_form_label')); testUtils.dom.click($('.modal .modal-footer button.btn-primary')); assert.containsNone(form, 'tr.o_data_row', "should have 0 data rows (invalid line has been discarded"); form.destroy(); }); QUnit.test('pressing enter in a o2m with a required empty field', 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 testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); form.$('input[name="turtle_foo"]').trigger($.Event('keydown', { which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER, })); assert.hasClass(form.$('input[name="turtle_foo"]'),'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.containsNone(form, 'tr.o_data_row', "should have 0 data rows"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // input some text in required turtle_foo field testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'aubergine'); assert.strictEqual(form.$('input[name="turtle_int"]').val(), "9", "onchange should have been triggered"); // save and check everything is fine testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_data_row td:contains(aubergine)').length, 1, "should have one row with turtle_foo value"); assert.strictEqual(form.$('.o_data_row td:contains(9)').length, 1, "should have one row with turtle_int value"); assert.verifySteps(['read', '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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "there should be one data row"); form.$('input[name="turtle_foo"]').trigger({ type: 'keydown', which: $.ui.keyCode.ESCAPE }); assert.containsNone(form, 'tr.o_data_row', "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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row', "there should be one data row"); form.$('input[name="turtle_foo"]').trigger({ type: 'keydown', which: $.ui.keyCode.ESCAPE }); assert.containsNone(form, 'tr.o_data_row', "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', }, }); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click($('.modal .o_field_x2many_list_row_add a')); assert.strictEqual($('.modal .o_data_row.o_selected_row').length, 1, "there should be a row in edition in the dialog"); // 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', }, }); testUtils.dom.click(form.$('.o_data_cell:first')); form.$('.o_field_widget[name="product_id"] input').val('').trigger('keyup'); assert.verifySteps(['read', 'read'], 'no onchange should be done as line is invalid'); testUtils.dom.click(form.$('.o_list_record_remove')); 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_one2many tbody td').first()); testUtils.fields.editInput(form.$('.o_field_one2many tbody td').first().find('input'), "new value"); testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_field_one2many tbody td').first().text(), 'from onchange', "display name of first record in o2m list should be 'new value'"); form.destroy(); }); QUnit.test('one2many, default_get and onchange (basic)', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('td:contains(9)')); testUtils.fields.editInput(form.$('td input[name="turtle_int"]'), "3"); // the 'change' event is triggered on the input when we focus somewhere // else, for example by clicking in the body. However, if we try to // programmatically click in the body, it does not trigger a change // event, so we simply trigger it directly instead. form.$('td input[name="turtle_int"]').trigger('change'); assert.verifySteps(['read', 'read', 'onchange']); form.destroy(); }); QUnit.test('one2many and onchange (with date)', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('td:contains(01/25/2017)')); testUtils.dom.click(form.$('.o_datepicker_input')); testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch').first()); testUtils.dom.click($('.bootstrap-datetimepicker-widget .picker-switch:eq(1)')); testUtils.dom.click($('.bootstrap-datetimepicker-widget .year:contains(2017)')); testUtils.dom.click($('.bootstrap-datetimepicker-widget .month').eq(1)); testUtils.dom.click($('.day:contains(22)')); testUtils.form.clickSave(form); 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.containsOnce(form, '.o_data_row', "o2m should contain one row"); // empty o2m by triggering the onchange testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange'); assert.containsNone(form, '.o_data_row', "rows of the o2m should have been deleted"); // add two new subrecords testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'x'); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'y'); assert.containsN(form, '.o_data_row', 2, "o2m should contain two rows"); // empty o2m by triggering the onchange testUtils.fields.editInput(form.$('.o_field_widget[name=foo]'), 'trigger onchange again'); assert.containsNone(form, '.o_data_row', "rows of the o2m should have been deleted"); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('.o_field_widget[name=display_name]'), 'z'); testUtils.form.clickSave(form); 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.containsOnce(form, '.o_data_row', "o2m should contain one row"); testUtils.dom.click(form.$('.o_field_one2many .o_list_view tbody tr:first td:first')); testUtils.fields.editInput(form.$('.o_field_one2many .o_list_view tbody tr:first input:first'), 'blurp'); testUtils.form.clickSave(form); form.destroy(); }); QUnit.test('one2many with CREATE onchanges correctly refreshed', function (assert) { assert.expect(5); var delta = 0; testUtils.mock.patch(AbstractField, { init: function () { delta++; this._super.apply(this, arguments); }, destroy: function () { delta--; this._super.apply(this, arguments); }, }); var deactiveOnchange = true; this.data.partner.records[0].turtles = []; this.data.partner.onchanges = { turtles: function (obj) { if (deactiveOnchange) { return; } // the onchange will either: // - create a second line if there is only one line // - edit the second line if there are two lines if (obj.turtles.length === 1) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, 0, { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } else if (obj.turtles.length === 2) { obj.turtles = [ [5], // delete all [0, obj.turtles[0][1], { display_name: "first", turtle_int: obj.turtles[0][2].turtle_int, }], [0, obj.turtles[1][1], { display_name: "second", turtle_int: -obj.turtles[0][2].turtle_int, }], ]; } }, }; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '' + '' + '', res_id: 1, viewOptions: { mode: 'edit', }, }); assert.containsNone(form, '.o_data_row', "o2m shouldn't contain any row"); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); // trigger the first onchange deactiveOnchange = false; testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '10'); // put the list back in non edit mode testUtils.dom.click(form.$('input[name="foo"]')); assert.strictEqual(form.$('.o_data_row').text(), "first10second-10", "should correctly refresh the records"); // trigger the second onchange testUtils.dom.click(form.$('.o_field_x2many_list tbody tr:first td:first')); testUtils.fields.editInput(form.$('input[name="turtle_int"]'), '20'); testUtils.dom.click(form.$('input[name="foo"]')); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records"); assert.containsN(form, '.o_field_widget', delta, "all (non visible) field widgets should have been destroyed"); testUtils.form.clickSave(form); assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", "should correctly refresh the records after save"); form.destroy(); testUtils.mock.unpatch(AbstractField); }); QUnit.test('editable one2many with sub widgets are rendered in readonly', function (assert) { assert.expect(2); var editableWidgets = 0; testUtils.mock.patch(AbstractField, { init: function () { this._super.apply(this, arguments); if (this.mode === 'edit') { editableWidgets++; } }, }); var form = 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"); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.strictEqual(editableWidgets, 3, "3 widgets currently in edit mode"); form.destroy(); testUtils.mock.unpatch(AbstractField); }); QUnit.test('one2many editable list with onchange keeps the order', 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"); testUtils.dom.click(form.$('.o_data_row:first .o_data_cell')); testUtils.fields.editInput(form.$('.o_selected_row .o_field_widget[name=display_name]'), 'new'); testUtils.dom.click(form.$el); assert.strictEqual(form.$('.o_data_cell').text(), 'newsecond recordaaa', "records should be display in the correct order"); form.destroy(); }); QUnit.test('one2many list (editable): readonly domain is evaluated', 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, }); testUtils.form.clickEdit(form); assert.hasClass(form.$('.o_list_view tbody tr:eq(0) td:first'),'o_readonly_modifier', "first record should have display_name in readonly mode"); assert.doesNotHaveClass(form.$('.o_list_view tbody tr:eq(1) td:first'), 'o_readonly_modifier', "second record should not have display_name in readonly mode"); form.destroy(); }); QUnit.test('pager of one2many field in new record', function (assert) { assert.expect(3); this.data.partner.records[0].p = []; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '' + '
', }); assert.ok(!form.$('.o_x2m_control_panel .o_cp_pager div').is(':visible'), 'o2m pager should be hidden'); // click to create a subrecord testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'tr.o_data_row'); 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"); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickItem('product_id', 'xpad'); testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' on a td"); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should still display xphone"); form.destroy(); }); QUnit.test('one2many list with inline form view', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); // write in the many2one field, value = 37 (xphone) testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickHighlightedItem('product_id'); // write in the integer field testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '123'); // save and close testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xphone)').length, 1, "should display 'xphone' in a td"); // reopen the record in form view testUtils.dom.click(form.$('tbody td:contains(xphone)')); assert.strictEqual($('.modal .modal-body input').val(), "xphone", "should display 'xphone' in an input"); testUtils.fields.editInput($('.modal .modal-body input.o_field_widget'), '456'); // discard testUtils.dom.click($('.modal .modal-footer span:contains(Discard)')); // reopen the record in form view testUtils.dom.click(form.$('tbody td:contains(xphone)')); assert.strictEqual($('.modal .modal-body input.o_field_widget').val(), "123", "should display 123 (previous change has been discarded)"); // write in the many2one field, value = 41 (xpad) testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickItem('product_id', 'xpad'); // save and close testUtils.dom.click($('.modal .modal-footer button:eq(0)')); assert.strictEqual(form.$('tbody td:contains(xpad)').length, 1, "should display 'xpad' in a td"); // save the record testUtils.form.clickSave(form); 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); }, }); testUtils.form.clickEdit(form); // open a modal testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)')); // write in the many2one field testUtils.dom.click($('.modal .o_field_many2one input')); 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_widget[name=product_id] input')); form.destroy(); }); QUnit.test('one2many list, editable, with many2one and with context with parent key', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tr.o_data_row:eq(0) td:contains(xphone)')); // trigger a name search testUtils.dom.click(form.$('table td input')); 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'hammer'); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); 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, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); assert.containsOnce(form, 'td input.o_field_widget', "should have created a row in edit mode"); testUtils.fields.editInput(form.$('td input.o_field_widget'), 'a'); assert.containsOnce(form, 'td input.o_field_widget', "should not have unselected the row after edition"); testUtils.fields.editInput(form.$('td input.o_field_widget'), 'abc'); testUtils.form.clickSave(form); assert.strictEqual(form.$('td:contains(abc)').length, 1, "should have a row with the correct value"); form.destroy(); }); QUnit.test('one2many list, the context is properly evaluated and sent', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); 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)"); testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input')); testUtils.dom.click($('.modal .o_select_button')); testUtils.dom.click($('.o_form_button_save')); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); assert.strictEqual($('.modal .o_data_row').length, 1, "sould have 1 record in the select view"); testUtils.dom.click($('.modal-footer button:eq(1)')); testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_foo"]'), 'tototo'); testUtils.fields.editInput($('.modal input.o_field_widget[name="turtle_int"]'), 50); testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickHighlightedItem('product_id'); testUtils.dom.click($('.modal-footer button:contains(&):first')); assert.strictEqual($('.modal').length, 0, "sould close the modals"); assert.containsN(form, '.o_data_row', 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"); testUtils.dom.click($('.o_form_button_save')); 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); }, }); testUtils.dom.click(form.$('.o_data_row:first')); assert.strictEqual($('.modal .modal-title').first().text().trim(), 'Open: one2many turtle field', "modal should use the python field string as title"); testUtils.dom.click($('.modal .o_form_button_cancel')); testUtils.form.clickEdit(form); // edit the first one2many record testUtils.dom.click(form.$('.o_data_row:first')); testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickHighlightedItem('product_id'); testUtils.dom.click($('.modal-footer button:first')); testUtils.dom.click($('.o_form_button_save')); // add a one2many record testUtils.form.clickEdit(form); testUtils.dom.click(form.$('.o_field_x2many_list_row_add a')); testUtils.dom.click($('.modal .o_data_row:first .o_list_record_selector input')); testUtils.dom.click($('.modal .o_select_button')); // edit the second one2many record testUtils.dom.click(form.$('.o_data_row:eq(1)')); testUtils.fields.many2one.clickOpenDropdown('product_id'); testUtils.fields.many2one.clickHighlightedItem('product_id'); testUtils.dom.click($('.modal-footer button:first')); testUtils.dom.click($('.o_form_button_save')); 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); }, }); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc', 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); }, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); form.destroy(); }); QUnit.test('parent data is properly sent on an onchange rpc, 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); }, }); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); 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); }, }); testUtils.dom.click(form.$('td.o_field_x2many_list_row_add a')); testUtils.fields.editInput(form.$('td input[name="turtle_foo"]'), 'cat'); testUtils.form.clickSave(form); 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, }); testUtils.form.clickEdit(form); testUtils.dom.click(form.$('tbody td.o_field_x2many_list_row_add a')); testUtils.dom.click($('.modal-footer button.btn-primary').first()); assert.strictEqual($('.modal').length, 1, "should still have an open modal"); assert.strictEqual($('.modal tbody label.o_field_invalid').length, 1, "should have displayed invalid fields"); form.destroy(); }); QUnit.test('one2many list with action button', function (assert) { assert.expect(4); this.data.partner.records[0].p = [2]; var form = createView({ View: FormView, model: 'partner', data: this.data, arch: '
' + '' + '' + '' + '' + '