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