[REF] mass_mailing: improve traces state management

PURPOSE

Improve modeling and performances of mass mailing by manually updating trace
status instead of using complex computed fields and lessening fields usage.

Remove some fields and keep only relevant metrics to simplify and clean trace
model.

SPECIFICATIONS

In this commit we clean the way trace state is managed. Currently it is a
computed field based on several datetime fields. However this generates a
lot of noise in the table as well as unnecessary computation

  * there are several columns (one for each state) storing datetime at
    which status was reached. Generally only 2 or 3 contain relevant
    information;
  * state could be set in code directly to avoid a computed field based on
    many triggers;
  * recomputing it each time a date changes is not necessarily necessary;
  * state value can always be updated manually as this is main done through
    some automated server update (mailgateway, link clicks, ...);

As trace states and its triggers should not be updated manually it is better
to synchronize it in code flow. When there is an exception or update done
through sending or gateway status is updated as well accordingly. Various
datetime fields are also updated at the same time. In order to align with
notification model mail and sms trace status are updated to a classic field.
Only last status update is now kept as there is no need to store the entire
history of status change.

We keep only a datetime for relevant metrics: open, reply and click. Other
datetime bring no real value. Knowing when a trace was in error or bounced
is not necessary. Indeed exception generally indicates a server issue (at
sending), cancel indicates a data issue (at sending) and bounce depends on
customer email server.

Status update is removed as using write_date is sufficient. Once created
traces are updated only when an external event occurs (opened, replied, ...).
It allows to simplify trace model.

We also rename ignored field into canceled to match naming use through mail
and sms.

QUERY COUNTERS

This change has some positive update on query counters when sending mailings
as traces have less unnecessary status update compared to priori this change.

LINKS

