diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js
index a79c7500669..898105f3a6e 100644
--- a/addons/bus/static/src/js/bus.js
+++ b/addons/bus/static/src/js/bus.js
@@ -76,7 +76,8 @@ bus.Bus = Widget.extend({
this.last_partners_presence_check = now;
}
var data = {channels: self.channels, last: self.last, options: options};
- session.rpc('/longpolling/poll', data, {shadow : true}).then(function(result) {
+ // The backend has a maximum cycle time of 50 seconds so give +10 seconds
+ session.rpc('/longpolling/poll', data, {shadow : true, timeout: 60000}).then(function(result) {
self.on_notification(result);
if(!self.stop){
self.poll();
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 0d34eec4505..78467cc43a8 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -415,7 +415,7 @@ class AlarmManager(models.AbstractModel):
result = False
if alarm.type == 'email':
- result = meeting.attendee_ids._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True)
+ result = meeting.attendee_ids.filtered(lambda r: r.state != 'declined')._send_mail_to_attendees('calendar.calendar_template_meeting_reminder', force_send=True)
return result
def do_notif_reminder(self, alert):
diff --git a/addons/event/models/event.py b/addons/event/models/event.py
index 584a0e991db..d455fac5941 100644
--- a/addons/event/models/event.py
+++ b/addons/event/models/event.py
@@ -319,7 +319,7 @@ class EventEvent(models.Model):
self.state = 'confirm'
@api.one
- def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: True):
+ def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'):
for attendee in self.registration_ids.filtered(filter_func):
self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send)
diff --git a/addons/sales_team/models/crm_team.py b/addons/sales_team/models/crm_team.py
index 67cec58b761..5d9bf2b5787 100644
--- a/addons/sales_team/models/crm_team.py
+++ b/addons/sales_team/models/crm_team.py
@@ -225,7 +225,7 @@ class CrmTeam(models.Model):
values[index][y_field] = data_item.get('y_value')
elif self.dashboard_graph_group == 'week':
- weeks_in_start_year = int(date(start_date.year, 12, 31).isocalendar()[1])
+ weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years
for week in range(0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1):
short_name = get_week_name(start_date + relativedelta(days=7 * week), locale)
values.append({x_field: short_name, y_field: 0})
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 4d58c0fab74..385f7bc1d24 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1352,7 +1352,7 @@ class ExportFormat(object):
model, fields, ids, domain, import_compat = \
operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params)
- Model = request.env[model].with_context(**params.get('context', {}))
+ Model = request.env[model].with_context(import_compat=import_compat, **params.get('context', {}))
records = Model.browse(ids) or Model.search(domain, offset=0, limit=False, order=False)
if not Model._is_an_ordinary_table():
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 2ea3b14b654..8d0dd5a174b 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -104,12 +104,15 @@
-
-
+
+
+
+
+
diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py
index 087b69e8499..927ede499be 100644
--- a/addons/website_event/models/event.py
+++ b/addons/website_event/models/event.py
@@ -32,8 +32,8 @@ class Event(models.Model):
website_menu = fields.Boolean(
'Dedicated Menu', compute='_compute_website_menu', inverse='_set_website_menu',
help="Creates menus Introduction, Location and Register on the page "
- " of the event on the website.", store=True, copy=False)
- menu_id = fields.Many2one('website.menu', 'Event Menu')
+ " of the event on the website.", store=True)
+ menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)
def _compute_is_participating(self):
# we don't allow public user to see participating label
diff --git a/doc/cla/individual/willmakenoise.md b/doc/cla/individual/willmakenoise.md
new file mode 100644
index 00000000000..3c8ac824146
--- /dev/null
+++ b/doc/cla/individual/willmakenoise.md
@@ -0,0 +1,10 @@
+Norway, 2018-02-08
+
+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,
+Marius Stedjan marius@stedjan.com https://github.com/willmakenoise
diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst
index b8881e9a935..6e08973d60e 100644
--- a/doc/reference/cmdline.rst
+++ b/doc/reference/cmdline.rst
@@ -54,7 +54,7 @@ Running the server
Maximum allowed virtual memory per worker. If the limit is exceeded,
the worker is killed and recycled at the end of the current request.
- Defaults to 640MB.
+ Defaults to 2048MB.
.. option:: --limit-memory-hard
@@ -62,7 +62,7 @@ Running the server
immediately killed without waiting for the end of the current request
processing.
- Defaults to 768MB.
+ Defaults to 2560MB.
.. option:: --limit-time-cpu
diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py
index 74f62bf7189..974d20e223f 100644
--- a/odoo/addons/test_impex/tests/test_export.py
+++ b/odoo/addons/test_impex/tests/test_export.py
@@ -600,7 +600,54 @@ class test_m2m(CreatorCase):
['', u'export.many2many.other:13'],
])
- # essentially same as o2m, so boring
+ def test_multiple_records_subfield(self):
+ r = self.make(self.commands)
+ xid = self.env['ir.model.data'].create({
+ 'name': 'whopwhopwhop',
+ 'module': '__t__',
+ 'model': r._name,
+ 'res_id': r.id,
+ }).complete_name
+ sids = [
+ self.env['ir.model.data'].create({
+ 'name': sub.str,
+ 'module': '__t__',
+ 'model': sub._name,
+ 'res_id': sub.id,
+ }).complete_name
+ for sub in r.value
+ ]
+ r.invalidate_cache()
+
+ self.assertEqual(
+ r._export_rows([['value', 'id']]),
+ [['__t__.record000,__t__.record001,__t__.record010,__t__.record011,__t__.record100']]
+ )
+ self.assertEqual(
+ r.with_context(import_compat=True)._export_rows([['value', 'id']]),
+ [['__t__.record000,__t__.record001,__t__.record010,__t__.record011,__t__.record100']]
+ )
+
+ self.assertEqual(
+ r.with_context(import_compat=False)._export_rows([['id'], ['value', 'id'], ['value', 'value']]),
+ [
+ [xid, u'__t__.record000', u'4'],
+ [u'', u'__t__.record001', u'42'],
+ [u'', u'__t__.record010', u'36'],
+ [u'', u'__t__.record011', u'4'],
+ [u'', u'__t__.record100', u'13']
+ ]
+ )
+ self.assertEqual(
+ r.with_context(import_compat=False)._export_rows([['id'], ['value', 'value'], ['value', 'id']]),
+ [
+ [xid, u'4', u'__t__.record000'],
+ [u'', u'42', u'__t__.record001'],
+ [u'', u'36', u'__t__.record010'],
+ [u'', u'4', u'__t__.record011'],
+ [u'', u'13', u'__t__.record100']
+ ]
+ )
class test_function(CreatorCase):
diff --git a/odoo/models.py b/odoo/models.py
index ff7d8dd0e06..29e0f29a9d0 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -646,6 +646,7 @@ class BaseModel(object):
:param fields: list of lists of fields to traverse
:return: list of lists of corresponding values
"""
+ import_compatible = self.env.context.get('import_compat', True)
lines = []
for record in self:
# main line of record, initially empty
@@ -679,8 +680,9 @@ class BaseModel(object):
else:
primary_done.append(name)
- # This is a special case, its strange behavior is intended!
- if field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
+ # in import_compat mode, m2m should always be exported as
+ # a comma-separated list of xids in a single cell
+ if import_compatible and field.type == 'many2many' and len(path) > 1 and path[1] == 'id':
xml_ids = [r.__export_xml_id() for r in value]
current[i] = ','.join(xml_ids) or False
continue
@@ -2852,51 +2854,52 @@ class BaseModel(object):
raise UserError(_('Unable to delete this document because it is used as a default property'))
# Delete the records' properties.
- self.env['ir.property'].search([('res_id', 'in', refs)]).unlink()
+ with self.env.norecompute():
+ self.env['ir.property'].search([('res_id', 'in', refs)]).unlink()
- self.check_access_rule('unlink')
+ self.check_access_rule('unlink')
- cr = self._cr
- Data = self.env['ir.model.data'].sudo().with_context({})
- Values = self.env['ir.values']
- Attachment = self.env['ir.attachment']
+ cr = self._cr
+ Data = self.env['ir.model.data'].sudo().with_context({})
+ Values = self.env['ir.values']
+ Attachment = self.env['ir.attachment']
- for sub_ids in cr.split_for_in_conditions(self.ids):
- query = "DELETE FROM %s WHERE id IN %%s" % self._table
- cr.execute(query, (sub_ids,))
+ for sub_ids in cr.split_for_in_conditions(self.ids):
+ query = "DELETE FROM %s WHERE id IN %%s" % self._table
+ cr.execute(query, (sub_ids,))
- # Removing the ir_model_data reference if the record being deleted
- # is a record created by xml/csv file, as these are not connected
- # with real database foreign keys, and would be dangling references.
- #
- # Note: the following steps are performed as superuser to avoid
- # access rights restrictions, and with no context to avoid possible
- # side-effects during admin calls.
- data = Data.search([('model', '=', self._name), ('res_id', 'in', sub_ids)])
- if data:
- data.unlink()
+ # Removing the ir_model_data reference if the record being deleted
+ # is a record created by xml/csv file, as these are not connected
+ # with real database foreign keys, and would be dangling references.
+ #
+ # Note: the following steps are performed as superuser to avoid
+ # access rights restrictions, and with no context to avoid possible
+ # side-effects during admin calls.
+ data = Data.search([('model', '=', self._name), ('res_id', 'in', sub_ids)])
+ if data:
+ data.unlink()
- # For the same reason, remove the relevant records in ir_values
- refs = ['%s,%s' % (self._name, i) for i in sub_ids]
- values = Values.search(['|', ('value', 'in', refs),
- '&', ('model', '=', self._name),
- ('res_id', 'in', sub_ids)])
- if values:
- values.unlink()
+ # For the same reason, remove the relevant records in ir_values
+ refs = ['%s,%s' % (self._name, i) for i in sub_ids]
+ values = Values.search(['|', ('value', 'in', refs),
+ '&', ('model', '=', self._name),
+ ('res_id', 'in', sub_ids)])
+ if values:
+ values.unlink()
- # For the same reason, remove the relevant records in ir_attachment
- # (the search is performed with sql as the search method of
- # ir_attachment is overridden to hide attachments of deleted
- # records)
- query = 'SELECT id FROM ir_attachment WHERE res_model=%s AND res_id IN %s'
- cr.execute(query, (self._name, sub_ids))
- attachments = Attachment.browse([row[0] for row in cr.fetchall()])
- if attachments:
- attachments.unlink()
+ # For the same reason, remove the relevant records in ir_attachment
+ # (the search is performed with sql as the search method of
+ # ir_attachment is overridden to hide attachments of deleted
+ # records)
+ query = 'SELECT id FROM ir_attachment WHERE res_model=%s AND res_id IN %s'
+ cr.execute(query, (self._name, sub_ids))
+ attachments = Attachment.browse([row[0] for row in cr.fetchall()])
+ if attachments:
+ attachments.unlink()
- # invalidate the *whole* cache, since the orm does not handle all
- # changes made in the database, like cascading delete!
- self.invalidate_cache()
+ # invalidate the *whole* cache, since the orm does not handle all
+ # changes made in the database, like cascading delete!
+ self.invalidate_cache()
# recompute new-style fields
if self.env.recompute and self._context.get('recompute', True):
diff --git a/odoo/service/wsgi_server.py b/odoo/service/wsgi_server.py
index 7dc5c0464e6..16772f320a8 100644
--- a/odoo/service/wsgi_server.py
+++ b/odoo/service/wsgi_server.py
@@ -63,7 +63,7 @@ def xmlrpc_return(start_response, service, method, params, string_faultcode=Fals
def xmlrpc_handle_exception_int(e):
if isinstance(e, odoo.exceptions.UserError):
- fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, odoo.tools.ustr(e.value))
+ fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, odoo.tools.ustr(e.name))
response = xmlrpclib.dumps(fault, allow_none=False, encoding=None)
elif isinstance(e, odoo.exceptions.RedirectWarning):
fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e))