[MERGE] mass_mailing(_sms): improve usability of mailing, subscriptions and lists
Purpose: Improve the general usability of Email Marketing based on what has been observed on Userbrain. Specification: 1) Provide instant feedback for small mailings and for onboardees. 2) Improve queue ribbon 3) Add subscription field in mailing contact 4) Remove system generated document 5) Improve widget domain view 6) Add buttons in mailing list to send email and sms from there. TaskId-2702607 -- I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr closes odoo/odoo#82107 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -224,6 +224,7 @@ class MassMailing(models.Model):
|
||||
clicks_ratio = fields.Float(compute="_compute_clicks_ratio", string="Number of Clicks")
|
||||
next_departure = fields.Datetime(compute="_compute_next_departure", string='Scheduled date')
|
||||
# UX
|
||||
next_departure_is_past = fields.Boolean(compute="_compute_next_departure")
|
||||
warning_message = fields.Char(
|
||||
'Warning Message', compute='_compute_warning_message',
|
||||
help='Warning message displayed in the mailing form view')
|
||||
@@ -336,6 +337,7 @@ class MassMailing(models.Model):
|
||||
row['bounced_ratio'] = float_round(100.0 * row['bounced'] / total, precision_digits=2)
|
||||
self.browse(row.pop('mailing_id')).update(row)
|
||||
|
||||
@api.depends('schedule_date', 'state')
|
||||
def _compute_next_departure(self):
|
||||
# Schedule_date should only be False if schedule_type = "now" or
|
||||
# mass_mailing is canceled.
|
||||
@@ -344,12 +346,19 @@ class MassMailing(models.Model):
|
||||
# execute this based on the cron.trigger's call_at which should
|
||||
# be now() when clicking "Send" or schedule_date if scheduled
|
||||
|
||||
# If the departure time has passed but mailing is still in queue, this compute field
|
||||
# will be used to display warning with reload button on a mailing form view.
|
||||
for mass_mailing in self:
|
||||
if mass_mailing.schedule_date:
|
||||
# max in case the user schedules a date in the past
|
||||
mass_mailing.next_departure = max(mass_mailing.schedule_date, fields.datetime.now())
|
||||
else:
|
||||
mass_mailing.next_departure = fields.datetime.now()
|
||||
past = self.filtered(
|
||||
lambda mailing: mailing.state == 'in_queue' and mailing.next_departure < fields.Datetime.now()
|
||||
)
|
||||
past.next_departure_is_past = True
|
||||
(self - past).next_departure_is_past = False
|
||||
|
||||
@api.depends('email_from', 'mail_server_id')
|
||||
def _compute_warning_message(self):
|
||||
@@ -621,6 +630,9 @@ class MassMailing(models.Model):
|
||||
self.write({'schedule_type': 'now'})
|
||||
return self.action_put_in_queue()
|
||||
|
||||
def action_reload(self):
|
||||
pass
|
||||
|
||||
def action_schedule(self):
|
||||
self.ensure_one()
|
||||
if self.schedule_date and self.schedule_date > fields.Datetime.now():
|
||||
|
||||
@@ -160,6 +160,8 @@ class MassMailingList(models.Model):
|
||||
'context': {
|
||||
**self.env.context,
|
||||
'default_contact_list_ids': self.ids,
|
||||
'default_mailing_type': 'mail',
|
||||
'default_model_id': self.env['ir.model']._get_id('mailing.list'),
|
||||
},
|
||||
'target': 'current',
|
||||
'view_type': 'form',
|
||||
|
||||
@@ -54,6 +54,23 @@
|
||||
color: $o-main-favorite-color;
|
||||
}
|
||||
}
|
||||
// Align the model / domain container with subject field in large devices
|
||||
// and provide better tap area to the filter
|
||||
@include media-breakpoint-up(md) {
|
||||
div[name="mailing_model_id_container"] {
|
||||
width: 96%;
|
||||
}
|
||||
.o_mailing_filter_width {
|
||||
max-width: 25%;
|
||||
}
|
||||
.o_readonly_modifier.o_mailing_filter_readonly_width ,
|
||||
&.o_form_readonly .o_mailing_filter_readonly_width {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_xxs_form_view.o_mass_mailing_mailing_form {
|
||||
@@ -176,3 +193,11 @@
|
||||
min-height: 55vh;
|
||||
}
|
||||
}
|
||||
|
||||
.o_mass_mailing_form_full_width.o_form_editable {
|
||||
.o_domain_selector.o_read_mode {
|
||||
.o_domain_tree_header.oe_edit_only {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,19 @@ class TestMailingListMerge(MassMailCommon):
|
||||
self.assertEqual(contact.subscription_ids[0].opt_out_datetime, datetime(2022, 1, 1, 12, 0, 0))
|
||||
self.assertFalse(contact.subscription_ids[1].opt_out_datetime)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_action_send_mailing(self):
|
||||
mailing_ctx = self.mailing_list_1.action_send_mailing().get('context', {})
|
||||
form = Form(self.env['mailing.mailing'].with_context(mailing_ctx))
|
||||
form.subject = 'Test Mail'
|
||||
mailing = form.save()
|
||||
# Check that mailing model and mailing list are set properly
|
||||
self.assertEqual(
|
||||
mailing.mailing_model_id, self.env['ir.model']._get('mailing.list'),
|
||||
'Should have correct mailing model set')
|
||||
self.assertEqual(mailing.contact_list_ids, self.mailing_list_1, 'Should have correct mailing list set')
|
||||
self.assertEqual(mailing.mailing_type, 'mail', 'Should have correct mailing_type')
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_contact_copy_in_context_of_mailing_list(self):
|
||||
MailingContact = self.env['mailing.contact']
|
||||
@@ -243,6 +256,7 @@ class TestMailingContactImport(MassMailCommon):
|
||||
# Test that the context key "default_list_ids" is ignored (because we manually set list_ids)
|
||||
contact_import.with_context(default_list_ids=(first_list | second_list).ids).action_import()
|
||||
|
||||
self.env['mailing.list'].invalidate_model(['contact_ids'])
|
||||
# Check the contact of the first mailing list
|
||||
contacts = [
|
||||
(contact.name, contact.email)
|
||||
|
||||
@@ -150,6 +150,7 @@
|
||||
<field name="opt_out_datetime" readonly="1"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
<field name="create_date" string="Subscription Date"/>
|
||||
</tree>
|
||||
</field>
|
||||
</sheet>
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
<field name="model">mailing.list</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Contact List">
|
||||
<header>
|
||||
<button name="action_send_mailing" string="Send Mailing"
|
||||
type="object" class="btn btn-primary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_contacts"
|
||||
|
||||
@@ -119,11 +119,27 @@
|
||||
</strong>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="o_mails_in_queue" invisible="state != 'in_queue'">
|
||||
<strong>
|
||||
<strong invisible="next_departure_is_past or schedule_type == 'now'">
|
||||
<span name="next_departure_text">This mailing is scheduled for </span>
|
||||
<field name="next_departure" class="oe_inline"/>.
|
||||
</strong>
|
||||
<strong invisible="not next_departure_is_past or schedule_type == 'now'" class="d-flex align-items-center">
|
||||
<field name="next_departure_is_past" invisible="1"/>
|
||||
<span name="refresh_text">This mailing will be sent as soon as possible.</span>
|
||||
<button name="action_reload" type="object" class="btn btn-link pe-0">
|
||||
<u>Refresh <i class="fa fa-refresh ms-1"/></u>
|
||||
</button>
|
||||
</strong>
|
||||
<strong invisible="schedule_type != 'now'" class="d-flex align-items-center">
|
||||
<field name="next_departure_is_past" invisible="1"/>
|
||||
<field name="schedule_type" invisible="1"/>
|
||||
<span name="mailing_schedule_type_now_text">This mailing will be sent as soon as possible.</span>
|
||||
<button name="action_reload" type="object" class="btn btn-link pe-0">
|
||||
<u>Refresh <i class="fa fa-refresh ms-1"/></u>
|
||||
</button>
|
||||
</strong>
|
||||
</div>
|
||||
<div invisible="not warning_message">
|
||||
<strong><field name="warning_message"/></strong>
|
||||
@@ -194,12 +210,12 @@
|
||||
</div>
|
||||
<label for="mailing_model_id" string="Recipients"/>
|
||||
<div name="mailing_model_id_container">
|
||||
<div class="d-flex flex-row align-items-baseline flex-wrap">
|
||||
<div class="me-5">
|
||||
<div class="row">
|
||||
<div class="col-md-auto">
|
||||
<field name="mailing_model_id" options="{'no_open': True, 'no_create': True}"
|
||||
readonly="state in ('sending', 'done')"/>
|
||||
</div>
|
||||
<div invisible="not mailing_on_mailing_list" class="o_mass_mailing_contact_list_ids pt-1">
|
||||
<div invisible="not mailing_on_mailing_list" class="o_mass_mailing_contact_list_ids col-md-auto pt-1">
|
||||
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
|
||||
<div class="d-inline-flex flex-row align-items-center">
|
||||
<field name="contact_list_ids" widget="many2many_tags"
|
||||
@@ -212,19 +228,17 @@
|
||||
name="action_view_mailing_contacts" title="Add Mailing Contacts"/>
|
||||
</div>
|
||||
</div>
|
||||
<div invisible="mailing_on_mailing_list" class="o_td_label">
|
||||
<!-- We don't want to display label in edit mode, unless mailing is in sending or done state (where filter will be readonly) -->
|
||||
<label for="mailing_filter_id" string="Filter" class="oe_read_only me-4"
|
||||
invisible="state in ('sending', 'done') or not mailing_filter_id"/>
|
||||
<label for="mailing_filter_id" string="Filter" class="me-4"
|
||||
invisible="state not in ('sending', 'done') or not mailing_filter_id"/>
|
||||
<div invisible="mailing_on_mailing_list" class="col-md-auto o_mailing_filter_width">
|
||||
<field name="mailing_filter_id" placeholder="Reload a favorite filter"
|
||||
class="w-auto" widget="mailing_filter"
|
||||
class="o_mailing_filter_readonly_width" widget="mailing_filter"
|
||||
options="{'no_create': 1, 'no_open': 1, 'domain_field': 'mailing_domain', 'model_field': 'mailing_model_id'}"
|
||||
readonly="state in ('sending', 'done')"/>
|
||||
</div>
|
||||
<field name="mailing_filter_count" invisible="1"/>
|
||||
<field name="mailing_filter_domain" invisible="1"/>
|
||||
<div name="mass_mailing_domain" invisible="mailing_model_name == 'mailing.list'"
|
||||
class="col-md-6 d-block">
|
||||
<field name="mailing_domain" widget="domain" options="{'model': 'mailing_model_real'}" readonly="state in ('sending', 'done')"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<field name="mailing_model_name" invisible="1"/>
|
||||
@@ -400,7 +414,7 @@
|
||||
<attribute name="invisible">state != 'in_queue'</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_scheduled')]/button" position="attributes">
|
||||
<attribute name="invisible">scheduled == 0</attribute>
|
||||
<attribute name="invisible">not scheduled or next_departure_is_past</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_scheduled')]//span[@name='scheduled_text']" position="replace">
|
||||
<span name="scheduled_text">email(s) scheduled for </span>
|
||||
@@ -410,8 +424,16 @@
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong/span[@name='next_departure_text']" position="attributes">
|
||||
<attribute name="invisible">scheduled != 0</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong/span[@name='refresh_text']" position="attributes">
|
||||
<attribute name="invisible">scheduled</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong/span[@name='mailing_schedule_type_now_text']" position="attributes">
|
||||
<attribute name="invisible">scheduled</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_scheduled')]" position="inside">
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong" position="move"/>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong" position="move"/>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]/strong" position="move"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('alert-info')]/div[hasclass('o_mails_in_queue')]" position="replace"/>
|
||||
<xpath expr="//button[@name='action_view_opened']" position="before">
|
||||
@@ -451,6 +473,9 @@
|
||||
</div>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='mass_mailing_domain']" position="attributes">
|
||||
<attribute name="class">col-md-7</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
aria-label="Blacklisted" invisible="not is_blacklisted" groups="base.group_user"></i>
|
||||
<field name="contact_id"/>
|
||||
</div>
|
||||
<field name="create_date" string="Subscription Date"/>
|
||||
<field name="opt_out_datetime" readonly="1"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
@@ -32,6 +33,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Mailing List Subscriptions">
|
||||
<field name="contact_id"/>
|
||||
<field name="create_date" string="Subscription Date"/>
|
||||
<field name="list_id"/>
|
||||
<field name="opt_out"/>
|
||||
<field name="opt_out_reason_id"/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, tools, _
|
||||
from odoo import fields, models, tools, Command, _
|
||||
from odoo.tools.misc import clean_context
|
||||
|
||||
|
||||
@@ -63,7 +63,10 @@ class MailingContactImport(models.TransientModel):
|
||||
if email not in existing_contacts:
|
||||
unique_contacts[email] = {
|
||||
'name': name,
|
||||
'list_ids': self.mailing_list_ids.ids,
|
||||
'subscription_ids': [
|
||||
Command.create({'list_id': mailing_list_id.id})
|
||||
for mailing_list_id in self.mailing_list_ids
|
||||
],
|
||||
}
|
||||
|
||||
if not unique_contacts:
|
||||
|
||||
@@ -27,6 +27,22 @@ class MailingList(models.Model):
|
||||
action['context'] = dict(action.get('context', {}), search_default_filter_valid_sms_recipient=1)
|
||||
return action
|
||||
|
||||
def action_send_mailing_sms(self):
|
||||
view = self.env.ref('mass_mailing_sms.mailing_mailing_view_form_sms')
|
||||
action = self.env["ir.actions.actions"]._for_xml_id('mass_mailing_sms.mailing_mailing_action_sms')
|
||||
action.update({
|
||||
'context': {
|
||||
'default_contact_list_ids': self.ids,
|
||||
'default_model_id': self.env['ir.model']._get_id('mailing.list'),
|
||||
'default_mailing_type': 'sms',
|
||||
},
|
||||
'target': 'current',
|
||||
'view_type': 'form',
|
||||
'views': [(view.id, 'form')],
|
||||
})
|
||||
|
||||
return action
|
||||
|
||||
def _get_contact_statistics_fields(self):
|
||||
""" See super method docstring for more info.
|
||||
Adds:
|
||||
|
||||
@@ -6,3 +6,4 @@ from . import test_mailing_internals
|
||||
from . import test_mailing_retry
|
||||
from . import test_mailing_sms_ab_testing
|
||||
from . import test_mailing_ui
|
||||
from . import test_mailing_list
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mass_mailing_sms.tests.common import MassSMSCommon
|
||||
from odoo.tests.common import Form, users
|
||||
|
||||
|
||||
class TestMailingListSms(MassSMSCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingListSms, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_action_send_sms(self):
|
||||
sms_ctx = self.mailing_list_1.action_send_mailing_sms().get('context', {})
|
||||
form = Form(self.env['mailing.mailing'].with_context(sms_ctx))
|
||||
form.sms_subject = 'Test SMS'
|
||||
form.body_plaintext = 'Test sms body'
|
||||
sms = form.save()
|
||||
# Check that mailing model and mailing list are set properly
|
||||
self.assertEqual(
|
||||
sms.mailing_model_id, self.env['ir.model']._get('mailing.list'),
|
||||
'Should have correct mailing model set')
|
||||
self.assertEqual(sms.contact_list_ids, self.mailing_list_1, 'Should have correct mailing list set')
|
||||
self.assertEqual(sms.mailing_type, 'sms', 'Should have correct mailing_type')
|
||||
@@ -29,6 +29,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_list_view_form" model="ir.ui.view">
|
||||
<field name="name">mailing.list.view.form.inherit.sms</field>
|
||||
<field name="model">mailing.list</field>
|
||||
<field name="inherit_id" ref="mass_mailing.mailing_list_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_send_mailing']" position="after">
|
||||
<button name="action_send_mailing_sms" string="Send SMS"
|
||||
type="object" class="btn btn-primary"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mailing_list_action_sms" model="ir.actions.act_window">
|
||||
<field name="name">Mailing Lists</field>
|
||||
<field name="res_model">mailing.list</field>
|
||||
|
||||
Reference in New Issue
Block a user