mirror of https://github.com/OCA/social.git
[IMP] mail_tracking: Failed Messages (Discuss & View)
parent
bc759d49ea
commit
75b966269b
|
@ -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
|
||||
===========
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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>: It’s a Carbon-Copy recipient. Can’t 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>: It’s a Carbon-Copy recipient. Can’t 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 <<a class="reference external" href="mailto:pedro.baeza@tecnativa.com">pedro.baeza@tecnativa.com</a>></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 |
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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&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>
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue