[MERGE] forward port of branch saas-3 up to fdc6271
[odoo/odoo.git] / addons / mail / mail_thread.py
index 0538ad7..3445772 100644 (file)
@@ -20,6 +20,7 @@
 ##############################################################################
 
 import base64
+from collections import OrderedDict
 import datetime
 import dateutil
 import email
@@ -35,6 +36,7 @@ import time
 import xmlrpclib
 import re
 from email.message import Message
+from urllib import urlencode
 
 from openerp import tools
 from openerp import SUPERUSER_ID
@@ -99,6 +101,9 @@ class mail_thread(osv.AbstractModel):
     #   :param function lambda: returns whether the tracking should record using this subtype
     _track = {}
 
+    # Mass mailing feature
+    _mail_mass_mailing = False
+
     def get_empty_list_help(self, cr, uid, help, context=None):
         """ Override of BaseModel.get_empty_list_help() to generate an help message
             that adds alias information. """
@@ -117,9 +122,9 @@ class mail_thread(osv.AbstractModel):
                     object_id.alias_id.alias_model_id.model == self._name and \
                     object_id.alias_id.alias_force_thread_id == 0:
                 alias = object_id.alias_id
-        elif catchall_domain and model:  # no specific res_id given -> generic help message, take an example alias (i.e. alias of some section_id)
+        if not alias and catchall_domain and model:  # no res_id or res_id not linked to an alias -> generic help message, take a generic alias of the model
             alias_obj = self.pool.get('mail.alias')
-            alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False)], context=context, order='id ASC')
+            alias_ids = alias_obj.search(cr, uid, [("alias_parent_model_id.model", "=", model), ("alias_name", "!=", False), ('alias_force_thread_id', '=', False), ('alias_parent_thread_id', '=', False)], context=context, order='id ASC')
             if alias_ids and len(alias_ids) == 1:
                 alias = alias_obj.browse(cr, uid, alias_ids[0], context=context)
 
@@ -190,8 +195,17 @@ class mail_thread(osv.AbstractModel):
 
         # find current model subtypes, add them to a dictionary
         subtype_obj = self.pool.get('mail.message.subtype')
-        subtype_ids = subtype_obj.search(cr, uid, ['|', ('res_model', '=', self._name), ('res_model', '=', False)], context=context)
-        subtype_dict = dict((subtype.name, dict(default=subtype.default, followed=False, id=subtype.id)) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
+        subtype_ids = subtype_obj.search(
+            cr, uid, [
+                '&', ('hidden', '=', False), '|', ('res_model', '=', self._name), ('res_model', '=', False)
+            ], context=context)
+        subtype_dict = OrderedDict(
+            (subtype.name, {
+                'default': subtype.default,
+                'followed': False,
+                'parent_model': subtype.parent_id and subtype.parent_id.res_model or self._name,
+                'id': subtype.id}
+            ) for subtype in subtype_obj.browse(cr, uid, subtype_ids, context=context))
         for id in ids:
             res[id]['message_subtype_data'] = subtype_dict.copy()
 
@@ -301,6 +315,8 @@ class mail_thread(osv.AbstractModel):
             auto_join=True,
             string='Messages',
             help="Messages and communication history"),
+        'message_last_post': fields.datetime('Last Message Date',
+            help='Date of the last message posted on the record.'),
         'message_unread': fields.function(_get_message_data,
             fnct_search=_search_message_unread, multi="_get_message_data",
             type='boolean', string='Unread Messages',
@@ -347,17 +363,17 @@ class mail_thread(osv.AbstractModel):
         if context is None:
             context = {}
 
+        if context.get('tracking_disable'):
+            return super(mail_thread, self).create(
+                cr, uid, values, context=context)
+
         # subscribe uid unless asked not to
         if not context.get('mail_create_nosubscribe'):
             pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid).partner_id.id
             message_follower_ids = values.get('message_follower_ids') or []  # webclient can send None or False
             message_follower_ids.append([4, pid])
             values['message_follower_ids'] = message_follower_ids
-            # add operation to ignore access rule checking for subscription
-            context_operation = dict(context, operation='create')
-        else:
-            context_operation = context
-        thread_id = super(mail_thread, self).create(cr, uid, values, context=context_operation)
+        thread_id = super(mail_thread, self).create(cr, uid, values, context=context)
 
         # automatic logging unless asked not to (mainly for various testing purpose)
         if not context.get('mail_create_nolog'):
