[IMP] mail_tracking: Failed Messages (Discuss & View)

pull/411/head
Alexandre Díaz 2019-07-11 17:36:34 +02:00
parent bc759d49ea
commit 75b966269b
25 changed files with 998 additions and 184 deletions

View File

@ -58,25 +58,25 @@ status icon will appear just right to name of notified partner.
These are all available status icons:
.. |sent| image:: static/src/img/sent.png
.. |sent| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/sent.png
:width: 10px
.. |delivered| image:: static/src/img/delivered.png
.. |delivered| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/delivered.png
:width: 15px
.. |opened| image:: static/src/img/opened.png
.. |opened| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/opened.png
:width: 15px
.. |error| image:: static/src/img/error.png
.. |error| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/error.png
:width: 10px
.. |waiting| image:: static/src/img/waiting.png
.. |waiting| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/waiting.png
:width: 10px
.. |unknown| image:: static/src/img/unknown.png
.. |unknown| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/unknown.png
:width: 10px
.. |cc| image:: static/src/img/cc.png
.. |cc| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/cc.png
:width: 10px
|unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never'
@ -93,6 +93,28 @@ These are all available status icons:
|cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown'
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
Bug Tracker
===========

View File

@ -24,10 +24,17 @@
"views/assets.xml",
"views/mail_tracking_email_view.xml",
"views/mail_tracking_event_view.xml",
"views/mail_message_view.xml",
"views/res_partner_view.xml",
"wizard/mail_compose_message_view.xml",
],
"qweb": [
"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",
}

View File

@ -3,7 +3,9 @@
import werkzeug
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 base64
_logger = logging.getLogger(__name__)
@ -37,7 +39,7 @@ def _env_get(db, callback, tracking_id, event_type, **kw):
return res
class MailTrackingController(http.Controller):
class MailTrackingController(MailController):
def _request_metadata(self):
request = http.request.httprequest
@ -85,3 +87,22 @@ class MailTrackingController(http.Controller):
response.mimetype = 'image/gif'
response.data = base64.b64decode(BLANK)
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

@ -1,10 +1,10 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from . import ir_mail_server
from . import mail_thread
from . import mail_mail
from . import mail_message
from . import mail_tracking_email
from . import mail_tracking_event
from . import mail_composer
from . import res_partner
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

