Files
odoo_source/addons/mass_mailing/models/utm.py
T
Thibault Delavallée bbf4783ac6 [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
2021-08-18 13:38:08 +00:00

99 lines
3.9 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class UtmCampaign(models.Model):
_inherit = 'utm.campaign'
mailing_mail_ids = fields.One2many(
'mailing.mailing', 'campaign_id',
domain=[('mailing_type', '=', 'mail')],
string='Mass Mailings')
mailing_mail_count = fields.Integer('Number of Mass Mailing', compute="_compute_mailing_mail_count")
# stat fields
received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio')
opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio')
replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio')
bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio')
@api.depends('mailing_mail_ids')
def _compute_mailing_mail_count(self):
if self.ids:
mailing_data = self.env['mailing.mailing'].read_group(
[('campaign_id', 'in', self.ids)],
['campaign_id'],
['campaign_id']
)
mapped_data = {m['campaign_id'][0]: m['campaign_id_count'] for m in mailing_data}
else:
mapped_data = dict()
for campaign in self:
campaign.mailing_mail_count = mapped_data.get(campaign.id, 0)
def _compute_statistics(self):
""" Compute statistics of the mass mailing campaign """
default_vals = {
'received_ratio': 0,
'opened_ratio': 0,
'replied_ratio': 0,
'bounced_ratio': 0
}
if not self.ids:
self.update(default_vals)
return
self.env.cr.execute("""
SELECT
c.id as campaign_id,
COUNT(s.id) AS expected,
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
utm_campaign c
ON (c.id = s.campaign_id)
WHERE
c.id IN %s
GROUP BY
c.id
""", (tuple(self.ids), ))
all_stats = self.env.cr.dictfetchall()
stats_per_campaign = {
stats['campaign_id']: stats
for stats in all_stats
}
for campaign in self:
stats = stats_per_campaign.get(campaign.id)
if not stats:
vals = default_vals
else:
total = (stats['expected'] - stats['cancel']) or 1
delivered = stats['sent'] - stats['bounce']
vals = {
'received_ratio': 100.0 * delivered / 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)
def _get_mailing_recipients(self, model=None):
"""Return the recipients of a mailing campaign. This is based on the statistics
build for each mailing. """
res = dict.fromkeys(self.ids, {})
for campaign in self:
domain = [('campaign_id', '=', campaign.id)]
if model:
domain += [('model', '=', model)]
res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id'))
return res