[MERGE] mass_mailing: clean module organization and prepare SMS addition

PURPOSE

Mass mailing is currently a bit messy. As SMS will be added as a way to notify
people in mass through SMS let us take this opportunity to somehow clean this
application: organization, light code cleaning, model renaming.

SPECIFICATIONS

Guidelines: re-organize model files and split them according to main
models. Also reorganize views.

Move some code bits / fields declarations to reorder them. Purpose is to
make code easier to understand and find. Funny we found two override of create
that have been merged. An override of name create is not necessary considering
code was present in one of the two merged create. Make internal methods private
and rename send_mail to action_send_mail to avoid confusion with composer and
template send_mail methods.

Fix UTM management and propagation in mass mailing. Right UTM definition in
mass mailing :

* mass mailing campaign -> utm.campaign
* mass mailing -> utm.source
* "email" -> utm.medium

Therefore :

* remove campaign setting source and medium as each mailing is a source
and medium is "email";
* ensure each mailing is a separate source (otherwise name is shared);
* ensure UTM values propagate to link creation are those values and not
the one coming from the mass mailing campaign;

Rename mass mailing models. Rationale :

* remove mail prefix, as mass mailing will allow to use SMS to contact
people and will soon be less mail-dependent;
* have simpler name, easier to read, find and understand;
* mailing.list/contact/tag seems a better naming than mail.mass_mailing.
list/contact/tag;
* have a mailing prefix for all models used in mass mailing;

MIGRATION

mail.mail.statistics model -> mailing.trace
mail.statistics.report model -> mail.trace.report
mail.mass_mailing.list model -> mailing.list
mail.mass_mailing.list.merge model -> mailing.list.merge
mail.mass_mailing.contact model -> mailing.contact
mail.mass_mailing.list_contact_rel model -> mailing.contact.subscription
mail_mass_mailing_contact_list_rel table -> mailing_contact_list_rel (specific
case of a decorated m2m)
mail.mass_mailing.stage model -> mailing.stage
mail.mass_mailing.tag model -> mailing.tag
mail.mass_mailing model -> mailing.mailing

fields updated (w column change)
* link.tracker.click: mail_stat_id -> mailing_trace_id

fields updated (no column change)
* mail.mail: statistics_ids -> mailing_trace_ids
* mail.mass_mailing (mailing.mailing): statistics_ids -> mailing_trace_ids
* mail.mass_mailing.list (mailing.list): subscription_contact_ids ->
subscription_ids

LINKS

Task ID 2037906
Preparing task ID 1997464 (SMS addition in mass mailing)
PR #34938

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Robot Odoo
2019-07-17 19:47:48 +02:00
committed by GitHub
70 changed files with 1334 additions and 1331 deletions
+1
View File
@@ -3,4 +3,5 @@
from . import controllers
from . import models
from . import report
from . import wizard
+13 -14
View File
@@ -20,26 +20,25 @@
],
'data': [
'security/mass_mailing_security.xml',
'security/ir.model.access.csv',
'data/mail_data.xml',
'data/mass_mailing_data.xml',
'wizard/mail_compose_message_views.xml',
'wizard/mass_mailing_list_merge_views.xml',
'wizard/test_mailing_views.xml',
'wizard/mass_mailing_schedule_date_views.xml',
'views/mass_mailing_views_menus.xml',
'views/mass_mailing_report_views.xml',
'views/mass_mailing_stats_views.xml',
'wizard/mailing_list_merge_views.xml',
'wizard/mailing_mailing_schedule_date_views.xml',
'wizard/mailing_mailing_test_views.xml',
'views/mailing_mailing_views_menus.xml',
'views/mailing_trace_views.xml',
'views/link_tracker_views.xml',
'views/mass_mailing_template.xml',
'views/mass_mailing_views.xml',
'views/mailing_contact_views.xml',
'views/mailing_list_views.xml',
'views/mailing_mailing_views.xml',
'views/res_config_settings_views.xml',
'security/ir.model.access.csv',
'views/mass_mailing_layout.xml',
'views/editor_field_html.xml',
'report/mailing_trace_report_views.xml',
'views/assets.xml',
'views/mass_mailing_templates_portal.xml',
'views/themes_templates.xml',
'views/snippets_themes.xml',
'views/snippets_themes_options.xml',
'views/mass_mailing_blacklist_views.xml',
'views/unsubscribe_templates.xml',
],
'demo': [
'data/mass_mailing_demo.xml',
+13 -13
View File
@@ -14,11 +14,11 @@ class MassMailController(http.Controller):
def _valid_unsubscribe_token(self, mailing_id, res_id, email, token):
if not (mailing_id and res_id and email and token):
return False
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
return consteq(mailing._unsubscribe_token(res_id, email), token)
def _log_blacklist_action(self, blacklist_entry, mailing_id, description):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
model_display = mailing.mailing_model_id.display_name
blacklist_entry._message_log(body=description + " ({})".format(model_display))
@@ -29,17 +29,17 @@ class MassMailController(http.Controller):
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
def mailing(self, mailing_id, email=None, res_id=None, token="", **post):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
res_id = res_id and int(res_id)
if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)):
raise exceptions.AccessDenied()
if mailing.mailing_model_real == 'mail.mass_mailing.contact':
if mailing.mailing_model_real == 'mailing.contact':
# Unsubscribe directly + Let the user choose his subscriptions
mailing.update_opt_out(email, mailing.contact_list_ids.ids, True)
contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
contacts = request.env['mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))])
subscription_list_ids = contacts.mapped('subscription_list_ids')
# In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2,
# assume that the user is not opt_out on both
@@ -49,7 +49,7 @@ class MassMailController(http.Controller):
opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids])
unique_list_ids = set([list.list_id.id for list in subscription_list_ids])
list_ids = request.env['mail.mass_mailing.list'].sudo().browse(unique_list_ids)
list_ids = request.env['mailing.list'].sudo().browse(unique_list_ids)
unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public)
return request.render('mass_mailing.page_unsubscribe', {
'contacts': contacts,
@@ -62,7 +62,7 @@ class MassMailController(http.Controller):
'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'),
})
else:
opt_in_lists = request.env['mail.mass_mailing.list_contact_rel'].sudo().search([
opt_in_lists = request.env['mailing.contact.subscription'].sudo().search([
('contact_id.email_normalized', '=', email),
('opt_out', '=', False)
]).mapped('list_id')
@@ -82,7 +82,7 @@ class MassMailController(http.Controller):
@http.route('/mail/mailing/unsubscribe', type='json', auth='public')
def unsubscribe(self, mailing_id, opt_in_ids, opt_out_ids, email, res_id, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists():
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
return 'unauthorized'
@@ -94,15 +94,15 @@ class MassMailController(http.Controller):
@http.route('/mail/track/<int:mail_id>/blank.gif', type='http', auth='public')
def track_mail_open(self, mail_id, **post):
""" Email tracking. """
request.env['mail.mail.statistics'].sudo().set_opened(mail_mail_ids=[mail_id])
request.env['mailing.trace'].sudo().set_opened(mail_mail_ids=[mail_id])
response = werkzeug.wrappers.Response()
response.mimetype = 'image/gif'
response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')
return response
@http.route('/r/<string:code>/m/<int:stat_id>', type='http', auth="public")
def full_url_redirect(self, code, stat_id, **post):
@http.route('/r/<string:code>/m/<int:mailing_trace_id>', type='http', auth="public")
def full_url_redirect(self, code, mailing_trace_id, **post):
# don't assume geoip is set, it is part of the website module
# which mass_mailing doesn't depend on
country_code = request.session.get('geoip', False) and request.session.geoip.get('country_code', False)
@@ -111,7 +111,7 @@ class MassMailController(http.Controller):
code,
ip=request.httprequest.remote_addr,
country_code=country_code,
mail_stat_id=stat_id
mailing_trace_id=mailing_trace_id
)
return werkzeug.utils.redirect(request.env['link.tracker'].get_url_from_code(code), 301)
@@ -152,7 +152,7 @@ class MassMailController(http.Controller):
@http.route('/mailing/feedback', type='json', auth='public')
def send_feedback(self, mailing_id, res_id, email, feedback, token):
mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id)
mailing = request.env['mailing.mailing'].sudo().browse(mailing_id)
if mailing.exists() and email:
if not self._valid_unsubscribe_token(mailing_id, res_id, email, token):
return 'unauthorized'
@@ -1,22 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="mass_mailing_mail_style">
<style>
@media screen and (max-width: 768px) {
.o_mail_col_mv {
display: block !important;
width: auto !important;
}
.o_mail_table_styles {
width: 100% !important;
}
.o_mail_col_container {
margin: 0px 0px 10px 0px !important;
}
}
</style>
</template>
<odoo><data noupdate="1">
<!-- Reference: https://litmus.com/community/learning/24-how-to-code-a-responsive-email-from-scratch -->
<template id="mass_mailing_mail_layout">
&lt;!DOCTYPE html&gt;
@@ -34,4 +17,4 @@
</body>
</html>
</template>
</odoo>
</data></odoo>
@@ -3,15 +3,15 @@
<data noupdate="1">
<!-- Default stages of mass mailing campaigns -->
<record id="campaign_stage_1" model="mail.mass_mailing.stage">
<record id="campaign_stage_1" model="mailing.stage">
<field name="name">Schedule</field>
<field name="sequence">10</field>
</record>
<record id="campaign_stage_2" model="mail.mass_mailing.stage">
<record id="campaign_stage_2" model="mailing.stage">
<field name="name">Design</field>
<field name="sequence">20</field>
</record>
<record id="campaign_stage_3" model="mail.mass_mailing.stage">
<record id="campaign_stage_3" model="mailing.stage">
<field name="name">Sent</field>
<field name="sequence">30</field>
</record>
@@ -19,7 +19,7 @@
<!-- Cron that process the mass mailing queue -->
<record id="ir_cron_mass_mailing_queue" model="ir.cron">
<field name="name">Mass Mailing: Process queue</field>
<field name="model_id" ref="model_mail_mass_mailing"/>
<field name="model_id" ref="model_mailing_mailing"/>
<field name="state">code</field>
<field name="code">model._process_mass_mailing_queue()</field>
<field name="user_id" ref="base.user_root" />
+22 -23
View File
@@ -8,38 +8,38 @@
</record>
<!-- Create mailing lists -->
<record id="mass_mail_list_2" model="mail.mass_mailing.list">
<record id="mass_mail_list_2" model="mailing.list">
<field name="name">Imported Contacts</field>
</record>
<!-- Create Contacts -->
<record id="mass_mail_contact_1" model="mail.mass_mailing.contact">
<record id="mass_mail_contact_1" model="mailing.contact">
<field name="name">Aristide Antario</field>
<field name="email">aa@example.com</field>
<field name="list_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_list_2')])]"/>
</record>
<record id="mass_mail_contact_2" model="mail.mass_mailing.contact">
<record id="mass_mail_contact_2" model="mailing.contact">
<field name="name">Beverly Bridge</field>
<field name="email">bb@example.com</field>
<field name="list_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_list_2')])]"/>
</record>
<record id="mass_mail_contact_3" model="mail.mass_mailing.contact">
<record id="mass_mail_contact_3" model="mailing.contact">
<field name="name">Carol Cartridge</field>
<field name="email">cc@example.com</field>
<field name="list_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_list_2')])]"/>
</record>
<record id="mass_mail_contact_4" model="mail.mass_mailing.contact">
<record id="mass_mail_contact_4" model="mailing.contact">
<field name="name">David Dawson</field>
<field name="email">dd@example.com</field>
</record>
<record id="mass_mail_contact_5" model="mail.mass_mailing.contact">
<record id="mass_mail_contact_5" model="mailing.contact">
<field name="name">Elsa Ericson</field>
<field name="email">ee@example.com</field>
<field name="list_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_list_2')])]"/>
</record>
<!-- Create Opt-out Records -->
<record id="mass_mail_contact_list_rel_1" model="mail.mass_mailing.list_contact_rel">
<record id="mass_mail_contact_list_rel_1" model="mailing.contact.subscription">
<field name="list_id" eval="ref('mass_mailing.mass_mail_list_2')"/>
<field name="contact_id" eval="ref('mass_mailing.mass_mail_contact_4')"/>
<field name="opt_out">True</field>
@@ -51,7 +51,7 @@
</record>
<!-- Create campaign and mailings -->
<record id="mass_mail_tag_1" model="mail.mass_mailing.tag">
<record id="mass_mail_tag_1" model="mailing.tag">
<field name="name">Marketing</field>
<field name="color" eval="1"/>
</record>
@@ -68,7 +68,7 @@
<field name="tag_ids" eval="[(6,0,[ref('mass_mailing.mass_mail_tag_1')])]"/>
</record>
<record id="mass_mail_1" model="mail.mass_mailing">
<record id="mass_mail_1" model="mailing.mailing">
<field name="name">Newsletter 1</field>
<field name="subject" eval="'%s Newsletter' % (DateTime.today().strftime('%B'))"/>
<field name="state">done</field>
@@ -78,7 +78,6 @@
<field name="mass_mailing_campaign_id" eval="ref('mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="mass_mailing.utm_source_0"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="mailing_model_id" ref="base.model_res_partner"/>
<field name="mailing_domain" eval="[('parent_id', '=', ref('base.res_partner_4'))]"/>
<field name="reply_to_mode">email</field>
@@ -197,9 +196,9 @@
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
</record>
<!-- Generate link tracker information from it -->
<function model="mail.mass_mailing" name="convert_links" eval="[ref('mass_mailing.mass_mail_1')]"/>
<function model="mailing.mailing" name="convert_links" eval="[ref('mass_mailing.mass_mail_1')]"/>
<record id="mass_mail_1_stat_0" model="mail.mail.statistics">
<record id="mass_mail_1_stat_0" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111000@odoo.com</field>
<field name="model">res.partner</field>
@@ -208,7 +207,7 @@
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_1" model="mail.mail.statistics">
<record id="mass_mail_1_stat_1" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111001@odoo.com</field>
<field name="model">res.partner</field>
@@ -217,7 +216,7 @@
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="replied" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_2" model="mail.mail.statistics">
<record id="mass_mail_1_stat_2" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111002@odoo.com</field>
<field name="model">res.partner</field>
@@ -225,7 +224,7 @@
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_3" model="mail.mail.statistics">
<record id="mass_mail_1_stat_3" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111003@odoo.com</field>
<field name="model">res.partner</field>
@@ -233,7 +232,7 @@
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_4" model="mail.mail.statistics">
<record id="mass_mail_1_stat_4" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@odoo.com</field>
<field name="model">res.partner</field>
@@ -241,7 +240,7 @@
<field name="sent" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="opened" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_5" model="mail.mail.statistics">
<record id="mass_mail_1_stat_5" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@odoo.com</field>
<field name="model">res.partner</field>
@@ -249,7 +248,7 @@
<field name="sent" eval="False"/>
<field name="exception" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
<record id="mass_mail_1_stat_6" model="mail.mail.statistics">
<record id="mass_mail_1_stat_6" model="mailing.trace">
<field name="mass_mailing_id" eval="ref('mass_mail_1')"/>
<field name="message_id">1111004@odoo.com</field>
<field name="model">res.partner</field>
@@ -265,7 +264,7 @@
use="code"/>
<value name="ip">100.01.02.03</value>
<value name="country_code">BE</value>
<value name="mail_stat_id" eval="ref('mass_mail_1_stat_0')"/>
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_0')"/>
</function>
<function model="link.tracker.click" name="add_click">
<value model="link.tracker.code"
@@ -273,7 +272,7 @@
use="code"/>
<value name="ip">100.01.02.03</value>
<value name="country_code">BE</value>
<value name="mail_stat_id" eval="ref('mass_mail_1_stat_0')"/>
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_0')"/>
</function>
<function model="link.tracker.click" name="add_click">
<value model="link.tracker.code"
@@ -281,7 +280,7 @@
use="code"/>
<value name="ip">100.01.02.04</value>
<value name="country_code">BE</value>
<value name="mail_stat_id" eval="ref('mass_mail_1_stat_1')"/>
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_1')"/>
</function>
<function model="link.tracker.click" name="add_click">
<value model="link.tracker.code"
@@ -289,7 +288,7 @@
use="code"/>
<value name="ip">100.01.02.04</value>
<value name="country_code">BE</value>
<value name="mail_stat_id" eval="ref('mass_mail_1_stat_0')"/>
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_0')"/>
</function>
<function model="link.tracker.click" name="add_click">
<value model="link.tracker.code"
@@ -297,7 +296,7 @@
use="code"/>
<value name="ip">100.01.02.05</value>
<value name="country_code">BE</value>
<value name="mail_stat_id" eval="ref('mass_mail_1_stat_2')"/>
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_2')"/>
</function>
</data>
+5 -4
View File
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mass_mailing
from . import mass_mailing_stats
from . import link_tracker
from . import mailing_contact
from . import mailing_list
from . import mailing_trace
from . import mailing
from . import mail_mail
from . import mail_template
from . import mail_thread
from . import res_config_settings
from . import mass_mailing_report
from . import link_tracker
+12 -12
View File
@@ -7,29 +7,29 @@ from odoo import api, fields, models
class LinkTracker(models.Model):
_inherit = "link.tracker"
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing')
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
class LinkTrackerClick(models.Model):
_inherit = "link.tracker.click"
mail_stat_id = fields.Many2one('mail.mail.statistics', string='Mail Statistics')
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing')
mailing_trace_id = fields.Many2one('mailing.trace', string='Mail Statistics')
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
def _prepare_click_values_from_route(self, **route_values):
click_values = super(LinkTrackerClick, self)._prepare_click_values_from_route(**route_values)
if click_values.get('mail_stat_id'):
stat_sudo = self.env['mail.mail.statistics'].sudo().browse(route_values['mail_stat_id']).exists()
if not stat_sudo:
click_values['mail_stat_id'] = False
if click_values.get('mailing_trace_id'):
trace_sudo = self.env['mailing.trace'].sudo().browse(route_values['mailing_trace_id']).exists()
if not trace_sudo:
click_values['mailing_trace_id'] = False
else:
if not click_values.get('mass_mailing_campaign_id'):
click_values['mass_mailing_campaign_id'] = stat_sudo.mass_mailing_campaign_id.id
click_values['mass_mailing_campaign_id'] = trace_sudo.mass_mailing_campaign_id.id
if not click_values.get('mass_mailing_id'):
click_values['mass_mailing_id'] = stat_sudo.mass_mailing_id.id
click_values['mass_mailing_id'] = trace_sudo.mass_mailing_id.id
return click_values
@@ -37,8 +37,8 @@ class LinkTrackerClick(models.Model):
def add_click(self, code, **route_values):
click = super(LinkTrackerClick, self).add_click(code, **route_values)
if click and click.mail_stat_id:
click.mail_stat_id.set_opened()
click.mail_stat_id.set_clicked()
if click and click.mailing_trace_id:
click.mailing_trace_id.set_opened()
click.mailing_trace_id.set_clicked()
return click
+13 -13
View File
@@ -13,17 +13,17 @@ class MailMail(models.Model):
"""Add the mass mailing campaign data to mail"""
_inherit = ['mail.mail']
mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing')
statistics_ids = fields.One2many('mail.mail.statistics', 'mail_mail_id', string='Statistics')
mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
mailing_trace_ids = fields.One2many('mailing.trace', 'mail_mail_id', string='Statistics')
@api.model
def create(self, values):
""" Override mail_mail creation to create an entry in mail.mail.statistics """
""" Override mail_mail creation to create an entry in mailing.trace """
# TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring)
mail = super(MailMail, self).create(values)
if values.get('statistics_ids'):
if values.get('mailing_trace_ids'):
mail_sudo = mail.sudo()
mail_sudo.statistics_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'})
mail_sudo.mailing_trace_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'})
return mail
def _get_tracking_url(self):
@@ -54,12 +54,12 @@ class MailMail(models.Model):
def _send_prepare_body(self):
""" Override to add the tracking URL to the body and to add
Statistic_id in shorted urls """
trace ID in shortened urls """
# TDE: temporary addition (mail was parameter) due to semi-new-API
self.ensure_one()
body = super(MailMail, self)._send_prepare_body()
if self.mailing_id and body and self.statistics_ids:
if self.mailing_id and body and self.mailing_trace_ids:
for match in re.findall(URL_REGEX, self.body_html):
href = match[0]
url = match[1]
@@ -67,7 +67,7 @@ class MailMail(models.Model):
parsed = werkzeug.urls.url_parse(url, scheme='http')
if parsed.scheme.startswith('http') and parsed.path.startswith('/r/'):
new_href = href.replace(url, url + '/m/' + str(self.statistics_ids[0].id))
new_href = href.replace(url, url + '/m/' + str(self.mailing_trace_ids[0].id))
body = body.replace(href, new_href)
# generate tracking URL
@@ -96,8 +96,8 @@ class MailMail(models.Model):
mail_sent = not failure_type # we consider that a recipient error is a failure with mass mailling and show them as failed
for mail in self:
if mail.mailing_id:
if mail_sent is True and mail.statistics_ids:
mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
elif mail_sent is False and mail.statistics_ids:
mail.statistics_ids.write({'exception': fields.Datetime.now()})
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
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()})
return super(MailMail, self)._postprocess_sent_message(success_pids, failure_reason=failure_reason, failure_type=failure_type)
+10 -10
View File
@@ -13,12 +13,12 @@ BLACKLIST_MAX_BOUNCED_LIMIT = 5
class MailThread(models.AbstractModel):
""" Update MailThread to add the support of bounce management in mass mailing statistics. """
""" Update MailThread to add the support of bounce management in mass mailing traces. """
_inherit = 'mail.thread'
@api.model
def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
""" Override to udpate mass mailing statistics based on bounce emails """
""" Override to udpate mass mailing traces based on bounce emails """
bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias")
email_to = decode_message_header(message, 'To')
email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower()
@@ -28,19 +28,19 @@ class MailThread(models.AbstractModel):
bounce_match = bounce_re.search(email_to)
if bounce_match:
bounced_mail_id = bounce_match.group(1)
self.env['mail.mail.statistics'].set_bounced(mail_mail_ids=[bounced_mail_id])
self.env['mailing.trace'].set_bounced(mail_mail_ids=[bounced_mail_id])
return super(MailThread, self).message_route(message, message_dict, model, thread_id, custom_values)
@api.model
def message_route_process(self, message, message_dict, routes):
""" Override to update the parent mail statistics. The parent is found
""" Override to update the parent mailing traces. The parent is found
by using the References header of the incoming message and looking for
matching message_id in mail.mail.statistics. """
matching message_id in mailing.trace. """
if message.get('References') and routes:
message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()]
self.env['mail.mail.statistics'].set_opened(mail_message_ids=message_ids)
self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids)
self.env['mailing.trace'].set_opened(mail_message_ids=message_ids)
self.env['mailing.trace'].set_replied(mail_message_ids=message_ids)
return super(MailThread, self).message_route_process(message, message_dict, routes)
def message_post_with_template(self, template_id, **kwargs):
@@ -64,11 +64,11 @@ class MailThread(models.AbstractModel):
super(MailThread, self)._message_receive_bounce(email, partner, mail_id=None)
three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13))
stats = self.env['mail.mail.statistics'] \
traces = self.env['mailing.trace'] \
.search(['&', ('bounced', '>', three_months_ago), ('email', '=ilike', email)]) \
.mapped('bounced')
if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT:
if max(stats) > min(stats) + datetime.timedelta(weeks=1):
if len(traces) >= BLACKLIST_MAX_BOUNCED_LIMIT:
if max(traces) > min(traces) + datetime.timedelta(weeks=1):
blacklist_rec = self.env['mail.blacklist'].sudo()._add(email)
blacklist_rec._message_log(
body='This email has been automatically blacklisted because of too much bounced.')
@@ -24,8 +24,8 @@ MASS_MAILING_BUSINESS_MODELS = [
'res.partner',
'event.track',
'sale.order',
'mail.mass_mailing.list',
'mail.mass_mailing.contact'
'mailing.list',
'mailing.contact'
]
# Syntax of the data URL Scheme: https://tools.ietf.org/html/rfc2397#section-3
@@ -33,9 +33,9 @@ MASS_MAILING_BUSINESS_MODELS = [
image_re = re.compile(r"data:(image/[A-Za-z]+);base64,(.*)")
class MassMailingTag(models.Model):
class MailingTag(models.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.tag'
_name = 'mailing.tag'
_description = 'Mass Mailing Tag'
_order = 'name'
@@ -43,256 +43,14 @@ class MassMailingTag(models.Model):
color = fields.Integer(string='Color Index')
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
class MassMailingContactListRel(models.Model):
""" Intermediate model between mass mailing list and mass mailing contact
Indicates if a contact is opted out for a particular list
"""
_name = 'mail.mass_mailing.list_contact_rel'
_description = 'Mass Mailing Subscription Information'
_table = 'mail_mass_mailing_contact_list_rel'
_rec_name = 'contact_id'
contact_id = fields.Many2one('mail.mass_mailing.contact', string='Contact', ondelete='cascade', required=True)
list_id = fields.Many2one('mail.mass_mailing.list', string='Mailing List', ondelete='cascade', required=True)
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)
_sql_constraints = [
('unique_contact_list', 'unique (contact_id, list_id)',
'A contact cannot be subscribed multiple times to the same list!')
]
@api.model
def create(self, vals):
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContactListRel, self).create(vals)
def write(self, vals):
if 'opt_out' in vals:
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': 'mail.mass_mailing.contact',
'view_mode': 'form',
'target': 'current',
'res_id': contact_id.id
}
return action
class MassMailingList(models.Model):
"""Model of a contact list. """
_name = 'mail.mass_mailing.list'
_order = 'name'
_description = 'Mailing List'
name = fields.Char(string='Mailing List', required=True)
active = fields.Boolean(default=True)
contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts')
contact_ids = fields.Many2many(
'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id',
string='Mailing Lists')
subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id',
string='Subscription Information')
is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription"
" page to allows him to update his subscription preferences.")
# Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list
def _compute_contact_nbr(self):
self.env.cr.execute('''
select
list_id, count(*)
from
mail_mass_mailing_contact_list_rel r
left join mail_mass_mailing_contact c on (r.contact_id=c.id)
left join mail_blacklist bl on c.email_normalized = bl.email and bl.active
where
list_id in %s
AND COALESCE(r.opt_out,FALSE) = FALSE
AND c.email_normalized IS NOT NULL
AND bl.id IS NULL
group by
list_id
''', (tuple(self.ids), ))
data = dict(self.env.cr.fetchall())
for mailing_list in self:
mailing_list.contact_nbr = data.get(mailing_list.id, 0)
def name_get(self):
return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self]
def action_merge(self, src_lists, archive):
"""
Insert all the contact from the mailing lists 'src_lists' to the
mailing list in 'self'. Possibility to archive the mailing lists
'src_lists' after the merge except the destination mailing list 'self'.
"""
# Explation of the SQL query with an example. There are the following lists
# A (id=4): yti@odoo.com; yti@example.com
# B (id=5): yti@odoo.com; yti@openerp.com
# C (id=6): nothing
# To merge the mailing lists A and B into C, we build the view st that looks
# like this with our example:
#
# contact_id | email | row_number | list_id |
# ------------+---------------------------+------------------------
# 4 | yti@odoo.com | 1 | 4 |
# 6 | yti@odoo.com | 2 | 5 |
# 5 | yti@example.com | 1 | 4 |
# 7 | yti@openerp.com | 1 | 5 |
#
# The row_column is kind of an occurence counter for the email address.
# Then we create the Many2many relation between the destination list and the contacts
# while avoiding to insert an existing email address (if the destination is in the source
# for example)
self.ensure_one()
# Put destination is sources lists if not already the case
src_lists |= self
self.env.cr.execute("""
INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id)
SELECT st.contact_id AS contact_id, %s AS list_id
FROM
(
SELECT
contact.id AS contact_id,
contact.email AS email,
mailing_list.id AS list_id,
row_number() OVER (PARTITION BY email ORDER BY email) AS rn
FROM
mail_mass_mailing_contact contact,
mail_mass_mailing_contact_list_rel contact_list_rel,
mail_mass_mailing_list mailing_list
WHERE contact.id=contact_list_rel.contact_id
AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE
AND contact.email_normalized NOT IN (select email from mail_blacklist where active = TRUE)
AND mailing_list.id=contact_list_rel.list_id
AND mailing_list.id IN %s
AND NOT EXISTS
(
SELECT 1
FROM
mail_mass_mailing_contact contact2,
mail_mass_mailing_contact_list_rel contact_list_rel2
WHERE contact2.email = contact.email
AND contact_list_rel2.contact_id = contact2.id
AND contact_list_rel2.list_id = %s
)
) st
WHERE st.rn = 1;""", (self.id, tuple(src_lists.ids), self.id))
self.invalidate_cache()
if archive:
(src_lists - self).write({'active': False})
def close_dialog(self):
return {'type': 'ir.actions.act_window_close'}
class MassMailingContact(models.Model):
"""Model of a contact. This model is different from the partner model
because it holds only some basic information: name, email. The purpose is to
be able to deal with large contact list to email without bloating the partner
base."""
_name = 'mail.mass_mailing.contact'
_inherit = ['mail.thread.blacklist']
_description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
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)
list_ids = fields.Many2many(
'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel',
'contact_id', 'list_id', string='Mailing Lists')
subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel',
'contact_id', string='Subscription Information')
country_id = fields.Many2one('res.country', string='Country')
tag_ids = fields.Many2many('res.partner.category', string='Tags')
opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out',
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
if operator != '=':
if operator == '!=' and isinstance(value, bool):
value = not value
else:
raise NotImplementedError()
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
[active_list_id] = self._context['default_list_ids']
contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
else:
raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')
@api.depends('subscription_list_ids')
def _compute_opt_out(self):
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
[active_list_id] = self._context['default_list_ids']
for record in self:
active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id)
record.opt_out = active_subscription_list.opt_out
else:
for record in self:
record.opt_out = False
def get_name_email(self, name):
name, email = self.env['res.partner']._parse_partner_name(name)
if name and not email:
email = name
if email and not name:
name = email
return name, email
@api.model
def name_create(self, name):
name, email = self.get_name_email(name)
contact = self.create({'name': name, 'email': email})
return contact.name_get()[0]
@api.model
def add_to_list(self, name, list_id):
name, email = self.get_name_email(name)
contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]})
return contact.name_get()[0]
def _message_get_default_recipients(self):
return {r.id: {
'partner_ids': [],
'email_to': r.email_normalized,
'email_cc': False}
for r in self}
class MassMailingStage(models.Model):
class MailingStage(models.Model):
"""Stage for mass mailing campaigns. """
_name = 'mail.mass_mailing.stage'
_name = 'mailing.stage'
_description = 'Mass Mailing Campaign Stage'
_order = 'sequence'
@@ -307,8 +65,8 @@ class MassMailingCampaign(models.Model):
_rec_name = "campaign_id"
_inherits = {'utm.campaign': 'campaign_id'}
stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True,
default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1),
stage_id = fields.Many2one('mailing.stage', string='Stage', ondelete='restrict', required=True,
default=lambda self: self.env['mailing.stage'].search([], limit=1),
group_expand='_group_expand_stage_ids')
user_id = fields.Many2one(
'res.users', string='Responsible',
@@ -320,10 +78,10 @@ class MassMailingCampaign(models.Model):
medium_id = fields.Many2one('utm.medium', string='Medium',
help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email', False))
tag_ids = fields.Many2many(
'mail.mass_mailing.tag', 'mail_mass_mailing_tag_rel',
'mailing.tag', 'mail_mass_mailing_tag_rel',
'tag_id', 'campaign_id', string='Tags')
mass_mailing_ids = fields.One2many(
'mail.mass_mailing', 'mass_mailing_campaign_id',
'mailing.mailing', 'mass_mailing_campaign_id',
string='Mass Mailings')
unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False,
help='If checked, recipients will be mailed only once for the whole campaign. '
@@ -331,7 +89,7 @@ class MassMailingCampaign(models.Model):
'the effectiveness of the mailings, without causing duplicate messages.')
color = fields.Integer(string='Color Index')
clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of clicks")
# stat fields
# trace statistics fields
total = fields.Integer(compute="_compute_statistics")
scheduled = fields.Integer(compute="_compute_statistics")
failed = fields.Integer(compute="_compute_statistics")
@@ -349,9 +107,9 @@ class MassMailingCampaign(models.Model):
def _compute_clicks_ratio(self):
self.env.cr.execute("""
SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id
FROM mail_mail_statistics AS stats
LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mailing_trace_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id
FROM mailing_trace AS stats
LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mailing_trace_id = stats.id
WHERE stats.mass_mailing_campaign_id IN %s
GROUP BY stats.mass_mailing_campaign_id
""", (tuple(self.ids), ))
@@ -376,7 +134,7 @@ class MassMailingCampaign(models.Model):
COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied ,
COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
FROM
mail_mail_statistics s
mailing_trace s
RIGHT JOIN
mail_mass_mailing_campaign c
ON (c.id = s.mass_mailing_campaign_id)
@@ -396,14 +154,14 @@ class MassMailingCampaign(models.Model):
self.browse(row.pop('campaign_id')).update(row)
def _compute_total_mailings(self):
campaign_data = self.env['mail.mass_mailing'].read_group(
campaign_data = self.env['mailing.mailing'].read_group(
[('mass_mailing_campaign_id', 'in', self.ids)],
['mass_mailing_campaign_id'], ['mass_mailing_campaign_id'])
mapped_data = dict([(c['mass_mailing_campaign_id'][0], c['mass_mailing_campaign_id_count']) for c in campaign_data])
for campaign in self:
campaign.total_mailings = mapped_data.get(campaign.id, 0)
def get_recipients(self, model=None):
def _get_recipients(self, model=None):
"""Return the recipients of a mailing campaign. This is based on the statistics
build for each mailing. """
res = dict.fromkeys(self.ids, {})
@@ -411,7 +169,7 @@ class MassMailingCampaign(models.Model):
domain = [('mass_mailing_campaign_id', '=', campaign.id)]
if model:
domain += [('model', '=', model)]
res[campaign.id] = set(self.env['mail.mail.statistics'].search(domain).mapped('res_id'))
res[campaign.id] = set(self.env['mailing.trace'].search(domain).mapped('res_id'))
return res
@api.model
@@ -426,8 +184,7 @@ class MassMailingCampaign(models.Model):
class MassMailing(models.Model):
""" MassMailing models a wave of emails for a mass mailign campaign.
A mass mailing is an occurence of sending emails. """
_name = 'mail.mass_mailing'
_name = 'mailing.mailing'
_description = 'Mass Mailing'
# number of periods for tracking mail_mail statistics
_period_number = 6
@@ -448,7 +205,7 @@ class MassMailing(models.Model):
def default_get(self, fields):
res = super(MassMailing, self).default_get(fields)
if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'):
if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']:
if res['mailing_model_real'] in ['res.partner', 'mailing.contact']:
res['reply_to_mode'] = 'email'
else:
res['reply_to_mode'] = 'thread'
@@ -471,8 +228,7 @@ class MassMailing(models.Model):
help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
source_id = fields.Many2one('utm.source', string='Source', required=True, ondelete='cascade',
help="This is the link source, e.g. Search Engine, another domain, or name of email list")
medium_id = fields.Many2one('utm.medium', string='Medium',
help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email'))
medium_id = fields.Many2one('utm.medium', string='Medium', help="Delivery method: Email")
clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks")
state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')],
string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states')
@@ -484,20 +240,20 @@ class MassMailing(models.Model):
reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address',
default=lambda self: self.env['mail.message']._get_default_from())
# recipients
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mailing.contact', required=True)
mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
default=lambda self: self.env.ref('mass_mailing.model_mailing_list').id)
mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True)
mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server',
default=_get_default_mail_server_id,
help="Use a specific mail server in priority. Otherwise Odoo relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.")
contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
contact_list_ids = fields.Many2many('mailing.list', 'mail_mass_mailing_list_rel',
string='Mailing Lists')
contact_ab_pc = fields.Integer(string='A/B Testing percentage',
help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100)
# statistics data
statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics')
mailing_trace_ids = fields.One2many('mailing.trace', 'mass_mailing_id', string='Emails Statistics')
total = fields.Integer(compute="_compute_total")
scheduled = fields.Integer(compute="_compute_statistics")
expected = fields.Integer(compute="_compute_statistics")
@@ -517,13 +273,13 @@ class MassMailing(models.Model):
def _compute_total(self):
for mass_mailing in self:
mass_mailing.total = len(mass_mailing.sudo().get_recipients())
mass_mailing.total = len(mass_mailing.sudo()._get_recipients())
def _compute_clicks_ratio(self):
self.env.cr.execute("""
SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id
FROM mail_mail_statistics AS stats
LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mailing_trace_id)) AS nb_clicks, stats.mass_mailing_id AS id
FROM mailing_trace AS stats
LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mailing_trace_id = stats.id
WHERE stats.mass_mailing_id IN %s
GROUP BY stats.mass_mailing_id
""", (tuple(self.ids), ))
@@ -536,7 +292,7 @@ class MassMailing(models.Model):
@api.depends('mailing_model_id')
def _compute_model(self):
for record in self:
record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact'
record.mailing_model_real = (record.mailing_model_name != 'mailing.list') and record.mailing_model_name or 'mailing.contact'
def _compute_statistics(self):
""" Compute statistics of the mass mailing """
@@ -555,9 +311,9 @@ class MassMailing(models.Model):
COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
FROM
mail_mail_statistics s
mailing_trace s
RIGHT JOIN
mail_mass_mailing m
mailing_mailing m
ON (m.id = s.mass_mailing_id)
WHERE
m.id IN %s
@@ -573,24 +329,6 @@ class MassMailing(models.Model):
row['bounced_ratio'] = 100.0 * row['bounced'] / total
self.browse(row.pop('mailing_id')).update(row)
def _unsubscribe_token(self, res_id, email):
"""Generate a secure hash for this mailing list and parameters.
This is appended to the unsubscription URL and then checked at
unsubscription time to ensure no malicious unsubscriptions are
performed.
:param int res_id:
ID of the resource that will be unsubscribed.
:param str email:
Email of the resource that will be unsubscribed.
"""
secret = self.env["ir.config_parameter"].sudo().get_param(
"database.secret")
token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email))
return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest()
def _compute_next_departure(self):
cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
str2dt = fields.Datetime.from_string
@@ -605,16 +343,13 @@ class MassMailing(models.Model):
@api.onchange('mass_mailing_campaign_id')
def _onchange_mass_mailing_campaign_id(self):
if self.mass_mailing_campaign_id:
dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id,
'source_id': self.mass_mailing_campaign_id.source_id,
'medium_id': self.mass_mailing_campaign_id.medium_id}
self.update(dic)
self.campaign_id = self.mass_mailing_campaign_id.campaign_id.id
@api.onchange('mailing_model_id', 'contact_list_ids')
def _onchange_model_and_list(self):
mailing_domain = []
if self.mailing_model_name:
if self.mailing_model_name == 'mail.mass_mailing.list':
if self.mailing_model_name == 'mailing.list':
if self.contact_list_ids:
mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids))
else:
@@ -632,21 +367,24 @@ class MassMailing(models.Model):
if self.subject and not self.name:
self.name = self.subject
#------------------------------------------------------
# Technical stuff
#------------------------------------------------------
# ------------------------------------------------------
# ORM
# ------------------------------------------------------
@api.model
def name_create(self, name):
""" _rec_name is source_id, creates a utm.source instead """
mass_mailing = self.create({'name': name, 'subject': name})
return mass_mailing.name_get()[0]
def create(self, values):
if values.get('name') and not values.get('subject'):
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:
values['medium_id'] = self.env.ref('utm.utm_medium_email').id
return super(MassMailing, self).create(values)
@api.model
def create(self, vals):
if vals.get('name') and not vals.get('subject'):
vals['subject'] = vals['name']
return super(MassMailing, self).create(vals)
def write(self, values):
if values.get('body_html'):
values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
return super(MassMailing, self).write(values)
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
@@ -658,74 +396,9 @@ class MassMailing(models.Model):
def _group_expand_states(self, states, domain, order):
return [key for key, val in type(self).state.selection]
def update_opt_out(self, email, list_ids, value):
if len(list_ids) > 0:
model = self.env['mail.mass_mailing.contact'].with_context(active_test=False)
records = model.search([('email_normalized', '=', tools.email_normalize(email))])
opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([
('contact_id', 'in', records.ids),
('list_id', 'in', list_ids),
('opt_out', '!=', value)
])
opt_out_records.write({'opt_out': value})
message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)')
for record in records:
# filter the list_id by record
record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
if len(record_lists) > 0:
record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id'))))
@api.model
def create(self, values):
if values.get('body_html'):
values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
return super(MassMailing, self).create(values)
def write(self, values):
if values.get('body_html'):
values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
return super(MassMailing, self).write(values)
def _convert_inline_images_to_urls(self, body_html):
"""
Find inline base64 encoded images, make an attachement out of
them and replace the inline image with an url to the attachement.
"""
def _image_to_url(b64image: bytes):
"""Store an image in an attachement and returns an url"""
attachment = self.env['ir.attachment'].create({
'datas': b64image,
'name': "cropped_image_mailing_{}".format(self.id),
'type': 'binary',})
attachment.generate_access_token()
return '/web/image/%s?access_token=%s' % (
attachment.id, attachment.access_token)
modified = False
root = lxml.html.fromstring(body_html)
for node in root.iter('img'):
match = image_re.match(node.attrib.get('src', ''))
if match:
mime = match.group(1) # unsed
image = match.group(2).encode() # base64 image as bytes
node.attrib['src'] = _image_to_url(image)
modified = True
if modified:
return lxml.html.tostring(root)
return body_html
#------------------------------------------------------
# Views & Actions
#------------------------------------------------------
# ------------------------------------------------------
# ACTIONS
# ------------------------------------------------------
def action_duplicate(self):
self.ensure_one()
@@ -736,7 +409,7 @@ class MassMailing(models.Model):
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.mass_mailing',
'res_model': 'mailing.mailing',
'res_id': mass_mailing_copy.id,
'context': context,
}
@@ -749,14 +422,14 @@ class MassMailing(models.Model):
'name': _('Test Mailing'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'mail.mass_mailing.test',
'res_model': 'mailing.mailing.test',
'target': 'new',
'context': ctx,
}
def action_schedule_date(self):
self.ensure_one()
action = self.env.ref('mass_mailing.mass_mailing_schedule_date_action').read()[0]
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
@@ -768,10 +441,10 @@ class MassMailing(models.Model):
def retry_failed_mail(self):
failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')])
failed_mails.mapped('statistics_ids').unlink()
failed_mails.mapped('mailing_trace_ids').unlink()
failed_mails.sudo().unlink()
res_ids = self.get_recipients()
except_mailed = self.env['mail.mail.statistics'].search([
res_ids = self._get_recipients()
except_mailed = self.env['mailing.trace'].search([
('model', '=', self.mailing_model_real),
('res_id', 'in', res_ids),
('exception', '!=', False),
@@ -798,11 +471,11 @@ class MassMailing(models.Model):
def _action_view_documents_filtered(self, view_filter):
if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'):
opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter])
opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat[view_filter])
elif view_filter == ('delivered'):
opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced)
opened_stats = self.mailing_trace_ids.filtered(lambda stat: stat.sent and not stat.bounced)
else:
opened_stats = self.env['mail.mail.statistics']
opened_stats = self.env['mailing.trace']
res_ids = opened_stats.mapped('res_id')
model_name = self.env['ir.model']._get(self.mailing_model_real).display_name
return {
@@ -813,20 +486,39 @@ class MassMailing(models.Model):
'domain': [('id', 'in', res_ids)],
}
#------------------------------------------------------
def update_opt_out(self, email, list_ids, value):
if len(list_ids) > 0:
model = self.env['mailing.contact'].with_context(active_test=False)
records = model.search([('email_normalized', '=', tools.email_normalize(email))])
opt_out_records = self.env['mailing.contact.subscription'].search([
('contact_id', 'in', records.ids),
('list_id', 'in', list_ids),
('opt_out', '!=', value)
])
opt_out_records.write({'opt_out': value})
message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)')
for record in records:
# filter the list_id by record
record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
if len(record_lists) > 0:
record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id'))))
# ------------------------------------------------------
# Email Sending
#------------------------------------------------------
# ------------------------------------------------------
def _get_opt_out_list(self):
"""Returns a set of emails opted-out in target model"""
self.ensure_one()
opt_out = {}
target = self.env[self.mailing_model_real]
if self.mailing_model_real == "mail.mass_mailing.contact":
if self.mailing_model_real == "mailing.contact":
# if user is opt_out on One list but not on another
# or if two user with same email address, one opted in and the other one opted out, send the mail anyway
# TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search(
target_list_contacts = self.env['mailing.contact.subscription'].search(
[('list_id', 'in', self.contact_list_ids.ids)])
opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized')
opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized')
@@ -840,18 +532,14 @@ class MassMailing(models.Model):
return opt_out
def _get_convert_links(self):
self.ensure_one()
utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
vals = {'mass_mailing_id': self.id}
vals = {
'mass_mailing_id': self.id,
'source_id': self.source_id.id,
'medium_id': self.medium_id.id,
}
if self.mass_mailing_campaign_id:
vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
if utm_mixin.campaign_id:
vals['campaign_id'] = utm_mixin.campaign_id.id
if utm_mixin.source_id:
vals['source_id'] = utm_mixin.source_id.id
if utm_mixin.medium_id:
vals['medium_id'] = utm_mixin.medium_id.id
vals['campaign_id'] = self.mass_mailing_campaign_id.campaign_id.id
return vals
def _get_seen_list(self):
@@ -863,7 +551,7 @@ class MassMailing(models.Model):
# + use a basic heuristic for extracting emails
query = """
SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
FROM mailing_trace s
JOIN %(target)s t ON (s.res_id = t.id)
WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
"""
@@ -873,7 +561,7 @@ class MassMailing(models.Model):
mail_field = 'email'
query = """
SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
FROM mail_mail_statistics s
FROM mailing_trace s
JOIN %(target)s t ON (s.res_id = t.id)
JOIN res_partner p ON (t.partner_id = p.id)
WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
@@ -914,7 +602,7 @@ class MassMailing(models.Model):
'post_convert_links': self._get_convert_links(),
}
def get_recipients(self):
def _get_recipients(self):
if self.mailing_domain:
domain = safe_eval(self.mailing_domain)
res_ids = self.env[self.mailing_model_real].search(domain).ids
@@ -927,7 +615,7 @@ class MassMailing(models.Model):
contact_nbr = self.env[self.mailing_model_real].search_count(domain)
topick = int(contact_nbr / 100.0 * self.contact_ab_pc)
if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing:
already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.mass_mailing_campaign_id.id]
already_mailed = self.mass_mailing_campaign_id._get_recipients()[self.mass_mailing_campaign_id.id]
else:
already_mailed = set([])
remaining = set(res_ids).difference(already_mailed)
@@ -936,20 +624,21 @@ class MassMailing(models.Model):
res_ids = random.sample(remaining, topick)
return res_ids
def get_remaining_recipients(self):
res_ids = self.get_recipients()
already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real),
('res_id', 'in', res_ids),
('mass_mailing_id', '=', self.id)], ['res_id'])
already_mailed_res_ids = [record['res_id'] for record in already_mailed]
return list(set(res_ids) - set(already_mailed_res_ids))
def _get_remaining_recipients(self):
res_ids = self._get_recipients()
already_mailed = self.env['mailing.trace'].search_read([
('model', '=', self.mailing_model_real),
('res_id', 'in', res_ids),
('mass_mailing_id', '=', self.id)], ['res_id'])
done_res_ids = [record['res_id'] for record in already_mailed]
return [rid for rid in res_ids if rid not in done_res_ids]
def send_mail(self, res_ids=None):
def action_send_mail(self, res_ids=None):
author_id = self.env.user.partner_id.id
for mailing in self:
if not res_ids:
res_ids = mailing.get_remaining_recipients()
res_ids = mailing._get_remaining_recipients()
if not res_ids:
raise UserError(_('There is no recipients selected.'))
@@ -1007,8 +696,63 @@ class MassMailing(models.Model):
for mass_mailing in mass_mailings:
user = mass_mailing.write_uid or self.env.user
mass_mailing = mass_mailing.with_context(**user.with_user(user).context_get())
if len(mass_mailing.get_remaining_recipients()) > 0:
if len(mass_mailing._get_remaining_recipients()) > 0:
mass_mailing.state = 'sending'
mass_mailing.send_mail()
mass_mailing.action_send_mail()
else:
mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
# ------------------------------------------------------
# TOOLS
# ------------------------------------------------------
def _unsubscribe_token(self, res_id, email):
"""Generate a secure hash for this mailing list and parameters.
This is appended to the unsubscription URL and then checked at
unsubscription time to ensure no malicious unsubscriptions are
performed.
:param int res_id:
ID of the resource that will be unsubscribed.
:param str email:
Email of the resource that will be unsubscribed.
"""
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email))
return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest()
def _convert_inline_images_to_urls(self, body_html):
"""
Find inline base64 encoded images, make an attachement out of
them and replace the inline image with an url to the attachement.
"""
def _image_to_url(b64image: bytes):
"""Store an image in an attachement and returns an url"""
attachment = self.env['ir.attachment'].create({
'datas': b64image,
'name': "cropped_image_mailing_{}".format(self.id),
'type': 'binary',})
attachment.generate_access_token()
return '/web/image/%s?access_token=%s' % (
attachment.id, attachment.access_token)
modified = False
root = lxml.html.fromstring(body_html)
for node in root.iter('img'):
match = image_re.match(node.attrib.get('src', ''))
if match:
mime = match.group(1) # unsed
image = match.group(2).encode() # base64 image as bytes
node.attrib['src'] = _image_to_url(image)
modified = True
if modified:
return lxml.html.tostring(root)
return body_html
@@ -0,0 +1,141 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
class MassMailingContactListRel(models.Model):
""" Intermediate model between mass mailing list and mass mailing contact
Indicates if a contact is opted out for a particular list
"""
_name = 'mailing.contact.subscription'
_description = 'Mass Mailing Subscription Information'
_table = 'mailing_contact_list_rel'
_rec_name = 'contact_id'
contact_id = fields.Many2one('mailing.contact', string='Contact', ondelete='cascade', required=True)
list_id = fields.Many2one('mailing.list', string='Mailing List', ondelete='cascade', required=True)
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)
_sql_constraints = [
('unique_contact_list', 'unique (contact_id, list_id)',
'A contact cannot be subscribed multiple times to the same list!')
]
@api.model
def create(self, vals):
if 'opt_out' in vals:
vals['unsubscription_date'] = vals['opt_out'] and fields.Datetime.now()
return super(MassMailingContactListRel, self).create(vals)
def write(self, vals):
if 'opt_out' in vals:
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
because it holds only some basic information: name, email. The purpose is to
be able to deal with large contact list to email without bloating the partner
base."""
_name = 'mailing.contact'
_inherit = ['mail.thread.blacklist']
_description = 'Mass Mailing Contact'
_order = 'email'
_rec_name = 'email'
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)
list_ids = fields.Many2many(
'mailing.list', 'mailing_contact_list_rel',
'contact_id', 'list_id', string='Mailing Lists')
subscription_list_ids = fields.One2many('mailing.contact.subscription', 'contact_id', string='Subscription Information')
country_id = fields.Many2one('res.country', string='Country')
tag_ids = fields.Many2many('res.partner.category', string='Tags')
opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out',
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
if operator != '=':
if operator == '!=' and isinstance(value, bool):
value = not value
else:
raise NotImplementedError()
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
[active_list_id] = self._context['default_list_ids']
contacts = self.env['mailing.contact.subscription'].search([('list_id', '=', active_list_id)])
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
else:
raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')
@api.depends('subscription_list_ids')
def _compute_opt_out(self):
if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
[active_list_id] = self._context['default_list_ids']
for record in self:
active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id)
record.opt_out = active_subscription_list.opt_out
else:
for record in self:
record.opt_out = False
def get_name_email(self, name):
name, email = self.env['res.partner']._parse_partner_name(name)
if name and not email:
email = name
if email and not name:
name = email
return name, email
@api.model
def name_create(self, name):
name, email = self.get_name_email(name)
contact = self.create({'name': name, 'email': email})
return contact.name_get()[0]
@api.model
def add_to_list(self, name, list_id):
name, email = self.get_name_email(name)
contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]})
return contact.name_get()[0]
def _message_get_default_recipients(self):
return {r.id: {
'partner_ids': [],
'email_to': r.email_normalized,
'email_cc': False}
for r in self
}
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MassMailingList(models.Model):
"""Model of a contact list. """
_name = 'mailing.list'
_order = 'name'
_description = 'Mailing List'
name = fields.Char(string='Mailing List', required=True)
active = fields.Boolean(default=True)
contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts')
contact_ids = fields.Many2many(
'mailing.contact', 'mailing_contact_list_rel', 'list_id', 'contact_id',
string='Mailing Lists')
subscription_ids = fields.One2many('mailing.contact.subscription', 'list_id',
string='Subscription Information')
is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription"
" page to allows him to update his subscription preferences.")
# Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list
def _compute_contact_nbr(self):
self.env.cr.execute('''
select
list_id, count(*)
from
mailing_contact_list_rel r
left join mailing_contact c on (r.contact_id=c.id)
left join mail_blacklist bl on c.email_normalized = bl.email and bl.active
where
list_id in %s
AND COALESCE(r.opt_out,FALSE) = FALSE
AND c.email_normalized IS NOT NULL
AND bl.id IS NULL
group by
list_id
''', (tuple(self.ids), ))
data = dict(self.env.cr.fetchall())
for mailing_list in self:
mailing_list.contact_nbr = data.get(mailing_list.id, 0)
def name_get(self):
return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self]
def action_merge(self, src_lists, archive):
"""
Insert all the contact from the mailing lists 'src_lists' to the
mailing list in 'self'. Possibility to archive the mailing lists
'src_lists' after the merge except the destination mailing list 'self'.
"""
# Explation of the SQL query with an example. There are the following lists
# A (id=4): yti@odoo.com; yti@example.com
# B (id=5): yti@odoo.com; yti@openerp.com
# C (id=6): nothing
# To merge the mailing lists A and B into C, we build the view st that looks
# like this with our example:
#
# contact_id | email | row_number | list_id |
# ------------+---------------------------+------------------------
# 4 | yti@odoo.com | 1 | 4 |
# 6 | yti@odoo.com | 2 | 5 |
# 5 | yti@example.com | 1 | 4 |
# 7 | yti@openerp.com | 1 | 5 |
#
# The row_column is kind of an occurence counter for the email address.
# Then we create the Many2many relation between the destination list and the contacts
# while avoiding to insert an existing email address (if the destination is in the source
# for example)
self.ensure_one()
# Put destination is sources lists if not already the case
src_lists |= self
self.env.cr.execute("""
INSERT INTO mailing_contact_list_rel (contact_id, list_id)
SELECT st.contact_id AS contact_id, %s AS list_id
FROM
(
SELECT
contact.id AS contact_id,
contact.email AS email,
list.id AS list_id,
row_number() OVER (PARTITION BY email ORDER BY email) AS rn
FROM
mailing_contact contact,
mailing_contact_list_rel contact_list_rel,
mailing_list list
WHERE contact.id=contact_list_rel.contact_id
AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE
AND contact.email_normalized NOT IN (select email from mail_blacklist where active = TRUE)
AND list.id=contact_list_rel.list_id
AND list.id IN %s
AND NOT EXISTS
(
SELECT 1
FROM
mailing_contact contact2,
mailing_contact_list_rel contact_list_rel2
WHERE contact2.email = contact.email
AND contact_list_rel2.contact_id = contact2.id
AND contact_list_rel2.list_id = %s
)
) st
WHERE st.rn = 1;""", (self.id, tuple(src_lists.ids), self.id))
self.invalidate_cache()
if archive:
(src_lists - self).write({'active': False})
def close_dialog(self):
return {'type': 'ir.actions.act_window_close'}
@@ -4,17 +4,17 @@
from odoo import api, fields, models
class MailMailStats(models.Model):
""" MailMailStats models the statistics collected about emails. Those statistics
class MailingTrace(models.Model):
""" MailingTrace models the statistics collected about emails. Those statistics
are stored in a separated model and table to avoid bloating the mail_mail table
with statistics values. This also allows to delete emails send with mass mailing
without loosing the statistics about them. """
_name = 'mail.mail.statistics'
_description = 'Email Statistics'
_name = 'mailing.trace'
_description = 'Mailing Statistics'
_rec_name = 'message_id'
_order = 'message_id'
# mail data
mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True)
mail_mail_id_int = fields.Integer(
string='Mail ID (tech)',
@@ -23,11 +23,13 @@ class MailMailStats(models.Model):
'However the ID is needed for several action and controllers.',
index=True,
)
email = fields.Char(string="Recipient email address")
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('mail.mass_mailing', string='Mass Mailing', index=True)
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', index=True)
mass_mailing_campaign_id = fields.Many2one(
related='mass_mailing_id.mass_mailing_campaign_id',
string='Mass Mailing Campaign',
@@ -42,7 +44,7 @@ class MailMailStats(models.Model):
replied = fields.Datetime(help='Date when this email has been replied for the first time.')
bounced = fields.Datetime(help='Date when this email has bounced.')
# Link tracking
links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click')
links_click_ids = fields.One2many('link.tracker.click', 'mailing_trace_id', string='Links click')
clicked = fields.Datetime(help='Date when customer clicked on at least one tracked link')
# Status
state = fields.Selection(compute="_compute_state",
@@ -54,9 +56,8 @@ class MailMailStats(models.Model):
('bounced', 'Bounced'),
('ignored', 'Ignored')], store=True)
state_update = fields.Datetime(compute="_compute_state", string='State Update',
help='Last state update of the mail',
store=True)
email = fields.Char(string="Recipient email address")
help='Last state update of the mail',
store=True)
@api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored')
def _compute_state(self):
@@ -81,7 +82,7 @@ class MailMailStats(models.Model):
def create(self, values):
if 'mail_mail_id' in values:
values['mail_mail_id_int'] = values['mail_mail_id']
res = super(MailMailStats, self).create(values)
res = super(MailingTrace, self).create(values)
return res
def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None):
@@ -96,22 +97,21 @@ class MailMailStats(models.Model):
return self.search(base_domain)
def set_opened(self, mail_mail_ids=None, mail_message_ids=None):
statistics = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)])
statistics.write({'opened': fields.Datetime.now(), 'bounced': False})
return statistics
traces = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)])
traces.write({'opened': fields.Datetime.now(), 'bounced': False})
return traces
def set_clicked(self, mail_mail_ids=None, mail_message_ids=None):
statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)])
statistics.write({'clicked': fields.Datetime.now()})
return statistics
traces = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)])
traces.write({'clicked': fields.Datetime.now()})
return traces
def set_replied(self, mail_mail_ids=None, mail_message_ids=None):
statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)])
statistics.write({'replied': fields.Datetime.now()})
return statistics
traces = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)])
traces.write({'replied': fields.Datetime.now()})
return traces
def set_bounced(self, mail_mail_ids=None, mail_message_ids=None):
statistics = self._get_records(
mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)])
statistics.write({'bounced': fields.Datetime.now()})
return statistics
traces = self._get_records(mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)])
traces.write({'bounced': fields.Datetime.now()})
return traces
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mailing_trace_report
@@ -1,15 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
from odoo import fields, models, tools
class MassMailingReport(models.Model):
_name = 'mail.statistics.report'
class MailingTraceReport(models.Model):
_name = 'mailing.trace.report'
_auto = False
_description = 'Mass Mailing Statistics'
scheduled_date = fields.Datetime(stirng='Scheduled Date', readonly=True)
scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True)
name = fields.Char(string='Mass Mail', readonly=True)
campaign = fields.Char(string='Mass Mail Campaign', readonly=True)
sent = fields.Integer(readonly=True)
@@ -23,12 +23,12 @@ class MassMailingReport(models.Model):
email_from = fields.Char('From', readonly=True)
def init(self):
"""Mass Mail Statistical Report: based on mail.mail.statistics that models the various
statistics collected for each mailing, and mail.mass_mailing model that models the
"""Mass Mail Statistical Report: based on mailing.trace that models the various
statistics collected for each mailing, and mailing.mailing model that models the
various mailing performed. """
tools.drop_view_if_exists(self.env.cr, 'mail_statistics_report')
tools.drop_view_if_exists(self.env.cr, 'mailing_trace_report')
self.env.cr.execute("""
CREATE OR REPLACE VIEW mail_statistics_report AS (
CREATE OR REPLACE VIEW mailing_trace_report AS (
SELECT
min(ms.id) as id,
ms.scheduled as scheduled_date,
@@ -43,8 +43,8 @@ class MassMailingReport(models.Model):
mm.state,
mm.email_from
FROM
mail_mail_statistics as ms
left join mail_mass_mailing as mm ON (ms.mass_mailing_id=mm.id)
mailing_trace as ms
left join mailing_mailing as mm ON (ms.mass_mailing_id=mm.id)
left join mail_mass_mailing_campaign as mc ON (ms.mass_mailing_campaign_id=mc.id)
left join utm_campaign as utm_campaign ON (mc.campaign_id = utm_campaign.id)
left join utm_source as utm_source ON (mm.source_id = utm_source.id)
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mail_statistics_report_pivot" model="ir.ui.view">
<field name="name">mail.statistics.report.pivot</field>
<field name="model">mail.statistics.report</field>
<record id="mailing_trace_report_view_pivot" model="ir.ui.view">
<field name="name">mailing.trace.report.view.pivot</field>
<field name="model">mailing.trace.report</field>
<field name="arch" type="xml">
<pivot string="Mass Mailing Statistics" disable_linking="True">
<field name="campaign" type="row"/>
@@ -16,9 +16,9 @@
</field>
</record>
<record id="view_mail_statistics_report_graph" model="ir.ui.view">
<field name="name">mail.statistics.report.graph</field>
<field name="model">mail.statistics.report</field>
<record id="mailing_trace_report_view_graph" model="ir.ui.view">
<field name="name">mailing.trace.report.view.graph</field>
<field name="model">mailing.trace.report</field>
<field name="arch" type="xml">
<graph string="Mass Mailing Statistics">
<field name="campaign"/>
@@ -27,9 +27,9 @@
</field>
</record>
<record id="view_mail_statistics_report_search" model="ir.ui.view">
<field name="name">mail.statistics.report.search</field>
<field name="model">mail.statistics.report</field>
<record id="mailing_trace_report_view_search" model="ir.ui.view">
<field name="name">mailing.trace.report.view.search</field>
<field name="model">mailing.trace.report</field>
<field name="arch" type="xml">
<search string="Mass Mailing Statistics">
<group expand="0" string="Extended Filters...">
@@ -51,14 +51,14 @@
</record>
<!-- Actions and Menuitems -->
<record id="action_mail_statistics_report" model="ir.actions.act_window">
<record id="mailing_trace_report_action" model="ir.actions.act_window">
<field name="name">Mass Mailing Analysis</field>
<field name="res_model">mail.statistics.report</field>
<field name="res_model">mailing.trace.report</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>
</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="action_mail_statistics_report" groups="mass_mailing.group_mass_mailing_user"/>
parent="mass_mailing_menu_root" action="mailing_trace_report_action" groups="mass_mailing.group_mass_mailing_user"/>
</odoo>
@@ -1,17 +1,17 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_mass_mailing_tag,mail.mass_mailing.tag,model_mail_mass_mailing_tag,mass_mailing.group_mass_mailing_campaign,1,1,1,1
access_mass_mailing_contact,mail.mass_mailing.contact,model_mail_mass_mailing_contact,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mass_mailing_list,mail.mass_mailing.list,model_mail_mass_mailing_list,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mass_mailing_stage,mail.mass_mailing.stage,model_mail_mass_mailing_stage,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_tag,access.mailing.tag,model_mailing_tag,mass_mailing.group_mass_mailing_campaign,1,1,1,1
access_mailing_contact_mm_user,access.mailing.contact.mm.user,model_mailing_contact,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_contact_subscription_mm_user,access.mailing.contact.subscription.mm.user,model_mailing_contact_subscription,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_list_mm_user,access.mailing.list.mm.user,model_mailing_list,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_stage,access.mailing.stage.mm.user,model_mailing_stage,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mass_mailing_campaign,mail.mass_mailing.campaign,model_mail_mass_mailing_campaign,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mass_mailing_campaign_system,mail.mass_mailing.campaign.system,model_mail_mass_mailing_campaign,base.group_system,1,1,1,1
access_mass_mailing,mail.mass_mailing,model_mail_mass_mailing,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mass_mailing_system,mail.mass_mailing.system,model_mail_mass_mailing,base.group_system,1,1,1,1
access_mail_mail_statistics_user,mail.mail.statistics,model_mail_mail_statistics,base.group_user,1,1,1,1
access_mail_mail_statistics_mass_mailing_user,mail.mail.statistics,model_mail_mail_statistics,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mail_statistics_report,mail.statistics.report,model_mail_statistics_report,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_mailing_mm_user,access.mailing.mailing.mm.user,model_mailing_mailing,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_mailing_system,access.mailing.mailing.system,model_mailing_mailing,base.group_system,1,1,1,1
access_mailing_trace_user,mailing.trace.user,model_mailing_trace,base.group_user,1,1,1,1
access_mailing_trace_mm_user,access.mailing.trace.mm.user,model_mailing_trace,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mailing_trace_report_mm_user,access.mailing.trace.report.mm.user,model_mailing_trace_report,mass_mailing.group_mass_mailing_user,1,1,1,1
access_utm_source,access_utm_source,utm.model_utm_source,mass_mailing.group_mass_mailing_user,1,1,1,0
access_ir_mail_server,access_ir_mail_server,base.model_ir_mail_server,mass_mailing.group_mass_mailing_user,1,0,0,0
access_mail_blacklist_mass_mailing_user,access.mail.blacklist.mass_mailing_user,mail.model_mail_blacklist,mass_mailing.group_mass_mailing_user,1,1,1,1
access_mail_mass_mailing_list_contact_rel,access.mail.mass_mailing_list_contact_rel,model_mail_mass_mailing_list_contact_rel,mass_mailing.group_mass_mailing_user,1,1,1,1
access_link_tracker_mailing,access.link.tracker.mailing,link_tracker.model_link_tracker,mass_mailing.group_mass_mailing_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mass_mailing_tag access_mailing_tag mail.mass_mailing.tag access.mailing.tag model_mail_mass_mailing_tag model_mailing_tag mass_mailing.group_mass_mailing_campaign 1 1 1 1
3 access_mass_mailing_contact access_mailing_contact_mm_user mail.mass_mailing.contact access.mailing.contact.mm.user model_mail_mass_mailing_contact model_mailing_contact mass_mailing.group_mass_mailing_user 1 1 1 1
4 access_mass_mailing_list access_mailing_contact_subscription_mm_user mail.mass_mailing.list access.mailing.contact.subscription.mm.user model_mail_mass_mailing_list model_mailing_contact_subscription mass_mailing.group_mass_mailing_user 1 1 1 1
5 access_mass_mailing_stage access_mailing_list_mm_user mail.mass_mailing.stage access.mailing.list.mm.user model_mail_mass_mailing_stage model_mailing_list mass_mailing.group_mass_mailing_user 1 1 1 1
6 access_mailing_stage access.mailing.stage.mm.user model_mailing_stage mass_mailing.group_mass_mailing_user 1 1 1 1
7 access_mass_mailing_campaign mail.mass_mailing.campaign model_mail_mass_mailing_campaign mass_mailing.group_mass_mailing_user 1 1 1 1
8 access_mass_mailing_campaign_system mail.mass_mailing.campaign.system model_mail_mass_mailing_campaign base.group_system 1 1 1 1
9 access_mass_mailing access_mailing_mailing_mm_user mail.mass_mailing access.mailing.mailing.mm.user model_mail_mass_mailing model_mailing_mailing mass_mailing.group_mass_mailing_user 1 1 1 1
10 access_mass_mailing_system access_mailing_mailing_system mail.mass_mailing.system access.mailing.mailing.system model_mail_mass_mailing model_mailing_mailing base.group_system 1 1 1 1
11 access_mail_mail_statistics_user access_mailing_trace_user mail.mail.statistics mailing.trace.user model_mail_mail_statistics model_mailing_trace base.group_user 1 1 1 1
12 access_mail_mail_statistics_mass_mailing_user access_mailing_trace_mm_user mail.mail.statistics access.mailing.trace.mm.user model_mail_mail_statistics model_mailing_trace mass_mailing.group_mass_mailing_user 1 1 1 1
13 access_mail_statistics_report access_mailing_trace_report_mm_user mail.statistics.report access.mailing.trace.report.mm.user model_mail_statistics_report model_mailing_trace_report mass_mailing.group_mass_mailing_user 1 1 1 1
14 access_utm_source access_utm_source utm.model_utm_source mass_mailing.group_mass_mailing_user 1 1 1 0
15 access_ir_mail_server access_ir_mail_server base.model_ir_mail_server mass_mailing.group_mass_mailing_user 1 0 0 0
16 access_mail_blacklist_mass_mailing_user access.mail.blacklist.mass_mailing_user mail.model_mail_blacklist mass_mailing.group_mass_mailing_user 1 1 1 1
access_mail_mass_mailing_list_contact_rel access.mail.mass_mailing_list_contact_rel model_mail_mass_mailing_list_contact_rel mass_mailing.group_mass_mailing_user 1 1 1 1
17 access_link_tracker_mailing access.link.tracker.mailing link_tracker.model_link_tracker mass_mailing.group_mass_mailing_user 1 1 1 1
@@ -16,7 +16,7 @@ KanbanRecord.include({
_openRecord: function () {
if (this.modelName === 'mail.mass_mailing.campaign') {
this.$('.oe_mailings').click();
} else if (this.modelName === 'mail.mass_mailing.list' &&
} else if (this.modelName === 'mailing.list' &&
this.$('.o_mailing_list_kanban_boxes a')) {
this.$('.o_mailing_list_kanban_boxes a').first().click();
} else {
@@ -28,7 +28,7 @@ KanbanRecord.include({
KanbanColumn.include({
init: function () {
this._super.apply(this, arguments);
if (this.modelName === 'mail.mass_mailing') {
if (this.modelName === 'mailing.mailing') {
this.draggable = false;
}
},
@@ -14,7 +14,7 @@ QUnit.module('mass_mailing', {}, function () {
QUnit.module('field html', {
beforeEach: function () {
this.data = weTestUtils.wysiwygData({
'mail.mass_mailing': {
'mailing.mailing': {
fields: {
display_name: {
string: "Displayed name",
@@ -68,7 +68,7 @@ QUnit.test('save arch and html', async function (assert) {
var form = await testUtils.createView({
View: FormView,
model: 'mail.mass_mailing',
model: 'mailing.mailing',
data: this.data,
arch: '<form>' +
' <field name="body_html" class="oe_read_only" widget="html"'+
@@ -152,7 +152,7 @@ QUnit.test('save and edit arch wysiwyg', async function (assert) {
var form = await testUtils.createView({
View: FormView,
model: 'mail.mass_mailing',
model: 'mailing.mailing',
data: this.data,
arch: '<form>' +
' <header style="min-height:31px;">' +
@@ -9,7 +9,7 @@ class TestMassMailingCommon(common.TransactionCase):
def test_00_test_mass_mailing_list_merge(self):
# Data set up
mailing_list_A = self.env['mail.mass_mailing.list'].create({
mailing_list_A = self.env['mailing.list'].create({
'name': 'A',
'contact_ids': [
(0, 0, {'name': 'Noel Flantier', 'email': 'noel.flantier@example.com'}),
@@ -18,7 +18,7 @@ class TestMassMailingCommon(common.TransactionCase):
]
})
mailing_list_B = self.env['mail.mass_mailing.list'].create({
mailing_list_B = self.env['mailing.list'].create({
'name': 'B',
'contact_ids': [
(0, 0, {'name': 'Icallhimtest', 'email': 'icallhimtest@example.com'}),
@@ -27,7 +27,7 @@ class TestMassMailingCommon(common.TransactionCase):
]
})
mailing_list_C = self.env['mail.mass_mailing.list'].create({
mailing_list_C = self.env['mailing.list'].create({
'name': 'C',
'contact_ids': [
(0, 0, {'name': 'Norberto', 'email': 'norbert@example.com'}),
@@ -39,7 +39,7 @@ class TestMassMailingCommon(common.TransactionCase):
# This test ensure that the mailing lists are correctly merged and no
# duplicates are appearing in C
result_list = self.env['mass.mailing.list.merge'].create({
result_list = self.env['mailing.list.merge'].create({
'src_list_ids': [(4, list_id) for list_id in [mailing_list_A.id, mailing_list_B.id]],
'dest_list_id': mailing_list_C.id,
'merge_options': 'existing',
@@ -35,23 +35,23 @@ class TestMassMailingShortener(common.TransactionCase):
self.addCleanup(patcher2.stop)
def test_00_test_mass_mailing_shortener(self):
mailing_list_A = self.env['mail.mass_mailing.list'].create({
mailing_list_A = self.env['mailing.list'].create({
'name': 'A',
})
self.env['mail.mass_mailing.contact'].create({
self.env['mailing.contact'].create({
'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
self.env['mail.mass_mailing.contact'].create({
self.env['mailing.contact'].create({
'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
self.env['mail.mass_mailing.contact'].create({
self.env['mailing.contact'].create({
'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)]
})
mass_mailing = self.env['mail.mass_mailing'].create({
mass_mailing = self.env['mailing.mailing'].create({
"reply_to_mode": "email",
"reply_to": "Administrator <admin@yourcompany.example.com>",
"mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id,
"mailing_model_id": self.env.ref('mass_mailing.model_mailing_list').id,
"mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id,
"contact_list_ids": [[6, False, [mailing_list_A.id]]],
"mass_mailing_campaign_id": False,
+78
View File
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="mass_mailing assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.scss"/>
<link rel="stylesheet" href="/mass_mailing/static/src/css/email_template.css"/>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_widget.js"></script>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_view.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/unsubscribe.js"></script>
</xpath>
</template>
<template id="assets_mail_themes">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/themes/theme_basic.scss"/>
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/themes/theme_default.scss"/>
<t t-call="mass_mailing.mass_mailing_mail_style"/>
</template>
<template id="assets_mail_themes_edition"> <!-- maybe to remove and convert into a field dumy with attr invisible if the template is not selected -->
<t t-call="web._assets_helpers">
<link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/wysiwyg_variables.scss"/>
</t>
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.ui.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/webclient.scss"/>
</template>
<template id="iframe_css_assets_edit">
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
<t t-call-assets="web_editor.assets_wysiwyg" t-js="false"/>
<t t-call-assets="mass_mailing.assets_mail_themes" t-js="false"/>
<t t-call-assets="mass_mailing.assets_mail_themes_edition" t-js="false"/>
</template>
<template id="iframe_css_assets_readonly">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/css/basic_theme_readonly.css"/>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="." position="inside">
<script type="text/javascript">
odoo.define('mass_mailing.FieldHtml.test', function (require) {
'use strict';
var MassMailingFieldHtml = require('mass_mailing.FieldHtml');
MassMailingFieldHtml.include({jsLibs: []});
});
</script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_snippets.js"/>
<script type="text/javascript" src="/mass_mailing/static/tests/mass_mailing_html_tests.js"/>
</xpath>
</template>
<template id="mass_mailing_mail_style">
<style>
@media screen and (max-width: 768px) {
.o_mail_col_mv {
display: block !important;
width: auto !important;
}
.o_mail_table_styles {
width: 100% !important;
}
.o_mail_col_container {
margin: 0px 0px 10px 0px !important;
}
}
</style>
</template>
</odoo>
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_mail_themes">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/themes/theme_basic.scss"/>
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/themes/theme_default.scss"/>
<t t-call="mass_mailing.mass_mailing_mail_style"/>
</template>
<template id="assets_mail_themes_edition"> <!-- maybe to remove and convert into a field dumy with attr invisible if the template is not selected -->
<t t-call="web._assets_helpers">
<link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/wysiwyg_variables.scss"/>
</t>
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.ui.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/webclient.scss"/>
</template>
<template id="iframe_css_assets_edit">
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
<t t-call-assets="web_editor.assets_wysiwyg" t-js="false"/>
<t t-call-assets="mass_mailing.assets_mail_themes" t-js="false"/>
<t t-call-assets="mass_mailing.assets_mail_themes_edition" t-js="false"/>
</template>
<template id="iframe_css_assets_readonly">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/css/basic_theme_readonly.css"/>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="." position="inside">
<script type="text/javascript">
odoo.define('mass_mailing.FieldHtml.test', function (require) {
'use strict';
var MassMailingFieldHtml = require('mass_mailing.FieldHtml');
MassMailingFieldHtml.include({jsLibs: []});
});
</script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_snippets.js"/>
<script type="text/javascript" src="/mass_mailing/static/tests/mass_mailing_html_tests.js"/>
</xpath>
</template>
</odoo>
@@ -80,7 +80,7 @@
<xpath expr="//field[@name='country_id']" position="after">
<field name="mass_mailing_campaign_id"/>
<field name="mass_mailing_id"/>
<field name="mail_stat_id"/>
<field name="mailing_trace_id"/>
</xpath>
</field>
</record>
@@ -0,0 +1,264 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MAILING CONTACT SUBSCRIPTION -->
<record model="ir.ui.view" id="mailing_contact_subscription_view_form">
<field name="name">mailing.contact.subscription.view.form</field>
<field name="model">mailing.contact.subscription</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List contacts">
<sheet>
<group>
<field name="list_id"/>
<field name="is_blacklisted" invisible="1"/>
<label for="contact_id" class="oe_inline"/>
<div class="o_row o_row_readonly">
<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="contact_id"/>
</div>
<field name="unsubscription_date" readonly="1"/>
<field name="opt_out"/>
<field name="message_bounce" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="mailing_contact_subscription_view_tree">
<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">
<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>
<record model="ir.ui.view" id="mailing_contact_subscription_view_search">
<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">
<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>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mailing.contact.search</field>
<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)]"
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"/>
<separator/>
<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>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
<field name="name">mailing.contact.tree</field>
<field name="model">mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing List Contacts">
<field name="create_date"/>
<field name="name"/>
<field name="company_name"/>
<field name="email"/>
<field name="message_bounce"/>
<field name="opt_out" invisible="'default_list_ids' not in context"/>
<field name="is_blacklisted"/>
</tree>
</field>
</record>
<record id="view_mail_mass_mailing_contact_kanban" model="ir.ui.view">
<field name="name">mailing.contact.kanban</field>
<field name="model">mailing.contact</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<field name="company_name"/>
<field name="email"/>
<field name="message_bounce"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<t t-esc="record.name.value"/>
</strong>
</div>
<span class="badge badge-pill" title="Number of bounced email.">
<i class="fa fa-exclamation-triangle" role="img" aria-label="Warning" title="Warning"/> <t t-esc="record.message_bounce.value" title=""/>
</span>
</div>
<div class="o_kanban_record_body">
<field name="tag_ids"/>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<strong>
<t t-esc="record.email.value"/>
</strong>
</div>
<div class="oe_kanban_bottom_right">
<t t-esc="record.company_name.value"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_form">
<field name="name">mailing.contact.form</field>
<field name="model">mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List Contacts">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. John Smith"/>
</h1>
<label for="tag_ids" class="oe_edit_only"/>
<div>
<field name="tag_ids" widget="many2many_tags" style="width: 100%%"/>
</div>
</div>
<group>
<group>
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<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"/>
</div>
<field name="title_id"/>
<field name="company_name"/>
<field name="country_id"/>
</group>
<group>
<field name="create_date" readonly="1"/>
<label for="message_bounce"/>
<div class="o_row">
<field name="message_bounce"/>
<span> times</span>
</div>
</group>
</group>
<field name="subscription_list_ids">
<tree editable="bottom">
<field name="list_id"/>
<field name="unsubscription_date"/>
<field name="opt_out"/>
</tree>
</field>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_pivot">
<field name="name">mailing.contact.pivot</field>
<field name="model">mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<pivot string="Mailing List Contacts" stacked="True">
<field name="create_date" type="row"/>
</pivot>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_graph">
<field name="name">mailing.contact.graph</field>
<field name="model">mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<graph string="Mailing List Contacts" stacked="True">
<field name="create_date" type="row"/>
</graph>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts">
<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="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>
</field>
</record>
<menuitem name="Mailing List Contacts" id="menu_email_mass_mailing_contacts"
parent="mass_mailing_mailing_list_menu" sequence="4"
action="action_view_mass_mailing_contacts"/>
</odoo>
@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MAILING LIST -->
<record model="ir.ui.view" id="mailing_list_view_search">
<field name="name">mailing.list.view.search</field>
<field name="model">mailing.list</field>
<field name="arch" type="xml">
<search string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Creation Period" name="group_create_date"
context="{'group_by': 'create_date'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="mailing_list_view_tree">
<field name="name">mailing.list.view.tree</field>
<field name="model">mailing.list</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<field name="contact_nbr"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="mailing_list_view_form">
<field name="name">mailing.list.form</field>
<field name="model">mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List">
<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">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Consumer Newsletter"/>
</h1>
</div>
<group>
<field name="active" invisible="1"/>
<field name="is_public"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="mailing_list_view_form_simplified" model="ir.ui.view">
<field name="name">mailing.list.form.simplified</field>
<field name="model">mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List">
<group>
<group>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Consumer Newsletter"/>
</h1>
</div>
</group>
</group>
<group>
<field name="is_public"/>
</group>
<footer>
<button string="Create" name="close_dialog" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="open_create_mass_mailing_list" model="ir.actions.act_window">
<field name="name">Create a Mass Mailing List</field>
<field name="res_model">mailing.list</field>
<field name="view_mode">form</field>
<field name="view_id" ref="mailing_list_view_form_simplified"/>
<field name="target">new</field>
</record>
<record id="mailing_list_view_kanban" model="ir.ui.view">
<field name="name">mailing.list.view.kanban</field>
<field name="model">mailing.list</field>
<field name="arch" type="xml">
<kanban js_class="mass_mailing_list_kanban" class="o_kanban_mobile" on_create="mass_mailing.open_create_mass_mailing_list">
<field name="name"/>
<field name="contact_nbr"/>
<field name="active"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_mass_mailing_kanban_main">
<div class="o_kanban_card_content">
<div class="o_kanban_primary_left">
<div class="o_primary">
<span><t t-esc="record.name.value"/></span>
</div>
</div>
</div>
<div class="o_kanban_card_manage_pane">
<div class="o_kanban_card_manage_section o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<a t-if="widget.editable" role="menuitem" type="edit" class="dropdown-item">Edit</a>
<a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
<a role="menuitem" class="dropdown-item o_kanban_mailing_active" name="toggle_active" type="object">
<t t-if="record.active.raw_value">Archive</t>
<t t-if="!record.active.raw_value">Restore</t>
</a>
<ul class="oe_kanban_colorpicker" data-field="color"/>
</div>
</div>
</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">
<div>
<span class="badge badge-pill">
<i class="fa fa-user" role="img" aria-label="Contacts" title="Contacts"/>
<t t-esc="record.contact_nbr.value"/>
</span>
</div>
</a>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_lists">
<field name="name">Mailing Lists</field>
<field name="res_model">mailing.list</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new mailing list
</p><p>
You don't need to import your mailing lists, you can easily
send emails<br/> to any contact saved in other Odoo apps.
</p>
</field>
</record>
<menuitem name="Mailing Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_mailing_list_menu" sequence="3"
action="action_view_mass_mailing_lists"/>
</odoo>
@@ -1,439 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MAILING CONTACT -->
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_rel_view_form">
<!--TODO DBE : To remove - Deprecated -->
<field name="name">mail.mass_mailing.list_contact_rel.view.form</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List contacts">
<sheet>
<group>
<field name="list_id"/>
<field name="is_blacklisted" invisible="1"/>
<label for="contact_id" class="oe_inline"/>
<div class="o_row o_row_readonly">
<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="contact_id"/>
</div>
<field name="unsubscription_date" readonly="1"/>
<field name="opt_out"/>
<field name="message_bounce" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_rel_list_contact_view_tree">
<!--TODO DBE : To remove - Deprecated -->
<field name="name">mail.mass_mailing.list_contact_rel.list_contact.view.tree</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<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>
<record model="ir.ui.view" id="mail_mass_mailing_list_contact_view_search">
<!--TODO DBE : To remove - Deprecated -->
<field name="name">mail.mass_mailing.list_contact_rel.view.search</field>
<field name="model">mail.mass_mailing.list_contact_rel</field>
<field name="arch" type="xml">
<search string="Mailing List Contacts">
<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>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
<field name="name">mail.mass_mailing.contact.search</field>
<field name="model">mail.mass_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)]"
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"/>
<separator/>
<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>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_tree">
<field name="name">mail.mass_mailing.contact.tree</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing List Contacts">
<field name="create_date"/>
<field name="name"/>
<field name="company_name"/>
<field name="email"/>
<field name="message_bounce"/>
<field name="opt_out" invisible="'default_list_ids' not in context"/>
<field name="is_blacklisted"/>
</tree>
</field>
</record>
<record id="view_mail_mass_mailing_contact_kanban" model="ir.ui.view">
<field name="name">mail.mass_mailing.contact.kanban</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="arch" type="xml">
<kanban>
<field name="name"/>
<field name="company_name"/>
<field name="email"/>
<field name="message_bounce"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<t t-esc="record.name.value"/>
</strong>
</div>
<span class="badge badge-pill" title="Number of bounced email.">
<i class="fa fa-exclamation-triangle" role="img" aria-label="Warning" title="Warning"/> <t t-esc="record.message_bounce.value" title=""/>
</span>
</div>
<div class="o_kanban_record_body">
<field name="tag_ids"/>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<strong>
<t t-esc="record.email.value"/>
</strong>
</div>
<div class="oe_kanban_bottom_right">
<t t-esc="record.company_name.value"/>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_form">
<field name="name">mail.mass_mailing.contact.form</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<form string="Mailing List Contacts">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. John Smith"/>
</h1>
<label for="tag_ids" class="oe_edit_only"/>
<div>
<field name="tag_ids" widget="many2many_tags" style="width: 100%%"/>
</div>
</div>
<group>
<group>
<field name="is_blacklisted" invisible="1"/>
<label for="email" class="oe_inline"/>
<div class="o_row o_row_readonly">
<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"/>
</div>
<field name="title_id"/>
<field name="company_name"/>
<field name="country_id"/>
</group>
<group>
<field name="create_date" readonly="1"/>
<label for="message_bounce"/>
<div class="o_row">
<field name="message_bounce"/>
<span> times</span>
</div>
</group>
</group>
<field name="subscription_list_ids">
<tree editable="bottom">
<field name="list_id"/>
<field name="unsubscription_date"/>
<field name="opt_out"/>
</tree>
</field>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_pivot">
<field name="name">mail.mass_mailing.contact.pivot</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<pivot string="Mailing List Contacts" stacked="True">
<field name="create_date" type="row"/>
</pivot>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_graph">
<field name="name">mail.mass_mailing.contact.graph</field>
<field name="model">mail.mass_mailing.contact</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<graph string="Mailing List Contacts" stacked="True">
<field name="create_date" type="row"/>
</graph>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_contacts">
<field name="name">Mailing List Contacts</field>
<field name="res_model">mail.mass_mailing.contact</field>
<field name="view_mode">tree,kanban,form,graph,pivot</field>
<field name="context">{'search_default_valid_recipient': 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">mail.mass_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>
</field>
</record>
<menuitem name="Mailing List Contacts" id="menu_email_mass_mailing_contacts"
parent="mass_mailing_mailing_list_menu" sequence="4"
action="action_view_mass_mailing_contacts"/>
<!-- MAILING LIST -->
<record model="ir.ui.view" id="view_mail_mass_mailing_list_search">
<field name="name">mail.mass_mailing.list.search</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<search string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<filter name="inactive" string="Archived" domain="[('active','=',False)]"/>
<group expand="0" string="Group By">
<filter string="Creation Period" name="group_create_date"
context="{'group_by': 'create_date'}"/>
</group>
</search>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_tree">
<field name="name">mail.mass_mailing.list.tree</field>
<field name="model">mail.mass_mailing.list</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailing Lists">
<field name="name"/>
<field name="create_date"/>
<field name="is_public"/>
<field name="contact_nbr"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_list_form">
<field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List">
<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">
<field name="contact_nbr" string="Recipients" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Consumer Newsletter"/>
</h1>
</div>
<group>
<field name="active" invisible="1"/>
<field name="is_public"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_mail_mass_mailing_list_form_simplified" model="ir.ui.view">
<field name="name">mail.mass_mailing.list.form.simplified</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<form string="Contact List">
<group>
<group>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1>
<field name="name" placeholder="e.g. Consumer Newsletter"/>
</h1>
</div>
</group>
</group>
<group>
<field name="is_public"/>
</group>
<footer>
<button string="Create" name="close_dialog" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="open_create_mass_mailing_list" model="ir.actions.act_window">
<field name="name">Create a Mass Mailing List</field>
<field name="res_model">mail.mass_mailing.list</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_mail_mass_mailing_list_form_simplified"/>
<field name="target">new</field>
</record>
<record id="view_mail_mass_mailing_list_kanban" model="ir.ui.view">
<field name="name">mail.mass_mailing.list.kanban</field>
<field name="model">mail.mass_mailing.list</field>
<field name="arch" type="xml">
<kanban js_class="mass_mailing_list_kanban" class="o_kanban_mobile" on_create="mass_mailing.open_create_mass_mailing_list">
<field name="name"/>
<field name="contact_nbr"/>
<field name="active"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_global_click">
<div class="o_mass_mailing_kanban_main">
<div class="o_kanban_card_content">
<div class="o_kanban_primary_left">
<div class="o_primary">
<span><t t-esc="record.name.value"/></span>
</div>
</div>
</div>
<div class="o_kanban_card_manage_pane">
<div class="o_kanban_card_manage_section o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
<a t-if="widget.editable" role="menuitem" type="edit" class="dropdown-item">Edit</a>
<a t-if="widget.deletable" role="menuitem" type="delete" class="dropdown-item">Delete</a>
<a role="menuitem" class="dropdown-item o_kanban_mailing_active" name="toggle_active" type="object">
<t t-if="record.active.raw_value">Archive</t>
<t t-if="!record.active.raw_value">Restore</t>
</a>
<ul class="oe_kanban_colorpicker" data-field="color"/>
</div>
</div>
</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">
<div>
<span class="badge badge-pill">
<i class="fa fa-user" role="img" aria-label="Contacts" title="Contacts"/>
<t t-esc="record.contact_nbr.value"/>
</span>
</div>
</a>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.actions.act_window" id="action_view_mass_mailing_lists">
<field name="name">Mailing Lists</field>
<field name="res_model">mail.mass_mailing.list</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new mailing list
</p><p>
You don't need to import your mailing lists, you can easily
send emails<br/> to any contact saved in other Odoo apps.
</p>
</field>
</record>
<menuitem name="Mailing Lists" id="menu_email_mass_mailing_lists"
parent="mass_mailing_mailing_list_menu" sequence="3"
action="action_view_mass_mailing_lists"/>
<!-- MAILING !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_search">
<field name="name">mail.mass_mailing.search</field>
<field name="model">mail.mass_mailing</field>
<field name="name">mailing.mailing.search</field>
<field name="model">mailing.mailing</field>
<field name="arch" type="xml">
<search string="Mailings">
<field name="name" string="Mailings"/>
@@ -456,8 +26,8 @@
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_tree">
<field name="name">mail.mass_mailing.tree</field>
<field name="model">mail.mass_mailing</field>
<field name="name">mailing.mailing.tree</field>
<field name="model">mailing.mailing</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailings">
@@ -473,8 +43,8 @@
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_form">
<field name="name">mail.mass_mailing.form</field>
<field name="model">mail.mass_mailing</field>
<field name="name">mailing.mailing.form</field>
<field name="model">mailing.mailing</field>
<field name="arch" type="xml">
<form string="Mailing">
<header style="min-height:31px;">
@@ -588,10 +158,10 @@
<field name="mailing_model_name" invisible="1"/>
<field name="mailing_model_real" invisible="1"/>
<field name="mailing_domain" widget="domain"
attrs="{'invisible': [('mailing_model_name', '=', 'mail.mass_mailing.list')]}"
attrs="{'invisible': [('mailing_model_name', '=', 'mailing.list')]}"
options="{'model': 'mailing_model_real'}">
</field>
<div attrs="{'invisible': [('mailing_model_name', '!=', 'mail.mass_mailing.list')]}">
<div attrs="{'invisible': [('mailing_model_name', '!=', 'mailing.list')]}">
<label for="contact_list_ids" string="Select mailing lists:" class="oe_edit_only"/>
<field name="contact_list_ids" widget="many2many_tags"
placeholder="Select mailing lists..." class="oe_inline"/>
@@ -617,7 +187,7 @@
<label for="reply_to"/>
<div>
<field name="reply_to_mode" widget="radio"
attrs="{'invisible': [('mailing_model_name', 'in', ['mail.mass_mailing.contact', 'res.partner', 'mail.mass_mailing.list'])]}"/>
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>
@@ -660,8 +230,8 @@
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_kanban">
<field name="name">mail.mass_mailing.kanban</field>
<field name="model">mail.mass_mailing</field>
<field name="name">mailing.mailing.kanban</field>
<field name="model">mailing.mailing</field>
<field name="arch" type="xml">
<kanban default_group_by="state">
<field name='state' readonly="1"/>
@@ -745,8 +315,8 @@
</record>
<record id="view_mail_mass_mailing_graph" model="ir.ui.view">
<field name="name">mail.mass_mailing.graph</field>
<field name="model">mail.mass_mailing</field>
<field name="name">mailing.mailing.graph</field>
<field name="model">mailing.mailing</field>
<field name="arch" type="xml">
<graph string="Mailing" type="bar">
<field name="state" type="row"/>
@@ -756,7 +326,7 @@
<record id="action_view_mass_mailings" model="ir.actions.act_window">
<field name="name">Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="res_model">mailing.mailing</field>
<field name="view_mode">kanban,tree,form,graph</field>
<field name="context">{
'search_default_assigned_to_me': 1,
@@ -774,7 +344,7 @@
<record id="action_view_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="res_model">mailing.mailing</field>
<field name="view_mode">kanban,tree,form</field>
<field name="context">{
'search_default_assigned_to_me': 1,
@@ -795,7 +365,7 @@
<record id="action_create_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="res_model">mailing.mailing</field>
<field name="view_mode">form,kanban,tree</field>
<field name="context">{
'search_default_assigned_to_me': 1,
@@ -816,7 +386,7 @@
<record id="action_create_ab_mass_mailings_from_campaign" model="ir.actions.act_window">
<field name="name">Mailings</field>
<field name="res_model">mail.mass_mailing</field>
<field name="res_model">mailing.mailing</field>
<field name="view_mode">form,kanban,tree</field>
<field name="context">{
'search_default_assigned_to_me': 1,
@@ -841,8 +411,8 @@
<!-- MAILING CAMPAIGN STAGE !-->
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_search">
<field name="name">mail.mass_mailing.stage.search</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="name">mailing.stage.search</field>
<field name="model">mailing.stage</field>
<field name="arch" type="xml">
<search string="Mailings">
<field name="name"/>
@@ -851,8 +421,8 @@
</record>
<record model="ir.ui.view" id="view_mail_mass_mailing_stage_tree">
<field name="name">mail.mass_mailing.stage.tree</field>
<field name="model">mail.mass_mailing.stage</field>
<field name="name">mailing.stage.tree</field>
<field name="model">mailing.stage</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Mailings" editable="top">
@@ -863,7 +433,7 @@
</record>
<record id="action_view_mass_mailing_stages" model="ir.actions.act_window">
<field name="name">Mailing Stages</field>
<field name="res_model">mail.mass_mailing.stage</field>
<field name="res_model">mailing.stage</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
@@ -879,8 +449,8 @@
<!-- MAILING CAMPAIGN TAGS !-->
<record id="mail_mass_mailing_tag_view_form" model="ir.ui.view">
<field name="name">mail.mass_mailing.tag.form</field>
<field name="model">mail.mass_mailing.tag</field>
<field name="name">mailing.tag.form</field>
<field name="model">mailing.tag</field>
<field name="arch" type="xml">
<form string="Campaign Tags">
<sheet>
@@ -893,7 +463,7 @@
</record>
<record id="mass_mailing_tag_action" model="ir.actions.act_window">
<field name="name">Campaign Tags</field>
<field name="res_model">mail.mass_mailing.tag</field>
<field name="res_model">mailing.tag</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add a new tag
@@ -4,9 +4,15 @@
<menuitem name="Email Marketing" id="mass_mailing_menu_root" sequence="60" web_icon="mass_mailing,static/description/icon.png"/>
<menuitem name="Mailing Lists" id="mass_mailing_mailing_list_menu"
parent="mass_mailing_menu_root" sequence="2" groups="mass_mailing.group_mass_mailing_user"/>
<!-- Marketing / Configuration -->
<menuitem name="Configuration" id="mass_mailing_configuration"
parent="mass_mailing_menu_root"
sequence="100"
groups="mass_mailing.group_mass_mailing_user"/>
<!-- Configuration / Blacklist -->
<menuitem id="mail_blacklist_mm_menu" name="Blacklist"
action="mail.mail_blacklist_action"
parent="mass_mailing_configuration"/>
</odoo>
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- MAIL MAIL STATISTICS !-->
<record model="ir.ui.view" id="view_mail_mail_statistics_search">
<field name="name">mail.mail.statistics.search</field>
<field name="model">mail.mail.statistics</field>
<!-- MAILING TRACE !-->
<record model="ir.ui.view" id="mailing_trace_view_search">
<field name="name">mailing.trace.search</field>
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<search string="Mail Statistics">
<field name="mail_mail_id_int"/>
@@ -30,9 +30,9 @@
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_tree">
<field name="name">mail.mail.statistics.tree</field>
<field name="model">mail.mail.statistics</field>
<record model="ir.ui.view" id="mailing_trace_view_tree">
<field name="name">mailing.trace.tree</field>
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<tree string="Mail Statistics" create="0">
<field name="email"/>
@@ -49,9 +49,9 @@
</field>
</record>
<record model="ir.ui.view" id="view_mail_mail_statistics_form">
<field name="name">mail.mail.statistics.form</field>
<field name="model">mail.mail.statistics</field>
<record model="ir.ui.view" id="mailing_trace_view_form">
<field name="name">mailing.trace.form</field>
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<form string="Mail Statistics" create="0">
<header>
@@ -88,13 +88,13 @@
<record id="action_view_mail_mail_statistics" model="ir.actions.act_window">
<field name="name">Mail Statistics</field>
<field name="res_model">mail.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">mail.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>
@@ -102,7 +102,7 @@
<record id="action_view_mail_mail_statistics_mailing_list" model="ir.actions.act_window">
<field name="name">Emails Sent</field>
<field name="res_model">mail.mail.statistics</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>
@@ -110,7 +110,7 @@
<record id="view_mail_mail_statistics_graph" model="ir.ui.view">
<field name="name">Mail Statistics Graph</field>
<field name="model">mail.mail.statistics</field>
<field name="model">mailing.trace</field>
<field name="arch" type="xml">
<graph string="Mail Statistics" type="bar" stacked="True" interval="day">
<field name="state_update" type="row" interval="day"/>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="mail_blacklist_mm_menu" name="Blacklist" action="mail.mail_blacklist_action" parent="mass_mailing_configuration"/>
</odoo>
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="mass_mailing assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/mass_mailing/static/src/scss/mass_mailing.scss"/>
<link rel="stylesheet" href="/mass_mailing/static/src/css/email_template.css"/>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_widget.js"></script>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_campaign_kanban_view.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_record.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_renderer.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/mass_mailing_list_kanban_view.js"></script>
<script type="text/javascript" src="/mass_mailing/static/src/js/unsubscribe.js"></script>
</xpath>
</template>
</odoo>
@@ -769,4 +769,52 @@
</div>
</div>
</template>
<!-- Snippet themes Options -->
<template id="snippet_options">
<t t-call="web_editor.snippet_options"/>
<t t-raw="0"/>
<div data-js="mass_mailing_sizing_x"
data-selector="img, .mv, .col_mv, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
<div data-js="mass_mailing_table_item"
data-selector="td, th"
data-exclude=".o_mail_no_options"/>
<div data-js="table_row"
data-selector="tr:has(> .row), tr:has(> .col_mv)"
data-exclude=".o_mail_no_options"
data-drop-near="tr:has(> .row), tr:has(> .col_mv)"/>
<div data-js="table_column"
data-selector=".col>td, .col>th"
data-exclude=".o_mail_no_options"
data-drop-near=".col>td, .col>th"/>
<div data-js="table_column_mv"
data-selector=".col_mv, td, th"
data-exclude=".o_mail_no_options"
data-drop-near=".col_mv, td, th"/>
<div data-js="content"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body"
data-exclude=".o_mail_no_options"
data-drop-near="[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *"
data-drop-in="[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure"/>
<div data-js="sizing_y"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
<div data-js="colorpicker"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_colorpicker, .o_mail_no_options">
<div class="dropdown-submenu">
<a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>
<div class="dropdown-menu" role="menu"/>
</div>
</div>
</template>
</odoo>
@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="snippet_options">
<t t-call="web_editor.snippet_options"/>
<t t-raw="0"/>
<div data-js="mass_mailing_sizing_x"
data-selector="img, .mv, .col_mv, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
<div data-js="mass_mailing_table_item"
data-selector="td, th"
data-exclude=".o_mail_no_options"/>
<div data-js="table_row"
data-selector="tr:has(> .row), tr:has(> .col_mv)"
data-exclude=".o_mail_no_options"
data-drop-near="tr:has(> .row), tr:has(> .col_mv)"/>
<div data-js="table_column"
data-selector=".col>td, .col>th"
data-exclude=".o_mail_no_options"
data-drop-near=".col>td, .col>th"/>
<div data-js="table_column_mv"
data-selector=".col_mv, td, th"
data-exclude=".o_mail_no_options"
data-drop-near=".col_mv, td, th"/>
<div data-js="content"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, .oe_snippet_body"
data-exclude=".o_mail_no_options"
data-drop-near="[data-oe-field='body_html']:not(:has(.o_layout)) > *, .oe_structure > *"
data-drop-in="[data-oe-field='body_html']:not(:has(.o_layout)), .oe_structure"/>
<div data-js="sizing_y"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_resize, .o_mail_no_options"/>
<div data-js="colorpicker"
data-selector=".note-editable > div:not(.o_layout), .note-editable .oe_structure > div, td, th"
data-exclude=".o_mail_no_colorpicker, .o_mail_no_options">
<div class="dropdown-submenu">
<a tabindex="-1" href="#" class="dropdown-item"><i class="fa fa-eyedropper"/>Background Color</a>
<div class="dropdown-menu" role="menu"/>
</div>
</div>
</template>
</odoo>
+3 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_mailing
from . import mail_compose_message
from . import mass_mailing_list_merge
from . import mass_mailing_schedule_date
from . import mailing_list_merge
from . import mailing_mailing_schedule_date
from . import mailing_mailing_test
@@ -1,22 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
from odoo.tools import email_re
class MailComposeMessage(models.TransientModel):
"""Add concept of mass mailing campaign to the mail.compose.message wizard
"""
_inherit = 'mail.compose.message'
mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing', ondelete='cascade')
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing', ondelete='cascade')
mass_mailing_name = fields.Char(string='Mass Mailing Name')
mailing_list_ids = fields.Many2many('mail.mass_mailing.list', string='Mailing List')
mailing_list_ids = fields.Many2many('mailing.list', string='Mailing List')
def get_mail_values(self, res_ids):
""" Override method that generated the mail content by creating the
mail.mail.statistics values in the o2m of mail_mail, when doing pure
mailing.trace values in the o2m of mail_mail, when doing pure
email mass mailing. """
self.ensure_one()
res = super(MailComposeMessage, self).get_mail_values(res_ids)
@@ -28,7 +27,7 @@ class MailComposeMessage(models.TransientModel):
if not mass_mailing:
reply_to_mode = 'email' if self.no_auto_thread else 'thread'
reply_to = self.reply_to if self.no_auto_thread else False
mass_mailing = self.env['mail.mass_mailing'].create({
mass_mailing = self.env['mailing.mailing'].create({
'mass_mailing_campaign_id': self.mass_mailing_campaign_id.id,
'name': self.mass_mailing_name,
'template_id': self.template_id.id,
@@ -71,7 +70,7 @@ class MailComposeMessage(models.TransientModel):
mail_values['state'] = 'cancel'
elif seen_list is not None:
seen_list.add(mail_to)
stat_vals = {
trace_vals = {
'model': self.model,
'res_id': res_id,
'mass_mailing_id': mass_mailing.id,
@@ -79,12 +78,12 @@ class MailComposeMessage(models.TransientModel):
}
if mail_values.get('body_html') and mass_mail_layout:
mail_values['body_html'] = mass_mail_layout.render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True)
# propagate ignored state to stat when still-born
# propagate ignored state to trace when still-born
if mail_values.get('state') == 'cancel':
stat_vals['ignored'] = fields.Datetime.now()
trace_vals['ignored'] = fields.Datetime.now()
mail_values.update({
'mailing_id': mass_mailing.id,
'statistics_ids': [(0, 0, stat_vals)],
'mailing_trace_ids': [(0, 0, trace_vals)],
# email-mode: keep original message for routing
'notification': mass_mailing.reply_to_mode == 'thread',
'auto_delete': not mass_mailing.keep_archives,
@@ -5,11 +5,11 @@ from odoo import api, fields, models
class MassMailingListMerge(models.TransientModel):
_name = 'mass.mailing.list.merge'
_name = 'mailing.list.merge'
_description = 'Merge Mass Mailing List'
src_list_ids = fields.Many2many('mail.mass_mailing.list', string='Mailing Lists')
dest_list_id = fields.Many2one('mail.mass_mailing.list', string='Destination Mailing List')
src_list_ids = fields.Many2many('mailing.list', string='Mailing Lists')
dest_list_id = fields.Many2one('mailing.list', string='Destination Mailing List')
merge_options = fields.Selection([
('new', 'Merge into a new mailing list'),
('existing', 'Merge into an existing mailing list'),
@@ -29,7 +29,7 @@ class MassMailingListMerge(models.TransientModel):
def action_mailing_lists_merge(self):
if self.merge_options == 'new':
self.dest_list_id = self.env['mail.mass_mailing.list'].create({
self.dest_list_id = self.env['mailing.list'].create({
'name': self.new_list_name,
}).id
self.dest_list_id.action_merge(self.src_list_ids, self.archive_src_lists)
@@ -1,9 +1,9 @@
<?xml version="1.0"?>
<odoo>
<!-- Merge Mailing List -->
<record id="mass_mailing_list_merge_view_form" model="ir.ui.view">
<field name="name">mass.mailing.list.merge.form</field>
<field name="model">mass.mailing.list.merge</field>
<record id="mailing_list_merge_view_form" model="ir.ui.view">
<field name="name">mailing.list.merge.form</field>
<field name="model">mailing.list.merge</field>
<field name="arch" type="xml">
<form string="Merge Mass Mailing List">
<group>
@@ -26,8 +26,8 @@
</field>
</record>
<act_window id="mass_mailing_list_merge_action" name="Merge Selected Mailing Lists"
res_model="mass.mailing.list.merge"
binding_model="mail.mass_mailing.list" binding_views="list"
<act_window id="mailing_list_merge_action" name="Merge Selected Mailing Lists"
res_model="mailing.list.merge"
binding_model="mailing.list" binding_views="list"
view_mode="form" target="new"/>
</odoo>
@@ -5,12 +5,12 @@ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class MassMailingScheduleDate(models.TransientModel):
_name = 'mass.mailing.schedule.date'
class MailingMailingScheduleDate(models.TransientModel):
_name = 'mailing.mailing.schedule.date'
_description = 'Mass Mailing Scheduling'
schedule_date = fields.Datetime(string='Schedule in the Future')
mass_mailing_id = fields.Many2one('mail.mass_mailing', required=True)
mass_mailing_id = fields.Many2one('mailing.mailing', required=True)
@api.constrains('schedule_date')
def _check_schedule_date(self):
@@ -1,8 +1,8 @@
<?xml version="1.0"?>
<odoo>
<record id="mass_mailing_schedule_date_view_form" model="ir.ui.view">
<field name="name">mass.mailing.schedule.date.view.form</field>
<field name="model">mass.mailing.schedule.date</field>
<record id="mailing_mailing_schedule_date_view_form" model="ir.ui.view">
<field name="name">mailing.mailing.schedule.date.view.form</field>
<field name="model">mailing.mailing.schedule.date</field>
<field name="arch" type="xml">
<form string="Take Future Schedule Date">
<group>
@@ -18,9 +18,9 @@
</field>
</record>
<record id="mass_mailing_schedule_date_action" model="ir.actions.act_window">
<record id="mailing_mailing_schedule_date_action" model="ir.actions.act_window">
<field name="name">When do you want to send your mailing?</field>
<field name="res_model">mass.mailing.schedule.date</field>
<field name="res_model">mailing.mailing.schedule.date</field>
<field name="type">ir.actions.act_window</field>
<field name="view_mode">form</field>
<field name="target">new</field>
@@ -5,12 +5,12 @@ from odoo import api, fields, models, tools
class TestMassMailing(models.TransientModel):
_name = 'mail.mass_mailing.test'
_name = 'mailing.mailing.test'
_description = 'Sample Mail Wizard'
email_to = fields.Char(string='Recipients', required=True,
help='Comma-separated list of email addresses.', default=lambda self: self.env['mail.message']._get_default_from())
mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mailing', required=True, ondelete='cascade')
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mailing', required=True, ondelete='cascade')
def send_mail_test(self):
self.ensure_one()
@@ -2,8 +2,8 @@
<odoo>
<record model="ir.ui.view" id="view_mail_mass_mailing_test_form">
<field name="name">mail.mass_mailing.test.form</field>
<field name="model">mail.mass_mailing.test</field>
<field name="name">mailing.mailing.test.form</field>
<field name="model">mailing.mailing.test</field>
<field name="arch" type="xml">
<form string="Send a Sample Mail">
<p class="text-muted">
@@ -22,7 +22,7 @@
<record id="action_mail_mass_mailing_test" model="ir.actions.act_window">
<field name="name">Mailing Test</field>
<field name="res_model">mail.mass_mailing.test</field>
<field name="res_model">mailing.mailing.test</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
+1 -1
View File
@@ -9,7 +9,7 @@
'description': """UTM and mass mailing on lead / opportunities""",
'depends': ['crm', 'mass_mailing'],
'data': [
'views/mass_mailing_views.xml',
'views/mailing_mailing_views.xml',
],
'demo': [
'data/mass_mailing_demo.xml',
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="1">
<record id="mass_mail_lead_0" model="mail.mass_mailing">
<record id="mass_mail_lead_0" model="mailing.mailing">
<field name="name">Lead Recall</field>
<field name="subject">We want to hear form you !</field>
<field name="state">in_queue</field>
@@ -10,7 +10,6 @@
<field name="mass_mailing_campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="utm.utm_source_mailing"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="mailing_model_id" ref="crm.model_crm_lead"/>
<field name="mailing_domain">[]</field>
<field name="reply_to_mode">email</field>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mass_mailing
from . import mailing_mailing
@@ -7,8 +7,8 @@ from odoo.osv import expression
class MassMailing(models.Model):
_name = 'mail.mass_mailing'
_inherit = 'mail.mass_mailing'
_name = 'mailing.mailing'
_inherit = 'mailing.mailing'
crm_lead_activated = fields.Boolean('Use Leads', compute='_compute_crm_lead_activated')
crm_lead_count = fields.Integer('Lead Count', groups='sales_team.group_sale_salesman', compute='_compute_crm_lead_and_opportunities_count')
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail_mass_mailing_view_form" model="ir.ui.view">
<field name="name">mail.mass_mailing.view.form.inherit.crm</field>
<field name="model">mail.mass_mailing</field>
<record id="mailing_mailing_view_form" model="ir.ui.view">
<field name="name">mailing.mailing.view.form.inherit.crm</field>
<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">
+1 -1
View File
@@ -2,4 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event
from . import mass_mailing
from . import mailing_mailing
+1 -1
View File
@@ -15,7 +15,7 @@ class Event(models.Model):
mass_mailing_action = dict(
name='Mass Mail Attendees',
type='ir.actions.act_window',
res_model='mail.mass_mailing',
res_model='mailing.mailing',
view_mode='form',
target='current',
context=dict(
@@ -2,10 +2,11 @@ from odoo import models, api
class MassMailingCampaign(models.Model):
_inherit = "mail.mass_mailing"
_inherit = "mailing.mailing"
@api.onchange('mailing_model_id', 'contact_list_ids')
def _onchange_model_and_list(self):
# TDE FIXME: whuuut ?
result = super(MassMailingCampaign, self)._onchange_model_and_list()
if self.mailing_model_name == 'event.registration' and self.mailing_domain == '[]':
self.mailing_domain = self.env.context.get('default_mailing_domain', '[]')
@@ -11,7 +11,7 @@ class Event(models.Model):
mass_mailing_action = dict(
name='Mass Mail Attendees',
type='ir.actions.act_window',
res_model='mail.mass_mailing',
res_model='mailing.mailing',
view_mode='form',
target='current',
context=dict(
+1 -1
View File
@@ -9,7 +9,7 @@
'description': """UTM and mass mailing on sale orders""",
'depends': ['sale', 'mass_mailing'],
'data': [
'views/mass_mailing_views.xml',
'views/mailing_mailing_views.xml',
],
'demo': [
'data/mass_mailing_demo.xml',
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<odoo>
<data noupdate="0">
<record id="mass_mail_sale_order_0" model="mail.mass_mailing">
<record id="mass_mail_sale_order_0" model="mailing.mailing">
<field name="name">Sale Promotion 1</field>
<field name="subject">Our last promotions, just for you !</field>
<field name="state">in_queue</field>
@@ -10,7 +10,6 @@
<field name="mass_mailing_campaign_id" eval="ref('mass_mailing.mass_mail_campaign_1')"/>
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
<field name="medium_id" ref="utm.utm_medium_email"/>
<field name="mailing_model_id" ref="sale.model_sale_order"/>
<field name="mailing_domain">[]</field>
<field name="reply_to_mode">email</field>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mass_mailing
from . import mailing_mailing
@@ -6,8 +6,8 @@ from odoo import api, fields, models
class MassMailing(models.Model):
_name = 'mail.mass_mailing'
_inherit = 'mail.mass_mailing'
_name = 'mailing.mailing'
_inherit = 'mailing.mailing'
sale_quotation_count = fields.Integer('Quotation Count', groups='sales_team.group_sale_salesman', compute='_compute_sale_quotation_count')
sale_invoiced_amount = fields.Integer('Invoiced Amount', groups='sales_team.group_sale_salesman', compute='_compute_sale_invoiced_amount')
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mail_mass_mailing_view_form" model="ir.ui.view">
<field name="name">mail.mass_mailing.view.form.inherit.sale</field>
<field name="model">mail.mass_mailing</field>
<record id="mailing_mailing_view_form" model="ir.ui.view">
<field name="name">mailing.mailing.view.form.inherit.sale</field>
<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">
@@ -15,7 +15,7 @@ class TestComposer(common.SavepointCase):
)
test_record = self.env['res.partner'].create({'name': 'Mass Mail Partner'})
mass_mail_record = self.env['mail.mass_mailing'].with_user(test_user).create({
mass_mail_record = self.env['mailing.mailing'].with_user(test_user).create({
'name': 'Test',
'subject': 'Test',
'state': 'draft',
+3 -3
View File
@@ -45,10 +45,10 @@ class TestLinkTracker(common.MassMailingCase):
@users('marketing')
def test_add_link_mail_stat(self):
mailing = self.env['mail.mass_mailing'].create({'name': 'Test Mailing', "subject": "Hi!"})
mailing = self.env['mailing.mailing'].create({'name': 'Test Mailing', "subject": "Hi!"})
code = self.link.code
self.assertEqual(self.link.count, 1)
stat = self.env['mail.mail.statistics'].create({'mass_mailing_id': mailing.id})
stat = self.env['mailing.trace'].create({'mass_mailing_id': mailing.id})
self.assertFalse(stat.opened)
self.assertFalse(stat.clicked)
@@ -57,7 +57,7 @@ class TestLinkTracker(common.MassMailingCase):
code,
ip='100.00.00.01',
country_code='BEL',
mail_stat_id=stat.id
mailing_trace_id=stat.id
)
self.assertEqual(self.link.count, 2)
self.assertEqual(click.mass_mailing_id, mailing)
@@ -6,10 +6,10 @@ import datetime
class TestAutoBlacklist(common.TransactionCase):
def test_mail_bounced_auto_blacklist(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing_contacts = self.env['mailing.contact']
mass_mailing = self.env['mailing.mailing']
mail_blacklist = self.env['mail.blacklist']
mail_statistics = self.env['mail.mail.statistics']
mail_statistics = self.env['mailing.trace']
mail_thread = self.env['mail.thread']
# create mailing contact record
@@ -17,35 +17,34 @@ class TestAutoBlacklist(common.TransactionCase):
# create bounced history
mail_statistics.create({
'model': 'mail.mass_mailing.contact',
'model': 'mailing.contact',
'res_id': self.mailing_contact_1.id,
'bounced': datetime.datetime.now() - datetime.timedelta(weeks=2),
'email': self.mailing_contact_1.email
})
self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1)
mail_statistics.create({
'model': 'mail.mass_mailing.contact',
'model': 'mailing.contact',
'res_id': self.mailing_contact_1.id,
'bounced': datetime.datetime.now() - datetime.timedelta(weeks=3),
'email': self.mailing_contact_1.email
})
self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1)
mail_statistics.create({
'model': 'mail.mass_mailing.contact',
'model': 'mailing.contact',
'res_id': self.mailing_contact_1.id,
'bounced': datetime.datetime.now() - datetime.timedelta(weeks=4),
'email': self.mailing_contact_1.email
})
self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1)
mail_statistics.create({
'model': 'mail.mass_mailing.contact',
'model': 'mailing.contact',
'res_id': self.mailing_contact_1.id,
'bounced': datetime.datetime.now() - datetime.timedelta(weeks=5),
'email': self.mailing_contact_1.email
})
self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1)
# create mass mailing record
self.mass_mailing = mass_mailing.create({
'name': 'test',
@@ -54,7 +53,7 @@ class TestAutoBlacklist(common.TransactionCase):
[self.mailing_contact_1.id])],
'body_html': 'This is a bounced mail for auto blacklist demo'})
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -71,7 +70,7 @@ class TestAutoBlacklist(common.TransactionCase):
composer.send_mail()
mail_statistics.create({
'model': 'mail.mass_mailing.contact',
'model': 'mailing.contact',
'res_id': self.mailing_contact_1.id,
'bounced': datetime.datetime.now(),
'email': self.mailing_contact_1.email
@@ -32,7 +32,7 @@ class TestMassMail(MassMailingCase):
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink')
def test_link_tracker(self):
_url = 'https://www.example.com/foo/bar?baz=qux'
mailing = self.env['mail.mass_mailing'].create({
mailing = self.env['mailing.mailing'].create({
'name': 'TestMailing',
'subject': 'Test',
'medium_id': self.test_medium.id,
@@ -42,7 +42,7 @@ class TestMassMail(MassMailingCase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
mailing.send_mail()
mailing.action_send_mail()
# basic test emails are sent
self.assertEqual(mailing.sent, 5)
@@ -68,8 +68,8 @@ class TestMassMail(MassMailingCase):
class TestAccessRights(TransactionCase):
def test_01_mass_mail_blacklist(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing_contacts = self.env['mailing.contact']
mass_mailing = self.env['mailing.mailing']
mail_blacklist = self.env['mail.blacklist']
# create mailing contact record
@@ -92,7 +92,7 @@ class TestAccessRights(TransactionCase):
self.mailing_contact_4.id, self.mailing_contact_5.id])],
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -110,10 +110,10 @@ class TestAccessRights(TransactionCase):
'blacklist ignored email number incorrect, should be equals to 2')
def test_02_mass_mail_simple_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing_contacts = self.env['mailing.contact']
mass_mailing_lists = self.env['mailing.list']
mass_mailing_optout = self.env['mailing.contact.subscription']
mass_mailing = self.env['mailing.mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'})
@@ -159,10 +159,10 @@ class TestAccessRights(TransactionCase):
'Opt Out ignored email number incorrect, should be equals to 1')
def test_03_mass_mail_multi_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing_contacts = self.env['mailing.contact']
mass_mailing_lists = self.env['mailing.list']
mass_mailing_optout = self.env['mailing.contact.subscription']
mass_mailing = self.env['mailing.mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'})
@@ -216,10 +216,10 @@ class TestAccessRights(TransactionCase):
'Opt Out ignored email number incorrect, should be equals to 0')
def test_04_mass_mail_multi_users_different_opt_out(self):
mass_mailing_contacts = self.env['mail.mass_mailing.contact']
mass_mailing_lists = self.env['mail.mass_mailing.list']
mass_mailing_optout = self.env['mail.mass_mailing.list_contact_rel']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing_contacts = self.env['mailing.contact']
mass_mailing_lists = self.env['mailing.list']
mass_mailing_optout = self.env['mailing.contact.subscription']
mass_mailing = self.env['mailing.mailing']
# create mailing contact record
mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email (A)', 'email': 'test@email.com'})
@@ -271,7 +271,7 @@ class TestOnResPartner(TransactionCase):
def test_mass_mail_on_res_partner(self):
partners = self.env['res.partner']
mass_mailing = self.env['mail.mass_mailing']
mass_mailing = self.env['mailing.mailing']
# create mailing contact record
partner_a = partners.create({
@@ -296,7 +296,7 @@ class TestOnResPartner(TransactionCase):
'body_html': 'This is mass mail marketing demo'})
self.mass_mailing.mailing_model_real = 'res.partner'
self.mass_mailing.put_in_queue()
res_ids = self.mass_mailing.get_remaining_recipients()
res_ids = self.mass_mailing._get_remaining_recipients()
composer_values = {
'body': self.mass_mailing.convert_links()[self.mass_mailing.id],
'subject': self.mass_mailing.name,
@@ -41,7 +41,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
@warmup
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
def test_send_mailing(self):
mailing = self.env['mail.mass_mailing'].create({
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
@@ -51,7 +51,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
})
with self.assertQueryCount(__system__=2477, marketing=3128):
mailing.send_mail()
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
self.assertEqual(mailing.delivered, 50)
@@ -79,7 +79,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
@warmup
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.models.unlink', 'odoo.tests')
def test_send_mailing_w_bl(self):
mailing = self.env['mail.mass_mailing'].create({
mailing = self.env['mailing.mailing'].create({
'name': 'Test',
'subject': 'Test',
'body_html': '<p>Hello <a role="button" href="https://www.example.com/foo/bar?baz=qux">quux</a><a role="button" href="/unsubscribe_from_list">Unsubscribe</a></p>',
@@ -89,7 +89,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
})
with self.assertQueryCount(__system__=2864, marketing=3611):
mailing.send_mail()
mailing.action_send_mail()
self.assertEqual(mailing.sent, 50)
self.assertEqual(mailing.delivered, 50)
+2 -1
View File
@@ -15,7 +15,8 @@ On a simple click, your visitors can subscribe to mailing lists managed in the E
'security/ir.model.access.csv',
'views/website_mass_mailing_templates.xml',
'views/snippets_templates.xml',
'views/mass_mailing_view.xml',
'views/mailing_list_views.xml',
'views/website_mass_mailing_views.xml',
],
'qweb': [
'static/src/xml/*.xml',
@@ -18,29 +18,29 @@ class MassMailController(MassMailController):
is_subscriber = False
if email:
contacts_count = request.env['mail.mass_mailing.list_contact_rel'].sudo().search_count([('list_id', 'in', [int(list_id)]), ('contact_id.email', '=', email), ('opt_out', '=', False)])
contacts_count = request.env['mailing.contact.subscription'].sudo().search_count([('list_id', 'in', [int(list_id)]), ('contact_id.email', '=', email), ('opt_out', '=', False)])
is_subscriber = contacts_count > 0
return {'is_subscriber': is_subscriber, 'email': email}
@route('/website_mass_mailing/subscribe', type='json', website=True, auth="public")
def subscribe(self, list_id, email, **post):
List_contact_rel = request.env['mail.mass_mailing.list_contact_rel'].sudo()
Contacts = request.env['mail.mass_mailing.contact'].sudo()
ContactSubscription = request.env['mailing.contact.subscription'].sudo()
Contacts = request.env['mailing.contact'].sudo()
name, email = Contacts.get_name_email(email)
list_contact_rel = List_contact_rel.search([('list_id', '=', int(list_id)), ('contact_id.email', '=', email)], limit=1)
if not list_contact_rel:
subscription = ContactSubscription.search([('list_id', '=', int(list_id)), ('contact_id.email', '=', email)], limit=1)
if not subscription:
# inline add_to_list as we've already called half of it
contact_id = Contacts.search([('email', '=', email)], limit=1)
if not contact_id:
contact_id = Contacts.create({'name': name, 'email': email})
List_contact_rel.create({'contact_id': contact_id.id, 'list_id': int(list_id)})
elif list_contact_rel.opt_out:
list_contact_rel.opt_out = False
ContactSubscription.create({'contact_id': contact_id.id, 'list_id': int(list_id)})
elif subscription.opt_out:
subscription.opt_out = False
# add email to session
request.session['mass_mailing_email'] = email
mass_mailing_list = request.env['mail.mass_mailing.list'].sudo().browse(list_id)
mass_mailing_list = request.env['mailing.list'].sudo().browse(list_id)
return {'toast_content': mass_mailing_list.toast_content}
@route(['/website_mass_mailing/get_content'], type='json', website=True, auth="public")
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mass_mailing_list
from . import mailing_list
from . import website_mass_mailing
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class MailingList(models.Model):
_inherit = 'mailing.list'
def _default_toast_content(self):
return '<p>Thanks for subscribing!</p>'
website_popup_ids = fields.One2many('website.mass_mailing.popup', 'mailing_list_id', string="Website Popups")
toast_content = fields.Html(default=_default_toast_content, translate=True)
@@ -4,16 +4,6 @@
from odoo import fields, models
class MassMailingList(models.Model):
_inherit = 'mail.mass_mailing.list'
def _default_toast_content(self):
return '<p>Thanks for subscribing!</p>'
website_popup_ids = fields.One2many('website.mass_mailing.popup', 'mailing_list_id', string="Website Popups")
toast_content = fields.Html(default=_default_toast_content, translate=True)
class MassMailingPopup(models.Model):
_name = 'website.mass_mailing.popup'
_description = "Mailing list popup"
@@ -21,6 +11,6 @@ class MassMailingPopup(models.Model):
def _default_popup_content(self):
return self.env['ir.ui.view'].render_template('website_mass_mailing.s_newsletter_block')
mailing_list_id = fields.Many2one('mail.mass_mailing.list')
mailing_list_id = fields.Many2one('mailing.list')
website_id = fields.Many2one('website')
popup_content = fields.Html(string="Website Popup Content", default=_default_popup_content, translate=True, sanitize=False)
@@ -30,7 +30,7 @@ options.registry.mailing_list_subscribe = options.Class.extend({
'select': _t("Newsletter"),
'init': function (field, dialog) {
return rpc.query({
model: 'mail.mass_mailing.list',
model: 'mailing.list',
method: 'name_search',
args: ['', [['is_public', '=', true]]],
context: self.options.recordInfo.context,
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="mailing_list_view_form" model="ir.ui.view">
<field name="name">mailing.list.view.form.inherit.website</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="//sheet" position="inside">
<notebook>
<page string="Website popups">
<field name="website_popup_ids">
<tree>
<field name="website_id"/>
</tree>
</field>
</page>
</notebook>
</xpath>
<xpath expr="//div[hasclass('oe_title')]" position="after">
<group>
<field name="toast_content"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -1,29 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_mail_mass_mailing_list_form" model="ir.ui.view">
<field name="name">mail.mass_mailing.list.form</field>
<field name="model">mail.mass_mailing.list</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_list_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="Website popups">
<field name="website_popup_ids">
<tree>
<field name="website_id"/>
</tree>
</field>
</page>
</notebook>
</xpath>
<xpath expr="//div[hasclass('oe_title')]" position="after">
<group>
<field name="toast_content"/>
</group>
</xpath>
</field>
</record>
<record id="view_mail_mass_mailing_popup_form" model="ir.ui.view">
<field name="name">website.mass_mailing.popup.form</field>
<field name="model">website.mass_mailing.popup</field>