[IMP] mass_mailing: improve A/B testing usability

PURPOSE

Provide various fixes and UX improvements to ease usage of Emails and
SMS marketing applications.

SPECIFICATIONS

- Add a compute field ab_testing_sms_count to compute the counts related to A/B
  testing SMS.

- Add an alert message for A/B testing when no mail is sent in the form view with the
  chatter.

-  Add "(final)" to the source name of the final and winner A/B test mail.

-  The buttons are now correctly display based on the mailing_type
(the auto mode can now send a winner manually)

- Change the copy override of `utm.source.mixin` to consider the name param of
  default arg. when getting a unique name for the record.

Task-2713198

closes odoo/odoo#88997

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2023-07-17 12:05:49 +02:00
committed by GitHub
7 changed files with 98 additions and 75 deletions
@@ -108,13 +108,16 @@
<data noupdate="0">
<template id="ab_testing_description" name="Mass Mailing: A/B Test Description">
<div name="ab_testing_description" class="mb-2">
<t t-if="mailing.state == 'done' and mailing.sent == 0 and mailing.failed == 0 and mailing.canceled == 0">
<span>There wasn't enough recipients left for this mailing</span>
</t>
<t t-if="mailing.ab_testing_completed">
<p t-if="mailing.ab_testing_is_winner_mailing">
This <t t-out="mailing.mailing_type_description"/> is the winner of the A/B testing campaign and has been sent to all remaining recipients.
</p>
<p t-else="">The winner has already been sent. Use <b>Compare Version</b> to get an overview of this A/B testing campaign.</p>
</t>
<t t-elif="mailing.ab_testing_mailings_count >= 2">
<t t-elif="ab_testing_count >= 2">
<p>
A sample of <b><t t-out="mailing.ab_testing_pc"/>% of recipients</b> will receive this version.<br/>
<t t-if="total_ab_testing_pc > 100 and mailing.active">
+10
View File
@@ -203,6 +203,7 @@ class MassMailing(models.Model):
related="campaign_id.ab_testing_winner_selection", readonly=False,
default="opened_ratio",
copy=True)
is_ab_test_sent = fields.Boolean(compute="_compute_is_ab_test_sent")
kpi_mail_required = fields.Boolean('KPI mail required', copy=False)
# statistics data
mailing_trace_ids = fields.One2many('mailing.trace', 'mass_mailing_id', string='Emails Statistics')
@@ -472,6 +473,13 @@ class MassMailing(models.Model):
mailing._get_ab_testing_description_values()
)
@api.depends('campaign_id.mailing_mail_ids.state')
def _compute_is_ab_test_sent(self):
for rec in self:
ab_testing_mailings = rec._get_ab_testing_siblings_mailings()
selected_mailings = ab_testing_mailings.filtered(lambda m: m.state == 'done')
rec.is_ab_test_sent = bool(selected_mailings)
def _get_ab_testing_description_modifying_fields(self):
return ['ab_testing_enabled', 'ab_testing_pc', 'ab_testing_schedule_datetime', 'ab_testing_winner_selection', 'campaign_id']
@@ -853,6 +861,7 @@ class MassMailing(models.Model):
raise ValueError(_("A/B test option has not been enabled"))
final_mailing = self.copy({
'ab_testing_pc': 100,
'name': _(" %(subject)s (final)", subject=self.name) # Add suffix on name to show it's the final mailing
})
self.campaign_id.ab_testing_winner_mailing_id = final_mailing
final_mailing.action_launch()
@@ -868,6 +877,7 @@ class MassMailing(models.Model):
self.ensure_one()
return {
'mailing': self,
'ab_testing_count': self.ab_testing_mailings_count,
'ab_testing_winner_selection_description': self._get_ab_testing_winner_selection()['description'],
'total_ab_testing_pc': sum([
mailing.ab_testing_pc for mailing in self._get_ab_testing_siblings_mailings()
@@ -48,6 +48,11 @@
width: 135px;
}
}
.o_notebook .tab-content .tab-pane .alert {
margin-left: -$o-sheet-cancel-hpadding;
margin-right: -$o-sheet-cancel-hpadding;
}
}
.o_form_view.o_mass_mailing_mailing_form .o_form_renderer {
@@ -178,15 +183,11 @@
}
.o_mass_mailing_mailing_form.o_mass_mailing_form_full_width .alert.alert-info:not(.o_invisible_modifier) {
margin: 0 0 0 12px;
flex: 1;
padding: unset;
padding: 0.5rem 0.5rem;
border-left-width: 1px;
border-left-color: #dee2e6;
> div {
min-height: 100%;
padding: 6px;
}
}
.o_form_view.o_xxl_form_view .o_mass_mailing_mailing_form {
@@ -72,61 +72,63 @@
<button name="action_cancel" type="object" attrs="{'invisible': [('state', '!=', 'in_queue')]}" class="btn-secondary" string="Cancel" data-hotkey="z"/>
<button name="action_retry_failed" type="object" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" class="oe_highlight" string="Retry" data-hotkey="y"/>
<div class="alert alert-info mb-2 text-center" role="alert"
attrs="{'invisible': ['&amp;','&amp;','&amp;','&amp;','&amp;',('state', 'not in', ['in_queue', 'done']),('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_canceled"
type="object">
<strong>
<field name="canceled" class="oe_inline me-2"/>
<span name="canceled_text">emails have been canceled and will not be sent.</span>
</strong>
</button>
</div>
<div class="o_mails_scheduled" attrs="{'invisible': [('scheduled', '=', 0)]}">
<button class="btn-link py-0"
name="action_view_traces_scheduled"
type="object">
<strong>
<field name="scheduled" class="oe_inline me-2"/>
<span name="scheduled_text">emails are in queue and will be sent soon.</span>
</strong>
</button>
</div>
<div class="o_mails_sent" attrs="{'invisible': ['&amp;', ('sent', '=', 0), ('state', 'in', ('draft', 'test', 'in_queue'))]}">
<button class="btn-link py-0"
name="action_view_traces_sent"
type="object">
<strong>
<field name="sent" class="oe_inline me-2"/>
<span name="sent">emails have been sent.</span>
</strong>
</button>
<strong class="d-block" attrs="{'invisible': ['|', ('mailing_type', '=', 'mail'), '|', ('ab_testing_enabled', '=', False), '|', ('state', '!=', 'done'), '|', ('sent', '!=', 0), '|', ('failed', '!=', 0), ('canceled', '!=', 0)]}">
<span name="ab_test_text">There wasn't enough recipients given to this mailing. </span>
</strong>
</div>
<div class="o_mails_failed" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}">
<button class="btn-link py-0"
name="action_view_traces_failed"
type="object">
<strong>
<field name="failed" class="oe_inline me-2"/>
<span name="failed_text">emails could not be sent.</span>
</strong>
</button>
</div>
<div class="o_mails_in_queue" attrs="{'invisible': [('state', '!=', 'in_queue')]}">
<strong>
<span name="next_departure_text">This mailing is scheduled for </span>
<field name="next_departure" class="oe_inline"/>.
</strong>
</div>
<div attrs="{'invisible': [('warning_message', '=', False)]}">
<strong><field name="warning_message"/></strong>
</div>
</div>
<field name="state" readonly="1" widget="statusbar"/>
</header>
<div class="alert alert-info mb-2 text-center" role="alert"
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_canceled"
type="object">
<strong>
<field name="canceled" class="oe_inline me-2"/>
<span name="canceled_text">emails have been canceled and will not be sent.</span>
</strong>
</button>
</div>
<div class="o_mails_scheduled" attrs="{'invisible': [('scheduled', '=', 0)]}">
<button class="btn-link py-0"
name="action_view_traces_scheduled"
type="object">
<strong>
<field name="scheduled" class="oe_inline me-2"/>
<span name="scheduled_text">emails are in queue and will be sent soon.</span>
</strong>
</button>
</div>
<div class="o_mails_sent" attrs="{'invisible': ['&amp;', ('sent', '=', 0), ('state', 'in', ('draft', 'test', 'in_queue'))]}">
<button class="btn-link py-0"
name="action_view_traces_sent"
type="object">
<strong>
<field name="sent" class="oe_inline me-2"/>
<span name="sent">emails have been sent.</span>
</strong>
</button>
</div>
<div class="o_mails_failed" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}">
<button class="btn-link py-0"
name="action_view_traces_failed"
type="object">
<strong>
<field name="failed" class="oe_inline me-2"/>
<span name="failed_text">emails could not be sent.</span>
</strong>
</button>
</div>
<div class="o_mails_in_queue" attrs="{'invisible': [('state', '!=', 'in_queue')]}">
<strong>
<span name="next_departure_text">This mailing is scheduled for </span>
<field name="next_departure" class="oe_inline"/>.
</strong>
</div>
<div attrs="{'invisible': [('warning_message', '=', False)]}">
<strong><field name="warning_message"/></strong>
</div>
</div>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_view_delivered"
@@ -274,22 +276,21 @@
attrs="{'required': [('ab_testing_enabled', '=', True), ('mailing_type', '=', 'mail')], 'invisible': ['|', ('ab_testing_enabled', '=', False), ('mailing_type', '!=', 'mail')], 'readonly': [('state', '!=', 'draft')]}"/>
<field name="ab_testing_schedule_datetime"
attrs="{'required': [('ab_testing_enabled', '=', True), ('ab_testing_winner_selection', '!=', 'manual')], 'readonly': ['|', ('ab_testing_enabled', '=', False), ('state', '!=', 'draft')], 'invisible': ['|', ('ab_testing_enabled', '=', False), ('ab_testing_winner_selection', '=', 'manual')]}"/>
<field name="is_ab_test_sent" invisible="1"/>
</group>
<div>
<field name="ab_testing_mailings_count" invisible="1"/>
<field name="ab_testing_completed" invisible="1"/>
<field name="ab_testing_description" nolabel="1"/>
<div attrs="{'invisible': ['|', ('ab_testing_mailings_count', '&lt;', 2), ('ab_testing_enabled', '=', False)]}">
<div id="mailing_form_ab_buttons" attrs="{'invisible': ['|', ('ab_testing_mailings_count', '&lt;', 2), ('ab_testing_enabled', '=', False)]}">
<button name="action_compare_versions" type="object" class="btn btn-link d-block">
<i class="fa fa-bar-chart"/> Compare Version
</button>
<button name="action_duplicate" type="object" class="btn btn-link d-block" attrs="{'invisible': [('ab_testing_completed', '=', True)]}">
<i class="fa fa-copy"/> Create an Alternative
</button>
<button name="action_send_winner_mailing" type="object" class="btn btn-link d-block" attrs="{'invisible': [('ab_testing_completed', '=', True)]}">
<i class="fa fa-envelope"/> <span name="ab_test_manual" attrs="{'invisible': [('ab_testing_winner_selection', '!=', 'manual')]}">
Send this version to remaining recipients
</span> <span name="ab_test_auto" attrs="{'invisible': [('ab_testing_winner_selection', '=', 'manual')]}">
<button name="action_send_winner_mailing" type="object" class="btn btn-link d-block" attrs="{'invisible': ['|', ('is_ab_test_sent', '!=', True), '|', ('ab_testing_completed', '=', True), ('ab_testing_winner_selection', '=', 'manual')]}">
<i class="fa fa-envelope"/><span name="ab_test_auto">
Send Winner Now
</span>
</button>
@@ -52,6 +52,7 @@ class Mailing(models.Model):
ab_testing_sms_winner_selection = fields.Selection(
related="campaign_id.ab_testing_sms_winner_selection",
default="clicks_ratio", readonly=False, copy=True)
ab_testing_mailings_sms_count = fields.Integer(related="campaign_id.ab_testing_mailings_sms_count")
@api.depends('mailing_type')
def _compute_medium_id(self):
@@ -337,6 +338,7 @@ class Mailing(models.Model):
values = super()._get_ab_testing_description_values()
if self.mailing_type == 'sms':
values.update({
'ab_testing_count': self.ab_testing_mailings_sms_count,
'ab_testing_winner_selection': self.ab_testing_sms_winner_selection,
})
return values
@@ -159,6 +159,9 @@
('mail_server_available', '=', False)], 'readonly': [('state', 'in', ('sending', 'done'))]}</attribute>
</xpath>
<!-- A/B Testing -->
<xpath expr="//field[@name='ab_testing_mailings_count']" position="after">
<field name="ab_testing_mailings_sms_count" invisible="1"/>
</xpath>
<xpath expr="//field[@name='ab_testing_winner_selection']" position="after">
<label for="ab_testing_sms_winner_selection" string="Winner Selection"
attrs="{'invisible': ['|', ('ab_testing_enabled', '=', False), ('mailing_type', '!=', 'sms')]}"/>
@@ -169,19 +172,21 @@
<field name="ab_testing_schedule_datetime"
attrs="{'required': [('ab_testing_enabled', '=', True), ('ab_testing_winner_selection', '!=', 'manual'), ('ab_testing_sms_winner_selection', '!=', 'manual')], 'readonly': ['|', ('ab_testing_enabled', '=', False), ('state', '!=', 'draft')], 'invisible': ['|', '|', ('ab_testing_enabled', '=', False), ('ab_testing_winner_selection', '=', 'manual'), ('ab_testing_sms_winner_selection', '=', 'manual')]}"/>
</xpath>
<xpath expr="//span[@name='ab_test_manual']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('ab_testing_winner_selection', '!=', 'manual'),
('ab_testing_sms_winner_selection', '!=', 'manual')]}</attribute>
<xpath expr="//div[@id='mailing_form_ab_buttons']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('ab_testing_enabled', '=', False), '&amp;',
('ab_testing_mailings_count', '&lt;', 2),
('ab_testing_mailings_sms_count', '&lt;', 2)]}</attribute>
</xpath>
<xpath expr="//span[@name='ab_test_auto']" position="attributes">
<attribute name="attrs">{'invisible': [('ab_testing_winner_selection', '=', 'manual'),
('ab_testing_sms_winner_selection', '=', 'manual')]}</attribute>
</xpath>
<xpath expr="//button[@name='action_select_as_winner']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('ab_testing_completed', '!=', False), '|',
<xpath expr="//button[@name='action_send_winner_mailing']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('is_ab_test_sent', '!=', True), '|', ('ab_testing_completed', '=', True), '|',
('ab_testing_winner_selection', '=', 'manual'),
('ab_testing_sms_winner_selection', '=', 'manual')]}</attribute>
</xpath>
<xpath expr="//button[@name='action_duplicate'][hasclass('btn-primary')]" position="attributes">
<attribute name="attrs">{'invisible': ['|', '|', ('ab_testing_enabled', '=', False),
('ab_testing_mailings_count', '&gt;=', 2),
('ab_testing_mailings_sms_count', '&gt;=', 2)]}</attribute>
</xpath>
</field>
</record>
+2 -1
View File
@@ -82,5 +82,6 @@ class UtmSourceMixin(models.AbstractModel):
def copy(self, default=None):
"""Increment the counter when duplicating the source."""
default = default or {}
default['name'] = self.env['utm.mixin']._get_unique_names(self._name, [self.name])[0]
default_name = default.get('name')
default['name'] = self.env['utm.mixin']._get_unique_names(self._name, [default_name or self.name])[0]
return super().copy(default)