[IMP] mail_tracking: mail.thread filter for tracking errors

- Any model inheriting from mail.thread will have a filter available to
obtain records with errors in their messages trackings.
- The messages can be marked as done to avoid false positives when the
issues are solved.
This commit is contained in:
David 2019-07-05 14:20:27 +02:00 committed by Jasmin Solanki
parent 28263adb84
commit 61463fc530
25 changed files with 1085 additions and 40 deletions

View File

@ -52,6 +52,7 @@ status icon will appear just right to name of notified partner.
These are all available status icons: These are all available status icons:
<<<<<<< HEAD
.. |sent| image:: mail_tracking/static/src/img/sent.png .. |sent| image:: mail_tracking/static/src/img/sent.png
:width: 10px :width: 10px
@ -68,9 +69,27 @@ These are all available status icons:
:width: 10px :width: 10px
.. |unknown| image:: mail_tracking/static/src/img/unknown.png .. |unknown| image:: mail_tracking/static/src/img/unknown.png
=======
.. |sent| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/sent.png
:width: 10px :width: 10px
.. |cc| image:: static/src/img/cc.png .. |delivered| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/delivered.png
:width: 15px
.. |opened| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/opened.png
:width: 15px
.. |error| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/error.png
:width: 10px
.. |waiting| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/waiting.png
:width: 10px
.. |unknown| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/unknown.png
>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
:width: 10px
.. |cc| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/cc.png
:width: 10px :width: 10px
|unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never'
@ -88,10 +107,33 @@ These are all available status icons:
|cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown' |cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown'
<<<<<<< HEAD
If you want to see all tracking emails and events you can go to If you want to see all tracking emails and events you can go to
* Settings > Technical > Email > Tracking emails * Settings > Technical > Email > Tracking emails
* Settings > Technical > Email > Tracking events * Settings > Technical > Email > Tracking events
=======
When the message generates and 'error' status, it will apear on discuss 'Failed'
channel. Any view that uses 'mail_thread' widget can show the failed messages
too.
* Discuss
.. image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png
* Chatter
.. image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_widget.png
Known issues / Roadmap
======================
* Handle message updates on discuss 'channel_failed' instead of showing the
'outdated' message.
* Adapt chat_manager changes in v12
* Adapt discuss changes in v12
* Add pivot for tracking events and mail trackings
>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
Bug Tracker Bug Tracker
=========== ===========

View File

@ -26,10 +26,17 @@
"views/assets.xml", "views/assets.xml",
"views/mail_tracking_email_view.xml", "views/mail_tracking_email_view.xml",
"views/mail_tracking_event_view.xml", "views/mail_tracking_event_view.xml",
"views/mail_message_view.xml",
"views/res_partner_view.xml", "views/res_partner_view.xml",
"wizard/mail_compose_message_view.xml",
], ],
"qweb": [ "qweb": [
"static/src/xml/mail_tracking.xml", "static/src/xml/mail_tracking.xml",
"static/src/xml/failed_message.xml",
"static/src/xml/client_action.xml",
],
'demo': [
'demo/demo.xml',
], ],
"pre_init_hook": "pre_init_hook", "pre_init_hook": "pre_init_hook",
} }

View File

@ -3,7 +3,9 @@
import werkzeug import werkzeug
from psycopg2 import OperationalError from psycopg2 import OperationalError
from odoo import api, http, registry, SUPERUSER_ID from odoo import api, http, registry, SUPERUSER_ID, _
from odoo.addons.mail.controllers.main import MailController
from odoo.http import request
import logging import logging
import base64 import base64
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -37,7 +39,7 @@ def _env_get(db, callback, tracking_id, event_type, **kw):
return res return res
class MailTrackingController(http.Controller): class MailTrackingController(MailController):
def _request_metadata(self): def _request_metadata(self):
request = http.request.httprequest request = http.request.httprequest
@ -85,3 +87,22 @@ class MailTrackingController(http.Controller):
response.mimetype = 'image/gif' response.mimetype = 'image/gif'
response.data = base64.b64decode(BLANK) response.data = base64.b64decode(BLANK)
return response return response
@http.route()
def mail_client_action(self):
values = super().mail_client_action()
values['channel_slots']['channel_channel'].append({
'id': 'channel_failed',
'name': _("Failed"),
'uuid': None,
'state': 'open',
'is_minimized': False,
'channel_type': 'static',
'public': False,
'mass_mailing': None,
'group_based_subscription': None,
})
values.update({
'failed_counter': request.env['mail.message'].get_failed_count(),
})
return values

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Failed Message A -->
<record id="mail_message_failed" model="mail.message">
<field name="model">res.partner</field>
<field name="res_id" ref="base.partner_demo" />
<field name="message_type">comment</field>
<field name="subtype_id" ref="mail.mt_comment" />
<field name="mail_tracking_needs_action">1</field>
<field name="body"><![CDATA[<p>This is a failed message</p>]]></field>
<field name="email_from">res1@yourcompany.example.com</field>
<field name="author_id" ref="base.res_partner_1" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field name="subject">Failed Message</field>
</record>
<record id="mail_tracking_email_failed" model="mail.tracking.email">
<field name="name">Failed Message</field>
<field name="mail_message_id" ref="mail_message_failed" />
<field name="partner_id" ref="base.res_partner_1" />
<field name="recipient">res1@yourcompany.example.com</field>
<field name="sender">demo@yourcompany.example.com</field>
<field name="state">error</field>
<field name="time" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
</record>
<!-- Failed Message B -->
<record id="mail_message_failed_b" model="mail.message">
<field name="model">res.partner</field>
<field name="res_id" ref="base.partner_demo" />
<field name="message_type">comment</field>
<field name="subtype_id" ref="mail.mt_comment" />
<field name="mail_tracking_needs_action">1</field>
<field name="body"><![CDATA[<p>This is another failed message</p>]]></field>
<field name="email_from">res10@yourcompany.example.com</field>
<field name="author_id" ref="base.res_partner_10" />
<field name="partner_ids" eval="[(6, 0, [ref('base.partner_demo')])]" />
<field name="subject">Failed Message</field>
</record>
<record id="mail_tracking_email_failed_b" model="mail.tracking.email">
<field name="name">Failed Message</field>
<field name="mail_message_id" ref="mail_message_failed_b" />
<field name="partner_id" ref="base.res_partner_10" />
<field name="recipient">res10@yourcompany.example.com</field>
<field name="sender">demo@yourcompany.example.com</field>
<field name="state">error</field>
<field name="time" eval="DateTime.today().strftime('%Y-%m-%d %H:%M')"/>
</record>
</data>
</odoo>

