[REF] mass_mailing: prepare mass_sms

PURPOSE

SMS are a powerful marketing tool. For instance it is perfect to announce a
sale or to communicate a coupon code, to welcome a new customer in a fidelity
program, ...

Purpose of this task is to integrate SMS sending in batch in mass mailing. It
will use same mailing objects but sending SMS instead of emails. Some metrics
and flows will have to be slightly updated at the same time.

SPECIFICATIONS

Prepare mass mailing module to addition of mass_mailing_sms.

Mailing model: mailing type
  * add a mailing_type selection field;
  * mass mailing contains only 'mail';
  * synchronize medium accordingly;
  * update actions of mass mailing application to add a domain on mailing
    type being 'mail';

Mailing contact
  * remove is_email_valid field as its purpose is achieved by email_normalized
    field coming from address mixin (added by blacklist management);

Mailing contact subscription
  * clean a bit fields and views as this should stay a technical model;

Trace model and report: trace type
  * add a trace_type selection field;
  * mass mailing contains only 'mail';

Various
  * clean some bits of code in views, remove old code bits;
  * add anchors to ease view inheritance to be able to customize views for
    SMS mailings;
  * ensure all views in mass mailing filter content on mail type (mailing
    and traces being type mail only);
  * rename some methods to be more updated with current guidelines, notably
    main action methods;

LINKS

