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))