[FIX] mail.message: all recipients explicitly mentioned should receive a notification...
[odoo/odoo.git] / addons / mail / mail_message.py
index 1f588fc..562a48f 100644 (file)
@@ -20,7 +20,7 @@
 ##############################################################################
 
 import logging
-import tools
+from openerp import tools
 
 from email.header import decode_header
 from openerp import SUPERUSER_ID
@@ -55,7 +55,7 @@ class mail_message(osv.Model):
 
     _message_read_limit = 30
     _message_read_fields = ['id', 'parent_id', 'model', 'res_id', 'body', 'subject', 'date', 'to_read', 'email_from',
-        'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name', 'favorite_user_ids']
+        'type', 'vote_user_ids', 'attachment_ids', 'author_id', 'partner_ids', 'record_name']
     _message_record_name_length = 18
     _message_read_more_limit = 1024
 
@@ -76,7 +76,7 @@ class mail_message(osv.Model):
         # TDE note: regroup by model/ids, to have less queries to perform
         result = dict.fromkeys(ids, False)
         for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
-            if not message.get('model') or not message.get('res_id'):
+            if not message.get('model') or not message.get('res_id') or not self.pool.get(message['model']):
                 continue
             result[message['id']] = self._shorten_name(self.pool.get(message['model']).name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1])
         return result
