',
+ res_id: 1,
+ session: {
+ currencies: _.indexBy(this.data.currency.records, 'id'),
+ },
+ });
+
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0-8.9',
+ 'The value should be displayed properly.');
+
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.strictEqual(form.$('input').val(), '-8.9',
+ 'The input should be rendered without the currency symbol.');
+ assert.strictEqual(form.$('input').parent().children().first().text(), '$',
+ 'The input should be preceded by a span containing the currency symbol.');
+
+ form.$('input').val('109.2458938598598').trigger('input');
+ assert.strictEqual(form.$('input').val(), '109.2458938598598',
+ 'The value should not be formated yet.');
+
+ form.$buttons.find('.o_form_button_save').click();
+ // Non-breaking space between the currency and the amount
+ assert.strictEqual(form.$('.o_field_widget').first().text(), '$\u00a0109.2',
+ 'The new value should be rounded properly.');
+
+ form.destroy();
+ });
+
QUnit.module('FieldEmail');
QUnit.test('email field in form view', function (assert) {
@@ -1472,6 +1518,40 @@ QUnit.module('basic_fields', {
session.get_file = oldGetFile;
});
+ QUnit.test('download_link widget in list view', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.filename = { string: "Filename", type: "char" };
+ this.data.partner.records[0].filename = "document.txt";
+
+ var form = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '
' +
+ '' +
+ '' +
+ '',
+ res_id: 1,
+ });
+
+ assert.ok(form.$('.o_data_cell').first().hasClass("o_download_link_cell"),
+ "this should be a download link cell");
+ assert.ok(form.$('.o_data_cell').first().find('a').eq(0),
+ "there should be a link in the cell");
+ assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('download'),
+ "document.txt",
+ "it should download a file named 'document.txt'");
+ assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).attr('href').trim(),
+ "data:application/octet-stream;base64,coucou==",
+ "it should have the appropriate link of a binary");
+ assert.strictEqual(form.$('.o_data_cell').first().find('a').eq(0).text(),
+ "Download",
+ "the text of the link should be 'Download'");
+
+ form.destroy();
+ });
+
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
diff --git a/addons/web/static/tests/fields/field_utils_tests.js b/addons/web/static/tests/fields/field_utils_tests.js
index 1f8c27e7192..e24571ea1d8 100644
--- a/addons/web/static/tests/fields/field_utils_tests.js
+++ b/addons/web/static/tests/fields/field_utils_tests.js
@@ -127,6 +127,15 @@ QUnit.test('format one2many', function(assert) {
assert.strictEqual(fieldUtils.format.one2many({data: [1, 2]}), '2 records');
});
+QUnit.test('format binary', function (assert) {
+ assert.expect(1);
+
+ // base64 estimated size (bytes) = value.length / 1.37 (http://en.wikipedia.org/wiki/Base64#MIME)
+ // Here: 4 / 1.37 = 2.91970800 => 2.92 (rounded 2 decimals by utils.human_size)
+ assert.strictEqual(fieldUtils.format.binary('Cg=='), '2.92 Bytes');
+
+});
+
QUnit.test('parse float', function(assert) {
assert.expect(10);
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 765166a52d4..a2be2447a07 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -5,6 +5,7 @@ var AbstractField = require('web.AbstractField');
var BasicModel = require('web.BasicModel');
var concurrency = require('web.concurrency');
var FormView = require('web.FormView');
+var ListRenderer = require('web.ListRenderer');
var ListView = require('web.ListView');
var relationalFields = require('web.relational_fields');
var StandaloneFieldManagerMixin = require('web.StandaloneFieldManagerMixin');
@@ -2081,6 +2082,39 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('transferring class attributes in one2many sub fields', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'
',
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('td.hey').length, 1,
+ 'should have a td with the desired class');
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.strictEqual(form.$('td.hey').length, 1,
+ 'should have a td with the desired class');
+
+ form.$('td.o_data_cell').click();
+
+ assert.strictEqual(form.$('input[name="turtle_foo"].hey').length, 1,
+ 'should have an input with the desired class');
+
+ form.destroy();
+ });
+
QUnit.test('one2many with date and datetime', function (assert) {
assert.expect(2);
@@ -2238,7 +2272,15 @@ QUnit.module('relational_fields', {
});
QUnit.test('embedded one2many with handle widget', function (assert) {
- assert.expect(7);
+ assert.expect(10);
+
+ var nbConfirmChange = 0;
+ testUtils.patch(ListRenderer, {
+ confirmChange: function () {
+ nbConfirmChange++;
+ return this._super.apply(this, arguments);
+ },
+ });
this.data.partner.records[0].turtles = [1, 2, 3];
@@ -2271,8 +2313,10 @@ QUnit.module('relational_fields', {
"should have the 3 rows in the correct order");
form.$buttons.find('.o_form_button_edit').click();
+
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
"should still have the 3 rows in the correct order");
+ assert.strictEqual(nbConfirmChange, 0, "should not have confirmed any change yet");
// Drag and drop the second line in first position
testUtils.dragAndDrop(
@@ -2281,6 +2325,7 @@ QUnit.module('relational_fields', {
{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");
@@ -2288,9 +2333,20 @@ QUnit.module('relational_fields', {
"should have the 3 rows in the new order");
form.$buttons.find('.o_form_button_save').click();
+
+ assert.deepEqual(_.map(this.data.turtle.records, function(turtle) {
+ return _.pick(turtle, 'id', 'turtle_foo', 'turtle_int');
+ }), [
+ {id: 1, turtle_foo: "yop", turtle_int: 1},
+ {id: 2, turtle_foo: "blip", turtle_int:0},
+ {id: 3, turtle_foo: "kawa", turtle_int:21}
+ ], "should have save the changed sequence");
+
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipyopkawa",
"should still have the 3 rows in the new order");
+ testUtils.unpatch(ListRenderer);
+
form.destroy();
});
@@ -2382,7 +2438,7 @@ QUnit.module('relational_fields', {
res_id: 1,
mockRPC: function (route, args) {
if (args.method === 'write') {
- assert.deepEqual(args.args[1].turtles, [[1,1,{"turtle_int":2}],[4,2,false],[1,3,{"turtle_int":3}]],
+ 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);
@@ -2411,9 +2467,20 @@ QUnit.module('relational_fields', {
});
QUnit.test('onchange (with command 5) for embedded one2many with handle widget', function (assert) {
- assert.expect(2);
+ assert.expect(3);
- this.data.partner.records[0].turtles = [1, 2, 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);
@@ -2439,6 +2506,7 @@ QUnit.module('relational_fields', {
res_id: 1,
});
+ form.$('.o_pager_next').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "yopblipkawa",
"should have the 3 rows in the correct order");
@@ -2453,7 +2521,10 @@ QUnit.module('relational_fields', {
{position: 'top'}
);
+ assert.strictEqual(form.$('.o_data_cell').text(), "blurpkawablip", "should display to record in 'turtle_int' order");
+
form.$buttons.find('.o_form_button_save').click();
+ form.$('.o_pager_next').click();
assert.strictEqual(form.$('.o_data_cell:not(.o_handle_cell)').text(), "blurpkawablip",
"should display to record in 'turtle_int' order");
@@ -2461,6 +2532,239 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange with modifiers for embedded one2many on the second page', function (assert) {
+ assert.expect(7);
+
+ var data = this.data;
+ var ids = [];
+ for (var i=10; i<60; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ data.turtle.records.push({
+ id: id,
+ turtle_int: 0,
+ turtle_foo: "#" + id,
+ });
+ }
+ ids.push(1, 2, 3);
+ data.partner.records[0].turtles = ids;
+ data.partner.onchanges = {
+ turtles: function (obj) {
+ // TODO: make this test more 'difficult'
+ // For now, the server only returns UPDATE commands (no LINK TO)
+ // even though it should do it (for performance reasons)
+ // var turtles = obj.turtles.splice(0, 20);
+
+ var turtles = [];
+ turtles.unshift([5]);
+ // create UPDATE commands for each records (this is the server
+ // usual answer for onchange)
+ for (var k in obj.turtles) {
+ var change = obj.turtles[k];
+ var record = _.findWhere(data.turtle.records, {id: change[1]});
+ if (change[0] === 1) {
+ _.extend(record, change[2]);
+ }
+ turtles.push([1, record.id, record]);
+ }
+ obj.turtles = turtles;
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: data,
+ arch:'
',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should display the records in order");
+
+ form.$('.o_field_one2many .o_list_view tbody tr:first td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:first input:first').val('blurp').trigger('input');
+
+ // the domain fail if the widget does not use the allready loaded data.
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "blurp#21#22#23#24#25#26#27#28#29",
+ "should display the records in order with the changes");
+
+ $('.modal .modal-footer button:first').click();
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should cancel changes and display the records in order");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
+ // Drag and drop the third line in second position
+ testUtils.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ {position: 'top'}
+ );
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#30#31#32#33#34#35#36#37#38",
+ "should display the records in order after resequence (display record with turtle_int=0)");
+
+ // Drag and drop the third line in second position
+ testUtils.dragAndDrop(
+ form.$('.ui-sortable-handle').eq(2),
+ form.$('.o_field_one2many tbody tr').eq(1),
+ {position: 'top'}
+ );
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
+ "should display the records in order after resequence (display record with turtle_int=0)");
+
+ form.$buttons.find('.o_form_button_cancel').click();
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#39#40#41#42#43#44#45#46#47",
+ "should display the records in order after resequence");
+
+ $('.modal .modal-footer button:first').click();
+
+ assert.equal(form.$('.o_field_one2many td[class="o_data_cell"]').text(), "#20#21#22#23#24#25#26#27#28#29",
+ "should cancel changes and display the records in order");
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange followed by edition on the second page', function (assert) {
+ assert.expect(20);
+
+ var ids = [];
+ for (var i=1; i<85; i++) {
+ var id = 10 + i;
+ ids.push(id);
+ this.data.turtle.records.push({
+ id: id,
+ turtle_int: id/3|0,
+ turtle_foo: "#" + i,
+ });
+ }
+ ids.splice(41, 0, 1, 2, 3);
+ this.data.partner.records[0].turtles = ids;
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [[5]].concat(obj.turtles);
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'
',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_pager_next').click();
+
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input');
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 0, "should display '#39' at the first line");
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
+ assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position");
+ assert.strictEqual(form.$('.o_data_row:first .o_field_char').val(), "", "should an empty input");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line");
+
+ form.$('.o_data_row input:first').val('value 3').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position after onchange");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 1, "should display '#39' at the second line after onchange");
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line");
+ assert.strictEqual(form.$('.o_data_row:first .o_field_char').length, 1, "should display the create line in first position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(value 3))').index(), 1, "should display the created line at the second position");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line");
+
+ form.destroy();
+
+ // bottom order
+
+ form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'
',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_pager_next').click();
+
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(1) td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(1) input:first').val('value 1').trigger('input');
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(2) td:first').click();
+ form.$('.o_field_one2many .o_list_view tbody tr:eq(2) input:first').val('value 2').trigger('input');
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 40 records");
+ assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#77))').index(), 39, "should display '#77' at the last line");
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 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(), 39, "should display the create line at the last position");
+
+ form.$('.o_data_row input:first').val('value 3').trigger('input');
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 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(), 39, "should display the create line at the last position");
+
+ form.destroy();
+ });
+
QUnit.test('embedded one2many with handle widget with minimum setValue calls', function (assert) {
var done = assert.async();
assert.expect(20);
@@ -5137,6 +5441,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('id field in one2many in a new record', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '
',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ var virtualID = args.args[0].turtles[0][1];
+ assert.deepEqual(args.args[0].turtles,
+ [[0, virtualID, {turtle_foo: "cat"}]],
+ 'should send proper commands');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+ form.$('td.o_field_x2many_list_row_add a').click();
+ form.$('td input[name="turtle_foo"]').val('cat').trigger('input');
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
QUnit.test('sub form view with a required field', function (assert) {
assert.expect(2);
this.data.partner.fields.foo.required = true;
@@ -5629,9 +5965,9 @@ QUnit.module('relational_fields', {
// enable the many2many onchange and generate it
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
- assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
+ assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
'should display the product xphone and xenomorphe');
- assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value');
// disable the many2many onchange
@@ -5661,19 +5997,19 @@ QUnit.module('relational_fields', {
// put list in readonly mode
form.$('input.o_field_integer[name="int_field"]').click();
- assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",
+ assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xenomorphexphone",
'should display the product xphone and xenomorphe');
- assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first recordsecond record",
+ assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "second recordfirst record",
'should display the tag in readonly (first record and second record)');
form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input');
- assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value');
form.$buttons.find('.o_form_button_save').click();
- assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ",
+ assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "firstxenomorphe second record new linexphone first record ",
'should display the name, one2many and many2many value after save');
form.destroy();
@@ -6811,8 +7147,8 @@ QUnit.module('relational_fields', {
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
- assert.strictEqual(args.args[1].turtles[42][0], 0);
- assert.deepEqual(args.args[1].turtles[42][2], {turtle_foo: 'rainbow dash'});
+ assert.strictEqual(args.args[1].turtles[40][0], 0, 'should send a create command');
+ assert.deepEqual(args.args[1].turtles[40][2], {turtle_foo: 'rainbow dash'});
}
return this._super.apply(this, arguments);
},
@@ -6865,8 +7201,8 @@ QUnit.module('relational_fields', {
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
- assert.strictEqual(args.args[1].turtles[0][0], 0);
- assert.deepEqual(args.args[1].turtles[0][2], {turtle_foo: 'rainbow dash'});
+ assert.strictEqual(args.args[1].turtles[40][0], 0);
+ assert.deepEqual(args.args[1].turtles[40][2], {turtle_foo: 'rainbow dash'});
}
return this._super.apply(this, arguments);
},
@@ -6882,7 +7218,8 @@ QUnit.module('relational_fields', {
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('tr.o_data_row').length, 3,
- "should have 3 data rows on the current page");
+ "should have 3 data rows on the current page (2 records and the created line)");
+
assert.ok(form.$('tr.o_data_row').first().hasClass('o_selected_row'),
"first row should be selected");
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 2993e393a4f..33b44948fca 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -989,7 +989,7 @@ QUnit.module('Views', {
});
QUnit.test('use default_order on editable tree: sort on demand', function (assert) {
- assert.expect(8);
+ assert.expect(11);
this.data.foo.records[0].o2m = [1, 3];
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
@@ -1029,12 +1029,21 @@ QUnit.module('Views', {
"Value 2 should be third (shouldn't be sorted)");
form.$('.o_form_sheet_bg').click(); // validate the row before sorting
- $o2m.find('.o_column_sortable').click();
- assert.ok(form.$('tbody tr:first td:contains(Value 3)').length,
- "Value 3 should be first");
- assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
+
+ $o2m.find('.o_column_sortable').click(); // resort list after edition
+ assert.strictEqual(form.$('tbody tr:first').text(), 'Value 1',
+ "Value 1 should be first");
+ assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
"Value 2 should be second (should be sorted after saving)");
- assert.ok(form.$('tbody tr:eq(2) td:contains(Value 1)').length,
+ assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 3',
+ "Value 3 should be third");
+
+ $o2m.find('.o_column_sortable').click();
+ assert.strictEqual(form.$('tbody tr:first').text(), 'Value 3',
+ "Value 3 should be first");
+ assert.strictEqual(form.$('tbody tr:eq(1)').text(), 'Value 2',
+ "Value 2 should be second (should be sorted after saving)");
+ assert.strictEqual(form.$('tbody tr:eq(2)').text(), 'Value 1',
"Value 1 should be third");
form.destroy();
@@ -1051,7 +1060,7 @@ QUnit.module('Views', {
ids.push(id);
this.data.bar.records.push({
id: id,
- name: "Value " + id,
+ name: "Value " + (id < 10 ? '0' : '') + id,
});
}
this.data.foo.records[0].o2m = ids;
@@ -1074,15 +1083,15 @@ QUnit.module('Views', {
// Change page
form.$('.o_pager_next').click();
- assert.ok(form.$('tbody tr:first td:contains(Value 44)').length,
+ assert.strictEqual(form.$('tbody tr:first').text(), 'Value 44',
"record 44 should be first");
- assert.ok(form.$('tbody tr:eq(4) td:contains(Value 48)').length,
+ assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 48',
"record 48 should be last");
form.$('.o_column_sortable').click();
- assert.ok(form.$('tbody tr:first td:contains(Value 48)').length,
+ assert.strictEqual(form.$('tbody tr:first').text(), 'Value 08',
"record 48 should be first");
- assert.ok(form.$('tbody tr:eq(4) td:contains(Value 44)').length,
+ assert.strictEqual(form.$('tbody tr:eq(4)').text(), 'Value 04',
"record 44 should be first");
form.destroy();
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index f6011be282d..6c6a33edb8b 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -814,7 +814,9 @@ registry.background = SnippetOption.extend({
var $image = $('
![]()
', {class: 'hidden', src: value}).appendTo(this.$target);
var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
- _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ _editor.opened(function () {
+ _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ });
_editor.on('save', this, function () {
this._setCustomBackground($image.attr('src'));
diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py
index e9e790cc0ca..750744ce7d1 100644
--- a/addons/website/models/__init__.py
+++ b/addons/website/models/__init__.py
@@ -11,3 +11,4 @@ from . import res_company
from . import res_partner
from . import res_config_settings
from . import ir_model_fields
+from . import ir_model
diff --git a/addons/website/models/ir_model.py b/addons/website/models/ir_model.py
new file mode 100644
index 00000000000..6d254374c38
--- /dev/null
+++ b/addons/website/models/ir_model.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details
+
+from odoo import api, models
+
+
+# This is a nasty hack, targeted for V11 only
+class IrModel(models.Model):
+ _inherit = 'ir.model'
+
+ @api.multi
+ def unlink(self):
+ self.env.cr.execute(
+ "DELETE FROM ir_model_fields WHERE name='website_id'")
+ return super(IrModel, self).unlink()
diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py
index 60332d9e1a4..2cef7af3dce 100644
--- a/addons/website/models/res_config_settings.py
+++ b/addons/website/models/res_config_settings.py
@@ -13,6 +13,7 @@ class ResConfigSettings(models.TransientModel):
def _default_website(self):
return self.env['website'].search([], limit=1)
+ # FIXME: Set website_id to ondelete='cascade' in master
website_id = fields.Many2one('website', string="website", default=_default_website, required=True)
website_name = fields.Char('Website Name', related='website_id.name')
language_ids = fields.Many2many(related='website_id.language_ids', relation='res.lang')
diff --git a/addons/website/security/website_security.xml b/addons/website/security/website_security.xml
index 99264d4e460..6702d0878f1 100644
--- a/addons/website/security/website_security.xml
+++ b/addons/website/security/website_security.xml
@@ -15,6 +15,10 @@
+
+
+
+
website_designer: Manage Website and qWeb view
diff --git a/addons/website/static/src/less/website.ui.less b/addons/website/static/src/less/website.ui.less
index 1527e9a7b87..b68be539a97 100644
--- a/addons/website/static/src/less/website.ui.less
+++ b/addons/website/static/src/less/website.ui.less
@@ -434,7 +434,6 @@ body .modal {
}
#theme_customize_modal {
overflow: visible;
- z-index: 1020;
display: block;
.modal-dialog {
.o-position-absolute(@odoo-navbar-height + 10, 10px);
diff --git a/addons/website_forum/i18n/website_forum.pot b/addons/website_forum/i18n/website_forum.pot
index f0c82a25110..3f9c7e1c750 100644
--- a/addons/website_forum/i18n/website_forum.pot
+++ b/addons/website_forum/i18n/website_forum.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-24 09:00+0000\n"
-"PO-Revision-Date: 2017-10-24 09:00+0000\n"
+"POT-Creation-Date: 2018-01-10 07:40+0000\n"
+"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -2401,7 +2401,9 @@ msgid "This community is for professional and enthusiast users,\n"
msgstr ""
#. module: website_forum
+#: code:addons/website_forum/models/forum.py:51
#: model:forum.forum,description:website_forum.forum_help
+#, python-format
msgid "This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together."
msgstr ""
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index 93c3144bbd0..857ea4f0a6e 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -48,9 +48,9 @@ class Forum(models.Model):
description = fields.Text(
'Description',
translate=True,
- default='This community is for professionals and enthusiasts of our products and services. '
- 'Share and discuss the best content and new marketing ideas, '
- 'build your professional profile and become a better marketer together.')
+ default=lambda s: _('This community is for professionals and enthusiasts of our products and services. '
+ 'Share and discuss the best content and new marketing ideas, '
+ 'build your professional profile and become a better marketer together.'))
welcome_message = fields.Html(
'Welcome Message',
default = """
@@ -437,7 +437,7 @@ class Post(models.Model):
elif post.parent_id and not post.can_answer:
raise KarmaError('Not enough karma to answer to a question')
if not post.parent_id and not post.can_post:
- post.state = 'pending'
+ post.sudo().state = 'pending'
# add karma for posting new questions
if not post.parent_id and post.state == 'active':
@@ -466,11 +466,18 @@ class Post(models.Model):
@api.multi
def write(self, vals):
+ trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually
if 'content' in vals:
vals['content'] = self._update_content(vals['content'], self.forum_id.id)
if 'state' in vals:
- if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self):
- raise KarmaError('Not enough karma to close or reopen a post.')
+ if vals['state'] in ['active', 'close']:
+ if any(not post.can_close for post in self):
+ raise KarmaError('Not enough karma to close or reopen a post.')
+ trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id']
+ elif vals['state'] == 'flagged':
+ if any(not post.can_flag for post in self):
+ raise KarmaError('Not enough karma to flag a post.')
+ trusted_keys += ['state', 'flag_user_id']
if 'active' in vals:
if any(not post.can_unlink for post in self):
raise KarmaError('Not enough karma to delete or reactivate a post')
@@ -487,7 +494,7 @@ class Post(models.Model):
tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
raise KarmaError(_('Not enough karma to retag.'))
- if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id', 'tag_ids'] for key in vals) and any(not post.can_edit for post in self):
+ if any(key not in trusted_keys for key in vals) and any(not post.can_edit for post in self):
raise KarmaError('Not enough karma to edit a post.')
res = super(Post, self).write(vals)
diff --git a/addons/website_forum/views/ir_qweb.xml b/addons/website_forum/views/ir_qweb.xml
index 671ae21944d..f823262d91b 100644
--- a/addons/website_forum/views/ir_qweb.xml
+++ b/addons/website_forum/views/ir_qweb.xml
@@ -5,9 +5,9 @@
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index 8bd8ff04f63..3e96864c45c 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -259,7 +259,8 @@ class SaleOrderOption(models.Model):
order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id)
if order_line:
- order_line[0].product_uom_qty += 1
+ order_line = order_line[0]
+ order_line.product_uom_qty += 1
else:
vals = {
'price_unit': self.price_unit,
diff --git a/addons/website_sale/i18n/website_sale.pot b/addons/website_sale/i18n/website_sale.pot
index bee548ff0bb..fa202144839 100644
--- a/addons/website_sale/i18n/website_sale.pot
+++ b/addons/website_sale/i18n/website_sale.pot
@@ -1227,6 +1227,7 @@ msgstr ""
#. module: website_sale
#: model:ir.ui.view,arch_db:website_sale.payment
+#: model:ir.ui.view,arch_db:website_sale.payment
msgid "Pay Now
"
msgstr ""
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 6ef3e822fa7..46af4a27002 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -1317,7 +1317,7 @@
Pay with
-
+ Pay Now
diff --git a/addons/website_slides/i18n/website_slides.pot b/addons/website_slides/i18n/website_slides.pot
index df24b77b97c..8568a4e5994 100644
--- a/addons/website_slides/i18n/website_slides.pot
+++ b/addons/website_slides/i18n/website_slides.pot
@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
-"Project-Id-Version: Odoo Server 10.saas~18\n"
+"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-02 11:27+0000\n"
-"PO-Revision-Date: 2017-10-02 11:27+0000\n"
+"POT-Creation-Date: 2018-01-10 07:40+0000\n"
+"PO-Revision-Date: 2018-01-10 07:40+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -195,6 +195,12 @@ msgstr ""
msgid " Likes"
msgstr ""
+#. module: website_slides
+#: code:addons/website_slides/models/slides.py:105
+#, python-format
+msgid "This channel is private and its content is restricted to some users.
"
+msgstr ""
+
#. module: website_slides
#: model:ir.ui.view,arch_db:website_slides.slide_detail_view
msgid "0\n"
diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py
index cb2d1836de2..b9d4aa2d757 100644
--- a/addons/website_slides/models/slides.py
+++ b/addons/website_slides/models/slides.py
@@ -102,7 +102,8 @@ class Channel(models.Model):
string='Channel Groups', help="Groups allowed to see presentations in this channel")
access_error_msg = fields.Html(
'Error Message', help="Message to display when not accessible due to access rights",
- default="This channel is private and its content is restricted to some users.
", translate=html_translate, sanitize_attributes=False)
+ default=lambda s: _("This channel is private and its content is restricted to some users.
"),
+ translate=html_translate, sanitize_attributes=False)
upload_group_ids = fields.Many2many(
'res.groups', 'rel_upload_groups', 'channel_id', 'group_id',
string='Upload Groups', help="Groups allowed to upload presentations in this channel. If void, every user can upload.")
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index db8a27dd325..e45bf0037b2 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -860,6 +860,8 @@ class IrModelFields(models.Model):
attrs['column1'] = field_data['column1'] or col1
attrs['column2'] = field_data['column2'] or col2
attrs['domain'] = safe_eval(field_data['domain'] or '[]')
+ elif field_data['ttype'] == 'monetary' and not self.pool.loaded:
+ return
# add compute function if given
if field_data['compute']:
attrs['compute'] = make_compute(field_data['compute'], field_data['depends'])
diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py
index 19405c3145d..dd70c9111ca 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -258,7 +258,7 @@ class TestTranslation(TransactionCase):
category = self.customers.with_context({'lang': 'fr_FR'}).copy({'name': 'Clients (copie)'})
category_no = category.with_context({})
- self.assertEqual(category_no.name, 'Customers', "Duplication erased original untranslated value")
+ self.assertEqual(category_no.name, 'Clients (copie)', "Duplication should set untranslated value")
category_fr = category.with_context({'lang': 'fr_FR'})
self.assertEqual(category_fr.name, 'Clients (copie)', "Did not used default value for translated value")
diff --git a/odoo/models.py b/odoo/models.py
index bd1fc1b415b..21397e83f04 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -3821,7 +3821,9 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
del vals['module'] # duplicated vals is not linked to any module
vals['res_id'] = target_id
if vals['lang'] == old.env.lang and field.translate is True:
- vals['source'] = old_wo_lang[name]
+ # force a source if the new_val was not changed by copy override
+ if new_val == old[name]:
+ vals['source'] = old_wo_lang[name]
# the value should be the new value (given by copy())
vals['value'] = new_val
Translation.create(vals)