Task 1997464
PR #34424
Original SMS addition: Task 1922163 (4287481)
This commit is contained in:
Thibault Delavallée
2019-08-12 12:00:22 +00:00
parent e7cd604afa
commit c7a14e9109
21 changed files with 258 additions and 231 deletions
+1 -1
View File
@@ -99,5 +99,5 @@ class MailMail(models.Model):
if mail_sent is True and mail.mailing_trace_ids:
mail.mailing_trace_ids.write({'sent': fields.Datetime.now(), 'exception': False})
elif mail_sent is False and mail.mailing_trace_ids:
mail.mailing_trace_ids.write({'exception': fields.Datetime.now()})
mail.mailing_trace_ids.write({'exception': fields.Datetime.now(), 'failure_type': failure_type})
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
+44 -19
View File
@@ -33,6 +33,7 @@ MASS_MAILING_BUSINESS_MODELS = [
# Used to find inline images
image_re = re.compile(r"data:(image/[A-Za-z]+);base64,(.*)")
class MassMailing(models.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
@@ -85,6 +86,7 @@ class MassMailing(models.Model):
color = fields.Integer(string='Color Index')
user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user)
# mailing options
mailing_type = fields.Selection([('mail', 'Email')], string="Mailing Type", default="mail", required=True)
reply_to_mode = fields.Selection(
[('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True)
reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address',
@@ -155,10 +157,9 @@ class MassMailing(models.Model):
m.id as mailing_id,
COUNT(s.id) AS expected,
COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null AND s.bounced is null THEN 1 ELSE null END) AS scheduled,
COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
COUNT(CASE WHEN s.sent is not null AND s.exception is null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
@@ -217,11 +218,21 @@ class MassMailing(models.Model):
mailing_domain = [(0, '=', 1)]
self.mailing_domain = repr(mailing_domain)
@api.onchange('mailing_type')
def _onchange_mailing_type(self):
if self.mailing_type == 'mail' and not self.medium_id:
self.medium_id = self.env.ref('utm.utm_medium_email').id
@api.onchange('subject')
def _onchange_subject(self):
if self.subject and not self.name:
self.name = self.subject
@api.onchange('name')
def _onchange_name(self):
if self.name and not self.subject:
self.subject = self.name
# ------------------------------------------------------
# ORM
# ------------------------------------------------------
@@ -232,7 +243,7 @@ class MassMailing(models.Model):
values['subject'] = values['name']
if values.get('body_html'):
values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
if 'medium_id' not in values:
if 'medium_id' not in values and values.get('mailing_type', 'mail') == 'mail':
values['medium_id'] = self.env.ref('utm.utm_medium_email').id
return super(MassMailing, self).create(values)
@@ -270,7 +281,7 @@ class MassMailing(models.Model):
}
return False
def action_test_mailing(self):
def action_test(self):
self.ensure_one()
ctx = dict(self.env.context, default_mass_mailing_id=self.id)
return {
@@ -282,30 +293,44 @@ class MassMailing(models.Model):
'context': ctx,
}
def action_schedule_date(self):
def action_schedule(self):
self.ensure_one()
action = self.env.ref('mass_mailing.mailing_mailing_schedule_date_action').read()[0]
action['context'] = dict(self.env.context, default_mass_mailing_id=self.id)
return action
def put_in_queue(self):
def action_put_in_queue(self):
self.write({'state': 'in_queue'})
def cancel_mass_mailing(self):
def action_cancel(self):
self.write({'state': 'draft', 'schedule_date': False})
def retry_failed_mail(self):
failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')])
def action_retry_failed(self):
failed_mails = self.env['mail.mail'].sudo().search([
('mailing_id', 'in', self.ids),
('state', '=', 'exception')
])
failed_mails.mapped('mailing_trace_ids').unlink()
failed_mails.sudo().unlink()
res_ids = self._get_recipients()
except_mailed = self.env['mailing.trace'].search([
('model', '=', self.mailing_model_real),
('res_id', 'in', res_ids),
('exception', '!=', False),
('mass_mailing_id', '=', self.id)]).unlink()
failed_mails.unlink()
self.write({'state': 'in_queue'})
def action_view_traces_scheduled(self):
return self._action_view_traces_filtered('scheduled')
def action_view_traces_ignored(self):
return self._action_view_traces_filtered('ignored')
def action_view_traces_failed(self):
return self._action_view_traces_filtered('failed')
def _action_view_traces_filtered(self, view_filter):
action = self.env.ref('mass_mailing.mailing_trace_action').read()[0]
action['name'] = _('%s Traces') % (self.name)
action['context'] = {'search_default_mass_mailing_id': self.id,}
filter_key = 'search_default_filter_%s' % (view_filter)
action['context'][filter_key] = True
return action
def action_view_sent(self):
return self._action_view_documents_filtered('sent')
@@ -387,7 +412,7 @@ class MassMailing(models.Model):
_logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name)
return opt_out
def _get_convert_links(self):
def _get_link_tracker_values(self):
self.ensure_one()
vals = {'mass_mailing_id': self.id}
@@ -456,7 +481,7 @@ class MassMailing(models.Model):
return {
'mass_mailing_opt_out_list': self._get_opt_out_list(),
'mass_mailing_seen_list': self._get_seen_list(),
'post_convert_links': self._get_convert_links(),
'post_convert_links': self._get_link_tracker_values(),
}
def _get_recipients(self):
+1 -22
View File
@@ -19,7 +19,6 @@ class MassMailingContactListRel(models.Model):
opt_out = fields.Boolean(string='Opt Out',
help='The contact has chosen not to receive mails anymore from this list', default=False)
unsubscription_date = fields.Datetime(string='Unsubscription Date')
contact_count = fields.Integer(related='list_id.contact_nbr', store=False, readonly=False)
message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False)
is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False)
@@ -39,19 +38,6 @@ class MassMailingContactListRel(models.Model):
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContactListRel, self).write(vals)
def action_open_mailing_list_contact(self):
"""TODO DBE : To remove - Deprecated"""
contact_id = self.contact_id
action = {
'name': _(contact_id.name),
'type': 'ir.actions.act_window',
'res_model': 'mailing.contact',
'view_mode': 'form',
'target': 'current',
'res_id': contact_id.id
}
return action
class MassMailingContact(models.Model):
"""Model of a contact. This model is different from the partner model
@@ -67,8 +53,7 @@ class MassMailingContact(models.Model):
name = fields.Char()
company_name = fields.Char(string='Company Name')
title_id = fields.Many2one('res.partner.title', string='Title')
email = fields.Char(required=True)
is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True)
email = fields.Char('Email')
list_ids = fields.Many2many(
'mailing.list', 'mailing_contact_list_rel',
'contact_id', 'list_id', string='Mailing Lists')
@@ -79,12 +64,6 @@ class MassMailingContact(models.Model):
help='Opt out flag for a specific mailing list.'
'This field should not be used in a view without a unique and active mailing list context.')
@api.depends('email')
def _compute_is_email_valid(self):
for record in self:
normalized = tools.email_normalize(record.email)
record.is_email_valid = normalized if not normalized else True
@api.model
def _search_opt_out(self, operator, value):
# Assumes operator is '=' or '!=' and value is True or False
@@ -45,6 +45,13 @@ class MassMailingList(models.Model):
def name_get(self):
return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self]
def action_view_contacts(self):
action = self.env.ref('mass_mailing.action_view_mass_mailing_contacts').read()[0]
action['domain'] = [('list_ids', 'in', self.ids)]
context = dict(self.env.context, search_default_filter_valid_email_recipient=1, default_list_ids=self.ids)
action['context'] = context
return action
def action_merge(self, src_lists, archive):
"""
Insert all the contact from the mailing lists 'src_lists' to the
+24 -11
View File
@@ -11,9 +11,11 @@ class MailingTrace(models.Model):
without loosing the statistics about them. """
_name = 'mailing.trace'
_description = 'Mailing Statistics'
_rec_name = 'message_id'
_order = 'message_id'
_rec_name = 'id'
_order = 'scheduled DESC'
trace_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True)
display_name = fields.Char(compute='_compute_display_name')
# mail data
mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True)
mail_mail_id_int = fields.Integer(
@@ -23,16 +25,16 @@ class MailingTrace(models.Model):
'However the ID is needed for several action and controllers.',
index=True,
)
email = fields.Char(string="Recipient email address")
email = fields.Char(string="Email")
message_id = fields.Char(string='Message-ID')
# document
model = fields.Char(string='Document model')
res_id = fields.Integer(string='Document ID')
# campaign / wave data
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', index=True)
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', index=True)
campaign_id = fields.Many2one(
related='mass_mailing_id.campaign_id',
string='Mass Mailing Campaign',
string='Campaign',
store=True, readonly=True, index=True)
# Bounce and tracking
ignored = fields.Datetime(help='Date when the email has been invalidated. '
@@ -55,10 +57,21 @@ class MailingTrace(models.Model):
('replied', 'Replied'),
('bounced', 'Bounced'),
('ignored', 'Ignored')], store=True)
failure_type = fields.Selection(selection=[
("SMTP", "Connection failed (outgoing mail server problem)"),
("RECIPIENT", "Invalid email address"),
("BOUNCE", "Email address rejected by destination"),
("UNKNOWN", "Unknown error"),
], string='Failure type')
state_update = fields.Datetime(compute="_compute_state", string='State Update',
help='Last state update of the mail',
store=True)
@api.depends('trace_type', 'mass_mailing_id')
def _compute_display_name(self):
for trace in self:
trace.display_name = '%s: %s (%s)' % (trace.trace_type, trace.mass_mailing_id.name, trace.id)
@api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored')
def _compute_state(self):
self.update({'state_update': fields.Datetime.now()})
@@ -78,12 +91,12 @@ class MailingTrace(models.Model):
else:
stat.state = 'outgoing'
@api.model
def create(self, values):
if 'mail_mail_id' in values:
values['mail_mail_id_int'] = values['mail_mail_id']
res = super(MailingTrace, self).create(values)
return res
@api.model_create_multi
def create(self, values_list):
for values in values_list:
if 'mail_mail_id' in values:
values['mail_mail_id_int'] = values['mail_mail_id']
return super(MailingTrace, self).create(values_list)
def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None):
if not self.ids and mail_mail_ids:
+1
View File
@@ -3,6 +3,7 @@
from odoo import api, fields, models
class UtmCampaign(models.Model):
_inherit = 'utm.campaign'
@@ -9,18 +9,22 @@ class MailingTraceReport(models.Model):
_auto = False
_description = 'Mass Mailing Statistics'
scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True)
# mailing
name = fields.Char(string='Mass Mail', readonly=True)
mailing_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True)
campaign = fields.Char(string='Mass Mail Campaign', readonly=True)
scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True)
state = fields.Selection(
[('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
string='Status', readonly=True)
email_from = fields.Char('From', readonly=True)
# traces
sent = fields.Integer(readonly=True)
delivered = fields.Integer(readonly=True)
opened = fields.Integer(readonly=True)
bounced = fields.Integer(readonly=True)
replied = fields.Integer(readonly=True)
clicked = fields.Integer(readonly=True)
state = fields.Selection([('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
string='Status', readonly=True)
email_from = fields.Char('From', readonly=True)
bounced = fields.Integer(readonly=True)
def init(self):
"""Mass Mail Statistical Report: based on mailing.trace that models the various
@@ -30,22 +34,23 @@ class MailingTraceReport(models.Model):
self.env.cr.execute("""
CREATE OR REPLACE VIEW mailing_trace_report AS (
SELECT
min(ms.id) as id,
ms.scheduled as scheduled_date,
min(trace.id) as id,
utm_source.name as name,
mailing.mailing_type,
utm_campaign.name as campaign,
count(ms.bounced) as bounced,
count(ms.sent) as sent,
(count(ms.sent) - count(ms.bounced)) as delivered,
count(ms.opened) as opened,
count(ms.replied) as replied,
count(ms.clicked) as clicked,
mm.state,
mm.email_from
trace.scheduled as scheduled_date,
mailing.state,
mailing.email_from,
count(trace.sent) as sent,
(count(trace.sent) - count(trace.bounced)) as delivered,
count(trace.opened) as opened,
count(trace.replied) as replied,
count(trace.clicked) as clicked,
count(trace.bounced) as bounced
FROM
mailing_trace as ms
left join mailing_mailing as mm ON (ms.mass_mailing_id=mm.id)
left join utm_campaign as utm_campaign ON (mm.campaign_id = utm_campaign.id)
left join utm_source as utm_source ON (mm.source_id = utm_source.id)
GROUP BY ms.scheduled, utm_source.name, utm_campaign.name, mm.state, mm.email_from
mailing_trace as trace
left join mailing_mailing as mailing ON (trace.mass_mailing_id=mailing.id)
left join utm_campaign as utm_campaign ON (mailing.campaign_id = utm_campaign.id)
left join utm_source as utm_source ON (mailing.source_id = utm_source.id)
GROUP BY trace.scheduled, utm_source.name, utm_campaign.name, mailing.mailing_type, mailing.state, mailing.email_from
)""")
@@ -51,14 +51,16 @@
</record>
<!-- Actions and Menuitems -->
<record id="mailing_trace_report_action" model="ir.actions.act_window">
<record id="mailing_trace_report_action_mail" model="ir.actions.act_window">
<field name="name">Mass Mailing Analysis</field>
<field name="res_model">mailing.trace.report</field>
<field name="domain">[('mailing_type', '=', 'mail')]</field>
<field name="view_mode">graph,pivot</field>
<field name="help">Mass Mailing Statistics allows you to check different mailing related information like number of bounced mails, opened mails, replied mails. You can sort out your analysis by different groups to get accurate grained analysis.</field>
<field name="help" type="html"><p>Mass Mailing Statistics allows you to check different mailing related information like number of bounced mails, opened mails, replied mails. You can sort out your analysis by different groups to get accurate grained analysis.</p></field>
</record>
<menuitem name="Mass Mailing" id="mass_mailing_menu_root" sequence="60"/>
<menuitem name="Reporting" id="menu_mass_mailing_report" sequence="99"
parent="mass_mailing_menu_root" action="mailing_trace_report_action" groups="mass_mailing.group_mass_mailing_user"/>
parent="mass_mailing_menu_root"
action="mailing_trace_report_action_mail"
groups="mass_mailing.group_mass_mailing_user"/>
</odoo>
@@ -156,7 +156,7 @@ QUnit.test('save and edit arch wysiwyg', async function (assert) {
data: this.data,
arch: '<form>' +
' <header style="min-height:31px;">' +
' <button name="put_in_queue" type="object" class="oe_highlight" string="Send Now"/>' +
' <button name="action_put_in_queue" type="object" class="oe_highlight" string="Send Now"/>' +
' </header>' +
' <field name="body_arch" class="oe_edit_only" widget="mass_mailing_html"' +
' options="{' +
@@ -174,14 +174,14 @@ QUnit.test('save and edit arch wysiwyg', async function (assert) {
},
});
await testUtils.dom.click($('[name=put_in_queue]'));
await testUtils.dom.click($('[name=action_put_in_queue]'));
await testUtils.form.clickEdit(form);
var $fieldEdit = form.$('.oe_form_field[name="body_arch"]');
var $iframe = $fieldEdit.find('iframe');
await $iframe.data('loadDef');
await testUtils.nextTick();
await testUtils.dom.click($('[name=put_in_queue]'));
await testUtils.dom.click($('[name=action_put_in_queue]'));
assert.verifySteps(["MassMailingFieldHtml", "FieldHtml", "MassMailingFieldHtml"]);
testUtils.mock.unpatch(FieldHtml);
@@ -72,7 +72,7 @@ Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
"keep_archives": True,
})
mass_mailing.put_in_queue()
mass_mailing.action_put_in_queue()
mass_mailing._process_mass_mailing_queue()
sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
@@ -12,7 +12,6 @@
</xpath>
<xpath expr="//group" position="inside">
<filter string="Mass Mailing" name="groupby_mass_mailing_id" context="{'group_by': 'mass_mailing_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'campaign_id'}"/>
</xpath>
</field>
</record>
@@ -66,7 +65,6 @@
</xpath>
<xpath expr="//filter[@name='groupby_country_id']" position="after">
<filter string="Mass Mailing" name="groupby_mass_mailing_id" context="{'group_by': 'mass_mailing_id'}"/>
<filter string="Mailing Campaign" name="groupby_mass_mailing_campaign_id" domain="[]" context="{'group_by': 'campaign_id'}"/>
</xpath>
</field>
</record>
@@ -6,7 +6,7 @@
<field name="model">mailing.contact.subscription</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List contacts">
<form string="Mailing List Subscription">
<sheet>
<group>
<field name="list_id"/>
@@ -30,13 +30,12 @@
<field name="name">mailing.contact.subscription.view.tree</field>
<field name="model">mailing.contact.subscription</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<tree string="Mailing List Subscriptions">
<field name="contact_id"/>
<field name="unsubscription_date"/>
<field name="opt_out"/>
<field name="message_bounce"/>
<field name="is_blacklisted"/>
<button name="action_open_mailing_list_contact" string="Open Contact" type="object" icon="fa-user"/>
</tree>
</field>
</record>
@@ -45,24 +44,10 @@
<field name="name">mailing.contact.subscription.view.search</field>
<field name="model">mailing.contact.subscription</field>
<field name="arch" type="xml">
<search string="Mailing List Contacts">
<search string="Mailing List Subscriptions">
<field name="contact_id"/>
<field name="opt_out"/>
<field name="list_id"/>
<separator/>
<filter string="Valid Recipients" name="valid_recipients" domain="[('opt_out', '=', False), ('contact_id.is_blacklisted', '=', False), ('contact_id.is_email_valid', '=', True)]"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"/>
<separator/>
<filter string="Exclude Blacklisted" name="not_blacklisted" domain="[('contact_id.is_blacklisted', '=', False)]"/>
<separator/>
<filter string="Exclude Invalid Emails" name="valid_email" domain="[('contact_id.is_email_valid', '=', True)]"/>
<separator/>
<filter string="Exclude Bounced" name="not_bounced" domain="[('message_bounce', '&lt;', 3)]"/>
<group expand="0" string="Group By">
<filter string="Creation Date" name="group_create_date"
context="{'group_by': 'create_date'}"/>
</group>
</search>
</field>
</record>
@@ -72,21 +57,25 @@
<field name="model">mailing.contact</field>
<field name="arch" type="xml">
<search string="Mailing List Contacts">
<field name="name" filter_domain="['|','|',('name','ilike',self),('company_name','ilike',self), ('email','ilike',self)]"
<field name="name"
filter_domain="['|', '|', ('name','ilike',self), ('company_name','ilike',self), ('email_normalized','ilike',self)]"
string="Name / Email"/>
<field name="tag_ids"/>
<field name="list_ids"/>
<separator/>
<filter string="Valid Recipients" name="valid_recipients" domain="[('opt_out', '=', False), ('is_blacklisted', '=', False), ('is_email_valid', '=', True)]"
invisible="'default_list_ids' not in context"/>
<separator/>
<filter string="Exclude Blacklisted" name="not_blacklisted" domain="[('is_blacklisted', '=', False)]"/>
<separator/>
<filter string="Exclude Opt Out" name="not_opt_out" domain="[('opt_out', '=', False)]"
invisible="'default_list_ids' not in context"/>
<filter string="Valid Email Recipients"
name="filter_valid_email_recipient"
domain="[('opt_out', '=', False), ('is_blacklisted', '=', False), ('email_normalized', '!=', False)]"
invisible="not context.get('default_list_ids')"/>
<separator/>
<filter string="Exclude Blacklisted Emails"
name="filter_not_email_bl"
domain="[('is_blacklisted', '=', False)]"/>
<separator/>
<filter string="Exclude Bounced" name="not_bounced" domain="[('message_bounce', '&lt;', 3)]"/>
<filter string="Exclude Opt Out"
name="filter_not_optout"
domain="[('opt_out', '=', False)]"
invisible="not context.get('default_list_ids')"/>
<group expand="0" string="Group By">
<filter string="Creation Date" name="group_create_date"
context="{'group_by': 'create_date'}"/>
@@ -173,12 +162,12 @@
</div>
<group>
<group>
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<div class="o_row o_row_readonly" name="email_details">
<i class="fa fa-ban" style="color: red;" role="img" title="This email is blacklisted for mass mailing"
aria-label="Blacklisted" attrs="{'invisible': [('is_blacklisted', '=', False)]}" groups="base.group_user"></i>
<field name="email" widget="email"/>
<field name="is_blacklisted" invisible="1"/>
</div>
<field name="title_id"/>
<field name="company_name"/>
@@ -186,11 +175,7 @@
</group>
<group>
<field name="create_date" readonly="1"/>
<label for="message_bounce"/>
<div class="o_row">
<field name="message_bounce"/>
<span> times</span>
</div>
<field name="message_bounce"/>
</group>
</group>
<field name="subscription_list_ids">
@@ -235,26 +220,13 @@
<field name="name">Mailing List Contacts</field>
<field name="res_model">mailing.contact</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="context">{'search_default_valid_recipient': 1}</field>
<field name="context">{'search_default_filter_not_email_bl': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a contact in your address book
</p><p>
Odoo helps you to easily track all activities related to a customer.
</p>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts_from_list">
<field name="name">Recipients</field>
<field name="res_model">mailing.contact</field>
<field name="view_mode">tree,form,graph</field>
<field name="domain">[('list_ids', 'in', [active_id])]</field>
<field name="context">{'search_default_valid_recipients': 1, 'default_list_ids': [active_id]}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new recipient
</p>
<p class="o_view_nocontent_smiling_face">
Create a contact in your address book
</p><p>
Odoo helps you to easily track all activities related to a customer.
</p>
</field>
</record>
@@ -40,8 +40,8 @@
<sheet>
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_button_box" name="button_box">
<button name="%(mass_mailing.action_view_mass_mailing_contacts_from_list)d"
type="action" icon="fa-user" class="oe_stat_button">
<button name="action_view_contacts"
type="object" icon="fa-user" class="oe_stat_button">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
</div>
@@ -131,7 +131,7 @@
</div>
</div>
<div class="o_mailing_list_kanban_boxes">
<a class="o_project_kanban_box" name="%(action_view_mass_mailing_contacts_from_list)d" type="action">
<a name="action_view_contacts" type="object">
<div>
<span class="badge badge-pill">
<i class="fa fa-user" role="img" aria-label="Contacts" title="Contacts"/>
@@ -7,6 +7,7 @@
<field name="arch" type="xml">
<search string="Mailings">
<field name="name" string="Mailings"/>
<field name="mailing_type" invisible="1"/>
<field name="campaign_id"/>
<filter string="My Mailings" name="assigned_to_me"
domain="[('user_id', '=', uid)]"
@@ -32,6 +33,7 @@
<field name="arch" type="xml">
<tree string="Mailings">
<field name="name"/>
<field name="mailing_type" invisible="1"/>
<field name="sent"/>
<field name="delivered"/>
<field name="opened"/>
@@ -48,55 +50,52 @@
<field name="arch" type="xml">
<form string="Mailing">
<header style="min-height:31px;">
<button name="put_in_queue" type="object" attrs="{'invisible': [('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send Now"
<button name="action_put_in_queue" type="object" attrs="{'invisible': [('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send Now"
confirm="This will send the email to all recipients. Do you still want to proceed ?"/>
<button name="action_schedule_date" type="object" attrs="{'invisible': [('state', 'in', ('in_queue', 'done'))]}" class="btn-secondary" string="Schedule"/>
<button name="action_test_mailing" type="object" class="btn-secondary" string="Test"/>
<button name="cancel_mass_mailing" type="object" attrs="{'invisible': [('state', '!=', 'in_queue')]}" class="btn-secondary" string="Cancel"/>
<button name="retry_failed_mail" type="object" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" class="oe_highlight" string="Retry"/>
<button name="action_schedule" type="object" attrs="{'invisible': [('state', 'in', ('in_queue', 'done'))]}" class="btn-secondary" string="Schedule"/>
<button name="action_test" type="object" class="btn-secondary" string="Test"/>
<button name="action_cancel" type="object" attrs="{'invisible': [('state', '!=', 'in_queue')]}" class="btn-secondary" string="Cancel"/>
<button name="action_retry_failed" type="object" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" class="oe_highlight" string="Retry"/>
<field name="state" readonly="1" widget="statusbar"/>
</header>
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': [('ignored', '=', 0)]}">
<div class="alert alert-info text-center" attrs="{'invisible': [('ignored', '=', 0)]}" role="alert">
<p>
<button class="btn-link"
name="%(action_view_mail_mail_statistics_mailing_list)d"
type="action"
context="{'search_default_filter_ignored': True}">
name="action_view_traces_ignored"
type="object">
<strong>
<field name="ignored" class="oe_inline"/>
email(s) have been ignored and will not be sent.
<field name="ignored" class="oe_inline mr-2"/>
<span name="ignored_text">emails have been ignored and will not be sent.</span>
</strong>
</button>
</p>
</div>
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': ['|', ('scheduled', '=', 0), ('ignored','!=',0)]}">
<div class="alert alert-info text-center" attrs="{'invisible': [('scheduled', '=', 0)]}" role="alert">
<p>
<button class="btn-link"
name="%(action_view_mail_mail_statistics_mailing_list)d"
type="action"
context="{'search_default_filter_scheduled': True}">
name="action_view_traces_scheduled"
type="object">
<strong>
<field name="scheduled" class="oe_inline"/>
email(s) are in queue and will be sent soon.
<field name="scheduled" class="oe_inline mr-2"/>
<span name="scheduled_text">emails are in queue and will be sent soon.</span>
</strong>
</button>
</p>
</div>
<div class="oe_form_box_info bg-info oe_text_center alert-info" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" role="alert">
<div class="alert alert-info text-center" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" role="alert">
<p>
<button class="btn-link"
name="%(action_view_mail_mail_statistics_mailing_list)d"
type="action"
context="{'search_default_filter_failed': True}">
name="action_view_traces_failed"
type="object">
<strong>
<field name="failed" class="oe_inline"/>
email(s) could not be sent.
<field name="failed" class="oe_inline mr-2"/>
<span name="failed_text">emails could not be sent.</span>
</strong>
</button>
</p>
</div>
<div class="oe_form_box_info bg-info oe_text_center" attrs="{'invisible': [('state', '!=', 'in_queue')]}">
<div class="alert alert-info text-center" attrs="{'invisible': [('state', '!=', 'in_queue')]}" role="alert">
<p><strong>
This mailing is scheduled for
<field name="next_departure" class="oe_inline"/>.
@@ -106,15 +105,16 @@
<widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<div class="oe_button_box" name="button_box">
<button name="action_view_sent"
id="button_view_sent"
type="object"
context="{'search_default_filter_sent': True}"
icon="fa-envelope-o" class="oe_stat_button o_mass_mailing_button"
attrs="{'invisible': ['&amp;', ('sent', '=', 0), ('state', 'in', ('draft','test'))]}" >
<field name="sent" string="Emails Sent" widget="statinfo"/>
icon="fa-envelope-o" class="oe_stat_button"
attrs="{'invisible': ['&amp;', ('sent', '=', 0), ('state', 'in', ('draft', 'test'))]}" >
<field name="sent" string="Sent" widget="statinfo"/>
</button>
<button name="action_view_delivered"
type="object"
context="{'search_default_filter_received': True}"
context="{'search_default_filter_delivered': True}"
attrs="{'invisible': [('state', 'in', ('draft','test'))]}"
class="oe_stat_button">
<field name="received_ratio" string="Received" widget="percentpie"/>
@@ -144,8 +144,9 @@
<group>
<field name="active" invisible="1"/>
<field name="subject" string="Subject"/>
<field name="name" required="1" string="Mailing Name"/>
<field name="mailing_type" widget="radio" options="{'horizontal': true}" invisible="1"/>
<field name="subject" string="Subject"/>
<label for="mailing_model_id" string="Recipients"/>
<div>
<field name="mailing_model_id" widget="selection" required="1"/>
@@ -163,7 +164,7 @@
</div>
</group>
<notebook>
<page string="Mail Body">
<page string="Mail Body" name="mail_body">
<field name="body_html" class="oe_read_only" widget="html" options="{
'cssReadonly': 'mass_mailing.iframe_css_assets_readonly',
}"/>
@@ -180,14 +181,14 @@
<field name="unique_ab_testing" groups="base.group_no_one"/>
<field name="email_from"/>
<label for="reply_to"/>
<div>
<div name="reply_to_details">
<field name="reply_to_mode" widget="radio"
attrs="{'invisible': [('mailing_model_name', 'in', ['mailing.contact', 'res.partner', 'mailing.list'])]}"/>
<field name="reply_to"
attrs="{'required': [('reply_to_mode', '=', 'email')], 'invisible': [('reply_to_mode', '=', 'thread')]}"/>
</div>
<label for="attachment_ids"/>
<div>
<div name="attachment_ids_details">
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" class="oe_inline"/>
</div>
<field name="mail_server_id" groups="base.group_no_one" options="{'no_create': True, 'no_open': True}"/>
@@ -256,6 +257,7 @@
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<h3 class="mt4 mb4"><field name="name"/></h3>
<field name="mailing_type" invisible="1"/>
<div class="o_kanban_record_subtitle" attrs="{'invisible': [('sent_date', '=', False)]}">
<h5 style="display: inline;"><field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/></h5>
<t t-if="record.campaign_id.raw_value" groups="mass_mailing.group_mass_mailing_campaign"> - </t><field name="sent_date"/>
@@ -264,7 +266,7 @@
</div>
<div class="o_kanban_record_body" t-if="!selection_mode" attrs="{'invisible': [('sent_date', '=', False)]}">
<t t-if="record.sent_date">
<div class="row">
<div class="row" name="stat_opened">
<div class="col-8">
<a name="%(action_view_mail_mail_statistics_mailing)d"
context="{'search_default_filter_opened': True}"
@@ -274,7 +276,7 @@
<field name="opened_ratio"/> %
</div>
</div>
<div class="row">
<div class="row" name="stat_replied">
<div class="col-8">
<a name="%(action_view_mail_mail_statistics_mailing)d"
context="{'search_default_filter_replied': True}"
@@ -284,7 +286,7 @@
<field name="replied_ratio"/> %
</div>
</div>
<div class="row">
<div class="row" name="stat_clicks">
<div class="col-8">
<a name="%(link_tracker_action_mass_mailing)d"
type="action">Clicks</a>
@@ -293,7 +295,7 @@
<field name="clicks_ratio"/> %
</div>
</div>
<div class='o_kanban_primary_bottom'>
<div class='o_kanban_primary_bottom' name="stat_delivered">
<field name="delivered" widget="progressbar" title="Delivered" options="{'current_value': 'delivered', 'max_value': 'expected', 'editable': false}"/>
</div>
</t>
@@ -317,13 +319,15 @@
</field>
</record>
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<record id="mailing_mailing_action_mail" model="ir.actions.act_window">
<field name="name">Mailings</field>
<field name="res_model">mailing.mailing</field>
<field name="view_mode">kanban,tree,form,graph</field>
<field name="domain">[('mailing_type', '=', 'mail')]</field>
<field name="context">{
'search_default_assigned_to_me': 1,
'default_user_id': uid,
'default_mailing_type': 'mail',
}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -399,7 +403,10 @@
</field>
</record>
<menuitem name="Mailings" id="mass_mailing_menu" parent="mass_mailing_menu_root"
sequence="1" action="action_view_mass_mailings" groups="mass_mailing.group_mass_mailing_user"/>
<menuitem name="Mailings" id="mass_mailing_menu"
parent="mass_mailing_menu_root"
sequence="1"
action="mailing_mailing_action_mail"
groups="mass_mailing.group_mass_mailing_user"/>
</odoo>
@@ -15,4 +15,10 @@
<menuitem id="mail_blacklist_mm_menu" name="Blacklist"
action="mail.mail_blacklist_action"
parent="mass_mailing_configuration"/>
<!-- Technical / Mass Mailing -->
<menuitem id="mailing_mailing_menu_technical"
name="Mass Mailing"
sequence="3"
parent="base.menu_custom"/>
</odoo>
@@ -8,17 +8,18 @@
<search string="Mail Statistics">
<field name="mail_mail_id_int"/>
<field name="message_id"/>
<field name="email"/>
<field name="mass_mailing_id"/>
<filter string="Sent" name="filter_sent" domain="[('sent', '!=', False)]"/>
<filter string="Received" name="filter_received" domain="[('sent', '!=', False), ('bounced', '=', False)]"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False)]"/>
<filter string="Failed" name="filter_failed" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '!=', False)]"/>
<filter string="Scheduled" name="filter_scheduled" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False), ('ignored', '=', False), ('bounced', '=', False)]"/>
<filter string="Ignored" name="filter_ignored" domain="[('scheduled', '!=', False), ('sent', '=', False), ('exception', '=', False), ('ignored', '!=', False)]"/>
<filter string="Sent" name="filter_sent" domain="[('sent', '!=', False)]"/>
<filter string="Delivered" name="filter_delivered" domain="[('sent', '!=', False), ('exception', '=', False), ('bounced', '=', False)]"/>
<separator/>
<filter string="Opened" name="filter_opened" domain="[('opened', '!=', False)]"/>
<filter string="Clicked" name="filter_clicked" domain="[('clicked', '!=', False)]"/>
<filter string="Replied" name="filter_replied" domain="[('replied', '!=', False)]"/>
<filter string="Bounced" name="filter_bounced" domain="[('bounced', '!=', False)]"/>
<filter string="Failed" name="filter_failed" domain="[('exception', '!=', False)]"/>
<group expand="0" string="Group By">
<filter string="State" name="state" domain="[]" context="{'group_by':'state'}"/>
<filter string="Open Date" name="group_open_date" context="{'group_by': 'opened:day'}"/>
@@ -35,9 +36,11 @@
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<tree string="Mail Statistics" create="0">
<field name="mass_mailing_id"/>
<field name="email"/>
<field name="state"/>
<field name="message_id"/>
<field name="scheduled"/>
<field name="sent"/>
<field name="exception"/>
<field name="opened"/>
@@ -58,27 +61,42 @@
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_form_box_info bg-info oe_text_center alert-info" attrs="{'invisible': [('exception', '=', False)]}" role="alert">
<p><strong>This email could not be sent.</strong></p>
<div class="alert alert-info text-center" role="alert" name="alert_mail_exception"
attrs="{'invisible': [('exception', '=', False), ('bounced', '=', False)]}">
<p>
<strong><span name="trace_type_name_mail">This email</span>
<span attrs="{'invisible': [('exception', '=', False)]}"> could not be sent</span>
<span attrs="{'invisible': [('bounced', '=', False)]}"> appears to be invalid</span>
</strong>
</p>
</div>
<group>
<group>
<field name="email" string="Email"/>
<field name="mass_mailing_id"/>
<field name="campaign_id" groups="mass_mailing.group_mass_mailing_campaign"/>
<field name="mail_mail_id_int" groups="base.group_no_one"/>
<field name="message_id" groups="base.group_no_one"/>
<field name="model" groups="base.group_no_one"/>
<field name="res_id" groups="base.group_no_one"/>
<group string="Recipient">
<field name="trace_type" invisible="1"/>
<field name="email"/>
<field name="mail_mail_id_int"/>
<field name="message_id"/>
</group>
<group string="Document">
<field name="model"/>
<field name="res_id"/>
<field name="state_update"/>
</group>
</group>
<group string="Marketing">
<group>
<field name="exception"/>
<field name="mass_mailing_id"/>
<field name="campaign_id"/>
<field name="sent"/>
<field name="opened"/>
<field name="clicked"/>
<field name="replied"/>
<field name="bounced"/>
</group>
<group>
<field name="exception"/>
<field name="ignored"/>
<field name="bounced"/>
<field name="failure_type"/>
</group>
</group>
</sheet>
@@ -86,28 +104,6 @@
</field>
</record>
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mailing.trace</field>
<field name="view_mode">tree,form</field>
</record>
<record id="action_view_mail_mail_statistics_mailing" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mailing.trace</field>
<field name="view_mode">graph,tree,form,pivot</field>
<field name="domain">[]</field>
<field name="context">{'search_default_mass_mailing_id': active_id}</field>
</record>
<record id="action_view_mail_mail_statistics_mailing_list" model="ir.actions.act_window">
<field name="name">Emails Sent</field>
<field name="res_model">mailing.trace</field>
<field name="view_mode">tree,graph,form,pivot</field>
<field name="domain">[]</field>
<field name="context">{'search_default_mass_mailing_id': active_id}</field>
</record>
<record id="view_mail_mail_statistics_graph" model="ir.ui.view">
<field name="name">Mail Statistics Graph</field>
<field name="model">mailing.trace</field>
@@ -119,8 +115,24 @@
</field>
</record>
<record id="mailing_trace_action" model="ir.actions.act_window">
<field name="name">Mailing Traces</field>
<field name="res_model">mailing.trace</field>
<field name="view_mode">tree,form,graph,pivot</field>
<field name="domain">[]</field>
</record>
<record id="action_view_mail_mail_statistics_mailing" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mailing.trace</field>
<field name="view_mode">graph,tree,form,pivot</field>
<field name="domain">[]</field>
<field name="context">{'search_default_mass_mailing_id': active_id}</field>
</record>
<!-- Add in Technical/Email -->
<menuitem name="Mail Statistics" id="menu_email_statistics"
parent="base.menu_email" sequence="50"
action="action_view_mail_mail_statistics"/>
<menuitem id="menu_email_statistics"
name="Mailing Traces"
parent="mass_mailing.mailing_mailing_menu_technical" sequence="2"
action="mailing_trace_action"/>
</odoo>
@@ -119,8 +119,8 @@
action="utm.action_view_utm_stage"/>
<menuitem id="mass_mailing_tag_menu"
parent="mass_mailing_configuration"
action="utm.action_view_utm_tag"
sequence="2"
groups="base.group_no_one"/>
parent="mass_mailing_configuration"
action="utm.action_view_utm_tag"
sequence="2"
groups="mass_mailing.group_mass_mailing_campaign"/>
</odoo>
@@ -5,7 +5,7 @@
<field name="model">mailing.mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml">
<xpath expr="//button[hasclass('o_mass_mailing_button')]" position="before">
<xpath expr="//button[@id='button_view_sent']" position="before">
<field name="crm_lead_activated" invisible="1"/>
<button name="action_redirect_to_leads"
type="object"
@@ -5,7 +5,7 @@
<field name="model">mailing.mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml">
<xpath expr="//button[hasclass('o_mass_mailing_button')]" position="before">
<xpath expr="//button[@id='button_view_sent']" position="before">
<button name="action_redirect_to_quotations"
type="object"
icon="fa-pencil"
@@ -45,7 +45,7 @@ class TestAutoBlacklist(common.TransactionCase):
'mailing_domain': [('id', 'in',
[self.mailing_contact_1.id])],
'body_html': 'This is a bounced mail for auto blacklist demo'})
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
@@ -91,7 +91,7 @@ class TestAccessRights(TransactionCase):
[self.mailing_contact_1.id, self.mailing_contact_2.id, self.mailing_contact_3.id,
self.mailing_contact_4.id, self.mailing_contact_5.id])],
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
@@ -140,7 +140,7 @@ class TestAccessRights(TransactionCase):
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id]
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -196,7 +196,7 @@ class TestAccessRights(TransactionCase):
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id, mailing_list_2.id]
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -246,7 +246,7 @@ class TestAccessRights(TransactionCase):
'mailing_model_id': self.env['ir.model']._get(mass_mailing_contacts).id,
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.contact_list_ids = [mailing_list_1.id]
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -295,7 +295,7 @@ class TestOnResPartner(TransactionCase):
'mailing_domain': [('id', 'in', [partner_a.id, partner_b.id])],
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.mailing_model_real = 'res.partner'
self.mass_mailing.put_in_queue()
self.mass_mailing.action_put_in_queue()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],