@@ -100,6 +100,25 @@ class mail_message(osv.Model):
             inversed because we search unread message on a read column. """
         return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.read', '=', not domain[0][2])]
 
+    def _get_starred(self, cr, uid, ids, name, arg, context=None):
+        """ Compute if the message is unread by the current user. """
+        res = dict((id, False) for id in ids)
+        partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        notif_obj = self.pool.get('mail.notification')
+        notif_ids = notif_obj.search(cr, uid, [
+            ('partner_id', 'in', [partner_id]),
+            ('message_id', 'in', ids),
+            ('starred', '=', True),
+        ], context=context)
+        for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
+            res[notif.message_id.id] = True
+        return res
+
+    def _search_starred(self, cr, uid, obj, name, domain, context=None):
+        """ Search for messages to read by the current user. Condition is
+            inversed because we search unread message on a read column. """
+        return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
+
     def name_get(self, cr, uid, ids, context=None):
         # name_get may receive int id instead of an id list
         if isinstance(ids, (int, long)):
@@ -138,7 +157,7 @@ class mail_message(osv.Model):
             store=True, string='Message Record Name',
             help="Name get of the related document."),
         'notification_ids': fields.one2many('mail.notification', 'message_id',
-            string='Notifications', _auto_join=True,
+            string='Notifications', auto_join=True,
             help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
         'subject': fields.char('Subject'),
         'date': fields.datetime('Date'),
@@ -146,15 +165,15 @@ class mail_message(osv.Model):
         'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
         'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
             type='boolean', string='To read',
-            help='Functional field to search for messages the current user has to read'),
+            help='Current user has an unread notification linked to this message'),
+        'starred': fields.function(_get_starred, fnct_search=_search_starred,
+            type='boolean', string='Starred',
+            help='Current user has a starred notification linked to this message'),
         'subtype_id': fields.many2one('mail.message.subtype', 'Subtype',
             ondelete='set null', select=1,),
         'vote_user_ids': fields.many2many('res.users', 'mail_vote',
             'message_id', 'user_id', string='Votes',
             help='Users that voted for this message'),
-        'favorite_user_ids': fields.many2many('res.users', 'mail_favorite',
-            'message_id', 'user_id', string='Favorite',
-            help='Users that set this message in their favorites'),
     }
 
     def _needaction_domain_get(self, cr, uid, context=None):
@@ -186,27 +205,78 @@ class mail_message(osv.Model):
         return new_has_voted or False
 
     #------------------------------------------------------
-    # Favorite
+    # download an attachment
     #------------------------------------------------------
 
-    def favorite_toggle(self, cr, uid, ids, context=None):
-        ''' Toggles favorite. Performed using read to avoid access rights issues.
-            Done as SUPERUSER_ID because uid may star a message he cannot modify. '''
-        for message in self.read(cr, uid, ids, ['favorite_user_ids'], context=context):
-            new_is_favorite = not (uid in message.get('favorite_user_ids'))
-            if new_is_favorite:
-                self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(4, uid)]}, context=context)
-                # when setting a favorite, set the related notification as unread, or create an unread notification if not existing
-                notification_obj = self.pool.get('mail.notification')
-                pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
-                notif_id = notification_obj.search(cr, SUPERUSER_ID, [('message_id', '=', message.get('id')), ('partner_id', '=', pid)], context=context)
-                if notif_id:
-                    notification_obj.write(cr, SUPERUSER_ID, notif_id, {'read': False}, context=context)
-                else:
-                    notification_obj.create(cr, SUPERUSER_ID, {'message_id': message.get('id'), 'partner_id': pid, 'read': False}, context=context)
-            else:
-                self.write(cr, SUPERUSER_ID, message.get('id'), {'favorite_user_ids': [(3, uid)]}, context=context)
-        return new_is_favorite or False
+    def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
+        """ Return the content of linked attachments. """
+        message = self.browse(cr, uid, id_message, context=context)
+        if attachment_id in [attachment.id for attachment in message.attachment_ids]:
+            attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
+            if attachment.datas and attachment.datas_fname:
+                return {
+                    'base64': attachment.datas,
+                    'filename': attachment.datas_fname,
+                }
+        return False
+
+    #------------------------------------------------------
+    # Notification API
+    #------------------------------------------------------
+
+    def set_message_read(self, cr, uid, msg_ids, read, context=None):
+        """ Set messages as (un)read. Technically, the notifications related
+            to uid are set to (un)read. If for some msg_ids there are missing
+            notifications (i.e. due to load more or thread parent fetching),
+            they are created.
+
+            :param bool read: set notification as (un)read
+        """
+        notification_obj = self.pool.get('mail.notification')
+        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        notif_ids = notification_obj.search(cr, uid, [
+            ('partner_id', '=', user_pid),
+            ('message_id', 'in', msg_ids)
+            ], context=context)
+
+        # all message have notifications: already set them as (un)read
+        if len(notif_ids) == len(msg_ids):
+            return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+
+        # some messages do not have notifications: find which one, create notification, update read status
+        notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
+        to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
+        for msg_id in to_create_msg_ids:
+            notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
+        return notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+
+    def set_message_starred(self, cr, uid, msg_ids, starred, context=None):
+        """ Set messages as (un)starred. Technically, the notifications related
+            to uid are set to (un)starred. If for some msg_ids there are missing
+            notifications (i.e. due to load more or thread parent fetching),
+            they are created.
+
+            :param bool starred: set notification as (un)starred
+        """
+        notification_obj = self.pool.get('mail.notification')
+        user_pid = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=context)['partner_id'][0]
+        notif_ids = notification_obj.search(cr, uid, [
+            ('partner_id', '=', user_pid),
+            ('message_id', 'in', msg_ids)
+            ], context=context)
+
+        # all message have notifications: already set them as (un)starred
+        if len(notif_ids) == len(msg_ids):
+            notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+            return starred
+
+        # some messages do not have notifications: find which one, create notification, update starred status
+        notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
+        to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
+        for msg_id in to_create_msg_ids:
+            notification_obj.create(cr, uid, {'partner_id': user_pid, 'starred': starred, 'message_id': msg_id}, context=context)
+        notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context)
+        return starred
 
     #------------------------------------------------------
     # Message loading for web interface
@@ -233,14 +303,12 @@ class mail_message(osv.Model):
                 partner_ids |= set([partner.id for partner in message.partner_ids])
             if message.attachment_ids:
                 attachment_ids |= set([attachment.id for attachment in message.attachment_ids])
-
-        # Filter author_ids uid can see
-        # partner_ids = self.pool.get('res.partner').search(cr, uid, [('id', 'in', partner_ids)], context=context)
-        partners = res_partner_obj.name_get(cr, uid, list(partner_ids), context=context)
+        # Read partners as SUPERUSER -> display the names like classic m2o even if no access
+        partners = res_partner_obj.name_get(cr, SUPERUSER_ID, list(partner_ids), context=context)
         partner_tree = dict((partner[0], partner) for partner in partners)
 
-        # 2. Attachments
-        attachments = ir_attachment_obj.read(cr, uid, list(attachment_ids), ['id', 'datas_fname'], context=context)
+        # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
+        attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname'], context=context)
         attachments_tree = dict((attachment['id'], {'id': attachment['id'], 'filename': attachment['datas_fname']}) for attachment in attachments)
 
         # 3. Update message dictionaries
@@ -281,7 +349,6 @@ class mail_message(osv.Model):
         # votes and favorites: res.users ids, no prefetching should be done
         vote_nb = len(message.vote_user_ids)
         has_voted = uid in [user.id for user in message.vote_user_ids]
-        is_favorite = uid in [user.id for user in message.favorite_user_ids]
 
         return {'id': message.id,
                 'type': message.type,
@@ -299,7 +366,7 @@ class mail_message(osv.Model):
                 'partner_ids': [],
                 'vote_nb': vote_nb,
                 'has_voted': has_voted,
-                'is_favorite': is_favorite,
+                'is_favorite': message.starred,
                 'attachment_ids': [],
             }
 
@@ -472,15 +539,6 @@ class mail_message(osv.Model):
             thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
         return message_list
 
-    # TDE Note: do we need this ?
-    # def user_free_attachment(self, cr, uid, context=None):
-    #     attachment = self.pool.get('ir.attachment')
-    #     attachment_list = []
-    #     attachment_ids = attachment.search(cr, uid, [('res_model', '=', 'mail.message'), ('create_uid', '=', uid)])
-    #     if len(attachment_ids):
-    #         attachment_list = [{'id': attach.id, 'name': attach.name, 'date': attach.create_date} for attach in attachment.browse(cr, uid, attachment_ids, context=context)]
-    #     return attachment_list
-
     #------------------------------------------------------
     # mail_message internals
     #------------------------------------------------------
@@ -490,6 +548,20 @@ class mail_message(osv.Model):
         if not cr.fetchone():
             cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
 
+    def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
+        doc_ids = doc_dict.keys()
+        allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
+        return set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
+
+    def _find_allowed_doc_ids(self, cr, uid, model_ids, context=None):
+        model_access_obj = self.pool.get('ir.model.access')
+        allowed_ids = set()
+        for doc_model, doc_dict in model_ids.iteritems():
+            if not model_access_obj.check(cr, uid, doc_model, 'read', False):
+                continue
+            allowed_ids |= self._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
+        return allowed_ids
+
     def _search(self, cr, uid, args, offset=0, limit=None, order=None,
         context=None, count=False, access_rights_uid=None):
         """ Override that adds specific access rights of mail.message, to remove