Task ID-2377974
Community PR odoo/odoo#61467
Enterprise PR odoo/enterprise#14633
Upgrade PR odoo/upgrade#1907
This commit is contained in:
Thibault Delavallée
2021-08-18 13:38:08 +00:00
parent d950b97dbf
commit bbf4783ac6
26 changed files with 340 additions and 360 deletions
+1 -1
View File
@@ -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==')
+28 -17
View File
@@ -184,9 +184,11 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_7"/>
<field name="email">billy.fox45@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">reply</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=4)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="reply_datetime" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_1" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -194,9 +196,11 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_13"/>
<field name="email">kim.snyder96@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">reply</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="reply_datetime" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_2" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -204,8 +208,10 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_14"/>
<field name="email">edith.sanchez68@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">open</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_3" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -213,8 +219,10 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_24"/>
<field name="email">theodore.gardner36@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">open</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_4" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -222,7 +230,9 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_32"/>
<field name="email">sandra.neal80@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_5" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -230,8 +240,8 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_33"/>
<field name="email">julie.richards84@example.com</field>
<field name="sent" eval="False"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">error</field>
<field name="sent_datetime" eval="False"/>
</record>
<record id="mass_mail_1_stat_6" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -239,8 +249,9 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_34"/>
<field name="email">travis.mendoza24@example.com</field>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">bounce</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_7" model="mailing.trace">
<field name="mass_mailing_id" ref="mass_mail_1"/>
@@ -248,8 +259,8 @@
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_34"/>
<field name="email">travis.mendoza24@example.com</field>
<field name="sent" eval="False"/>
<field name="ignored" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">bounce</field>
<field name="sent_datetime" eval="False"/>
</record>
<!-- Generate some clicks -->
+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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)
+29 -23
View File
@@ -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,
+47 -71
View File
@@ -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
+11 -11
View File
@@ -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)
@@ -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',
+35 -27
View File
@@ -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
@@ -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
)
@@ -66,14 +66,14 @@
<field name="state" readonly="1" widget="statusbar"/>
</header>
<div class="alert alert-info text-center" role="alert"
attrs="{'invisible': ['&amp;','&amp;','&amp;','&amp;','&amp;',('state', '!=', 'in_queue'),('sent', '=', 0),('ignored', '=', 0),('scheduled', '=', 0),('failed', '=', 0),('warning_message', '=', False)]}">
<div class="o_mails_ignored" attrs="{'invisible': [('ignored', '=', 0)]}">
attrs="{'invisible': ['&amp;','&amp;','&amp;','&amp;','&amp;',('state', '!=', 'in_queue'),('sent', '=', 0),('canceled', '=', 0),('scheduled', '=', 0),('failed', '=', 0),('warning_message', '=', False)]}">
<div class="o_mails_canceled" attrs="{'invisible': [('canceled', '=', 0)]}">
<button class="btn-link py-0"
name="action_view_traces_ignored"
name="action_view_traces_canceled"
type="object">
<strong>
<field name="ignored" class="oe_inline mr-2"/>
<span name="ignored_text">emails have been ignored and will not be sent.</span>
<field name="canceled" class="oe_inline mr-2"/>
<span name="canceled_text">emails have been canceled and will not be sent.</span>
</strong>
</button>
</div>
@@ -339,7 +339,7 @@
<xpath expr="//div[hasclass('alert-info')]" position="attributes">
<attribute name="attrs">{'invisible': ['|',('state', '=', 'draft'),'&amp;','&amp;',('state', '!=', 'in_queue'),('scheduled', '=', 0),('failed', '=', 0)]}</attribute>
</xpath>
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_ignored')]" position="replace"/>
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_canceled')]" position="replace"/>
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_sent')]" position="replace"/>
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_failed')]//span[@name='failed_text']" position="replace">
<span name="failed_text">email(s) not sent.</span>
@@ -374,10 +374,10 @@
type="object" class="oe_stat_button" icon="fa-paper-plane">
<field name="sent" widget="statinfo" string="Sent"/>
</button>
<button name="action_view_traces_ignored"
<button name="action_view_traces_canceled"
attrs="{'invisible': [('state', 'in', ('draft','test'))]}"
type="object" class="oe_stat_button" icon="fa-paper-plane-o">
<field name="ignored" widget="statinfo" string="Ignored"/>
<field name="canceled" widget="statinfo" string="Ignored"/>
</button>
</xpath>
<xpath expr="//notebook" position="inside">
@@ -10,22 +10,22 @@
<field name="message_id"/>
<field name="email"/>
<field name="mass_mailing_id"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False), ('ignored', '=', False), ('bounced', '=', False)]"/>
<filter string="Ignored" name="filter_ignored" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False), ('ignored', '!=', False)]"/>
<filter string="Sent" name="filter_sent" domain="[('sent', '!=', False)]"/>
<filter string="Delivered" name="filter_delivered" domain="[('sent', '!=', False), ('exception', '=', False), ('bounced', '=', False)]"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('trace_status', '=', 'outgoing')]"/>
<filter string="Canceled" name="filter_canceled" domain="[('trace_status', '=', 'cancel')]"/>
<filter string="Sent" name="filter_sent" domain="[('sent_datetime', '!=', False)]"/>
<filter string="Delivered" name="filter_delivered" domain="[('sent_datetime', '!=', False), ('trace_status', 'not in', ['error', 'cancel'])]"/>
<separator/>
<filter string="Opened" name="filter_opened" domain="[('opened', '!=', False)]"/>
<filter string="Clicked" name="filter_clicked" domain="[('clicked', '!=', False)]"/>
<filter string="Replied" name="filter_replied" domain="[('replied', '!=', False)]"/>
<filter string="Bounced" name="filter_bounced" domain="[('bounced', '!=', False)]"/>
<filter string="Failed" name="filter_failed" domain="[('exception', '!=', False)]"/>
<filter string="Opened" name="filter_opened" domain="[('trace_status', 'in', ['open', 'reply'])]"/>
<filter string="Clicked" name="filter_clicked" domain="[('links_click_datetime', '!=', False)]"/>
<filter string="Replied" name="filter_replied" domain="[('trace_status', '=', 'reply')]"/>
<filter string="Bounced" name="filter_bounced" domain="[('trace_status', '=', 'bounce')]"/>
<filter string="Failed" name="filter_failed" domain="[('trace_status', '=', 'error')]"/>
<group expand="0" string="Group By">
<filter string="State" name="state" domain="[]" context="{'group_by':'state'}"/>
<filter string="Open Date" name="group_open_date" context="{'group_by': 'opened:day'}"/>
<filter string="Reply Date" name="group_reply_date" context="{'group_by': 'replied:day'}"/>
<filter string="Last State Update" name="state_update" domain="[]" context="{'group_by':'state_update'}"/>
<filter string="Mass Mailing" name="mass_mailing" domain="[]" context="{'group_by':'mass_mailing_id'}"/>
<filter string="State" name="state" domain="[]" context="{'group_by': 'trace_status'}"/>
<filter string="Open Date" name="group_open_date" domain="[('trace_status', 'in', ['open', 'reply'])]" context="{'group_by': 'open_datetime:day'}"/>
<filter string="Reply Date" name="group_reply_date" domain="[('trace_status', '=', 'reply')]" context="{'group_by': 'reply_datetime:day'}"/>
<filter string="Last State Update" name="state_update" domain="[]" context="{'group_by': 'write_date'}"/>
<filter string="Mass Mailing" name="mass_mailing" domain="[]" context="{'group_by': 'mass_mailing_id'}"/>
</group>
</search>
</field>
@@ -38,16 +38,13 @@
<tree string="Mail Statistics" create="0">
<field name="mass_mailing_id"/>
<field name="email"/>
<field name="state"/>
<field name="message_id"/>
<field name="scheduled"/>
<field name="sent"/>
<field name="exception"/>
<field name="opened"/>
<field name="clicked"/>
<field name="replied"/>
<field name="bounced"/>
<field name="ignored"/>
<field name="sent_datetime"/>
<field name="trace_status"/>
<field name="open_datetime" optional="hide"/>
<field name="reply_datetime" optional="hide"/>
<field name="links_click_datetime" optional="hide"/>
<field name="failure_type"/>
</tree>
</field>
</record>
@@ -58,7 +55,7 @@
<field name="arch" type="xml">
<form string="Mail Statistics" create="0" edit="0">
<header>
<field name="state" widget="statusbar"/>
<field name="trace_status" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
@@ -71,24 +68,19 @@
<group string="Mailing">
<field name="trace_type" invisible="1"/>
<field name="email" string="Recipient Address" readonly="1"/>
<field name="sent" attrs="{'invisible' : [('sent', '=', False)]}" readonly="1"/>
<field name="opened" attrs="{'invisible' : [('opened', '=', False)]}" readonly="1"/>
<field name="replied" attrs="{'invisible' : [('replied', '=', False)]}" readonly="1"/>
<field name="bounced" attrs="{'invisible' : [('bounced', '=', False)]}" readonly="1"/>
<field name="ignored" attrs="{'invisible' : [('ignored', '=', False)]}" readonly="1"/>
<field name="sent_datetime" attrs="{'invisible' : [('sent_datetime', '=', False)]}" readonly="1"/>
<field name="open_datetime" attrs="{'invisible' : [('open_datetime', '=', False)]}" readonly="1"/>
<field name="reply_datetime" attrs="{'invisible' : [('reply_datetime', '=', False)]}" readonly="1"/>
<field name="failure_type" attrs="{'invisible' : [('failure_type', '=', False)]}" readonly="1"/>
</group>
<group string="Marketing">
<field name="campaign_id" readonly="1"/>
<field name="medium_id" readonly="1"/>
<field name="source_id" readonly="1"/>
<field name="mass_mailing_id" readonly="1"/>
<field name="links_click_datetime" readonly="1"/>
</group>
<group string="Technical">
<field name="exception" readonly="1"/>
<field name="state_update" readonly="1"/>
<field name="mail_mail_id_int" string="Mail ID" readonly="1"/>
<field name="message_id" readonly="1"/>
</group>
@@ -103,8 +95,8 @@
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<graph string="Mail Statistics" type="bar" stacked="True" sample="1">
<field name="state_update" type="row" interval="day"/>
<field name="state" type="row"/>
<field name="write_date" type="row" interval="day"/>
<field name="trace_status" type="row"/>
</graph>
</field>
</record>
@@ -17,6 +17,7 @@ class MailComposeMessage(models.TransientModel):
""" Override method that generated the mail content by creating the
mailing.trace values in the o2m of mail_mail, when doing pure
email mass mailing. """
now = fields.Datetime.now()
self.ensure_one()
res = super(MailComposeMessage, self).get_mail_values(res_ids)
# use only for allowed models in mass mailing
@@ -32,7 +33,7 @@ class MailComposeMessage(models.TransientModel):
'state': 'done',
'reply_to_mode': self.reply_to_mode,
'reply_to': self.reply_to if self.reply_to_mode == 'new' else False,
'sent_date': fields.Datetime.now(),
'sent_date': now,
'body_html': self.body,
'mailing_model_id': self.env['ir.model']._get(self.model).id,
'mailing_domain': self.active_domain,
@@ -98,9 +99,9 @@ class MailComposeMessage(models.TransientModel):
}
# propagate failed states to trace when still-born
if mail_values.get('state') == 'cancel':
trace_vals['ignored'] = fields.Datetime.now()
trace_vals['trace_status'] = 'cancel'
elif mail_values.get('state') == 'exception':
trace_vals['exception'] = fields.Datetime.now()
trace_vals['trace_status'] = 'error'
if error_code:
trace_vals['failure_type'] = error_code
+30 -15
View File
@@ -21,7 +21,9 @@
<field name="sms_number">+32465000000</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_7"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_1" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -29,7 +31,9 @@
<field name="sms_number">+32465000001</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_13"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_2" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -37,7 +41,9 @@
<field name="sms_number">+32465000002</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_14"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_3" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -45,7 +51,9 @@
<field name="sms_number">+32465000003</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_24"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_4" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -53,8 +61,10 @@
<field name="sms_number">+32465000004</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_33"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">error</field>
<field name="failure_type">sms_server</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_5" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -62,8 +72,9 @@
<field name="sms_number">123123</field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_33"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="bounced" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">bounce</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_0_trace_6" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_0"/>
@@ -71,8 +82,9 @@
<field name="sms_number"></field>
<field name="model">res.partner</field>
<field name="res_id" ref="base.res_partner_address_34"/>
<field name="sent" eval="False"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">error</field>
<field name="failure_type">sms_number_missing</field>
<field name="sent_datetime" eval="False"/>
</record>
<!-- Generate some clicks -->
@@ -128,7 +140,8 @@
<field name="sms_number">+32456001100</field>
<field name="model">mailing.contact</field>
<field name="res_id" ref="mass_mailing_sms.mailing_contact_0_0"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_1_trace_1" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_1"/>
@@ -136,7 +149,8 @@
<field name="sms_number">+32456001111</field>
<field name="model">mailing.contact</field>
<field name="res_id" ref="mass_mailing_sms.mailing_contact_0_1"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_1_trace_2" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_1"/>
@@ -144,7 +158,8 @@
<field name="sms_number">+32456001122</field>
<field name="model">mailing.contact</field>
<field name="res_id" ref="mass_mailing_sms.mailing_contact_0_2"/>
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">sent</field>
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mailing_sms_1_trace_3" model="mailing.trace">
<field name="mass_mailing_id" ref="mailing_sms_1"/>
@@ -152,7 +167,7 @@
<field name="sms_number">+32456001133</field>
<field name="model">mailing.contact</field>
<field name="res_id" ref="mass_mailing_sms.mailing_contact_0_3"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">error</field>
<field name="failure_type">sms_credit</field>
</record>
<record id="mailing_sms_1_trace_4" model="mailing.trace">
@@ -161,7 +176,7 @@
<field name="sms_number">dummy</field>
<field name="model">mailing.contact</field>
<field name="res_id" ref="mass_mailing_sms.mailing_contact_0_4"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="trace_status">error</field>
<field name="failure_type">sms_number_format</field>
</record>
@@ -53,36 +53,3 @@ class MailingTrace(models.Model):
as it serves as obfuscation when unsubscribing. A valid trio
code / mailing_id / number will be requested. """
return ''.join(random.choice(string.ascii_letters + string.digits) for dummy in range(self.CODE_SIZE))
def _get_records_from_sms(self, sms_sms_ids=None, additional_domain=None):
if not self.ids and sms_sms_ids:
domain = [('sms_sms_id_int', 'in', sms_sms_ids)]
else:
domain = [('id', 'in', self.ids)]
if additional_domain:
domain = expression.AND([domain, additional_domain])
return self.search(domain)
def set_failed(self, failure_type):
for trace in self:
trace.write({'exception': fields.Datetime.now(), 'failure_type': failure_type})
def set_sms_sent(self, sms_sms_ids=None):
statistics = self._get_records_from_sms(sms_sms_ids, [('sent', '=', False)])
statistics.write({'sent': fields.Datetime.now()})
return statistics
def set_sms_clicked(self, sms_sms_ids=None):
statistics = self._get_records_from_sms(sms_sms_ids, [('clicked', '=', False)])
statistics.write({'clicked': fields.Datetime.now()})
return statistics
def set_sms_ignored(self, sms_sms_ids=None):
statistics = self._get_records_from_sms(sms_sms_ids, [('ignored', '=', False)])
statistics.write({'ignored': fields.Datetime.now()})
return statistics
def set_sms_exception(self, sms_sms_ids=None):
statistics = self._get_records_from_sms(sms_sms_ids, [('exception', '=', False)])
statistics.write({'exception': fields.Datetime.now()})
return statistics
+1 -1
View File
@@ -37,7 +37,7 @@ class SmsSms(models.Model):
('sms_sms_id_int', 'in', sms_ids)
])
if traces and state == 'success':
traces.write({'sent': fields.Datetime.now(), 'exception': False})
traces.set_sent()
elif traces:
traces.set_failed(failure_type=self.IAP_TO_SMS_STATE[state])
return super(SmsSms, self)._postprocess_iap_sent_sms(iap_results, failure_reason=failure_reason, delete_all=delete_all)
+12 -12
View File
@@ -31,7 +31,7 @@ class MassSMSCase(SMSCase):
# TRACE
'partner': res.partner record (may be empty),
'number': number used for notification (may be empty, computed based on partner),
'state': outgoing / sent / ignored / bounced / exception / opened (outgoing by default),
'trace_status': outgoing / sent / cancel / bounce / error / opened (sent by default),
'record: linked record,
# SMS.SMS
'content': optional: if set, check content of sent SMS;
@@ -55,9 +55,9 @@ class MassSMSCase(SMSCase):
state_mapping = {
'sent': 'sent',
'outgoing': 'outgoing',
'exception': 'error',
'ignored': 'canceled',
'bounced': 'error',
'error': 'error',
'cancel': 'canceled',
'bounce': 'error',
}
traces = self.env['mailing.trace'].search([
('mass_mailing_id', 'in', mailing.ids),
@@ -74,27 +74,27 @@ class MassSMSCase(SMSCase):
for recipient_info, link_info, record in zip(recipients_info, sms_links_info, records):
partner = recipient_info.get('partner', self.env['res.partner'])
number = recipient_info.get('number')
state = recipient_info.get('state', 'outgoing')
status = recipient_info.get('trace_status', 'outgoing')
content = recipient_info.get('content', None)
if number is None and partner:
number = partner._sms_get_recipients_info()[partner.id]['sanitized']
trace = traces.filtered(
lambda t: t.sms_number == number and t.state == state and (t.res_id == record.id if record else True)
lambda t: t.sms_number == number and t.trace_status == status and (t.res_id == record.id if record else True)
)
self.assertTrue(len(trace) == 1,
'SMS: found %s notification for number %s, (state: %s) (1 expected)' % (len(trace), number, state))
'SMS: found %s notification for number %s, (status: %s) (1 expected)' % (len(trace), number, status))
self.assertTrue(bool(trace.sms_sms_id_int))
if check_sms:
if state == 'sent':
if status == 'sent':
if sent_unlink:
self.assertSMSIapSent([number], content=content)
else:
self.assertSMS(partner, number, 'sent', content=content)
elif state in state_mapping:
sms_state = state_mapping[state]
error_code = recipient_info['failure_type'] if state in ('exception', 'ignored', 'bounced') else None
elif status in state_mapping:
sms_state = state_mapping[status]
error_code = recipient_info['failure_type'] if status in ('error', 'cancel', 'bounce') else None
self.assertSMS(partner, number, sms_state, error_code=error_code, content=content)
else:
raise NotImplementedError()
@@ -103,7 +103,7 @@ class MassSMSCase(SMSCase):
# shortened links are directly included in sms.sms record as well as
# in sent sms (not like mails who are post-processed)
sms_sent = self._find_sms_sent(partner, number)
sms_sms = self._find_sms_sms(partner, number, state_mapping[state])
sms_sms = self._find_sms_sms(partner, number, state_mapping[status])
for (url, is_shortened, add_link_params) in link_info:
if url == 'unsubscribe':
url = '%s/sms/%d/%s' % (mailing.get_base_url(), mailing.id, trace.sms_code)
@@ -61,11 +61,11 @@
</button>
</div>
</xpath>
<xpath expr="//span[@name='ignored_text']" position="attributes">
<xpath expr="//span[@name='canceled_text']" position="attributes">
<attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
</xpath>
<xpath expr="//span[@name='ignored_text']" position="after">
<span name="ignored_text_sms" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">SMS Text Message have been ignored and will not be sent.</span>
<xpath expr="//span[@name='canceled_text']" position="after">
<span name="canceled_text_sms" attrs="{'invisible': [('mailing_type', '!=', 'sms')]}">SMS Text Message have been canceled and will not be sent.</span>
</xpath>
<xpath expr="//span[@name='scheduled_text']" position="attributes">
<attribute name="attrs">{'invisible': [('mailing_type', '!=', 'mail')]}</attribute>
@@ -33,14 +33,14 @@
<field name="priority">20</field>
<field name="arch" type="xml">
<tree string="SMS Traces" create="0">
<field name="mass_mailing_id"/>
<field name="sms_number"/>
<field name="state"/>
<field name="scheduled"/>
<field name="sent"/>
<field name="clicked"/>
<field name="bounced"/>
<field name="exception"/>
<field name="ignored"/>
<field name="message_id"/>
<field name="sent_datetime"/>
<field name="trace_status"/>
<field name="open_datetime" optional="hide"/>
<field name="reply_datetime" optional="hide"/>
<field name="links_click_datetime" optional="hide"/>
<field name="failure_type"/>
</tree>
</field>
@@ -60,16 +60,6 @@
<xpath expr="//field[@name='message_id']" position="attributes">
<attribute name="attrs">{'invisible': [('trace_type', '!=', 'mail')]}</attribute>
</xpath>
<xpath expr="//field[@name='opened']" position="attributes">
<attribute name="attrs">
{'invisible': ['|', ('opened', '=', False), ('trace_type', '=', 'sms')]}
</attribute>
</xpath>
<xpath expr="//field[@name='replied']" position="attributes">
<attribute name="attrs">
{'invisible': ['|', ('replied', '=', False), ('trace_type', '=', 'sms')]}
</attribute>
</xpath>
<xpath expr="//field[@name='email']" position="after">
<field name="sms_number" attrs="{'invisible': [('trace_type', '!=', 'sms')]}"/>
</xpath>
@@ -87,13 +77,13 @@
<field name="arch" type="xml">
<form string="SMS Trace" create="0">
<header>
<field name="state" widget="statusbar"/>
<field name="trace_status" widget="statusbar"/>
</header>
<sheet>
<div class="alert alert-info text-center" attrs="{'invisible': [('exception', '=', False)]}" role="alert">
<div class="alert alert-info text-center" attrs="{'invisible': [('trace_status', '!=', 'error')]}" role="alert">
<strong>This SMS could not be sent.</strong>
</div>
<div class="alert alert-info text-center" attrs="{'invisible': [('bounced', '=', False)]}" role="alert">
<div class="alert alert-info text-center" attrs="{'invisible': [('trace_status', '!=', 'bounce')]}" role="alert">
<strong>This number appears to be invalid.</strong>
</div>
<group>
@@ -106,13 +96,9 @@
<field name="res_id" groups="base.group_no_one"/>
</group>
<group>
<field name="scheduled"/>
<field name="sent"/>
<field name="clicked"/>
<field name="bounced"/>
<field name="exception"/>
<field name="failure_type" attrs="{'invisible': [('exception', '=', False)]}"/>
<field name="ignored"/>
<field name="sent_datetime"/>
<field name="links_click_datetime"/>
<field name="failure_type" attrs="{'invisible': [('trace_status', '!=', 'error')]}"/>
</group>
</group>
</sheet>
@@ -36,10 +36,10 @@ class SMSComposer(models.TransientModel):
}
if sms_values['state'] == 'error':
trace_values['failure_type'] = sms_values['error_code']
trace_values['exception'] = fields.Datetime.now()
trace_values['trace_status'] = 'error'
elif sms_values['state'] == 'canceled':
trace_values['failure_type'] = sms_values['error_code']
trace_values['ignored'] = fields.Datetime.now()
trace_values['trace_status'] = 'cancel'
else:
if self.mass_sms_allow_unsubscribe:
sms_values['body'] = '%s\n%s' % (sms_values['body'] or '', _('STOP SMS : %s', self._get_unsubscribe_url(record.id, trace_code, sms_values['number'])))
@@ -45,25 +45,25 @@ class TestMassMailing(TestMailFullCommon):
recipient_info = {
'email': recipient.email_normalized,
'content': 'Hello %s' % recipient.name}
# opt-out: ignored (cancel mail)
# opt-out: cancel (cancel mail)
if recipient in recipients[1] | recipients[2]:
recipient_info['state'] = 'ignored'
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_optout"
# blacklisted: ignored (cancel mail)
# blacklisted: cancel (cancel mail)
elif recipient in recipients[3] | recipients[4]:
recipient_info['state'] = 'ignored'
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_bl"
# duplicates: ignored (cancel mail)
# duplicates: cancel (cancel mail)
elif recipient == recipient_dup_1:
recipient_info['state'] = 'ignored'
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "mail_dup"
# void: error (failed mail)
elif recipient == recipient_void_1:
recipient_info['state'] = 'ignored'
recipient_info['trace_status'] = 'cancel'
recipient_info['failure_type'] = "mail_email_missing"
# falsy: error (failed mail)
elif recipient == recipient_falsy_1:
recipient_info['state'] = 'ignored'
recipient_info['trace_status'] = "cancel"
recipient_info['failure_type'] = "RECIPIENT"
recipient_info['email'] = recipient.email_from # normalized is False but email should be falsymail
else:
@@ -118,4 +118,4 @@ class TestMassMailing(TestMailFullCommon):
# sent: 13, 2 bl, 2 opt-out, 3 invalid -> 6 remaining
# ignored: 2 bl + 2 optout + 2 invalid + 1 duplicate; failed: 0
self.assertMailingStatistics(mailing, expected=13, delivered=6, sent=6, ignored=7, failed=0)
self.assertMailingStatistics(mailing, expected=13, delivered=6, sent=6, canceled=7, failed=0)
+14 -14
View File
@@ -169,28 +169,28 @@ class TestMassSMSInternals(TestMassSMSCommon):
# duplicates
self.assertSMSTraces(
[{'partner': new_record_2.customer_id, 'number': self.records_numbers[0],
'content': 'Dear %s this is a mass SMS.' % new_record_2.display_name, 'state': 'ignored',
'content': 'Dear %s this is a mass SMS.' % new_record_2.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_duplicate'}],
self.mailing, new_record_2,
)
# blacklist
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': phone_validation.phone_format(bl_record_1.phone_nbr, 'BE', '32', force_format='E164'),
'content': 'Dear %s this is a mass SMS.' % bl_record_1.display_name, 'state': 'ignored',
'content': 'Dear %s this is a mass SMS.' % bl_record_1.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_blacklist'}],
self.mailing, bl_record_1,
)
# missing number
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': False,
'content': 'Dear %s this is a mass SMS.' % void_record.display_name, 'state': 'ignored',
'content': 'Dear %s this is a mass SMS.' % void_record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_number_missing'}],
self.mailing, void_record,
)
# wrong values
self.assertSMSTraces(
[{'partner': self.env['res.partner'], 'number': record.phone_nbr,
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'state': 'ignored',
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_number_format'}
for record in falsy_record_1 + falsy_record_2],
self.mailing, falsy_record_1 + falsy_record_2,
@@ -220,7 +220,7 @@ class TestMassSMSInternals(TestMassSMSCommon):
# new failed traces generated for duplicates
self.assertSMSTraces(
[{'partner': record.customer_id, 'number': self.records_numbers[i],
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'state': 'ignored',
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'trace_status': 'cancel',
'failure_type': 'sms_duplicate'}
for i, record in enumerate(self.records[:5])],
self.mailing, self.records[:5],
@@ -282,7 +282,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': self.records_numbers[i],
'state': 'sent',
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for i, record in enumerate(self.records)],
mailing, self.records,
@@ -317,7 +317,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': record.customer_id,
'number': record.customer_id.phone_sanitized,
'state': 'sent',
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % record.display_name
} for record in records],
mailing, records,
@@ -342,7 +342,7 @@ class TestMassSMS(TestMassSMSCommon):
self.assertSMSTraces(
[{'partner': new_record.customer_id,
'number': new_record.customer_id.phone_sanitized,
'state': 'sent',
'trace_status': 'sent',
'content': 'Dear %s this is a mass SMS with two links' % new_record.display_name
}],
mailing, new_record,
@@ -395,11 +395,11 @@ class TestMassSMS(TestMassSMSCommon):
mailing.action_send_sms()
self.assertSMSTraces(
[{'number': '+32456000000', 'state': 'ignored', 'failure_type': 'sms_optout'},
{'number': '+32456000101', 'state': 'ignored', 'failure_type': 'sms_optout'},
{'number': '+32456000202', 'state': 'sent'},
{'number': '+32456000303', 'state': 'sent'},
{'number': '+32456000404', 'state': 'ignored', 'failure_type': 'sms_blacklist'}],
[{'number': '+32456000000', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000101', 'trace_status': 'cancel', 'failure_type': 'sms_optout'},
{'number': '+32456000202', 'trace_status': 'sent'},
{'number': '+32456000303', 'trace_status': 'sent'},
{'number': '+32456000404', 'trace_status': 'cancel', 'failure_type': 'sms_blacklist'}],
mailing, recipients
)
self.assertEqual(mailing.ignored, 3)
self.assertEqual(mailing.canceled, 3)
@@ -3,11 +3,18 @@
import datetime
from odoo.addons.test_mass_mailing.tests import common
from odoo.tests.common import users
from freezegun import freeze_time
from unittest.mock import patch
from odoo.addons.mass_mailing.models.mail_thread import BLACKLIST_MAX_BOUNCED_LIMIT
from odoo.addons.test_mass_mailing.tests import common
from odoo.tests import tagged
from odoo.tests.common import users
from odoo.tools import mute_logger
from odoo.sql_db import Cursor
@tagged('mail_blacklist')
class TestAutoBlacklist(common.TestMassMailCommon):
@classmethod
@@ -42,15 +49,21 @@ class TestAutoBlacklist(common.TestMassMailCommon):
})
self._test_mailing_bounce_w_auto_bl({'bounced_partner': bounced_partners})
@mute_logger('odoo.addons.mail.models.mail_thread')
def _test_mailing_bounce_w_auto_bl(self, bounce_base_values):
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
target = self.env['mailing.test.blacklist'].browse(self.target_rec.ids)
# create bounced history of 4 statistics
traces = self.env['mailing.trace']
for idx in range(4):
new_mailing = mailing.copy()
self._create_bounce_trace(new_mailing, target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
self.gateway_mail_bounce(new_mailing, target, bounce_base_values)
new_dt = datetime.datetime.now() - datetime.timedelta(weeks=idx+2)
# Cursor.now() uses transaction's timestamp and not datetime lib -> freeze_time
# is not sufficient
with freeze_time(new_dt), patch.object(Cursor, 'now', lambda *args, **kwargs: new_dt):
traces += self._create_bounce_trace(new_mailing, target, dt=datetime.datetime.now() - datetime.timedelta(weeks=idx+2))
self.gateway_mail_bounce(new_mailing, target, bounce_base_values)
# mass mail record: ok, not blacklisted yet
mailing.action_put_in_queue()
@@ -78,6 +91,6 @@ class TestAutoBlacklist(common.TestMassMailCommon):
with self.mock_mail_gateway(mail_unlink_sent=False):
new_mailing._process_mass_mailing_queue()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'state': 'ignored', 'failure_type': 'mail_bl'}],
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
new_mailing, target, check_mail=True
)
@@ -48,22 +48,23 @@ class TestLinkTracker(common.TestMassMailCommon):
record = self.env['mailing.test.blacklist'].create({})
code = self.link.code
self.assertEqual(self.link.count, 1)
stat = self.env['mailing.trace'].create({
trace = self.env['mailing.trace'].create({
'mass_mailing_id': self.mailing_bl.id,
'model': record._name,
'res_id': record.id,
})
self.assertFalse(stat.opened)
self.assertFalse(stat.clicked)
self.assertEqual(trace.trace_status, 'outgoing')
self.assertFalse(trace.links_click_datetime)
# click from a new IP should create a new entry and update stat when provided
click = self.env['link.tracker.click'].sudo().add_click(
code,
ip='100.00.00.01',
country_code='BEL',
mailing_trace_id=stat.id
mailing_trace_id=trace.id
)
self.assertEqual(self.link.count, 2)
self.assertEqual(click.mass_mailing_id, self.mailing_bl)
self.assertTrue(stat.opened)
self.assertTrue(stat.clicked)
self.assertTrue(trace.trace_status, 'open')
self.assertTrue(trace.links_click_datetime)
self.assertEqual(trace.links_click_ids, click)
+14 -14
View File
@@ -59,11 +59,11 @@ class TestMassMailing(TestMassMailCommon):
traces = self.env['mailing.trace'].search([('model', '=', customers._name), ('res_id', 'in', customers.ids)])
self.assertEqual(len(traces), 3)
customer0_trace = traces.filtered(lambda t: t.res_id == customers[0].id)
self.assertEqual(customer0_trace.state, 'replied')
self.assertEqual(customer0_trace.trace_status, 'reply')
customer1_trace = traces.filtered(lambda t: t.res_id == customers[1].id)
self.assertEqual(customer1_trace.state, 'replied')
self.assertEqual(customer1_trace.trace_status, 'reply')
customer2_trace = traces.filtered(lambda t: t.res_id == customers[2].id)
self.assertEqual(customer2_trace.state, 'sent')
self.assertEqual(customer2_trace.trace_status, 'sent')
# check mailing statistics
self.assertEqual(mailing.sent, 3)
@@ -245,11 +245,11 @@ class TestMassMailing(TestMassMailCommon):
[{'email': 'test.record.00@test.example.com'},
{'email': 'test.record.01@test.example.com'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com', 'state': 'ignored', 'failure_type': 'mail_bl'},
{'email': 'test.record.04@test.example.com', 'state': 'ignored', 'failure_type': 'mail_bl'}],
{'email': 'test.record.03@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'},
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
)
self.assertEqual(mailing.ignored, 2)
self.assertEqual(mailing.canceled, 2)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -271,14 +271,14 @@ class TestMassMailing(TestMassMailCommon):
mailing._process_mass_mailing_queue()
self.assertMailTraces(
[{'email': 'test.record.00@test.example.com', 'state': 'ignored', 'failure_type': 'mail_optout'},
{'email': 'test.record.01@test.example.com', 'state': 'ignored', 'failure_type': 'mail_optout'},
[{'email': 'test.record.00@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.01@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'},
{'email': 'test.record.02@test.example.com'},
{'email': 'test.record.03@test.example.com'},
{'email': 'test.record.04@test.example.com', 'state': 'ignored', 'failure_type': 'mail_bl'}],
{'email': 'test.record.04@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
mailing, recipients, check_mail=True
)
self.assertEqual(mailing.ignored, 3)
self.assertEqual(mailing.canceled, 3)
@users('user_marketing')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -331,13 +331,13 @@ class TestMassMailing(TestMassMailCommon):
mailing._process_mass_mailing_queue()
self.assertMailTraces(
[{'email': 'test@test.example.com', 'state': 'sent'},
{'email': 'test@test.example.com', 'state': 'ignored', 'failure_type': 'mail_dup'},
[{'email': 'test@test.example.com', 'trace_status': 'sent'},
{'email': 'test@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_dup'},
{'email': 'test3@test.example.com'},
{'email': 'test4@test.example.com'},
{'email': 'test5@test.example.com', 'state': 'ignored', 'failure_type': 'mail_optout'}],
{'email': 'test5@test.example.com', 'trace_status': 'cancel', 'failure_type': 'mail_optout'}],
mailing,
mailing_contact_1 + mailing_contact_2 + mailing_contact_3 + mailing_contact_4 + mailing_contact_5,
check_mail=True
)
self.assertEqual(mailing.ignored, 2)
self.assertEqual(mailing.canceled, 2)
@@ -50,8 +50,8 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +101 compared to local (1616)
with self.assertQueryCount(__system__=1717, marketing=1717):
# runbot needs +101 compared to local (1567)
with self.assertQueryCount(__system__=1668, marketing=1668):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
@@ -90,8 +90,8 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
# runbot needs +125 compared to local (1882)
with self.assertQueryCount(__system__=2007, marketing=2007):
# runbot needs +125 compared to local (1833)
with self.assertQueryCount(__system__=1958, marketing=1958):
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)