@@ -380,7 +396,7 @@ class mail_thread(osv.AbstractModel):
         if not context.get('mail_notrack'):
             tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
             if tracked_fields:
-                initial_values = {thread_id: dict((item, False) for item in tracked_fields)}
+                initial_values = {thread_id: dict.fromkeys(tracked_fields, False)}
                 self.message_track(cr, uid, [thread_id], tracked_fields, initial_values, context=track_ctx)
         return thread_id
 
@@ -389,26 +405,31 @@ class mail_thread(osv.AbstractModel):
             context = {}
         if isinstance(ids, (int, long)):
             ids = [ids]
+        if context.get('tracking_disable'):
+            return super(mail_thread, self).write(
+                cr, uid, ids, values, context=context)
         # Track initial values of tracked fields
         track_ctx = dict(context)
         if 'lang' not in track_ctx:
             track_ctx['lang'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).lang
-        tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
+
+        tracked_fields = None
+        if not context.get('mail_notrack'):
+            tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
+
         if tracked_fields:
             records = self.browse(cr, uid, ids, context=track_ctx)
-            initial_values = dict((this.id, dict((key, getattr(this, key)) for key in tracked_fields.keys())) for this in records)
+            initial_values = dict((record.id, dict((key, getattr(record, key)) for key in tracked_fields))
+                                  for record in records)
 
         # Perform write, update followers
         result = super(mail_thread, self).write(cr, uid, ids, values, context=context)
         self.message_auto_subscribe(cr, uid, ids, values.keys(), context=context, values=values)
 
-        if not context.get('mail_notrack'):
-            # Perform the tracking
-            tracked_fields = self._get_tracked_fields(cr, uid, values.keys(), context=track_ctx)
-        else:
-            tracked_fields = None
+        # Perform the tracking
         if tracked_fields:
             self.message_track(cr, uid, ids, tracked_fields, initial_values, context=track_ctx)
+
         return result
 
     def unlink(self, cr, uid, ids, context=None):
@@ -445,14 +466,15 @@ class mail_thread(osv.AbstractModel):
             :return list: a list of (field_name, column_info obj), containing
                 always tracked fields and modified on_change fields
         """
-        lst = []
+        tracked_fields = []
         for name, column_info in self._all_columns.items():
             visibility = getattr(column_info.column, 'track_visibility', False)
             if visibility == 'always' or (visibility == 'onchange' and name in updated_fields) or name in self._track:
-                lst.append(name)
-        if not lst:
-            return lst
-        return self.fields_get(cr, uid, lst, context=context)
+                tracked_fields.append(name)
+
+        if tracked_fields:
+            return self.fields_get(cr, uid, tracked_fields, context=context)
+        return {}
 
     def message_track(self, cr, uid, ids, tracked_fields, initial_values, context=None):
 
@@ -580,23 +602,6 @@ class mail_thread(osv.AbstractModel):
         model_obj.check_access_rights(cr, uid, check_operation)
         model_obj.check_access_rule(cr, uid, mids, check_operation, context=context)
 
-    def _get_formview_action(self, cr, uid, id, model=None, context=None):
-        """ Return an action to open the document. This method is meant to be
-            overridden in addons that want to give specific view ids for example.
-
-            :param int id: id of the document to open
-            :param string model: specific model that overrides self._name
-        """
-        return {
-                'type': 'ir.actions.act_window',
-                'res_model': model or self._name,
-                'view_type': 'form',
-                'view_mode': 'form',
-                'views': [(False, 'form')],
-                'target': 'current',
-                'res_id': id,
-            }
-
     def _get_inbox_action_xml_id(self, cr, uid, context=None):
         """ When redirecting towards the Inbox, choose which action xml_id has
             to be fetched. This method is meant to be inherited, at least in portal
@@ -639,10 +644,7 @@ class mail_thread(osv.AbstractModel):
             if model_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
                 try:
                     model_obj.check_access_rule(cr, uid, [res_id], 'read', context=context)
