diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py
index f92a0003e00..f835071aa44 100644
--- a/addons/mass_mailing/controllers/main.py
+++ b/addons/mass_mailing/controllers/main.py
@@ -98,7 +98,7 @@ class MassMailController(http.Controller):
if not consteq(token, tools.hmac(request.env(su=True), 'mass_mailing-mail_mail-open', mail_id)):
raise BadRequest()
- request.env['mailing.trace'].sudo().set_opened(mail_mail_ids=[mail_id])
+ request.env['mailing.trace'].sudo().set_opened(domain=[('mail_mail_id_int', 'in', [mail_id])])
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')
diff --git a/addons/mass_mailing/data/mass_mailing_demo.xml b/addons/mass_mailing/data/mass_mailing_demo.xml
index 15e9cc2b790..67157c1b7dd 100644
--- a/addons/mass_mailing/data/mass_mailing_demo.xml
+++ b/addons/mass_mailing/data/mass_mailing_demo.xml
@@ -184,9 +184,11 @@
res.partner
billy.fox45@example.com
-
-
-
+ reply
+
+
+
+
@@ -194,9 +196,11 @@
res.partner
kim.snyder96@example.com
-
-
-
+ reply
+
+
+
+
@@ -204,8 +208,10 @@
res.partner
edith.sanchez68@example.com
-
-
+ open
+
+
+
@@ -213,8 +219,10 @@
res.partner
theodore.gardner36@example.com
-
-
+ open
+
+
+
@@ -222,7 +230,9 @@
res.partner
sandra.neal80@example.com
-
+ sent
+
+
@@ -230,8 +240,8 @@
res.partner
julie.richards84@example.com
-
-
+ error
+
@@ -239,8 +249,9 @@
res.partner
travis.mendoza24@example.com
-
-
+ bounce
+
+
@@ -248,8 +259,8 @@
res.partner
travis.mendoza24@example.com
-
-
+ bounce
+
diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py
index bc619a4067a..f9446867c70 100644
--- a/addons/mass_mailing/models/mail_mail.py
+++ b/addons/mass_mailing/models/mail_mail.py
@@ -81,7 +81,7 @@ class MailMail(models.Model):
for mail in self:
if mail.mailing_id:
if mail_sent is True and mail.mailing_trace_ids:
- mail.mailing_trace_ids.write({'sent': fields.Datetime.now(), 'exception': False})
+ mail.mailing_trace_ids.set_sent()
elif mail_sent is False and mail.mailing_trace_ids:
- mail.mailing_trace_ids.write({'exception': fields.Datetime.now(), 'failure_type': failure_type})
+ mail.mailing_trace_ids.set_failed(failure_type=failure_type)
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py
index 1f442675922..08fb3b67e86 100644
--- a/addons/mass_mailing/models/mail_thread.py
+++ b/addons/mass_mailing/models/mail_thread.py
@@ -23,8 +23,8 @@ class MailThread(models.AbstractModel):
thread_references = message_dict['references'] or message_dict['in_reply_to']
msg_references = tools.mail_header_msgid_re.findall(thread_references)
if msg_references:
- self.env['mailing.trace'].set_opened(mail_message_ids=msg_references)
- self.env['mailing.trace'].set_replied(mail_message_ids=msg_references)
+ self.env['mailing.trace'].set_opened(domain=[('message_id', 'in', msg_references)])
+ self.env['mailing.trace'].set_replied(domain=[('message_id', 'in', msg_references)])
return super(MailThread, self)._message_route_process(message, message_dict, routes)
def message_post_with_template(self, template_id, **kwargs):
@@ -53,10 +53,10 @@ class MailThread(models.AbstractModel):
bounced_partner = message_dict['bounced_partner']
if bounced_msg_id:
- self.env['mailing.trace'].set_bounced(mail_message_ids=bounced_msg_id)
+ self.env['mailing.trace'].set_bounced(domain=[('message_id', 'in', bounced_msg_id)])
if bounced_email:
three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13))
- stats = self.env['mailing.trace'].search(['&', ('bounced', '>', three_months_ago), ('email', '=ilike', bounced_email)]).mapped('bounced')
+ stats = self.env['mailing.trace'].search(['&', '&', ('trace_status', '=', 'bounce'), ('write_date', '>', three_months_ago), ('email', '=ilike', bounced_email)]).mapped('write_date')
if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT and (not bounced_partner or any(p.message_bounce >= BLACKLIST_MAX_BOUNCED_LIMIT for p in bounced_partner)):
if max(stats) > min(stats) + datetime.timedelta(weeks=1):
blacklist_rec = self.env['mail.blacklist'].sudo()._add(bounced_email)
diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py
index 66cfef1c12f..e956434495c 100644
--- a/addons/mass_mailing/models/mailing.py
+++ b/addons/mass_mailing/models/mailing.py
@@ -143,7 +143,7 @@ class MassMailing(models.Model):
total = fields.Integer(compute="_compute_total")
scheduled = fields.Integer(compute="_compute_statistics")
expected = fields.Integer(compute="_compute_statistics")
- ignored = fields.Integer(compute="_compute_statistics")
+ canceled = fields.Integer(compute="_compute_statistics")
sent = fields.Integer(compute="_compute_statistics")
delivered = fields.Integer(compute="_compute_statistics")
opened = fields.Integer(compute="_compute_statistics")
@@ -203,7 +203,7 @@ class MassMailing(models.Model):
def _compute_statistics(self):
""" Compute statistics of the mass mailing """
for key in (
- 'scheduled', 'expected', 'ignored', 'sent', 'delivered', 'opened',
+ 'scheduled', 'expected', 'canceled', 'sent', 'delivered', 'opened',
'clicked', 'replied', 'bounced', 'failed', 'received_ratio',
'opened_ratio', 'replied_ratio', 'bounced_ratio',
):
@@ -216,15 +216,15 @@ class MassMailing(models.Model):
SELECT
m.id as mailing_id,
COUNT(s.id) AS expected,
- COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
- COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null AND s.bounced is null THEN 1 ELSE null END) AS scheduled,
- COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
- COUNT(CASE WHEN s.sent is not null AND s.exception is null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
- COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
- COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
- COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
- COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
- COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
+ COUNT(s.sent_datetime) AS sent,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'outgoing') AS scheduled,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'cancel') AS canceled,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('sent', 'open', 'reply')) AS delivered,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('open', 'reply')) AS opened,
+ COUNT(s.links_click_datetime) AS clicked,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'reply') AS replied,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'bounce') AS bounced,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'error') AS failed
FROM
mailing_trace s
RIGHT JOIN
@@ -236,7 +236,7 @@ class MassMailing(models.Model):
m.id
""", (tuple(self.ids), ))
for row in self.env.cr.dictfetchall():
- total = (row['expected'] - row['ignored']) or 1
+ total = (row['expected'] - row['canceled']) or 1
row['received_ratio'] = 100.0 * row['delivered'] / total
row['opened_ratio'] = 100.0 * row['opened'] / total
row['replied_ratio'] = 100.0 * row['replied'] / total
@@ -431,8 +431,8 @@ class MassMailing(models.Model):
def action_view_traces_scheduled(self):
return self._action_view_traces_filtered('scheduled')
- def action_view_traces_ignored(self):
- return self._action_view_traces_filtered('ignored')
+ def action_view_traces_canceled(self):
+ return self._action_view_traces_filtered('canceled')
def action_view_traces_failed(self):
return self._action_view_traces_filtered('failed')
@@ -460,25 +460,31 @@ class MassMailing(models.Model):
}
def action_view_opened(self):
- return self._action_view_documents_filtered('opened')
+ return self._action_view_documents_filtered('open')
def action_view_replied(self):
- return self._action_view_documents_filtered('replied')
+ return self._action_view_documents_filtered('reply')
def action_view_bounced(self):
- return self._action_view_documents_filtered('bounced')
+ return self._action_view_documents_filtered('bounce')
def action_view_delivered(self):
return self._action_view_documents_filtered('delivered')
def _action_view_documents_filtered(self, view_filter):
- if view_filter in ('opened', 'replied', 'bounced'):
- opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat[view_filter])
- elif view_filter == ('delivered'):
- opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat.sent and not stat.bounced)
+ if view_filter in ('reply', 'bounce'):
+ found_traces = self.mailing_trace_ids.filtered(lambda trace: trace.trace_status == view_filter)
+ elif view_filter == 'open':
+ found_traces = self.mailing_trace_ids.filtered(lambda trace: trace.trace_status in ('open', 'reply'))
+ elif view_filter == 'click':
+ found_traces = self.mailing_trace_ids.filtered(lambda trace: trace.links_click_datetime)
+ elif view_filter == 'delivered':
+ found_traces = self.mailing_trace_ids.filtered(lambda trace: trace.trace_status in ('sent', 'open', 'reply'))
+ elif view_filter == 'sent':
+ found_traces = self.mailing_trace_ids.filtered(lambda trace: trace.sent_datetime)
else:
- opened_stats = self.env['mailing.trace']
- res_ids = opened_stats.mapped('res_id')
+ found_traces = self.env['mailing.trace']
+ res_ids = found_traces.mapped('res_id')
model_name = self.env['ir.model']._get(self.mailing_model_real).display_name
return {
'name': model_name,
diff --git a/addons/mass_mailing/models/mailing_trace.py b/addons/mass_mailing/models/mailing_trace.py
index a50aefbb59a..cf0f3bc799f 100644
--- a/addons/mass_mailing/models/mailing_trace.py
+++ b/addons/mass_mailing/models/mailing_trace.py
@@ -12,9 +12,9 @@ class MailingTrace(models.Model):
Note:: State management / Error codes / Failure types summary
- * state
+ * trace_status
'outgoing', 'sent', 'opened', 'replied',
- 'exception', 'bounced', 'ignored'
+ 'error', 'bouce', 'cancel'
* failure_type
# mass_mailing
"SMTP", "RECIPIENT", "BOUNCE", "UNKNOWN", "mail_email_missing"
@@ -25,7 +25,7 @@ class MailingTrace(models.Model):
'sms_server', 'sms_acc'
# mass_mailing_sms mass mode specific codes
'sms_blacklist', 'sms_duplicate', 'sms_optout',
- * ignored:
+ * cancel:
* mail: set in get_mail_values in composer, if email is blacklisted
(mail) or in opt_out / seen list (mass_mailing) or email_to is void
or incorrectly formatted (mass_mailing) - based on mail cancel state
@@ -47,7 +47,7 @@ class MailingTrace(models.Model):
_name = 'mailing.trace'
_description = 'Mailing Statistics'
_rec_name = 'id'
- _order = 'scheduled DESC'
+ _order = 'create_date DESC'
trace_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True)
display_name = fields.Char(compute='_compute_display_name')
@@ -61,39 +61,30 @@ class MailingTrace(models.Model):
index=True,
)
email = fields.Char(string="Email", help="Normalized email address")
- message_id = fields.Char(string='Message-ID')
+ message_id = fields.Char(string='Message-ID', help="Technical field for the email Message-ID (RFC 2392)")
medium_id = fields.Many2one(related='mass_mailing_id.medium_id')
source_id = fields.Many2one(related='mass_mailing_id.source_id')
# document
model = fields.Char(string='Document model', required=True)
res_id = fields.Many2oneReference(string='Document ID', model_field='model', required=True)
- # campaign / wave data
+ # campaign data
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', index=True, ondelete='cascade')
campaign_id = fields.Many2one(
related='mass_mailing_id.campaign_id',
string='Campaign',
store=True, readonly=True, index=True)
- # Bounce and tracking
- ignored = fields.Datetime(help='Date when the email has been invalidated. '
- 'Invalid emails are blacklisted, opted-out or invalid email format')
- scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now)
- sent = fields.Datetime(help='Date when the email has been sent')
- exception = fields.Datetime(help='Date of technical error leading to the email not being sent')
- opened = fields.Datetime(help='Date when the email has been opened the first time')
- replied = fields.Datetime(help='Date when this email has been replied for the first time.')
- bounced = fields.Datetime(help='Date when this email has bounced.')
- # Link tracking
- links_click_ids = fields.One2many('link.tracker.click', 'mailing_trace_id', string='Links click')
- clicked = fields.Datetime(help='Date when customer clicked on at least one tracked link')
# Status
- state = fields.Selection(compute="_compute_state",
- selection=[('outgoing', 'Outgoing'),
- ('exception', 'Exception'),
- ('sent', 'Sent'),
- ('opened', 'Opened'),
- ('replied', 'Replied'),
- ('bounced', 'Bounced'),
- ('ignored', 'Ignored')], store=True)
+ sent_datetime = fields.Datetime('Sent On')
+ open_datetime = fields.Datetime('Opened On')
+ reply_datetime = fields.Datetime('Replied On')
+ trace_status = fields.Selection(selection=[
+ ('outgoing', 'Outgoing'),
+ ('sent', 'Sent'),
+ ('open', 'Opened'),
+ ('reply', 'Replied'),
+ ('bounce', 'Bounced'),
+ ('error', 'Exception'),
+ ('cancel', 'Canceled')], string='Status', default='outgoing')
failure_type = fields.Selection(selection=[
("SMTP", "Connection failed (outgoing mail server problem)"),
("RECIPIENT", "Invalid email address"),
@@ -105,34 +96,15 @@ class MailingTrace(models.Model):
("mail_optout", "Opted Out"),
("mail_dup", "Duplicated Email"),
], string='Failure type')
- state_update = fields.Datetime(compute="_compute_state", string='State Update',
- help='Last state update of the mail',
- store=True)
+ # Link tracking
+ links_click_ids = fields.One2many('link.tracker.click', 'mailing_trace_id', string='Links click')
+ links_click_datetime = fields.Datetime('Clicked On', help='Stores last click datetime in case of multi clicks.')
@api.depends('trace_type', 'mass_mailing_id')
def _compute_display_name(self):
for trace in self:
trace.display_name = '%s: %s (%s)' % (trace.trace_type, trace.mass_mailing_id.name, trace.id)
- @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored')
- def _compute_state(self):
- self.update({'state_update': fields.Datetime.now()})
- for stat in self:
- if stat.ignored:
- stat.state = 'ignored'
- elif stat.exception:
- stat.state = 'exception'
- elif stat.replied:
- stat.state = 'replied'
- elif stat.opened or stat.clicked:
- stat.state = 'opened'
- elif stat.bounced:
- stat.state = 'bounced'
- elif stat.sent:
- stat.state = 'sent'
- else:
- stat.state = 'outgoing'
-
@api.model_create_multi
def create(self, values_list):
for values in values_list:
@@ -150,33 +122,37 @@ class MailingTrace(models.Model):
'res_id': self.res_id
}
- def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None):
- if not self.ids and mail_mail_ids:
- base_domain = [('mail_mail_id_int', 'in', mail_mail_ids)]
- elif not self.ids and mail_message_ids:
- base_domain = [('message_id', 'in', mail_message_ids)]
- else:
- base_domain = [('id', 'in', self.ids)]
- if domain:
- base_domain = ['&'] + domain + base_domain
- return self.search(base_domain)
-
- def set_opened(self, mail_mail_ids=None, mail_message_ids=None):
- traces = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)])
- traces.write({'opened': fields.Datetime.now(), 'bounced': False})
+ def set_sent(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'sent', 'sent_datetime': fields.Datetime.now(), 'failure_type': False})
return traces
- def set_clicked(self, mail_mail_ids=None, mail_message_ids=None):
- traces = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)])
- traces.write({'clicked': fields.Datetime.now()})
+ def set_opened(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'open', 'open_datetime': fields.Datetime.now()})
return traces
- def set_replied(self, mail_mail_ids=None, mail_message_ids=None):
- traces = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)])
- traces.write({'replied': fields.Datetime.now()})
+ def set_clicked(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'links_click_datetime': fields.Datetime.now()})
return traces
- def set_bounced(self, mail_mail_ids=None, mail_message_ids=None):
- traces = self._get_records(mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)])
- traces.write({'bounced': fields.Datetime.now()})
+ def set_replied(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'reply', 'reply_datetime': fields.Datetime.now()})
+ return traces
+
+ def set_bounced(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'bounce'})
+ return traces
+
+ def set_failed(self, domain=None, failure_type=False):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'error', 'failure_type': failure_type})
+ return traces
+
+ def set_canceled(self, domain=None):
+ traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
+ traces.write({'trace_status': 'cancel'})
return traces
diff --git a/addons/mass_mailing/models/utm.py b/addons/mass_mailing/models/utm.py
index 88c99e0f0ea..ece57e963b2 100644
--- a/addons/mass_mailing/models/utm.py
+++ b/addons/mass_mailing/models/utm.py
@@ -47,12 +47,12 @@ class UtmCampaign(models.Model):
SELECT
c.id as campaign_id,
COUNT(s.id) AS expected,
- COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
- COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
- COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
- COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
- COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
- COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
+ COUNT(s.sent_datetime) AS sent,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('sent', 'open', 'reply')) AS delivered,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('open', 'reply')) AS open,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'reply') AS reply,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'bounce') AS bounce,
+ COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'cancel') AS cancel
FROM
mailing_trace s
RIGHT JOIN
@@ -75,13 +75,13 @@ class UtmCampaign(models.Model):
if not stats:
vals = default_vals
else:
- total = (stats['expected'] - stats['ignored']) or 1
- delivered = stats['sent'] - stats['bounced']
+ total = (stats['expected'] - stats['cancel']) or 1
+ delivered = stats['sent'] - stats['bounce']
vals = {
'received_ratio': 100.0 * delivered / total,
- 'opened_ratio': 100.0 * stats['opened'] / total,
- 'replied_ratio': 100.0 * stats['replied'] / total,
- 'bounced_ratio': 100.0 * stats['bounced'] / total
+ 'opened_ratio': 100.0 * stats['open'] / total,
+ 'replied_ratio': 100.0 * stats['reply'] / total,
+ 'bounced_ratio': 100.0 * stats['bounce'] / total
}
campaign.update(vals)
diff --git a/addons/mass_mailing/report/mailing_trace_report.py b/addons/mass_mailing/report/mailing_trace_report.py
index 02931d629d4..e3711151077 100644
--- a/addons/mass_mailing/report/mailing_trace_report.py
+++ b/addons/mass_mailing/report/mailing_trace_report.py
@@ -21,10 +21,12 @@ class MailingTraceReport(models.Model):
# traces
sent = fields.Integer(readonly=True)
delivered = fields.Integer(readonly=True)
+ error = fields.Integer(readonly=True)
opened = fields.Integer(readonly=True)
replied = fields.Integer(readonly=True)
- clicked = fields.Integer(readonly=True)
bounced = fields.Integer(readonly=True)
+ canceled = fields.Integer(readonly=True)
+ clicked = fields.Integer(readonly=True)
def init(self):
"""Mass Mail Statistical Report: based on mailing.trace that models the various
@@ -52,23 +54,25 @@ class MailingTraceReport(models.Model):
'utm_source.name as name',
'mailing.mailing_type',
'utm_campaign.name as campaign',
- 'trace.scheduled as scheduled_date',
+ 'trace.create_date as scheduled_date',
'mailing.state',
'mailing.email_from',
- 'count(trace.sent) as sent',
- '(count(trace.sent) - count(trace.bounced)) as delivered',
- 'count(trace.opened) as opened',
- 'count(trace.replied) as replied',
- 'count(trace.clicked) as clicked',
- 'count(trace.bounced) as bounced'
+ 'COUNT(trace.sent_datetime) as sent',
+ "(COUNT(trace.sent_datetime) - COUNT(trace.trace_status) FILTER (WHERE trace.trace_status IN ('error', 'bounce', 'cancel'))) as delivered",
+ "COUNT(trace.trace_status) FILTER (WHERE trace.trace_status = 'error') as error",
+ "COUNT(trace.trace_status) FILTER (WHERE trace.trace_status = 'bounce') as bounced",
+ "COUNT(trace.trace_status) FILTER (WHERE trace.trace_status = 'cancel') as cancel",
+ "COUNT(trace.trace_status) FILTER (WHERE trace.trace_status = 'open') as opened",
+ "COUNT(trace.trace_status) FILTER (WHERE trace.trace_status = 'reply') as replied",
+ "COUNT(trace.links_click_datetime) as clicked",
]
def _report_get_request_from_items(self):
return [
'mailing_trace as trace',
- 'left join mailing_mailing as mailing ON (trace.mass_mailing_id=mailing.id)',
- 'left join utm_campaign as utm_campaign ON (mailing.campaign_id = utm_campaign.id)',
- 'left join utm_source as utm_source ON (mailing.source_id = utm_source.id)'
+ 'LEFT JOIN mailing_mailing as mailing ON (trace.mass_mailing_id=mailing.id)',
+ 'LEFT JOIN utm_campaign as utm_campaign ON (mailing.campaign_id = utm_campaign.id)',
+ 'LEFT JOIN utm_source as utm_source ON (mailing.source_id = utm_source.id)'
]
def _report_get_request_where_items(self):
@@ -76,7 +80,7 @@ class MailingTraceReport(models.Model):
def _report_get_request_group_by_items(self):
return [
- 'trace.scheduled',
+ 'trace.create_date',
'utm_source.name',
'utm_campaign.name',
'mailing.mailing_type',
diff --git a/addons/mass_mailing/tests/common.py b/addons/mass_mailing/tests/common.py
index 56b7155919d..96b5fcfee0c 100644
--- a/addons/mass_mailing/tests/common.py
+++ b/addons/mass_mailing/tests/common.py
@@ -6,9 +6,13 @@ import random
import re
import werkzeug
+from unittest.mock import patch
+
+from odoo import tools
from odoo.addons.link_tracker.tests.common import MockLinkTracker
from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_user
-from odoo import tools
+from odoo.sql_db import Cursor
+
class MassMailCase(MailCase, MockLinkTracker):
@@ -25,7 +29,7 @@ class MassMailCase(MailCase, MockLinkTracker):
kwargs['delivered'] = len(mailing.mailing_trace_ids)
for fname in ['scheduled', 'expected', 'sent', 'delivered',
'opened', 'replied', 'clicked',
- 'ignored', 'failed', 'bounced']:
+ 'canceled', 'failed', 'bounced']:
self.assertEqual(
mailing[fname], kwargs.get(fname, 0),
'Mailing %s statistics failed: got %s instead of %s' % (fname, mailing[fname], kwargs.get(fname, 0))
@@ -44,7 +48,7 @@ class MassMailCase(MailCase, MockLinkTracker):
# TRACE
'partner': res.partner record (may be empty),
'email': email used when sending email (may be empty, computed based on partner),
- 'state': outgoing / sent / ignored / bounced / exception / opened (sent by default),
+ 'trace_status': outgoing / sent / open / reply / bounce / error / cancel (sent by default),
'record: linked record,
# MAIL.MAIL
'content': optional content that should be present in mail.mail body_html;
@@ -67,12 +71,11 @@ class MassMailCase(MailCase, MockLinkTracker):
# map trace state to email state
state_mapping = {
'sent': 'sent',
- 'opened': 'sent', # opened implies something has been sent
- 'replied': 'sent', # replied implies something has been sent
- 'ignored': 'cancel',
- 'exception': 'exception',
- 'canceled': 'cancel',
- 'bounced': 'cancel',
+ 'open': 'sent', # opened implies something has been sent
+ 'reply': 'sent', # replied implies something has been sent
+ 'error': 'exception',
+ 'cancel': 'cancel',
+ 'bounce': 'cancel',
}
traces = self.env['mailing.trace'].search([
@@ -90,21 +93,23 @@ class MassMailCase(MailCase, MockLinkTracker):
for recipient_info, link_info, record in zip(recipients_info, mail_links_info, records):
partner = recipient_info.get('partner', self.env['res.partner'])
email = recipient_info.get('email')
- state = recipient_info.get('state', 'sent')
+ status = recipient_info.get('trace_status', 'sent')
record = record or recipient_info.get('record')
content = recipient_info.get('content')
if email is None and partner:
email = partner.email_normalized
recipient_trace = traces.filtered(
- lambda t: t.email == email and t.state == state and (t.res_id == record.id if record else True)
+ lambda t: (t.email == email or (not email and not t.email)) and \
+ t.trace_status == status and \
+ (t.res_id == record.id if record else True)
)
self.assertTrue(
len(recipient_trace) == 1,
- 'MailTrace: email %s (recipient %s, state: %s, record: %s): found %s records (1 expected)' % (email, partner, state, record, len(recipient_trace))
+ 'MailTrace: email %s (recipient %s, status: %s, record: %s): found %s records (1 expected)' % (email, partner, status, record, len(recipient_trace))
)
self.assertTrue(bool(recipient_trace.mail_mail_id_int))
- if 'failure_type' in recipient_info or state in ('ignored', 'exception', 'canceled', 'bounced'):
+ if 'failure_type' in recipient_info or status in ('error', 'cancel', 'bounce'):
self.assertEqual(recipient_trace.failure_type, recipient_info['failure_type'])
if check_mail:
@@ -116,15 +121,15 @@ class MassMailCase(MailCase, MockLinkTracker):
# specific for partner: email_formatted is used
if partner:
- if state == 'sent' and sent_unlink:
+ if status == 'sent' and sent_unlink:
self.assertSentEmail(author, [partner])
else:
- self.assertMailMail(partner, state_mapping[state], author=author, content=content, fields_values=fields_values)
+ self.assertMailMail(partner, state_mapping[status], author=author, content=content, fields_values=fields_values)
# specific if email is False -> could have troubles finding it if several falsy traces
- elif not email and state in ('ignored', 'canceled', 'bounced'):
- self.assertMailMailWId(recipient_trace.mail_mail_id_int, state_mapping[state], content=content, fields_values=fields_values)
+ elif not email and status in ('cancel', 'bounce'):
+ self.assertMailMailWId(recipient_trace.mail_mail_id_int, state_mapping[status], content=content, fields_values=fields_values)
else:
- self.assertMailMailWEmails([email], state_mapping[state], author=author, content=content, fields_values=fields_values)
+ self.assertMailMailWEmails([email], state_mapping[status], author=author, content=content, fields_values=fields_values)
if link_info:
trace_mail = self._find_mail_mail_wrecord(record)
@@ -200,15 +205,18 @@ class MassMailCase(MailCase, MockLinkTracker):
if dt is None:
dt = datetime.datetime.now() - datetime.timedelta(days=1)
randomized = random.random()
- trace = cls.env['mailing.trace'].create({
- 'mass_mailing_id': mailing.id,
- 'model': record._name,
- 'res_id': record.id,
- 'bounced': dt,
- # TDE FIXME: improve this with a mail-enabled heuristics
- 'email': trace_email,
- 'message_id': '<%5f@gilbert.boitempomils>' % randomized,
- })
+ # Cursor.now() uses transaction's timestamp and not datetime lib -> freeze_time
+ # is not sufficient
+ with patch.object(Cursor, 'now', lambda *args, **kwargs: dt):
+ trace = cls.env['mailing.trace'].sudo().create({
+ 'mass_mailing_id': mailing.id,
+ 'model': record._name,
+ 'res_id': record.id,
+ 'trace_status': 'bounce',
+ # TDE FIXME: improve this with a mail-enabled heuristics
+ 'email': trace_email,
+ 'message_id': '<%5f@gilbert.boitempomils>' % randomized,
+ })
return trace
diff --git a/addons/mass_mailing/tests/test_mailing_internals.py b/addons/mass_mailing/tests/test_mailing_internals.py
index 4fafbf6e3f8..b90210405eb 100644
--- a/addons/mass_mailing/tests/test_mailing_internals.py
+++ b/addons/mass_mailing/tests/test_mailing_internals.py
@@ -276,7 +276,7 @@ class TestMassMailFeatures(MassMailCommon, CronMixinCase):
self.assertMailTraces(
[{'partner': partner_a},
- {'partner': partner_b, 'state': 'ignored', 'failure_type': 'mail_bl'}],
+ {'partner': partner_b, 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, partner_a + partner_b, check_mail=True
)
diff --git a/addons/mass_mailing/views/mailing_mailing_views.xml b/addons/mass_mailing/views/mailing_mailing_views.xml
index 248205f8a24..01e21298c95 100644
--- a/addons/mass_mailing/views/mailing_mailing_views.xml
+++ b/addons/mass_mailing/views/mailing_mailing_views.xml
@@ -66,14 +66,14 @@
-
+ attrs="{'invisible': ['&','&','&','&','&',('state', '!=', 'in_queue'),('sent', '=', 0),('canceled', '=', 0),('scheduled', '=', 0),('failed', '=', 0),('warning_message', '=', False)]}">
+
@@ -339,7 +339,7 @@
{'invisible': ['|',('state', '=', 'draft'),'&','&',('state', '!=', 'in_queue'),('scheduled', '=', 0),('failed', '=', 0)]}
-
+
email(s) not sent.
@@ -374,10 +374,10 @@
type="object" class="oe_stat_button" icon="fa-paper-plane">
-
diff --git a/addons/mass_mailing/views/mailing_trace_views.xml b/addons/mass_mailing/views/mailing_trace_views.xml
index b2205d374bb..fc8d54adc6f 100644
--- a/addons/mass_mailing/views/mailing_trace_views.xml
+++ b/addons/mass_mailing/views/mailing_trace_views.xml
@@ -10,22 +10,22 @@
-
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -38,16 +38,13 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -58,7 +55,7 @@
-
+
{'invisible': [('mailing_type', '!=', 'mail')]}
-
- SMS Text Message have been ignored and will not be sent.
+
+ SMS Text Message have been canceled and will not be sent.
{'invisible': [('mailing_type', '!=', 'mail')]}
diff --git a/addons/mass_mailing_sms/views/mailing_trace_views.xml b/addons/mass_mailing_sms/views/mailing_trace_views.xml
index 32e1fc65b6f..297286f05c3 100644
--- a/addons/mass_mailing_sms/views/mailing_trace_views.xml
+++ b/addons/mass_mailing_sms/views/mailing_trace_views.xml
@@ -33,14 +33,14 @@
20
+
-
-
-
-
-
-
-
+
+
+
+
+
+
@@ -60,16 +60,6 @@
{'invisible': [('trace_type', '!=', 'mail')]}
-
-
- {'invisible': ['|', ('opened', '=', False), ('trace_type', '=', 'sms')]}
-
-
-
-
- {'invisible': ['|', ('replied', '=', False), ('trace_type', '=', 'sms')]}
-
-
@@ -87,13 +77,13 @@