@ -15,14 +15,18 @@ class MailMessage(models.Model):
mail_tracking_ids = fields.One2many(
comodel_name='mail.tracking.email',
inverse_name='mail_message_id',
string="Mail Trackings Associated with this message",
string="Mail Trackings",
)
track_needs_action = fields.Boolean(
string="The message tracking will be considered"
"for filter tracking issues",
default=True,
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):
return {
'False': 'waiting',
@ -120,6 +124,17 @@ class MailMessage(models.Model):
res[message.id] = partner_trackings
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
def _message_read_dict_postprocess(self, messages, message_tree):
res = super(MailMessage, self)._message_read_dict_postprocess(
@ -127,23 +142,76 @@ class MailMessage(models.Model):
mail_message_ids = {m.get('id') for m in messages if m.get('id')}
mail_messages = self.browse(mail_message_ids)
partner_trackings = mail_messages.tracking_status()
failed_message = mail_messages._get_failed_message()
for message_dict in messages:
mail_message_id = message_dict.get('id', False)
if mail_message_id:
message_dict['partner_trackings'] = \
partner_trackings[mail_message_id]
message_dict.update({
'partner_trackings': partner_trackings[mail_message_id],
'failed_message': failed_message[mail_message_id],
})
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"""
# a user should always be able to star a message he can read
self.check_access_rule('read')
self.track_needs_action = not self.track_needs_action
notification = {'type': 'toggle_track', 'message_ids': [self.id],
'tracked': self.track_needs_action,
'res_id': self.res_id, 'model': self.model}
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner',
self.env.user.partner_id.id),
notification)
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,17 +1,22 @@
# Copyright 2019 Alexandre Díaz
# 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 odoo.tools import email_split_and_format
from lxml import etree
import logging
_logger = logging.getLogger(__name__)
class MailThread(models.AbstractModel):
_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.returns('self', lambda value: value.id)
def message_post(self, body='', subject=None, message_type='notification',
@ -30,6 +35,8 @@ class MailThread(models.AbstractModel):
@api.multi
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()
ResPartnerObj = self.env['res.partner']
email_cc_formated_list = []
@ -49,36 +56,53 @@ class MailThread(models.AbstractModel):
partner = ResPartnerObj.browse(partner_id, self._prefetch)
record._message_add_suggested_recipient(
res, partner=partner, reason=_('Cc'))
return res
@api.model
def fields_view_get(self, view_id=None, view_type='form', toolbar=False,
submenu=False):
"""Add a filter to any model with mail.thread that will show up records
with tracking errors.
"""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':
if view_type != 'search' and view_type != 'form':
return res
# Create filter element
filter_name = "message_ids_with_tracking_errors"
tracking_error_domain = """[
("message_ids.mail_tracking_ids.state", "in",
['error', 'rejected', 'spam', 'bounced', 'soft-bounced']),
("message_ids.track_needs_action", "=", True)]"""
new_filter = etree.Element(
'filter', {
'string': _('Messages with errors'),
'name': filter_name,
'domain': tracking_error_domain})
separator = etree.Element('separator', {})
new_filter.append(separator)
# Modify view to add new filter element
doc = etree.XML(res['arch'])
node = doc.xpath("//search")[0]
node.insert(0, new_filter)
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

@ -94,11 +94,10 @@ class MailTrackingEmail(models.Model):
@api.multi
def write(self, vals):
if 'state' in vals and vals['state'] in \
['error', 'rejected', 'spam', 'bounced', 'soft-bounced']:
for tracking_mail in self:
if tracking_mail.mail_message_id:
tracking_mail.mail_message_id.track_needs_action = True
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
@ -106,8 +105,8 @@ class MailTrackingEmail(models.Model):
if not email:
return False
res = self._email_last_tracking_state(email)
return res and res[0].get('state', '') in ['rejected', 'error',
'spam', 'bounced']
return res and res[0].get('state', '') in {'rejected', 'error',
'spam', 'bounced'}
@api.model
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

@ -10,25 +10,25 @@ status icon will appear just right to name of notified partner.
These are all available status icons:
.. |sent| image:: static/src/img/sent.png
.. |sent| image:: ../static/src/img/sent.png
:width: 10px
.. |delivered| image:: static/src/img/delivered.png
.. |delivered| image:: ../static/src/img/delivered.png
:width: 15px
.. |opened| image:: static/src/img/opened.png
.. |opened| image:: ../static/src/img/opened.png
:width: 15px
.. |error| image:: static/src/img/error.png
.. |error| image:: ../static/src/img/error.png
:width: 10px
.. |waiting| image:: static/src/img/waiting.png
.. |waiting| image:: ../static/src/img/waiting.png
:width: 10px
.. |unknown| image:: static/src/img/unknown.png
.. |unknown| image:: ../static/src/img/unknown.png
:width: 10px
.. |cc| image:: static/src/img/cc.png
.. |cc| image:: ../static/src/img/cc.png
:width: 10px
|unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never'
@ -44,3 +44,16 @@ These are all available status icons:
|opened| **Opened**: Opened by partner
|cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown'
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,11 +376,12 @@ right to his name.</p>
<ul class="simple">
<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="#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="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">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>
</ul>
</li>
</ul>
@ -404,16 +405,37 @@ 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
status icon will appear just right to name of notified partner.</p>
<p>These are all available status icons:</p>
<p><img alt="unknown" src="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="static/src/img/waiting.png" style="width: 10px;" /> <strong>Waiting</strong>: Waiting to be sent</p>
<p><img alt="error" src="static/src/img/error.png" style="width: 10px;" /> <strong>Error</strong>: Error while sending</p>
<p><img alt="sent" src="static/src/img/sent.png" style="width: 10px;" /> <strong>Sent</strong>: Sent to SMTP server configured</p>
<p><img alt="delivered" src="static/src/img/delivered.png" style="width: 15px;" /> <strong>Delivered</strong>: Delivered to final MX server</p>
<p><img alt="opened" src="static/src/img/opened.png" style="width: 15px;" /> <strong>Opened</strong>: Opened by partner</p>
<p><img alt="cc" src="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><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>
</ul>
</div>
<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>.
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
@ -421,15 +443,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>
</div>
<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">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<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">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Pedro M. Baeza &lt;<a class="reference external" href="mailto:pedro.baeza&#64;tecnativa.com">pedro.baeza&#64;tecnativa.com</a>&gt;</li>
@ -442,7 +464,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<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>
<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,12 +1,13 @@
/* Copyright 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
Copyright 2019 Alexandre Díaz
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */
.mail_tracking {
span {
color: #909090;
color: @odoo-color-0;
&.mail_tracking_opened {
color: #a34a8b;
color: @odoo-color-5;
}
}
}
@ -19,12 +20,6 @@
margin: 0;
}
.o_mail_tracking {
margin: 0 0 2px 0;
}
}
.o_thread_icon {
&.fa-check-square {
opacity: @mail-thread-icon-opacity !important;
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

@ -7,58 +7,28 @@ odoo.define('mail_tracking.partner_tracking', function (require) {
var core = require('web.core');
var ActionManager = require('web.ActionManager');
var web_client = require('web.web_client');
var chat_manager = require('mail.chat_manager');
var ChatThread = require('mail.ChatThread');
var Chatter = require('mail.Chatter');
var ThreadField = require('mail.ThreadField');
var Bus = require('bus.bus').bus;
var _t = core._t;
// chat_manager is a simple dictionary, not an OdooClass
// Chat_manager is a simple dictionary, not an OdooClass
chat_manager._make_message_super = chat_manager.make_message;
chat_manager.make_message = function (data) {
var msg = this._make_message_super(data);
msg.partner_trackings = data.partner_trackings || [];
<<<<<<< HEAD
=======
msg.email_cc = data.email_cc || [];
msg.track_needs_action = data.track_needs_action || false;
>>>>>>> [IMP] mail_tracking: Filter messages with errors
return msg;
};
chat_manager.toggle_tracking_status = function (message_id) {
return this._rpc({
model: 'mail.message',
method: 'toggle_tracking_status',
args: [[message_id]],
});
};
ChatThread.include({
events: _.extend(ChatThread.prototype.events, {
'click .o_mail_action_tracking_partner':
'on_tracking_partner_click',
'click .o_mail_action_tracking_status': 'on_tracking_status_click',
'click .o_thread_message_tracking': 'on_thread_message_tracking_click'
}),
_preprocess_message: function () {
var msg = this._super.apply(this, arguments);
msg.partner_trackings = msg.partner_trackings || [];
<<<<<<< HEAD
=======
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;
>>>>>>> [IMP] mail_tracking: Filter messages with errors
return msg;
},
on_tracking_partner_click: function (event) {
@ -101,68 +71,11 @@ odoo.define('mail_tracking.partner_tracking', function (require) {
};
this.do_action(action);
},
on_thread_message_tracking_click: function (event) {
var message_id = $(event.currentTarget).data('message-id');
this.trigger("toggle_tracking_status", message_id);
},
init: function (parent, options) {
init: function () {
this._super.apply(this, arguments);
this.action_manager = this.findAncestor(function (ancestor) {
return ancestor instanceof ActionManager;
});
},
});
/* Propagate toggle tracking event */
ThreadField.include({
_toggleTrackingStatus: function (message_id) {
this.trigger_up('toggle_tracking_status', {
message_id: message_id,
});
},
start: function () {
var res = this._super.apply(this, arguments);
this.thread.on('toggle_tracking_status', this,
this._toggleTrackingStatus);
return res;
},
});
web_client.on('toggle_tracking_status', web_client, function (event) {
chat_manager.toggle_tracking_status(event.data.message_id);
});
/* Because "messages" are an isolated object need store new track states
to apply it when process messages */
var messages_tracked_changes = [];
function on_toggle_tracked_notification (notif_data) {
_.each(notif_data.message_ids, function (msg_id) {
var message_track = _.findWhere(messages_tracked_changes, {
id: msg_id,
});
if (message_track) {
message_track.status = notif_data.tracked;
} else {
messages_tracked_changes.push({
'id': msg_id,
'status': notif_data.tracked,
});
}
});
// Update thread messages
chat_manager.bus.trigger('update_message', {
'model': notif_data.model,
'res_id': notif_data.res_id,
});
}
function on_notification (notifications) {
_.each(notifications, function (notification) {
var model = notification[0][1];
var notif_vals = notification[1];
if (model === 'res.partner' && notif_vals.type === 'toggle_track') {
on_toggle_tracked_notification(notif_vals);
}
});
}
Bus.on('notification', null, on_notification);
});

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

@ -47,10 +47,6 @@
</t>
<t t-extend="mail.ChatThread.Message">
<t t-jquery="span[t-attf-class='o_thread_icons']>i:last" t-operation="after">
<i t-attf-class="fa fa-lg o_thread_icon o_thread_message_tracking #{message.track_needs_action ? 'fa-check-square' : 'fa-square-o'}"
t-att-data-message-id="message.id" title="Trackin reviewed"/>
</t>
<t t-jquery="p[class='o_mail_info']" t-operation="after">
<p class="o_mail_tracking">
<strong>To:</strong>

View File

@ -8,6 +8,7 @@ import base64
from odoo import http
from odoo.tests.common import TransactionCase
from ..controllers.main import MailTrackingController, BLANK
from lxml import etree
mock_send_email = ('odoo.addons.base.ir.ir_mail_server.'
'IrMailServer.send_email')
@ -170,6 +171,28 @@ class TestMailTracking(TransactionCase):
self.assertEqual(len(recipients[self.recipient.id][0]), 3)
self._check_partner_trackings(message)
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):
mail = self.env['mail.mail'].create({
'subject': 'Test subject',
@ -382,3 +405,21 @@ class TestMailTracking(TransactionCase):
self.assertEqual(b'NONE', none.response[0])
none = controller.mail_tracking_event(db, 'open')
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

@ -8,8 +8,12 @@
<xpath expr="." position="inside">
<link rel="stylesheet"
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"
src="/mail_tracking/static/src/js/mail_tracking.js"/>
<script type="text/javascript"
src="/mail_tracking/static/src/js/failed_message.js"/>
</xpath>
</template>
</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>