-                    if not hasattr(model_obj, '_get_formview_action'):
-                        action = self.pool.get('mail.thread')._get_formview_action(cr, uid, res_id, model=model, context=context)
-                    else:
-                        action = model_obj._get_formview_action(cr, uid, res_id, context=context)
+                    action = model_obj.get_formview_action(cr, uid, res_id, context=context)
                 except (osv.except_osv, orm.except_orm):
                     pass
             action.update({
@@ -653,19 +655,60 @@ class mail_thread(osv.AbstractModel):
             })
         return action
 
+    def _get_access_link(self, cr, uid, mail, partner, context=None):
+        # the parameters to encode for the query and fragment part of url
+        query = {'db': cr.dbname}
+        fragment = {
+            'login': partner.user_ids[0].login,
+            'action': 'mail.action_mail_redirect',
+        }
+        if mail.notification:
+            fragment['message_id'] = mail.mail_message_id.id
+        elif mail.model and mail.res_id:
+            fragment.update(model=mail.model, res_id=mail.res_id)
+
+        return "/web?%s#%s" % (urlencode(query), urlencode(fragment))
+
     #------------------------------------------------------
     # Email specific
     #------------------------------------------------------
 
+    def message_get_default_recipients(self, cr, uid, ids, context=None):
+        if context and context.get('thread_model') and context['thread_model'] in self.pool and context['thread_model'] != self._name:
+            if hasattr(self.pool[context['thread_model']], 'message_get_default_recipients'):
+                sub_ctx = dict(context)
+                sub_ctx.pop('thread_model')
+                return self.pool[context['thread_model']].message_get_default_recipients(cr, uid, ids, context=sub_ctx)
+        res = {}
+        for record in self.browse(cr, SUPERUSER_ID, ids, context=context):
+            recipient_ids, email_to, email_cc = set(), False, False
+            if 'partner_id' in self._all_columns and record.partner_id:
+                recipient_ids.add(record.partner_id.id)
+            elif 'email_from' in self._all_columns and record.email_from:
+                email_to = record.email_from
+            elif 'email' in self._all_columns:
+                email_to = record.email
+            res[record.id] = {'partner_ids': list(recipient_ids), 'email_to': email_to, 'email_cc': email_cc}
+        return res
+
     def message_get_reply_to(self, cr, uid, ids, context=None):
         """ Returns the preferred reply-to email address that is basically
             the alias of the document, if it exists. """
         if not self._inherits.get('mail.alias'):
             return [False for id in ids]
-        return ["%s@%s" % (record['alias_name'], record['alias_domain'])
-                    if record.get('alias_domain') and record.get('alias_name')
-                    else False
-                    for record in self.read(cr, SUPERUSER_ID, ids, ['alias_name', 'alias_domain'], context=context)]
+        return ["%s@%s" % (record.alias_name, record.alias_domain)
+                if record.alias_domain and record.alias_name else False
+                for record in self.browse(cr, SUPERUSER_ID, ids, context=context)]
+
+    def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None):
+        """ Temporary method to create custom notification email values for a given
+        model and document. This should be better to have a headers field on
+        the mail.mail model, computed when creating the notification email, but
+        this cannot be done in a stable version.
+
+        TDE FIXME: rethink this ulgy thing. """
+        res = dict()
+        return res
 
     #------------------------------------------------------
     # Mail gateway
@@ -687,7 +730,7 @@ class mail_thread(osv.AbstractModel):
         s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
         return filter(lambda x: x, self._find_partner_from_emails(cr, uid, None, tools.email_split(s), context=context))
 