View File

@ -6,5 +6,6 @@ from . import mail_mail
from . import mail_message from . import mail_message
from . import mail_tracking_email from . import mail_tracking_email
from . import mail_tracking_event from . import mail_tracking_event
from . import mail_composer
from . import res_partner from . import res_partner
from . import mail_thread from . import mail_thread

View File

@ -0,0 +1,30 @@
# Copyright 2019 Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, fields, api
class MailComposer(models.TransientModel):
_inherit = 'mail.compose.message'
hide_followers = fields.Boolean(string="Hide follower message",
default=False)
@api.multi
def send_mail(self, auto_commit=False):
""" This method marks as reviewed the message when using the 'Retry'
option in the mail_failed_message widget"""
message = self.env['mail.message'].browse(
self._context.get('message_id'))
if message.exists():
message.mail_tracking_needs_action = False
return super().send_mail(auto_commit=auto_commit)
@api.model
def get_record_data(self, values):
values = super(MailComposer, self).get_record_data(values)
if self._context.get('default_hide_followers', False):
values['partner_ids'] = [
(6, 0, self._context.get('default_partner_ids', list()))
]
return values

View File

@ -12,6 +12,20 @@ class MailMessage(models.Model):
# Recipients # Recipients
email_cc = fields.Char("Cc", help='Additional recipients that receive a ' email_cc = fields.Char("Cc", help='Additional recipients that receive a '
'"Carbon Copy" of the e-mail') '"Carbon Copy" of the e-mail')
mail_tracking_ids = fields.One2many(
comodel_name='mail.tracking.email',
inverse_name='mail_message_id',
string="Mail Trackings",
)
mail_tracking_needs_action = fields.Boolean(
help="The message tracking will be considered"
" to filter tracking issues",
default=False,
)
@api.model
def get_failed_states(self):
return {'error', 'rejected', 'spam', 'bounced', 'soft-bounced'}
def _tracking_status_map_get(self): def _tracking_status_map_get(self):
return { return {
@ -95,6 +109,17 @@ class MailMessage(models.Model):
}) })
return res return res
@api.multi
def _get_failed_message(self):
res = {}
for message in self:
res.update({
message.id: message.mail_tracking_needs_action
and bool(message.mail_tracking_ids.filtered(
lambda x: x.state in self.get_failed_states()))
})
return res
@api.model @api.model
def _message_read_dict_postprocess(self, messages, message_tree): def _message_read_dict_postprocess(self, messages, message_tree):
res = super(MailMessage, self)._message_read_dict_postprocess( res = super(MailMessage, self)._message_read_dict_postprocess(
@ -103,11 +128,77 @@ class MailMessage(models.Model):
mail_messages = self.browse(mail_message_ids) mail_messages = self.browse(mail_message_ids)
partner_trackings = mail_messages.tracking_status() partner_trackings = mail_messages.tracking_status()
email_cc = mail_messages._get_email_cc() email_cc = mail_messages._get_email_cc()
failed_message = mail_messages._get_failed_message()
for message_dict in messages: for message_dict in messages:
mail_message_id = message_dict.get('id', False) mail_message_id = message_dict.get('id', False)
if mail_message_id: if mail_message_id:
message_dict.update({ message_dict.update({
'partner_trackings': partner_trackings[mail_message_id], 'partner_trackings': partner_trackings[mail_message_id],
'email_cc': email_cc[mail_message_id], 'email_cc': email_cc[mail_message_id],
'failed_message': failed_message[mail_message_id],
}) })
return res return res
@api.model
def _prepare_dict_failed_message(self, message):
failed_trackings = message.mail_tracking_ids.filtered(
lambda x: x.state in self.get_failed_states())
failed_partners = failed_trackings.mapped('partner_id')
failed_recipients = failed_partners.name_get()
return {
'id': message.id,
'date': message.date,
'author_id': message.author_id.name_get()[0],
'body': message.body,
'failed_recipients': failed_recipients,
}
@api.multi
def get_failed_messages(self):
return [self._prepare_dict_failed_message(msg) for msg in self]
@api.multi
def toggle_tracking_status(self):
"""Toggle message tracking action needed to ignore them in the tracking
issues filter"""
self.mail_tracking_needs_action = not self.mail_tracking_needs_action
return self.mail_tracking_needs_action
def _get_failed_message_domain(self):
return [
('mail_tracking_ids.state', 'in', list(self.get_failed_states())),
('mail_tracking_needs_action', '=', True)
]
@api.model
def get_failed_count(self):
""" Gets the number of failed messages """
return self.search_count(self._get_failed_message_domain())
@api.model
def message_fetch(self, domain, limit=20):
# HACK: Because can't modify the domain in discuss JS to search the
# failed messages we force the change here to clean it of
# not valid criterias
if self.env.context.get('filter_failed_message'):
domain = self._get_failed_message_domain()
return super().message_fetch(domain, limit=limit)
@api.multi
def _notify(self, force_send=False, send_after_commit=True,
user_signature=True):
self_sudo = self.sudo()
hide_followers = self_sudo._context.get('default_hide_followers',
False)
if hide_followers:
# HACK: Because Odoo uses subtype to found message followers
# whe modify it to False to avoid include them.
orig_subtype_id = self_sudo.subtype_id
self_sudo.subtype_id = False
res = super()._notify(force_send=force_send,
send_after_commit=send_after_commit,
user_signature=user_signature)
if hide_followers:
self_sudo.subtype_id = orig_subtype_id
return res

View File

@ -1,14 +1,22 @@
# Copyright 2019 Alexandre Díaz # Copyright 2019 Alexandre Díaz
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api, _ from odoo import fields, models, api, _
from email.utils import getaddresses from email.utils import getaddresses
from odoo.tools import email_split_and_format from odoo.tools import email_split_and_format
from lxml import etree
class MailThread(models.AbstractModel): class MailThread(models.AbstractModel):
_inherit = "mail.thread" _inherit = "mail.thread"
failed_message_ids = fields.One2many(
'mail.message', 'res_id', string='Failed Messages',
domain=lambda self:
[('model', '=', self._name)]
+ self.env['mail.message']._get_failed_message_domain(),
auto_join=True)
@api.multi @api.multi
@api.returns('self', lambda value: value.id) @api.returns('self', lambda value: value.id)
def message_post(self, *args, **kwargs): def message_post(self, *args, **kwargs):
@ -22,6 +30,8 @@ class MailThread(models.AbstractModel):
@api.multi @api.multi
def message_get_suggested_recipients(self): def message_get_suggested_recipients(self):
"""Adds email Cc recipients as suggested recipients.
If the recipient have an res.partner uses it."""
res = super().message_get_suggested_recipients() res = super().message_get_suggested_recipients()
ResPartnerObj = self.env['res.partner'] ResPartnerObj = self.env['res.partner']
email_cc_formated_list = [] email_cc_formated_list = []
@ -42,3 +52,52 @@ class MailThread(models.AbstractModel):
record._message_add_suggested_recipient( record._message_add_suggested_recipient(
res, partner=partner, reason=_('Cc')) res, partner=partner, reason=_('Cc'))
return res return res
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False,
submenu=False):
"""Add filters for failed messages.
These filters will show up on any form or search views of any
model inheriting from ``mail.thread``.
"""
res = super().fields_view_get(
view_id=view_id, view_type=view_type, toolbar=toolbar,
submenu=submenu)
if view_type != 'search' and view_type != 'form':
return res
doc = etree.XML(res['arch'])
if view_type == 'search':
# Modify view to add new filter element
nodes = doc.xpath("//search")
if nodes:
# Create filter element
new_filter = etree.Element(
'filter', {
'string': _('Failed sent messages'),
'name': "failed_message_ids",
'domain': str([
['failed_message_ids.mail_tracking_ids.state',
'in',
list(
self.env['mail.message'].get_failed_states()
)],
['failed_message_ids.mail_tracking_needs_action',
'=', True]
])
})
nodes[0].append(etree.Element('separator'))
nodes[0].append(new_filter)
elif view_type == 'form':
# Modify view to add new field element
nodes = doc.xpath(
"//field[@name='message_ids' and @widget='mail_thread']")
if nodes:
# Create field
field_failed_messages = etree.Element('field', {
'name': 'failed_message_ids',
'widget': 'mail_failed_message',
})
nodes[0].addprevious(field_failed_messages)
res['arch'] = etree.tostring(doc, encoding='unicode')
return res

View File

@ -92,13 +92,21 @@ class MailTrackingEmail(models.Model):
string="Tracking events", comodel_name='mail.tracking.event', string="Tracking events", comodel_name='mail.tracking.event',
inverse_name='tracking_email_id', readonly=True) inverse_name='tracking_email_id', readonly=True)
@api.multi
def write(self, vals):
if vals.get('state') in self.env['mail.message'].get_failed_states():
self.mapped('mail_message_id').write({
'mail_tracking_needs_action': True,
})
super().write(vals)
@api.model @api.model
def email_is_bounced(self, email): def email_is_bounced(self, email):
if not email: if not email:
return False return False
res = self._email_last_tracking_state(email) res = self._email_last_tracking_state(email)
return res and res[0].get('state', '') in ['rejected', 'error', return res and res[0].get('state', '') in {'rejected', 'error',
'spam', 'bounced'] 'spam', 'bounced'}
@api.model @api.model
def _email_last_tracking_state(self, email): def _email_last_tracking_state(self, email):

