[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.
This commit is contained in:
Thibault Delavallée
2016-09-01 12:57:56 +02:00
parent 2194993763
commit 72dfcae2a4
15 changed files with 202 additions and 18 deletions
+1
View File
@@ -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
+13
View File
@@ -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
+45 -8
View File
@@ -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,
})
+30
View File
@@ -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)')
+1 -1
View File
@@ -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():
+18 -2
View File
@@ -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
+3
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
10 access_mail_followers_portal mail.followers.portal model_mail_followers base.group_portal 1 1 1 0
11 access_mail_followers_user mail.followers.user model_mail_followers base.group_user 1 1 1 0
12 access_mail_followers_system mail.followers.system model_mail_followers base.group_system 1 1 1 1
13 access_mail_notification_portal mail.notification.portal model_mail_notification base.group_portal 1 0 0 0
14 access_mail_notification_user mail.notification.user model_mail_notification base.group_user 1 1 1 0
15 access_mail_notification_system mail.notification.system model_mail_notification base.group_system 1 1 1 1
16 access_mail_channel_all mail.group.all model_mail_channel 1 0 0 0
17 access_mail_channel_user mail.group.user model_mail_channel base.group_user 1 1 1 1
18 access_mail_channel_partner_public mail.channel.partner.public model_mail_channel_partner base.group_public 1 0 0 0
+10
View File
@@ -23,6 +23,16 @@
<field name="perm_read" eval="False"/>
</record>
<record id="ir_rule_mail_notifications_group_user" model="ir.rule">
<field name="name">mail.notifications: group_user: write its own entries</field>
<field name="model_id" ref="model_mail_notification"/>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="domain_force">[('res_partner_id', '=', user.partner_id.id)]</field>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
<field name="perm_read" eval="False"/>
</record>
<record id="mail_message_subtype_rule_public" model="ir.rule">
<field name="name">mail.message.subtype: portal/public: read public subtypes</field>
<field name="model_id" ref="model_mail_message_subtype"/>
@@ -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,
+1
View File
@@ -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,
});
@@ -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,
};
},
+1
View File
@@ -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 = [];
+43 -3
View File
@@ -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 {
+15
View File
@@ -94,6 +94,21 @@
<t t-if="message.origin_id &amp;&amp; (message.origin_id !== options.channel_id)">
(from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
</t>
<span t-if="options.display_email_icon &amp;&amp; message.customer_email_data.length" class="o_thread_tooltip_container">
<i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope-o'"/>
<span class="o_thread_tooltip_content">
<t t-foreach="message.customer_email_data" t-as="customer">
<span>
<t t-if="customer[2] == 'sent'"><i class='fa fa-check'/></t>
<t t-if="customer[2] == 'bounce'"><i class='fa fa-exclamation'/></t>
<t t-if="customer[2] == 'exception'"><i class='fa fa-exclamation'/></t>
<t t-if="customer[2] == 'ready'"><i class='fa fa-send-o'/></t>
<t t-esc="customer[1]"/>
</span>
<br />
</t>
</span>
</span>
<span>
<i t-if="options.display_stars &amp;&amp; !message.is_system_notification"
t-att-class="'fa fa-lg o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
+18 -4
View File
@@ -59,10 +59,24 @@
</page>
<page string="Recipients">
<group>
<field name="partner_ids" widget="many2many_tags"/>
<field name="channel_ids" widget="many2many_tags"/>
<field name="needaction_partner_ids" widget="many2many_tags"/>
<field name="starred_partner_ids" widget="many2many_tags"/>
<group>
<field name="partner_ids" widget="many2many_tags"/>
<field name="channel_ids" widget="many2many_tags"/>
</group>
<group>
<field name="needaction_partner_ids" widget="many2many_tags"/>
<field name="starred_partner_ids" widget="many2many_tags"/>
</group>
</group>
<group>
<field name="notification_ids">
<tree>
<field name="res_partner_id"/>
<field name="is_read"/>
<field name="is_email"/>
<field name="email_status"/>
</tree>
</field>
</group>
</page>
<page string="Tracking">