From eba1e0de8afc49c2761f8f4d9fbd5cc71b51e044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexandre=20D=C3=ADaz?= Date: Thu, 17 Oct 2019 03:28:42 +0200 Subject: [PATCH] [IMP] mail_tracking: Failed Messages to 12.0 --- mail_tracking/README.rst | 57 +-- mail_tracking/__manifest__.py | 6 +- mail_tracking/controllers/main.py | 128 +++--- mail_tracking/demo/demo.xml | 61 ++- mail_tracking/i18n/es.po | 308 ++++++++++---- mail_tracking/i18n/mail_tracking.pot | 172 +++++++- .../migrations/12.0.2.0.0/post-migrate.py | 11 + mail_tracking/models/__init__.py | 2 +- mail_tracking/models/mail_bounced_mixin.py | 4 - mail_tracking/models/mail_composer.py | 30 -- mail_tracking/models/mail_mail.py | 5 +- mail_tracking/models/mail_message.py | 170 +++++--- mail_tracking/models/mail_resend_message.py | 64 +++ mail_tracking/models/mail_thread.py | 23 +- mail_tracking/models/mail_tracking_email.py | 74 +++- mail_tracking/readme/ROADMAP.rst | 4 - mail_tracking/readme/USAGE.rst | 16 +- mail_tracking/static/description/index.html | 73 ++-- .../static/img/failed_message_filter.png | Bin 0 -> 97662 bytes .../static/src/css/failed_message.less | 108 ----- .../static/src/css/failed_message.scss | 343 +++++++++++++++ ...{mail_tracking.less => mail_tracking.scss} | 4 +- mail_tracking/static/src/js/failed_message.js | 365 ---------------- .../static/src/js/failed_message/discuss.js | 397 ++++++++++++++++++ .../static/src/js/failed_message/thread.js | 316 ++++++++++++++ mail_tracking/static/src/js/mail_tracking.js | 46 +- .../static/src/xml/client_action.xml | 31 -- .../static/src/xml/failed_message.xml | 63 --- .../static/src/xml/failed_message/common.xml | 15 + .../static/src/xml/failed_message/discuss.xml | 34 ++ .../static/src/xml/failed_message/thread.xml | 60 +++ .../static/src/xml/mail_tracking.xml | 11 +- mail_tracking/tests/test_mail_tracking.py | 133 ++++-- mail_tracking/views/assets.xml | 8 +- .../wizard/mail_compose_message_view.xml | 19 - 35 files changed, 2139 insertions(+), 1022 deletions(-) create mode 100644 mail_tracking/migrations/12.0.2.0.0/post-migrate.py delete mode 100644 mail_tracking/models/mail_composer.py create mode 100644 mail_tracking/models/mail_resend_message.py create mode 100644 mail_tracking/static/img/failed_message_filter.png delete mode 100644 mail_tracking/static/src/css/failed_message.less create mode 100644 mail_tracking/static/src/css/failed_message.scss rename mail_tracking/static/src/css/{mail_tracking.less => mail_tracking.scss} (88%) delete mode 100644 mail_tracking/static/src/js/failed_message.js create mode 100644 mail_tracking/static/src/js/failed_message/discuss.js create mode 100644 mail_tracking/static/src/js/failed_message/thread.js delete mode 100644 mail_tracking/static/src/xml/client_action.xml delete mode 100644 mail_tracking/static/src/xml/failed_message.xml create mode 100644 mail_tracking/static/src/xml/failed_message/common.xml create mode 100644 mail_tracking/static/src/xml/failed_message/discuss.xml create mode 100644 mail_tracking/static/src/xml/failed_message/thread.xml delete mode 100644 mail_tracking/wizard/mail_compose_message_view.xml diff --git a/mail_tracking/README.rst b/mail_tracking/README.rst index b8ad2e9..4c3f345 100644 --- a/mail_tracking/README.rst +++ b/mail_tracking/README.rst @@ -52,44 +52,28 @@ status icon will appear just right to name of notified partner. These are all available status icons: -<<<<<<< HEAD -.. |sent| image:: mail_tracking/static/src/img/sent.png +.. |sent| image:: ../static/src/img/sent.png :width: 10px -.. |delivered| image:: mail_tracking/static/src/img/delivered.png +.. |delivered| image:: ../static/src/img/delivered.png :width: 15px -.. |opened| image:: mail_tracking/static/src/img/opened.png +.. |opened| image:: ../static/src/img/opened.png :width: 15px -.. |error| image:: mail_tracking/static/src/img/error.png +.. |error| image:: ../static/src/img/error.png :width: 10px -.. |waiting| image:: mail_tracking/static/src/img/waiting.png +.. |waiting| image:: ../static/src/img/waiting.png :width: 10px -.. |unknown| image:: mail_tracking/static/src/img/unknown.png -======= -.. |sent| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/sent.png +.. |unknown| image:: ../static/src/img/unknown.png :width: 10px -.. |delivered| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/delivered.png - :width: 15px - -.. |opened| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/opened.png - :width: 15px - -.. |error| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/error.png +.. |cc| image:: ../static/src/img/cc.png :width: 10px -.. |waiting| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/waiting.png - :width: 10px - -.. |unknown| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/unknown.png ->>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View) - :width: 10px - -.. |cc| image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/src/img/cc.png +.. |noemail| image:: ../static/src/img/no_email.png :width: 10px |unknown| **Unknown**: No email tracking info available. Maybe this notified partner has 'Receive Inbox Notifications by Email' == 'Never' @@ -106,34 +90,37 @@ These are all available status icons: |cc| **Cc**: It's a Carbon-Copy recipient. Can't know the status so is 'Unknown' +|noemail| **No Email**: The partner doesn't have a defined email + -<<<<<<< HEAD If you want to see all tracking emails and events you can go to * Settings > Technical > Email > Tracking emails * Settings > Technical > Email > Tracking events -======= -When the message generates and 'error' status, it will apear on discuss 'Failed' -channel. Any view that uses 'mail_thread' widget can show the failed messages + +When the message generates an 'error' status, it will apear on discuss 'Failed' +channel. Any view with chatter can show the failed messages too. * Discuss - .. image:: https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png + .. image:: https://raw.githubusercontent.com/OCA/social/12.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 + .. image:: https://raw.githubusercontent.com/OCA/social/12.0/mail_tracking/static/img/failed_message_widget.png + +You can use "Failed sent messages" filter present in all views to show records +with messages in failed status and that needs an user action. + +* Filter + + .. image:: https://raw.githubusercontent.com/OCA/social/12.0/mail_tracking/static/img/failed_message_filter.png Known issues / Roadmap ====================== -* Handle message updates on discuss 'channel_failed' instead of showing the - 'outdated' message. -* Adapt chat_manager changes in v12 -* Adapt discuss changes in v12 * Add pivot for tracking events and mail trackings ->>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View) Bug Tracker =========== diff --git a/mail_tracking/__manifest__.py b/mail_tracking/__manifest__.py index 3a5b3e4..9770529 100644 --- a/mail_tracking/__manifest__.py +++ b/mail_tracking/__manifest__.py @@ -28,12 +28,12 @@ "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", + "static/src/xml/failed_message/common.xml", + "static/src/xml/failed_message/thread.xml", + "static/src/xml/failed_message/discuss.xml", ], 'demo': [ 'demo/demo.xml', diff --git a/mail_tracking/controllers/main.py b/mail_tracking/controllers/main.py index 2f53ecb..c4a5e0e 100644 --- a/mail_tracking/controllers/main.py +++ b/mail_tracking/controllers/main.py @@ -2,10 +2,11 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import werkzeug -from psycopg2 import OperationalError -from odoo import api, http, registry, SUPERUSER_ID, _ +import odoo +from contextlib import contextmanager +from odoo import api, http, SUPERUSER_ID + from odoo.addons.mail.controllers.main import MailController -from odoo.http import request import logging import base64 _logger = logging.getLogger(__name__) @@ -13,35 +14,23 @@ _logger = logging.getLogger(__name__) BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' -def _env_get(db, callback, tracking_id, event_type, **kw): - res = 'NOT FOUND' - reg = False - current = http.request.db and db == http.request.db - env = current and http.request.env - if not env: - with api.Environment.manage(): - try: - reg = registry(db) - except OperationalError: - _logger.warning("Selected BD '%s' not found", db) - except Exception: # pragma: no cover - _logger.warning("Selected BD '%s' connection error", db) - if reg: - _logger.info("New environment for database '%s'", db) - with reg.cursor() as new_cr: - new_env = api.Environment(new_cr, SUPERUSER_ID, {}) - res = callback(new_env, tracking_id, event_type, **kw) - new_env.cr.commit() - else: - # make sudo when reusing environment - env = env(user=SUPERUSER_ID) - res = callback(env, tracking_id, event_type, **kw) - return res +@contextmanager +def db_env(dbname): + if not http.db_filter([dbname]): + raise werkzeug.exceptions.BadRequest() + cr = None + if dbname == http.request.db: + cr = http.request.cr + if not cr: + cr = odoo.sql_db.db_connect(dbname).cursor() + with api.Environment.manage(): + yield api.Environment(cr, SUPERUSER_ID, {}) class MailTrackingController(MailController): def _request_metadata(self): + """Prepare remote info metadata""" request = http.request.httprequest return { 'ip': request.remote_addr or False, @@ -50,37 +39,45 @@ class MailTrackingController(MailController): 'ua_family': request.user_agent.browser or False, } - def _tracking_open(self, env, tracking_id, event_type, **kw): - tracking_email = env['mail.tracking.email'].search([ - ('id', '=', tracking_id), - ]) - if tracking_email: - metadata = self._request_metadata() - tracking_email.event_create('open', metadata) - else: - _logger.warning( - "MailTracking email '%s' not found", tracking_id) - - def _tracking_event(self, env, tracking_id, event_type, **kw): + @http.route(['/mail/tracking/all/', + '/mail/tracking/event//'], + type='http', auth='none', csrf=False) + def mail_tracking_event(self, db, event_type=None, **kw): + """Route used by external mail service""" metadata = self._request_metadata() - return env['mail.tracking.email'].event_process( - http.request, kw, metadata, event_type=event_type) + res = None + with db_env(db) as env: + try: + res = env['mail.tracking.email'].event_process( + http.request, kw, metadata, event_type=event_type) + except Exception: + pass + if not res or res == 'NOT FOUND': + return werkzeug.exceptions.NotAcceptable() + return res - @http.route('/mail/tracking/all/', - type='http', auth='none', csrf=False) - def mail_tracking_all(self, db, **kw): - return _env_get(db, self._tracking_event, None, None, **kw) - - @http.route('/mail/tracking/event//', - type='http', auth='none', csrf=False) - def mail_tracking_event(self, db, event_type, **kw): - return _env_get(db, self._tracking_event, None, event_type, **kw) - - @http.route('/mail/tracking/open/' - '//blank.gif', - type='http', auth='none') - def mail_tracking_open(self, db, tracking_email_id, **kw): - _env_get(db, self._tracking_open, tracking_email_id, None, **kw) + @http.route(['/mail/tracking/open/' + '//blank.gif', + '/mail/tracking/open/' + '///blank.gif'], + type='http', auth='none', methods=['GET']) + def mail_tracking_open(self, db, tracking_email_id, token=False, **kw): + """Route used to track mail openned (With & Without Token)""" + metadata = self._request_metadata() + with db_env(db) as env: + try: + tracking_email = env['mail.tracking.email'].search([ + ('id', '=', tracking_email_id), + ('state', 'in', ['sent', 'delivered']), + ('token', '=', token), + ]) + if tracking_email: + tracking_email.event_create('open', metadata) + else: + _logger.warning( + "MailTracking email '%s' not found", tracking_email_id) + except Exception: + pass # Always return GIF blank image response = werkzeug.wrappers.Response() @@ -89,20 +86,11 @@ class MailTrackingController(MailController): 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, - }) + def mail_init_messaging(self): + """Route used to initial values of Discuss app""" + values = super().mail_init_messaging() values.update({ - 'failed_counter': request.env['mail.message'].get_failed_count(), + 'failed_counter': + http.request.env['mail.message'].get_failed_count(), }) return values diff --git a/mail_tracking/demo/demo.xml b/mail_tracking/demo/demo.xml index e3bfd31..e90d45d 100644 --- a/mail_tracking/demo/demo.xml +++ b/mail_tracking/demo/demo.xml @@ -2,6 +2,31 @@ + + + res.partner + + comment + + acc@testmail.com,wood.corner26@example.com,toni.rhodes11@example.com + 1 + This is a message with CC

]]>
+ wood.corner26@example.com + + + Message with CC +
+ + + Message with CC + + + demo@yourcompany.example.com + wood.corner26@example.com + sent + + + res.partner @@ -10,7 +35,7 @@ 1 This is a failed message

]]>
- res1@yourcompany.example.com + wood.corner26@example.com Failed Message @@ -20,8 +45,8 @@ Failed Message - res1@yourcompany.example.com - demo@yourcompany.example.com + demo@yourcompany.example.com + wood.corner26@example.com error
@@ -34,7 +59,7 @@ 1 This is another failed message

]]>
- res10@yourcompany.example.com + jackson.group82@example.com Failed Message @@ -44,8 +69,32 @@ Failed Message - res10@yourcompany.example.com - demo@yourcompany.example.com + demo@yourcompany.example.com + jackson.group82@example.com + error + + + + + + res.partner + + comment + + 1 + This is another failed message

]]>
+ admin@example.com + + + Failed Message +
+ + + Failed Message + + + demo@yourcompany.example.com + admin@example.com error diff --git a/mail_tracking/i18n/es.po b/mail_tracking/i18n/es.po index 4ef7df8..9685329 100644 --- a/mail_tracking/i18n/es.po +++ b/mail_tracking/i18n/es.po @@ -1,23 +1,21 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * mail_tracking +# * mail_tracking # -# Translators: -# OCA Transbot , 2017 msgid "" msgstr "" -"Project-Id-Version: Odoo Server 10.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-12-01 02:19+0000\n" -"PO-Revision-Date: 2019-08-04 17:44+0000\n" -"Last-Translator: eduardgm \n" -"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"POT-Creation-Date: 2019-11-12 17:22+0000\n" +"PO-Revision-Date: 2019-11-12 18:26+0100\n" +"Last-Translator: <>\n" +"Language-Team: \n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 3.7.1\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 2.2.4\n" #. module: mail_tracking #: model:ir.model.fields,help:mail_tracking.field_mail_tracking_email__state @@ -43,11 +41,18 @@ msgid "" "recipient Mail Exchange (MX) server.\n" msgstr "" +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:189 +#, python-format +msgid "-Unknown Author-" +msgstr "-Autor Desconocido-" + #. module: mail_tracking #: model:ir.model.fields,help:mail_tracking.field_mail_compose_message__email_cc #: model:ir.model.fields,help:mail_tracking.field_mail_message__email_cc msgid "Additional recipients that receive a \"Carbon Copy\" of the e-mail" msgstr "" +"Destinatarios adicionales que reciben una \"Copia de Carbón\" del correo" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search @@ -70,8 +75,8 @@ msgid "Bounced" msgstr "Rebotado" #. module: mail_tracking -#: code:addons/mail_tracking/models/mail_thread.py:39 -#: code:addons/mail_tracking/models/mail_thread.py:43 +#: code:addons/mail_tracking/models/mail_thread.py:64 +#: code:addons/mail_tracking/models/mail_thread.py:68 #: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__email_cc #: model:ir.model.fields,field_description:mail_tracking.field_mail_message__email_cc #, python-format @@ -93,6 +98,20 @@ msgstr "Clicado" msgid "Clicked URL" msgstr "URL Clicada" +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:22 +#, python-format +msgid "Congratulations, you don't have any failed messages" +msgstr "¡Enhorabuena! No tienes mensajes fallidos" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/js/failed_message/discuss.js:231 +#, python-format +msgid "Congratulations, your failed mailbox is empty" +msgstr "¡Enhorabuena! tu buzón de fallidos está vacio" + #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_res_partner msgid "Contact" @@ -128,21 +147,23 @@ msgstr "Fecha" #. module: mail_tracking #: selection:mail.tracking.event,event_type:0 msgid "Deferral" -msgstr "" +msgstr "Aplazamiento" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: selection:mail.tracking.email,state:0 msgid "Deferred" -msgstr "" +msgstr "Diferido" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:74 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 +#, python-format msgid "Delivered" -msgstr "" +msgstr "Entregado" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_bounced_mixin__display_name @@ -165,94 +186,151 @@ msgstr "Correo electrónico" #: model:ir.model.fields,field_description:mail_tracking.field_mail_bounced_mixin__email_bounced #: model:ir.model.fields,field_description:mail_tracking.field_res_partner__email_bounced #: model:ir.model.fields,field_description:mail_tracking.field_res_users__email_bounced -#, fuzzy msgid "Email Bounced" -msgstr "E mail rebotado" +msgstr "Correo rebotado" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_res_partner__email_score #: model:ir.model.fields,field_description:mail_tracking.field_res_users__email_score -#, fuzzy msgid "Email Score" -msgstr "Reputación del email" +msgstr "Reputación del correo" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_thread -#, fuzzy msgid "Email Thread" -msgstr "Reputación del email" +msgstr "Hilo de mensajes" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_res_partner_filter msgid "Email bounced" -msgstr "E mail rebotado" +msgstr "Email rebotado" #. module: mail_tracking +#: model:ir.model,name:mail_tracking.model_mail_resend_message +msgid "Email resend wizard" +msgstr "Asistente de reenvio de correo electrónico" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:73 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_form #: selection:mail.tracking.email,state:0 +#, python-format msgid "Error" msgstr "" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__error_smtp_server msgid "Error SMTP server" -msgstr "" +msgstr "Error servidor SMTP" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__error_description #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__error_description msgid "Error description" -msgstr "" +msgstr "Descripción del error" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__error_details msgid "Error details" -msgstr "" +msgstr "Detalles del error" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__error_type #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__error_type msgid "Error type" -msgstr "" +msgstr "Tipo de error" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__event_type msgid "Event type" -msgstr "" +msgstr "Tipo de evento" #. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/js/failed_message/discuss.js:350 +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:13 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:21 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search +#, python-format msgid "Failed" -msgstr "" +msgstr "Fallido" + +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_account_analytic_account__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_calendar_event__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_hr_department__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_hr_employee__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_hr_job__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_blacklist__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_channel__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_thread__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_res_partner__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_res_users__failed_message_ids +msgid "Failed Messages" +msgstr "Mensajes Fallidos" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:39 +#, python-format +msgid "Failed Recipients:" +msgstr "Destinatarios Fallidos:" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:7 +#, python-format +msgid "Failed messages" +msgstr "Mensajes fallidos" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:23 +#, python-format +msgid "Failed messages appear here." +msgstr "Los mensajes fallidos se muestran aquí." + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_thread.py:92 +#, python-format +msgid "Failed sent messages" +msgstr "Mensajes enviados fallidos" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Group By" -msgstr "" +msgstr "Agrupar por" #. module: mail_tracking #: selection:mail.tracking.event,event_type:0 msgid "Hard bounce" -msgstr "" +msgstr "Rebote duro" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_bounced_mixin__id #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__id #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__id msgid "ID" -msgstr "ID" +msgstr "" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "IP" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__is_failed_message +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__is_failed_message +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__is_failed_message +msgid "Is Failed Message" +msgstr "Es un Mensajes Fallido" + #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__mobile msgid "Is mobile?" -msgstr "" +msgstr "Es móvil?" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_bounced_mixin____last_update @@ -275,49 +353,68 @@ msgstr "Última actualización en" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_ir_mail_server -#, fuzzy msgid "Mail Server" -msgstr "ir.mail_server" +msgstr "Servidor de correo" + +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__mail_tracking_needs_action +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__mail_tracking_needs_action +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__mail_tracking_needs_action +msgid "Mail Tracking Needs Action" +msgstr "Mail Tracking necesita acción" + +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__mail_tracking_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__mail_tracking_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__mail_tracking_ids +msgid "Mail Trackings" +msgstr "" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_bounced_mixin -#, fuzzy msgid "Mail bounced mixin" -msgstr "E mail rebotado" +msgstr "Correo rebotado mixin" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_tracking_email msgid "MailTracking email" -msgstr "MailTracking email" +msgstr "MailTracking correo" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search msgid "MailTracking email search" -msgstr "" +msgstr "MailTracking búsqueda de correo" #. module: mail_tracking #: model:ir.actions.act_window,name:mail_tracking.action_view_mail_tracking_email #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_tree msgid "MailTracking emails" -msgstr "" +msgstr "MailTracking correos" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_tracking_event #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_form #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_form msgid "MailTracking event" -msgstr "MailTracking event" +msgstr "MailTracking evento" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "MailTracking event search" -msgstr "" +msgstr "MailTracking búsqueda de eventos" #. module: mail_tracking #: model:ir.actions.act_window,name:mail_tracking.action_view_mail_tracking_event #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_tree msgid "MailTracking events" -msgstr "" +msgstr "MailTracking eventos" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:30 +#, python-format +msgid "Mark all as reviewed" +msgstr "Marcar todos como revisado" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_message @@ -329,10 +426,10 @@ msgstr "Mensaje" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/js/mail_tracking.js:135 +#: code:addons/mail_tracking/static/src/js/mail_tracking.js:147 #, python-format msgid "Message tracking" -msgstr "" +msgstr "Seguimiento del mensaje" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search @@ -351,17 +448,19 @@ msgstr "SO" #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search #: selection:mail.tracking.event,event_type:0 msgid "Open" -msgstr "" +msgstr "Abrir" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:75 #: selection:mail.tracking.email,state:0 +#, python-format msgid "Opened" -msgstr "" +msgstr "Abierto" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__os_family msgid "Operating system family" -msgstr "" +msgstr "Familia del sistema operativo" #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_mail @@ -379,83 +478,127 @@ msgstr "Empresa" #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_tree #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Recipient" -msgstr "" +msgstr "Destinatario" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__recipient msgid "Recipient email" -msgstr "" +msgstr "Correo del destinatario" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__recipient_address #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__recipient_address msgid "Recipient email address" -msgstr "" +msgstr "Dirección de correo de destinatario" #. module: mail_tracking #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 msgid "Rejected" -msgstr "" +msgstr "Rechazado" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/common.xml:10 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:35 +#, python-format +msgid "Retry" +msgstr "Reintentar" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__smtp_server msgid "SMTP server" -msgstr "" +msgstr "Servidor SMTP" + +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__token +msgid "Security Token" +msgstr "Token de seguridad" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_tree msgid "Sender" -msgstr "" +msgstr "Remitente" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__sender msgid "Sender email" -msgstr "" +msgstr "Correo del remitente" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:74 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 +#, python-format msgid "Sent" -msgstr "" +msgstr "Enviado" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:30 +#, python-format +msgid "Set all as reviewed" +msgstr "Marcar todos como revisados" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/common.xml:7 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:32 +#, python-format +msgid "Set as Reviewed" +msgstr "Marcar como Revisado" #. module: mail_tracking #: selection:mail.tracking.event,event_type:0 msgid "Soft bounce" -msgstr "" +msgstr "Rebote débil" #. module: mail_tracking #: selection:mail.tracking.email,state:0 msgid "Soft bounced" -msgstr "" +msgstr "Rebotado débil" #. module: mail_tracking #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 msgid "Spam" -msgstr "" +msgstr "No deseado" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__state #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search msgid "State" -msgstr "" +msgstr "Estado" #. module: mail_tracking -#. openerp-web -#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:96 +#: code:addons/mail_tracking/models/mail_message.py:76 #, python-format -msgid "Status: unknown" -msgstr "" +msgid "Status: %s" +msgstr "Estado: %s" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__name #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search msgid "Subject" +msgstr "Asunto" + +#. module: mail_tracking +#: model:ir.model.fields,help:mail_tracking.field_mail_compose_message__mail_tracking_needs_action +#: model:ir.model.fields,help:mail_tracking.field_mail_mail__mail_tracking_needs_action +#: model:ir.model.fields,help:mail_tracking.field_mail_message__mail_tracking_needs_action +msgid "The message tracking will be considered to filter tracking issues" msgstr "" +"El seguimiento del correo puede ser considerado para filtrar incidencia de " +"seguimiento" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:82 +#, python-format +msgid "The partner doesn't have a defined email" +msgstr "El socio no tiene un correo definido" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__time @@ -463,27 +606,26 @@ msgstr "" #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Time" -msgstr "" +msgstr "Tiempo" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:53 +#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:54 #, python-format msgid "To:" -msgstr "" +msgstr "Para:" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_res_partner__tracking_emails_count #: model:ir.model.fields,field_description:mail_tracking.field_res_users__tracking_emails_count -#, fuzzy msgid "Tracking Emails Count" -msgstr "MailTracking email" +msgstr "Contador de correos con seguimiento" #. module: mail_tracking #: model:ir.ui.menu,name:mail_tracking.menu_mail_tracking_email #: model_terms:ir.ui.view,arch_db:mail_tracking.view_partner_form msgid "Tracking emails" -msgstr "" +msgstr "Correos con seguimiento" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__tracking_event_ids @@ -494,15 +636,15 @@ msgstr "Eventos de seguimiento" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/js/mail_tracking.js:115 +#: code:addons/mail_tracking/static/src/js/mail_tracking.js:127 #, python-format msgid "Tracking partner" -msgstr "" +msgstr "Socio del seguimiento" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Type" -msgstr "" +msgstr "Tipo" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search @@ -515,22 +657,28 @@ msgstr "" msgid "UTC timestamp" msgstr "" +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:75 +#, python-format +msgid "Unknown" +msgstr "Desconocido" + #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Unsubscribe" -msgstr "" +msgstr "Darse de baja" #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 msgid "Unsubscribed" -msgstr "" +msgstr "Dado de baja" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__ip msgid "User IP" -msgstr "" +msgstr "IP del usuario" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__user_agent @@ -554,4 +702,10 @@ msgstr "" #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__user_country_id msgid "User country" -msgstr "" +msgstr "País del Usuario" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:73 +#, python-format +msgid "Waiting" +msgstr "Esperando" diff --git a/mail_tracking/i18n/mail_tracking.pot b/mail_tracking/i18n/mail_tracking.pot index f9e75ab..7cfc6d1 100644 --- a/mail_tracking/i18n/mail_tracking.pot +++ b/mail_tracking/i18n/mail_tracking.pot @@ -27,6 +27,12 @@ msgid " * The 'Error' status indicates that there was an error when trying to se "" msgstr "" +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:189 +#, python-format +msgid "-Unknown Author-" +msgstr "" + #. module: mail_tracking #: model:ir.model.fields,help:mail_tracking.field_mail_compose_message__email_cc #: model:ir.model.fields,help:mail_tracking.field_mail_message__email_cc @@ -54,8 +60,8 @@ msgid "Bounced" msgstr "" #. module: mail_tracking -#: code:addons/mail_tracking/models/mail_thread.py:39 -#: code:addons/mail_tracking/models/mail_thread.py:43 +#: code:addons/mail_tracking/models/mail_thread.py:64 +#: code:addons/mail_tracking/models/mail_thread.py:68 #: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__email_cc #: model:ir.model.fields,field_description:mail_tracking.field_mail_message__email_cc #, python-format @@ -77,6 +83,20 @@ msgstr "" msgid "Clicked URL" msgstr "" +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:22 +#, python-format +msgid "Congratulations, you don't have any failed messages" +msgstr "" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/js/failed_message/discuss.js:231 +#, python-format +msgid "Congratulations, your failed mailbox is empty" +msgstr "" + #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_res_partner msgid "Contact" @@ -121,10 +141,12 @@ msgid "Deferred" msgstr "" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:74 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 +#, python-format msgid "Delivered" msgstr "" @@ -169,8 +191,15 @@ msgid "Email bounced" msgstr "" #. module: mail_tracking +#: model:ir.model,name:mail_tracking.model_mail_resend_message +msgid "Email resend wizard" +msgstr "" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:73 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_form #: selection:mail.tracking.email,state:0 +#, python-format msgid "Error" msgstr "" @@ -202,11 +231,54 @@ msgid "Event type" msgstr "" #. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/js/failed_message/discuss.js:350 +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:13 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:21 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search +#, python-format msgid "Failed" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_account_analytic_account__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_calendar_event__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_blacklist__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_channel__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_thread__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_res_partner__failed_message_ids +#: model:ir.model.fields,field_description:mail_tracking.field_res_users__failed_message_ids +msgid "Failed Messages" +msgstr "" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:39 +#, python-format +msgid "Failed Recipients:" +msgstr "" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:7 +#, python-format +msgid "Failed messages" +msgstr "" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:23 +#, python-format +msgid "Failed messages appear here." +msgstr "" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_thread.py:92 +#, python-format +msgid "Failed sent messages" +msgstr "" + #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search @@ -230,6 +302,13 @@ msgstr "" msgid "IP" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__is_failed_message +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__is_failed_message +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__is_failed_message +msgid "Is Failed Message" +msgstr "" + #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__mobile msgid "Is mobile?" @@ -259,6 +338,20 @@ msgstr "" msgid "Mail Server" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__mail_tracking_needs_action +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__mail_tracking_needs_action +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__mail_tracking_needs_action +msgid "Mail Tracking Needs Action" +msgstr "" + +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_compose_message__mail_tracking_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_mail__mail_tracking_ids +#: model:ir.model.fields,field_description:mail_tracking.field_mail_message__mail_tracking_ids +msgid "Mail Trackings" +msgstr "" + #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_bounced_mixin msgid "Mail bounced mixin" @@ -298,6 +391,13 @@ msgstr "" msgid "MailTracking events" msgstr "" +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:30 +#, python-format +msgid "Mark all as reviewed" +msgstr "" + #. module: mail_tracking #: model:ir.model,name:mail_tracking.model_mail_message #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__mail_message_id @@ -308,7 +408,7 @@ msgstr "" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/js/mail_tracking.js:135 +#: code:addons/mail_tracking/static/src/js/mail_tracking.js:147 #, python-format msgid "Message tracking" msgstr "" @@ -333,7 +433,9 @@ msgid "Open" msgstr "" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:75 #: selection:mail.tracking.email,state:0 +#, python-format msgid "Opened" msgstr "" @@ -377,11 +479,24 @@ msgstr "" msgid "Rejected" msgstr "" +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/common.xml:10 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:35 +#, python-format +msgid "Retry" +msgstr "" + #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__smtp_server msgid "SMTP server" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__token +msgid "Security Token" +msgstr "" + #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_tree @@ -394,13 +509,30 @@ msgid "Sender email" msgstr "" #. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:74 #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_email_search #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search #: selection:mail.tracking.email,state:0 #: selection:mail.tracking.event,event_type:0 +#, python-format msgid "Sent" msgstr "" +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/discuss.xml:30 +#, python-format +msgid "Set all as reviewed" +msgstr "" + +#. module: mail_tracking +#. openerp-web +#: code:addons/mail_tracking/static/src/xml/failed_message/common.xml:7 +#: code:addons/mail_tracking/static/src/xml/failed_message/thread.xml:32 +#, python-format +msgid "Set as Reviewed" +msgstr "" + #. module: mail_tracking #: selection:mail.tracking.event,event_type:0 msgid "Soft bounce" @@ -424,10 +556,9 @@ msgid "State" msgstr "" #. module: mail_tracking -#. openerp-web -#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:96 +#: code:addons/mail_tracking/models/mail_message.py:76 #, python-format -msgid "Status: unknown" +msgid "Status: %s" msgstr "" #. module: mail_tracking @@ -436,6 +567,19 @@ msgstr "" msgid "Subject" msgstr "" +#. module: mail_tracking +#: model:ir.model.fields,help:mail_tracking.field_mail_compose_message__mail_tracking_needs_action +#: model:ir.model.fields,help:mail_tracking.field_mail_mail__mail_tracking_needs_action +#: model:ir.model.fields,help:mail_tracking.field_mail_message__mail_tracking_needs_action +msgid "The message tracking will be considered to filter tracking issues" +msgstr "" + +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:82 +#, python-format +msgid "The partner doesn't have a defined email" +msgstr "" + #. module: mail_tracking #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_email__time #: model:ir.model.fields,field_description:mail_tracking.field_mail_tracking_event__time @@ -446,7 +590,7 @@ msgstr "" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:53 +#: code:addons/mail_tracking/static/src/xml/mail_tracking.xml:54 #, python-format msgid "To:" msgstr "" @@ -472,7 +616,7 @@ msgstr "" #. module: mail_tracking #. openerp-web -#: code:addons/mail_tracking/static/src/js/mail_tracking.js:115 +#: code:addons/mail_tracking/static/src/js/mail_tracking.js:127 #, python-format msgid "Tracking partner" msgstr "" @@ -493,6 +637,12 @@ msgstr "" msgid "UTC timestamp" msgstr "" +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:75 +#, python-format +msgid "Unknown" +msgstr "" + #. module: mail_tracking #: model_terms:ir.ui.view,arch_db:mail_tracking.view_mail_tracking_event_search msgid "Unsubscribe" @@ -534,3 +684,9 @@ msgstr "" msgid "User country" msgstr "" +#. module: mail_tracking +#: code:addons/mail_tracking/models/mail_message.py:73 +#, python-format +msgid "Waiting" +msgstr "" + diff --git a/mail_tracking/migrations/12.0.2.0.0/post-migrate.py b/mail_tracking/migrations/12.0.2.0.0/post-migrate.py new file mode 100644 index 0000000..6b2993c --- /dev/null +++ b/mail_tracking/migrations/12.0.2.0.0/post-migrate.py @@ -0,0 +1,11 @@ +# Copyright 2019 Alexandre Díaz +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + + +from openupgradelib.openupgrade import migrate + + +@migrate() +def migrate(env, version): + cr = env.cr + cr.execute("UPDATE mail_tracking_email SET token = NULL") diff --git a/mail_tracking/models/__init__.py b/mail_tracking/models/__init__.py index aab92b3..896721a 100644 --- a/mail_tracking/models/__init__.py +++ b/mail_tracking/models/__init__.py @@ -6,6 +6,6 @@ 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 +from . import mail_resend_message diff --git a/mail_tracking/models/mail_bounced_mixin.py b/mail_tracking/models/mail_bounced_mixin.py index 9f26507..91737d4 100644 --- a/mail_tracking/models/mail_bounced_mixin.py +++ b/mail_tracking/models/mail_bounced_mixin.py @@ -1,12 +1,8 @@ # Copyright 2018 Tecnativa - Ernesto Tejeda # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import logging - from odoo import api, fields, models -_logger = logging.getLogger(__name__) - class MailBouncedMixin(models.AbstractModel): """ A mixin class to use if you want to add is_bounced flag on a model. diff --git a/mail_tracking/models/mail_composer.py b/mail_tracking/models/mail_composer.py deleted file mode 100644 index f4f847e..0000000 --- a/mail_tracking/models/mail_composer.py +++ /dev/null @@ -1,30 +0,0 @@ -# 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 diff --git a/mail_tracking/models/mail_mail.py b/mail_tracking/models/mail_mail.py index 2342d1c..1d4f42b 100644 --- a/mail_tracking/models/mail_mail.py +++ b/mail_tracking/models/mail_mail.py @@ -12,6 +12,7 @@ class MailMail(models.Model): _inherit = 'mail.mail' def _tracking_email_prepare(self, partner, email): + """Prepare email.tracking.email record values""" ts = time.time() dt = datetime.utcfromtimestamp(ts) email_to_list = email.get('email_to', []) @@ -28,7 +29,9 @@ class MailMail(models.Model): } def _send_prepare_values(self, partner=None): - email = super(MailMail, self)._send_prepare_values(partner=partner) + """Creates the mail.tracking.email record and adds the image tracking + to the email""" + email = super()._send_prepare_values(partner=partner) vals = self._tracking_email_prepare(partner, email) tracking_email = self.env['mail.tracking.email'].sudo().create(vals) return tracking_email.tracking_img_add(email) diff --git a/mail_tracking/models/mail_message.py b/mail_tracking/models/mail_message.py index f67def5..f552b7b 100644 --- a/mail_tracking/models/mail_message.py +++ b/mail_tracking/models/mail_message.py @@ -22,12 +22,29 @@ class MailMessage(models.Model): " to filter tracking issues", default=False, ) + is_failed_message = fields.Boolean(compute="_compute_is_failed_message") @api.model def get_failed_states(self): + """The 'failed' states of the message""" return {'error', 'rejected', 'spam', 'bounced', 'soft-bounced'} + @api.depends('mail_tracking_needs_action', 'author_id', 'partner_ids', + 'mail_tracking_ids.state') + def _compute_is_failed_message(self): + """Compute 'is_failed_message' field for the active user""" + failed_states = self.get_failed_states() + for message in self: + needs_action = message.mail_tracking_needs_action + involves_me = self.env.user.partner_id in ( + message.author_id | message.partner_ids) + has_failed_trackings = failed_states.intersection( + message.mapped("mail_tracking_ids.state")) + message.is_failed_message = bool( + needs_action and involves_me and has_failed_trackings) + def _tracking_status_map_get(self): + """Map tracking states to be used in chatter""" return { 'False': 'waiting', 'error': 'error', @@ -43,6 +60,7 @@ class MailMessage(models.Model): } def _partner_tracking_status_get(self, tracking_email): + """Determine tracking status""" tracking_status_map = self._tracking_status_map_get() status = 'unknown' if tracking_email: @@ -51,12 +69,23 @@ class MailMessage(models.Model): return status def _partner_tracking_status_human_get(self, status): + """Translations for tracking statuses to be used on qweb""" statuses = {'waiting': _('Waiting'), 'error': _('Error'), 'sent': _('Sent'), 'delivered': _('Delivered'), 'opened': _('Opened'), 'unknown': _('Unknown')} return _("Status: %s") % statuses[status] + @api.model + def _get_error_description(self, tracking): + """Translations for error descriptions to be used on qweb""" + descriptions = { + 'no_recipient': _("The partner doesn't have a defined email"), + } + return descriptions.get(tracking.error_type, + tracking.error_description) + def tracking_status(self): + """Generates a complete status tracking of the messages by partner""" res = {} for message in self: partner_trackings = [] @@ -79,6 +108,9 @@ class MailMessage(models.Model): 'status': status, 'status_human': self._partner_tracking_status_human_get(status), + 'error_type': tracking.error_type, + 'error_description': + self._get_error_description(tracking), 'tracking_id': tracking.id, 'recipient': recipient, 'partner_id': tracking.partner_id.id, @@ -94,114 +126,132 @@ class MailMessage(models.Model): partners |= message.needaction_partner_ids # Remove recipients already included partners -= partners_already + tracking_unkown_values = { + 'status': 'unknown', + 'status_human': self._partner_tracking_status_human_get( + 'unknown'), + 'error_type': False, + 'error_description': False, + 'tracking_id': False, + } for partner in partners: # If there is partners not included, then status is 'unknown' - # Because can be an Cc recipient + # and perhaps a Cc recipient isCc = False if partner.email in email_cc_list: email_cc_list.discard(partner.email) isCc = True - partner_trackings.append({ - 'status': 'unknown', - 'status_human': - self._partner_tracking_status_human_get('unknown'), - 'tracking_id': False, + tracking_unkown_values.update({ 'recipient': partner.name, 'partner_id': partner.id, 'isCc': isCc, }) + partner_trackings.append(tracking_unkown_values.copy()) for email in email_cc_list: # If there is Cc without partner - partner_trackings.append({ - 'status': 'unknown', - 'status_human': - self._partner_tracking_status_human_get('unknown'), - 'tracking_id': False, + tracking_unkown_values.update({ 'recipient': email, 'partner_id': False, 'isCc': True, }) - res[message.id] = partner_trackings + partner_trackings.append(tracking_unkown_values.copy()) + res[message.id] = { + 'partner_trackings': partner_trackings, + 'is_failed_message': message.is_failed_message, + } return res @api.model def _message_read_dict_postprocess(self, messages, message_tree): - res = super(MailMessage, self)._message_read_dict_postprocess( + """Preare values to be used by the chatter widget""" + res = super()._message_read_dict_postprocess( messages, message_tree) 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() + tracking_statuses = mail_messages.tracking_status() for message_dict in messages: mail_message_id = message_dict.get('id', False) if mail_message_id: - message_dict.update({ - 'partner_trackings': partner_trackings[mail_message_id], - 'failed_message': failed_message[mail_message_id], - }) - message_dict['partner_trackings'] = \ - partner_trackings[mail_message_id] + message_dict.update(tracking_statuses[mail_message_id]) return res - @api.model - def _prepare_dict_failed_message(self, message): - failed_trackings = message.mail_tracking_ids.filtered( + @api.multi + def _prepare_dict_failed_message(self): + """Preare values to be used by the chatter widget""" + self.ensure_one() + failed_trackings = self.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() + if self.author_id: + author = self.author_id.name_get()[0] + else: + author = (-1, _('-Unknown Author-')) return { - 'id': message.id, - 'date': message.date, - 'author_id': message.author_id.name_get()[0], - 'body': message.body, + 'id': self.id, + 'date': self.date, + 'author': author, + 'body': self.body, 'failed_recipients': failed_recipients, } @api.multi def get_failed_messages(self): - return [self._prepare_dict_failed_message(msg) for msg in self] + """Returns the list of failed messages to be used by the + failed_messages widget""" + return [msg._prepare_dict_failed_message() + for msg in self.sorted('date', reverse=True)] @api.multi def toggle_tracking_status(self): - """Toggle message tracking action needed to ignore them in the tracking - issues filter""" + """Toggle message tracking action. + + This will mark them to be (or not) ignored in the tracking issues + filter. + """ + self.check_access_rule('read') self.mail_tracking_needs_action = not self.mail_tracking_needs_action - return self.mail_tracking_needs_action + notification = { + 'type': 'toggle_tracking_status', + 'message_ids': [self.id], + 'needs_actions': self.mail_tracking_needs_action + } + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), + notification) def _get_failed_message_domain(self): - return [ - ('mail_tracking_ids.state', 'in', list(self.get_failed_states())), - ('mail_tracking_needs_action', '=', True) + domain = self.env['mail.thread']._get_failed_message_domain() + domain += [ + '|', + ('partner_ids', 'in', [self.env.user.partner_id.id]), + ('author_id', '=', self.env.user.partner_id.id), ] + return domain @api.model def get_failed_count(self): - """ Gets the number of failed messages """ + """ Gets the number of failed messages used on discuss mailbox item""" 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) + def set_all_as_reviewed(self): + """ Sets all messages in the given domain as reviewed. - @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 + Used by Discuss """ + + unreviewed_messages = self.search(self._get_failed_message_domain()) + unreviewed_messages.write({'mail_tracking_needs_action': False}) + ids = unreviewed_messages.ids + + self.env['bus.bus'].sendone( + (self._cr.dbname, 'res.partner', + self.env.user.partner_id.id), + { + 'type': 'toggle_tracking_status', + 'message_ids': ids, + 'needs_actions': False, + } + ) + + return ids diff --git a/mail_tracking/models/mail_resend_message.py b/mail_tracking/models/mail_resend_message.py new file mode 100644 index 0000000..555e30b --- /dev/null +++ b/mail_tracking/models/mail_resend_message.py @@ -0,0 +1,64 @@ +# Copyright 2019 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models, api + + +class MailResendMessage(models.TransientModel): + _inherit = "mail.resend.message" + + @api.model + def default_get(self, fields): + rec = super().default_get(fields) + message_id = self._context.get('mail_message_to_resend') + if message_id: + MailMessageObj = self.env['mail.message'] + mail_message_id = MailMessageObj.browse(message_id) + failed_states = MailMessageObj.get_failed_states() + tracking_ids = mail_message_id.mail_tracking_ids.filtered( + lambda x: x.state in failed_states) + if any(tracking_ids): + partner_ids = [(0, 0, { + "partner_id": tracking.partner_id.id, + "name": tracking.partner_id.name, + "email": tracking.partner_id.email, + "resend": True, + "message": tracking.error_description, + }) for tracking in tracking_ids] + rec['partner_ids'].extend(partner_ids) + return rec + + @api.multi + def resend_mail_action(self): + for wizard in self: + to_send = wizard.partner_ids.filtered("resend").mapped( + "partner_id") + if to_send: + # Set as reviewed + wizard.mail_message_id.mail_tracking_needs_action = False + # Reset mail.tracking.email state + tracking_ids = wizard.mail_message_id.mail_tracking_ids\ + .filtered(lambda x: x.partner_id in to_send) + tracking_ids.write({'state': False}) + # Send bus notifications to update Discuss and + # mail_failed_messages widget + notifications = [ + [ + (self._cr.dbname, 'res.partner', + self.env.user.partner_id.id), + { + 'type': 'update_failed_messages', + } + ], + [ + (self._cr.dbname, 'res.partner', + self.env.user.partner_id.id), + { + 'type': 'toggle_tracking_status', + 'message_ids': [self.mail_message_id.id], + 'needs_actions': False, + } + ] + ] + self.env['bus.bus'].sendmany(notifications) + super().resend_mail_action() diff --git a/mail_tracking/models/mail_thread.py b/mail_tracking/models/mail_thread.py index 920fa17..4a385b4 100644 --- a/mail_tracking/models/mail_thread.py +++ b/mail_tracking/models/mail_thread.py @@ -14,12 +14,25 @@ class MailThread(models.AbstractModel): 'mail.message', 'res_id', string='Failed Messages', domain=lambda self: [('model', '=', self._name)] - + self.env['mail.message']._get_failed_message_domain(), - auto_join=True) + + self._get_failed_message_domain()) + + def _get_failed_message_domain(self): + """Domain used to display failed messages on the 'failed_messages' + widget""" + failed_states = self.env['mail.message'].get_failed_states() + return [ + ('mail_tracking_needs_action', '=', True), + ('mail_tracking_ids.state', 'in', list(failed_states)), + ] @api.multi @api.returns('self', lambda value: value.id) def message_post(self, *args, **kwargs): + """Adds CC recipient to the message. + + Because Odoo implementation avoid store cc recipients we ensure that + this information its written into the mail.message record. + """ new_message = super().message_post(*args, **kwargs) email_cc = kwargs.get('cc') if email_cc: @@ -31,7 +44,9 @@ 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.""" + + If the recipient has a res.partner, use it. + """ res = super().message_get_suggested_recipients() ResPartnerObj = self.env['res.partner'] email_cc_formated_list = [] @@ -64,7 +79,7 @@ class MailThread(models.AbstractModel): res = super().fields_view_get( view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) - if view_type != 'search' and view_type != 'form': + if view_type not in {'search', 'form'}: return res doc = etree.XML(res['arch']) if view_type == 'search': diff --git a/mail_tracking/models/mail_tracking_email.py b/mail_tracking/models/mail_tracking_email.py index f7bdcd8..c7e84f0 100644 --- a/mail_tracking/models/mail_tracking_email.py +++ b/mail_tracking/models/mail_tracking_email.py @@ -5,6 +5,7 @@ import logging import urllib.parse import time import re +import uuid from datetime import datetime from odoo import models, api, fields, tools @@ -91,14 +92,30 @@ class MailTrackingEmail(models.Model): tracking_event_ids = fields.One2many( string="Tracking events", comodel_name='mail.tracking.event', inverse_name='tracking_email_id', readonly=True) + # Token isn't generated here to have compatibility with older trackings. + # New trackings have token and older not + token = fields.Char(string="Security Token", readonly=True, + default=lambda s: uuid.uuid4().hex, + groups="base.group_system") + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + failed_states = self.env['mail.message'].get_failed_states() + records \ + .filtered(lambda one: one.state in failed_states) \ + .mapped("mail_message_id") \ + .write({'mail_tracking_needs_action': True}) + return records @api.multi def write(self, vals): - if vals.get('state') in self.env['mail.message'].get_failed_states(): + super().write(vals) + state = vals.get('state') + if state and 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 def email_is_bounced(self, email): @@ -162,7 +179,9 @@ class MailTrackingEmail(models.Model): @api.depends('recipient') def _compute_recipient_address(self): for email in self: - if email.recipient: + is_empty_recipient = (not email.recipient + or '' in email.recipient) + if not is_empty_recipient: matches = re.search(r'<(.*@.*)>', email.recipient) if matches: email.recipient_address = matches.group(1).lower() @@ -187,13 +206,23 @@ class MailTrackingEmail(models.Model): def _get_mail_tracking_img(self): m_config = self.env['ir.config_parameter'] - base_url = (m_config.get_param('mail_tracking.base.url') or - m_config.get_param('web.base.url')) - path_url = ( - 'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % { - 'db': self.env.cr.dbname, - 'tracking_email_id': self.id, - }) + base_url = (m_config.get_param('mail_tracking.base.url') + or m_config.get_param('web.base.url')) + if self.token: + path_url = ( + 'mail/tracking/open/%(db)s/%(tracking_email_id)s/%(token)s/' + 'blank.gif' % { + 'db': self.env.cr.dbname, + 'tracking_email_id': self.id, + 'token': self.token, + }) + else: + # This is here for compatibility with older records + path_url = ( + 'mail/tracking/open/%(db)s/%(tracking_email_id)s/blank.gif' % { + 'db': self.env.cr.dbname, + 'tracking_email_id': self.id, + }) track_url = urllib.parse.urljoin(base_url, path_url) return ( ' Technical > Email > Tracking emails * Settings > Technical > Email > Tracking events -When the message generates and 'error' status, it will apear on discuss 'Failed' -channel. Any view that uses 'mail_thread' widget can show the failed messages +When the message generates an 'error' status, it will apear on discuss 'Failed' +channel. Any view with chatter can show the failed messages too. * Discuss @@ -56,3 +61,10 @@ too. * Chatter .. image:: ../static/img/failed_message_widget.png + +You can use "Failed sent messages" filter present in all views to show records +with messages in failed status and that needs an user action. + +* Filter + + .. image:: ../static/img/failed_message_filter.png diff --git a/mail_tracking/static/description/index.html b/mail_tracking/static/description/index.html index 72874c0..36ea5bc 100644 --- a/mail_tracking/static/description/index.html +++ b/mail_tracking/static/description/index.html @@ -376,24 +376,16 @@ right to his name.

  • Installation
  • Usage
  • -<<<<<<< HEAD -
  • Bug Tracker
  • -
  • Credits @@ -412,47 +404,42 @@ For example, --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.

    These are all available status icons:

    -<<<<<<< HEAD -

    unknown Unknown: No email tracking info available. Maybe this notified partner has ‘Receive Inbox Notifications by Email’ == ‘Never’

    -

    waiting Waiting: Waiting to be sent

    -

    error Error: Error while sending

    -

    sent Sent: Sent to SMTP server configured

    -

    delivered Delivered: Delivered to final MX server

    -

    opened Opened: Opened by partner

    -

    cc Cc: It’s a Carbon-Copy recipient. Can’t know the status so is ‘Unknown’

    +

    unknown Unknown: No email tracking info available. Maybe this notified partner has ‘Receive Inbox Notifications by Email’ == ‘Never’

    +

    waiting Waiting: Waiting to be sent

    +

    error Error: Error while sending

    +

    sent Sent: Sent to SMTP server configured

    +

    delivered Delivered: Delivered to final MX server

    +

    opened Opened: Opened by partner

    +

    cc Cc: It’s a Carbon-Copy recipient. Can’t know the status so is ‘Unknown’

    +

    noemail No Email: The partner doesn’t have a defined email

    If you want to see all tracking emails and events you can go to

    • Settings > Technical > Email > Tracking emails
    • Settings > Technical > Email > Tracking events
    • -======= -

      unknown Unknown: No email tracking info available. Maybe this notified partner has ‘Receive Inbox Notifications by Email’ == ‘Never’

      -

      waiting Waiting: Waiting to be sent

      -

      error Error: Error while sending

      -

      sent Sent: Sent to SMTP server configured

      -

      delivered Delivered: Delivered to final MX server

      -

      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 +

    +

    When the message generates an ‘error’ status, it will apear on discuss ‘Failed’ +channel. Any view with chatter can show the failed messages too.

    • Discuss

      -https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_discuss.png +https://raw.githubusercontent.com/OCA/social/12.0/mail_tracking/static/img/failed_message_discuss.png
    • Chatter

      -https://raw.githubusercontent.com/OCA/social/11.0/mail_tracking/static/img/failed_message_widget.png +https://raw.githubusercontent.com/OCA/social/12.0/mail_tracking/static/img/failed_message_widget.png +
    • +
    +

    You can use “Failed sent messages” filter present in all views to show records +with messages in failed status and that needs an user action.

    +
      +
    • Filter

      +https://raw.githubusercontent.com/OCA/social/12.0/mail_tracking/static/img/failed_message_filter.png

    Known issues / Roadmap

      -
    • Handle message updates on discuss ‘channel_failed’ instead of showing the -‘outdated’ message.
    • -
    • Adapt chat_manager changes in v12
    • -
    • Adapt discuss changes in v12
    • Add pivot for tracking events and mail trackings
    • ->>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View)
    @@ -486,9 +473,9 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