View File

@ -0,0 +1,5 @@
* Handle message updates on discuss 'channel_failed' instead of showing the
'outdated' message.
* Adapt chat_manager changes in v12
* Adapt discuss changes in v12
* Add pivot for tracking events and mail trackings

View File

@ -4,25 +4,25 @@ status icon will appear just right to name of notified partner.
These are all available status icons: These are all available status icons:
.. |sent| image:: mail_tracking/static/src/img/sent.png .. |sent| image:: ../static/src/img/sent.png
:width: 10px :width: 10px
.. |delivered| image:: mail_tracking/static/src/img/delivered.png .. |delivered| image:: ../static/src/img/delivered.png
:width: 15px :width: 15px
.. |opened| image:: mail_tracking/static/src/img/opened.png .. |opened| image:: ../static/src/img/opened.png
:width: 15px :width: 15px
.. |error| image:: mail_tracking/static/src/img/error.png .. |error| image:: ../static/src/img/error.png
:width: 10px :width: 10px
.. |waiting| image:: mail_tracking/static/src/img/waiting.png .. |waiting| image:: ../static/src/img/waiting.png
:width: 10px :width: 10px
.. |unknown| image:: mail_tracking/static/src/img/unknown.png .. |unknown| image:: ../static/src/img/unknown.png
:width: 10px :width: 10px
.. |cc| image:: static/src/img/cc.png .. |cc| image:: ../static/src/img/cc.png
:width: 10px :width: 10px
|unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never'
@ -44,3 +44,15 @@ If you want to see all tracking emails and events you can go to
* Settings > Technical > Email > Tracking emails * Settings > Technical > Email > Tracking emails
* Settings > Technical > Email > Tracking events * Settings > Technical > Email > Tracking events
When the message generates and 'error' status, it will apear on discuss 'Failed'
channel. Any view that uses 'mail_thread' widget can show the failed messages
too.
* Discuss
.. image:: ../static/img/failed_message_discuss.png
* Chatter
.. image:: ../static/img/failed_message_widget.png

