diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 02903ea8f33..7843a096940 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1744,12 +1744,13 @@ class res_partner(osv.osv): return super(res_partner, self).copy(cr, uid, id, default, context) -class mail_compose_message(osv.osv): +class mail_compose_message(osv.Model): _inherit = 'mail.compose.message' def send_mail(self, cr, uid, ids, context=None): context = context or {} if context.get('default_model') == 'account.invoice' and context.get('default_res_id') and context.get('mark_invoice_as_sent'): + context = dict(context, mail_post_autofollow=True) self.pool.get('account.invoice').write(cr, uid, [context['default_res_id']], {'sent': True}, context=context) return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index d80b65ed648..600c3c6d832 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -224,23 +224,25 @@ class mail_message(osv.Model): # Notification API #------------------------------------------------------ - def set_message_read(self, cr, uid, msg_ids, read, context=None): + def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, 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 + :param bool create_missing: create notifications for missing entries + (i.e. when acting on displayed messages not notified) """ 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) + domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)] + if not create_missing: + domain += [('read', '=', not read)] + notif_ids = notification_obj.search(cr, uid, domain, context=context) # all message have notifications: already set them as (un)read - if len(notif_ids) == len(msg_ids): + if len(notif_ids) == len(msg_ids) or not create_missing: 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 @@ -250,23 +252,23 @@ class mail_message(osv.Model): 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): + def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, 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. + to uid are set to (un)starred. :param bool starred: set notification as (un)starred + :param bool create_missing: create notifications for missing entries + (i.e. when acting on displayed messages not notified) """ 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) + domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)] + if not create_missing: + domain += [('starred', '=', not starred)] + notif_ids = notification_obj.search(cr, uid, domain, context=context) # all message have notifications: already set them as (un)starred - if len(notif_ids) == len(msg_ids): + if len(notif_ids) == len(msg_ids) or not create_missing: notification_obj.write(cr, uid, notif_ids, {'starred': starred}, context=context) return starred @@ -350,9 +352,15 @@ class mail_message(osv.Model): vote_nb = len(message.vote_user_ids) has_voted = uid in [user.id for user in message.vote_user_ids] + try: + body_html = html_email_clean(message.body) + except Exception: + body_html = '

Encoding Error :
Unable to convert this message (id: %s).