-

Other credits

+

Other credits

-

Images

+

Images

  • Odoo Community Association: Icon.
  • Thanks to LlubNek and Openclipart for the icon.
  • @@ -496,11 +483,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
-<<<<<<< HEAD -

Maintainers

-======= -

Maintainers

->>>>>>> 75b9662... [IMP] mail_tracking: Failed Messages (Discuss & View) +

Maintainers

This module is maintained by the OCA.

Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose diff --git a/mail_tracking/static/img/failed_message_filter.png b/mail_tracking/static/img/failed_message_filter.png new file mode 100644 index 0000000000000000000000000000000000000000..7843d140eae22c488b25b7109b49ef7cb9e3c522 GIT binary patch literal 97662 zcmb5UWmsI@vNhU36B=(UKyV4}1b3I<(6|Pd4({#}+&xI}puybOK z;2cF|)IcE6@}}Yj06+?m5f@Q&TRLv@bW@vcJAc(n@lHz*nDLR4ikqDdX7@l0${bq z{{Q+w7ZfWcLLQeXQ|{#CG?Y&oCq4EjP`l8x_ZRC>LrD3mV3E#d z=yw!eHxt<=^3vdyaWdWgmd|e+4lY_Gv`+5DR1&h96)VC3{Ny}8_pGly4igZ`_%%G; zoPB#pqp#Q5)DfGTi**+Jb~_d15dsI+o2KPTh+bYVlRAlidk%;CO`@wPX76Xr4p=}m zkvy;3{PVqp#){gf-H5^#-oe=IfpL?{bM`OqDcJrDC-q1f#BPlr zqG~$5c7=4bH8kZe^GB<`y6zf)}6DF+Oy zEeNf`hueLTE9@nC#Hu(60d9tg`Oi~9eNWe`JF6w7{!=h_p&-X9@^gH zen})bz#dO3*c+zO3E#FMzYM0`5&T`-QDrmzU?U#O<#8imz>cem2{l4`l$?CQ7BCz4=Kc}cQ^MkF_p^?P!m0o8(@_Lj8 z1P^F3A6;BBw!i+PF-P)Il1+T{kbl&(p$p z^}FlyeG->%o0%&+01zqXq0s(w^I&>1;(B+O51j|DwQKEhph1X+_w}ft=W%dnJ1tRq z>;tAJI;&siFw-;u5I@D@0%$mWnAXyd4DrH7H$%1+iAqGE7XU=Wv1^#>ooyx z%+cJ-b7h92<^(F^+Vq*VH5HE%z;LpyoG4lp$Bb&~aou*UuB?Ya(DotoGxIr|-^KbC z)59(F>}EAqO?{1AvGC0!!M1$h58A|tQLR6fVkC5y7zhA>P|3tKyX7QTHhe>tr~Utd~nzI~2vD^JE`X!M%e;nK$%oCg3hZ=OA~ z*P$QJJ-uBJ1-M_`-RVBY|3W-Vr8Ri)a93nr7N3IYNjce0UwrMd^?klUkO@<6=P)_t z%>25u)fXLz_90EJBJAM%P%7!6z-!G_B@J`rw)->jZ%VxDG9+QoReHtNcp&$elz;g@ zn6TNfH-&y!QtF8q9-#S0$+Ktfma}6rj^ElPZWqV*o}Pl4p!`JpxoG`N;Ll6qKY^$& z<_s@4AmHf=R^5;~$+oFUk{+|to#ttAjU_4LcMsHh!<_?wBr)t8sQiN%|3Q*}!0A6o z^54MbpY{KM&Ho#O1{aZ5J38e54O}9kAb0FHTe*P$e!+kC{MWty2e2IsZ|DWOzrH+I zYmU@IwGiL1a+vg_wZr%I#fewoOD_llG5r`TZG!3aZvXf=QQ@B_9;1w8J;46|gvf2( z&>^vj;r6<#?4`*^;d!wFV`{~0emY9@$hUWP_^^-TI+mdc1Q_;z;QFauSMRfyg`>jo z&tofy(T5`9M3v(eG}QKPi^NHr)H;~47)SAaxUQa-g#moYu(J9x{a%5I_sQ_Gd$3@) zUH)ipmaAV5l%y_~%~`F60ASJKEZ5Ol$7BEJt~XT5)!qbi<;&H~W&y+>oJVUdaU=}q zgdo!nqb3VgU)LA-$|~c?;>4|X2tU@Bn5`A|G%p6U7F~&%zqbQDTnnp&)STsm8z_ODC5It zY9UTKAfN!td`&BHG*^~-ybR5B2V=@PEX~a2NU5(^d5RXZB*{^izPaLP@kp1sZ8yC- zL;Yj{AwL>Pt46_ZD%@3JrOIekHKuPw>0rT{N`Y7hX!Z@CjyH2WNOs`niO5>F!DYHV zwE`FCdekN2hw7WB1y+R@Xpo0OVHvCO_5t8;UB%(-^i*)YH&D$ME9i6aA(Ft43`SNz zo6hN}Mk9m$^oYD<=Q7T&+C%qzC%{5Oz5VLIhXg522PX0}AdyjA_FteJ`((t}z3-ptb2D-mIYi@)rSEG#)I3cP2(&Qne>S73nk zt4^Tis?dI15S{oYsmE@AxlnhMy~I+=>$HtfsWZ!MrGMQE2BcnFYjCKK46hOM{_z*T zMDHTaBKo>q58djIN+>cQkCTc0?*!q04eO-w_o5(Uxon@esM(2BR#K`mA17`C{1U&k zWRdn%%qqx5pF+5>S+;ZcOKbwK6ee=TLJ5}(6Ul?#4Qjj>ZLIi)h#Jx(7!zHM zqBf4lWPFmvs;Al`(!u{BkPESf!CHyuBSE>6EWTrbs$i7GM7oFZ#pWt<= zhEY+sY;cGoPWu4&F$;p+-azJ&se#;4{WO28>(r~7;(gl?<0EmBPYee*z5nWPhjI3> zvAC%CLFj34{@d6RbNl_N{p&@jKUGBI_JFV6W97Y+!2R4+Yjpy<>E*7}h z>Zpi{AS{bw?0EEUwHh3+ZFxO%t<-|d%T&IhWVG01P^d2&I(cdkq~Ot0E+ zw%+8u8zVXVdE{}Ga~F+sJL-8;=W?A_Io515uULMsL(FtPnwa6TdJpDr-#Dpc%=-J} zipH>5f6fN*gHu$7bS^q(iG7ObjV?Mta9W6wU_LVEzsfOuc`hwDR<@@xUZm&;YY zxzcrSr%{N+AklEgjnXmol;i1R*1=qPJ4^f5rW)g4nu2SEZXIJLK+kAty^H-tE zh4>zH>gVA)^tIP#)#ss>?5Bo%UpyM^CX>@?m<9|O@5k} zftHTzjHdy0Xq$?s<3YpgGh^x+MaFeZNApHF>g{)2gJ-V}lIm-&amB~`Vc!;Pm-5y} z9d;6iG95OTtw7hG$o%z(vxx2&j4`8`R|{uF{OonM%KlhMEFN2{uk|XAlg#>AN|@`H z3nPoKaSfZnwpIhoxhYGyK3uMAPd(^GUeAREuY1bwM$+Y(^;)~DFUyqzFK7T=`^TrP z0a~g0@?vTZhF+%Tmt_Ouufkt?Na{0vA49_T9$_3ROWUoFug71Sc9O54%L;W)A2*+_ zY|FHKmoL8EANKmf`2EyU7^ekHR9j1JRrpVRs=F_~Sz+|`>MQlg!vxfpb$H--y~bRt z&(416HF%a^-REhkV4Mb2WWTOj&kIgGmVf@*e*16*3m8a9d3_*xvhV@|DxOYz8eZ&& z94ncdE=xmSpS*l14d9S3uV5$eVo0=R!e#G7c5DwN#}`>modBe-OYn}Bt;Z!P=w6OP z7oYb{4-+flgy$HhXjSOHyBo_1LFuV~RBf->a}*eKsF@r2gFV z9vxcO-4vqv%E*j+){({W?=w0{Q@$=Zl=p55eU=pWc;&ZsN^8rfK%00vGr8ob-md=)Nr8}x5?ez#@Tk$zQA$k z75*K%G72N>^g>I+y7|{HJJ$84eF#sO1Bu{NyU#lnrnhZX^k4gKpAK>dj)n?->*Um- z_~l;F+V?4s?T;z8uYb=5*_Gz*w<_7*kG#Ad_r2b>8+m7Qwm&J!&*yz-N`07Ss@tDB zBJuf~Ozszc%U^3e7|-|Bs(Lo>Tl?P$bX%quhW7GVpN$RwUVtr4_^phnzNb+>TE%~+g93?{;P_Yu@kQCauDI{26pOGVCb6mQ$nR4FLkb- z{Qzpp>503~%=W?DkWR-_mRrv8p{Is`Ziyw!*+Au*=ekf#@RPEJ?_-Nl+^MOXRi~t!KS6Q_OI$|>P>@HL+QM?vl9UDGwPMq?7Gg^*UV}|TxZt1@^~-h>wQPV z-uKJBY#+p;uC?b-NfgFpg*cfB8P=!%fict~uaHj__m!Tw^F(1fC)Yv-H%mD42Dq^U z;#4A-g9ov`!&t-V#Jar3qN3j-D<1mBzw*$QC8|9?CdN-+jvu6uzHMcm0L=RAB`GtJ z^L7y%V*G#an*7f~W{)QDvfiVfy=&uG<=k?MO%gF~fb6aMK-;_UV4UXP^eEYz!b#N& zOjAjhF4*xXd3G2=)-#(o?YxT6qCQn&(DZl@_F%_5#1o?9pu%0Tj30$eR-z;C{v@roaoRpX(D$((W8 z_l=mpUku~fAz9I-YVlnKNyb^^NB-NNtP?+di&TFj@!y-ZC30G0I7Ug20!YS2+p+Cd8CHf%vf~D(~{uqj`5o=HI=Uo73DSxlfQrsYf5S zQU-xZ6+5<_@^KbvzgAI;cpv$X_*0tGA;!x)2Wr#9J&k43IU|2<+ze^4)`@X*SpH+l zwPLp%QJBE=aj~|2_obzS13Hvwx!&yA74cxEGxxho?b}<)m}|2i2?=X+zMd3BC*ZVI z^bROL>BqTUX`#|$HGbJr6l$vVh__w-^A^mXeewh>O51K-nNK3 zzm{XkYi)%(t`6v17B9c2?O1TF$>8d!_40P>pF*(xprMY(6d9TJ>9&i&o51==C{E*e zZq8!p{)eZJm5z*HL4*^UD1xHE{_WYcFe8b<#Y)J-zI;MA01)MTy?Mb-KF$FH_{5M= zuc>!;{8JeKC}&>tp1&!vCMi8`*>|*lF7q6a`>wXISMvLBL7A!{K)B|jZ)ezGX)RNQ zoTAK3ZJZD{Wc$JMY)OxUL;F!zZ005jGL~i_kazkLOkhmgc&jscuQ>hzu6$XY(V*Ji zRR_r{?($%EHepE3&%_uydWq}G$(I|4Grc!b5h-rK|E0`C3_4na+t5%j6A*{BH5lT1|~^wWNDi_H)0kH+2Z9=&NIWT!Hgd|L&y> zjua#1`pV4v2=Qny6^?ErI8v>KZGY{@{(Hp={rmmD=erWf`Xf{f)lqdVWyD%-F4t2T zhQ_1mA>A3odYU+NY3+Xxw2T4A%{$0k4~2pc+lRBC_YzO^+w;cWg|#>D`(O9}mC_Jw zb5hrujt@1k3Z`hVax+sdcbpbetn_(^Lp-1Q!x42Ef46d!G&3Zm@{#*$i+jdCX=l{= zS_WWIT6POj^CI+&EGe}+JW2=TIA4{G;$RKM)F5%&wK7m@b@+HL{yjuAHR=gd8_8xh zJL@0Ss>F``uB}yEDycPl7gwToxW8d}GeliVE#4h+h#c${D_f9LcUq8TY&KF6hPU~< z5#JG4fyiD?aC^a}lp{UBZgED+z|Cch#$Cqi>#U5`4$1!QwyMC48=|UnP@nf>PeVx3NIqNBT-P)>Q&W?SZZ#Z8X_%d#pYQlk z_Va`5adO;cM8HWM^K~1^bZxuKH=<~4@ooi29oymZ)wy&eoNV#Hf%+X}E2G1yqVGP3 z;Zh`-+Jyi>5OPn+T)jTz(`>o+x|_+js@0`;JM6?Be-?4)1OLJor z5>p)-R@QSxbp0trL?AUt?d;r4fB&sHj$h5eELQ&bN=r_CdkM$VQZ4kk=bL`bz<8>r zg_<0lt+wiwmFkoN=RC8)F99}7ukAa}*oY+nz~$3UacN$$p%j6Ybpv00LO}OL!ccvs zR&_Z?nhjL5uGP%(mla_Xq$lF>{m+#-t7T5W0vb1)?pyEP>qWn$n9y;?ga+XC;<&Ew-mo!Iz1$3K@FUzQP5xa3laePi zfi%+o#y1U!dP7O`*~0E84a2|29RKe6{g1Il`ERkeVaI=3j{gib{$=P;FWdlKc2V%D zHlEMg^2Sql$>qO|S2Z4pCTc_nkPZNe;NQ9fEEWZ4N@D$*Q%>IcG5pW*in4TBtWI>H zAF@fc@oU#xNx3NBQb$4!S5_RtX3+2Ye+DQ2KYE;llM}9MujiRhUk0KPF-2MWgEk^B zaz7Te+-R-_5X?ooU6!Q9CAd32{~^#PyjscMAvkR^02tIW zvRs%=pnbqMQ$?TsLU==~%WCby=3rGRJN7|e{}u}}4J3yp+ikAMuunxDvDYI5BHf9m zA2u*be}txltMO*ZQq%g&v6(Jf27xh!K`;T_ThUwG0f=LGj!naUFf2+{0f^OEkppK( z3IiO>&aq`>+P{C#SF!P{U@KV)YZM=W=a*hzJe%~tYkWi^=h;+hsOa_P#adCwXyW1N z2|-=Hos^Opn1b~=ymFA`RT-R1u3vb8yGVBV+QMK=8;0i(fMAy@b)4EyM`I~AUDPh8 z%@Cfy^NK?bISA$<$6LUSIy;KgB4jJ6&62jM;5*$$NOX(_ZV^%hRe>`p$k56(04a6* zBe|0OknF><_+RhjRZH?%Iz<77WS~u0jJ?53X$MP@Nv~hg8*aze;_8{t)&J?u&ELOD`P`G25G8OCCE4q;L~Vt22ex`vryW7 z52pYFU@1+6UBAR1owZrJ&dykzIHD>qYpC;>`#4MB<<$8|jFv{V7`N9%=+LVye%kA6 zIa}njJ~^I)=b9&0$ce99^F8{EL;t-Iv8GZz=D>G(iSNv^T%XT;W)s12ZVdl_N#-+x zo^wh%zmEG(ptN;{+r2!F=Fy15zntH7;=zFeSirzepi)2nS$JH@DSMC-1G4Tsd}%RI zHJCek6b}wun1tcSZ63)g2clBrX39&?^Ehz|;lYOwoG(%`#FmqdqXT0qOvZNmR@@bx zj(wF^)*HAu%zl0p^vZwng~tVGM7>U>)X&T#bHqyPSn)%*_dXcq({yZXbOm}6WB|y% z{X)O>-CknQRWmf?NVaF`7WFfPMFd3hm@it00$_zT$+&n>}`>@Z+NDmSaGl z>B@%CXXYEsbVNCAS9ly$OpU(JBP`uW|8Jz?^vbG-{hL)*2C;|7^>mD!U&{MR%S7r! zIe(xbAb^z$52o7>Lj_R6By~|c$hrwkW6DpbP8$3nKkU+U(wy9*ixx)X2g2=aveSEu zSkvZ%Nb4395Yjtu35iW92 z-88&z5j4Ee7dO*NyGdJ);8@YTDtU5YBHl^WlDLh2K?=cf8XxP|vpud_ z%D*S>k#df(oMXTf{)j|XTVyW{BI5=z0f+k;Kt@1PAgM6G4*(($iuKRK0TUuJ2>-2@ z{`-j^5j>$2yDk22Is=;si6XflImP}oTWL!mbQB&DhQwlam>GU_wzN7=j|17^u;y$~ z$kI;zx!%~~_2;SGNPPOizg~dNmhOAoWw@P@COm;C8Of&t74L$re^Z^Bnv$4y7D#ph zJCICG8Alio{64@=RIdvID>AUq(iGQg6B$?U{rO&h^R!-8)>-TG?K6o_sx%cLQzJOB zm{7Nq&={#R7$6J65oS?W`yP;$lS{WSNZInuK`cejsYx>NCYjTYG+rZ;;Ss&V^Wu`8 z>GuAK2}>+A>AvT_DOk+L-!T*g5^EIwf3yH4zK&9F4xB3yTtB+CifGn?YTG`$x~iwE z8%f(wCoho@idOGbTQ4D*h@=4)$H@aYB9aHG$blsB$bcXq=v^k9c_+rZplW(%}3KGxWwW!B{6>}A%%ee;7ny%cm!ZTPZb8P+(Sy`?k<`=4==hk{xBgY zY5x zr*1WO*RMvVDVUDb?ku30M^^h^;?V^G-lP-&Ojv(F5Q`re);Q91d8h<~3Di)>0cT=+ z{PtO5(Aepy66b%G*s5`HX{>1HW^dQGm7BOYIuZoI7%pGJ*RkA&7twdqb=LQ(X7iAX zSwRCY@zLee;aseH*gwCY`AKryD_`Th^LNliRXBAcyIxwZs@vaKxUa9zUx_76M(NNQ z4`Iad%HcD4pY4fPS$^5@(X}aCvXlm-G_`PN8rv|_a9g!g!4)1w@!~v8YGBZWVJ&N= zeG6)a+=t~PjPOxON#)N~9P-qhwX?6l`Fy$G`pY3|0!q(9owGG18xVQ(+QL)}*+%%< z*LO=v%9A`T%$1sOn5>zKMK;}qn_Bu43Z8du2anC7PY+gujr&xSpTYJizIVaimpjfnY#JWPbK&C@kFp_LQARvpDb+LtbhS2;tf3 z;pr)6W?FL6+v!ZRu^%W+e3zj*mXqvhY&kK^j?s59(mIG0jmb;LFDq|rVNaJ$&D{6t z1L;Vg$fh$^CsJz~1|joYJ2?$#RNjw12tj)znhRchGuAwB16|i1u0n*oYHeevH!NdZ zT7Nyl>6|B?G^bKQsgq?%Y>vYE6JCVFO0~^BHR!V66Owhh^sASYD#^e36^Kk6f-N!G zmYzrF*9hopw8L*8!iAyo1Bdxjpbf#FS^hXU7!1VtLkb4G%c-#)r^QTrm{_qGE%mgm zR3_h1rjc%4A^?1hJnU5}W3?1g$8RhRIe>VP$z_(^1Vul0#gz*r?0e&qj zBbt6;fcmSG24tj&p|KVvJEO(Z7jc{A*O#;U4qKz-5+#8w*#}giT($WbaaRUH6g$3z z{jWlmPQQhsT0Su|-=5ii^?c0g7b`zG$f-(M#*wPb)r_E3a9;7xsdl3`Tck`sdzC;iW zD)m#MGVW{M!rL@tp#fh$JjLbqu=HnM%_S#CCkNK=utoD4hu_>VL1sB5OzZI{Xcv+d zCj0W4K*9Wsp=dViYgJcS0)#fA{Dq^mlF6or>4($KD5R<1z+5ODqhxWce8w_8=SD%8 z7vjqL07n8sb`{+XuRFY3!o}30Z?E8^NN7jmqnOn%mfp#Vt&U@Txq?yQ59ep zt)ZdE&?@qM;3yZVkdSQI1&j6K&eCF*7D^&HZcRp`R@$t$-HP?&ldf{o8l#Z~ zAg9^j^{Q1*Q7Z3+r1WIl9W4q^~8%>vA{9c7UTe+D{fRTl}V@kYP86?sl z*)POL#{9POa{J(`HC>1O#e6!(w`O@<*}`k>NC{Dp{+O`g1IZIfraqN7R7;!KE*nNb zAj)d+AP0C?z5d|eE;cZL-o8#5rExoaVLt#e07&jBCcQD;+_lnpT>r@Vqkr6hui?*h z8_Y#`$euK1(YnbnUQa4%WZITtB#Q_LY_~aC7f;qF_BjIeyAhr~7}nlou>zo^hW@eC zSo=e6nhDqNk1OP>+f#Sn;r04L)_R;IG92z|_$~l3WV+h!XJTPa9=U)G4Jjc|7}DdE z%a|;wiTuu64zKXUg4kDwA!+9CQ)XTy{_wFtVhZjMDlqRWrf2JlvxC@X4#vJh|7h*O z1x-D)U?ox1qH_x;nkcF-GG`N=Zm}M{A8+@qs@C0Q(|`l&vMg^RiTTm=C2nHfB=#-l z3I!M$;&J!G`U{Ja_6Hw1q+~fXPBm5wLX>jm=Ao~xI!GDW-}!vS%3292%|Qx&#xSJ9 zq$s*9K^Wa2tRJ3E@?Bw$Nrn;Ok~k2N)UdpG(Vr2S{Q1Q+;w1&jT+B9~TBpxbMs*T6 zO@iVLW&0&N?HA62Q2GMp;0uOjpD4zMiL}8?Xz-zXxQW}iADW401mf&y=EXwstN`pn zJ3JwOrtNqW_9LA!f{W9I-jBL{qQD-iV;Ep*ja+=m>7c}%{Dh9xiwiQAUEus3Z1UXZ$|au#XPtaf;q^D1Aopuu z4M&FSfG7oXMJliPZsXQ%+deyg(ePz5WXydddupE8ZGXHP+v{~%Z{63*W8>OD;5uV| z{o9MlJ)66`v_jNz7*8aa3ItS=W2FLNL|0k15M5wTSPZ?_Uv+@T?Ih~wZOQ90EatOn zdap^_Zv5EtHu%}U_DCf~!$o@6C*uwVf|x$l#%c@>UckdO8(9zsa0GoqtLRaicE)2# z;;AnS$O%xz(gols<7_4N!Ej#`ui6irg$LxpPFH&ij_zI6*Si8mHwEP7UxZ1q$Le%5 z5I?Iz00{P((gk9hh8heJv`yy$7-^@eF%!q+RdOt305Cu_j5JsU0-3^3VZ{xH835Y% z1NTHhU|kSefc@H4Bv?00^{8R0%HFXw&x)`!b;#k48@)93Q8an>ie5DLf(t_EiWxVv z%W-YIE7;^@C9)Oqn8Kx@Ce-AyBc_Vnz^tXT{Aq)lM1a5Oyin@B*GDzBlh>z99o*BG zGm@9f&YfSrH#6fqb!0rIHPARw7KWPX9DA5Bf5R~H**&|DJ<{oAoY(%uE;F%W&mVi3 zF}pKxnPi+>mg|p?xf9Rby&?|l2>n^+K*&Zd3yPs3wVUq!=TDunIrEh{Iij#15Zx22 z6vsLKO6wQ1Y$KP>NRbuO-iO5P-M+(yY>C>Zp-+l-z|zY@Cr)^%}t zDmYXhugT7al*{3w&!AVd?D1-ycR2y*32zbpV~~3Q=k`%D=RAgO! z&z0GX>s(zdN{ig2!@+*6W21xj7VGVEH!cRGU{ENKw9?2>lyRVrp{VIT=cqAFEu^J; zb3`J5qDz6MC1P8u)QqcH-|mhp9KoF`>->Z0ryiKwdc%yxP7|-`pcnf&<5uKq$b! z$1+*Z8TQq{Y2ZjSY#@2=_pq3Ql_AQaQ5%pV+Rsp&1tScWT=-LrkQDlRQrphL4{*W} z6-pyKH8$4I!^|D)cQ7zEd~f}PAl5#=s;vU0jd^6a=9F07j<0T?mL3xWVDv>a;a zFgC2iaIzEUnm!uy*s}KIrbg2)O~`=GE=(p6AsW0~2EGr8)Q=sdvVn7-nbvg@l=mr# zZ_;y%QrO4KQ-0IWQ_Y#A%&g{m`k?g`18*(Pz-9-I_ z^P^Vqh~|dnCVY#R$3gC~vm*n6D19ZG1#9gNFIVU{9zRYuEjijw=*ugav}?Ubykis( zr?;+BbD01D0fqBmF_xOc!8IP-gz_lXy5kwEKxXU!?5;;HN9DsjPq3 z8qg}8%{;!R%ERRZnWrhhqk-+zCDkCMVj73)2H2f6#%LgK|=?giFTN;_*Bq zd($lB0TeoRkXy`BV(yv4w}xVE^v&4Q(~}{~K5zZNR?|lJ6~Vf7cjgX4PxQ&Vz^)3- zhGBR}(#_b|%4Pu}*1JxBI8ixJDEs6xcL1vk21;hXA()WcoXI%Su^)b~Z4()xOVQM$ zF(m5Reat9G5Csng-pp?&DD*q4yzzY>h!?RM5UI4U_MPfV

C9Y`24nJP5q8OyC#pWP~m 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 literal 0 HcmV?d00001 diff --git a/mail_tracking/static/src/css/failed_message.less b/mail_tracking/static/src/css/failed_message.less deleted file mode 100644 index 7457ccb..0000000 --- a/mail_tracking/static/src/css/failed_message.less +++ /dev/null @@ -1,108 +0,0 @@ -/* 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; -} diff --git a/mail_tracking/static/src/css/failed_message.scss b/mail_tracking/static/src/css/failed_message.scss new file mode 100644 index 0000000..e9216e0 --- /dev/null +++ b/mail_tracking/static/src/css/failed_message.scss @@ -0,0 +1,343 @@ +/* Copyright 2019 Alexandre Díaz + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */ +// FIXME: More of these classes are cloned from other scss files. +.o_mail_failed_message { + &.o_field_widget { + display: block; + } + + .o_thread_date_separator.o_border_dashed { + border-bottom-style: dashed; + + &[data-toggle="collapse"] { + cursor: pointer; + + .o_chatter_failed_message_summary { + display: none; + } + + &.collapsed { + margin-bottom: 0; + transition: margin 0.8s ease 0s; + + .o_chatter_failed_message_summary { + display: inline-block; + + span { + padding: 0 5px; + border-radius: 100%; + font-size: 11px; + } + } + + i.fa-caret-down:before { + content: '\f0da'; + } + } + } + } + + .o_thread_show_more { + text-align: center; + } + + .o_mail_thread_content { + display: flex; + flex-direction: column; + min-height: 100%; + } + + .o_thread_bottom_free_space { + height: 15px; + } + + .o_thread_typing_notification_free_space { + flex-grow: 1, + } + + .o_thread_typing_notification_bar { + flex: 0, 0, 20px; + background-color: rgba($white, 0.75); + padding: 5px; + text-align: center; + color: gray('600'); + + &.o_thread_order_asc { + @include o-position-sticky($bottom: 0px); + } + + &.o_thread_order_desc { + @include o-position-sticky($top: 0px); + } + } + + .o_thread_tooltip_container { + display: inline; + position: relative; + } + + .o_thread_date_separator { + margin-top: 15px; + margin-bottom: 30px; + @include media-breakpoint-down(sm) { + margin-top: 0px; + margin-bottom: 15px; + } + border-bottom: 1px solid gray('400'); + text-align: center; + + .o_thread_date { + position: relative; + top: 10px; + margin: 0 auto; + padding: 0 10px; + font-weight: bold; + background: white; + } + } + + .o_thread_new_messages_separator { + margin-bottom: 15px; + border-bottom: solid lighten($o-brand-odoo, 15%) 1px; + text-align: right; + .o_thread_separator_label { + position: relative; + top: 8px; + padding: 0 10px; + background: white; + color: lighten($o-brand-odoo, 15%); + font-size: smaller; + } + } + + .o_thread_message { + display: flex; + padding: 4px $o-horizontal-padding; + margin-bottom: 0px; + + &.o_mail_not_discussion { + background-color: rgba(gray('300'), 0.5); + border-bottom: 1px solid gray('400'); + } + + .o_thread_message_sidebar { + flex: 0 0 $o-mail-thread-avatar-size; + margin-right: 10px; + margin-top: 2px; + text-align: center; + font-size: smaller; + + @include media-breakpoint-down(sm) { + margin-top: 4px; + font-size: x-small; + } + + .o_thread_message_avatar { + max-width: $o-mail-thread-avatar-size; + } + .o_thread_message_side_date { + margin-left: -5px; + } + .o_thread_message_star { + margin-right: -5px; + } + + .o_thread_message_side_date { + opacity: 0; + } + } + .o_thread_icon { + cursor: pointer; + opacity: 0; + &.fa-star { + opacity: $o-mail-thread-icon-opacity; + color: gold; + } + } + + &:hover, &.o_thread_selected_message { + .o_thread_message_side_date { + opacity: $o-mail-thread-side-date-opacity; + } + .o_thread_icon { + opacity: $o-mail-thread-icon-opacity; + &:hover { + opacity: 1; + } + } + } + + .o_mail_redirect { + cursor: pointer; + } + + .o_thread_message_core { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; + word-wrap: break-word; + > pre { + white-space: pre-wrap; + word-break: break-word; + text-align: justify; + } + + + + .o_mail_subject { + font-style: italic; + } + + .o_mail_notification { + font-style: italic; + color: gray; + } + + [summary~=o_mail_notification] { // name conflicts with channel notifications, but is odoo notification buttons to hide in chatter if present + display: none; + } + + p { + margin: 0 0 9px; // Required by the old design to override a general rule on p's + &:last-child { + margin-bottom: 0; + } + } + a { + display: inline-block; + word-break: break-all; + } + :not(.o_image_box) > img { + max-width: 100%; + height: auto; + } + + .o_mail_body_long { + display: none; + } + + .o_mail_info { + margin-bottom: 2px; + + strong { + color: $headings-color; + } + } + + .o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply, .o_thread_message_email { + padding: 4px; + } + + i.o_thread_message_email { + &.o_thread_message_email_ready { + color: grey; + } + &.o_thread_message_email_exception, &.o_thread_message_email_bounce { + color: red; + opacity: 1; + cursor: pointer; + } + } + + .o_attachments_list, .o_attachments_previews { + &:last-child { + margin-bottom: $grid-gutter-width; + } + } + + .o_thread_tooltip_container { + display: inline; + position: relative; + } + } + } + .o_thread_title { + margin-top: 20px; + margin-bottom: 20px; + font-weight: bold; + font-size: 125%; + } + + .o_mail_no_content { + @include o-position-absolute(30%, 0, 0, 0); + text-align: center; + font-size: 115%; + } + + .o_thread_message .o_thread_message_core .o_mail_read_more { + display: block; + } + + #o_chatter_failed_message { + .o_thread_message { + .o_thread_message_sidebar { + .o_avatar_stack { + position: relative; + text-align: left; + margin-bottom: 8px; + + img { + width: 31px; + height: 31px; + } + + .o_avatar_icon { + @include o-position-absolute($right: -5px, $bottom: -5px); + width: 25px; + height: 25px; + padding: 6px 5px; + text-align: center; + line-height: 1.2; + color: white; + border-radius: 100%; + border: 2px solid white; + } + } + } + + .o_mail_info { + .o_activity_info { + vertical-align: baseline; + padding: 4px 6px; + background: theme-color('light'); + border-radius: 2px 2px 0 0; + @include o-hover-opacity(1, 1); + + &.collapsed { + @include o-hover-opacity(0.5, 1); + background: transparent; + } + } + } + + .o_thread_message_collapse .dl-horizontal.card { + display: inline-block; + margin-bottom: 0; + + dt { + max-width: 80px; + } + dd { + margin-left: 95px; + } + } + + .o_thread_message_note { + margin: 2px 0 5px; + padding: 0px; + } + .o_thread_message_warning { + margin: 2px 0 5px; + } + + .o_thread_message_tools { + .o_failed_message_link { + padding: 0 $input-btn-padding-x; + } + .o_failed_message_retry { + padding-left: 0; + } + } + } + } +} diff --git a/mail_tracking/static/src/css/mail_tracking.less b/mail_tracking/static/src/css/mail_tracking.scss similarity index 88% rename from mail_tracking/static/src/css/mail_tracking.less rename to mail_tracking/static/src/css/mail_tracking.scss index 4db2930..8f125cc 100644 --- a/mail_tracking/static/src/css/mail_tracking.less +++ b/mail_tracking/static/src/css/mail_tracking.scss @@ -4,10 +4,10 @@ .mail_tracking { span { - color: @odoo-color-0; + color: #909090; &.mail_tracking_opened { - color: @odoo-color-5; + color: #a34a8b; } } } diff --git a/mail_tracking/static/src/js/failed_message.js b/mail_tracking/static/src/js/failed_message.js deleted file mode 100644 index bdd9d74..0000000 --- a/mail_tracking/static/src/js/failed_message.js +++ /dev/null @@ -1,365 +0,0 @@ -/* 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; - -}); diff --git a/mail_tracking/static/src/js/failed_message/discuss.js b/mail_tracking/static/src/js/failed_message/discuss.js new file mode 100644 index 0000000..715cc30 --- /dev/null +++ b/mail_tracking/static/src/js/failed_message/discuss.js @@ -0,0 +1,397 @@ +/* Copyright 2019 Alexandre Díaz + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */ +odoo.define('mail_tracking.FailedMessageDiscuss', function (require) { + "use strict"; + + // To be considered: + // - One message can be displayed in many threads + // - A thread can be a mailbox, channel, ... + // - A mailbox is a type of thread that is displayed on top of + // the discuss menu, has a counter, etc... + + var MailManagerNotif = require('mail.Manager.Notification'); + var AbstractMessage = require('mail.model.AbstractMessage'); + var Message = require('mail.model.Message'); + var Discuss = require('mail.Discuss'); + var MailManager = require('mail.Manager'); + var Mailbox = require('mail.model.Mailbox'); + var core = require('web.core'); + var session = require('web.session'); + + var QWeb = core.qweb; + var _t = core._t; + + /* The states to consider a message as failed message */ + var FAILED_STATES = [ + 'error', 'rejected', 'spam', 'bounced', 'soft-bounced', + ]; + + + AbstractMessage.include({ + + /** + * Abstract declaration to know if a message is included in the + * failed mailbox. By default it should be false. + * + * @returns {Boolean} + */ + isFailed: function () { + return false; + }, + }); + + Message.include({ + + /** + * Overrides to store information from server + * + * @override + */ + init: function (parent, data) { + this._isFailedMessage = data.is_failed_message; + return this._super.apply(this, arguments); + }, + + /** + * Implementation to know if a message is included in the + * failed mailbox. + * + * @override + */ + isFailed: function () { + return _.contains(this._threadIDs, 'mailbox_failed'); + }, + + /** + * Adds/Removes message to/from failed mailbox + * + * @param {Boolean} failed + */ + setFailed: function (failed) { + if (failed) { + this._addThread('mailbox_failed'); + } else { + this.removeThread('mailbox_failed'); + } + }, + + /** + * Include the message in the 'failed' mailbox if needed + * + * @override + */ + _processMailboxes: function () { + this.setFailed(this._isFailedMessage); + return this._super.apply(this, arguments); + }, + }); + + MailManagerNotif.include({ + + /** + * Overrides to handle changes in the 'mail_tracking_needs_action' flag + * + * @override + */ + _handlePartnerNotification: function (data) { + if (data.type === 'toggle_tracking_status') { + this._handlePartnerToggleFailedNotification(data); + } else { + // Workaround to avoid call '_handlePartnerChannelNotification' + // because this is related with the failed mailbox, not a + // channel. + this._super.apply(this, arguments); + } + }, + + /** + * This method updates messages in the failed mailbox when the flag + * 'mail_tracking_needs_action' is toggled. This can remove/add + * the message from/to failed mailbox and update mailbox counter. + * + * @private + * @param {Object} data + */ + _handlePartnerToggleFailedNotification: function (data) { + var self = this; + var failed = this.getMailbox('failed'); + _.each(data.message_ids, function (messageID) { + var message = _.find(self._messages, function (msg) { + return msg.getID() === messageID; + }); + if (message) { + message.setFailed(data.needs_actions); + if (message.isFailed() === false) { + self._removeMessageFromThread( + 'mailbox_failed', message); + } else { + self._addMessageToThreads(message, []); + var channelFailed = self.getMailbox('failed'); + channelFailed.invalidateCaches(); + } + self._mailBus.trigger('update_message', message, data.type); + } + }); + + if (data.needs_actions) { + // Increase failed counter if message is marked as failed + failed.incrementMailboxCounter(data.message_ids.length); + } else { + // Decrease failed counter if message is removed from failed + failed.decrementMailboxCounter(data.message_ids.length); + } + + // Trigger event to refresh threads + this._mailBus.trigger('update_failed', failed.getMailboxCounter()); + }, + }); + + Discuss.include({ + events: _.extend({}, Discuss.prototype.events, { + 'click .o_failed_message_retry': '_onRetryFailedMessage', + 'click .o_failed_message_reviewed': '_onMarkFailedMessageReviewed', + }), + + /** + * Paramaters used to render 'failed' mailbox entry in Discuss + * + * @private + * @returns {Object} + */ + _sidebarQWebParams: function () { + var failed = this.call('mail_service', 'getMailbox', 'failed'); + return { + activeThreadID: this._thread ? this._thread.getID() : undefined, + failedCounter: failed.getMailboxCounter(), + }; + }, + + /** + * Render 'failed' mailbox menu entry in Discuss + * + * @private + * @returns {jQueryElementt} + */ + _renderSidebar: function () { + var $sidebar = this._super.apply(this, arguments); + // Because Odoo implementation isn't designed to be inherited + // properly, we inject 'failed' button using jQuery. + var $failed_item = $(QWeb.render('mail_tracking.SidebarFailed', + this._sidebarQWebParams())); + $failed_item.insertAfter( + $sidebar.find(".o_mail_discuss_title_main").filter(":last")); + return $sidebar; + }, + + /** + * Overrides to listen click on 'Set all as reviewed' button + * + * @override + */ + _renderButtons: function () { + this._super.apply(this, arguments); + this.$btn_set_all_reviewed = this.$buttons.find( + '.o_mail_discuss_button_set_all_reviewed'); + this.$btn_set_all_reviewed + .on('click', $.proxy(this, "_onSetAllAsReviewedClicked")); + }, + + /** + * Show or hide 'set all as reviewed' button in discuss mailbox + * + * This means in which thread the button should be displayed. + * + * @override + */ + _updateControlPanelButtons: function (thread) { + this.$btn_set_all_reviewed + .toggleClass( + 'd-none d-md-none', + thread.getID() !== 'mailbox_failed'); + + return this._super.apply(this, arguments); + }, + + /** + * Overrides to update 'set all as reviewed' button. + * + * Disabled button if doesn't have more failed messages + * + * @override + */ + _updateButtonStatus: function (disabled, type) { + if (this._thread.getID() === 'mailbox_failed') { + this.$btn_set_all_reviewed + .toggleClass('disabled', disabled); + // Display Rainbowman when all failed messages are reviewed + // through 'TOGGLE TRACKING STATUS' or marking last failed + // message as reviewed + if (disabled && type === 'toggle_tracking_status') { + this.trigger_up('show_effect', { + message: _t( + "Congratulations, your failed mailbox is empty"), + type: 'rainbow_man', + }); + } + } + }, + + /** + * Overrides to update messages in 'failed' mailbox thread + * + * @override + */ + _onMessageUpdated: function (message, type) { + var self = this; + var currentThreadID = this._thread.getID(); + if (currentThreadID === 'mailbox_failed' && !message.isFailed()) { + this._thread.fetchMessages(this.domain) + .then(function () { + var options = self._getThreadRenderingOptions(); + self._threadWidget.removeMessageAndRender( + message.getID(), self._thread, options) + .then(function () { + self._updateButtonStatus( + !self._thread.hasMessages(), type); + }); + }); + } else { + // Workaround to avoid calling '_fetchAndRenderThread' and + // refetching thread messages because these messages are + // actually fetched above. + this._super.apply(this, arguments); + } + }, + + /** + * Hide reply feature in the 'failed' mailbox, where it has no sense. + * Show instead 'Retry' and 'Set as reviewed' buttons. + * + * @override + */ + _getThreadRenderingOptions: function () { + var values = this._super.apply(this, arguments); + if (this._thread.getID() === 'mailbox_failed') { + values.displayEmailIcons = true; + values.displayReplyIcons = false; + values.displayRetryButton = true; + values.displayReviewedButton = true; + } + return values; + }, + + /** + * Listen also to the event that refreshes thread messages + * + * @override + */ + _startListening: function () { + this._super.apply(this, arguments); + this.call('mail_service', 'getMailBus') + .on('update_failed', this, this._throttledUpdateThreads); + }, + + // Handlers + /** + * Open the resend mail.resend.message wizard + * + * @private + * @param {Event} event + */ + _onRetryFailedMessage: function (event) { + event.preventDefault(); + var messageID = $(event.currentTarget).data('message-id'); + this.do_action('mail.mail_resend_message_action', { + additional_context: { + mail_message_to_resend: messageID, + }, + }); + }, + + /** + * Toggle 'mail_tracking_needs_action' flag + * + * @private + * @param {Event} event + * @returns {Promise} + */ + _onMarkFailedMessageReviewed: function (event) { + event.preventDefault(); + var messageID = $(event.currentTarget).data('message-id'); + return this._rpc({ + model: 'mail.message', + method: 'toggle_tracking_status', + args: [[messageID]], + context: this.getSession().user_context, + }); + }, + + /** + * Inheritable method that call thread implementation + * + * @private + */ + _onSetAllAsReviewedClicked: function () { + this._thread.setAllMessagesAsReviewed(); + }, + }); + + MailManager.include({ + + /** + * Add the 'failed' mailbox + * + * @override + */ + _updateMailboxesFromServer: function (data) { + this._super.apply(this, arguments); + this._addMailbox({ + id: 'failed', + name: _t("Failed"), + mailboxCounter: data.failed_counter || 0, + }); + }, + }); + + Mailbox.include({ + + /** + * Overrides to add domain for 'failed' mailbox thread + * + * @override + */ + _getThreadDomain: function () { + if (this._id === 'mailbox_failed') { + return [ + ['mail_tracking_ids.state', 'in', FAILED_STATES], + ['mail_tracking_needs_action', '=', true], + '|', + ['partner_ids', 'in', [session.partner_id]], + ['author_id', '=', session.partner_id], + ]; + } + // Workaround to avoid throw 'Missing domain' exception. Call _super + // without a valid (hard-coded) thread id causes that exeception. + return this._super.apply(this, arguments); + }, + + /** + * Sets all messages from the mailbox as reviewed. + * + * At the moment, this method makes only sense for 'Failed'. + * + * @returns {$.Promise} resolved when all messages have been marked as + * reviewed on the server + */ + setAllMessagesAsReviewed: function () { + if (this._id === 'mailbox_failed' && this.getMailboxCounter() > 0) { + return this._rpc({ + model: 'mail.message', + method: 'set_all_as_reviewed', + }); + } + return $.when(); + }, + }); + +}); diff --git a/mail_tracking/static/src/js/failed_message/thread.js b/mail_tracking/static/src/js/failed_message/thread.js new file mode 100644 index 0000000..78e9ec6 --- /dev/null +++ b/mail_tracking/static/src/js/failed_message/thread.js @@ -0,0 +1,316 @@ +/* Copyright 2019 Alexandre Díaz + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */ +odoo.define('mail_tracking.FailedMessageThread', function (require) { + "use strict"; + + var AbstractField = require('web.AbstractField'); + var BasicModel = require('web.BasicModel'); + var BasicView = require('web.BasicView'); + var Chatter = require('mail.Chatter'); + var MailThread = require('mail.widget.Thread'); + var utils = require('mail.utils'); + var core = require('web.core'); + var field_registry = require('web.field_registry'); + var time = require('web.time'); + + var QWeb = core.qweb; + + + /** + * Helper method to fetch failed messages + * + * @private + * @param {Object} widget + * @param {Array} ids + * @returns {Array} + */ + function _readMessages (widget, ids) { + if (!ids.length) { + return $.when(); + } + var context = widget.record && widget.record.getContext(); + return widget._rpc({ + model: 'mail.message', + method: 'get_failed_messages', + args: [ids], + context: context || widget.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 = utils.timeFromNow(msg.date); + }); + return messages; + }); + } + + BasicModel.include({ + + /** + * Fetch data for the 'mail_failed_message' field widget in form views. + * + * @private + * @param {Object} record + * @param {String} fieldName + * @returns {Array} + */ + _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 FailedMessage = AbstractField.extend({ + className: 'o_mail_failed_message', + events: { + 'click .o_failed_message_retry': '_onRetryFailedMessage', + 'click .o_failed_message_reviewed': '_onMarkFailedMessageReviewed', + }, + specialData: '_fetchSpecialFailedMessages', + + /** + * Overrides to reference failed messages in a easy way + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.failed_messages = this.record.specialData[this.name] || []; + }, + + /** + * Overrides to listen bus notifications + * + * @override + */ + start: function () { + this._super.apply(this, arguments); + this.call( + 'bus_service', 'onNotification', this, this._onNotification); + }, + + /** + * Paremeters used to render widget + * + * @private + * @returns {Object} + */ + _failedItemsQWebParams: function () { + return { + failed_messages: this.failed_messages, + nbFailedMessages: this.failed_messages.length, + date_format: time.getLangDateFormat(), + datetime_format: time.getLangDatetimeFormat(), + }; + }, + + /** + * @private + */ + _render: function () { + if (this.failed_messages.length) { + this.$el.html(QWeb.render( + 'mail_tracking.failed_message_items', + this._failedItemsQWebParams())); + } else { + this.$el.empty(); + } + }, + + /** + * Reset widget data using selected record + * + * @private + * @param {Object} record + */ + _reset: function (record) { + this._super.apply(this, arguments); + this.failed_messages = this.record.specialData[this.name] || []; + this.res_id = record.res_id; + }, + + /** + * Trigger event to reload mail widgets + * + * @private + * @param {Array} fieldsToReload + */ + _reload: function (fieldsToReload) { + this.trigger_up('reload_mail_fields', fieldsToReload); + }, + + /** + * Mark failed message as reviewed + * + * @private + * @param {Int} id + * @returns {Promise} + */ + _markFailedMessageReviewed: function (id) { + return this._rpc({ + model: 'mail.message', + method: 'toggle_tracking_status', + args: [[id]], + context: this.record.getContext(), + }); + }, + + // Handlers + /** + * Listen bus notification to launch reload process. + * This bus notification is received when the user uses + * 'mail.resend.message' wizard. + * + * @private + * @param {Array} notifs + */ + _onNotification: function (notifs) { + var self = this; + _.each(notifs, function (notif) { + var model = notif[0][1]; + if (model === 'res.partner') { + var data = notif[1]; + if (data.type === 'update_failed_messages') { + // Reload 'mail_failed_message' widget + self._reload({failed_message: true}); + } + } + }); + }, + + /** + * Handle retry failed message event to open the mail.resend.message + * wizard. + * + * @private + * @param {Event} event + */ + _onRetryFailedMessage: function (event) { + event.preventDefault(); + var messageID = $(event.currentTarget).data('message-id'); + this.do_action('mail.mail_resend_message_action', { + additional_context: { + mail_message_to_resend: messageID, + }, + }); + }, + + /** + * Handle mark message as reviewed event + * + * @private + * @param {Event} event + */ + _onMarkFailedMessageReviewed: function (event) { + event.preventDefault(); + var messageID = $(event.currentTarget).data('message-id'); + this._markFailedMessageReviewed(messageID).then( + $.proxy(this, "_reload", {failed_message: true})); + }, + }); + + field_registry.add('mail_failed_message', FailedMessage); + + var mailWidgets = ['mail_failed_message']; + BasicView.include({ + + /** + * Overrides to add 'mail_failed_message' widget as "mail widget" used + * in Chatter. + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + var fieldsInfo = this.fieldsInfo[this.viewType]; + for (var fieldName in fieldsInfo) { + var fieldInfo = fieldsInfo[fieldName]; + // Search fields using 'mail_failed_messsage' widget. + // Only one field can exists using the widget, the last + // found wins. + if (_.contains(mailWidgets, fieldInfo.widget)) { + // Add field as "mail field" shared with Chatter + this.mailFields[fieldInfo.widget] = fieldName; + // Avoid fetch x2many data, this will be done by widget + fieldInfo.__no_fetch = true; + } + } + // Update renderParmans mailFields to include the found field + // using 'mail_failed_messsage' widget. This info is used by the + // renderers [In Odoo vanilla by the form renderer to initialize + // Chatter widget]. + _.extend(this.rendererParams.mailFields, this.mailFields); + }, + }); + + Chatter.include({ + + /** + * Overrides to initialize 'mail_failed_message' widget. + * + * @override + */ + 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); + } + }, + + /** + * Injects failed messages widget before the chatter + * + * @private + * @returns {Promise} + */ + _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')); + } + }); + }, + + /** + * Overrides to reload 'mail_failed_message' widget + * + * @override + */ + _onReloadMailFields: function (event) { + if (this.fields.failed_message && event.data.failed_message) { + this.trigger_up('reload', { + fieldNames: [this.fields.failed_message.name], + keepChanges: true, + }); + } else { + // Workaround to avoid trigger reload event twice (once for + // mail_failed_message and again with empty 'fieldNames'. + this._super.apply(this, arguments); + } + }, + }); + + MailThread.include({ + + /** + * Show 'retry' & 'Set as reviewed' buttons in the Chatter + * + * @override + */ + init: function () { + this._super.apply(this, arguments); + this._enabledOptions.displayRetryButton = true; + this._enabledOptions.displayReviewedButton = true; + this._disabledOptions.displayRetryButton = false; + this._disabledOptions.displayReviewedButton = false; + }, + }); + + return FailedMessage; + +}); diff --git a/mail_tracking/static/src/js/mail_tracking.js b/mail_tracking/static/src/js/mail_tracking.js index 3c98179..34148a7 100644 --- a/mail_tracking/static/src/js/mail_tracking.js +++ b/mail_tracking/static/src/js/mail_tracking.js @@ -2,7 +2,7 @@ Copyright 2018 David Vidal - License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). */ -odoo.define('mail_tracking.partner_tracking', function(require){ +odoo.define('mail_tracking.partner_tracking', function (require) { "use strict"; var core = require('web.core'); @@ -14,10 +14,11 @@ odoo.define('mail_tracking.partner_tracking', function(require){ var _t = core._t; AbstractMessage.include({ + /** * Messages do not have any PartnerTrackings. * - * @return {boolean} + * @returns {Boolean} */ hasPartnerTrackings: function () { return false; @@ -26,7 +27,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ /** * Messages do not have any email Cc values. * - * @return {boolean} + * @returns {Boolean} */ hasEmailCc: function () { return false; @@ -34,7 +35,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ }); Message.include({ - init: function (parent, data, emojis) { + init: function (parent, data) { this._super.apply(this, arguments); this._partnerTrackings = data.partner_trackings || []; this._emailCc = data.email_cc || []; @@ -45,7 +46,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ * State whether this message contains some PartnerTrackings values * * @override - * @return {boolean} + * @returns {Boolean} */ hasPartnerTrackings: function () { return _.some(this._partnerTrackings); @@ -54,7 +55,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ /** * State whether this message contains some email Cc values * - * @return {boolean} + * @returns {Boolean} */ hasEmailCc: function () { return _.some(this._emailCc); @@ -65,7 +66,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ * If this message has no PartnerTrackings values, returns [] * * @override - * @return {Object[]} + * @returns {Object[]} */ getPartnerTrackings: function () { if (!this.hasPartnerTrackings()) { @@ -78,7 +79,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ * Get the email Cc values of this message * If this message has no email Cc values, returns [] * - * @return {Array} + * @returns {Array} */ getEmailCc: function () { if (!this.hasEmailCc()) { @@ -91,7 +92,8 @@ odoo.define('mail_tracking.partner_tracking', function(require){ * Check if the email is an Cc * If this message has no email Cc values, returns false * - * @return {Boolean} + * @param {String} email + * @returns {Boolean} */ isEmailCc: function (email) { if (!this.hasEmailCc()) { @@ -104,31 +106,19 @@ odoo.define('mail_tracking.partner_tracking', function(require){ toggleTrackingStatus: function () { return this._rpc({ - model: 'mail.message', - method: 'toggle_tracking_status', - args: [[this.id]], - }); + model: 'mail.message', + method: 'toggle_tracking_status', + args: [[this.id]], + }); }, }); ThreadWidget.include({ events: _.extend(ThreadWidget.prototype.events, { - 'click .o_mail_action_tracking_partner': 'on_tracking_partner_click', + 'click .o_mail_action_tracking_partner': + 'on_tracking_partner_click', 'click .o_mail_action_tracking_status': 'on_tracking_status_click', }), - _preprocess_message: function () { - var msg = this._super.apply(this, arguments); - msg.partner_trackings = msg.partner_trackings || []; - var needs_action = msg.track_needs_action; - var message_track = _.findWhere(messages_tracked_changes, { - id: msg.id, - }); - if (message_track) { - needs_action = message_track.status; - } - msg.track_needs_action = needs_action; - return msg; - }, on_tracking_partner_click: function (event) { var partner_id = this.$el.find(event.currentTarget).data('partner'); var state = { @@ -171,7 +161,7 @@ odoo.define('mail_tracking.partner_tracking', function(require){ }, init: function () { this._super.apply(this, arguments); - this.action_manager = this.findAncestor(function(ancestor){ + this.action_manager = this.findAncestor(function (ancestor) { return ancestor instanceof ActionManager; }); }, diff --git a/mail_tracking/static/src/xml/client_action.xml b/mail_tracking/static/src/xml/client_action.xml deleted file mode 100644 index 28c3d35..0000000 --- a/mail_tracking/static/src/xml/client_action.xml +++ /dev/null @@ -1,31 +0,0 @@ - - diff --git a/mail_tracking/static/src/xml/failed_message.xml b/mail_tracking/static/src/xml/failed_message.xml deleted file mode 100644 index 367cb40..0000000 --- a/mail_tracking/static/src/xml/failed_message.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - -

- - - - -
- - - -
-
- - diff --git a/mail_tracking/static/src/xml/failed_message/common.xml b/mail_tracking/static/src/xml/failed_message/common.xml new file mode 100644 index 0000000..0961617 --- /dev/null +++ b/mail_tracking/static/src/xml/failed_message/common.xml @@ -0,0 +1,15 @@ + + + + + + + Set as Reviewed + + + Retry + + + + + diff --git a/mail_tracking/static/src/xml/failed_message/discuss.xml b/mail_tracking/static/src/xml/failed_message/discuss.xml new file mode 100644 index 0000000..45fb1f5 --- /dev/null +++ b/mail_tracking/static/src/xml/failed_message/discuss.xml @@ -0,0 +1,34 @@ + + + + + + + + + + +
+ Failed + + +
+
+ + + + +
Congratulations, you don't have any failed messages
+
Failed messages appear here.
+
+
+
+ + + + + + + +
diff --git a/mail_tracking/static/src/xml/failed_message/thread.xml b/mail_tracking/static/src/xml/failed_message/thread.xml new file mode 100644 index 0000000..85e008c --- /dev/null +++ b/mail_tracking/static/src/xml/failed_message/thread.xml @@ -0,0 +1,60 @@ + + + + + +
+ +
+
+
+ + +
+
+
+
+ + + + - + + + Set as Reviewed + + + Retry + + +
+ Failed Recipients: + + + - + + + + + +
+
+ +
+
+
+
+
+
+ +
diff --git a/mail_tracking/static/src/xml/mail_tracking.xml b/mail_tracking/static/src/xml/mail_tracking.xml index 3553d9f..5144d16 100644 --- a/mail_tracking/static/src/xml/mail_tracking.xml +++ b/mail_tracking/static/src/xml/mail_tracking.xml @@ -22,8 +22,9 @@ - - + + + @@ -51,7 +52,7 @@

To: - + - @@ -65,9 +66,11 @@ + + + t-att-title="title_status"> diff --git a/mail_tracking/tests/test_mail_tracking.py b/mail_tracking/tests/test_mail_tracking.py index 1659d6e..f9c6642 100644 --- a/mail_tracking/tests/test_mail_tracking.py +++ b/mail_tracking/tests/test_mail_tracking.py @@ -5,6 +5,8 @@ import mock from odoo.tools import mute_logger import time import base64 +import psycopg2 +import psycopg2.errorcodes from odoo import http from odoo.tests.common import TransactionCase from ..controllers.main import MailTrackingController, BLANK @@ -36,8 +38,9 @@ class TestMailTracking(TransactionCase): }) self.last_request = http.request http.request = type('obj', (object,), { - 'db': self.env.cr.dbname, 'env': self.env, + 'cr': self.env.cr, + 'db': self.env.cr.dbname, 'endpoint': type('obj', (object,), { 'routing': [], }), @@ -116,6 +119,30 @@ class TestMailTracking(TransactionCase): tracking_email.event_create('open', metadata) self.assertEqual(tracking_email.state, 'opened') + def test_message_post_partner_no_email(self): + # Create message with recipient without defined email + self.recipient.write({'email': False}) + message = self.env['mail.message'].create({ + 'subject': 'Message test', + 'author_id': self.sender.id, + 'email_from': self.sender.email, + 'message_type': 'comment', + 'model': 'res.partner', + 'res_id': self.recipient.id, + 'partner_ids': [(4, self.recipient.id)], + 'body': '

This is a test message

', + }) + message._notify(message, {}, force_send=True) + # Search tracking created + tracking_email = self.env['mail.tracking.email'].search([ + ('mail_message_id', '=', message.id), + ('partner_id', '=', self.recipient.id), + ]) + # No email should generate a error state: no_recipient + self.assertEqual(tracking_email.state, 'error') + self.assertEqual(tracking_email.error_type, 'no_recipient') + self.assertFalse(self.recipient.email_bounced) + def _check_partner_trackings(self, message): message_dict = message.message_format()[0] self.assertEqual(len(message_dict['partner_trackings']), 3) @@ -136,18 +163,15 @@ class TestMailTracking(TransactionCase): self.assertTrue(foundNoPartner) def test_email_cc(self): - message = self.env['mail.message'].create({ - 'subject': 'Message test', - 'author_id': self.sender.id, - 'email_from': self.sender.email, - 'message_type': 'comment', - 'model': 'res.partner', - 'res_id': self.recipient.id, - 'partner_ids': [(4, self.recipient.id)], - 'email_cc': 'unnamed@test.com, sender@example.com', - 'body': '

This is a test message

', + sender_user = self.env['res.users'].create({ + 'name': 'Sender User Test', + 'partner_id': self.sender.id, + 'login': 'sender-test', }) - self._check_partner_trackings(message) + message = self.recipient.sudo(user=sender_user).message_post( + body='

This is a test message

', + cc='unnamed@test.com, sender@example.com' + ) # suggested recipients recipients = self.recipient.message_get_suggested_recipients() suggested_mails = { @@ -168,31 +192,37 @@ class TestMailTracking(TransactionCase): ', recipient@example.com', 'body': '

This is another test message

', }) + message._notify(message, {}, force_send=True) recipients = self.recipient.message_get_suggested_recipients() self.assertEqual(len(recipients[self.recipient.id][0]), 3) self._check_partner_trackings(message) def test_failed_message(self): + MailMessageObj = self.env['mail.message'] # 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() + failed_count = MailMessageObj.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([]) + messages = MailMessageObj.search([]) + messages_failed = MailMessageObj.search( + MailMessageObj._get_failed_message_domain()) 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) + MailMessageObj.get_failed_count() < failed_count) + # No author_id + tracking.mail_message_id.author_id = False + values = tracking.mail_message_id.get_failed_messages()[0] + self.assertEqual(values['author'][0], -1) def mail_send(self, recipient): mail = self.env['mail.mail'].create({ @@ -215,15 +245,52 @@ class TestMailTracking(TransactionCase): mail, tracking = self.mail_send(self.recipient.email) self.assertEqual(mail.email_to, tracking.recipient) self.assertEqual(mail.email_from, tracking.sender) - res = controller.mail_tracking_open(db, tracking.id) - self.assertEqual(image, res.response[0]) - # Two events: sent and open - self.assertEqual(2, len(tracking.tracking_event_ids)) - # Fake event: tracking_email_id = False - res = controller.mail_tracking_open(db, False) - self.assertEqual(image, res.response[0]) - # Two events again because no tracking_email_id found for False - self.assertEqual(2, len(tracking.tracking_event_ids)) + with mock.patch('odoo.http.db_filter') as mock_client: + mock_client.return_value = True + res = controller.mail_tracking_open( + db, tracking.id, tracking.token) + self.assertEqual(image, res.response[0]) + # Two events: sent and open + self.assertEqual(2, len(tracking.tracking_event_ids)) + # Fake event: tracking_email_id = False + res = controller.mail_tracking_open(db, False, False) + self.assertEqual(image, res.response[0]) + # Two events again because no tracking_email_id found for False + self.assertEqual(2, len(tracking.tracking_event_ids)) + + def test_mail_tracking_open(self): + controller = MailTrackingController() + db = self.env.cr.dbname + with mock.patch('odoo.http.db_filter') as mock_client: + mock_client.return_value = True + mail, tracking = self.mail_send(self.recipient.email) + # Tracking is in sent or delivered state. But no token give. + # Don't generates tracking event + controller.mail_tracking_open(db, tracking.id) + self.assertEqual(1, len(tracking.tracking_event_ids)) + tracking.write({'state': 'opened'}) + # Tracking isn't in sent or delivered state. + # Don't generates tracking event + controller.mail_tracking_open(db, tracking.id, tracking.token) + self.assertEqual(1, len(tracking.tracking_event_ids)) + tracking.write({'state': 'sent'}) + # Tracking is in sent or delivered state and a token is given. + # Generates tracking event + controller.mail_tracking_open(db, tracking.id, tracking.token) + self.assertEqual(2, len(tracking.tracking_event_ids)) + # Generate new email due concurrent event filter + mail, tracking = self.mail_send(self.recipient.email) + tracking.write({'token': False}) + # Tracking is in sent or delivered state but a token is given for a + # record that doesn't have a token. + # Don't generates tracking event + controller.mail_tracking_open(db, tracking.id, 'tokentest') + self.assertEqual(1, len(tracking.tracking_event_ids)) + # Tracking is in sent or delivered state and not token is given for + # a record that doesn't have a token. + # Generates tracking event + controller.mail_tracking_open(db, tracking.id, False) + self.assertEqual(2, len(tracking.tracking_event_ids)) def test_concurrent_open(self): mail, tracking = self.mail_send(self.recipient.email) @@ -400,12 +467,14 @@ class TestMailTracking(TransactionCase): def test_db(self): db = self.env.cr.dbname controller = MailTrackingController() - not_found = controller.mail_tracking_all('not_found_db') - self.assertEqual(b'NOT FOUND', not_found.response[0]) - none = controller.mail_tracking_all(db) - self.assertEqual(b'NONE', none.response[0]) - none = controller.mail_tracking_event(db, 'open') - self.assertEqual(b'NONE', none.response[0]) + with mock.patch('odoo.http.db_filter') as mock_client: + mock_client.return_value = True + with self.assertRaises(psycopg2.OperationalError): + controller.mail_tracking_event('not_found_db') + none = controller.mail_tracking_event(db) + self.assertEqual(b'NONE', none.response[0]) + none = controller.mail_tracking_event(db, 'open') + self.assertEqual(b'NONE', none.response[0]) class TestMailTrackingViews(TransactionCase): diff --git a/mail_tracking/views/assets.xml b/mail_tracking/views/assets.xml index 72c66cd..5853ae1 100644 --- a/mail_tracking/views/assets.xml +++ b/mail_tracking/views/assets.xml @@ -7,13 +7,15 @@ inherit_id="web.assets_backend"> + href="/mail_tracking/static/src/css/mail_tracking.scss"/> + href="/mail_tracking/static/src/css/failed_message.scss"/>