utm_campaign.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from dateutil.relativedelta import relativedelta
  4. from odoo import api, fields, models, _
  5. from odoo.exceptions import ValidationError
  6. class UtmCampaign(models.Model):
  7. _inherit = 'utm.campaign'
  8. mailing_mail_ids = fields.One2many(
  9. 'mailing.mailing', 'campaign_id',
  10. domain=[('mailing_type', '=', 'mail')],
  11. string='Mass Mailings',
  12. groups="mass_mailing.group_mass_mailing_user")
  13. mailing_mail_count = fields.Integer('Number of Mass Mailing',
  14. compute="_compute_mailing_mail_count",
  15. groups="mass_mailing.group_mass_mailing_user")
  16. is_mailing_campaign_activated = fields.Boolean(compute="_compute_is_mailing_campaign_activated")
  17. # A/B Testing
  18. ab_testing_mailings_count = fields.Integer("A/B Test Mailings #", compute="_compute_mailing_mail_count")
  19. ab_testing_completed = fields.Boolean("A/B Testing Campaign Finished", copy=False)
  20. ab_testing_schedule_datetime = fields.Datetime('Send Final On',
  21. default=lambda self: fields.Datetime.now() + relativedelta(days=1),
  22. help="Date that will be used to know when to determine and send the winner mailing")
  23. ab_testing_total_pc = fields.Integer("Total A/B test percentage", compute="_compute_ab_testing_total_pc", store=True)
  24. ab_testing_winner_selection = fields.Selection([
  25. ('manual', 'Manual'),
  26. ('opened_ratio', 'Highest Open Rate'),
  27. ('clicks_ratio', 'Highest Click Rate'),
  28. ('replied_ratio', 'Highest Reply Rate')], string="Winner Selection", default="opened_ratio",
  29. help="Selection to determine the winner mailing that will be sent.")
  30. # stat fields
  31. received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio')
  32. opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio')
  33. replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio')
  34. bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio')
  35. @api.depends('mailing_mail_ids')
  36. def _compute_ab_testing_total_pc(self):
  37. for campaign in self:
  38. campaign.ab_testing_total_pc = sum([
  39. mailing.ab_testing_pc for mailing in campaign.mailing_mail_ids.filtered('ab_testing_enabled')
  40. ])
  41. @api.depends('mailing_mail_ids')
  42. def _compute_mailing_mail_count(self):
  43. if self.ids:
  44. mailing_data = self.env['mailing.mailing']._read_group(
  45. [('campaign_id', 'in', self.ids), ('mailing_type', '=', 'mail')],
  46. ['campaign_id', 'ab_testing_enabled'],
  47. ['campaign_id', 'ab_testing_enabled'],
  48. lazy=False,
  49. )
  50. ab_testing_mapped_data = {}
  51. mapped_data = {}
  52. for data in mailing_data:
  53. if data['ab_testing_enabled']:
  54. ab_testing_mapped_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
  55. mapped_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
  56. else:
  57. mapped_data = dict()
  58. ab_testing_mapped_data = dict()
  59. for campaign in self:
  60. campaign.mailing_mail_count = sum(mapped_data.get(campaign._origin.id or campaign.id, []))
  61. campaign.ab_testing_mailings_count = sum(ab_testing_mapped_data.get(campaign._origin.id or campaign.id, []))
  62. @api.constrains('ab_testing_total_pc', 'ab_testing_completed')
  63. def _check_ab_testing_total_pc(self):
  64. for campaign in self:
  65. if not campaign.ab_testing_completed and campaign.ab_testing_total_pc >= 100:
  66. raise ValidationError(_("The total percentage for an A/B testing campaign should be less than 100%"))
  67. def _compute_statistics(self):
  68. """ Compute statistics of the mass mailing campaign """
  69. default_vals = {
  70. 'received_ratio': 0,
  71. 'opened_ratio': 0,
  72. 'replied_ratio': 0,
  73. 'bounced_ratio': 0
  74. }
  75. if not self.ids:
  76. self.update(default_vals)
  77. return
  78. self.env.cr.execute("""
  79. SELECT
  80. c.id as campaign_id,
  81. COUNT(s.id) AS expected,
  82. COUNT(s.sent_datetime) AS sent,
  83. COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('sent', 'open', 'reply')) AS delivered,
  84. COUNT(s.trace_status) FILTER (WHERE s.trace_status in ('open', 'reply')) AS open,
  85. COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'reply') AS reply,
  86. COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'bounce') AS bounce,
  87. COUNT(s.trace_status) FILTER (WHERE s.trace_status = 'cancel') AS cancel
  88. FROM
  89. mailing_trace s
  90. RIGHT JOIN
  91. utm_campaign c
  92. ON (c.id = s.campaign_id)
  93. WHERE
  94. c.id IN %s
  95. GROUP BY
  96. c.id
  97. """, (tuple(self.ids), ))
  98. all_stats = self.env.cr.dictfetchall()
  99. stats_per_campaign = {
  100. stats['campaign_id']: stats
  101. for stats in all_stats
  102. }
  103. for campaign in self:
  104. stats = stats_per_campaign.get(campaign.id)
  105. if not stats:
  106. vals = default_vals
  107. else:
  108. total = (stats['expected'] - stats['cancel']) or 1
  109. delivered = stats['sent'] - stats['bounce']
  110. vals = {
  111. 'received_ratio': 100.0 * delivered / total,
  112. 'opened_ratio': 100.0 * stats['open'] / total,
  113. 'replied_ratio': 100.0 * stats['reply'] / total,
  114. 'bounced_ratio': 100.0 * stats['bounce'] / total
  115. }
  116. campaign.update(vals)
  117. def _compute_is_mailing_campaign_activated(self):
  118. self.is_mailing_campaign_activated = self.env.user.has_group('mass_mailing.group_mass_mailing_campaign')
  119. def _get_mailing_recipients(self, model=None):
  120. """Return the recipients of a mailing campaign. This is based on the statistics
  121. build for each mailing. """
  122. res = dict.fromkeys(self.ids, {})
  123. for campaign in self:
  124. domain = [('campaign_id', '=', campaign.id)]
  125. if model:
  126. domain += [('model', '=', model)]
  127. res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id'))
  128. return res
  129. @api.model
  130. def _cron_process_mass_mailing_ab_testing(self):
  131. """ Cron that manages A/B testing and sends a winner mailing computed based on
  132. the value set on the A/B testing campaign.
  133. In case there is no mailing sent for an A/B testing campaign we ignore this campaign
  134. """
  135. ab_testing_campaign = self.search([
  136. ('ab_testing_schedule_datetime', '<=', fields.Datetime.now()),
  137. ('ab_testing_winner_selection', '!=', 'manual'),
  138. ('ab_testing_completed', '=', False),
  139. ])
  140. for campaign in ab_testing_campaign:
  141. ab_testing_mailings = campaign.mailing_mail_ids.filtered(lambda m: m.ab_testing_enabled)
  142. if not ab_testing_mailings.filtered(lambda m: m.state == 'done'):
  143. continue
  144. ab_testing_mailings.action_send_winner_mailing()
  145. return ab_testing_campaign