[MERGE][IMP] mass_mailing[_sms]: improve utm campaign a/b testing integration

PURPOSE

Improve the Marketing Campaign and Mass SMS form/tree views.
Notably to display clearly what's happening with the A/B testing.

SPECIFICATIONS

- Re-arrange and improve the "mailings" embed tree view in the Marketing
  Campaign form, for both mail and sms
- Add A/B testing information in Marketing Campaign form views
- Reword some labels
- Do not override existing campaign when enabling A/B testing

See underlying commits for details.

LINKS

PR #77768
Task 2653806

Related: odoo/enterprise#21421
Signed-off-by: Warnon Aurélien (awa) <awa@odoo.com>
This commit is contained in:
Odoo's Mergebot
2022-08-12 11:28:32 +02:00
committed by GitHub
9 changed files with 112 additions and 100 deletions
+11 -11
View File
@@ -461,13 +461,7 @@ class MassMailing(models.Model):
at = fields.Datetime.from_string(values['ab_testing_schedule_datetime'])
ab_testing_cron._trigger(at=at)
mailings = super().create(vals_list)
campaign_vals = [
mailing._get_default_ab_testing_campaign_values()
for mailing in mailings
if mailing.ab_testing_enabled and not mailing.campaign_id
]
self.env['utm.campaign'].create(campaign_vals)
mailings._create_ab_testing_utm_campaigns()
mailings._fix_attachment_ownership()
return mailings
@@ -475,15 +469,13 @@ class MassMailing(models.Model):
def write(self, values):
if values.get('body_html'):
values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
# When ab_testing_enabled is checked we create a campaign if there is none set.
if values.get('ab_testing_enabled') and not values.get('campaign_id'):
# Compute the values of the A/B test campaign based on the first mailing
values['campaign_id'] = self.env['utm.campaign'].create(self[0]._get_default_ab_testing_campaign_values(values)).id
# If ab_testing is already enabled on a mailing and the campaign is removed, we raise a ValidationError
if values.get('campaign_id') is False and any(mailing.ab_testing_enabled for mailing in self) and 'ab_testing_enabled' not in values:
raise ValidationError(_("A campaign should be set when A/B test is enabled"))
result = super(MassMailing, self).write(values)
if values.get('ab_testing_enabled'):
self._create_ab_testing_utm_campaigns()
self._fix_attachment_ownership()
if any(self.mapped('ab_testing_schedule_datetime')):
@@ -493,6 +485,14 @@ class MassMailing(models.Model):
return result
def _create_ab_testing_utm_campaigns(self):
""" Creates the A/B test campaigns for the mailings that do not have campaign set already """
campaign_vals = [
mailing._get_default_ab_testing_campaign_values()
for mailing in self.filtered(lambda mailing: mailing.ab_testing_enabled and not mailing.campaign_id)
]
return self.env['utm.campaign'].create(campaign_vals)
def _fix_attachment_ownership(self):
for record in self:
record.attachment_ids.write({'res_model': record._name, 'res_id': record.id})
+3 -3
View File
@@ -22,7 +22,7 @@ class UtmCampaign(models.Model):
# A/B Testing
ab_testing_mailings_count = fields.Integer("A/B Test Mailings #", compute="_compute_mailing_mail_count")
ab_testing_completed = fields.Boolean("A/B Testing Campaign Finished")
ab_testing_completed = fields.Boolean("A/B Testing Campaign Finished", copy=False)
ab_testing_schedule_datetime = fields.Datetime('Send Final On',
default=lambda self: fields.Datetime.now() + relativedelta(days=1),
help="Date that will be used to know when to determine and send the winner mailing")
@@ -66,8 +66,8 @@ class UtmCampaign(models.Model):
mapped_data = dict()
ab_testing_mapped_data = dict()
for campaign in self:
campaign.mailing_mail_count = sum(mapped_data.get(campaign.id, []))
campaign.ab_testing_mailings_count = sum(ab_testing_mapped_data.get(campaign.id, []))
campaign.mailing_mail_count = sum(mapped_data.get(campaign._origin.id or campaign.id, []))
campaign.ab_testing_mailings_count = sum(ab_testing_mapped_data.get(campaign._origin.id or campaign.id, []))
@api.constrains('ab_testing_total_pc', 'ab_testing_completed')
def _check_ab_testing_total_pc(self):
@@ -215,23 +215,6 @@
font-size: 90%;
}
@media only screen and (min-width: 1200px) {
.o_utm_campaign_mass_mailing_substats {
padding-right: 210px;
div {
margin-left: 50px;
}
}
}
@media only screen and (min-width: 768px) and (max-width: 1200px) {
.o_utm_campaign_mass_mailing_substats {
padding-right: 180px;
div {
margin-left: 30px;
}
}
}
.o_mailing_contact_import_list {
min-height: 150px;
@@ -1,9 +1,4 @@
@include media-breakpoint-down(md) {
.o_utm_campaign_mass_mailing_substats {
width: 80%;
margin: 0 auto;
justify-content: space-between !important;
}
// For field to occupy full width in small devices. We need '!important'
// here because of the 'w-auto' class used in the view.
.o_form_editable div[name="mailing_filter_id"] {
@@ -103,11 +103,35 @@ class TestMailingABTesting(MassMailCommon):
})
ab_mailing.invalidate_recordset()
# Check if the campaign is correclty created and the values set on the mailing are still the same
# Check if the campaign is correctly created and the values set on the mailing are still the same
self.assertTrue(ab_mailing.campaign_id, "A campaign id is present for the A/B test mailing")
self.assertEqual(ab_mailing.ab_testing_winner_selection, 'manual', "The selection winner has been propagated correctly")
self.assertEqual(ab_mailing.ab_testing_schedule_datetime, schedule_datetime, "The schedule date has been propagated correctly")
# Check that while enabling the A/B testing, if campaign is already set, new one should not be created
created_mailing_campaign = ab_mailing.campaign_id
ab_mailing.ab_testing_enabled = False
ab_mailing.ab_testing_enabled = True
self.assertEqual(ab_mailing.campaign_id, created_mailing_campaign, "No new campaign should have been created")
# Check that while enabling the A/B testing, if user manually selects a campaign, it should be saved
# rather than being replaced with the automatically created one
ab_mailing.write({'ab_testing_enabled': False, 'campaign_id': False})
ab_mailing.write({'ab_testing_enabled': True, 'campaign_id': created_mailing_campaign})
self.assertEqual(ab_mailing.campaign_id, created_mailing_campaign, "No new campaign should have been created")
ab_mailing_2 = self.env['mailing.mailing'].create({
'subject': 'A/B Testing V2',
'contact_list_ids': self.mailing_list.ids,
})
ab_mailing_2.invalidate_recordset()
ab_mailing_2.ab_testing_enabled = True
# Check if the campaign is correctly created with default values
self.assertTrue(ab_mailing.campaign_id, "A campaign id is present for the A/B test mailing")
self.assertTrue(ab_mailing.ab_testing_winner_selection, "The selection winner has been set to default value")
self.assertTrue(ab_mailing.ab_testing_schedule_datetime, "The schedule date has been set to default value")
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('user_marketing')
def test_mailing_ab_testing_manual_flow(self):
@@ -9,7 +9,7 @@
<field name="is_mailing_campaign_activated" invisible="1"/>
<button name="%(action_create_mass_mailings_from_campaign)d"
type="action" class="oe_highlight" attrs="{'invisible': [('is_mailing_campaign_activated', '=', False)]}"
groups="mass_mailing.group_mass_mailing_user" string="Send new Mailing"/>
groups="mass_mailing.group_mass_mailing_user" string="Send Mailing"/>
</xpath>
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button name="%(action_view_mass_mailings_from_campaign)d"
@@ -17,64 +17,44 @@
attrs="{'invisible': ['|', ('mailing_mail_count', '=', 0), ('is_mailing_campaign_activated', '=', False)]}"
groups="mass_mailing.group_mass_mailing_user">
<field name="mailing_mail_count" widget="statinfo" string="Mailings"/>
<field name="ab_testing_mailings_count" invisible="1"/>
</button>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Mailings" name="mailings"
attrs="{'invisible': ['|', ('mailing_mail_count', '=', 0), ('is_mailing_campaign_activated', '=', False)]}"
groups="mass_mailing.group_mass_mailing_user">
<field name="mailing_mail_ids" readonly="1" nolabel="1">
<field name="mailing_mail_ids" nolabel="1">
<tree>
<field name="subject"/>
<field name="sent_date"/>
<field name="state"/>
<field name="delivered"/>
<field name="opened"/>
<field name="replied"/>
<field name="bounced"/>
<field name="calendar_date" string="Date"/>
<field name="subject" attrs="{'readonly': [('state', 'in', ('sending', 'done'))]}"/>
<field name="mailing_model_id" string="Recipients" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="ab_testing_enabled" string="A/B Test"
groups="mass_mailing.group_mass_mailing_campaign"
attrs="{'column_invisible': [('parent.ab_testing_mailings_count', '=', 0)]}"/>
<field name="campaign_id" string="Campaign" groups="mass_mailing.group_mass_mailing_campaign" optional="hide"/>
<field name="received_ratio" string="Delivered (%)" avg="Average of Delivered"/>
<field name="opened_ratio" string="Opened (%)" avg="Average of Opened"/>
<field name="bounced_ratio" string="Bounced (%)" optional="hide" avg="Average of Bounced"/>
<field name="clicks_ratio" string="Clicked (%)" avg="Average of Clicked"/>
<field name="replied_ratio" string="Replied (%)" avg="Average of Replied"/>
<field name="state" decoration-info="state in ('draft', 'in_queue')" decoration-success="state in ('sending', 'done')" widget="badge"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
<div class="o_utm_campaign_mass_mailing_substats d-flex justify-content-end align-items-center">
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="received_ratio" string="Delivered" class="m-0"/>
<div class="m-0">
<span class="text-end">
<field name="received_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="opened_ratio" string="Opened" class="m-0"/>
<div class="m-0">
<span class="text-end">
<field name="opened_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="replied_ratio" string="Replied" class="m-0"/>
<div class="m-0">
<span class="text-end">
<field name="replied_ratio"/>
<span>%</span>
</span>
</div>
</div>
<div class="d-flex justify-content-end align-items-center flex-column">
<label for="bounced_ratio" string="Bounced" class="m-0"/>
<div class="m-0">
<span class="text-end">
<field name="bounced_ratio"/>
<span>%</span>
</span>
</div>
</div>
</div>
</page>
</xpath>
<xpath expr="//notebook" position="after">
<group name="ab_test_group" groups="mass_mailing.group_mass_mailing_campaign" attrs="{'invisible': [('ab_testing_mailings_count', '=', 0)]}">
<group string="A/B Test">
<field name="ab_testing_completed" invisible="1"/>
<field name="ab_testing_winner_selection" attrs="{'readonly': [('ab_testing_completed', '=', True)]}"/>
<field name="ab_testing_schedule_datetime"
attrs="{'invisible': [('ab_testing_winner_selection', '=', 'manual')], 'readonly': [('ab_testing_completed', '=', True)]}"/>
</group>
</group>
</xpath>
</field>
</record>
+19 -1
View File
@@ -18,6 +18,7 @@ class UtmCampaign(models.Model):
groups="mass_mailing.group_mass_mailing_user")
# A/B Testing
ab_testing_mailings_sms_count = fields.Integer("A/B Test Mailings SMS #", compute="_compute_mailing_sms_count")
ab_testing_sms_winner_selection = fields.Selection([
('manual', 'Manual'),
('clicks_ratio', 'Highest Click Rate')], string="SMS Winner Selection", default="clicks_ratio")
@@ -32,8 +33,25 @@ class UtmCampaign(models.Model):
@api.depends('mailing_sms_ids')
def _compute_mailing_sms_count(self):
if self.ids:
mailing_sms_data = self.env['mailing.mailing'].read_group(
[('campaign_id', 'in', self.ids), ('mailing_type', '=', 'sms')],
['campaign_id', 'ab_testing_enabled'],
['campaign_id', 'ab_testing_enabled'],
lazy=False,
)
ab_testing_mapped_sms_data = {}
mapped_sms_data = {}
for data in mailing_sms_data:
if data['ab_testing_enabled']:
ab_testing_mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
mapped_sms_data.setdefault(data['campaign_id'][0], []).append(data['__count'])
else:
mapped_sms_data = dict()
ab_testing_mapped_sms_data = dict()
for campaign in self:
campaign.mailing_sms_count = len(campaign.mailing_sms_ids)
campaign.mailing_sms_count = sum(mapped_sms_data.get(campaign._origin.id or campaign.id, []))
campaign.ab_testing_mailings_sms_count = sum(ab_testing_mapped_sms_data.get(campaign._origin.id or campaign.id, []))
def action_create_mass_sms(self):
action = self.env["ir.actions.actions"]._for_xml_id("mass_mailing.action_create_mass_mailings_from_campaign")
@@ -233,17 +233,17 @@
<field name="priority">20</field>
<field name="arch" type="xml">
<tree string="SMS Marketing" sample="1" decoration-info="state == 'draft'">
<field name="subject"/>
<field name="calendar_date" string="Date"/>
<field name="subject" string="Title"/>
<field name="mailing_type" invisible="1"/>
<field name="mailing_model_id" string="Recipients"/>
<field name="mailing_model_id" string="Recipients" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="schedule_date" string="Scheduled" widget="remaining_days"/>
<field name="sent_date" widget="date"/>
<field name="state" decoration-info="state == 'draft' or state == 'in_queue'" decoration-success="state == 'sending' or state == 'done'" widget="badge"/>
<field name="campaign_id" string="Campaign" groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="sent"/>
<field name="clicked"/>
<field name="bounced"/>
<field name="campaign_id" string="Campaign" groups="mass_mailing.group_mass_mailing_campaign" optional="hide"/>
<field name="ab_testing_enabled" string="A/B Test" groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="sent" sum="Total Sent"/>
<field name="clicked" string="Clicked (%)" widget="progressbar" avg="Average of Clicked"/>
<field name="bounced" string="Bounced (%)" widget="progressbar" optional="hide" avg="Average of Bounced"/>
<field name="state" decoration-info="state in ('draft', 'in_queue')" decoration-success="state in ('sending', 'done')" widget="badge"/>
</tree>
</field>
</record>
@@ -17,6 +17,7 @@
attrs="{'invisible': ['|', ('mailing_sms_count', '=', 0), ('is_mailing_campaign_activated', '=', False)]}"
icon="fa-mobile" groups="mass_mailing.group_mass_mailing_user">
<field name="mailing_sms_count" widget="statinfo" string="SMS"/>
<field name="ab_testing_mailings_sms_count" invisible="1"/>
</button>
</xpath>
<xpath expr="//notebook" position="inside">
@@ -24,19 +25,30 @@
attrs="{'invisible': ['|', ('mailing_sms_count', '=', 0), ('is_mailing_campaign_activated', '=', False)]}"
groups="mass_mailing.group_mass_mailing_user">
<group>
<field name="mailing_sms_ids" readonly="1" nolabel="1">
<field name="mailing_sms_ids" nolabel="1">
<tree>
<field name="subject"/>
<field name="sent_date"/>
<field name="state"/>
<field name="bounced"/>
<field name="delivered"/>
<field name="calendar_date" string="Date"/>
<field name="subject" string="Title"/>
<field name="mailing_type" invisible="1"/>
<field name="mailing_model_id" string="Recipients" optional="hide"/>
<field name="user_id" widget="many2one_avatar_user"/>
<field name="campaign_id" string="Campaign" groups="mass_mailing.group_mass_mailing_campaign" optional="hide"/>
<field name="ab_testing_enabled" string="A/B Test"
groups="mass_mailing.group_mass_mailing_campaign"
attrs="{'column_invisible': [('parent.ab_testing_mailings_sms_count', '=', 0)]}"/>
<field name="sent" sum="Total Sent"/>
<field name="clicked" string="Clicked (%)" avg="Average of Clicked"/>
<field name="bounced" string="Bounced (%)" optional="hide" avg="Average of Bounced"/>
<field name="state" decoration-info="state in ('draft', 'in_queue')" decoration-success="state in ('sending', 'done')" widget="badge"/>
<button name="action_duplicate" type="object" string="Duplicate"/>
</tree>
</field>
</group>
</page>
</xpath>
<xpath expr="//group[@name='ab_test_group']" position="attributes">
<attribute name="attrs">{'invisible': [('ab_testing_mailings_sms_count', '=', 0), ('ab_testing_mailings_count', '=', 0)]}</attribute>
</xpath>
</field>
</record>