-    def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, context=None):
+    def message_route_verify(self, cr, uid, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False, context=None):
         """ Verify route validity. Check and rules:
             1 - if thread_id -> check that document effectively exists; otherwise
                 fallback on a message_new by resetting thread_id
@@ -808,6 +851,9 @@ class mail_thread(osv.AbstractModel):
             _create_bounce_email()
             return ()
 
+        if not model and not thread_id and not alias and not allow_private:
+            return ()
+
         return (model, thread_id, route[2], route[3], route[4])
 
     def message_route(self, cr, uid, message, message_dict, model=None, thread_id=None,
@@ -859,22 +905,23 @@ class mail_thread(osv.AbstractModel):
         thread_references = references or in_reply_to
 
         # 1. message is a reply to an existing message (exact match of message_id)
+        ref_match = thread_references and tools.reference_re.search(thread_references)
         msg_references = mail_header_msgid_re.findall(thread_references)
         mail_message_ids = mail_msg_obj.search(cr, uid, [('message_id', 'in', msg_references)], context=context)
-        if mail_message_ids:
+        if ref_match and mail_message_ids:
             original_msg = mail_msg_obj.browse(cr, SUPERUSER_ID, mail_message_ids[0], context=context)
             model, thread_id = original_msg.model, original_msg.res_id
-            _logger.info(
-                'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
-                email_from, email_to, message_id, model, thread_id, custom_values, uid)
             route = self.message_route_verify(
                 cr, uid, message, message_dict,
                 (model, thread_id, custom_values, uid, None),
-                update_author=True, assert_model=True, create_fallback=True, context=context)
-            return route and [route] or []
+                update_author=True, assert_model=False, create_fallback=True, context=context)
+            if route:
+                _logger.info(
+                    'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
+                    email_from, email_to, message_id, model, thread_id, custom_values, uid)
+                return [route]
 
         # 2. message is a reply to an existign thread (6.1 compatibility)
-        ref_match = thread_references and tools.reference_re.search(thread_references)
         if ref_match:
             reply_thread_id = int(ref_match.group(1))
             reply_model = ref_match.group(2) or fallback_model
@@ -892,16 +939,17 @@ class mail_thread(osv.AbstractModel):
                             ('res_id', '=', thread_id),
                         ], context=context)
                     if compat_mail_msg_ids and model_obj.exists(cr, uid, thread_id) and hasattr(model_obj, 'message_update'):
-                        _logger.info(
-                            'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
-                            email_from, email_to, message_id, model, thread_id, custom_values, uid)
                         route = self.message_route_verify(
                             cr, uid, message, message_dict,
                             (model, thread_id, custom_values, uid, None),
                             update_author=True, assert_model=True, create_fallback=True, context=context)
-                        return route and [route] or []
+                        if route:
+                            _logger.info(
+                                'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
+                                email_from, email_to, message_id, model, thread_id, custom_values, uid)
+                            return [route]
 
-        # 2. Reply to a private message
+        # 3. Reply to a private message
         if in_reply_to:
             mail_message_ids = mail_msg_obj.search(cr, uid, [
                                 ('message_id', '=', in_reply_to),
@@ -909,14 +957,16 @@ class mail_thread(osv.AbstractModel):
                             ], limit=1, context=context)
             if mail_message_ids:
                 mail_message = mail_msg_obj.browse(cr, uid, mail_message_ids[0], context=context)
-                _logger.info('Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
-                                email_from, email_to, message_id, mail_message.id, custom_values, uid)
                 route = self.message_route_verify(cr, uid, message, message_dict,
                                 (mail_message.model, mail_message.res_id, custom_values, uid, None),
-                                update_author=True, assert_model=True, create_fallback=True, context=context)
-                return route and [route] or []
-
-        # 3. Look for a matching mail.alias entry
+                                update_author=True, assert_model=True, create_fallback=True, allow_private=True, context=context)
+                if route:
+                    _logger.info(
+                        'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
+                        email_from, email_to, message_id, mail_message.id, custom_values, uid)
+                    return [route]
+
+        # 4. Look for a matching mail.alias entry
         # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
         # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
         rcpt_tos = \
@@ -942,15 +992,16 @@ class mail_thread(osv.AbstractModel):
                         user_id = uid
                         _logger.info('No matching user_id for the alias %s', alias.alias_name)
                     route = (alias.alias_model_id.model, alias.alias_force_thread_id, eval(alias.alias_defaults), user_id, alias)
-                    _logger.info('Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
-                                email_from, email_to, message_id, route)
                     route = self.message_route_verify(cr, uid, message, message_dict, route,
                                 update_author=True, assert_model=True, create_fallback=True, context=context)
                     if route:
+                        _logger.info(
+                            'Routing mail from %s to %s with Message-Id %s: direct alias match: %r',
+                            email_from, email_to, message_id, route)
                         routes.append(route)
                 return routes
 
-        # 4. Fallback to the provided parameters, if they work
+        # 5. Fallback to the provided parameters, if they work
         if not thread_id:
             # Legacy: fallback to matching [ID] in the Subject
             match = tools.res_re.search(decode_header(message, 'Subject'))
@@ -960,15 +1011,16 @@ class mail_thread(osv.AbstractModel):
                 thread_id = int(thread_id)
             except:
                 thread_id = False
-        _logger.info('Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
-                    email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
         route = self.message_route_verify(cr, uid, message, message_dict,
                         (fallback_model, thread_id, custom_values, uid, None),
                         update_author=True, assert_model=True, context=context)
         if route:
+            _logger.info(
+                'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
+                email_from, email_to, message_id, fallback_model, thread_id, custom_values, uid)
             return [route]
 
-        # AssertionError if no routes found and if no bounce occured
+        # ValueError if no routes found and if no bounce occured
         raise ValueError(
                 'No possible route found for incoming message from %s to %s (Message-Id %s:). '
                 'Create an appropriate mail.alias or force the destination model.' %
@@ -1519,8 +1571,7 @@ class mail_thread(osv.AbstractModel):
         if subtype:
             if '.' not in subtype:
                 subtype = 'mail.%s' % subtype
-            ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, *subtype.split('.'))
-            subtype_id = ref and ref[1] or False
+            subtype_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, subtype)
 
         # automatically subscribe recipients if asked to
         if context.get('mail_post_autofollow') and thread_id and partner_ids:
@@ -1563,8 +1614,13 @@ class mail_thread(osv.AbstractModel):
         for x in ('from', 'to', 'cc'):
             values.pop(x, None)
 
-        # Create and auto subscribe the author
+        # Post the message
         msg_id = mail_message.create(cr, uid, values, context=context)
+
+        # Post-process: subscribe author, update message_last_post
+        if model and model != 'mail.thread' and thread_id and subtype_id:
+            # done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access
+            self.write(cr, SUPERUSER_ID, [thread_id], {'message_last_post': fields.datetime.now()}, context=context)
         message = mail_message.browse(cr, uid, msg_id, context=context)
         if message.author_id and thread_id and type != 'notification' and not context.get('mail_create_nosubscribe'):
             self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context)
@@ -1601,10 +1657,7 @@ class mail_thread(osv.AbstractModel):
         if set(partner_ids) == set([user_pid]):
             try:
                 self.check_access_rights(cr, uid, 'read')
-                if context.get('operation', '') == 'create':
-                    self.check_access_rule(cr, uid, ids, 'create')
-                else:
-                    self.check_access_rule(cr, uid, ids, 'read')
+                self.check_access_rule(cr, uid, ids, 'read')
             except (osv.except_osv, orm.except_orm):
                 return False
         else:
@@ -1842,3 +1895,35 @@ class mail_thread(osv.AbstractModel):
             }
             threads.append(data)
         return sorted(threads, key=lambda x: (x['popularity'], x['id']), reverse=True)[:3]
+
+    def message_change_thread(self, cr, uid, id, new_res_id, new_model, context=None):
+        """
+        Transfert the list of the mail thread messages from an model to another
+
+        :param id : the old res_id of the mail.message
+        :param new_res_id : the new res_id of the mail.message
+        :param new_model : the name of the new model of the mail.message
+
+        Example :   self.pool.get("crm.lead").message_change_thread(self, cr, uid, 2, 4, "project.issue", context) 
+                    will transfert thread of the lead (id=2) to the issue (id=4)
+        """
+
+        # get the sbtype id of the comment Message
+        subtype_res_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'mail.mt_comment', raise_if_not_found=True)
+        
+        # get the ids of the comment and none-comment of the thread
+        message_obj = self.pool.get('mail.message')
+        msg_ids_comment = message_obj.search(cr, uid, [
+                    ('model', '=', self._name),
+                    ('res_id', '=', id),
+                    ('subtype_id', '=', subtype_res_id)], context=context)
+        msg_ids_not_comment = message_obj.search(cr, uid, [
+                    ('model', '=', self._name),
+                    ('res_id', '=', id),
+                    ('subtype_id', '!=', subtype_res_id)], context=context)
+        
+        # update the messages
+        message_obj.write(cr, uid, msg_ids_comment, {"res_id" : new_res_id, "model" : new_model}, context=context)
+        message_obj.write(cr, uid, msg_ids_not_comment, {"res_id" : new_res_id, "model" : new_model, "subtype_id" : None}, context=context)
+        
+        return True