@@ -527,13 +599,7 @@ class mail_message(osv.Model):
             elif message.get('model') and message.get('res_id'):
                 model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
 
-        model_access_obj = self.pool.get('ir.model.access')
-        for doc_model, doc_dict in model_ids.iteritems():
-            if not model_access_obj.check(cr, uid, doc_model, 'read', False):
-                continue
-            doc_ids = doc_dict.keys()
-            allowed_doc_ids = self.pool.get(doc_model).search(cr, uid, [('id', 'in', doc_ids)], context=context)
-            allowed_ids |= set([message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id]])
+        allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
 
         final_ids = author_ids | partner_ids | allowed_ids
         if count:
@@ -549,54 +615,73 @@ class mail_message(osv.Model):
                 - uid have read access to the related document if model, res_id
                 - otherwise: raise
             - create: if
-                - no model, no res_id, I create a private message
+                - no model, no res_id, I create a private message OR
                 - pid in message_follower_ids if model, res_id OR
+                - mail_notification (parent_id.id, pid) exists, uid has been notified of the parent, OR
                 - uid have write access on the related document if model, res_id, OR
                 - otherwise: raise
             - write: if
+                - author_id == pid, uid is the author, OR
                 - uid has write access on the related document if model, res_id
-                - Otherwise: raise
+                - otherwise: raise
             - unlink: if
                 - uid has write access on the related document if model, res_id
