@@ -213,5 +260,16 @@
+
+
+ Inventory Lines
+ ir.actions.act_window
+ stock.inventory.line
+ form
+ tree
+
+ [('inventory_id', '=', active_id)]
+
+
-
\ No newline at end of file
+
diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py
index 36ffe593acb..23a87f4cd02 100644
--- a/addons/web/models/ir_http.py
+++ b/addons/web/models/ir_http.py
@@ -5,8 +5,6 @@ import json
from odoo import models
from odoo.http import request
-import pytz
-import datetime
import odoo
@@ -26,7 +24,6 @@ class Http(models.AbstractModel):
version_info = odoo.service.common.exp_version()
return {
"session_id": request.session.sid,
- "tzOffset": self._context.get('tz') and datetime.datetime.now(pytz.timezone(self._context['tz'])).utcoffset().total_seconds() / 60 or False,
"uid": request.session.uid,
"is_system": request.env.user._is_system(),
"is_superuser": request.env.user._is_superuser(),
diff --git a/addons/web/static/src/js/chrome/search_inputs.js b/addons/web/static/src/js/chrome/search_inputs.js
index 2a265ff0fdd..2dba4c0dd99 100644
--- a/addons/web/static/src/js/chrome/search_inputs.js
+++ b/addons/web/static/src/js/chrome/search_inputs.js
@@ -232,7 +232,7 @@ var IntegerField = NumberField.extend(/** @lends instance.web.search.IntegerFiel
error_message: _t("not a valid integer"),
parse: function (value) {
try {
- return field_utils.parse.interger(value);
+ return field_utils.parse.integer(value);
} catch (e) {
return NaN;
}
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 5962bb28acb..2dae906c6db 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -26,7 +26,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
search: function(event) {
var d = event.data;
_.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
- this.active_view.controller.reload(this.env);
+ this.active_view.controller.reload(_.extend({}, this.env));
},
switch_view: function(event) {
if ('res_id' in event.data) {
diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js
index c98ff7fbe9a..033d7995c9c 100644
--- a/addons/web/static/src/js/core/session.js
+++ b/addons/web/static/src/js/core/session.js
@@ -285,10 +285,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
var result = _.extend({}, window.odoo.session_info);
delete result.session_id;
_.extend(this, result);
- if (this.tzOffset === false) {
- // send by server to have the user timezone and not the browser timezone
- this.tzOffset = -new Date().getTimezoneOffset();
- }
return $.when();
},
check_session_id: function () {
@@ -410,6 +406,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
}) ? '' : this.prefix;
return prefix + path + qs;
},
+ /**
+ * Returns the time zone difference (in minutes) from the current locale
+ * (host system settings) to UTC, for a given date. The offset is positive
+ * if the local timezone is behind UTC, and negative if it is ahead.
+ *
+ * @param {string || moment} date a valid string date or moment instance
+ * @returns {integer}
+ */
+ getTZOffset: function (date) {
+ return -new Date(date).getTimezoneOffset();
+ },
});
return Session;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 2de5a73802d..0026cea0413 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -341,6 +341,9 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
*/
_renderEdit: function () {
var def = this._super.apply(this, arguments);
+ if (this.field.size && this.field.size > 0) {
+ this.$el.attr('maxlength', this.field.size);
+ }
this.$el = this.$el.add(this._renderTranslateButton());
return def;
},
@@ -450,7 +453,7 @@ var FieldDateTime = FieldDate.extend({
*/
_getValue: function () {
var value = this.datewidget.getValue();
- return value && value.add(-this.getSession().tzOffset, 'minutes');
+ return value && value.add(-this.getSession().getTZOffset(value), 'minutes');
},
/**
@@ -460,7 +463,7 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_makeDatePicker: function () {
- var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
+ var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
return new datepicker.DateTimeWidget(this, {defaultDate: value});
},
@@ -471,7 +474,7 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_renderEdit: function () {
- var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
+ var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
this.datewidget.setValue(value);
this.$input = this.datewidget.$input;
},
@@ -791,6 +794,7 @@ var FieldFloatTime = FieldFloat.extend({
var FieldText = InputField.extend(TranslatableFieldMixin, {
className: 'o_field_text',
supportedFieldTypes: ['text'],
+ tagName: 'span',
/**
* @constructor
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 0a44a52e315..db5cd34d8b9 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -95,8 +95,10 @@ function formatDate(value, field, options) {
if (value === false) {
return "";
}
- if (!options || !('timezone' in options) || options.timezone) {
- value = value.clone().add(session.tzOffset < 0 ? -1 : 0, 'days');
+ if (field && field.type === 'datetime') {
+ if (!options || !('timezone' in options) || options.timezone) {
+ value = value.clone().add(session.getTZOffset(value), 'minutes');
+ }
}
var l10n = core._t.database.parameters;
var date_format = time.strftime_to_moment_format(l10n.date_format);
@@ -121,7 +123,7 @@ function formatDateTime(value, field, options) {
return "";
}
if (!options || !('timezone' in options) || options.timezone) {
- value = value.clone().add(session.tzOffset, 'minutes');
+ value = value.clone().add(session.getTZOffset(value), 'minutes');
}
var l10n = core._t.database.parameters;
var date_format = time.strftime_to_moment_format(l10n.date_format);
@@ -357,11 +359,8 @@ function parseDate(value, field, options) {
date = moment.utc(value);
} else {
date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
- if (options && options.timezone) {
- date.add(session.tzOffset > 0 ? -1 : 0, 'days');
- }
}
- if (date.isValid() && date.year() >= 1900) {
+ if (date.isValid()) {
if (date.year() === 0) {
date.year(moment.utc().year());
}
@@ -405,7 +404,7 @@ function parseDateTime(value, field, options) {
} else {
datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
if (options && options.timezone) {
- datetime.add(-session.tzOffset, 'minutes');
+ datetime.add(-session.getTZOffset(datetime), 'minutes');
}
}
if (datetime.isValid()) {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 0dce6ec2bb4..e05a53194c3 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -21,7 +21,6 @@ var dialogs = require('web.view_dialogs');
var core = require('web.core');
var data = require('web.data');
var Dialog = require('web.Dialog');
-var field_utils = require('web.field_utils');
var KanbanRenderer = require('web.KanbanRenderer');
var ListRenderer = require('web.ListRenderer');
var Pager = require('web.Pager');
@@ -226,7 +225,7 @@ var FieldMany2One = AbstractField.extend({
var self = this;
var def = $.Deferred();
var slowCreate = function () {
- var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
+ var dialog = self._searchCreatePopup("form", false, self._createContext(name));
dialog.on('closed', self, def.resolve.bind(def));
};
if (this.nodeOptions.quick_create) {
@@ -290,7 +289,7 @@ var FieldMany2One = AbstractField.extend({
_reset: function () {
this._super.apply(this, arguments);
this.floating = false;
- this.m2o_value = field_utils.format.many2one(this.value);
+ this.m2o_value = this._formatValue(this.value);
},
/**
* @private
@@ -394,7 +393,7 @@ var FieldMany2One = AbstractField.extend({
*/
_searchCreatePopup: function (view, ids, context) {
var self = this;
- new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
+ return new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
res_model: this.field.relation,
domain: this.record.getDomain({fieldName: this.name}),
context: _.extend({}, this.record.getContext(this.recordParams), context || {}),
@@ -567,7 +566,7 @@ var KanbanFieldMany2One = AbstractField.extend({
tagName: 'span',
init: function () {
this._super.apply(this, arguments);
- this.m2o_value = field_utils.format.many2one(this.value);
+ this.m2o_value = this._formatValue(this.value);
},
//--------------------------------------------------------------------------
@@ -2023,7 +2022,8 @@ var FieldRadio = FieldSelection.extend({
* `name_get` as data).
*/
var FieldReference = FieldMany2One.extend({
- supportedFieldTypes: ['reference'],
+ specialData: "_fetchSpecialReference",
+ supportedFieldTypes: ['char', 'reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
@@ -2036,22 +2036,8 @@ var FieldReference = FieldMany2One.extend({
// needs to be copied as it is an unmutable object
this.field = _.extend({}, this.field);
- if (this.value) {
- var model = null;
- // The value of a Reference field can either be a string in the form
- // of model,record_id or an object
- if (typeof this.value === "string") {
- var value = this.value.split(',');
- model = value[0];
- this.value = {
- res_id: parseInt(value[1]),
- };
- } else {
- model = this.value.model;
- }
- this._setRelation(model);
- }
+ this._setState();
},
/**
* @override
@@ -2071,7 +2057,13 @@ var FieldReference = FieldMany2One.extend({
* @override
*/
_formatValue: function () {
- return this.recordData.display_name;
+ var value;
+ if (this.field.type === 'char') {
+ value = this.record.specialData[this.name];
+ } else {
+ value = this.value;
+ }
+ return value && value.data && value.data.display_name || '';
},
/**
@@ -2097,8 +2089,10 @@ var FieldReference = FieldMany2One.extend({
* @private
*/
_reset: function () {
+ var value = this.$('select').val();
+ this._setState();
this._super.apply(this, arguments);
- this._setRelation(this.value && this.value.model);
+ this.$('select').val(this.value && this.value.model || value);
},
/**
* Set `relation` key in field properties.
@@ -2110,6 +2104,19 @@ var FieldReference = FieldMany2One.extend({
// used to generate the search in many2one
this.field.relation = model;
},
+ /**
+ * @private
+ */
+ _setState: function () {
+ if (this.field.type === 'char') {
+ // in this case, the value is stored in specialData instead
+ this.value = this.record.specialData[this.name];
+ }
+
+ if (this.value) {
+ this._setRelation(this.value.model);
+ }
+ },
/**
* @override
* @private
diff --git a/addons/web/static/src/js/fields/special_fields.js b/addons/web/static/src/js/fields/special_fields.js
index 69647902afe..7a0bb9ed092 100644
--- a/addons/web/static/src/js/fields/special_fields.js
+++ b/addons/web/static/src/js/fields/special_fields.js
@@ -54,7 +54,7 @@ var FieldTimezoneMismatch = FieldSelection.extend({
}
var offset = this.recordData.tz_offset.match(/([+-])([0-9]{2})([0-9]{2})/);
offset = (offset[1] === '-' ? -1 : 1) * (parseInt(offset[2])*60 + parseInt(offset[3]));
- var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), {timezone: false});
+ var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), this.field, {timezone: false});
var content = this.$option.html().split(' ')[0];
content += ' ('+ datetime + ')';
this.$option.html(content);
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 6ba9dbea477..cfdee6793dd 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -329,9 +329,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
if (options && options.readonlyIfRealDiscard && !needDiscard) {
return;
}
- if (needDiscard) { // Just some optimization
- self.model.discardChanges(recordID);
- }
+ self.model.discardChanges(recordID);
if (self.model.isNew(recordID)) {
self._abandonRecord(recordID);
return;
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 8755805f993..0d95c292070 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -185,6 +185,29 @@ var BasicModel = AbstractModel.extend({
return id;
});
},
+ /**
+ * Onchange RPCs may return values for fields that are not in the current
+ * view. Those fields might even be unknown when the onchange returns (e.g.
+ * in x2manys, we only know the fields that are used in the inner view, but
+ * not those used in the potential form view opened in a dialog when a sub-
+ * record is clicked). When this happens, we can't infer their type, so the
+ * given value can't be processed. It is instead stored in the '_rawChanges'
+ * key of the record, without any processing. Later on, if this record is
+ * displayed in another view (e.g. the user clicked on it in the x2many
+ * list, and the record opens in a dialog), those changes that were left
+ * behind must be applied. This function applies changes stored in
+ * '_rawChanges' for a given viewType.
+ *
+ * @param {string} id local resource id of a record
+ * @param {string} viewType the current viewType
+ * @returns {Deferred -> string} resolves to the id of the record
+ */
+ applyRawChanges: function (recordID, viewType) {
+ var record = this.localData[recordID];
+ return this._applyOnChange(record._rawChanges, record, viewType).then(function () {
+ return record.id;
+ });
+ },
/**
* Delete a list of records, then, if the records have a parent, reload it.
*
@@ -754,14 +777,15 @@ var BasicModel = AbstractModel.extend({
if (newValue instanceof Array) {
rec._savePoint = newValue.slice(0);
} else {
- // save the viewType of edition, so that the correct readonly modifiers
- // can be evaluated when the record will be saved
- for (var fieldName in (rec._changes || {})) {
- rec._editionViewType[fieldName] = options.viewType;
- }
rec._savePoint = _.extend({}, newValue);
}
});
+
+ // save the viewType of edition, so that the correct readonly modifiers
+ // can be evaluated when the record will be saved
+ _.each((record._changes || {}), function (value, fieldName) {
+ record._editionViewType[fieldName] = options.viewType;
+ });
}
var shouldReload = 'reload' in options ? options.reload : true;
var method = self.isNew(record_id) ? 'create' : 'write';
@@ -896,18 +920,23 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred -> string} resolves to the group id
*/
toggleGroup: function (groupId) {
+ var self = this;
var group = this.localData[groupId];
if (group.isOpen) {
group.isOpen = false;
group.data = [];
+ group.res_ids = [];
group.offset = 0;
+ this._updateParentResIDs(group);
return $.when(groupId);
}
if (!group.isOpen) {
group.isOpen = true;
var def;
if (group.count > 0) {
- def = this._load(group);
+ def = this._load(group).then(function () {
+ self._updateParentResIDs(group);
+ });
}
return $.when(def).then(function () {
return groupId;
@@ -1093,21 +1122,33 @@ var BasicModel = AbstractModel.extend({
* @param {Object} values the result of the onchange RPC (a mapping of
* fieldnames to their value)
* @param {Object} record
+ * @param {string} [viewType] current viewType. If not set, we will assume
+ * main viewType from the record
* @returns {Deferred}
*/
- _applyOnChange: function (values, record) {
+ _applyOnChange: function (values, record, viewType) {
var self = this;
var defs = [];
var rec;
+ viewType = viewType || record.viewType;
record._changes = record._changes || {};
_.each(values, function (val, name) {
var field = record.fields[name];
if (!field) {
- return; // ignore changes of unknown fields
+ // this field is unknown so we can't process it for now (it is not
+ // in the current view anyway, otherwise it wouldn't be unknown.
+ // we store its value without processing it, so that if we later
+ // on switch to another view in which this field is displayed,
+ // we could process it as we would know its type then.
+ // use case: an onchange sends a create command for a one2many,
+ // in the dict of values, there is a value for a field that is
+ // not in the one2many list, but that is in the one2many form.
+ record._rawChanges[name] = val;
+ return;
}
-
- if (field.type === 'many2one' ) {
- var id = false;
+ var id;
+ if (field.type === 'many2one') {
+ id = false;
// in some case, the value returned by the onchange can
// be false (no value), so we need to avoid creating a
// local record for that.
@@ -1126,13 +1167,27 @@ var BasicModel = AbstractModel.extend({
id = rec.id;
}
record._changes[name] = id;
+ } else if (field.type === 'reference') {
+ id = false;
+ if (val) {
+ var ref = val.split(',');
+ rec = self._makeDataPoint({
+ context: record.context,
+ data: {id: parseInt(ref[1])},
+ modelName: ref[0],
+ parentID: record.id,
+ });
+ defs.push(self._fetchNameGet(rec));
+ id = rec.id;
+ }
+ record._changes[name] = id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var listId = record._changes[name] || record.data[name];
var list;
if (listId) {
list = self.localData[listId];
} else {
- var fieldInfo = record.fieldsInfo[record.viewType][name];
+ var fieldInfo = record.fieldsInfo[viewType][name];
if (!fieldInfo) {
return; //ignore changes of x2many not in view
}
@@ -1142,7 +1197,7 @@ var BasicModel = AbstractModel.extend({
parentID: record.id,
static: true,
type: 'list',
- viewtype: fieldInfo.viewType,
+ viewType: fieldInfo.viewType,
});
}
record._changes[name] = list.id;
@@ -1535,15 +1590,17 @@ var BasicModel = AbstractModel.extend({
var self = this;
var model;
var records = [];
+ var ids = [];
list = this._applyX2ManyOperations(list);
- var ids = _.map(list.data, function (localId) {
+ _.each(list.data, function (localId) {
var record = self.localData[localId];
var data = record._changes || record.data;
var many2oneId = data[fieldName];
+ if (!many2oneId) { return; }
var many2oneRecord = self.localData[many2oneId];
records.push(many2oneRecord);
+ ids.push(many2oneRecord.res_id);
model = many2oneRecord.model;
- return many2oneRecord.res_id;
});
return this._rpc({
model: model,
@@ -1599,6 +1656,39 @@ var BasicModel = AbstractModel.extend({
});
});
},
+ /**
+ * Fetch the `name_get` for a reference field.
+ *
+ * @private
+ * @param {Object} record
+ * @param {string} fieldName
+ * @returns {Deferred}
+ */
+ _fetchReference: function (record, fieldName) {
+ var self = this;
+ var def;
+ var value = record._changes && record._changes[fieldName] || record.data[fieldName];
+ var model = value && value.split(',')[0];
+ var resID = value && parseInt(value.split(',')[1]);
+ if (model && model !== 'False' && resID) {
+ def = self._rpc({
+ model: model,
+ method: 'name_get',
+ args: [resID],
+ context: record.getContext({fieldName: fieldName}),
+ }).then(function (result) {
+ return self._makeDataPoint({
+ data: {
+ id: result[0][0],
+ display_name: result[0][1],
+ },
+ modelName: model,
+ parentID: record.id,
+ });
+ });
+ }
+ return $.when(def);
+ },
/**
* Fetch the extra data (`name_get`) for the reference fields of the record
* model.
@@ -1614,28 +1704,12 @@ var BasicModel = AbstractModel.extend({
_.each(fieldNames, function (fieldName) {
var field = record.fields[fieldName];
if (field.type === 'reference') {
- var value = record.data[fieldName];
- if (value) {
- var model = value.split(',')[0];
- var resID = parseInt(value.split(',')[1]);
- var def = self._rpc({
- model: model,
- method: 'name_get',
- args: [resID],
- context: record.getContext({fieldName: fieldName}),
- }).then(function (result) {
- var referenceDp = self._makeDataPoint({
- data: {
- id: result[0][0],
- display_name: result[0][1],
- },
- modelName: model,
- parentID: record.id,
- });
- record.data[fieldName] = referenceDp.id;
- });
- defs.push(def);
- }
+ var def = self._fetchReference(record, fieldName).then(function (dataPoint) {
+ if (dataPoint) {
+ record.data[fieldName] = dataPoint.id;
+ }
+ });
+ defs.push(def);
}
});
return $.when.apply($, defs);
@@ -1905,6 +1979,25 @@ var BasicModel = AbstractModel.extend({
context: context
});
},
+ /**
+ * Fetches the `name_get` associated to the reference widget if the field is
+ * a `char` (which is a supported case).
+ *
+ * @private
+ * @param {Object} record - an element from the localData
+ * @param {Object} fieldName - the name of the field
+ * @returns {Deferred}
+ */
+ _fetchSpecialReference: function (record, fieldName) {
+ var def;
+ var field = record.fields[fieldName];
+ if (field.type === 'char') {
+ // if the widget reference is set on a char field, the name_get
+ // needs to be fetched a posteriori
+ def = this._fetchReference(record, fieldName);
+ }
+ return $.when(def);
+ },
/**
* Fetches all the m2o records associated to the given fieldName. If the
* given fieldName is not a m2o field, nothing is done. The difference with
@@ -2712,6 +2805,7 @@ var BasicModel = AbstractModel.extend({
_cache: type === 'list' ? {} : undefined,
_changes: null,
_domains: {},
+ _rawChanges: {},
aggregateValues: params.aggregateValues || {},
context: params.context || {},
count: params.count || res_ids.length,
@@ -2826,14 +2920,25 @@ var BasicModel = AbstractModel.extend({
var field = params.fields[name];
data[name] = null;
record._changes = record._changes || {};
+ var dp;
if (field.type === 'many2one' && result[name]) {
- var rec = self._makeDataPoint({
+ dp = self._makeDataPoint({
context: record.context,
data: {id: result[name]},
modelName: field.relation,
parentID: record.id,
});
- record._changes[name] = rec.id;
+ record._changes[name] = dp.id;
+ } else if (field.type === 'reference' && result[name]) {
+ var ref = result[name].split(',');
+ dp = self._makeDataPoint({
+ context: record.context,
+ data: {id: parseInt(ref[1])},
+ modelName: ref[0],
+ parentID: record.id,
+ });
+ defs.push(self._fetchNameGet(dp));
+ record._changes[name] = dp.id;
} else if (field.type === 'one2many' || field.type === 'many2many') {
var fieldInfo = record.fieldsInfo[record.viewType][name];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
@@ -2859,6 +2964,7 @@ var BasicModel = AbstractModel.extend({
x2manyList._changes = [];
var many2ones = {};
var r;
+ result[name] = result[name] || []; // handle false value
var isCommandList = result[name].length && _.isArray(result[name][0]);
if (!isCommandList) {
result[name] = [[6, false, result[name]]];
@@ -2885,7 +2991,7 @@ var BasicModel = AbstractModel.extend({
r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
- if (r._changes[name] === null) {
+ if (!r._changes[name]) {
continue;
}
var isFieldInView = name in r.fields;
@@ -3318,6 +3424,7 @@ var BasicModel = AbstractModel.extend({
list.data = data;
list.res_ids = ids;
}
+ self._updateParentResIDs(list);
return list;
});
},
@@ -3363,6 +3470,26 @@ var BasicModel = AbstractModel.extend({
});
}
},
+ /**
+ * Updates the res_ids of the parent of a given element of type list.
+ *
+ * After some operations (e.g. loading more records, folding/unfolding a
+ * group), the res_ids list of an element may be updated. When this happens,
+ * the res_ids of its ancestors need to be updated as well. This is the
+ * purpose of this function.
+ *
+ * @param {Object} element
+ */
+ _updateParentResIDs: function (element) {
+ var self = this;
+ if (element.parentID) {
+ var parent = this.localData[element.parentID];
+ parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
+ return self.localData[dataPointID].res_ids;
+ }));
+ this._updateParentResIDs(parent);
+ }
+ },
/**
* Helper method. Recursively traverses the data, starting from the element
* record (or list), then following all relations. This is useful when one
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 736416dea27..bdc250011f0 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -63,6 +63,7 @@ var BasicView = AbstractView.extend({
*/
_loadData: function () {
if (this.recordID) {
+ var self = this;
var record = this.model.get(this.recordID);
var viewType = this.viewType;
var viewFields = Object.keys(record.fieldsInfo[viewType]);
@@ -107,15 +108,27 @@ var BasicView = AbstractView.extend({
}
}
});
- if (fieldNames.length && !this.model.isNew(record.id)) {
- return this.model.reload(this.recordID, {
- fieldNames: fieldNames,
- keepChanges: true,
- viewType: viewType,
+
+ var def;
+ if (fieldNames.length) {
+ // Some fields in the new view weren't in the previous one, so
+ // we might have stored changes for them (e.g. coming from
+ // onchange RPCs), that we haven't been able to process earlier
+ // (because those fields were unknow at that time). So we ask
+ // the model to process them.
+ def = this.model.applyRawChanges(record.id, viewType).then(function () {
+ if (!self.model.isNew(record.id)) {
+ return self.model.reload(record.id, {
+ fieldNames: fieldNames,
+ keepChanges: true,
+ viewType: viewType,
+ });
+ }
});
- } else {
- return $.when(this.recordID);
}
+ return $.when(def).then(function () {
+ return record.id;
+ });
}
return this._super.apply(this, arguments);
},
diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js
index d2b0c02c524..c8b9fd574b2 100644
--- a/addons/web/static/src/js/views/calendar/calendar_model.js
+++ b/addons/web/static/src/js/views/calendar/calendar_model.js
@@ -38,39 +38,47 @@ return AbstractModel.extend({
var start = event.start.clone();
var end = event.end && event.end.clone();
- if (!end || end.diff(start) < 0) {
- end = start.clone().add(1, 'h');
+ // Detects allDay events (86400000 = 1 day in ms)
+ if (event.allDay || (end && end.diff(start) % 86400000 === 0)) {
+ event.allDay = true;
}
- if (event.allDay || end.diff(start) % 86400000 === 0) {
- event.allDay = true;
+ // Set end date if not existing
+ if (!end || end.diff(start) < 0) { // undefined or invalid end date
+ if (event.allDay) {
+ end = start.clone();
+ } else {
+ // in week mode or day mode, convert allday event to event
+ end = start.clone().add(2, 'h');
+ }
+ } else if (event.allDay) {
+ // For an "allDay", FullCalendar gives the end day as the
+ // next day at midnight (instead of 23h59).
+ end.add(-1, 'days');
+ }
- if (this.scale === 'month') {
+ // An "allDay" event without the "all_day" option is not considered
+ // as a 24h day. It's just a part of the day (by default: 7h-19h).
+ if (event.allDay) {
+ if (!this.mapping.all_day) {
if (event.r_start) {
start.hours(event.r_start.hours())
.minutes(event.r_start.minutes())
.seconds(event.r_start.seconds())
.utc();
- end.add(-1, 'days')
- .hours(event.r_end.hours())
+ end.hours(event.r_end.hours())
.minutes(event.r_end.minutes())
.seconds(event.r_end.seconds())
.utc();
} else {
- start.utc();
- end.utc();
+ // default hours in the user's timezone
+ start.hours(7).add(-this.getSession().getTZOffset(start), 'minutes');
+ end.hours(19).add(-this.getSession().getTZOffset(end), 'minutes');
}
- } else if (this.mapping.all_day) {
- start.startOf('day');
- end.startOf('day').add(-1, 'days');
- } else {
- // default hours in the user's timezone
- start.hours(7).add(-this.getSession().tzOffset, 'minutes');
- end.hours(19).add(-this.getSession().tzOffset, 'minutes');
}
} else {
- start.add(-this.getSession().tzOffset, 'minutes');
- end.add(-this.getSession().tzOffset, 'minutes');
+ start.add(-this.getSession().getTZOffset(start), 'minutes');
+ end.add(-this.getSession().getTZOffset(end), 'minutes');
}
if (this.mapping.all_day) {
@@ -365,6 +373,7 @@ return AbstractModel.extend({
monthNamesShort: moment.monthsShort(),
dayNames: moment.weekdays(),
dayNamesShort: moment.weekdaysShort(),
+ firstDay: moment().startOf('week').isoWeekday(),
};
},
_getRangeDomain: function () {
@@ -599,8 +608,8 @@ return AbstractModel.extend({
date_stop = date_start.clone().add(date_delay,'hours');
}
- date_start.add(this.getSession().tzOffset, 'minutes');
- date_stop.add(this.getSession().tzOffset, 'minutes');
+ date_start.add(this.getSession().getTZOffset(date_start), 'minutes');
+ date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes');
if (this.mapping.all_day && evt[this.mapping.all_day]) {
date_stop.add(1, 'days');
@@ -622,6 +631,10 @@ return AbstractModel.extend({
// r.start = date_start.format('YYYY-MM-DD');
// r.end = date_stop.format('YYYY-MM-DD');
} else if (this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') {
+ // In month, FullCalendar gives the end day as the
+ // next day at midnight (instead of 23h59).
+ date_stop.add(1, 'days');
+
// allow to resize in month mode
r.reset_allday = r.allDay;
r.allDay = true;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index 5fb79cfa3a0..422258b4035 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -331,12 +331,15 @@ return AbstractRenderer.extend({
}
element.find('.fc-content .fc-time').text(display_hour);
},
+ // Dirty hack to ensure a correct first render
+ eventAfterAllRender: function (view) {
+ window.dispatchEvent(new Event('resize'));
+ },
height: 'parent',
unselectAuto: false,
});
this.$calendar.fullCalendar(fc_options);
- window.dispatchEvent(new Event('resize'));
},
/**
* Initialize the mini calendar in the sidebar
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 6870dbbbe24..91ada3a2629 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -107,7 +107,10 @@ var KanbanController = BasicController.extend({
* @returns {Deferred}
*/
_resequenceRecords: function (column_id, ids) {
- return this.model.resequence(this.modelName, ids, column_id);
+ var self = this;
+ return this.model.resequence(this.modelName, ids, column_id).then(function () {
+ self._updateEnv();
+ });
},
/**
* In grouped mode, set 'Create' button as btn-default if there is no column
@@ -181,6 +184,7 @@ var KanbanController = BasicController.extend({
.then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
}
},
@@ -218,6 +222,7 @@ var KanbanController = BasicController.extend({
this.model.loadMore(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
},
/**
@@ -325,9 +330,12 @@ var KanbanController = BasicController.extend({
* @param {OdooEvent} event
*/
_onResequenceColumn: function (event) {
+ var self = this;
var state = this.model.get(this.handle, {raw: true});
var model = state.fields[state.groupedBy[0]].relation;
- this.model.resequence(model, event.data.ids, this.handle);
+ this.model.resequence(model, event.data.ids, this.handle).then(function () {
+ self._updateEnv();
+ });
},
/**
* @param {OdooEvent} event
@@ -338,6 +346,7 @@ var KanbanController = BasicController.extend({
this.model.toggleGroup(column.db_id).then(function (db_id) {
var data = self.model.get(db_id);
self.renderer.updateColumn(db_id, data);
+ self._updateEnv();
});
},
/**
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 71402fc92ec..e21a1376e51 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -35,12 +35,8 @@ var KanbanModel = BasicModel.extend({
group.count++;
// update the res_ids and count of the parent
- var self = this;
- var parent = this.localData[group.parentID];
- parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
- return self.localData[dataPointID].res_ids;
- }));
- parent.count++;
+ this.localData[group.parentID].count++;
+ this._updateParentResIDs(group);
return this._fetchRecord(new_record).then(function (result) {
return result.id;
@@ -151,6 +147,7 @@ var KanbanModel = BasicModel.extend({
return this.notifyChanges(recordID, changes).then(function () {
return self.save(recordID);
}).then(function () {
+ var resID = self.localData[recordID].res_id;
// Remove record from its current group
var old_group;
for (var i = 0; i < parent.data.length; i++) {
@@ -159,11 +156,14 @@ var KanbanModel = BasicModel.extend({
if (index >= 0) {
old_group.data.splice(index, 1);
old_group.count--;
+ old_group.res_ids = _.without(old_group.res_ids, resID);
+ self._updateParentResIDs(old_group);
break;
}
}
// Add record to its new group
new_group.data.push(recordID);
+ new_group.res_ids.push(resID);
new_group.count++;
return [old_group.id, new_group.id];
});
@@ -201,6 +201,16 @@ var KanbanModel = BasicModel.extend({
data.data = _.sortBy(data.data, function (d) {
return _.indexOf(resIDs, self.localData[d].res_id);
});
+ data.res_ids = [];
+ _.each(data.data, function (d) {
+ var dataPoint = self.localData[d];
+ if (dataPoint.type === 'record') {
+ data.res_ids.push(dataPoint.res_id);
+ } else {
+ data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ }
+ });
+ self._updateParentResIDs(data);
return parentID;
});
},
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 6e69c0217b2..2976ab7926a 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -387,15 +387,15 @@ var ListRenderer = BasicRenderer.extend({
var $th = $('
')
.addClass('o_group_name')
.text(name + ' (' + group.count + ')');
- if (group.count > 0) {
- var $arrow = $('')
+ var $arrow = $('')
.css('padding-left', (groupLevel * 20) + 'px')
.css('padding-right', '5px')
- .addClass('fa')
- .toggleClass('fa-caret-right', !group.isOpen)
- .toggleClass('fa-caret-down', group.isOpen);
- $th.prepend($arrow);
+ .addClass('fa');
+ if (group.count > 0) {
+ $arrow.toggleClass('fa-caret-right', !group.isOpen)
+ .toggleClass('fa-caret-down', group.isOpen);
}
+ $th.prepend($arrow);
if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) {
var $pager = this._renderGroupPager(group);
var $lastCell = $cells[$cells.length-1];
diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js
index d38400c2e84..876c37fbd59 100644
--- a/addons/web/static/src/js/views/view_dialogs.js
+++ b/addons/web/static/src/js/views/view_dialogs.js
@@ -343,6 +343,7 @@ var SelectCreateDialog = ViewDialog.extend({
groupBy: searchResult.groupBy,
modelName: self.dataset.model,
hasSelectors: !self.options.disable_multiple_selection,
+ readonly: true,
}, self.options.list_view_options));
listView.setController(SelectCreateListController);
return listView.getController(self);
@@ -402,6 +403,7 @@ var SelectCreateDialog = ViewDialog.extend({
},
})).open();
dialog.on('closed', this, this.close.bind(this));
+ return dialog;
},
});
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index c11551ca2cc..a94b38f04b9 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -179,7 +179,7 @@ var DataExport = Dialog.extend({
var self = this;
this.exports.create({
name: value,
- resource: this.dataset.model,
+ resource: this.record.model,
export_fields: _.map(fields, function (field) {
return [0, 0, {name: field}];
}),
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index 912a30edb2c..0de80c8e3c6 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -348,7 +348,117 @@ DebugManager.include({
});
},
set_defaults: function() {
- this._active_view.controller.open_defaults_dialog();
+ var self = this;
+
+ var display = function (fieldInfo, value) {
+ var displayed = value;
+ if (value && fieldInfo.type === 'many2one') {
+ displayed = value.data.display_name;
+ value = value.data.id;
+ } else if (value && fieldInfo.type === 'selection') {
+ displayed = _.find(fieldInfo.selection, function (option) {
+ return option[0] === value;
+ })[1];
+ }
+ return [value, displayed];
+ };
+
+ var renderer = this._active_view.controller.renderer;
+ var fields = self._active_view.fields_view.fields;
+ var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
+ var fieldNamesInView = renderer.state.getFieldNames();
+ var fieldsValues = renderer.state.data;
+ var modifierDatas = {};
+ _.each(fieldNamesInView, function (fieldName) {
+ modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
+ return modifierdata.node.attrs.name === fieldName;
+ });
+ });
+ this.fields = _.chain(fieldNamesInView)
+ .map(function (fieldName) {
+ var modifierData = modifierDatas[fieldName];
+ var invisibleOrReadOnly;
+ if (modifierData) {
+ var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
+ invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
+ }
+ var fieldInfo = fields[fieldName];
+ var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
+ var value = valueDisplayed[0];
+ var displayed = valueDisplayed[1];
+ // ignore fields which are empty, invisible, readonly, o2m
+ // or m2m
+ if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
+ fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
+ fieldsInfo[fieldName].options.isPassword) {
+ return false;
+ }
+ return {
+ name: fieldName,
+ string: fieldInfo.string,
+ value: value,
+ displayed: displayed,
+ };
+ })
+ .compact()
+ .sortBy(function (field) { return field.string; })
+ .value();
+
+ var conditions = _.chain(fieldNamesInView)
+ .filter(function (fieldName) {
+ var fieldInfo = fields[fieldName];
+ return fieldInfo.change_default;
+ })
+ .map(function (fieldName) {
+ var fieldInfo = fields[fieldName];
+ var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
+ var value = valueDisplayed[0];
+ var displayed = valueDisplayed[1];
+ return {
+ name: fieldName,
+ string: fieldInfo.string,
+ value: value,
+ displayed: displayed,
+ };
+ })
+ .value();
+ var d = new Dialog(this, {
+ title: _t("Set Default"),
+ buttons: [
+ {text: _t("Close"), close: true},
+ {text: _t("Save default"), click: function () {
+ var $defaults = d.$el.find('#formview_default_fields');
+ var fieldToSet = $defaults.val();
+ if (!fieldToSet) {
+ $defaults.parent().addClass('o_form_invalid');
+ return;
+ }
+ var allUsers = d.$el.find('#formview_default_all').is(':checked');
+ var condition = d.$el.find('#formview_default_conditions').val();
+ var value = _.find(self.fields, function (field) {
+ return field.name === fieldToSet;
+ }).value;
+ self._rpc({
+ model: 'ir.values',
+ method: 'set_default',
+ args: [
+ self._active_view.fields_view.model,
+ fieldToSet,
+ value,
+ allUsers,
+ true,
+ condition || false,
+ ],
+ }).done(function () { d.close(); });
+ }}
+ ]
+ });
+ d.args = {
+ fields: this.fields,
+ conditions: conditions,
+ };
+ d.template = 'FormView.set_default';
+ d.open();
},
fvg: function() {
var dialog = new Dialog(this, { title: _t("Fields View Get") }).open();
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 9d4b3326c22..1c633f30100 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -339,7 +339,7 @@
|
- |
-
+
|