View File

@ -376,6 +376,7 @@ right to his name.</p>
<ul class="simple"> <ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li> <li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li> <li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<<<<<<< HEAD
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li> <li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul> <li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li> <li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
@ -385,6 +386,14 @@ right to his name.</p>
</ul> </ul>
</li> </li>
<li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li> <li><a class="reference internal" href="#maintainers" id="id9">Maintainers</a></li>
=======
<li><a class="reference internal" href="#known-issues-roadmap" id="id3">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
</ul> </ul>
</li> </li>
</ul> </ul>
@ -403,6 +412,7 @@ For example, <tt class="docutils literal"><span class="pre">--load=web,mail_trac
form, then an email tracking is created for each email notification. Then a form, then an email tracking is created for each email notification. Then a
status icon will appear just right to name of notified partner.</p> status icon will appear just right to name of notified partner.</p>
<p>These are all available status icons:</p> <p>These are all available status icons:</p>
<<<<<<< HEAD
<p><img alt="unknown" src="mail_tracking/static/src/img/unknown.png" style="width: 10px;" /> <strong>Unknown</strong>: No email tracking info available. Maybe this notified partner has Receive Inbox Notifications by Email == Never</p> <p><img alt="unknown" src="mail_tracking/static/src/img/unknown.png" style="width: 10px;" /> <strong>Unknown</strong>: No email tracking info available. Maybe this notified partner has Receive Inbox Notifications by Email == Never</p>
<p><img alt="waiting" src="mail_tracking/static/src/img/waiting.png" style="width: 10px;" /> <strong>Waiting</strong>: Waiting to be sent</p> <p><img alt="waiting" src="mail_tracking/static/src/img/waiting.png" style="width: 10px;" /> <strong>Waiting</strong>: Waiting to be sent</p>
<p><img alt="error" src="mail_tracking/static/src/img/error.png" style="width: 10px;" /> <strong>Error</strong>: Error while sending</p> <p><img alt="error" src="mail_tracking/static/src/img/error.png" style="width: 10px;" /> <strong>Error</strong>: Error while sending</p>
@ -414,10 +424,39 @@ status icon will appear just right to name of notified partner.</p>
<ul class="simple"> <ul class="simple">
<li>Settings &gt; Technical &gt; Email &gt; Tracking emails</li> <li>Settings &gt; Technical &gt; Email &gt; Tracking emails</li>
<li>Settings &gt; Technical &gt; Email &gt; Tracking events</li> <li>Settings &gt; Technical &gt; Email &gt; Tracking events</li>
=======
<p><img alt="unknown" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/unknown.png" style="width: 10px;" /> <strong>Unknown</strong>: No email tracking info available. Maybe this notified partner has Receive Inbox Notifications by Email == Never</p>
<p><img alt="waiting" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/waiting.png" style="width: 10px;" /> <strong>Waiting</strong>: Waiting to be sent</p>
<p><img alt="error" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/error.png" style="width: 10px;" /> <strong>Error</strong>: Error while sending</p>
<p><img alt="sent" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/sent.png" style="width: 10px;" /> <strong>Sent</strong>: Sent to SMTP server configured</p>
<p><img alt="delivered" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/delivered.png" style="width: 15px;" /> <strong>Delivered</strong>: Delivered to final MX server</p>
<p><img alt="opened" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/opened.png" style="width: 15px;" /> <strong>Opened</strong>: Opened by partner</p>
<p><img alt="cc" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/cc.png" style="width: 10px;" /> <strong>Cc</strong>: Its a Carbon-Copy recipient. Cant know the status so is Unknown</p>
<p>When the message generates and error status, it will apear on discuss Failed
channel. Any view that uses mail_thread widget can show the failed messages
too.</p>
<ul>
<li><p class="first">Discuss</p>
<img alt="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png" />
</li>
<li><p class="first">Chatter</p>
<img alt="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_widget.png" src="https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_widget.png" />
</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id3">Known issues / Roadmap</a></h1>
<ul class="simple">
<li>Handle message updates on discuss channel_failed instead of showing the
outdated message.</li>
<li>Adapt chat_manager changes in v12</li>
<li>Adapt discuss changes in v12</li>
<li>Add pivot for tracking events and mail trackings</li>
>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
</ul> </ul>
</div> </div>
<div class="section" id="bug-tracker"> <div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1> <h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>. <p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/social/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported. In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed If you spotted it first, help us smashing it by providing a detailed and welcomed
@ -425,15 +464,15 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>Do not contact contributors directly about support or help with technical issues.</p> <p>Do not contact contributors directly about support or help with technical issues.</p>
</div> </div>
<div class="section" id="credits"> <div class="section" id="credits">
<h1><a class="toc-backref" href="#id4">Credits</a></h1> <h1><a class="toc-backref" href="#id5">Credits</a></h1>
<div class="section" id="authors"> <div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2> <h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple"> <ul class="simple">
<li>Tecnativa</li> <li>Tecnativa</li>
</ul> </ul>
</div> </div>
<div class="section" id="contributors"> <div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2> <h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple"> <ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul> <li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Pedro M. Baeza</li> <li>Pedro M. Baeza</li>
@ -457,7 +496,11 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
</div> </div>
</div> </div>
<div class="section" id="maintainers"> <div class="section" id="maintainers">
<<<<<<< HEAD
<h2><a class="toc-backref" href="#id9">Maintainers</a></h2> <h2><a class="toc-backref" href="#id9">Maintainers</a></h2>
=======
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
>>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
<p>This module is maintained by the OCA.</p> <p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a> <a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose <p>OCA, or the Odoo Community Association, is a nonprofit organization whose

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@ -0,0 +1,108 @@
/* Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.o_mail_failed_message {
&.o_field_widget {
display: block;
}
.o_thread_date_separator
{
margin-top: 1.5rem;
margin-bottom: 3rem;
@media (max-width: @screen-xs-max) {
margin-top: 0;
margin-bottom: 1.5rem;
}
border-bottom: 1px solid @gray-lighter-darker;
border-bottom-style: solid;
text-align: center;
&.o_border_dashed {
border-bottom-style: dashed;
&[data-toggle="collapse"] {
cursor: pointer;
.o_chatter_failed_message_summary {
display: none;
}
&.collapsed {
margin-bottom: 0;
.o-transition(margin, 0.8s);
.o_chatter_failed_message_summary {
display: inline-block;
span {
padding: 0 0.5rem;
border-radius: 100%;
font-size: 1.1rem;
}
}
i.fa-caret-down:before {
content: '\f0da';
}
}
}
}
.o_thread_date {
position: relative;
top: 1rem;
margin: 0 auto;
padding: 0 1rem;
font-weight: bold;
background: white;
}
}
.o_thread_message {
display: -ms-flexbox;
display: -moz-box;
display: -webkit-box;
display: -webkit-flex;
display: flex;
padding: 0.4rem @odoo-horizontal-padding;
margin-bottom: 0px;
.o_thread_message_sidebar {
.o-flex(0, 0, @mail-thread-avatar-size);
margin-right: 1rem;
margin-top: 0.2rem;
text-align: center;
font-size: smaller;
.o_avatar_stack {
position: relative;
text-align: left;
margin-bottom: 0.8rem;
img {
.square(31px);
}
.o_avatar_icon {
.o-position-absolute(@right: -5px, @bottom: -5px);
.square(25px);
padding: 0.6rem 0.5rem;
text-align: center;
line-height: 1.2;
color: white;
border-radius: 100%;
border: 2px solid white;
}
}
}
.o_thread_message_core .o_mail_info {
.text-muted();
}
}
}
.o_mail_chat .o_mail_chat_sidebar .o_mail_failed_message_refresh {
margin-right: 0.5em;
margin-top: 0.2em;
}

View File

@ -1,21 +0,0 @@
/* Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.mail_tracking span {
color: #909090;
}
.mail_tracking_pointer {
cursor: pointer;
}
.mail_tracking span.mail_tracking_opened {
color: #a34a8b;
}
.o_mail_thread .o_thread_message .o_thread_message_core .o_mail_info {
margin: 0;
}
.o_mail_thread .o_thread_message .o_thread_message_core .o_mail_tracking {
margin: 0 0 2px 0;
}

View File

@ -0,0 +1,25 @@
/* Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.mail_tracking {
span {
color: @odoo-color-0;
&.mail_tracking_opened {
color: @odoo-color-5;
}
}
}
.mail_tracking_pointer {
cursor: pointer;
}
.o_mail_thread .o_thread_message .o_thread_message_core {
.o_mail_info {
margin: 0;
}
.o_mail_tracking {
margin: 0 0 0.2rem 0;
}
}

View File

@ -0,0 +1,365 @@
/* Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
odoo.define('mail_tracking.FailedMessage', function (require) {
"use strict";
var ChatAction = require('mail.chat_client_action');
var AbstractField = require('web.AbstractField');
var BasicModel = require('web.BasicModel');
var BasicView = require('web.BasicView');
var Chatter = require('mail.Chatter');
var utils = require('mail.utils');
var chat_manager = require('mail.chat_manager');
var core = require('web.core');
var field_registry = require('web.field_registry');
var time = require('web.time');
var session = require('web.session');
var config = require('web.config');
var QWeb = core.qweb;
var _t = core._t;
/* DISCUSS */
var failed_counter = 0;
var is_channel_failed_outdated = false;
ChatAction.include({
init: function () {
this._super.apply(this, arguments);
// HACK: Custom event to update messsages
core.bus.on('force_update_message', this, function (data) {
is_channel_failed_outdated = true;
this._onMessageUpdated(data);
this.throttledUpdateChannels();
});
},
_renderSidebar: function (options) {
options.failed_counter = chat_manager.get_failed_counter();
return this._super.apply(this, arguments);
},
_onMessageUpdated: function (message, type) {
var self = this;
var current_channel_id = this.channel.id;
// HACK: break inheritance because can't override properly
if (current_channel_id === "channel_failed" &&
!message.is_failed) {
chat_manager.get_messages({
channel_id: this.channel.id,
domain: this.domain,
}).then(function (messages) {
var options = self._getThreadRenderingOptions(messages);
self.thread.remove_message_and_render(
message.id, messages, options).then(function () {
self._updateButtonStatus(messages.length === 0, type);
});
});
} else {
this._super.apply(this, arguments);
}
},
_updateChannels: function () {
var self = this;
// HACK: break inheritance because can't override properly
if (this.channel.id === "channel_failed") {
var $sidebar = this._renderSidebar({
active_channel_id:
this.channel ? this.channel.id: undefined,
channels: chat_manager.get_channels(),
needaction_counter: chat_manager.get_needaction_counter(),
starred_counter: chat_manager.get_starred_counter(),
failed_counter: chat_manager.get_failed_counter(),
});
this.$(".o_mail_chat_sidebar").html($sidebar.contents());
_.each(['dm', 'public', 'private'], function (type) {
var $input = self.$(
'.o_mail_add_channel[data-type=' + type + '] input');
self._prepareAddChannelInput($input, type);
});
} else {
this._super.apply(this, arguments);
}
// FIXME: Because can't refresh "channel_failed" we add a flag
// to indicate that the data is outdated
var refresh_elm = this.$(
".o_mail_chat_sidebar .o_mail_failed_message_refresh");
refresh_elm.click(function (event) {
event.preventDefault();
event.stopPropagation();
location.reload();
});
if (is_channel_failed_outdated) {
refresh_elm.removeClass('hidden');
}
},
});
chat_manager.get_failed_counter = function () {
return failed_counter;
};
chat_manager._onMailClientAction_failed_message_super =
chat_manager._onMailClientAction;
chat_manager._onMailClientAction = function (result) {
failed_counter = result.failed_counter;
return this._onMailClientAction_failed_message_super(result);
};
function add_channel_to_message (message, channel_id) {
message.channel_ids.push(channel_id);
message.channel_ids = _.uniq(message.channel_ids);
}
chat_manager._make_message_failed_message_super = chat_manager.make_message;
chat_manager.make_message = function (data) {
var msg = this._make_message_failed_message_super(data);
function property_descr (channel) {
return {
enumerable: true,
get: function () {
return _.contains(msg.channel_ids, channel);
},
set: function (bool) {
if (bool) {
add_channel_to_message(msg, channel);
} else {
msg.channel_ids = _.without(msg.channel_ids, channel);
}
},
};
}
Object.defineProperties(msg, {
is_failed: property_descr("channel_failed"),
});
msg.is_failed = data.failed_message;
return msg;
};
chat_manager._fetchFromChannel_failed_message_super =
chat_manager._fetchFromChannel;
chat_manager._fetchFromChannel = function (channel, options) {
if (channel.id !== "channel_failed") {
return this._fetchFromChannel_failed_message_super(
channel, options);
}
// HACK: Can't override '_fetchFromChannel' properly to modify the
// domain, uses context instead and does it in python.
session.user_context.filter_failed_message = true;
var res = this._fetchFromChannel_failed_message_super(
channel, options);
res.then(function () {
delete session.user_context.filter_failed_message;
});
return res;
};
// HACK: Get failed_counter. Because 'chat_manager' call 'start' need call
// to '/mail/client_action' again with overrided '_onMailClientAction'
session.is_bound.then(function () {
var context = _.extend({isMobile: config.device.isMobile},
session.user_context);
return session.rpc('/mail/client_action', {context: context});
}).then(chat_manager._onMailClientAction.bind(chat_manager));
/* FAILED MESSAGES CHATTER WIDGET */
// TODO: Use timeFromNow() in v12
function time_from_now (date) {
if (moment().diff(date, 'seconds') < 45) {
return _t("now");
}
return date.fromNow();
}
function _readMessages (self, ids) {
if (!ids.length) {
return $.when([]);
}
var context = self.record && self.record.getContext();
return self._rpc({
model: 'mail.message',
method: 'get_failed_messages',
args: [ids],
context: context || self.getSession().user_context,
}).then(function (messages) {
// Convert date to moment
_.each(messages, function (msg) {
msg.date = moment(time.auto_str_to_date(msg.date));
msg.hour = time_from_now(msg.date);
});
return _.sortBy(messages, 'date');
});
}
BasicModel.include({
_fetchSpecialFailedMessages: function (record, fieldName) {
var localID = record._changes && fieldName in record._changes
? record._changes[fieldName] : record.data[fieldName];
return _readMessages(this, this.localData[localID].res_ids);
},
});
var AbstractFailedMessagesField = AbstractField.extend({
_markFailedMessageReviewed: function (id) {
return this._rpc({
model: 'mail.message',
method: 'toggle_tracking_status',
args: [[id]],
context: this.record.getContext(),
}).then(function (status) {
var fake_message = {
'id': id,
'is_failed': status,
};
chat_manager.bus.trigger('update_message', fake_message);
core.bus.trigger('force_update_message', fake_message);
});
},
});
var FailedMessage = AbstractFailedMessagesField.extend({
className: 'o_mail_failed_message',
events: {
'click .o_failed_message_retry': '_onRetryFailedMessage',
'click .o_failed_message_reviewed': '_onMarkFailedMessageReviewed',
},
specialData: '_fetchSpecialFailedMessages',
init: function () {
this._super.apply(this, arguments);
this.failed_messages = this.record.specialData[this.name];
},
_render: function () {
if (this.failed_messages.length) {
this.$el.html(QWeb.render(
'mail_tracking.failed_message_items', {
failed_messages: this.failed_messages,
nbFailedMessages: this.failed_messages.length,
date_format: time.getLangDateFormat(),
datetime_format: time.getLangDatetimeFormat(),
}));
} else {
this.$el.empty();
}
},
_reset: function (record) {
this._super.apply(this, arguments);
this.failed_messages = this.record.specialData[this.name];
this.res_id = record.res_id;
},
_reload: function (fieldsToReload) {
this.trigger_up('reload_mail_fields', fieldsToReload);
},
_openComposer: function (context) {
var self = this;
this.do_action({
type: 'ir.actions.act_window',
res_model: 'mail.compose.message',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: context,
}, {
on_close: function () {
self._reload({failed_message: true});
self.trigger('need_refresh');
chat_manager.get_messages({
model: self.model,
res_id: self.res_id,
});
},
}).then(this.trigger.bind(this, 'close_composer'));
},
// Handlers
_onRetryFailedMessage: function (event) {
event.preventDefault();
var message_id = $(event.currentTarget).data('message-id');
var failed_msg = _.findWhere(this.failed_messages,
{'id': message_id});
var failed_partner_ids = _.map(failed_msg.failed_recipients,
function (item) {
return item[0];
});
this._openComposer({
default_body: utils.get_text2html(failed_msg.body),
default_partner_ids: failed_partner_ids,
default_is_log: false,
default_model: this.model,
default_res_id: this.res_id,
default_composition_mode: 'comment',
// Omit followers
default_hide_followers: true,
mail_post_autofollow: true,
message_id: message_id,
});
},
_onMarkFailedMessageReviewed: function (event) {
event.preventDefault();
var message_id = $(event.currentTarget).data('message-id');
this._markFailedMessageReviewed(message_id).then(
this._reload.bind(this, {failed_message: true}));
},
});
field_registry.add('mail_failed_message', FailedMessage);
var mailWidgets = ['mail_failed_message'];
BasicView.include({
init: function (viewInfo) {
this._super.apply(this, arguments);
// Adds mail_failed_message as valid mail widget
var fieldsInfo = viewInfo.fieldsInfo[this.viewType];
for (var fieldName in fieldsInfo) {
var fieldInfo = fieldsInfo[fieldName];
if (_.contains(mailWidgets, fieldInfo.widget)) {
this.mailFields[fieldInfo.widget] = fieldName;
fieldInfo.__no_fetch = true;
}
}
Object.assign(this.rendererParams.mailFields, this.mailFields);
},
});
Chatter.include({
init: function (parent, record, mailFields, options) {
this._super.apply(this, arguments);
// Initialize mail_failed_message widget
if (mailFields.mail_failed_message) {
this.fields.failed_message = new FailedMessage(
this, mailFields.mail_failed_message, record, options);
}
},
_render: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
if (self.fields.failed_message) {
self.fields.failed_message.$el.insertBefore(
self.$el.find('.o_mail_thread'));
}
});
},
_onReloadMailFields: function (event) {
this._super.apply(this, arguments);
var fieldNames = [];
if (this.fields.failed_message && event.data.failed_message) {
fieldNames.push(this.fields.failed_message.name);
}
this.trigger_up('reload', {
fieldNames: fieldNames,
keepChanges: true,
});
},
});
return FailedMessage;
});