-                - Otherwise: raise
+                - otherwise: raise
         """
+        def _generate_model_record_ids(msg_val, msg_ids=[]):
+            """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
+                :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
+            """
+            model_record_ids = {}
+            for id in msg_ids:
+                if msg_val[id]['model'] and msg_val[id]['res_id']:
+                    model_record_ids.setdefault(msg_val[id]['model'], dict()).setdefault(msg_val[id]['res_id'], set()).add(msg_val[id]['res_id'])
+            return model_record_ids
+
         if uid == SUPERUSER_ID:
             return
         if isinstance(ids, (int, long)):
             ids = [ids]
+        not_obj = self.pool.get('mail.notification')
+        fol_obj = self.pool.get('mail.followers')
         partner_id = self.pool.get('res.users').read(cr, uid, uid, ['partner_id'], context=None)['partner_id'][0]
 
         # Read mail_message.ids to have their values
         message_values = dict.fromkeys(ids)
-        model_record_ids = {}
-        cr.execute('SELECT DISTINCT id, model, res_id, author_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
-        for id, rmod, rid, author_id in cr.fetchall():
-            message_values[id] = {'res_model': rmod, 'res_id': rid, 'author_id': author_id}
-            if rmod:
-                model_record_ids.setdefault(rmod, dict()).setdefault(rid, set()).add(id)
-
-        # Author condition, for read and create (private message) -> could become an ir.rule, but not till we do not have a many2one variable field
-        if operation == 'read':
+        cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
+        for id, rmod, rid, author_id, parent_id in cr.fetchall():
+            message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
+
+        # Author condition (READ, WRITE, CREATE (private)) -> could become an ir.rule ?
+        author_ids = []
+        if operation == 'read' or operation == 'write':
             author_ids = [mid for mid, message in message_values.iteritems()
                 if message.get('author_id') and message.get('author_id') == partner_id]
         elif operation == 'create':
             author_ids = [mid for mid, message in message_values.iteritems()
                 if not message.get('model') and not message.get('res_id')]
-        else:
-            author_ids = []
+
+        # Parent condition, for create (check for received notifications for the created message parent)
+        notified_ids = []
+        if operation == 'create':
+            parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
+                if message.get('parent_id')]
+            not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
+            not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
+            notified_ids += [mid for mid, message in message_values.iteritems()
+                if message.get('parent_id') in not_parent_ids]
 
         # Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field
+        other_ids = set(ids).difference(set(author_ids), set(notified_ids))
+        model_record_ids = _generate_model_record_ids(message_values, other_ids)
         if operation == 'read':
-            not_obj = self.pool.get('mail.notification')
             not_ids = not_obj.search(cr, SUPERUSER_ID, [
                 ('partner_id', '=', partner_id),
                 ('message_id', 'in', ids),
             ], context=context)
             notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
         elif operation == 'create':
-            notified_ids = []
             for doc_model, doc_dict in model_record_ids.items():
-                fol_obj = self.pool.get('mail.followers')
                 fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
                     ('res_model', '=', doc_model),
                     ('res_id', 'in', list(doc_dict.keys())),
@@ -604,22 +689,15 @@ class mail_message(osv.Model):
                     ], context=context)
                 fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
                 notified_ids += [mid for mid, message in message_values.iteritems()
-                    if message.get('res_model') == doc_model and message.get('res_id') in fol_mids]
-        else:
-            notified_ids = []
-
-        # Calculate remaining ids, and related model/res_ids
-        model_record_ids = {}
-        other_ids = set(ids).difference(set(author_ids), set(notified_ids))
-        for id in other_ids:
-            if message_values[id]['res_model']:
-                model_record_ids.setdefault(message_values[id]['res_model'], set()).add(message_values[id]['res_id'])
+                    if message.get('model') == doc_model and message.get('res_id') in fol_mids]
 
         # CRUD: Access rights related to the document
+        other_ids = other_ids.difference(set(notified_ids))
+        model_record_ids = _generate_model_record_ids(message_values, other_ids)
         document_related_ids = []
-        for model, mids in model_record_ids.items():
+        for model, doc_dict in model_record_ids.items():
             model_obj = self.pool.get(model)
-            mids = model_obj.exists(cr, uid, mids)
+            mids = model_obj.exists(cr, uid, doc_dict.keys())
             if operation in ['create', 'write', 'unlink']:
                 model_obj.check_access_rights(cr, uid, 'write')
                 model_obj.check_access_rule(cr, uid, mids, 'write', context=context)
@@ -627,10 +705,10 @@ class mail_message(osv.Model):
                 model_obj.check_access_rights(cr, uid, operation)
                 model_obj.check_access_rule(cr, uid, mids, operation, context=context)
             document_related_ids += [mid for mid, message in message_values.iteritems()
-                if message.get('res_model') == model and message.get('res_id') in mids]
+                if message.get('model') == model and message.get('res_id') in mids]
 
         # Calculate remaining ids: if not void, raise an error
-        other_ids = other_ids - set(document_related_ids)
+        other_ids = other_ids.difference(set(document_related_ids))
         if not other_ids:
             return
         raise orm.except_orm(_('Access Denied'),
@@ -638,12 +716,23 @@ class mail_message(osv.Model):
                             (self._description, operation))
 
     def create(self, cr, uid, values, context=None):
+        if context is None:
+            context = {}
+        default_starred = context.pop('default_starred', False)
         if not values.get('message_id') and values.get('res_id') and values.get('model'):
             values['message_id'] = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
         elif not values.get('message_id'):
             values['message_id'] = tools.generate_tracking_message_id('private')
         newid = super(mail_message, self).create(cr, uid, values, context)
-        self._notify(cr, SUPERUSER_ID, newid, context=context)
+        self._notify(cr, uid, newid, context=context)
+        # TDE FIXME: handle default_starred. Why not setting an inv on starred ?
+        # Because starred will call set_message_starred, that looks for notifications.
+        # When creating a new mail_message, it will create a notification to a message
+        # that does not exist, leading to an error (key not existing). Also this
+        # this means unread notifications will be created, yet we can not assure
+        # this is what we want.
+        if default_starred:
+            self.set_message_starred(cr, uid, [newid], True, context=context)
         return newid
 
     def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'):
@@ -753,35 +842,50 @@ class mail_message(osv.Model):
         """ Add the related record followers to the destination partner_ids if is not a private message.
             Call mail_notification.notify to manage the email sending
         """
-        message = self.read(cr, uid, newid, ['model', 'res_id', 'author_id', 'subtype_id', 'partner_ids'], context=context)
+        notification_obj = self.pool.get('mail.notification')
+        message = self.browse(cr, uid, newid, context=context)
 
         partners_to_notify = set([])
         # message has no subtype_id: pure log message -> no partners, no one notified
-        if not message.get('subtype_id'):
+        if not message.subtype_id:
             return True
-        # all partner_ids of the mail.message have to be notified
-        if message.get('partner_ids'):
-            partners_to_notify |= set(message.get('partner_ids'))
         # all followers of the mail.message document have to be added as partners and notified
-        if message.get('model') and message.get('res_id'):
+        if message.model and message.res_id:
             fol_obj = self.pool.get("mail.followers")
-            fol_ids = fol_obj.search(cr, uid, [
-                ('res_model', '=', message.get('model')),
-                ('res_id', '=', message.get('res_id')),
-                ('subtype_ids', 'in', message.get('subtype_id')[0])
+            # browse as SUPERUSER because rules could restrict the search results
+            fol_ids = fol_obj.search(cr, SUPERUSER_ID, [
+                ('res_model', '=', message.model),
+                ('res_id', '=', message.res_id),
+                ('subtype_ids', 'in', message.subtype_id.id)
                 ], context=context)
-            fol_objs = fol_obj.read(cr, uid, fol_ids, ['partner_id'], context=context)
-            partners_to_notify |= set(fol['partner_id'][0] for fol in fol_objs)
+            partners_to_notify |= set(fo.partner_id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
         # remove me from notified partners, unless the message is written on my own wall
-        if message.get('author_id') and message.get('model') == "res.partner" and message.get('res_id') == message.get('author_id')[0]:
-            partners_to_notify |= set([message.get('author_id')[0]])
-        elif message.get('author_id'):
-            partners_to_notify = partners_to_notify - set([message.get('author_id')[0]])
+        if message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
+            partners_to_notify |= set([message.author_id])
+        elif message.author_id:
+            partners_to_notify -= set([message.author_id])
 
-        if partners_to_notify:
-            self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p_id) for p_id in partners_to_notify]}, context=context)
+        # all partner_ids of the mail.message have to be notified regardless of the above (even the author if explicitly added!)
+        if message.partner_ids:
+            partners_to_notify |= set(message.partner_ids)
 
-        self.pool.get('mail.notification')._notify(cr, uid, newid, context=context)
+        # notify
+        if partners_to_notify:
+            self.write(cr, SUPERUSER_ID, [newid], {'notified_partner_ids': [(4, p.id) for p in partners_to_notify]}, context=context)
+        notification_obj._notify(cr, uid, newid, context=context)
+        message.refresh()
+
+        # An error appear when a user receive a notification without notifying
+        # the parent message -> add a read notification for the parent
+        if message.parent_id:
+            # all notified_partner_ids of the mail.message have to be notified for the parented messages
+            partners_to_parent_notify = set(message.notified_partner_ids).difference(message.parent_id.notified_partner_ids)
+            for partner in partners_to_parent_notify:
+                notification_obj.create(cr, uid, {
+                        'message_id': message.parent_id.id,
+                        'partner_id': partner.id,
+                        'read': True,
+                    }, context=context)
 
     #------------------------------------------------------
     # Tools