summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-10-21 15:22:36 +0200
committerChristophe Simonis <chs@odoo.com>2017-10-21 15:22:36 +0200
commitde987831da237ed5f9c96d50a9e51d083e072aa0 (patch)
treeef714d2d98693eae3446620d28bb48314573018a /addons/web/static
parentea2a976f31b2611b6b4ff69f4a5cdc2ffc7798fe (diff)
parentd70518d04f8f8061c8c683ea4ba8c95ae631ae45 (diff)
[MERGE] forward port branch 11.0 up to d70518d04f8
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/core/ajax.js11
-rw-r--r--addons/web/static/src/js/core/misc.js17
-rw-r--r--addons/web/static/src/js/core/session.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js34
-rw-r--r--addons/web/static/src/js/report/qwebactionmanager.js7
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js36
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_controller.js21
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js8
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js14
-rw-r--r--addons/web/static/src/less/modal.less2
-rw-r--r--addons/web/static/src/xml/base.xml8
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js106
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js168
-rw-r--r--addons/web/static/tests/helpers/mock_server.js8
-rw-r--r--addons/web/static/tests/views/calendar_tests.js14
-rw-r--r--addons/web/static/tests/views/form_tests.js38
16 files changed, 453 insertions, 41 deletions
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index d69574d2e10..0ee6d2ad1ed 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -212,6 +212,9 @@ var loadJS = (function () {
* @param {Function} [options.success] callback in case of download success
* @param {Function} [options.error] callback in case of request error, provided with the error body
* @param {Function} [options.complete] called after both ``success`` and ``error`` callbacks have executed
+ * @returns {boolean} a false value means that a popup window was blocked. This
+ * mean that we probably need to inform the user that something needs to be
+ * changed to make it work.
*/
function get_file(options) {
// need to detect when the file is done downloading (not used
@@ -232,7 +235,12 @@ function get_file(options) {
var url = options.session.url(options.url, params);
if (options.complete) { options.complete(); }
- return window.open(url);
+ var w = window.open(url);
+ if (!w || w.closed || typeof w.closed === 'undefined') {
+ // popup was blocked
+ return false;
+ }
+ return true;
}
var $form, $form_data = $('<div>');
@@ -309,6 +317,7 @@ function get_file(options) {
}
};
timer = setTimeout(waitLoop, CHECK_INTERVAL);
+ return true;
};
function post (controller_url, data) {
diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js
index 64ebeeb1794..983b26d3606 100644
--- a/addons/web/static/src/js/core/misc.js
+++ b/addons/web/static/src/js/core/misc.js
@@ -173,6 +173,23 @@ function ReloadContext (parent, action) {
}
core.action_registry.add("reload_context", ReloadContext);
+// In Internet Explorer, document doesn't have the contains method, so we make a
+// polyfill for the method in order to be compatible.
+if (!document.contains) {
+ document.contains = function contains (node) {
+ if (!(0 in arguments)) {
+ throw new TypeError('1 argument is required');
+ }
+
+ do {
+ if (this === node) {
+ return true;
+ }
+ } while (node = node && node.parentNode);
+
+ return false;
+ };
+}
return {
blockUI: blockUI,
diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js
index 0ecea92fb71..c3181a7abe6 100644
--- a/addons/web/static/src/js/core/session.js
+++ b/addons/web/static/src/js/core/session.js
@@ -278,7 +278,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
options.data.session_id = this.session_id;
}
options.session = this;
- ajax.get_file(options);
+ return ajax.get_file(options);
},
/**
* (re)loads the content of a session: db name, username, user id, session
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 6392870dc90..8eae8f5b8f1 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -394,7 +394,7 @@ var LinkButton = AbstractField.extend({
var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
- supportedFieldTypes: ['date'],
+ supportedFieldTypes: ['date', 'datetime'],
/**
* @override
@@ -453,12 +453,13 @@ var FieldDate = InputField.extend({
* @override
* @private
* @param {Moment|false} value
+ * @returns {boolean}
*/
_isSameValue: function (value) {
if (value === false) {
return this.value === false;
}
- return value.isSame(this.value);
+ return value.isSame(this.value, 'day');
},
/**
* Instantiates a new DateWidget datepicker.
@@ -497,7 +498,16 @@ var FieldDateTime = FieldDate.extend({
var value = this.datewidget.getValue();
return value && value.add(-this.getSession().getTZOffset(value), 'minutes');
},
-
+ /**
+ * @override
+ * @private
+ */
+ _isSameValue: function (value) {
+ if (value === false) {
+ return this.value === false;
+ }
+ return value.isSame(this.value);
+ },
/**
* Instantiates a new DateTimeWidget datepicker rather than DateWidget.
*
@@ -966,7 +976,7 @@ var FieldPhone = FieldEmail.extend({
* @override
*/
getFocusableElement: function () {
- if (this._canCall()) {
+ if (this.mode !== 'readonly' || this._canCall()) {
return this._super.apply(this, arguments);
}
return $();
@@ -1171,7 +1181,6 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
supportedFieldTypes: ['binary'],
_render: function () {
var self = this;
- var attrs = this.attrs;
var url = this.placeholder;
if (this.value) {
if (!utils.is_bin_size(this.value)) {
@@ -1186,11 +1195,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
});
}
}
- var $img = $('<img>').attr('src', url);
- $img.css({
- width: this.nodeOptions.size ? this.nodeOptions.size[0] : attrs.img_width || attrs.width,
- height: this.nodeOptions.size ? this.nodeOptions.size[1] : attrs.img_height || attrs.height,
- });
+ var $img = $(qweb.render("FieldBinaryImage-img", {widget: this, url: url}));
this.$('> img').remove();
this.$el.prepend($img);
$img.on('error', function () {
@@ -2175,6 +2180,15 @@ var FieldDomain = AbstractField.extend({
//--------------------------------------------------------------------------
/**
+ * A domain field is always set since the false value is considered to be
+ * equal to "[]" (match all records).
+ *
+ * @override
+ */
+ isSet: function () {
+ return true;
+ },
+ /**
* @override isValid from AbstractField.isValid
* Parsing the char value is not enough for this field. It is considered
* valid if the internal domain selector was built correctly and that the
diff --git a/addons/web/static/src/js/report/qwebactionmanager.js b/addons/web/static/src/js/report/qwebactionmanager.js
index 3c8fee859e3..cc875882ce2 100644
--- a/addons/web/static/src/js/report/qwebactionmanager.js
+++ b/addons/web/static/src/js/report/qwebactionmanager.js
@@ -21,7 +21,7 @@ var WKHTMLTOPDF_MESSAGES = {
};
var trigger_download = function (session, response, c, action, options) {
- session.get_file({
+ return session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: framework.unblockUI,
@@ -107,7 +107,10 @@ ActionManager.include({
report_urls['qweb-pdf'],
action.report_type, //The 'root' report is considered the maine one, so we use its type for all the others.
];
- trigger_download(self.getSession(), response, c, current_action, options);
+ var success = trigger_download(self.getSession(), response, c, current_action, options);
+ if (!success) {
+ self.do_warn(_t('Warning'), _t('A popup window with your report was blocked. You may need to change your browser settings to allow popup windows for this page.'), true);
+ }
treated_actions.push(current_action);
current_action = current_action.next_report_to_generate;
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 6884442039e..1df118ecfd9 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -875,7 +875,10 @@ var BasicModel = AbstractModel.extend({
// id never changes, and should not be written
delete record._changes.id;
}
- var changes = self._generateChanges(record, {viewType: options.viewType});
+ var changes = self._generateChanges(record, {viewType: options.viewType, changesOnly: method !== 'create'});
+
+ // id field should never be written/changed
+ delete changes.id;
if (method === 'create') {
var fieldNames = record.getFieldNames();
@@ -1551,6 +1554,9 @@ var BasicModel = AbstractModel.extend({
removedDef = this._applyX2ManyChange(record, fieldName, {
operation: 'DELETE',
ids: _.map(removedIds, function (resID) {
+ if (resID in list._cache) {
+ return list._cache[resID];
+ }
return _.findWhere(listData, {res_id: resID}).id;
}),
});
@@ -2455,7 +2461,7 @@ var BasicModel = AbstractModel.extend({
changes = _.extend({}, record.data, record._changes);
} else {
changes = _.extend({}, record._changes);
- };
+ }
var withReadonly = options.withReadonly || false;
var commands = this._generateX2ManyCommands(record, {
changesOnly: 'changesOnly' in options ? options.changesOnly : true,
@@ -2492,6 +2498,7 @@ var BasicModel = AbstractModel.extend({
changes[fieldName] = false;
}
}
+
return changes;
},
/**
@@ -2608,6 +2615,7 @@ var BasicModel = AbstractModel.extend({
_.each(relRecordUpdated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
if (!_.isEmpty(changes)) {
+ delete changes.id;
var command = x2ManyCommands.update(relRecord.res_id, changes);
commands[fieldName].push(command);
}
@@ -2638,7 +2646,12 @@ var BasicModel = AbstractModel.extend({
// this is a new id
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
- commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes));
+ if ('id' in changes) {
+ delete changes.id;
+ commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes));
+ } else {
+ commands[fieldName].push(x2ManyCommands.create(relRecord.ref, changes));
+ }
}
}
if (options.changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) {
@@ -3294,11 +3307,17 @@ var BasicModel = AbstractModel.extend({
* This method is quite important: it is supposed to perform the /onchange
* rpc and apply the result.
*
+ * The changes that triggered the onchange are assumed to have already been
+ * applied to the record.
+ *
* @param {Object} record
* @param {string[]} fields changed fields
* @param {string} [viewType] current viewType. If not set, we will assume
* main viewType from the record
- * @returns {Deferred}
+ * @returns {Deferred} Note: this deferred cannot fail. It is either in the
+ * 'success' or 'pending' state. If the onchange rpc fails, it will be
+ * resolved with an empty dictionary. Note that the initial change was
+ * already applied.
*/
_performOnChange: function (record, fields, viewType) {
var self = this;
@@ -3318,7 +3337,9 @@ var BasicModel = AbstractModel.extend({
var context = this._getContext(record, options);
var currentData = this._generateOnChangeData(record, {changesOnly: false});
- return self._rpc({
+ var def = $.Deferred();
+
+ self._rpc({
model: record.model,
method: 'onchange',
args: [idList, currentData, fields, onchangeSpec, context],
@@ -3343,9 +3364,12 @@ var BasicModel = AbstractModel.extend({
record._domains = _.extend(record._domains, result.domain);
}
return self._applyOnChange(result.value, record).then(function () {
- return result;
+ def.resolve(result);
});
+ }).fail(function () {
+ def.resolve({});
});
+ return def;
},
/**
* Once a record is created and some data has been fetched, we need to do
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 4168d901065..08d03c037f0 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -257,13 +257,20 @@ var CalendarController = AbstractController.extend({
id = id && parseInt(id).toString() === id ? parseInt(id) : id;
if (!this.eventOpenPopup) {
- this.do_action({
- type: 'ir.actions.act_window',
- res_id: id,
- res_model: this.modelName,
- views: [[this.formViewId || false, 'form']],
- target: 'current',
- context: event.context || self.context,
+ this._rpc({
+ model: self.modelName,
+ method: 'get_formview_id',
+ //The event can be called by a view that can have another context than the default one.
+ args: [[id], event.context || self.context],
+ }).then(function (viewId) {
+ self.do_action({
+ type:'ir.actions.act_window',
+ res_id: id,
+ res_model: self.modelName,
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ context: event.context || self.context,
+ });
});
return;
}
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index b9711d1d392..0ddf2f0a129 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -807,7 +807,15 @@ var FormRenderer = BasicRenderer.extend({
// Attach the tooltips on the fields' label
_.each(this.allFieldWidgets[this.state.id], function (widget) {
var idForLabel = self.idsForLabels[widget.name];
+ // We usually don't support multiple widgets for the same field on the
+ // same view but it is the case with the new settings view on V11.0.
+ // Therefore, we need to retrieve the correct label since it could be
+ // displayed multiple times on the view, otherwise, for example the
+ // enterprise label will be displayed as many times as the field
+ // exists on settings.
+ var $widgets = self.$('.o_field_widget[name=' + widget.name + ']');
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
+ $label = $label.eq($widgets.index(widget.$el));
if (config.debug || widget.attrs.help || widget.field.help) {
self._addFieldTooltip(widget, $label);
}
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 9027fc3c984..b0ddbdcd385 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -136,7 +136,9 @@ ListRenderer.include({
currentRowID = this.state.data[this.currentRow].id;
currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex];
focusedElement = currentWidget.getFocusableElement().get(0);
- selectionRange = dom.getSelectionRange(focusedElement);
+ if (currentWidget.formatType !== 'boolean') {
+ selectionRange = dom.getSelectionRange(focusedElement);
+ }
}
var oldData = this.state.data;
@@ -179,7 +181,9 @@ ListRenderer.include({
currentRowID = self.state.data[newRowIndex].id;
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
focusedElement = currentWidget.getFocusableElement().get(0);
- dom.setSelectionRange(focusedElement, selectionRange);
+ if (selectionRange) {
+ dom.setSelectionRange(focusedElement, selectionRange);
+ }
});
}
});
@@ -585,12 +589,18 @@ ListRenderer.include({
if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) {
return $.Deferred().reject();
}
+ // _activateFieldWidget might trigger an onchange,
+ // which requires currentFieldIndex to be set
+ // so that the cursor can be restored
+ var oldFieldIndex = self.currentFieldIndex;
+ self.currentFieldIndex = fieldIndex;
fieldIndex = self._activateFieldWidget(record, fieldIndex, {
inc: 1,
wrap: wrap,
event: options && options.event,
});
if (fieldIndex < 0) {
+ self.currentFieldIndex = oldFieldIndex;
return $.Deferred().reject();
}
self.currentFieldIndex = fieldIndex;
diff --git a/addons/web/static/src/less/modal.less b/addons/web/static/src/less/modal.less
index 1d95ddc219b..95a14420773 100644
--- a/addons/web/static/src/less/modal.less
+++ b/addons/web/static/src/less/modal.less
@@ -52,6 +52,8 @@
.modal-body {
overflow: auto;
+ // fix iOS issue https://github.com/scottjehl/Device-Bugs/issues/8
+ -webkit-transform: translate3d(0, 0, 0);
min-height: 0;
&.o_dialog_error {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index e1f955370d3..7da3f448378 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -335,12 +335,12 @@
<t t-if="item.create_uid and item.create_uid[0]" t-set="item.title">
<t t-raw="item.title"/><br/>
<b>Created by : </b><br/>
- <t t-raw="item.create_uid[1] + ' ' + item.create_date"/>
+ <t t-raw="item.create_uid[1]"/> <t t-esc="item.create_date_string" />
</t>
<t t-if="item.create_uid and item.write_uid and item.create_uid[0] != item.write_uid[0]" t-set="item.title">
<t t-raw="item.title"/><br/>
<b>Modified by : </b><br/>
- <t t-raw="item.write_uid[1] + ' ' + item.write_date"/>
+ <t t-raw="item.write_uid[1]"/> <t t-esc="item.write_date_string"/>
</t>
</t>
<a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url">
@@ -861,8 +861,8 @@
t-att-src='url'
t-att-border="widget.readonly ? 0 : 1"
t-att-name="widget.name"
- t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
- t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"/>
+ t-att-width="widget.nodeOptions.size ? widget.nodeOptions.size[0] : widget.attrs.img_width || widget.attrs.width"
+ t-att-height="widget.nodeOptions.size ? widget.nodeOptions.size[1] : widget.attrs.img_height || widget.attrs.height"/>
</t>
<t t-name="FieldBinaryFile">
<a t-if="widget.mode === 'readonly'" href="javascript:void(0)" class="o_form_uri"/>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index f01f2d8b589..1e623e54d9a 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1462,6 +1462,42 @@ QUnit.module('basic_fields', {
}
});
+ QUnit.module('FieldImage');
+
+ QUnit.test('image fields are correctly rendered', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].document = 'myimage';
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" widget="image" options="{\'size\': [90, 90]}"/> ' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === 'data:image/png;base64,myimage') {
+ assert.ok(true, "should called the correct route");
+ return $.when('wow');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(form.$('div[name="document"]').hasClass('o_field_image'),
+ "the widget should have the correct class");
+ assert.strictEqual(form.$('div[name="document"] > img').length, 1,
+ "the widget should contain an image");
+ assert.ok(form.$('div[name="document"] > img').hasClass('img-responsive'),
+ "the image should have the correct class");
+ assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90",
+ "the image should correctly set its attributes");
+ form.destroy();
+
+ });
+
QUnit.module('JournalDashboardGraph');
QUnit.test('graph dashboard widget is rendered correctly', function (assert) {
@@ -1844,6 +1880,39 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('do not trigger a field_changed for datetime field with date widget', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners"><field name="datetime" widget="date"/></form>',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017',
+ 'the date should be correct');
+
+ form.$('input[name="datetime"]').val('02/08/2017').trigger('input').trigger('change');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['read']); // should not have save as nothing changed
+
+ form.destroy();
+ });
+
QUnit.module('FieldDatetime');
QUnit.test('datetime field in form view', function (assert) {
@@ -2828,6 +2897,33 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('use TAB to navigate to a phone field', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="display_name"/>' +
+ '<field name="foo" widget="phone"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ form.$('input[name=display_name]').click();
+ assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
+ "display_name should be focused");
+ form.$('input[name="display_name"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ "foo should be focused");
+
+ form.destroy();
+ });
+
QUnit.module('PriorityWidget');
QUnit.test('priority widget when not set', function (assert) {
@@ -3939,7 +4035,7 @@ QUnit.module('basic_fields', {
});
QUnit.test('domain field: handle false domain as []', function (assert) {
- assert.expect(2);
+ assert.expect(3);
this.data.partner.records[0].foo = false;
this.data.partner.fields.bar.type = "char";
@@ -3965,11 +4061,13 @@ QUnit.module('basic_fields', {
return this._super.apply(this, arguments);
},
res_id: 1,
- viewOptions: {
- mode: 'edit',
- },
});
+ assert.strictEqual(form.$('.o_field_widget[name=foo]:not(.o_field_empty)').length, 1,
+ "there should be a domain field, not considered empty");
+
+ form.$buttons.find('.o_form_button_edit').click();
+
var $warning = form.$('.o_field_widget[name=foo] .text-warning');
assert.strictEqual($warning.length, 0, "should not display that the domain is invalid");
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 641546b3fc1..2d328f0b7ff 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1064,14 +1064,14 @@ QUnit.module('relational_fields', {
});
QUnit.test('list in form: default_get with x2many create and onchange', function (assert) {
- assert.expect(1);
+ assert.expect(2);
this.data.partner.onchanges.turtles = function (obj) {
assert.deepEqual(
obj.turtles,
[
- [0, 2, {turtle_foo: 'blip', id: 2}],
- [0, 3, {turtle_foo: 'kawa', id: 3}]
+ [1, 2, {turtle_foo: 'blip'}],
+ [1, 3, {turtle_foo: 'kawa'}]
],
"should have properly created the x2many command list");
};
@@ -1094,10 +1094,21 @@ QUnit.module('relational_fields', {
if (args.method === 'default_get') {
return $.when({turtles: [[6, 0, [2,3]]]});
}
+ if (args.method === 'create') {
+ // it would be even better if we did not send the current
+ // unchanged state with the command 1, but this seems more
+ // difficult.
+ assert.deepEqual(args.args[0].turtles, [
+ [1, 2, {turtle_foo: 'blip'}],
+ [1, 3, {turtle_foo: 'kawa'}]
+ ], 'should send proper commands to create method');
+ }
return this._super.apply(this, arguments);
},
});
+ form.$buttons.find('.o_form_button_save').click();
+
form.destroy();
});
@@ -6248,6 +6259,35 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('checkbox in an x2many that triggers an onchange', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = {
+ bar: function () {}
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="bar"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ form.$('.o_field_widget[name=bar] input').click();
+ assert.notOk(form.$('.o_field_widget[name=bar] input').prop('checked'),
+ "the checkbox should be unticked");
+
+ form.destroy();
+ });
+
QUnit.test('one2many with default value: edit line to make it invalid', function (assert) {
assert.expect(3);
@@ -6284,6 +6324,66 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('display correct value after validation error', function (assert) {
+ assert.expect(4);
+
+ this.data.partner.onchanges.turtles = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ // we simulate a validation error. In the 'real' web client,
+ // the server error will be used by the session to display
+ // an error dialog. From the point of view of the basic
+ // model, the deferred is just rejected.
+ return $.Deferred().reject();
+ }
+ if (args.method === 'write') {
+ assert.deepEqual(args.args[1].turtles[0], [1,2,{turtle_foo: 'foo'}],
+ 'should send the "bad" value');
+ // we simulate a validation error
+ return $.Deferred().reject();
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {mode: 'edit'},
+ res_id: 1,
+ });
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'blip',
+ "initial text should be correct");
+ form.$('.o_data_row .o_data_cell:nth(0)').click();
+ form.$('.o_field_widget[name=turtle_foo]').val('foo').trigger('input');
+
+ // we try to validate the line. This triggers an onchange which will be
+ // rejected. The line will be returned to readonly mode, but with the
+ // new invalid value.
+ form.$el.click();
+
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(0)').text(), 'foo',
+ "turtle_foo text should now be foo (invalid value)");
+
+ // we make sure here that when we save, the values are the current
+ // values displayed in the field.
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.mode, 'edit', "form view should still be in edit mode");
+
+ form.destroy();
+ });
+
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
@@ -7128,6 +7228,42 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('default_get, onchange, onchange on m2m', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.int_field = function (obj) {
+ if (obj.int_field === 2) {
+ assert.deepEqual(obj.timmy, [
+ [6, false, [12]],
+ [1, 12, {display_name: 'gold'}]
+ ]);
+ }
+ obj.timmy = [
+ [5],
+ [1, 12, {display_name: 'gold'}]
+ ];
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="timmy">' +
+ '<tree>' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '<field name="int_field"/>' +
+ '</sheet>' +
+ '</form>',
+ });
+
+ form.$('.o_field_widget[name=int_field]').val(2).trigger('input');
+ form.destroy();
+ });
+
QUnit.module('FieldStatus');
QUnit.test('static statusbar widget on many2one field', function (assert) {
@@ -8288,6 +8424,32 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('widget many2many_checkboxes: start non empty, then remove twice', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].timmy = [12,14];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<group><field name="timmy" widget="many2many_checkboxes"/></group>' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ });
+
+ form.$('div.o_field_widget div.o_checkbox input').eq(0).click();
+ form.$('div.o_field_widget div.o_checkbox input').eq(1).click();
+ form.$buttons.find('.o_form_button_save').click();
+ assert.notOk(form.$('div.o_field_widget div.o_checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('div.o_field_widget div.o_checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2ManyBinaryMultiFiles');
QUnit.test('widget many2many_binary', function (assert) {
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 8cb30291f24..dfcbdba5985 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -110,6 +110,14 @@ var MockServer = Class.extend({
console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString));
}
return JSON.parse(resultString);
+ }).fail(function (result) {
+ var errorString = JSON.stringify(result || false);
+ if (logLevel === 1) {
+ console.log('Mock: (ERROR)' + route, JSON.parse(errorString));
+ } else if (logLevel === 2) {
+ console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString));
+ }
+ return JSON.parse(errorString);
});
},
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index ecf02be9c4a..ed9e416a302 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -975,6 +975,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.Deferred().resolve('A view');
+ }
+ return this._super(route, args);
+ },
});
// click on an existing event to open the form view
@@ -985,7 +991,7 @@ QUnit.module('Views', {
type: "ir.actions.act_window",
res_id: 4,
res_model: "event",
- views: [[false, "form"]],
+ views: [['A view', "form"]],
target: "current",
context: {}
},
@@ -1103,6 +1109,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
});
assert.strictEqual(calendar.$('.fc-resizer').length, 0, "should not have resize button");
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index ac45b6de256..989c67e78dd 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -2649,6 +2649,44 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('default_get, onchange which fails, should still work', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges.foo = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="foo"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ // we simulate a validation error. In the 'real' web client,
+ // the server error will be used by the session to display
+ // an error dialog. From the point of view of the basic
+ // model, the deferred is just rejected.
+ return $.Deferred().reject();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ // this test checks that a form view is still rendered when the server
+ // onchange fails (for example, because of a validation error, or, more
+ // likely, a bug in the onchange code). This is quite rare, but if the
+ // onchange fails, we still want to display the form view (with the error
+ // dialog from the session/crashmanager). Otherwise, we could be in the
+ // situation where a user clicks on a button, it should open a wizard,
+ // but something fails and the wizard is not even rendered. In that
+ // case, there is nothing that the user could do.
+ assert.strictEqual(form.$('.o_field_widget[name="foo"]').val(), 'My little Foo Value',
+ "should display proper default value");
+
+ form.destroy();
+ });
+
QUnit.test('attrs are properly transmitted to new records', function (assert) {
assert.expect(2);