From 72dfcae2a4b03623e780b857abf37c7aec5b8456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 7 Jul 2016 16:03:05 +0200 Subject: [PATCH] [IMP] mail: improve notification management for customers Currently it is impossible in Discuss to know whether an email has been sent to a customer and whether it failed or bounced. A notified partner has an entry in the needaction m2m table. In this commit we decorate this table to add fields about the email notification: is an email sent, did it failed, did it bounce. This information is kept only for customers. Internal users does not use this information. Moreover their notification is deleted once the message is read in the Chatter. This avoids having a notification table that grows quickly. Chatter now holds a new icon for email details. It allows to know on a thread status of emails sent to customers. --- addons/mail/models/__init__.py | 1 + addons/mail/models/mail_mail.py | 13 ++++++ addons/mail/models/mail_message.py | 53 ++++++++++++++++++---- addons/mail/models/mail_notification.py | 30 ++++++++++++ addons/mail/models/mail_thread.py | 2 +- addons/mail/models/res_partner.py | 20 +++++++- addons/mail/security/ir.model.access.csv | 3 ++ addons/mail/security/mail_security.xml | 10 ++++ addons/mail/static/src/js/chat_manager.js | 2 + addons/mail/static/src/js/chat_window.js | 1 + addons/mail/static/src/js/client_action.js | 1 + addons/mail/static/src/js/thread.js | 1 + addons/mail/static/src/less/thread.less | 46 +++++++++++++++++-- addons/mail/static/src/xml/thread.xml | 15 ++++++ addons/mail/views/mail_message_views.xml | 22 +++++++-- 15 files changed, 202 insertions(+), 18 deletions(-) create mode 100644 addons/mail/models/mail_notification.py diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py index 1ba9746eb4e..2426a6dc9e6 100644 --- a/addons/mail/models/__init__.py +++ b/addons/mail/models/__init__.py @@ -4,6 +4,7 @@ import mail_message_subtype import mail_tracking_value import mail_alias import mail_followers +import mail_notification import mail_message import mail_mail import mail_thread diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 5d90c54c0af..7297c763953 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -129,6 +129,19 @@ class MailMail(models.Model): :param browse_record mail: the mail that was just sent :return: True """ + notif_emails = self.filtered(lambda email: email.notification) + if notif_emails: + notifications = self.env['mail.notification'].search([ + ('mail_message_id', 'in', notif_emails.mapped('mail_message_id').ids), + ('is_email', '=', True)]) + if mail_sent: + notifications.write({ + 'email_status': 'sent', + }) + else: + notifications.write({ + 'email_status': 'exception', + }) if mail_sent: self.sudo().filtered(lambda self: self.auto_delete).unlink() return True diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 8d6a933221b..0f8054d958a 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -82,6 +82,10 @@ class Message(models.Model): help='Need Action') channel_ids = fields.Many2many( 'mail.channel', 'mail_message_mail_channel_rel', string='Channels') + # notifications + notification_ids = fields.One2many( + 'mail.notification', 'mail_message_id', 'Notifications', + auto_join=True, copy=False) # user interface starred_partner_ids = fields.Many2many( 'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By') @@ -105,7 +109,10 @@ class Message(models.Model): @api.multi def _get_needaction(self): """ Need action on a mail.message = notified on my channel """ - my_messages = self.sudo().filtered(lambda msg: self.env.user.partner_id in msg.needaction_partner_ids) + my_messages = self.env['mail.notification'].sudo().search([ + ('mail_message_id', 'in', self.ids), + ('res_partner_id', '=', self.env.user.partner_id.id), + ('is_read', '=', False)]).mapped('mail_message_id') for message in self: message.needaction = message in my_messages @@ -117,8 +124,8 @@ class Message(models.Model): @api.model def _search_needaction(self, operator, operand): if operator == '=' and operand: - return [('needaction_partner_ids', 'in', self.env.user.partner_id.id)] - return [('needaction_partner_ids', 'not in', self.env.user.partner_id.id)] + return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)] + return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)] @api.depends('starred_partner_ids') def _get_starred(self): @@ -147,7 +154,8 @@ class Message(models.Model): """ Remove all needactions of the current partner. If channel_ids is given, restrict to messages written in one of those channels. """ partner_id = self.env.user.partner_id.id - if domain is None: + delete_mode = not self.env.user.share # delete employee notifs, keep customer ones + if domain is None and delete_mode: query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s" args = [(partner_id,)] if channel_ids: @@ -170,7 +178,14 @@ class Message(models.Model): if channel_ids: msg_domain += [('channel_ids', 'in', channel_ids)] unread_messages = self.search(expression.AND([msg_domain, domain])) - unread_messages.sudo().write({'needaction_partner_ids': [(3, partner_id)]}) + notifications = self.env['mail.notification'].sudo().search([ + ('mail_message_id', 'in', unread_messages.ids), + ('res_partner_id', '=', self.env.user.partner_id.id), + ('is_read', '=', False)]) + if delete_mode: + notifications.unlink() + else: + notifications.write({'is_read': True}) ids = unread_messages.mapped('id') notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids} @@ -193,14 +208,20 @@ class Message(models.Model): def set_message_done(self): """ Remove the needaction from messages for the current partner. """ partner_id = self.env.user.partner_id - messages = self.filtered(lambda msg: partner_id in msg.needaction_partner_ids) - if not len(messages): + delete_mode = not self.env.user.share # delete employee notifs, keep customer ones + + notifications = self.env['mail.notification'].sudo().search([ + ('mail_message_id', 'in', self.ids), + ('res_partner_id', '=', partner_id.id), + ('is_read', '=', False)]) + + if not notifications: return - messages.sudo().write({'needaction_partner_ids': [(3, partner_id.id)]}) # notifies changes in messages through the bus. To minimize the number of # notifications, we need to group the messages depending on their channel_ids groups = [] + messages = notifications.mapped('mail_message_id') current_channel_ids = messages[0].channel_ids current_group = [] for record in messages: @@ -215,6 +236,11 @@ class Message(models.Model): current_group = [record.id] current_channel_ids = record.channel_ids + if delete_mode: + notifications.unlink() + else: + notifications.write({'is_read': True}) + for (msg_ids, channel_ids) in groups: notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]} self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id.id), notification) @@ -270,6 +296,8 @@ class Message(models.Model): partners |= message.partner_ids elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log) partners |= message.partner_ids + if message.needaction_partner_ids: # notified + partners |= message.needaction_partner_ids if message.attachment_ids: attachments |= message.attachment_ids if message.tracking_value_ids: @@ -311,6 +339,11 @@ class Message(models.Model): else: partner_ids = [partner_tree[partner.id] for partner in message.partner_ids if partner.id in partner_tree] + + customer_email_data = [] + for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share): + customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status)) + attachment_ids = [] for attachment in message.attachment_ids: if attachment.id in attachments_tree: @@ -323,6 +356,10 @@ class Message(models.Model): message_dict.update({ 'author_id': author, 'partner_ids': partner_ids, + 'customer_email_status': (all(d[2] == 'sent' for d in customer_email_data) and 'sent') or + (any(d[2] == 'exception' for d in customer_email_data) and 'exception') or + (any(d[2] == 'bounce' for d in customer_email_data) and 'bounce') or 'ready', + 'customer_email_data': customer_email_data, 'attachment_ids': attachment_ids, 'tracking_value_ids': tracking_value_ids, }) diff --git a/addons/mail/models/mail_notification.py b/addons/mail/models/mail_notification.py new file mode 100644 index 00000000000..e14754b7e8f --- /dev/null +++ b/addons/mail/models/mail_notification.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from odoo import api, fields, models + + +class Notification(models.Model): + _name = 'mail.notification' + _table = 'mail_message_res_partner_needaction_rel' + _rec_name = 'res_partner_id' + _log_access = False + _description = 'Message Notifications' + + mail_message_id = fields.Many2one( + 'mail.message', 'Message', index=True, ondelete='cascade', required=True) + res_partner_id = fields.Many2one( + 'res.partner', 'Needaction Recipient', index=True, ondelete='cascade', required=True) + is_read = fields.Boolean('Is Read', index=True) + is_email = fields.Boolean('Sent by Email', index=True) + email_status = fields.Selection([ + ('ready', 'Ready to Send'), + ('sent', 'Sent'), + ('bounce', 'Bounced'), + ('exception', 'Exception')], 'Email Status', + default='ready', index=True) + + @api.model_cr + def init(self): + self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_email_status_mail_message_id',)) + if not self._cr.fetchone(): + self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)') diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 9a84e8546a4..cea21f5c668 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -190,7 +190,7 @@ class MailThread(models.AbstractModel): # search for unread messages, directly in SQL to improve performances self._cr.execute(""" SELECT msg.res_id FROM mail_message msg RIGHT JOIN mail_message_res_partner_needaction_rel rel - ON rel.mail_message_id = msg.id AND rel.res_partner_id = %s + ON rel.mail_message_id = msg.id AND rel.res_partner_id = %s AND (rel.is_read = false OR rel.is_read IS NULL) WHERE msg.model = %s AND msg.res_id in %s""", (self.env.user.partner_id.id, self._name, tuple(self.ids),)) for result in self._cr.fetchall(): diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 2e47dacc1dc..7c3a96ba36b 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -133,6 +133,17 @@ class Partner(models.Model): emails |= self.env['mail.mail'].create(create_values) return emails, recipients_nbr + @api.model + def _notify_udpate_notifications(self, emails): + for email in emails: + notifications = self.env['mail.notification'].sudo().search([ + ('mail_message_id', '=', email.mail_message_id.id), + ('res_partner_id', 'in', email.recipient_ids.ids)]) + notifications.write({ + 'is_email': True, + 'email_status': 'ready', + }) + @api.multi def _notify(self, message, force_send=False, user_signature=True): # TDE TODO: model-dependant ? (like customer -> always email ?) @@ -166,7 +177,6 @@ class Partner(models.Model): base_template_ctx['signature'] = False base_mail_values = self._notify_prepare_email_values(message) - # classify recipients: actions / no action if message.model and message.res_id and hasattr(self.env[message.model], '_message_notification_recipients'): recipients = self.env[message.model].browse(message.res_id)._message_notification_recipients(message, self) @@ -185,6 +195,9 @@ class Partner(models.Model): fol_values = template_fol.generate_email(message.id, fields=['body_html', 'subject']) # send email new_emails, new_recipients_nbr = self._notify_send(fol_values['body'], fol_values['subject'], recipient_template_values['followers'], **base_mail_values) + # update notifications + self._notify_udpate_notifications(new_emails) + emails |= new_emails recipients_nbr += new_recipients_nbr if recipient_template_values['not_followers']: @@ -196,6 +209,9 @@ class Partner(models.Model): not_values = template_not.generate_email(message.id, fields=['body_html', 'subject']) # send email new_emails, new_recipients_nbr = self._notify_send(not_values['body'], not_values['subject'], recipient_template_values['not_followers'], **base_mail_values) + # update notifications + self._notify_udpate_notifications(new_emails) + emails |= new_emails recipients_nbr += new_recipients_nbr @@ -226,7 +242,7 @@ class Partner(models.Model): self.env.cr.execute(""" SELECT count(*) as needaction_count FROM mail_message_res_partner_needaction_rel R - WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,)) + WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,)) return self.env.cr.dictfetchall()[0].get('needaction_count') _logger.error('Call to needaction_count without partner_id') return 0 diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 384e99d91fa..59a37569bfe 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -10,6 +10,9 @@ access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0 access_mail_followers_portal,mail.followers.portal,model_mail_followers,base.group_portal,1,1,1,0 access_mail_followers_user,mail.followers.user,model_mail_followers,base.group_user,1,1,1,0 access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1 +access_mail_notification_portal,mail.notification.portal,model_mail_notification,base.group_portal,1,0,0,0 +access_mail_notification_user,mail.notification.user,model_mail_notification,base.group_user,1,1,1,0 +access_mail_notification_system, mail.notification.system,model_mail_notification,base.group_system,1,1,1,1 access_mail_channel_all,mail.group.all,model_mail_channel,,1,0,0,0 access_mail_channel_user,mail.group.user,model_mail_channel,base.group_user,1,1,1,1 access_mail_channel_partner_public,mail.channel.partner.public,model_mail_channel_partner,base.group_public,1,0,0,0 diff --git a/addons/mail/security/mail_security.xml b/addons/mail/security/mail_security.xml index deab4c51438..65495db6925 100644 --- a/addons/mail/security/mail_security.xml +++ b/addons/mail/security/mail_security.xml @@ -23,6 +23,16 @@ + + mail.notifications: group_user: write its own entries + + + [('res_partner_id', '=', user.partner_id.id)] + + + + + mail.message.subtype: portal/public: read public subtypes diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index d37e9dcf282..2cf6090871f 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -132,6 +132,8 @@ function make_message (data) { attachment_ids: data.attachment_ids || [], subject: data.subject, email_from: data.email_from, + customer_email_status: data.customer_email_status, + customer_email_data: data.customer_email_data, record_name: data.record_name, tracking_value_ids: data.tracking_value_ids, channel_ids: data.channel_ids, diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index da8900b8230..7bdece26fb3 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -32,6 +32,7 @@ return Widget.extend({ autofocus: true, display_stars: true, display_reply_icon: false, + display_email_icon: false, placeholder: _t("Say something"), input_less: false, }); diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index f94997c946c..7de0b1ac83b 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -491,6 +491,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, { display_empty_channel: !messages.length && !this.domain.length, display_no_match: !messages.length && this.domain.length, display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox", + display_email_icon: false, display_reply_icon: true, }; }, diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index ea593d21dfa..2176eff55fa 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -72,6 +72,7 @@ var Thread = Widget.extend({ display_document_link: true, display_avatar: true, squash_close_messages: true, + display_email_icon: true, display_reply_icon: false, }); this.expanded_msg_ids = []; diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index 5da7d2b4963..7a9efa45731 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -70,7 +70,8 @@ } } - i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply { + + i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply, i.o_thread_message_email { cursor: pointer; opacity: 0; &.fa-star { @@ -84,7 +85,7 @@ opacity: @mail-thread-side-date-opacity; } - i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply { + i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply, i.o_thread_message_email { opacity: @mail-thread-icon-opacity; &:hover { opacity: 1; @@ -146,9 +147,48 @@ } } - .o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply { + .o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply, .o_thread_message_email { padding: 4px; } + + i.o_thread_message_email { + &.o_thread_message_email_ready { + color: grey; + } + &.o_thread_message_email_exception { + color: red; + opacity: 1; + } + &.o_thread_message_email_bounce { + color: red; + opacity: 1; + } + } + + .o_thread_tooltip_container { + display: inline; + position: relative; + + .o_thread_tooltip_content { + top: 10px; + background: @gray-lighter; + padding: 4px; + border: solid @gray-lighter 1px; + border-radius: 5px; + color: gray; + width: 300px; + opacity: 0; + position: absolute; + transition: opacity 0.5s; + i.fa-exclamation { + color: red; + } + } + + .o_thread_tooltip:hover + .o_thread_tooltip_content { + opacity: 1; + } + } } } .o_thread_title { diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 9896cb0ce6e..47e84e38f60 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -94,6 +94,21 @@ (from #) + + + + + + + + + + + +
+
+
+
- - - - + + + + + + + + + + + + + + + + + +