diff --git a/addons/pos_sale/models/crm_team.py b/addons/pos_sale/models/crm_team.py
index 7c5b5d602a0..ffbfcd026c6 100644
--- a/addons/pos_sale/models/crm_team.py
+++ b/addons/pos_sale/models/crm_team.py
@@ -3,6 +3,7 @@
from odoo import api, fields, models, _
from datetime import datetime
+import pytz
class CrmTeam(models.Model):
@@ -41,13 +42,16 @@ class CrmTeam(models.Model):
""" If the type of the sales team is point of sale ('pos'), the graph will display the sales data.
The override here is to get data from pos.order instead of sale.order.
"""
+ offset = datetime.now(pytz.timezone(self.env.user.tz or 'UTC')).utcoffset()
+ min_date = fields.Datetime.to_string(datetime.combine(start_date, datetime.min.time()) - offset)
+ max_date = fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()) - offset)
if self.team_type == 'pos':
result = []
if self.dashboard_graph_group_pos == 'pos':
order_data = self.env['report.pos.order'].read_group(
domain=[
- ('date', '>=', fields.Date.to_string(start_date)),
- ('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
+ ('date', '>=', min_date),
+ ('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['config_id', 'price_total'],
@@ -63,8 +67,8 @@ class CrmTeam(models.Model):
elif self.dashboard_graph_group_pos == 'user':
order_data = self.env['report.pos.order'].read_group(
domain=[
- ('date', '>=', fields.Date.to_string(start_date)),
- ('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
+ ('date', '>=', min_date),
+ ('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['user_id', 'price_total'],
@@ -78,8 +82,8 @@ class CrmTeam(models.Model):
# /!\ do not use en_US as it's not ISO-standard and does not match datetime's library
order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group(
domain=[
- ('date', '>=', fields.Date.to_string(start_date)),
- ('date', '<=', fields.Datetime.to_string(datetime.combine(end_date, datetime.max.time()))),
+ ('date', '>=', min_date),
+ ('date', '<=', max_date),
('config_id', 'in', self.pos_config_ids.ids),
('state', 'in', ['paid', 'done', 'invoiced'])],
fields=['date', 'price_total'],
diff --git a/addons/project/__manifest__.py b/addons/project/__manifest__.py
index 838fe9b832c..cb4516c221a 100644
--- a/addons/project/__manifest__.py
+++ b/addons/project/__manifest__.py
@@ -22,7 +22,6 @@
'data': [
'security/project_security.xml',
'security/ir.model.access.csv',
- 'data/project_data.xml',
'report/project_report_views.xml',
'views/rating_views.xml',
'views/project_views.xml',
@@ -34,6 +33,7 @@
'views/project_rating_templates.xml',
'views/res_users_views.xml',
'data/project_mail_template_data.xml',
+ 'data/project_data.xml',
],
'qweb': ['static/src/xml/project.xml'],
'demo': ['data/project_demo.xml'],
diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py
index 4ef9dad4bda..22ea62df7cd 100644
--- a/addons/purchase_stock/models/stock.py
+++ b/addons/purchase_stock/models/stock.py
@@ -16,7 +16,7 @@ class StockMove(models.Model):
_inherit = 'stock.move'
purchase_line_id = fields.Many2one('purchase.order.line',
- 'Purchase Order Line', ondelete='set null', index=True, readonly=True, copy=False)
+ 'Purchase Order Line', ondelete='set null', index=True, readonly=True)
created_purchase_line_id = fields.Many2one('purchase.order.line',
'Created Purchase Order Line', ondelete='set null', readonly=True, copy=False)
diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py
index 4542116c47c..f3708fc5b10 100644
--- a/addons/sales_team/models/crm_team.py
+++ b/addons/sales_team/models/crm_team.py
@@ -209,7 +209,7 @@ class CrmTeam(models.Model):
self.ensure_one()
values = []
- today = date.today()
+ today = fields.Date.from_string(fields.Date.context_today(self))
start_date, end_date = self._graph_get_dates(today)
graph_data = self._graph_data(start_date, end_date)
diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py
index 017d067267a..58835e4b675 100644
--- a/addons/survey/models/survey.py
+++ b/addons/survey/models/survey.py
@@ -727,7 +727,6 @@ class SurveyUserInput(models.Model):
_sql_constraints = [
('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
- ('deadline_in_the_past', 'CHECK (deadline >= date_create)', 'The deadline cannot be in the past')
]
@api.model
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index c86411b6cd9..deccd3d7709 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1593,6 +1593,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
this.$('form.o_form_binary_form').submit();
this.$('.oe_fileupload').hide();
+ ev.target.value = "";
},
/**
* @private
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 9d509dd703c..4b4a4a86c16 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -825,7 +825,8 @@ var BasicModel = AbstractModel.extend({
return this.mutex.exec(this._applyChange.bind(this, record_id, changes, options));
},
/**
- * Reload all data for a given resource
+ * Reload all data for a given resource. At any time there is at most one
+ * reload operation active.
*
* @param {string} id local id for a resource
* @param {Object} [options]
@@ -834,64 +835,7 @@ var BasicModel = AbstractModel.extend({
* @returns {Deferred
} resolves to the id of the resource
*/
reload: function (id, options) {
- options = options || {};
- var element = this.localData[id];
-
- if (element.type === 'record') {
- if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
- var params = {
- context: element.context,
- fieldsInfo: element.fieldsInfo,
- fields: element.fields,
- viewType: element.viewType,
- };
- return this._makeDefaultRecord(element.model, params);
- }
- if (!options.keepChanges) {
- this.discardChanges(id, {rollback: false});
- }
- } else if (element._changes) {
- delete element.tempLimitIncrement;
- _.each(element._changes, function (change) {
- delete change.isNew;
- });
- }
-
- if (options.context !== undefined) {
- element.context = options.context;
- }
- if (options.domain !== undefined) {
- element.domain = options.domain;
- }
- if (options.groupBy !== undefined) {
- element.groupedBy = options.groupBy;
- }
- if (options.limit !== undefined) {
- element.limit = options.limit;
- }
- if (options.offset !== undefined) {
- this._setOffset(element.id, options.offset);
- }
- if (options.loadMoreOffset !== undefined) {
- element.loadMoreOffset = options.loadMoreOffset;
- } else {
- // reset if not specified
- element.loadMoreOffset = 0;
- }
- if (options.currentId !== undefined) {
- element.res_id = options.currentId;
- }
- if (options.ids !== undefined) {
- element.res_ids = options.ids;
- element.count = element.res_ids.length;
- }
- if (element.type === 'record') {
- element.offset = _.indexOf(element.res_ids, element.res_id);
- }
- var loadOptions = _.pick(options, 'fieldNames', 'viewType');
- return this._load(element, loadOptions).then(function (result) {
- return result.id;
- });
+ return this.mutex.exec(this._reload.bind(this, id, options));
},
/**
* In some case, we may need to remove an element from a list, without going
@@ -2934,13 +2878,10 @@ var BasicModel = AbstractModel.extend({
continue;
}
changes = this._generateChanges(relRecord, options);
- if (changes.id) {
+ if (!this.isNew(relRecord.id)) {
// the subrecord already exists in db
+ commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
delete changes.id;
- if (this.isNew(record.id)) {
- // if the main record is new, link the subrecord to it
- commands[fieldName].push(x2ManyCommands.link_to(relRecord.res_id));
- }
if (!_.isEmpty(changes)) {
commands[fieldName].push(x2ManyCommands.update(relRecord.res_id, changes));
}
@@ -4045,6 +3986,76 @@ var BasicModel = AbstractModel.extend({
});
});
},
+ /**
+ * Reload all data for a given resource
+ *
+ * @private
+ * @param {string} id local id for a resource
+ * @param {Object} [options]
+ * @param {boolean} [options.keepChanges=false] if true, doesn't discard the
+ * changes on the record before reloading it
+ * @returns {Deferred} resolves to the id of the resource
+ */
+ _reload: function (id, options) {
+ options = options || {};
+ var element = this.localData[id];
+
+ if (element.type === 'record') {
+ if (!options.currentId && (('currentId' in options) || this.isNew(id))) {
+ var params = {
+ context: element.context,
+ fieldsInfo: element.fieldsInfo,
+ fields: element.fields,
+ viewType: element.viewType,
+ };
+ return this._makeDefaultRecord(element.model, params);
+ }
+ if (!options.keepChanges) {
+ this.discardChanges(id, {rollback: false});
+ }
+ } else if (element._changes) {
+ delete element.tempLimitIncrement;
+ _.each(element._changes, function (change) {
+ delete change.isNew;
+ });
+ }
+
+ if (options.context !== undefined) {
+ element.context = options.context;
+ }
+ if (options.domain !== undefined) {
+ element.domain = options.domain;
+ }
+ if (options.groupBy !== undefined) {
+ element.groupedBy = options.groupBy;
+ }
+ if (options.limit !== undefined) {
+ element.limit = options.limit;
+ }
+ if (options.offset !== undefined) {
+ this._setOffset(element.id, options.offset);
+ }
+ if (options.loadMoreOffset !== undefined) {
+ element.loadMoreOffset = options.loadMoreOffset;
+ } else {
+ // reset if not specified
+ element.loadMoreOffset = 0;
+ }
+ if (options.currentId !== undefined) {
+ element.res_id = options.currentId;
+ }
+ if (options.ids !== undefined) {
+ element.res_ids = options.ids;
+ element.count = element.res_ids.length;
+ }
+ if (element.type === 'record') {
+ element.offset = _.indexOf(element.res_ids, element.res_id);
+ }
+ var loadOptions = _.pick(options, 'fieldNames', 'viewType');
+ return this._load(element, loadOptions).then(function (result) {
+ return result.id;
+ });
+ },
/**
* Allows to save a value in the specialData cache associated to a given
* record and fieldName. If the value in the cache was already the given
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 0cf7c43618e..b232a9bc4b9 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -3236,6 +3236,136 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('onchange for embedded one2many in a one2many with a second page', function (assert) {
+ assert.expect(1);
+
+ this.data.turtle.fields.partner_ids.type = 'one2many';
+ this.data.turtle.records[0].partner_ids = [1];
+ // we need a second page, so we set two records and only display one per page
+ this.data.partner.records[0].turtles = [1, 2];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 1, {
+ turtle_foo: "hop",
+ partner_ids: [[5], [4, 1]],
+ }],
+ [1, 2, {
+ turtle_foo: "blip",
+ partner_ids: [[5], [4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ var expectedResultTurtles = [
+ [1, 1, {
+ turtle_foo: "hop",
+ }],
+ [1, 2, {
+ partner_ids: [[4, 2, false], [4, 4, false]],
+ turtle_foo: "blip",
+ }],
+ ];
+ assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
+ "the right values should be written");
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_cell').eq(1).click();
+ var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
+ $cell.val("hop").trigger('change');
+ form.$buttons.find('.o_form_button_save').click();
+
+ form.destroy();
+ });
+
+ QUnit.test('onchange for embedded one2many in a one2many updated by server', function (assert) {
+ // here we test that after an onchange, the embedded one2many field has
+ // been updated by a new list of ids by the server response, to this new
+ // list should be correctly sent back at save time
+ assert.expect(3);
+
+ this.data.turtle.fields.partner_ids.type = 'one2many';
+ this.data.partner.records[0].turtles = [2];
+ this.data.turtle.records[1].partner_ids = [2];
+
+ this.data.partner.onchanges = {
+ turtles: function (obj) {
+ obj.turtles = [
+ [5],
+ [1, 2, {
+ turtle_foo: "hop",
+ partner_ids: [[5], [4, 2], [4, 4]],
+ }],
+ ];
+ },
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === '/web/dataset/call_kw/partner/write') {
+ var expectedResultTurtles = [
+ [1, 2, {
+ partner_ids: [[4, 2, false], [4, 4, false]],
+ turtle_foo: "hop",
+ }],
+ ];
+ assert.deepEqual(args.args[1].turtles, expectedResultTurtles,
+ 'The right values should be written');
+ }
+ return this._super.apply(this, arguments);
+ }
+ });
+
+ assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim(), "second record",
+ "the partner_ids should be as specified at initialization");
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('.o_data_cell').eq(1).click();
+ var $cell = form.$('.o_selected_row .o_input[name=turtle_foo]');
+ $cell.val("hop").trigger("change");
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.deepEqual(form.$('.o_many2many_tags_cell').text().trim().split(/\s+/),
+ [ "second", "record", "aaa" ],
+ 'The partner_ids should have been updated');
+
+ form.destroy();
+ });
+
QUnit.test('onchange for embedded one2many with handle widget', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 5afdc3dd447..5e82766b051 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -237,7 +237,6 @@ var MockServer = Class.extend({
if (attrs) {
attrs = pyeval.py_eval(attrs);
_.extend(modifiers, attrs);
- node.removeAttribute('attrs');
}
var states = node.getAttribute('states');
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 04c1c603118..57dfd96e288 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -3633,6 +3633,56 @@ QUnit.module('Views', {
testUtils.unpatch(mixins.ParentedMixin);
});
+ QUnit.test('grouped kanban becomes ungrouped when clearing domain then clearing groupby', function (assert) {
+ // in this test, we simulate that clearing the domain is slow, so that
+ // clearing the groupby does not corrupt the data handled while
+ // reloading the kanban view.
+ assert.expect(4);
+
+ var def = $.Deferred();
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '' +
+ '
' +
+ '',
+ domain: [['foo', '=', 'norecord']],
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ var result = this._super(route, args);
+ if (args.method === 'read_group') {
+ var isFirstUpdate = _.isEmpty(args.kwargs.domain) &&
+ args.kwargs.groupby &&
+ args.kwargs.groupby[0] === 'bar';
+ if (isFirstUpdate) {
+ return def.then(_.constant(result));
+ }
+ }
+ return result;
+ },
+ });
+
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
+ "the kanban view should be grouped");
+ assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
+ "the kanban view should not be ungrouped");
+
+ kanban.update({domain: []}); // 1st update on kanban view
+ kanban.update({groupBy: false}); // 2n update on kanban view
+ def.resolve(); // simulate slow 1st update of kanban view
+
+ assert.notOk(kanban.$('.o_kanban_view').hasClass('o_kanban_grouped'),
+ "the kanban view should not longer be grouped");
+ assert.ok(kanban.$('.o_kanban_view').hasClass('o_kanban_ungrouped'),
+ "the kanban view should have become ungrouped");
+
+ kanban.destroy();
+ });
+
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
assert.expect(3);
diff --git a/addons/website/static/src/js/menu/new_content.js b/addons/website/static/src/js/menu/new_content.js
index 127338f05a6..c577858f3eb 100644
--- a/addons/website/static/src/js/menu/new_content.js
+++ b/addons/website/static/src/js/menu/new_content.js
@@ -15,6 +15,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
}),
events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
'click > a': '_onMenuToggleClick',
+ 'click > #o_new_content_menu_choices': '_onBackgroundClick',
}),
/**
@@ -73,6 +74,15 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
ev.preventDefault();
this.$newContentMenuChoices.toggleClass('o_hidden');
},
+ /**
+ * Called when a click outside the menu's options occurs -> Closes the menu
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onBackgroundClick: function (ev) {
+ this.$newContentMenuChoices.addClass('o_hidden');
+ },
});
websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu');
diff --git a/doc/cla/individual/maulik-raval.md b/doc/cla/individual/maulik-raval.md
new file mode 100644
index 00000000000..78d77ea9afe
--- /dev/null
+++ b/doc/cla/individual/maulik-raval.md
@@ -0,0 +1,11 @@
+India, 2018-06-27
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Maulik Raval maulik.raval502@gmail.com https://github.com/maulik-raval
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 02f48a00c86..efb991167f3 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -412,6 +412,12 @@ actual arch.
self.clear_caches()
return super(View, self).write(self._compute_defaults(vals))
+ def unlink(self):
+ # if in uninstall mode and has children views, emulate an ondelete cascade
+ if self.env.context.get('_force_unlink', False) and self.mapped('inherit_children_ids'):
+ self.mapped('inherit_children_ids').unlink()
+ super(View, self).unlink()
+
@api.multi
def toggle(self):
""" Switches between enabled and disabled statuses
diff --git a/odoo/addons/base/views/ir_model_views.xml b/odoo/addons/base/views/ir_model_views.xml
index 27699e330df..b20c49a413d 100644
--- a/odoo/addons/base/views/ir_model_views.xml
+++ b/odoo/addons/base/views/ir_model_views.xml
@@ -9,7 +9,7 @@