summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-06-09 17:11:47 +0200
committerChristophe Simonis <chs@odoo.com>2017-06-09 17:11:47 +0200
commitab02bf34b0c1d3fa3752ca1f50b098d04dc5b137 (patch)
treea29591cae8777390cbeb35d65e23c9f8ae143135 /addons/web
parentb350c29e44b4ed1df0c428308c396771b22d73e8 (diff)
parentcc38b93ab2a6daf9c5badbc387cff20d1a01ddee (diff)
[MERGE] forward port branch saas-16 up to cc38b93ab2a
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js6
-rw-r--r--addons/web/static/src/js/chrome/abstract_web_client.js17
-rw-r--r--addons/web/static/src/js/fields/abstract_field.js24
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js22
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js30
-rw-r--r--addons/web/static/src/js/services/data_manager.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js11
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js6
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js4
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js20
-rw-r--r--addons/web/static/src/js/views/form/form_renderer.js40
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js2
-rw-r--r--addons/web/static/src/js/views/view_dialogs.js13
-rw-r--r--addons/web/static/src/js/widgets/date_picker.js1
-rw-r--r--addons/web/static/src/js/widgets/model_field_selector.js5
-rw-r--r--addons/web/static/src/less/model_field_selector.less4
-rw-r--r--addons/web/static/src/less/webclient.less1
-rw-r--r--addons/web/static/src/xml/base.xml7
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js286
-rw-r--r--addons/web/static/tests/helpers/test_utils.js73
-rw-r--r--addons/web/static/tests/views/basic_model_tests.js49
-rw-r--r--addons/web/static/tests/views/form_tests.js97
-rw-r--r--addons/web/static/tests/views/list_tests.js23
23 files changed, 655 insertions, 87 deletions
diff --git a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
index 807bdff70dd..8f0724bbb00 100644
--- a/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
+++ b/addons/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js
@@ -421,6 +421,12 @@
element.children().first().after(widget);
}
+ // !! ODOO FIX START !!
+ var parentOffset = parent.offset();
+ position.top = offset.top - parentOffset.top;
+ position.left = offset.left - parentOffset.left;
+ // !! ODOO FIX END !!
+
// Top and bottom logic
if (vertical === 'auto') {
if (offset.top + widget.height() * 1.5 >= $(window).height() + $(window).scrollTop() &&
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 9f56cd01ef4..481e9a7fc54 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -82,6 +82,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
}
});
},
+ show_wow: '_onShowWow',
},
init: function (parent) {
this.client_options = {};
@@ -319,6 +320,22 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
}
},
+ /**
+ * Displays a thumb up, heart or peace image (randomly) for a moment (e.g.
+ * used when an opportunity is won)
+ *
+ * @private
+ */
+ _onShowWow: function () {
+ var className = 'o_wow_thumbs';
+ if (Math.random() > 0.9) {
+ var otherClasses = ['o_wow_peace', 'o_wow_heart'];
+ className = otherClasses[Math.floor(Math.random()*otherClasses.length)];
+ }
+ var $body = $('body');
+ $body.addClass(className);
+ setTimeout($body.removeClass.bind($body, className), 1000);
+ },
});
return AbstractWebClient;
diff --git a/addons/web/static/src/js/fields/abstract_field.js b/addons/web/static/src/js/fields/abstract_field.js
index 5908f7f5a8b..492d9f26552 100644
--- a/addons/web/static/src/js/fields/abstract_field.js
+++ b/addons/web/static/src/js/fields/abstract_field.js
@@ -176,8 +176,8 @@ var AbstractField = Widget.extend({
* focusable element was not found or invisible
*/
activate: function (options) {
- var $focusable = this.getFocusableElement();
- if ($focusable.length && $focusable.is(':visible')) {
+ if (this.isFocusable()) {
+ var $focusable = this.getFocusableElement();
$focusable.focus();
if ($focusable.is('input[type="text"], textarea')) {
$focusable[0].selectionStart = $focusable[0].selectionEnd = $focusable[0].value.length;
@@ -212,6 +212,15 @@ var AbstractField = Widget.extend({
return $();
},
/**
+ * Returns true iff the widget has a visible element that can take the focus
+ *
+ * @returns {boolean}
+ */
+ isFocusable: function () {
+ var $focusable = this.getFocusableElement();
+ return $focusable.length && $focusable.is(':visible');
+ },
+ /**
* this method is used to determine if the field value is set to a meaningful
* value. This is useful to determine if a field should be displayed as empty
*
@@ -351,12 +360,13 @@ var AbstractField = Widget.extend({
* @param {Object} [options]
* @param {boolean} [options.forceChange=false] if true, the change event will be
* triggered even if the new value is the same as the old one
+ * @returns {Deferred}
*/
_setValue: function (value, options) {
// we try to avoid doing useless work, if the value given has not
// changed. Note that we compare the unparsed values.
if (this.lastSetValue === value || (this.value === false && value === '')) {
- return;
+ return $.when();
}
this.lastSetValue = value;
try {
@@ -364,18 +374,22 @@ var AbstractField = Widget.extend({
this._isValid = true;
} catch (e) {
this._isValid = false;
- return;
+ return $.Deferred().reject();
}
if (!(options && options.forceChange) && this._isSameValue(value)) {
- return;
+ return $.when();
}
+ var def = $.Deferred();
var changes = {};
changes[this.name] = value;
this.trigger_up('field_changed', {
dataPointID: this.dataPointID,
changes: changes,
viewType: this.viewType,
+ onSuccess: def.resolve.bind(def),
+ onFailure: def.reject.bind(def),
});
+ return def;
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index a5ceba50d3f..d4013a8bcdb 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1281,8 +1281,26 @@ var PriorityWidget = AbstractField.extend({
},
});
-var AttachmentImage = AbstractField.extend({
- template: 'AttachmentImage',
+var AttachmentImage = AbstractField.extend({
+ className: 'o_attachment_image',
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Reset cover image when widget value change
+ *
+ * @private
+ */
+ _render: function () {
+ if (this.value) {
+ this.$el.empty().append($('<img>/', {
+ src: "/web/image/" + this.value.data.id + "?unique=1",
+ title: this.value.data.display_name
+ }));
+ }
+ }
});
var StateSelectionWidget = AbstractField.extend({
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index e13c3d29004..ae3b06d3a02 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -213,17 +213,20 @@ var FieldMany2One = AbstractField.extend({
var self = this;
var slowCreate = this._searchCreatePopup.bind(this, "form", false, this._createContext(name));
if (this.nodeOptions.quick_create) {
- return this._rpc({
- model: this.field.relation,
- method: 'name_create',
- args: [name],
- context: this.record.getContext(this.recordParams),
- })
- .then(function (result) {
- if (self.mode === "edit") {
- self.reinitialize({id: result[0], display_name: result[1]});
- }
- }, slowCreate);
+ this.trigger_up('mutexify', {
+ action: function () {
+ return self._rpc({
+ model: self.field.relation,
+ method: 'name_create',
+ args: [name],
+ context: self.record.getContext(self.recordParams),
+ }).then(function (result) {
+ if (self.mode === "edit") {
+ self.reinitialize({id: result[0], display_name: result[1]});
+ }
+ }, slowCreate);
+ },
+ });
} else {
slowCreate();
}
@@ -964,7 +967,6 @@ var FieldOne2Many = FieldX2Many.extend({
var index = self.editable === 'top' ? 0 : self.value.data.length - 1;
var newID = self.value.data[index].id;
self.renderer.editRecord(newID);
- self.creatingRecord = false;
}
}
});
@@ -1014,6 +1016,7 @@ var FieldOne2Many = FieldX2Many.extend({
* in the kanban view
*/
_onAddRecord: function (ev) {
+ var self = this;
// we don't want interference with the components upstream.
ev.stopPropagation();
@@ -1023,10 +1026,11 @@ var FieldOne2Many = FieldX2Many.extend({
this._setValue({
operation: 'CREATE',
position: this.editable,
+ }).always(function () {
+ self.creatingRecord = false;
});
}
} else {
- var self = this;
this._openFormDialog({
on_saved: function (record) {
self._setValue({ operation: 'ADD', id: record.id });
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 5131db19028..41fc293e383 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -11,6 +11,7 @@ var utils = require('web.utils');
return core.Class.extend({
init: function () {
this._init_cache();
+ core.bus.on('clear_cache', this, this.invalidate.bind(this));
},
_init_cache: function () {
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 53e5be8ff6e..16a066db495 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -46,6 +46,9 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @returns {Deferred}
*/
start: function () {
+ // add classname to reflect the (absence of) access rights (used to
+ // correctly display the nocontent helper)
+ this.$el.toggleClass('o_cannot_create', !this.activeActions.create);
return this._super.apply(this, arguments)
.then(this._updateEnv.bind(this));
},
@@ -186,8 +189,12 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// user if some discarding has to be made). This operation must also be
// mutex-protected as commitChanges function of x2m has to be aware of
// all final changes made to a row.
- this.mutex.exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle)); // TODO write a test for this
- return this.mutex.exec(this._saveRecord.bind(this, recordID, options));
+ var self = this;
+ return this.mutex
+ .exec(this.renderer.commitChanges.bind(this.renderer, recordID || this.handle))
+ .then(function () {
+ return self.mutex.exec(self._saveRecord.bind(self, recordID, options));
+ });
},
/**
* @override
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 60abfde4344..61c285d6581 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1841,7 +1841,6 @@ var BasicModel = AbstractModel.extend({
return d.res_id === res_id;
});
m2mList.data.push(dataPoint.id);
- m2mList.count++;
});
});
});
@@ -2476,7 +2475,10 @@ var BasicModel = AbstractModel.extend({
// REPLACE_WITH
x2manyList.res_ids = value[2];
x2manyList.count = x2manyList.res_ids.length;
- defs.push(self._readUngroupedList(x2manyList));
+ var def = self._readUngroupedList(x2manyList).then(function () {
+ return self._fetchX2ManysBatched(x2manyList);
+ });
+ defs.push(def);
}
});
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 9578e568558..422af121d96 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -99,7 +99,9 @@ var FieldManagerMixin = {
// subrecord's form view), otherwise it bubbles up to the main form view
// but its model doesn't have any data related to the given dataPointID
event.stopPropagation();
- this._applyChanges(event.data.dataPointID, event.data.changes, event);
+ this._applyChanges(event.data.dataPointID, event.data.changes, event)
+ .done(event.data.onSuccess || function () {})
+ .fail(event.data.onFailure || function () {});
},
/**
* Some widgets need to trigger a reload of their data. For example, a
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index ce449f2dcc9..8eef70a916c 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -39,12 +39,10 @@ var FormController = BasicController.extend({
//--------------------------------------------------------------------------
/**
- * This method is supposed to focus the first active control, I think. It
- * is currently only called by the FormViewDialog.
- *
- * @todo To be implemented
+ * Calls autofocus on the renderer
*/
autofocus: function () {
+ this.renderer.autofocus();
},
/**
* This method switches the form view in edit mode, with a new record.
@@ -93,6 +91,14 @@ var FormController = BasicController.extend({
return dataPoint.data.display_name || _t('New');
},
/**
+ * Called each time the form view is attached into the DOM
+ *
+ * @todo convert to new style
+ */
+ on_attach_callback: function () {
+ this.autofocus();
+ },
+ /**
* Render buttons for the control panel. The form view can be rendered in
* a dialog, and in that case, if we have buttons defined in the footer, we
* have to use them instead of the standard buttons.
@@ -374,10 +380,8 @@ var FormController = BasicController.extend({
});
}
- if (event.data.show_wow) {
- def.then(function () {
- self.show_wow();
- });
+ if (event.data.showWow) {
+ def.then(this.trigger_up.bind(this, 'show_wow'));
}
def.always(this._enableButtons.bind(this));
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 ddd77ee5dc6..08f28438368 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -26,6 +26,29 @@ var FormRenderer = BasicRenderer.extend({
//--------------------------------------------------------------------------
/**
+ * Focuses the field having attribute 'default_focus' set, if any, or the
+ * first focusable field otherwise.
+ */
+ autofocus: function () {
+ if (this.mode === 'readonly') {
+ return;
+ }
+ var focusWidget = this.defaultFocusField;
+ if (!focusWidget || !focusWidget.isFocusable()) {
+ var widgets = this.allFieldWidgets[this.state.id];
+ for (var i = 0; i < (widgets ? widgets.length : 0); i++) {
+ var widget = widgets[i];
+ if (widget.isFocusable()) {
+ focusWidget = widget;
+ break;
+ }
+ }
+ }
+ if (focusWidget) {
+ focusWidget.activate({noselect: true});
+ }
+ },
+ /**
* Extend the method so that labels also receive the 'o_field_invalid' class
* if necessary.
*
@@ -183,7 +206,7 @@ var FormRenderer = BasicRenderer.extend({
self.trigger_up('button_clicked', {
attrs: node.attrs,
record: self.state,
- show_wow: self.$el.hasClass('o_wow'), // TODO: implement this (in view)
+ showWow: $el.hasClass('o_wow'),
});
});
},
@@ -709,7 +732,10 @@ var FormRenderer = BasicRenderer.extend({
* @returns {jQueryElement}
*/
_renderTagSeparator: function (node) {
- return $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
+ var $separator = $('<div/>').addClass('o_horizontal_separator').text(node.attrs.string);
+ this._handleAttributes($separator, node);
+ this._registerModifiers(node, this.state, $separator);
+ return $separator;
},
/**
* @private
@@ -769,20 +795,16 @@ var FormRenderer = BasicRenderer.extend({
core.bus.trigger('DOM_updated');
// Attach the tooltips on the fields' label
- var focusWidget = this.defaultFocusField;
_.each(this.allFieldWidgets[this.state.id], function (widget) {
- if (!focusWidget) {
- focusWidget = widget;
- }
if (core.debug || widget.attrs.help || widget.field.help) {
var idForLabel = self.idsForLabels[widget.name];
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
self._addFieldTooltip(widget, $label);
}
});
- if (focusWidget) {
- focusWidget.activate({noselect: true});
- }
+
+ // Focus the default field (in edit mode)
+ self.autofocus();
},
/**
* Sets id attribute of given widget to idForLabel
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 9364d1ba9fe..08b9e7f1602 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
@@ -155,7 +155,7 @@ ListRenderer.include({
var self = this;
var rowIndex = _.findIndex(this.state.data, {id: recordID});
if (rowIndex < 0) {
- return;
+ return $.when();
}
var editMode = (mode === 'edit');
var record = this.state.data[rowIndex];
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index b84d60d5b7c..a19e440913e 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -4,6 +4,7 @@ odoo.define('web.view_dialogs', function (require) {
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
+var dom = require('web.dom');
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var pyeval = require('web.pyeval');
@@ -194,8 +195,10 @@ var FormViewDialog = ViewDialog.extend({
if ($buttons.children().length) {
self.$footer.empty().append($buttons.contents());
}
- _super().$el.append(fragment);
- self.form_view.autofocus();
+ dom.append(_super().$el, fragment, {
+ callbacks: [{widget: self.form_view}],
+ in_DOM: true,
+ });
});
});
@@ -281,6 +284,7 @@ var SelectCreateDialog = ViewDialog.extend({
if(this.options.initial_view !== "search") {
return this.create_edit_record();
}
+ var self = this;
var user_context = this.getSession().user_context;
var _super = this._super.bind(this);
@@ -298,7 +302,10 @@ var SelectCreateDialog = ViewDialog.extend({
this.loadViews(this.dataset.model, this.dataset.get_context(), [[false, 'list'], [false, 'search']], {})
.then(this.setup.bind(this, search_defaults))
.then(function (fragment) {
- _super().$el.append(fragment);
+ dom.append(_super().$el, fragment, {
+ callbacks: [{widget: self.list_controller}],
+ in_DOM: true,
+ });
});
return this;
},
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index 353a5dc78f2..76281d9366a 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -42,6 +42,7 @@ var DateWidget = Widget.extend({
locale : moment.locale(),
allowInputToggle: true,
keyBinds: null,
+ widgetParent: 'body',
});
},
/**
diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js
index 279428e8939..6607561a2bb 100644
--- a/addons/web/static/src/js/widgets/model_field_selector.js
+++ b/addons/web/static/src/js/widgets/model_field_selector.js
@@ -16,6 +16,11 @@ var modelFieldsCache = {
cacheDefs: {},
};
+core.bus.on('clear_cache', null, function () {
+ modelFieldsCache.cache = {};
+ modelFieldsCache.cacheDefs = {};
+});
+
/**
* The ModelFieldSelector widget can be used to display/select a particular
* field chain from a given model.
diff --git a/addons/web/static/src/less/model_field_selector.less b/addons/web/static/src/less/model_field_selector.less
index 124c15cb8e5..aee5bd17867 100644
--- a/addons/web/static/src/less/model_field_selector.less
+++ b/addons/web/static/src/less/model_field_selector.less
@@ -7,6 +7,10 @@
.o-flex-flow(row, wrap);
.o-align-items(center);
height: 100%;
+ min-height: 20px; // needed when there is no value in it and used standalone
+ &:active, &:focus, &:active:focus {
+ outline: none;
+ }
> .o_field_selector_chain_part {
padding: 0px 1px;
diff --git a/addons/web/static/src/less/webclient.less b/addons/web/static/src/less/webclient.less
index 2f9878fbdc3..5e84d74b599 100644
--- a/addons/web/static/src/less/webclient.less
+++ b/addons/web/static/src/less/webclient.less
@@ -12,6 +12,7 @@ a {
// General
// ------------------------------------------------------------------
.o_web_client {
+ position: relative; // normally useless but required by bootstrap-datepicker
background-color: @odoo-webclient-background-color;
}
.o_text_overflow {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 394abe18c7c..20e38e98912 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -24,7 +24,7 @@
<div t-name="CrashManager.warning" class="o_dialog_warning">
<t t-js="d">
- var message = d.message ? d.message : d.error.data.message;
+ var message = (d.message !== undefined) ? d.message : d.error.data.message;
d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
</t>
<t t-raw="html_error"/>
@@ -836,11 +836,6 @@
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>
-<t t-name="AttachmentImage">
- <div class="o_attachment_image">
- <img t-att-src="'/web/image/' + widget.value + '?unique=1'"/>
- </div>
-</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/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 4f326e14f98..127d5761fd2 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -482,6 +482,183 @@ QUnit.module('relational_fields', {
// assert.strictEqual(form.$('input').eq(1).val(), 'xphone', "onchange should have been applied");
// });
+ QUnit.test('form: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(5);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ form.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('list: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(8);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="top">' +
+ '<field name="trululu"/>' +
+ '</tree>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ var $dropdown = list.$('.o_field_many2one input').autocomplete('widget');
+ list.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ list.$buttons.find('.o_list_button_add').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+ assert.strictEqual(list.$('.o_data_row').length, 4,
+ "should wait for the name_create before adding the new row");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+ assert.strictEqual(list.$('.o_data_row:nth(1) .o_data_cell').text(), 'b',
+ "created row should have the correct m2o value");
+ assert.strictEqual(list.$('.o_data_row').length, 5,
+ "should have added the fifth row");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ list.destroy();
+ done();
+ });
+ });
+
+ QUnit.test('list in form: quick create then save directly', function (assert) {
+ var done = assert.async();
+ assert.expect(6);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var def = $.Deferred();
+ var newRecordID;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<sheet>' +
+ '<field name="p">' +
+ '<tree editable="bottom">' +
+ '<field name="trululu"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'name_create') {
+ assert.step('name_create');
+ return def.then(_.constant(result)).then(function (nameGet) {
+ newRecordID = nameGet[0];
+ return nameGet;
+ });
+ }
+ if (args.method === 'create') {
+ assert.step('create');
+ assert.strictEqual(args.args[0].p[0][2].trululu, newRecordID,
+ "should create with the correct m2o id");
+ }
+ return result;
+ },
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
+ form.$('.o_field_many2one input').val('b').trigger('keydown');
+ concurrency.delay(0).then(function () {
+ $dropdown.find('li:first()').click(); // quick create 'b'
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['name_create'],
+ "should wait for the name_create before creating the record");
+
+ def.resolve();
+
+ assert.verifySteps(['name_create', 'create']);
+ assert.strictEqual(form.$('.o_data_row:first .o_data_cell').text(), 'b',
+ "first row should have the correct m2o value");
+
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
+ form.destroy();
+ done();
+ });
+ });
+
QUnit.test('autocompletion in a many2one, in form view with a domain', function (assert) {
assert.expect(1);
@@ -3852,6 +4029,115 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('editable list: value reset by an onchange', function (assert) {
+ // this test reproduces a subtle behavior that may occur in a form view:
+ // the user adds a record in a one2many field, and directly clicks on a
+ // datetime field of the form view which has an onchange, which totally
+ // overrides the value of the one2many (commands 5 and 0). The handler
+ // that switches the edited row to readonly is then called after the
+ // new value of the one2many field is applied (the one returned by the
+ // onchange), so the row that must go to readonly doesn't exist anymore.
+ assert.expect(2);
+
+ this.data.partner.onchanges = {
+ datetime: function (obj) {
+ obj.turtles = [[5], [0, 0, {display_name: 'new'}]];
+ },
+ };
+
+ var def;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="datetime"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'onchange') {
+ return $.when(def).then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ // trigger the two onchanges
+ form.$('.o_field_x2many_list_row_add a').click();
+ form.$('.o_data_row .o_field_widget').val('a name').trigger('input');
+ def = $.Deferred();
+ form.$('.o_datepicker_input').click(); // focusout o2m and set value to today
+
+ // resolve the onchange def
+ def.resolve();
+
+ assert.strictEqual(form.$('.o_data_row').length, 1,
+ "should have one record in the o2m");
+ assert.strictEqual(form.$('.o_data_row .o_data_cell').text(), 'new',
+ "should be the record created by the onchange");
+
+ form.destroy();
+ });
+
+ QUnit.test('editable list: onchange that returns a warning', function (assert) {
+ assert.expect(5);
+
+ this.data.turtle.onchanges = {
+ display_name: function () {},
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'onchange') {
+ assert.step(args.method);
+ return $.when({
+ value: {},
+ warning: {
+ title: "Warning",
+ message: "You must first select a partner"
+ },
+ });
+ }
+ return this._super.apply(this, arguments);
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ intercepts: {
+ warning: function () {
+ assert.step('warning');
+ },
+ },
+ });
+
+ // add a line (this should trigger an onchange and a warning)
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ // check if 'Add an item' still works (this should trigger an onchange
+ // and a warning again)
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.verifySteps(['onchange', 'warning', 'onchange', 'warning']);
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index eae31e68e80..bc2cf05f360 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -13,6 +13,7 @@ odoo.define('web.test_utils', function (require) {
var basic_fields = require('web.basic_fields');
var config = require('web.config');
var core = require('web.core');
+var dom = require('web.dom');
var session = require('web.session');
var MockServer = require('web.MockServer');
var Widget = require('web.Widget');
@@ -172,7 +173,14 @@ function createAsyncView(params) {
widget.destroy();
$('#qunit-fixture').off('DOMNodeInserted.removeSRC');
};
- return view.appendTo($view_manager).then(function () {
+ // render the view in a fragment as they must be able to render correctly
+ // without being in the DOM
+ var fragment = document.createDocumentFragment();
+ return view.appendTo(fragment).then(function () {
+ dom.append($view_manager, fragment, {
+ callbacks: [{widget: view}],
+ in_DOM: true,
+ });
view.$el.on('click', 'a', function (ev) {
ev.preventDefault();
});
@@ -244,52 +252,53 @@ function addMockEnvironment(widget, params) {
logLevel: params.logLevel,
currentDate: params.currentDate,
});
- var widgetDestroy;
-
// make sure the debounce value for input fields is set to 0
var initialDebounce = DebouncedField.prototype.DEBOUNCE;
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
-
+ var initialSession, initialConfig, initialParameters;
if ('session' in params) {
- var initialSession = _.extend({}, session);
+ initialSession = _.extend({}, session);
_.extend(session, params.session);
- widgetDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in session) {
- delete session[key];
- }
- _.extend(session, initialSession);
- widgetDestroy.call(this);
- };
}
-
if ('config' in params) {
- var initialConfig = _.extend({}, config);
+ initialConfig = _.extend({}, config);
_.extend(config, params.config);
- widgetDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in config) {
- delete config[key];
- }
- _.extend(config, initialConfig);
- DebouncedField.prototype.DEBOUNCE = initialDebounce;
- widgetDestroy.call(this);
- };
}
-
if ('translateParameters' in params) {
- var initialParameters = _.extend({}, core._t.database.parameters);
+ initialParameters = _.extend({}, core._t.database.parameters);
_.extend(core._t.database.parameters, params.translateParameters);
- var oldDestroy = widget.destroy;
- widget.destroy = function () {
- for (var key in core._t.database.parameters) {
+ }
+
+ var widgetDestroy = widget.destroy;
+ widget.destroy = function () {
+ // clear the caches (e.g. data_manager, ModelFieldSelector) when the
+ // widget is destroyed, at the end of each test to avoid collisions
+ core.bus.trigger('clear_cache');
+
+ DebouncedField.prototype.DEBOUNCE = initialDebounce;
+
+ var key;
+ if ('session' in params) {
+ for (key in session) {
+ delete session[key];
+ }
+ _.extend(session, initialSession);
+ }
+ if ('config' in params) {
+ for (key in config) {
+ delete config[key];
+ }
+ _.extend(config, initialConfig);
+ }
+ if ('translateParameters' in params) {
+ for (key in core._t.database.parameters) {
delete core._t.database.parameters[key];
}
_.extend(core._t.database.parameters, initialParameters);
- oldDestroy.call(this);
- };
- }
+ }
+ widgetDestroy.call(this);
+ };
intercept(widget, 'call_service', function (event) {
if (event.data.service === 'ajax') {
diff --git a/addons/web/static/tests/views/basic_model_tests.js b/addons/web/static/tests/views/basic_model_tests.js
index 582e06ba62b..fefc7231e5b 100644
--- a/addons/web/static/tests/views/basic_model_tests.js
+++ b/addons/web/static/tests/views/basic_model_tests.js
@@ -1165,6 +1165,54 @@ QUnit.module('Views', {
model.destroy();
});
+ QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.o2m = {
+ string: "O2M", type: 'one2many', relation: 'partner', default: [[6, 0, [1]]],
+ };
+
+ var model = createModel({
+ Model: BasicModel,
+ data: this.data,
+ });
+
+ var params = {
+ fieldNames: ['o2m'],
+ fields: this.data.partner.fields,
+ fieldsInfo: {
+ form: {
+ o2m: {
+ relatedFields: this.data.partner.fields,
+ fieldsInfo: {
+ list: {
+ category: {
+ relatedFields: { display_name: {} },
+ },
+ },
+ },
+ viewType: 'list',
+ },
+ },
+ },
+ modelName: 'partner',
+ type: 'record',
+ viewType: 'form',
+ };
+
+ model.load(params).then(function (resultID) {
+ var record = model.get(resultID);
+ assert.strictEqual(record.data.o2m.count, 1, "o2m field should contain 1 record");
+ var categoryList = record.data.o2m.data[0].data.category;
+ assert.strictEqual(categoryList.count, 1,
+ "category field should contain 1 record");
+ assert.strictEqual(categoryList.data[0].data.display_name,
+ 'gold', "category records should have been fetched");
+ });
+
+ model.destroy();
+ });
+
QUnit.test('contexts and domains can be properly fetched', function (assert) {
assert.expect(8);
@@ -1448,6 +1496,7 @@ QUnit.module('Views', {
fieldsInfo: {
form: {
product_ids: {
+ relatedFields: this.data.product.fields,
fieldsInfo: { list: { name: {}, date: {} } },
viewType: 'list',
}
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index a07f022eb74..3f829a654b7 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -837,6 +837,28 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('invisible attrs on separators', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<separator string="Geolocation" attrs=\'{"invisible": [["bar", "=", True]]}\'/>'+
+ '<field name="bar"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ res_id: 1,
+ });
+ assert.strictEqual(form.$('div.o_horizontal_separator').hasClass('o_invisible_modifier'), true,
+ "separator div should be hidden");
+ form.destroy();
+ });
+
QUnit.test('buttons in form view', function (assert) {
assert.expect(7);
@@ -3770,7 +3792,7 @@ QUnit.module('Views', {
});
form.$buttons.find('.o_form_button_edit').click();
- assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
assert.strictEqual(form.$('input[name="foo"]')[0].selectionStart, 3,
"cursor should be at the end");
@@ -3792,7 +3814,25 @@ QUnit.module('Views', {
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
- assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "foo field should have focus");
+
+ form.destroy();
+ });
+
+ QUnit.test('in create mode, autofocus fields are focused', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field"/>' +
+ '<field name="foo" default_focus="1"/>' +
+ '</form>',
+ });
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
"foo field should have focus");
form.destroy();
@@ -3821,6 +3861,24 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('autofocus first visible field', function (assert) {
+ assert.expect(1);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="int_field" invisible="1"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ });
+ assert.strictEqual(document.activeElement, form.$('input[name="foo"]')[0],
+ "foo field should have focus");
+
+ form.destroy();
+ });
+
QUnit.test('open one2many form containing many2many_tags', function (assert) {
assert.expect(4);
@@ -4893,5 +4951,40 @@ QUnit.module('Views', {
form.destroy();
});
+ QUnit.test('wow effect', function (assert) {
+ assert.expect(3);
+
+ this.data.partner.fields.model_name = { string: "Model name", type: "char" };
+
+ var clickCount = 0;
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<button name="test" string="Mark Won" type="object" class="o_wow"/>' +
+ '</form>',
+ intercepts: {
+ execute_action: function (event) {
+ if (clickCount === 0) {
+ event.data.on_success();
+ } else {
+ event.data.on_fail();
+ }
+ clickCount++;
+ },
+ show_wow: function () {
+ assert.step('wow');
+ },
+ },
+ });
+
+ form.$('button.o_wow').click();
+ assert.verifySteps(['wow']);
+ form.$('button.o_wow').click();
+ assert.verifySteps(['wow']);
+
+ form.destroy();
+ });
});
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index e1cfef2b23b..332ba6687f9 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -84,7 +84,7 @@ QUnit.module('Views', {
QUnit.module('ListView');
QUnit.test('simple readonly list', function (assert) {
- assert.expect(9);
+ assert.expect(10);
var list = createView({
View: ListView,
@@ -93,6 +93,9 @@ QUnit.module('Views', {
arch: '<tree><field name="foo"/><field name="int_field"/></tree>',
});
+ assert.notOk(list.$el.hasClass('o_cannot_create'),
+ "should not have className 'o_cannot_create'");
+
// 3 th (1 for checkbox, 2 for columns)
assert.strictEqual(list.$('th').length, 3, "should have 3 columns");
@@ -114,6 +117,24 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('list with create="0"', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree create="0"><field name="foo"/></tree>',
+ });
+
+ assert.ok(list.$el.hasClass('o_cannot_create'),
+ "should have className 'o_cannot_create'");
+ assert.strictEqual(list.$buttons.find('.o_list_button_add').length, 0,
+ "should not have the 'Create' button");
+
+ list.destroy();
+ });
+
QUnit.test('simple editable rendering', function (assert) {
assert.expect(9);