View File

@ -38,6 +38,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){
this._super.apply(this, arguments); this._super.apply(this, arguments);
this._partnerTrackings = data.partner_trackings || []; this._partnerTrackings = data.partner_trackings || [];
this._emailCc = data.email_cc || []; this._emailCc = data.email_cc || [];
this._trackNeedsAction = data.track_needs_action || false;
}, },
/** /**
@ -100,6 +101,14 @@ odoo.define('mail_tracking.partner_tracking', function(require){
return item[0] === email; return item[0] === email;
}); });
}, },
toggleTrackingStatus: function () {
return this._rpc({
model: 'mail.message',
method: 'toggle_tracking_status',
args: [[this.id]],
});
},
}); });
ThreadWidget.include({ ThreadWidget.include({
@ -107,6 +116,20 @@ odoo.define('mail_tracking.partner_tracking', function(require){
'click .o_mail_action_tracking_partner': 'on_tracking_partner_click', 'click .o_mail_action_tracking_partner': 'on_tracking_partner_click',
'click .o_mail_action_tracking_status': 'on_tracking_status_click', 'click .o_mail_action_tracking_status': 'on_tracking_status_click',
}), }),
_preprocess_message: function () {
var msg = this._super.apply(this, arguments);
msg.partner_trackings = msg.partner_trackings || [];
msg.email_cc = msg.email_cc || [];
var needs_action = msg.track_needs_action;
var message_track = _.findWhere(messages_tracked_changes, {
id: msg.id,
});
if (message_track) {
needs_action = message_track.status;
}
msg.track_needs_action = needs_action;
return msg;
},
on_tracking_partner_click: function (event) { on_tracking_partner_click: function (event) {
var partner_id = this.$el.find(event.currentTarget).data('partner'); var partner_id = this.$el.find(event.currentTarget).data('partner');
var state = { var state = {
@ -147,7 +170,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){
}; };
this.do_action(action); this.do_action(action);
}, },
init: function (parent, options) { init: function () {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.action_manager = this.findAncestor(function(ancestor){ this.action_manager = this.findAncestor(function(ancestor){
return ancestor instanceof ActionManager; return ancestor instanceof ActionManager;

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>
<t t-name="mail.chat.SidebarFailed">
<span class="o_mail_failed_message_refresh hidden"><i class="fa fa-refresh"/> Outdated</span>
<span t-attf-class="o_mail_sidebar_failed badge #{(!counter ? 'hide' : '')}">
<t t-esc="counter"/>
</span>
</t>
<t t-extend="mail.chat.Sidebar">
<t t-jquery="div[class='o_mail_chat_sidebar']>hr[class='mb8']" t-operation="before">
<div t-attf-class="o_mail_chat_title_main o_mail_chat_channel_item #{(active_channel_id === 'channel_failed') ? 'o_active': ''}"
data-channel-id="channel_failed">
<span class="o_channel_name"><i class="fa fa-exclamation mr8"/>Failed</span>
<t t-set="counter" t-value="failed_counter"/>
<t t-call="mail.chat.SidebarFailed"/>
</div>
</t>
</t>
<t t-extend="mail.EmptyChannel">
<t t-jquery="t:last-child" t-operation="after">
<t t-if="options.channel_id==='channel_failed'">
<div class="o_thread_title">Congratulations, doesn't have failed messages</div>
<div>Failed messages appear here.</div>
</t>
</t>
</t>
</template>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail_tracking.failed_message">
<div class="o_thread_message" style="margin-bottom: 10px">
<div class="o_thread_message_sidebar">
<div class="o_avatar_stack">
<img t-attf-src="/web/image#{message.author_id[0] >= 0 ? ('/res.partner/' + message.author_id[0] + '/image_small') : ''}" class="o_thread_message_avatar img-circle mb8" t-att-title="message.author_id[1]"/>
<i class="o_avatar_icon fa fa-exclamation bg-danger-full" title="Failed"></i>
</div>
</div>
<div class="o_thread_message_core">
<div class="o_mail_info">
<strong class="o_thread_author">
<t t-esc="message.author_id[1]"/>
</strong>
- <small class="o_mail_timestamp" t-att-title="message.date.format(date_format)"><t t-esc="message.hour"/></small><br/>
<strong class="text-danger">Failed Recipients:</strong>
<t t-set="first_recipient" t-value="true"/>
<t t-foreach="message.failed_recipients" t-as="recipient">
<t t-if="!first_recipient">
-
</t>
<a class="o_mail_action_tracking_partner"
t-att-data-partner="recipient[0]"
t-attf-href="#model=res.partner&amp;id=#{recipient[0]}">
<t t-esc="recipient[1]"/>
</a>
<t t-set="first_recipient" t-value="false"/>
</t>
</div>
<div class="o_thread_message_note small">
<t t-raw="message.body"/>
</div>
<div class="o_thread_message_tools btn-group">
<a href="#" class="btn btn-link btn-success text-muted btn-sm o_failed_message_reviewed o_activity_link mr8" t-att-data-message-id="message.id">
<i class="fa fa-check"/> Mark Reviewed
</a>
<a href="#" class="btn btn-link btn-default text-muted btn-sm o_failed_message_retry" t-att-data-message-id="message.id">
<i class="fa fa-retweet"/> Retry
</a>
</div>
</div>
</div>
</t>
<t t-name="mail_tracking.failed_message_items">
<div class="o_thread_date_separator o_border_dashed collapsed" data-toggle="collapse" data-target="#o_chatter_failed_message">
<span class="o_thread_date btn">
<i class="fa fa-fw fa-caret-down"/>
Failed messages
<small class="o_chatter_failed_message_summary ml8">
<span class="label img-circle label-danger"><t t-esc="nbFailedMessages"/></span>
</small>
</span>
</div>
<div id="o_chatter_failed_message" class="collapse">
<t t-foreach="failed_messages" t-as="message">
<t t-call="mail_tracking.failed_message" />
</t>
</div>
</t>
</templates>

View File

@ -8,6 +8,7 @@ import base64
from odoo import http from odoo import http
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
from ..controllers.main import MailTrackingController, BLANK from ..controllers.main import MailTrackingController, BLANK
from lxml import etree
mock_send_email = ('odoo.addons.base.models.ir_mail_server.' mock_send_email = ('odoo.addons.base.models.ir_mail_server.'
'IrMailServer.send_email') 'IrMailServer.send_email')
@ -159,6 +160,28 @@ class TestMailTracking(TransactionCase):
recipients = self.recipient.message_get_suggested_recipients() recipients = self.recipient.message_get_suggested_recipients()
self.assertEqual(len(recipients[self.recipient.id][0]), 3) self.assertEqual(len(recipients[self.recipient.id][0]), 3)
def test_failed_message(self):
# Create message
mail, tracking = self.mail_send(self.recipient.email)
self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action)
# Force error state
tracking.state = 'error'
self.assertTrue(tracking.mail_message_id.mail_tracking_needs_action)
failed_count = self.env['mail.message'].get_failed_count()
self.assertTrue(failed_count > 0)
values = tracking.mail_message_id.get_failed_messages()
self.assertEqual(values[0]['id'], tracking.mail_message_id.id)
messages = self.env['mail.message'].message_fetch([])
messages_failed = self.env['mail.message'].with_context(
filter_failed_message=True).message_fetch([])
self.assertTrue(messages)
self.assertTrue(messages_failed)
self.assertTrue(len(messages) > len(messages_failed))
tracking.mail_message_id.toggle_tracking_status()
self.assertFalse(tracking.mail_message_id.mail_tracking_needs_action)
self.assertTrue(
self.env['mail.message'].get_failed_count() < failed_count)
def mail_send(self, recipient): def mail_send(self, recipient):
mail = self.env['mail.mail'].create({ mail = self.env['mail.mail'].create({
'subject': 'Test subject', 'subject': 'Test subject',
@ -371,3 +394,21 @@ class TestMailTracking(TransactionCase):
self.assertEqual(b'NONE', none.response[0]) self.assertEqual(b'NONE', none.response[0])
none = controller.mail_tracking_event(db, 'open') none = controller.mail_tracking_event(db, 'open')
self.assertEqual(b'NONE', none.response[0]) self.assertEqual(b'NONE', none.response[0])
class TestMailTrackingViews(TransactionCase):
def test_fields_view_get(self):
result = self.env['res.partner'].fields_view_get(
view_id=self.env.ref('base.view_partner_form').id,
view_type='form')
doc = etree.XML(result['arch'])
nodes = doc.xpath(
"//field[@name='failed_message_ids'"
" and @widget='mail_failed_message']")
self.assertTrue(nodes)
result = self.env['res.partner'].fields_view_get(
view_id=self.env.ref('base.view_res_partner_filter').id,
view_type='search')
doc = etree.XML(result['arch'])
nodes = doc.xpath("//filter[@name='failed_message_ids']")
self.assertTrue(nodes)

View File

@ -7,9 +7,13 @@
inherit_id="web.assets_backend"> inherit_id="web.assets_backend">
<xpath expr="." position="inside"> <xpath expr="." position="inside">
<link rel="stylesheet" <link rel="stylesheet"
href="/mail_tracking/static/src/css/mail_tracking.css"/> href="/mail_tracking/static/src/css/mail_tracking.less"/>
<link rel="stylesheet"
href="/mail_tracking/static/src/css/failed_message.less"/>
<script type="text/javascript" <script type="text/javascript"
src="/mail_tracking/static/src/js/mail_tracking.js"/> src="/mail_tracking/static/src/js/mail_tracking.js"/>
<script type="text/javascript"
src="/mail_tracking/static/src/js/failed_message.js"/>
</xpath> </xpath>
</template> </template>
</odoo> </odoo>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="view_message_form">
<field name="model">mail.message</field>
<field name="inherit_id" ref="mail.view_message_form"/>
<field name="arch" type="xml">
<field name="subtype_id" position="after">
<field name="mail_tracking_needs_action" />
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record model="ir.ui.view" id="email_compose_message_wizard_form">
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<field name="active_domain" position="after">
<field name="hide_followers" invisible="1" />
</field>
<xpath expr="//div[@groups='base.group_user']/span[2]" position="attributes">
<attribute name="attrs">{'invisible':['|', '|', ('model', '=', False), ('composition_mode', '=', 'mass_mail'), ('hide_followers', '=', True)]}</attribute>
</xpath>
</field>
</record>
</data>
</odoo>