' % message.id + _logger.exception(Exception) + return {'id': message.id, 'type': message.type, - 'body': html_email_clean(message.body or ''), + 'body': body_html, 'model': message.model, 'res_id': message.res_id, 'record_name': message.record_name, @@ -492,6 +500,7 @@ class mail_message(osv.Model): message_unload_ids = message_unload_ids if message_unload_ids is not None else [] if message_unload_ids: domain += [('id', 'not in', message_unload_ids)] + notification_obj = self.pool.get('mail.notification') limit = limit or self._message_read_limit message_tree = {} message_list = [] @@ -529,6 +538,7 @@ class mail_message(osv.Model): message_id_list.sort(key=lambda item: item['id']) message_id_list.insert(0, self._message_read_dict(cr, uid, message_tree[key], context=context)) + # create final ordered message_list based on parent_tree parent_list = parent_tree.items() parent_list = sorted(parent_list, key=lambda item: max([msg.get('id') for msg in item[1]]) if item[1] else item[0], reverse=True) message_list = [message for (key, msg_list) in parent_list for message in msg_list] @@ -600,12 +610,14 @@ class mail_message(osv.Model): model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id')) allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context) - final_ids = author_ids | partner_ids | allowed_ids + if count: return len(final_ids) else: - return list(final_ids) + # re-construct a list based on ids, because set did not keep the original order + id_list = [id for id in ids if id in final_ids] + return id_list def check_access_rule(self, cr, uid, ids, operation, context=None): """ Access rules of mail.message: diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index daef5d7681a..a62ac73101f 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -590,7 +590,11 @@ class mail_thread(osv.AbstractModel): assert thread_id == 0, "Posting a message without model should be with a null res_id, to create a private message." model_pool = self.pool.get('mail.thread') new_msg_id = model_pool.message_post_user_api(cr, uid, [thread_id], context=context, content_subtype='html', **msg) - + + # when posting an incoming email to a document: subscribe the author, if a partner, as follower + if model and thread_id and msg.get('author_id'): + model_pool.message_subscribe(cr, uid, [thread_id], [msg.get('author_id')], context=context) + if partner_ids: # postponed after message_post, because this is an external message and we don't want to create # duplicate emails due to notifications @@ -824,7 +828,7 @@ class mail_thread(osv.AbstractModel): mail_message = self.pool.get('mail.message') model = context.get('thread_model', self._name) if thread_id else False - attachment_ids = [] + attachment_ids = kwargs.pop('attachment_ids', []) for name, content in attachments: if isinstance(content, unicode): content = content.encode('utf-8') @@ -899,7 +903,6 @@ class mail_thread(osv.AbstractModel): - extra_email: [ 'Fabien ', 'al@openerp.com' ] """ ir_attachment = self.pool.get('ir.attachment') - mail_message = self.pool.get('mail.message') # 1. Pre-processing: body, partner_ids, type and subtype if content_subtype == 'plaintext': @@ -932,11 +935,7 @@ class mail_thread(osv.AbstractModel): message_type = kwargs.pop('type', 'comment') message_subtype = kwargs.pop('subtype', 'mail.mt_comment') - # 2. Post message - new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type, - subtype=message_subtype, parent_id=parent_id, context=context, partner_ids=partner_ids, **kwargs) - - # 3. Post-processing + # 2. Pre-processing: free attachments linked to the model # HACK TDE FIXME: Chatter: attachments linked to the document (not done JS-side), load the message if attachment_ids: # TDE FIXME (?): when posting a private message, we use mail.thread as a model @@ -952,7 +951,13 @@ class mail_thread(osv.AbstractModel): if filtered_attachment_ids: if thread_id and model: ir_attachment.write(cr, SUPERUSER_ID, attachment_ids, {'res_model': model, 'res_id': thread_id}, context=context) - mail_message.write(cr, SUPERUSER_ID, [new_message_id], {'attachment_ids': [(6, 0, [pid for pid in attachment_ids])]}, context=context) + else: + attachment_ids = [] + + # 3. Post message + new_message_id = self.message_post(cr, uid, thread_id=thread_id, body=body, subject=subject, type=message_type, + subtype=message_subtype, parent_id=parent_id, attachment_ids=[(4, id) for id in attachment_ids], + context=context, partner_ids=partner_ids, **kwargs) return new_message_id diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 32b6e85f118..50d24b93ca4 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -939,7 +939,7 @@ openerp.mail = function (session) { } var message_ids = _.map(messages, function (val) { return val.id; }); - this.ds_message.call('set_message_read', [message_ids, read_value, this.context]) + this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context]) .then(function () { // apply modification _.each(messages, function (msg) { @@ -988,7 +988,7 @@ openerp.mail = function (session) { var self=this; var button = self.$('.oe_star:first'); - this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite]) + this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true]) .then(function (star) { self.is_favorite=star; if (self.is_favorite) { @@ -1040,6 +1040,7 @@ openerp.mail = function (session) { * use with browse, fetch... [O]= top parent */ init: function (parent, datasets, options) { + var self = this; this._super(parent, options); this.domain = options.domain || []; this.context = _.extend(options.context || {}); @@ -1052,14 +1053,15 @@ openerp.mail = function (session) { this.parent_message= parent.thread!= undefined ? parent : false ; // data of this thread - this.id = datasets.id || false, - this.last_id = datasets.last_id || false, - this.parent_id = datasets.parent_id || false, - - this.is_private = datasets.is_private || false, - this.author_id = datasets.author_id || false, - this.thread_level = (datasets.thread_level+1) || 0, - this.partner_ids = _.filter(datasets.partner_ids, function (partner) { return partner[0]!=datasets.author_id[0]; } ) + this.id = datasets.id || false; + this.last_id = datasets.last_id || false; + this.parent_id = datasets.parent_id || false; + this.is_private = datasets.is_private || false; + this.author_id = datasets.author_id || false; + this.thread_level = (datasets.thread_level+1) || 0; + this.partner_ids = datasets.partner_ids || []; + if (datasets.author_id) + this.partner_ids.push(datasets.author_id); this.messages = []; this.options.flat_mode = !!(this.options.display_indented_thread > this.thread_level ? this.options.display_indented_thread - this.thread_level : 0); @@ -1069,6 +1071,7 @@ openerp.mail = function (session) { this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model || 'mail.thread'); this.ds_message = new session.web.DataSetSearch(this, 'mail.message'); + this.render_mutex = new $.Mutex(); }, start: function () { @@ -1261,7 +1264,17 @@ openerp.mail = function (session) { (replace_context ? replace_context : this.context), // parent_id this.context.default_parent_id || undefined - ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')); + ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message') + ).done(this.proxy('message_fetch_set_read')); + }, + + message_fetch_set_read: function (message_list) { + if (! this.context.mail_read_set_read) return; + this.render_mutex.exec(_.bind(function() { + msg_ids = _.pluck(message_list, 'id'); + return this.ds_message.call('set_message_read', [ + msg_ids, true, false, this.context]); + }, this)); }, /** @@ -1622,6 +1635,7 @@ openerp.mail = function (session) { 'show_compose_message': this.view.is_action_enabled('edit'), }); this.node.context = { + 'mail_read_set_read': true, // set messages as read in Chatter 'default_res_id': this.view.datarecord.id || false, 'default_model': this.view.model || false, }; diff --git a/addons/mail/tests/test_mail_base.py b/addons/mail/tests/test_mail_base.py index 527375644df..fb53220ef1f 100644 --- a/addons/mail/tests/test_mail_base.py +++ b/addons/mail/tests/test_mail_base.py @@ -25,7 +25,7 @@ from openerp.tests import common class TestMailBase(common.TransactionCase): def _mock_smtp_gateway(self, *args, **kwargs): - return True + return args[2]['Message-Id'] def _init_mock_build_email(self): self._build_email_args_list = [] diff --git a/addons/mail/tests/test_message_read.py b/addons/mail/tests/test_message_read.py index ebb39c92a02..a4ff3685788 100644 --- a/addons/mail/tests/test_message_read.py +++ b/addons/mail/tests/test_message_read.py @@ -26,7 +26,8 @@ class test_mail_access_rights(TestMailBase): def test_00_message_read(self): """ Tests for message_read and expandables. """ - cr, uid, user_admin, group_pigs = self.cr, self.uid, self.user_admin, self.group_pigs + cr, uid, user_admin, user_raoul, group_pigs = self.cr, self.uid, self.user_admin, self.user_raoul, self.group_pigs + self.mail_group.message_subscribe_users(cr, uid, [group_pigs.id], [user_raoul.id]) pigs_domain = [('model', '=', 'mail.group'), ('res_id', '=', self.group_pigs_id)] # Data: create a discussion in Pigs (3 threads, with respectively 0, 4 and 4 answers) @@ -44,16 +45,20 @@ class test_mail_access_rights(TestMailBase): msg_ids = [msg_id10, msg_id9, msg_id8, msg_id7, msg_id6, msg_id5, msg_id4, msg_id3, msg_id2, msg_id1, msg_id0] ordered_msg_ids = [msg_id2, msg_id4, msg_id6, msg_id8, msg_id10, msg_id1, msg_id3, msg_id5, msg_id7, msg_id9, msg_id0] + # Test: raoul received notifications + raoul_notification_ids = self.mail_notification.search(cr, user_raoul.id, [('read', '=', False), ('message_id', 'in', msg_ids), ('partner_id', '=', user_raoul.partner_id.id)]) + self.assertEqual(len(raoul_notification_ids), 11, 'message_post: wrong number of produced notifications') + # Test: read some specific ids - read_msg_list = self.mail_message.message_read(cr, uid, ids=msg_ids[2:4], domain=[('body', 'like', 'dummy')]) + read_msg_list = self.mail_message.message_read(cr, user_raoul.id, ids=msg_ids[2:4], domain=[('body', 'like', 'dummy')], context={'mail_read_set_read': True}) read_msg_ids = [msg.get('id') for msg in read_msg_list] self.assertEqual(msg_ids[2:4], read_msg_ids, 'message_read with direct ids should read only the requested ids') # Test: read messages of Pigs through a domain, being thread or not threaded - read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200) + read_msg_list = self.mail_message.message_read(cr, user_raoul.id, domain=pigs_domain, limit=200) read_msg_ids = [msg.get('id') for msg in read_msg_list] self.assertEqual(msg_ids, read_msg_ids, 'message_read flat with domain on Pigs should equal all messages of Pigs') - read_msg_list = self.mail_message.message_read(cr, uid, domain=pigs_domain, limit=200, thread_level=1) + read_msg_list = self.mail_message.message_read(cr, user_raoul.id, domain=pigs_domain, limit=200, thread_level=1) read_msg_ids = [msg.get('id') for msg in read_msg_list] self.assertEqual(ordered_msg_ids, read_msg_ids, 'message_read threaded with domain on Pigs should equal all messages of Pigs, and sort them with newer thread first, last message last in thread') diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 44396e5d143..0062b5e01bd 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -214,6 +214,9 @@ class mail_compose_message(osv.TransientModel): new_attachments = email_dict.pop('attachments', []) post_values['attachments'] += new_attachments post_values.update(email_dict) + # automatically subscribe recipients if asked to + if context.get('mail_post_autofollow') and wizard.model and post_values.get('partner_ids'): + active_model_pool.message_subscribe(cr, uid, [res_id], [item[1] for item in post_values.get('partner_ids')], context=context) # post the message active_model_pool.message_post(cr, uid, [res_id], type='comment', subtype='mt_comment', context=context, **post_values) diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index e88c68379ab..e3047cd5edb 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -212,6 +212,7 @@ class wizard_user(osv.osv_memory): 'subject': _(WELCOME_EMAIL_SUBJECT) % data, 'body_html': '
%s
' % (_(WELCOME_EMAIL_BODY) % data), 'state': 'outgoing', + 'type': 'email', } mail_id = mail_mail.create(cr, uid, mail_values, context=this_context) return mail_mail.send(cr, uid, [mail_id], context=this_context) diff --git a/addons/portal_hr_employees/security/ir.model.access.csv b/addons/portal_hr_employees/security/ir.model.access.csv index 914649ef1fe..f8a1eb74c35 100644 --- a/addons/portal_hr_employees/security/ir.model.access.csv +++ b/addons/portal_hr_employees/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_hr_employee_user,hr.employee user,hr.model_hr_employee,portal.group_portal,1,0,0,0 -access_hr_employee_user,hr.employee user,hr.model_hr_employee,portal.group_anonymous,1,0,0,0 +access_hr_employee_portal,hr.employee user,hr.model_hr_employee,portal.group_portal,1,0,0,0 +access_hr_employee_anonymous,hr.employee user,hr.model_hr_employee,portal.group_anonymous,1,0,0,0 diff --git a/addons/portal_sale/portal_sale.py b/addons/portal_sale/portal_sale.py index 3556fd5ca0e..d4d5b64cea3 100644 --- a/addons/portal_sale/portal_sale.py +++ b/addons/portal_sale/portal_sale.py @@ -68,6 +68,7 @@ class sale_order(osv.Model): 'subject': 'Invitation to follow %s' % document.name_get()[0][1], 'body_html': 'You have been invited to follow %s' % document.name_get()[0][1], 'auto_delete': True, + 'type': 'email', } mail_obj = self.pool.get('mail.mail') mail_id = mail_obj.create(cr, uid, mail_values, context=context) @@ -128,6 +129,7 @@ class account_invoice(osv.Model): 'subject': 'Invitation to follow %s' % document.name_get()[0][1], 'body_html': 'You have been invited to follow %s' % document.name_get()[0][1], 'auto_delete': True, + 'type': 'email', } mail_obj = self.pool.get('mail.mail') mail_id = mail_obj.create(cr, uid, mail_values, context=context) diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index ac8b9bd7af8..68a81942a0f 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1163,7 +1163,7 @@ class procurement_order(osv.osv): return super(procurement_order, self)._product_virtual_get(cr, uid, order_point) -class mail_mail(osv.osv): +class mail_mail(osv.Model): _name = 'mail.mail' _inherit = 'mail.mail' @@ -1173,9 +1173,8 @@ class mail_mail(osv.osv): wf_service.trg_validate(uid, 'purchase.order', mail.res_id, 'send_rfq', cr) return super(mail_mail, self)._postprocess_sent_message(cr, uid, mail=mail, context=context) -mail_mail() -class product_template(osv.osv): +class product_template(osv.Model): _name = 'product.template' _inherit = 'product.template' _columns = { @@ -1185,13 +1184,14 @@ class product_template(osv.osv): 'purchase_ok': 1, } -product_template() -class mail_compose_message(osv.osv): +class mail_compose_message(osv.Model): _inherit = 'mail.compose.message' + def send_mail(self, cr, uid, ids, context=None): context = context or {} if context.get('default_model') == 'purchase.order' and context.get('default_res_id'): + context = dict(context, mail_post_autofollow=True) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'purchase.order', context['default_res_id'], 'send_rfq', cr) return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 0a5d8f01af5..e22b99656c2 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -977,11 +977,13 @@ class sale_order_line(osv.osv): return super(sale_order_line, self).unlink(cr, uid, ids, context=context) -class mail_compose_message(osv.osv): +class mail_compose_message(osv.Model): _inherit = 'mail.compose.message' + def send_mail(self, cr, uid, ids, context=None): context = context or {} if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'): + context = dict(context, mail_post_autofollow=True) wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'sale.order', context['default_res_id'], 'quotation_sent', cr) return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)