Начальное наполнение
This commit is contained in:
parent
067ad00fea
commit
082399ed82
97
README.md
97
README.md
@ -1,2 +1,97 @@
|
|||||||
# mass_mailing
|
Odoo Mass Mailing
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Easily send mass mailing to your leads, opportunities or customers
|
||||||
|
with Odoo <a href="https://www.odoo.com/app/email-marketing">Email Marketing</a>. Track
|
||||||
|
marketing campaigns performance to improve conversion rates. Design
|
||||||
|
professional emails and reuse templates in a few clicks.
|
||||||
|
|
||||||
|
Send Professional Emails
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Import database of prospects or filter on existing leads, opportunities and
|
||||||
|
customers in just a few clicks.
|
||||||
|
|
||||||
|
Define email templates to reuse content or specific design for your newsletter.
|
||||||
|
Setup several email servers with their own IP/domain to optimise opening rates.
|
||||||
|
|
||||||
|
Organize Marketing Campaigns
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
Design, Send, Track by Campaigns with our <a href="https://www.odoo.com/app/email-marketing">Lead Automation</a> app.
|
||||||
|
|
||||||
|
Get real time statistics on campaigns performance to improve your conversion
|
||||||
|
rate. Track mails sent, received, opened and answered.
|
||||||
|
|
||||||
|
Easily manage your marketing campaigns, discussion groups, leads and
|
||||||
|
opportunities in one simple and powerful platform.
|
||||||
|
|
||||||
|
Integrated with Odoo Apps
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Get access to mass mailing features from every Odoo app to improve the way your
|
||||||
|
users communicate.
|
||||||
|
|
||||||
|
Send template of emails from Odoo <a href="https://www.odoo.com/app/email-marketing">CRM opportunities</a>, select leads based
|
||||||
|
on marketing segments, send <a href="https://www.odoo.com/app/recruitment">job offers</a> and automate
|
||||||
|
answers to applicants, reuse email template in the lead automation marketing
|
||||||
|
campaigns.
|
||||||
|
|
||||||
|
Answers to your emails appears automatically in the history of every document
|
||||||
|
with the social network module.
|
||||||
|
|
||||||
|
Clean Your Lead Database
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Get a clean lead database that improves over the time using the performance of
|
||||||
|
your mails. Odoo handle bounce mails efficiently, flag erroneous leads
|
||||||
|
accordingly and gives you statistics on the quality of your leads.
|
||||||
|
|
||||||
|
One click emails send
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The marketing department will love working on campaigns. But you can also give
|
||||||
|
a one click mass mailing facility to all others users on their own prospects or
|
||||||
|
documents.
|
||||||
|
|
||||||
|
Select a few documents (e.g. leads, support tickets, suppliers, applicants,
|
||||||
|
...) and send emails to their contacts in one click, reusing existing emails
|
||||||
|
templates.
|
||||||
|
|
||||||
|
Follow-up On Answers
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The chatter feature enables you to communicate faster and more efficiently with
|
||||||
|
your customer. Get documents created automatically (leads, opportunities,
|
||||||
|
tasks, ...) based on answers to your mass mailing campaigns Follow the
|
||||||
|
discussion directly on the business documents within Odoo or via email.
|
||||||
|
|
||||||
|
Get all the negotiations and discussions attached to the right document and
|
||||||
|
relevent managers notified on specific events.
|
||||||
|
|
||||||
|
Campaigns Dashboard
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Get the insights you need to make smarter marketing campaign. Track statistics
|
||||||
|
per campaign: bounce rates, sent mails, best content, etc. The clear dashboards
|
||||||
|
gives you a direct overview of your campaign performance.
|
||||||
|
|
||||||
|
Fully Integrated With Others Apps
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Define automation rules (e.g. ask a salesperson to call, send an email, ...)
|
||||||
|
based on triggers (no activity since 20 days, answered a promotional email,
|
||||||
|
etc.)
|
||||||
|
|
||||||
|
Optimize campaigns from lead to close, on every channel. Make smarter decisions
|
||||||
|
about where to invest and show the impact of your marketing activities on your
|
||||||
|
company's bottom line.
|
||||||
|
|
||||||
|
Integrate a contact form in your website easily. Forms submissions create leads
|
||||||
|
automatically in Odoo CRM. Leads can be used in marketing campaigns.
|
||||||
|
|
||||||
|
Manage your <a href="https://www.odoo.com/app/crm">sales funnel</a> with no
|
||||||
|
effort. Attract leads, follow-up on phone calls and meetings. Analyse the
|
||||||
|
quality of your leads to make informed decisions and save time by integrating
|
||||||
|
emails directly into the application.
|
||||||
|
|
||||||
|
7
__init__.py
Normal file
7
__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
|
from . import report
|
||||||
|
from . import wizard
|
174
__manifest__.py
Normal file
174
__manifest__.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Email Marketing',
|
||||||
|
'summary': 'Design, send and track emails',
|
||||||
|
'version': '2.7',
|
||||||
|
'sequence': 60,
|
||||||
|
'website': 'https://www.odoo.com/app/email-marketing',
|
||||||
|
'category': 'Marketing/Email Marketing',
|
||||||
|
'depends': [
|
||||||
|
'contacts',
|
||||||
|
'mail',
|
||||||
|
'utm',
|
||||||
|
'link_tracker',
|
||||||
|
'web_editor',
|
||||||
|
'social_media',
|
||||||
|
'web_tour',
|
||||||
|
'digest',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/res_groups_data.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/digest_data.xml',
|
||||||
|
'data/ir_attachment_data.xml',
|
||||||
|
'data/ir_config_parameter_data.xml',
|
||||||
|
'data/ir_cron_data.xml',
|
||||||
|
'data/ir_module_data.xml',
|
||||||
|
'data/mailing_data_templates.xml',
|
||||||
|
'data/mailing_list_contact.xml',
|
||||||
|
'data/mailing_subscription_optout.xml',
|
||||||
|
'data/mailing_subscription.xml',
|
||||||
|
'data/res_users_data.xml',
|
||||||
|
'wizard/mail_compose_message_views.xml',
|
||||||
|
'wizard/mailing_contact_import_views.xml',
|
||||||
|
'wizard/mailing_contact_to_list_views.xml',
|
||||||
|
'wizard/mailing_list_merge_views.xml',
|
||||||
|
'wizard/mailing_mailing_test_views.xml',
|
||||||
|
'wizard/mailing_mailing_schedule_date_views.xml',
|
||||||
|
'report/mailing_trace_report_views.xml',
|
||||||
|
'views/mail_blacklist_views.xml',
|
||||||
|
'views/mailing_filter_views.xml',
|
||||||
|
'views/mailing_mobile_preview_content.xml',
|
||||||
|
'views/mailing_trace_views.xml',
|
||||||
|
'views/link_tracker_views.xml',
|
||||||
|
'views/mailing_contact_views.xml',
|
||||||
|
'views/mailing_list_views.xml',
|
||||||
|
'views/mailing_mailing_views.xml',
|
||||||
|
'views/mailing_subscription_views.xml',
|
||||||
|
'views/mailing_subscription_optout_views.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/utm_campaign_views.xml',
|
||||||
|
'views/mailing_menus.xml',
|
||||||
|
'views/mailing_templates_portal_layouts.xml',
|
||||||
|
'views/mailing_templates_portal_management.xml',
|
||||||
|
'views/mailing_templates_portal_unsubscribe.xml',
|
||||||
|
'views/themes_templates.xml',
|
||||||
|
'views/snippets_themes.xml',
|
||||||
|
'views/snippets/s_alert.xml',
|
||||||
|
'views/snippets/s_blockquote.xml',
|
||||||
|
'views/snippets/s_call_to_action.xml',
|
||||||
|
'views/snippets/s_coupon_code.xml',
|
||||||
|
'views/snippets/s_cover.xml',
|
||||||
|
'views/snippets/s_color_blocks_2.xml',
|
||||||
|
'views/snippets/s_company_team.xml',
|
||||||
|
'views/snippets/s_comparisons.xml',
|
||||||
|
'views/snippets/s_event.xml',
|
||||||
|
'views/snippets/s_features.xml',
|
||||||
|
'views/snippets/s_features_grid.xml',
|
||||||
|
'views/snippets/s_hr.xml',
|
||||||
|
'views/snippets/s_image_text.xml',
|
||||||
|
'views/snippets/s_masonry_block.xml',
|
||||||
|
'views/snippets/s_media_list.xml',
|
||||||
|
'views/snippets/s_numbers.xml',
|
||||||
|
'views/snippets/s_picture.xml',
|
||||||
|
'views/snippets/s_product_list.xml',
|
||||||
|
'views/snippets/s_rating.xml',
|
||||||
|
'views/snippets/s_references.xml',
|
||||||
|
'views/snippets/s_showcase.xml',
|
||||||
|
'views/snippets/s_text_block.xml',
|
||||||
|
'views/snippets/s_text_highlight.xml',
|
||||||
|
'views/snippets/s_text_image.xml',
|
||||||
|
'views/snippets/s_three_columns.xml',
|
||||||
|
'views/snippets/s_title.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'demo/utm.xml',
|
||||||
|
'demo/mailing_list_contact.xml',
|
||||||
|
'demo/mailing_subscription.xml',
|
||||||
|
'demo/mailing_mailing.xml',
|
||||||
|
'demo/mailing_trace.xml',
|
||||||
|
],
|
||||||
|
'application': True,
|
||||||
|
'assets': {
|
||||||
|
'mass_mailing.iframe_css_assets_edit': [
|
||||||
|
('include', 'mass_mailing.assets_mail_themes'),
|
||||||
|
('include', 'web.assets_frontend'),
|
||||||
|
('after', 'web/static/lib/bootstrap/scss/_variables.scss', 'mass_mailing/static/src/scss/mass_mailing.ui.scss'),
|
||||||
|
('include', 'web_editor.backend_assets_wysiwyg'),
|
||||||
|
('include', 'web_editor.assets_legacy_wysiwyg'),
|
||||||
|
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing_mail.scss',
|
||||||
|
],
|
||||||
|
'mass_mailing.iframe_css_assets_readonly': [
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing_mail.scss',
|
||||||
|
'mass_mailing/static/src/css/basic_theme_readonly.css'
|
||||||
|
],
|
||||||
|
'mass_mailing.mailing_assets': [
|
||||||
|
'mass_mailing/static/src/scss/mailing_portal.scss',
|
||||||
|
'mass_mailing/static/src/js/mailing_portal_subscription.js',
|
||||||
|
'mass_mailing/static/src/js/mailing_portal_subscription_blocklist.js',
|
||||||
|
'mass_mailing/static/src/js/mailing_portal_subscription_feedback.js',
|
||||||
|
'mass_mailing/static/src/js/mailing_portal_subscription_form.js',
|
||||||
|
'mass_mailing/static/src/xml/mailing_portal_subscription_blocklist.xml',
|
||||||
|
'mass_mailing/static/src/xml/mailing_portal_subscription_feedback.xml',
|
||||||
|
'mass_mailing/static/src/xml/mailing_portal_subscription_form.xml',
|
||||||
|
],
|
||||||
|
'web_editor.backend_assets_wysiwyg': [
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_wysiwyg.js',
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing.wysiwyg.scss',
|
||||||
|
],
|
||||||
|
'web.assets_backend': [
|
||||||
|
'mass_mailing/static/src/scss/mailing_filter_widget.scss',
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing.scss',
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing_mobile.scss',
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing_mobile_preview.scss',
|
||||||
|
'mass_mailing/static/src/css/email_template.css',
|
||||||
|
'mass_mailing/static/src/js/mailing_m2o_filter.js',
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_design_constants.js',
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_mobile_preview.js',
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_html_field.js',
|
||||||
|
'mass_mailing/static/src/js/mailing_mailing_view_form_full_width.js',
|
||||||
|
'mass_mailing/static/src/xml/mailing_filter_widget.xml',
|
||||||
|
'mass_mailing/static/src/xml/mass_mailing.xml',
|
||||||
|
'mass_mailing/static/src/xml/mass_mailing_mobile_preview.xml',
|
||||||
|
'mass_mailing/static/src/js/tours/**/*',
|
||||||
|
],
|
||||||
|
'mass_mailing.assets_mail_themes': [
|
||||||
|
'mass_mailing/static/src/scss/themes/**/*',
|
||||||
|
],
|
||||||
|
'mass_mailing.assets_mail_themes_edition': [
|
||||||
|
('include', 'web._assets_helpers'),
|
||||||
|
'web/static/src/scss/pre_variables.scss',
|
||||||
|
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||||
|
'mass_mailing/static/src/scss/mass_mailing.ui.scss',
|
||||||
|
],
|
||||||
|
'mass_mailing.assets_wysiwyg': [
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_snippets.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_masonry_block/options.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_media_list/options.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_showcase/options.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_rating/options.js'
|
||||||
|
],
|
||||||
|
'web_editor.assets_legacy_wysiwyg': [
|
||||||
|
'mass_mailing/static/src/js/snippets.editor.js',
|
||||||
|
],
|
||||||
|
'web.assets_frontend': [
|
||||||
|
'mass_mailing/static/src/js/tours/**/*',
|
||||||
|
],
|
||||||
|
'web.assets_tests': [
|
||||||
|
'mass_mailing/static/tests/tours/**/*',
|
||||||
|
],
|
||||||
|
'web.qunit_suite_tests': [
|
||||||
|
'mass_mailing/static/tests/mass_mailing_favourite_filter_tests.js',
|
||||||
|
'mass_mailing/static/src/js/mass_mailing_snippets.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_media_list/options.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_showcase/options.js',
|
||||||
|
'mass_mailing/static/src/snippets/s_rating/options.js',
|
||||||
|
'mass_mailing/static/tests/mass_mailing_html_tests.js',
|
||||||
|
'mass_mailing/static/tests/mailing_mailing_view_form_tests.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
5
controllers/__init__.py
Normal file
5
controllers/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import legacy
|
||||||
|
from . import main
|
21
controllers/legacy.py
Normal file
21
controllers/legacy.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
|
||||||
|
class MailingLegacy(http.Controller):
|
||||||
|
""" Retro compatibility layer for legacy endpoint"""
|
||||||
|
|
||||||
|
@http.route(['/mail/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
|
||||||
|
def mailing_unsubscribe(self, mailing_id, email=None, res_id=None, token="", **post):
|
||||||
|
""" Old route, using mail/mailing prefix, and outdated parameter names """
|
||||||
|
params = werkzeug.urls.url_encode(
|
||||||
|
dict(**post, document_id=res_id, email=email, hash_token=token)
|
||||||
|
)
|
||||||
|
return request.redirect(
|
||||||
|
f'/mailing/{mailing_id}/unsubscribe?{params}'
|
||||||
|
)
|
491
controllers/main.py
Normal file
491
controllers/main.py
Normal file
@ -0,0 +1,491 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from markupsafe import Markup, escape
|
||||||
|
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
||||||
|
|
||||||
|
from odoo import _, fields, http, tools
|
||||||
|
from odoo.http import request, Response
|
||||||
|
from odoo.tools import consteq
|
||||||
|
|
||||||
|
|
||||||
|
class MassMailController(http.Controller):
|
||||||
|
|
||||||
|
def _check_mailing_email_token(self, mailing_id, document_id, email, hash_token,
|
||||||
|
required_mailing_id=False):
|
||||||
|
""" Return the mailing based on given credentials, sudo-ed. Raises if
|
||||||
|
there is an issue fetching it.
|
||||||
|
|
||||||
|
Specific use case
|
||||||
|
* hash_token is always required for public users, no generic page is
|
||||||
|
available for them;
|
||||||
|
* hash_token is not required for generic page for logged user, aka
|
||||||
|
if no mailing_id is given;
|
||||||
|
* hash_token is not required for mailing specific page if the user
|
||||||
|
is a mailing user;
|
||||||
|
* hash_token is not required for generic page for logged user, aka
|
||||||
|
if no mailing_id is given and if mailing_id is not required;
|
||||||
|
* hash_token always requires the triplet mailing_id, email and
|
||||||
|
document_id, as it indicates it comes from a mailing email and
|
||||||
|
is used when comparing hashes;
|
||||||
|
"""
|
||||||
|
if not hash_token:
|
||||||
|
if request.env.user._is_public():
|
||||||
|
raise BadRequest()
|
||||||
|
if mailing_id and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
|
||||||
|
raise BadRequest()
|
||||||
|
if hash_token and (not mailing_id or not email or not document_id):
|
||||||
|
raise BadRequest()
|
||||||
|
if mailing_id:
|
||||||
|
mailing_sudo = request.env['mailing.mailing'].sudo().browse(mailing_id)
|
||||||
|
if not mailing_sudo.exists():
|
||||||
|
raise NotFound()
|
||||||
|
if hash_token and not consteq(mailing_sudo._generate_mailing_recipient_token(document_id, email), hash_token):
|
||||||
|
raise Unauthorized()
|
||||||
|
else:
|
||||||
|
if required_mailing_id:
|
||||||
|
raise BadRequest()
|
||||||
|
mailing_sudo = request.env['mailing.mailing'].sudo()
|
||||||
|
return mailing_sudo
|
||||||
|
|
||||||
|
def _fetch_blocklist_record(self, email):
|
||||||
|
if not email or not tools.email_normalize(email):
|
||||||
|
return None
|
||||||
|
return request.env['mail.blacklist'].sudo().with_context(
|
||||||
|
active_test=False
|
||||||
|
).search(
|
||||||
|
[('email', '=', tools.email_normalize(email))]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_contacts(self, email):
|
||||||
|
if not email or not tools.email_normalize(email):
|
||||||
|
return request.env['mailing.contact']
|
||||||
|
return request.env['mailing.contact'].sudo().search(
|
||||||
|
[('email_normalized', '=', tools.email_normalize(email))]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _fetch_subscription_optouts(self):
|
||||||
|
return request.env['mailing.subscription.optout'].sudo().search([])
|
||||||
|
|
||||||
|
def _fetch_user_information(self, email, hash_token):
|
||||||
|
if hash_token or request.env.user._is_public():
|
||||||
|
return email, hash_token
|
||||||
|
return request.env.user.email_normalized, None
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# SUBSCRIPTION MANAGEMENT
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@http.route('/mailing/my', type='http', website=True, auth='user')
|
||||||
|
def mailing_my(self):
|
||||||
|
email, _hash_token = self._fetch_user_information(None, None)
|
||||||
|
if not email:
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
render_values = self._prepare_mailing_subscription_values(
|
||||||
|
request.env['mailing.mailing'], False, email, None
|
||||||
|
)
|
||||||
|
render_values.update(feedback_enabled=False)
|
||||||
|
return request.render(
|
||||||
|
'mass_mailing.page_mailing_unsubscribe',
|
||||||
|
render_values
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route(['/mailing/<int:mailing_id>/unsubscribe'], type='http', website=True, auth='public')
|
||||||
|
def mailing_unsubscribe(self, mailing_id, document_id=None, email=None, hash_token=None):
|
||||||
|
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||||
|
try:
|
||||||
|
mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email_found, hash_token_found,
|
||||||
|
required_mailing_id=True
|
||||||
|
)
|
||||||
|
except NotFound as e: # avoid leaking ID existence
|
||||||
|
raise Unauthorized() from e
|
||||||
|
|
||||||
|
if mailing_sudo.mailing_on_mailing_list:
|
||||||
|
return self._mailing_unsubscribe_from_list(mailing_sudo, document_id, email_found, hash_token_found)
|
||||||
|
return self._mailing_unsubscribe_from_document(mailing_sudo, document_id, email_found, hash_token_found)
|
||||||
|
|
||||||
|
def _mailing_unsubscribe_from_list(self, mailing, document_id, email, hash_token):
|
||||||
|
# Unsubscribe directly + Let the user choose their subscriptions
|
||||||
|
|
||||||
|
mailing.contact_list_ids._update_subscription_from_email(email, opt_out=True)
|
||||||
|
# compute name of unsubscribed list: hide non public lists
|
||||||
|
if all(not mlist.is_public for mlist in mailing.contact_list_ids):
|
||||||
|
lists_unsubscribed_name = _('You are no longer part of our mailing list(s).')
|
||||||
|
elif len(mailing.contact_list_ids) == 1:
|
||||||
|
lists_unsubscribed_name = _('You are no longer part of the %(mailing_name)s mailing list.',
|
||||||
|
mailing_name=mailing.contact_list_ids.name)
|
||||||
|
else:
|
||||||
|
lists_unsubscribed_name = _(
|
||||||
|
'You are no longer part of the %(mailing_names)s mailing list.',
|
||||||
|
mailing_names=', '.join(mlist.name for mlist in mailing.contact_list_ids if mlist.is_public)
|
||||||
|
)
|
||||||
|
|
||||||
|
return request.render(
|
||||||
|
'mass_mailing.page_mailing_unsubscribe',
|
||||||
|
dict(
|
||||||
|
self._prepare_mailing_subscription_values(
|
||||||
|
mailing, document_id, email, hash_token
|
||||||
|
),
|
||||||
|
last_action='subscription_updated',
|
||||||
|
unsubscribed_name=lists_unsubscribed_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mailing_unsubscribe_from_document(self, mailing, document_id, email, hash_token):
|
||||||
|
if document_id:
|
||||||
|
message = Markup(_(
|
||||||
|
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (document %(record_link)s)',
|
||||||
|
**self._format_bl_request(mailing, document_id)
|
||||||
|
))
|
||||||
|
else:
|
||||||
|
message = Markup(_(
|
||||||
|
'Blocklist request from unsubscribe link of mailing %(mailing_link)s (direct link usage)',
|
||||||
|
**self._format_bl_request(mailing, document_id)
|
||||||
|
))
|
||||||
|
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email, message=Markup('<p>%s</p>') % message)
|
||||||
|
|
||||||
|
return request.render(
|
||||||
|
'mass_mailing.page_mailing_unsubscribe',
|
||||||
|
dict(
|
||||||
|
self._prepare_mailing_subscription_values(
|
||||||
|
mailing, document_id, email, hash_token
|
||||||
|
),
|
||||||
|
last_action='blocklist_add',
|
||||||
|
unsubscribed_name=_('You are no longer part of our services and will not be contacted again.'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _prepare_mailing_subscription_values(self, mailing, document_id, email, hash_token):
|
||||||
|
""" Prepare common values used in various subscription management or
|
||||||
|
blocklist flows done in portal. """
|
||||||
|
mail_blocklist = self._fetch_blocklist_record(email)
|
||||||
|
email_normalized = tools.email_normalize(email)
|
||||||
|
|
||||||
|
# fetch optout/blacklist reasons
|
||||||
|
opt_out_reasons = self._fetch_subscription_optouts()
|
||||||
|
|
||||||
|
# as there may be several contacts / email -> consider any opt-in overrides
|
||||||
|
# opt-out
|
||||||
|
contacts = self._fetch_contacts(email)
|
||||||
|
lists_optin = contacts.subscription_ids.filtered(
|
||||||
|
lambda sub: not sub.opt_out
|
||||||
|
).list_id.filtered('active')
|
||||||
|
lists_optout = contacts.subscription_ids.filtered(
|
||||||
|
lambda sub: sub.opt_out and sub.list_id not in lists_optin
|
||||||
|
).list_id.filtered('active')
|
||||||
|
lists_public = request.env['mailing.list'].sudo().search(
|
||||||
|
[('is_public', '=', True),
|
||||||
|
('id', 'not in', (lists_optin + lists_optout).ids)
|
||||||
|
],
|
||||||
|
limit=10,
|
||||||
|
order='create_date DESC, id DESC',
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
# customer
|
||||||
|
'document_id': document_id,
|
||||||
|
'email': email,
|
||||||
|
'email_valid': bool(email_normalized),
|
||||||
|
'hash_token': hash_token,
|
||||||
|
'mailing_id': mailing.id,
|
||||||
|
'res_id': document_id,
|
||||||
|
# feedback
|
||||||
|
'feedback_enabled': True,
|
||||||
|
'feedback_readonly': False,
|
||||||
|
'opt_out_reasons': opt_out_reasons,
|
||||||
|
# blocklist
|
||||||
|
'blocklist_enabled': bool(
|
||||||
|
request.env['ir.config_parameter'].sudo().get_param(
|
||||||
|
'mass_mailing.show_blacklist_buttons',
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'blocklist_possible': mail_blocklist is not None,
|
||||||
|
'is_blocklisted': mail_blocklist.active if mail_blocklist else False,
|
||||||
|
# mailing lists
|
||||||
|
'contacts': contacts,
|
||||||
|
'lists_contacts': contacts.subscription_ids.list_id.filtered('active'),
|
||||||
|
'lists_optin': lists_optin,
|
||||||
|
'lists_optout': lists_optout,
|
||||||
|
'lists_public': lists_public,
|
||||||
|
}
|
||||||
|
|
||||||
|
@http.route('/mailing/list/update', type='json', auth='public', csrf=True)
|
||||||
|
def mailing_update_list_subscription(self, mailing_id=None, document_id=None,
|
||||||
|
email=None, hash_token=None,
|
||||||
|
lists_optin_ids=None, **post):
|
||||||
|
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||||
|
try:
|
||||||
|
_mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email_found, hash_token_found,
|
||||||
|
required_mailing_id=False
|
||||||
|
)
|
||||||
|
except BadRequest:
|
||||||
|
return 'error'
|
||||||
|
except (NotFound, Unauthorized):
|
||||||
|
return 'unauthorized'
|
||||||
|
|
||||||
|
contacts = self._fetch_contacts(email_found)
|
||||||
|
lists_optin = request.env['mailing.list'].sudo().browse(lists_optin_ids or []).exists()
|
||||||
|
# opt-out all not chosen lists
|
||||||
|
lists_to_optout = contacts.subscription_ids.filtered(
|
||||||
|
lambda sub: not sub.opt_out and sub.list_id not in lists_optin
|
||||||
|
).list_id
|
||||||
|
# opt-in in either already member, either public (to avoid trying to opt-in
|
||||||
|
# in private lists)
|
||||||
|
lists_to_optin = lists_optin.filtered(
|
||||||
|
lambda mlist: mlist.is_public or mlist in contacts.list_ids
|
||||||
|
)
|
||||||
|
lists_to_optout._update_subscription_from_email(email_found, opt_out=True)
|
||||||
|
lists_to_optin._update_subscription_from_email(email_found, opt_out=False)
|
||||||
|
|
||||||
|
return len(lists_to_optout)
|
||||||
|
|
||||||
|
@http.route('/mailing/feedback', type='json', auth='public', csrf=True)
|
||||||
|
def mailing_send_feedback(self, mailing_id=None, document_id=None,
|
||||||
|
email=None, hash_token=None,
|
||||||
|
last_action=None,
|
||||||
|
opt_out_reason_id=False, feedback=None,
|
||||||
|
**post):
|
||||||
|
""" Feedback can be given after some actions, notably after opt-outing
|
||||||
|
from mailing lists or adding an email in the blocklist.
|
||||||
|
|
||||||
|
This controller tries to write the customer feedback in the most relevant
|
||||||
|
record. Feedback consists in two parts, the opt-out reason (based on data
|
||||||
|
in 'mailing.subscription.optout' model) and the feedback itself (which
|
||||||
|
is triggered by the optout reason 'is_feedback' fields).
|
||||||
|
"""
|
||||||
|
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||||
|
try:
|
||||||
|
mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email_found, hash_token_found,
|
||||||
|
required_mailing_id=False,
|
||||||
|
)
|
||||||
|
except BadRequest:
|
||||||
|
return 'error'
|
||||||
|
except (NotFound, Unauthorized):
|
||||||
|
return 'unauthorized'
|
||||||
|
|
||||||
|
if not opt_out_reason_id:
|
||||||
|
return 'error'
|
||||||
|
feedback = feedback.strip() if feedback else ''
|
||||||
|
message = ''
|
||||||
|
if feedback:
|
||||||
|
if not request.env.user._is_public():
|
||||||
|
author_name = f'{request.env.user.name} ({email_found})'
|
||||||
|
else:
|
||||||
|
author_name = email_found
|
||||||
|
message = Markup("<p>%s<br />%s</p>") % (
|
||||||
|
_('Feedback from %(author_name)s', author_name=author_name),
|
||||||
|
feedback
|
||||||
|
)
|
||||||
|
|
||||||
|
# blocklist addition: opt-out and feedback linked to the mail.blacklist records
|
||||||
|
if last_action == 'blocklist_add':
|
||||||
|
mail_blocklist = self._fetch_blocklist_record(email)
|
||||||
|
if mail_blocklist:
|
||||||
|
if message:
|
||||||
|
mail_blocklist._track_set_log_message(message)
|
||||||
|
mail_blocklist.opt_out_reason_id = opt_out_reason_id
|
||||||
|
|
||||||
|
# opt-outed from mailing lists (either from a mailing or directly from 'my')
|
||||||
|
# -> in that case, update recently-updated subscription records and log on
|
||||||
|
# contacts
|
||||||
|
documents_for_post = []
|
||||||
|
if (last_action in {'subscription_updated', 'subscription_updated_optout'} or
|
||||||
|
(not last_action and (not mailing_sudo or mailing_sudo.mailing_on_mailing_list))):
|
||||||
|
contacts = self._fetch_contacts(email_found)
|
||||||
|
contacts.subscription_ids.filtered(
|
||||||
|
lambda sub: sub.opt_out and sub.opt_out_datetime >= (fields.Datetime.now() - timedelta(minutes=10))
|
||||||
|
).opt_out_reason_id = opt_out_reason_id
|
||||||
|
if message:
|
||||||
|
documents_for_post = contacts
|
||||||
|
# feedback coming from a mailing, without additional context information: log
|
||||||
|
elif mailing_sudo and message:
|
||||||
|
documents_for_post = request.env[mailing_sudo.mailing_model_real].sudo().search(
|
||||||
|
[('id', '=', document_id)
|
||||||
|
])
|
||||||
|
|
||||||
|
for document_sudo in documents_for_post:
|
||||||
|
document_sudo.message_post(body=message)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public', sitemap=False)
|
||||||
|
def mailing_unsubscribe_placeholder_link(self, **post):
|
||||||
|
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
|
||||||
|
return request.redirect('/mailing/my', code=301, local=True)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# TRACKING
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@http.route('/mail/track/<int:mail_id>/<string:token>/blank.gif', type='http', auth='public')
|
||||||
|
def track_mail_open(self, mail_id, token, **post):
|
||||||
|
""" Email tracking. """
|
||||||
|
expected_token = request.env['mail.mail']._generate_mail_recipient_token(mail_id)
|
||||||
|
if not consteq(token, expected_token):
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
request.env['mailing.trace'].sudo().set_opened(domain=[('mail_mail_id_int', 'in', [mail_id])])
|
||||||
|
response = Response()
|
||||||
|
response.mimetype = 'image/gif'
|
||||||
|
response.data = base64.b64decode(b'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==')
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@http.route('/r/<string:code>/m/<int:mailing_trace_id>', type='http', auth="public")
|
||||||
|
def full_url_redirect(self, code, mailing_trace_id, **post):
|
||||||
|
request.env['link.tracker.click'].sudo().add_click(
|
||||||
|
code,
|
||||||
|
ip=request.httprequest.remote_addr,
|
||||||
|
country_code=request.geoip.country_code,
|
||||||
|
mailing_trace_id=mailing_trace_id
|
||||||
|
)
|
||||||
|
redirect_url = request.env['link.tracker'].get_url_from_code(code)
|
||||||
|
if not redirect_url:
|
||||||
|
raise NotFound()
|
||||||
|
return request.redirect(redirect_url, code=301, local=False)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# MAILING MANAGEMENT
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@http.route('/mailing/report/unsubscribe', type='http', website=True, auth='public')
|
||||||
|
def mailing_report_deactivate(self, token, user_id):
|
||||||
|
if not token or not user_id:
|
||||||
|
raise BadRequest()
|
||||||
|
user = request.env['res.users'].sudo().browse(int(user_id)).exists()
|
||||||
|
if not user or not user.has_group('mass_mailing.group_mass_mailing_user') or \
|
||||||
|
not consteq(token, request.env['mailing.mailing']._generate_mailing_report_token(user.id)):
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
request.env['ir.config_parameter'].sudo().set_param('mass_mailing.mass_mailing_reports', False)
|
||||||
|
render_vals = {}
|
||||||
|
if user.has_group('base.group_system'):
|
||||||
|
render_vals = {'menu_id': request.env.ref('mass_mailing.menu_mass_mailing_global_settings').id}
|
||||||
|
return request.render('mass_mailing.mailing_report_deactivated', render_vals)
|
||||||
|
|
||||||
|
@http.route(['/mailing/<int:mailing_id>/view'], type='http', website=True, auth='public')
|
||||||
|
def mailing_view_in_browser(self, mailing_id, email=None, document_id=None, hash_token=None, **kwargs):
|
||||||
|
# backward compatibility: temporary for mailings sent before migation to 17
|
||||||
|
document_id = document_id or kwargs.get('res_id')
|
||||||
|
hash_token = hash_token or kwargs.get('token')
|
||||||
|
try:
|
||||||
|
mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email, hash_token,
|
||||||
|
required_mailing_id=True,
|
||||||
|
)
|
||||||
|
except NotFound as e:
|
||||||
|
raise Unauthorized() from e
|
||||||
|
|
||||||
|
# do not force lang, will simply use user context
|
||||||
|
document_id = int(document_id) if document_id and document_id.isdigit() else 0
|
||||||
|
html_markupsafe = mailing_sudo._render_field(
|
||||||
|
'body_html',
|
||||||
|
[document_id],
|
||||||
|
compute_lang=False,
|
||||||
|
options={'post_process': False}
|
||||||
|
)[document_id]
|
||||||
|
# Update generic URLs (without parameters) to final ones
|
||||||
|
if document_id:
|
||||||
|
html_markupsafe = html_markupsafe.replace(
|
||||||
|
'/unsubscribe_from_list',
|
||||||
|
mailing_sudo._get_unsubscribe_url(email, document_id)
|
||||||
|
)
|
||||||
|
else: # when manually trying a /view on a mailing, not through email link
|
||||||
|
html_markupsafe = html_markupsafe.replace(
|
||||||
|
'/unsubscribe_from_list',
|
||||||
|
werkzeug.urls.url_join(
|
||||||
|
mailing_sudo.get_base_url(),
|
||||||
|
f'/mailing/{mailing_sudo.id}/unsubscribe',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return request.render(
|
||||||
|
'mass_mailing.mailing_view',
|
||||||
|
{
|
||||||
|
'body': html_markupsafe,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# BLACKLIST
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@http.route('/mailing/blocklist/add', type='json', auth='public')
|
||||||
|
def mail_blocklist_add(self, mailing_id=None, document_id=None,
|
||||||
|
email=None, hash_token=None):
|
||||||
|
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||||
|
try:
|
||||||
|
mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email_found, hash_token_found,
|
||||||
|
required_mailing_id=False,
|
||||||
|
)
|
||||||
|
except BadRequest:
|
||||||
|
return 'error'
|
||||||
|
except (NotFound, Unauthorized):
|
||||||
|
return 'unauthorized'
|
||||||
|
|
||||||
|
if mailing_sudo:
|
||||||
|
message = Markup(
|
||||||
|
_(
|
||||||
|
'Blocklist request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||||
|
**self._format_bl_request(mailing_sudo, document_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = Markup('<p>%s</p>') % _('Blocklist request from portal')
|
||||||
|
|
||||||
|
_blocklist_rec = request.env['mail.blacklist'].sudo()._add(email_found, message=message)
|
||||||
|
return True
|
||||||
|
|
||||||
|
@http.route('/mailing/blocklist/remove', type='json', auth='public')
|
||||||
|
def mail_blocklist_remove(self, mailing_id=None, document_id=None,
|
||||||
|
email=None, hash_token=None):
|
||||||
|
email_found, hash_token_found = self._fetch_user_information(email, hash_token)
|
||||||
|
try:
|
||||||
|
mailing_sudo = self._check_mailing_email_token(
|
||||||
|
mailing_id, document_id, email_found, hash_token_found,
|
||||||
|
required_mailing_id=False,
|
||||||
|
)
|
||||||
|
except BadRequest:
|
||||||
|
return 'error'
|
||||||
|
except (NotFound, Unauthorized):
|
||||||
|
return 'unauthorized'
|
||||||
|
|
||||||
|
if mailing_sudo and document_id:
|
||||||
|
message = Markup(
|
||||||
|
_(
|
||||||
|
'Blocklist removal request from portal of mailing %(mailing_link)s (document %(record_link)s)',
|
||||||
|
**self._format_bl_request(mailing_sudo, document_id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = Markup('<p>%s</p>') % _('Blocklist removal request from portal')
|
||||||
|
|
||||||
|
_blocklist_rec = request.env['mail.blacklist'].sudo()._remove(email_found, message=message)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _format_bl_request(self, mailing, document_id):
|
||||||
|
mailing_model_name = request.env['ir.model']._get(mailing.mailing_model_real).display_name
|
||||||
|
return {
|
||||||
|
'mailing_link': Markup(f'<a href="#" data-oe-model="mailing.mailing" data-oe-id="{mailing.id}">{escape(mailing.subject)}</a>'),
|
||||||
|
'record_link': Markup(
|
||||||
|
f'<a href="#" data-oe-model="{escape(mailing.mailing_model_real)}" data-oe-id="{int(document_id)}">{escape(mailing_model_name)}</a>'
|
||||||
|
) if document_id else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# PREVIEW
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
@http.route('/mailing/mobile/preview', methods=['GET'], type='http', auth='user', website=True)
|
||||||
|
def mass_mailing_preview_mobile_content(self):
|
||||||
|
return request.render("mass_mailing.preview_content_mobile", {})
|
16
data/digest_data.xml
Normal file
16
data/digest_data.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<template id="digest_mail_main" inherit_id="digest.digest_mail_main">
|
||||||
|
<xpath expr="//div[hasclass('by_odoo')]/t[last()]" position="after">
|
||||||
|
<t t-if="mailing_report_token">
|
||||||
|
–
|
||||||
|
<a t-attf-href="/mailing/report/unsubscribe?token=#{mailing_report_token}&user_id=#{user_id}"
|
||||||
|
target="_blank" style="text-decoration: none;">
|
||||||
|
<span style="color: #8f8f8f;">Turn off Mailing Reports</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
162
data/ir_attachment_data.xml
Normal file
162
data/ir_attachment_data.xml
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Snippets' Default Images -->
|
||||||
|
<record id="mass_mailing.s_cover_default_image" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_cover_default_image.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_cover.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_media_list_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_media_list_default_image_1.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_media_list_1.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_media_list_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_media_list_default_image_2.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_media_list_2.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_media_list_default_image_3" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_media_list_default_image_3.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_media_list_3.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_company_team_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_company_team_default_image_1.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_team_member_1.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_company_team_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_company_team_default_image_2.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_team_member_2.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_company_team_default_image_3" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_company_team_default_image_3.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_team_member_3.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_company_team_default_image_4" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_company_team_default_image_4.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_team_member_4.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_reference_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_reference_default_image_1.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_references_1.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_reference_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_reference_default_image_2.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_references_2.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_reference_default_image_3" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_reference_default_image_3.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_references_3.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_reference_default_image_4" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_reference_default_image_4.png</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_references_4.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_product_list_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_product_list_default_image_1.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_product_1.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_product_list_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_product_list_default_image_2.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_product_2.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_product_list_default_image_3" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_product_list_default_image_3.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_product_3.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_blockquote_default_image" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_blockquote_default_image.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_team_member_2.png</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_image_text_default_image" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_image_text_default_image.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_image_text.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_event_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_event_default_image_1.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_event_1.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_event_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_event_default_image_2.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_event_2.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_masonry_block_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_masonry_block_default_image_1.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_masonry_block_1.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_masonry_block_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_masonry_block_default_image_2.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_masonry_block_2.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_picture_default_image" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_picture_default_image.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_picture.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_text_image_default_image" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_text_image_default_image.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_text_image.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_three_columns_default_image_1" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_three_columns_default_image_1.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_three_cols_1.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_three_columns_default_image_2" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_three_columns_default_image_2.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_three_cols_2.jpg</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mailing.s_three_columns_default_image_3" model="ir.attachment">
|
||||||
|
<field name="public" eval="True"/>
|
||||||
|
<field name="name">s_three_columns_default_image_3.jpg</field>
|
||||||
|
<field name="type">url</field>
|
||||||
|
<field name="url">/mass_mailing/static/src/img/theme_default/s_default_image_three_cols_3.jpg</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
13
data/ir_config_parameter_data.xml
Normal file
13
data/ir_config_parameter_data.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Enable Mass Mailing Reports -->
|
||||||
|
<function model="ir.config_parameter"
|
||||||
|
name="set_param"
|
||||||
|
eval="('mass_mailing.mass_mailing_reports', 'True')"/>
|
||||||
|
<!-- Display blacklist button to customers on portal page -->
|
||||||
|
<function model="ir.config_parameter"
|
||||||
|
name="set_param"
|
||||||
|
eval="('mass_mailing.show_blacklist_buttons', True)"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
30
data/ir_cron_data.xml
Normal file
30
data/ir_cron_data.xml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Cron that processes the mass mailing queue -->
|
||||||
|
<record id="ir_cron_mass_mailing_queue" model="ir.cron">
|
||||||
|
<field name="name">Mail Marketing: Process queue</field>
|
||||||
|
<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" />
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field eval="False" name="doall" />
|
||||||
|
</record>
|
||||||
|
<!-- Cron that processes the a/b testing -->
|
||||||
|
<record id="ir_cron_mass_mailing_ab_testing" model="ir.cron">
|
||||||
|
<field name="name">Mail Marketing: A/B Testing</field>
|
||||||
|
<field name="model_id" ref="model_utm_campaign"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._cron_process_mass_mailing_ab_testing()</field>
|
||||||
|
<field name="user_id" ref="base.user_root"/>
|
||||||
|
<field name="active" eval="False"/>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="numbercall">-1</field>
|
||||||
|
<field name="doall" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
10
data/ir_module_data.xml
Normal file
10
data/ir_module_data.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="0">
|
||||||
|
<record model="ir.module.category" id="base.module_category_marketing_email_marketing">
|
||||||
|
<field name="sequence">19</field>
|
||||||
|
<field name="description">Helps you manage your mass mailing to design
|
||||||
|
professional emails and reuse templates.</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
117
data/mailing_data_templates.xml
Normal file
117
data/mailing_data_templates.xml
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!--
|
||||||
|
Reference: https://litmus.com/community/learning/24-how-to-code-a-responsive-email-from-scratch
|
||||||
|
https://www.campaignmonitor.com/css/link-element/link-in-head/
|
||||||
|
-->
|
||||||
|
<template id="mass_mailing_mail_layout">
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<meta name="format-detection" content="telephone=no"/>
|
||||||
|
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=no;"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=9; IE=8; IE=7; IE=EDGE" />
|
||||||
|
|
||||||
|
<t t-out="mailing_style"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Prevent Outlook from distorting images with DPI scaling (see
|
||||||
|
https://www.htmlemailcheck.com/knowledge-base/dpi-scaling-in-outlook-2007-2013/):
|
||||||
|
-->
|
||||||
|
<!--[if mso]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<t t-out="body"/>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<data noupdate="0">
|
||||||
|
<template id="ab_testing_description" name="Mass Mailing: A/B Test Description">
|
||||||
|
<div name="ab_testing_description" class="mb-2">
|
||||||
|
<t t-if="mailing.state == 'done' and mailing.sent == 0 and mailing.failed == 0 and mailing.canceled == 0">
|
||||||
|
<span>There wasn't enough recipients left for this mailing</span>
|
||||||
|
</t>
|
||||||
|
<t t-if="mailing.ab_testing_completed">
|
||||||
|
<p t-if="mailing.ab_testing_is_winner_mailing">
|
||||||
|
This <t t-out="mailing.mailing_type_description"/> is the winner of the A/B testing campaign and has been sent to all remaining recipients.
|
||||||
|
</p>
|
||||||
|
<p t-else="">The winner has already been sent. Use <b>Compare Version</b> to get an overview of this A/B testing campaign.</p>
|
||||||
|
</t>
|
||||||
|
<t t-elif="ab_testing_count >= 2">
|
||||||
|
<p>
|
||||||
|
A sample of <b><t t-out="mailing.ab_testing_pc"/>% of recipients</b> will receive this version.<br/>
|
||||||
|
<t t-if="total_ab_testing_pc > 100 and mailing.active">
|
||||||
|
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"/>
|
||||||
|
The sum of all percentages for this A/B campaign totals <b class="text-danger"><t t-out="total_ab_testing_pc"/>%</b> of all potential recipients.<br/>
|
||||||
|
<b class="text-danger">Some of the mailings will not be sent</b>, as only 1 email will be sent for each unique recipient in this campaign.
|
||||||
|
</t>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<t t-if="mailing.ab_testing_winner_selection == 'manual'">Don't forget to send your preferred version</t>
|
||||||
|
<t t-elif="not mailing.ab_testing_schedule_datetime">Since the date and time for this test has not been scheduled, don't forget to manually send your preferred version.</t>
|
||||||
|
<t t-else="">
|
||||||
|
Then on <b><t t-out="mailing.ab_testing_schedule_datetime.strftime('%b %d, %Y')"/></b> the <t t-out="mailing.mailing_type_description"/> having the <b><t t-out="ab_testing_winner_selection_description"/></b> will be sent
|
||||||
|
</t> to the remaining recipients.
|
||||||
|
</p>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<p>
|
||||||
|
A sample of <b><t t-out="mailing.ab_testing_pc"/>% of recipients</b> will receive this version.
|
||||||
|
</p>
|
||||||
|
<t t-if="mailing.ab_testing_winner_selection != 'manual'">
|
||||||
|
<p>Try different variations in the campaign to compare their <t t-out="ab_testing_winner_selection_description"/>.</p>
|
||||||
|
<p>Once the best version is identified, we will send the best one to the remaining recipients.</p>
|
||||||
|
</t>
|
||||||
|
<p t-else="">Use alternative versions to be able to select the winner.</p>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="mass_mailing.mass_mailing_kpi_link_trackers" name="Marketing: mailing link trackers statistic">
|
||||||
|
<table t-if="link_trackers" class="global_table_layout" bgcolor="#ffffff" cellspacing="0" cellpadding="0" align="center" border="0" style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="width: 100%;">
|
||||||
|
<table cellspacing="0" cellpadding="0" border="0" align="center" style="width:100%;">
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
<span style="color:#282f33; font-size: 15px; font-weight: bold; line-height: 30px">
|
||||||
|
<t t-esc="'Click Rate Report on %i %s Sent' % (object.expected, mailing_type)"/>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="margin: 0; padding:0;">
|
||||||
|
<table cellspacing="0" cellpadding="0" border="0" align="center" style="width:100%; border-collapse: collapse;">
|
||||||
|
<tr t-att-style="'color: ' + (company.email_secondary_color or '#875a7b') + '; font-size: 16px; font-weight: 500;'">
|
||||||
|
<td style="width: 70%;padding: 10px 0; text-align: center; border: 1px solid #e7e7e7;">Button Label</td>
|
||||||
|
<td style="width: 30%;padding: 10px 0; text-align: center; border: 1px solid #e7e7e7;">%Click (Total)</td>
|
||||||
|
</tr>
|
||||||
|
<tr t-foreach="link_trackers" t-as="link_tracker" style="color: #888888; font-size: 15px; font-weight: 300;">
|
||||||
|
<td style="width: 70%;padding: 10px 0; border: 1px solid #e7e7e7;">
|
||||||
|
<a t-att-href="link_tracker.absolute_url" target="_blank" style="color: #56b3b5; text-decoration: none;" t-esc="link_tracker.label or link_tracker.url"/>
|
||||||
|
</td>
|
||||||
|
<td style="width: 30%;padding: 10px 0; text-align: center; border: 1px solid #e7e7e7;">
|
||||||
|
<t t-esc="int(link_tracker.count * 100 / object.sent) if object.sent else 0"/>% (<t t-esc="link_tracker.count"/>)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
14
data/mailing_list_contact.xml
Normal file
14
data/mailing_list_contact.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="mailing_list_data" model="mailing.list">
|
||||||
|
<field name="name">Newsletter</field>
|
||||||
|
<field name="is_public">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_0" model="mailing.contact">
|
||||||
|
<field name="name" model="res.users" eval="obj().env.ref('base.user_admin').name"/>
|
||||||
|
<field name="email" model="res.users" eval="obj().env.ref('base.user_admin').email"/>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
9
data/mailing_subscription.xml
Normal file
9
data/mailing_subscription.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="mailing_list_data_sub_contact_0" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_0"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
24
data/mailing_subscription_optout.xml
Normal file
24
data/mailing_subscription_optout.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo><data noupdate="0">
|
||||||
|
<record id="mailing_subscription_optout_data_0" model="mailing.subscription.optout">
|
||||||
|
<field name="name">I never subscribed to this list</field>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_subscription_optout_data_1" model="mailing.subscription.optout">
|
||||||
|
<field name="name">I changed my mind</field>
|
||||||
|
<field name="sequence">2</field>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_subscription_optout_data_2" model="mailing.subscription.optout">
|
||||||
|
<field name="name">I receive too many emails from this list</field>
|
||||||
|
<field name="sequence">3</field>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_subscription_optout_data_3" model="mailing.subscription.optout">
|
||||||
|
<field name="name">The content of these emails is not relevant to me</field>
|
||||||
|
<field name="sequence">4</field>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_subscription_optout_data_4" model="mailing.subscription.optout">
|
||||||
|
<field name="name">Other</field>
|
||||||
|
<field name="sequence">99</field>
|
||||||
|
<field name="is_feedback" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data></odoo>
|
8
data/res_users_data.xml
Normal file
8
data/res_users_data.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="base.default_user" model="res.users">
|
||||||
|
<field name="groups_id" eval="[(4,ref('mass_mailing.group_mass_mailing_user'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
43
demo/mailing_list_contact.xml
Normal file
43
demo/mailing_list_contact.xml
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Create Extra Mailing List for Demo -->
|
||||||
|
<record id="mailing_list_1" model="mailing.list">
|
||||||
|
<field name="name">Imported Contacts</field>
|
||||||
|
<field name="is_public">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Create Contacts -->
|
||||||
|
<record id="mass_mail_contact_1" model="mailing.contact">
|
||||||
|
<field name="name">Aristide Antario</field>
|
||||||
|
<field name="email">alexandre.antario@example.com</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_2" model="mailing.contact">
|
||||||
|
<field name="name">Beverly Bridge</field>
|
||||||
|
<field name="email">beverly.bridge@example.com</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_3" model="mailing.contact">
|
||||||
|
<field name="name">Carol Cartridge</field>
|
||||||
|
<field name="email">carol.cartridge@example.com</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_4" model="mailing.contact">
|
||||||
|
<field name="name">David Dawson</field>
|
||||||
|
<field name="email">david.dawson@example.com</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_5" model="mailing.contact">
|
||||||
|
<field name="name">Elsa Ericson</field>
|
||||||
|
<field name="email">elsa.ericson@example.com</field>
|
||||||
|
<field name="message_bounce">5</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_contact_6" model="mailing.contact">
|
||||||
|
<field name="name">Franz Faubourg</field>
|
||||||
|
<field name="email">franz.faubourg@example.com</field>
|
||||||
|
<field name="list_ids" eval="[(5, 0, 0)]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
102
demo/mailing_mailing.xml
Normal file
102
demo/mailing_mailing.xml
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="mass_mail_attach_1" model="ir.attachment">
|
||||||
|
<field name="datas">bWlncmF0aW9uIHRlc3Q=</field>
|
||||||
|
<field name="name">SampleDoc.doc</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mass_mail_1" model="mailing.mailing">
|
||||||
|
<field name="name">Newsletter 1</field>
|
||||||
|
<field name="subject">Monthly Newsletter</field>
|
||||||
|
<field name="state">done</field>
|
||||||
|
<field name="user_id" ref="base.user_admin"/>
|
||||||
|
<field name="email_from">info@yourcompany.example.com</field>
|
||||||
|
<field name="sent_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="campaign_id" ref="mass_mail_campaign_1"/>
|
||||||
|
<field name="source_id" ref="mass_mailing.utm_source_0"/>
|
||||||
|
<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">new</field>
|
||||||
|
<field name="reply_to">Info <info@yourcompany.example.com></field>
|
||||||
|
<field name="body_arch" type="html">
|
||||||
|
<div class="o_layout o_default_theme oe_unremovable oe_unmovable" data-name="Mailing">
|
||||||
|
<div class="container o_mail_wrapper oe_unremovable oe_unmovable" style="border-collapse:collapse;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col o_mail_no_options o_mail_wrapper_td bg-white oe_structure o_editable" style="text-align:left;width:100%;">
|
||||||
|
<div class="o_mail_block_header_logo" data-snippet="s_mail_block_header_logo">
|
||||||
|
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||||
|
<div class="container o_mail_h_padding" style="padding:0 20px 0 20px;width:100%;border-collapse:separate;">
|
||||||
|
<div class="row">
|
||||||
|
<div valign="center" width="30%" class="col text-center o_mail_v_padding pb0" style="padding:20px 0 0px 0;vertical-align:middle;text-align:center;">
|
||||||
|
<a href="http://www.example.com" style="text-decoration:none;font-weight:bold;background-color:transparent;color:rgb(100, 89, 116);">
|
||||||
|
<img border="0" src="/mass_mailing/static/src/img/theme_default/s_default_image_header_logo.png" style="border-style:none;height:auto;vertical-align:middle;max-width:400px;width:auto"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_mail_block_footer_separator" data-snippet="s_hr" style="margin:0 20px 0 20px;">
|
||||||
|
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||||
|
<div class="container" style="width:100%;border-collapse:separate;">
|
||||||
|
<div class="row">
|
||||||
|
<div valign="top" style="padding:20px 0 20px 0;text-align:left;vertical-align:top;width:100%;" class="col o_mail_v_padding o_mail_no_colorpicker">
|
||||||
|
<div style="background-color:rgb(245, 245, 245);height:2px;width:100%;" class="separator"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_mail_block_text" data-snippet="s_text_block">
|
||||||
|
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||||
|
<div class="container" style="width:100%;border-collapse:separate;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 o_mail_h_padding o_mail_v_padding o_mail_no_colorpicker" style="padding:20px;text-align:left;vertical-align:top;">
|
||||||
|
<p style="margin:0px 0 1rem 0;font-size:14px;">
|
||||||
|
Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.
|
||||||
|
<br/>Great stories are for everyone even when only written for just one person. If you try to write with a wide general audience in mind, your story will ring false and be bland. No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="o_mail_block_footer_social o_mail_footer_social_center" data-snippet="s_mail_block_footer_social">
|
||||||
|
<div class="o_mail_snippet_general" style="margin:0px auto 0px auto;background-color:rgb(255, 255, 255);max-width:600px;width:100%;">
|
||||||
|
<div align="center" class="container" style="border-style:solid none none none;padding:20px 0 20px 0;border-top-color:rgb(245, 245, 245);border-top-width:2px;width:100%;border-collapse:separate;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col o_mail_footer_links o_default_snippet_text" style="padding:10px 0 10px 0;text-align:center;vertical-align:middle;">
|
||||||
|
<a href="/unsubscribe_from_list" class="btn btn-link o_default_snippet_text" style="text-decoration:none;border-radius:0.25rem;border-style:solid;padding:0px;cursor:pointer;line-height:1.5;font-size:12px;border-start-color:transparent;border-bottom-color:transparent;border-end-color:transparent;border-top-color:transparent;border-start-width:1px;border-bottom-width:1px;border-end-width:1px;border-top-width:1px;user-select:none;vertical-align:middle;white-space:nowrap;text-align:center;font-weight:bold;display:inline-block;background-color:transparent;color:rgb(100, 89, 116);">Unsubscribe</a> |
|
||||||
|
|
||||||
|
<a href="/contactus" class="btn btn-link o_default_snippet_text" style="text-decoration:none;border-radius:0.25rem;border-style:solid;padding:0px;cursor:pointer;line-height:1.5;font-size:12px;border-start-color:transparent;border-bottom-color:transparent;border-end-color:transparent;border-top-color:transparent;border-start-width:1px;border-bottom-width:1px;border-end-width:1px;border-top-width:1px;user-select:none;vertical-align:middle;white-space:nowrap;text-align:center;font-weight:bold;display:inline-block;background-color:transparent;color:rgb(100, 89, 116);">Contact</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col" style="text-align:left;vertical-align:middle;">
|
||||||
|
<p class="o_mail_footer_copy" style="margin:0px 0 1rem 0;text-align:center;font-weight:bold;color:rgb(147, 146, 146);font-size:9px;">
|
||||||
|
<img src="/web_editor/font_to_img/61945/rgb(147,146,146)/9" data-class="fa fa-copyright" style="border-style:none;max-width:100%;width:100%;vertical-align:middle;height: auto; width: auto;"/>2018 All Rights Reserved
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div align="center" class="container" style="width:100%;border-collapse:separate;">
|
||||||
|
<div class="row">
|
||||||
|
<div align="center" style="padding:16px 0 16px 0;" class="col pt16 pb16">
|
||||||
|
Powered by <a target="_blank" href="https://www.odoo.com" style="text-decoration:none;background-color:transparent;color:#875A7B;">Odoo</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
<field name="attachment_ids" eval="[(4, ref('mass_mail_attach_1'))]"/>
|
||||||
|
</record>
|
||||||
|
<!-- Generate link tracker information from it -->
|
||||||
|
<function model="mailing.mailing" name="convert_links" eval="[ref('mass_mailing.mass_mail_1')]"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
46
demo/mailing_subscription.xml
Normal file
46
demo/mailing_subscription.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Subscription to 'mailing_list_data' -->
|
||||||
|
<record id="mailing_list_data_sub_contact_1" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_1"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_data_sub_contact_2" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_2"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_data_sub_contact_3" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_3"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_data_sub_contact_4" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_4"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
<field name="opt_out">True</field>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_data_sub_contact_5" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_5"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_data_sub_contact_6" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_6"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_data"/>
|
||||||
|
<field name="opt_out">True</field>
|
||||||
|
</record>
|
||||||
|
<!-- Subscription to 'mailing_list_1' -->
|
||||||
|
<record id="mailing_list_1_sub_contact_3" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_3"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_1"/>
|
||||||
|
</record>
|
||||||
|
<record id="mailing_list_1_sub_contact_6" model="mailing.subscription">
|
||||||
|
<field name="contact_id" ref="mass_mailing.mass_mail_contact_6"/>
|
||||||
|
<field name="list_id" ref="mass_mailing.mailing_list_1"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Create Blacklist Records -->
|
||||||
|
<record id="mail_blacklist_demo_1" model="mail.blacklist">
|
||||||
|
<field name="email">elsa.ericson@example.com</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
132
demo/mailing_trace.xml
Normal file
132
demo/mailing_trace.xml
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="mass_mail_1_stat_0" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111000@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_7"/>
|
||||||
|
<field name="email">billy.fox45@example.com</field>
|
||||||
|
<field name="trace_status">reply</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=4)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="reply_datetime" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_1" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111001@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_13"/>
|
||||||
|
<field name="email">kim.snyder96@example.com</field>
|
||||||
|
<field name="trace_status">reply</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="reply_datetime" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_2" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111002@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_14"/>
|
||||||
|
<field name="email">edith.sanchez68@example.com</field>
|
||||||
|
<field name="trace_status">open</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_3" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111003@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_24"/>
|
||||||
|
<field name="email">theodore.gardner36@example.com</field>
|
||||||
|
<field name="trace_status">open</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="open_datetime" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_4" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111004@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_32"/>
|
||||||
|
<field name="email">sandra.neal80@example.com</field>
|
||||||
|
<field name="trace_status">sent</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_5" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111005@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_33"/>
|
||||||
|
<field name="email">julie.richards84@example.com</field>
|
||||||
|
<field name="trace_status">error</field>
|
||||||
|
<field name="sent_datetime" eval="False"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_6" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111006@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_34"/>
|
||||||
|
<field name="email">travis.mendoza24@example.com</field>
|
||||||
|
<field name="trace_status">bounce</field>
|
||||||
|
<field name="sent_datetime" eval="(DateTime.today() - relativedelta(days=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
<field name="write_date" eval="(DateTime.today() - relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_1_stat_7" model="mailing.trace">
|
||||||
|
<field name="mass_mailing_id" ref="mass_mail_1"/>
|
||||||
|
<field name="message_id">1111007@odoo.com</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="res_id" ref="base.res_partner_address_34"/>
|
||||||
|
<field name="email">travis.mendoza24@example.com</field>
|
||||||
|
<field name="trace_status">bounce</field>
|
||||||
|
<field name="sent_datetime" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Generate some clicks -->
|
||||||
|
<function model="link.tracker.click" name="add_click">
|
||||||
|
<value model="link.tracker.code"
|
||||||
|
search="[('link_id.url', '=', 'http://www.example.com')]"
|
||||||
|
use="code"/>
|
||||||
|
<value name="ip">100.01.02.03</value>
|
||||||
|
<value name="country_code">BE</value>
|
||||||
|
<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"
|
||||||
|
search="[('link_id.url', '=', 'http://www.example.net/page/contactus')]"
|
||||||
|
use="code"/>
|
||||||
|
<value name="ip">100.01.02.03</value>
|
||||||
|
<value name="country_code">BE</value>
|
||||||
|
<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"
|
||||||
|
search="[('link_id.url', '=', 'http://www.example.com')]"
|
||||||
|
use="code"/>
|
||||||
|
<value name="ip">100.01.02.04</value>
|
||||||
|
<value name="country_code">BE</value>
|
||||||
|
<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"
|
||||||
|
search="[('link_id.url', '=', 'http://www.example.net/page/contactus')]"
|
||||||
|
use="code"/>
|
||||||
|
<value name="ip">100.01.02.04</value>
|
||||||
|
<value name="country_code">BE</value>
|
||||||
|
<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"
|
||||||
|
search="[('link_id.url', '=', 'http://www.example.com')]"
|
||||||
|
use="code"/>
|
||||||
|
<value name="ip">100.01.02.05</value>
|
||||||
|
<value name="country_code">BE</value>
|
||||||
|
<value name="mailing_trace_id" eval="ref('mass_mail_1_stat_2')"/>
|
||||||
|
</function>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
15
demo/utm.xml
Normal file
15
demo/utm.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Create campaign and mailings -->
|
||||||
|
<record id="utm_source_0" model="utm.source">
|
||||||
|
<field name="name">Newsletter 1</field>
|
||||||
|
</record>
|
||||||
|
<record id="mass_mail_campaign_1" model="utm.campaign">
|
||||||
|
<field name="name">Newsletter</field>
|
||||||
|
<field name="stage_id" ref="utm.campaign_stage_1"/>
|
||||||
|
<field name="user_id" ref="base.user_admin"/>
|
||||||
|
<field name="tag_ids" eval="[(6,0,[ref('utm.utm_tag_1')])]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
9
doc/changelog.rst
Normal file
9
doc/changelog.rst
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.. _changelog:
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
=========
|
||||||
|
|
||||||
|
`trunk (saas-2)`
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- added module
|
13
doc/index.rst
Normal file
13
doc/index.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Mass Mailing module documentation
|
||||||
|
=================================
|
||||||
|
|
||||||
|
Mass Mailing documentation topics
|
||||||
|
'''''''''''''''''''''''''''''''''
|
||||||
|
|
||||||
|
Changelog
|
||||||
|
'''''''''
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
changelog.rst
|
4762
i18n/af.po
Normal file
4762
i18n/af.po
Normal file
File diff suppressed because it is too large
Load Diff
4758
i18n/am.po
Normal file
4758
i18n/am.po
Normal file
File diff suppressed because it is too large
Load Diff
5465
i18n/ar.po
Normal file
5465
i18n/ar.po
Normal file
File diff suppressed because it is too large
Load Diff
4769
i18n/az.po
Normal file
4769
i18n/az.po
Normal file
File diff suppressed because it is too large
Load Diff
5260
i18n/bg.po
Normal file
5260
i18n/bg.po
Normal file
File diff suppressed because it is too large
Load Diff
4763
i18n/bs.po
Normal file
4763
i18n/bs.po
Normal file
File diff suppressed because it is too large
Load Diff
5478
i18n/ca.po
Normal file
5478
i18n/ca.po
Normal file
File diff suppressed because it is too large
Load Diff
5363
i18n/cs.po
Normal file
5363
i18n/cs.po
Normal file
File diff suppressed because it is too large
Load Diff
5354
i18n/da.po
Normal file
5354
i18n/da.po
Normal file
File diff suppressed because it is too large
Load Diff
5536
i18n/de.po
Normal file
5536
i18n/de.po
Normal file
File diff suppressed because it is too large
Load Diff
4764
i18n/el.po
Normal file
4764
i18n/el.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/en_GB.po
Normal file
4761
i18n/en_GB.po
Normal file
File diff suppressed because it is too large
Load Diff
5513
i18n/es.po
Normal file
5513
i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
5514
i18n/es_419.po
Normal file
5514
i18n/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_BO.po
Normal file
4761
i18n/es_BO.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_CL.po
Normal file
4761
i18n/es_CL.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_CO.po
Normal file
4761
i18n/es_CO.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_CR.po
Normal file
4761
i18n/es_CR.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_DO.po
Normal file
4761
i18n/es_DO.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_EC.po
Normal file
4761
i18n/es_EC.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_PE.po
Normal file
4761
i18n/es_PE.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_PY.po
Normal file
4761
i18n/es_PY.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/es_VE.po
Normal file
4761
i18n/es_VE.po
Normal file
File diff suppressed because it is too large
Load Diff
5444
i18n/et.po
Normal file
5444
i18n/et.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/eu.po
Normal file
4761
i18n/eu.po
Normal file
File diff suppressed because it is too large
Load Diff
5261
i18n/fa.po
Normal file
5261
i18n/fa.po
Normal file
File diff suppressed because it is too large
Load Diff
5531
i18n/fi.po
Normal file
5531
i18n/fi.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/fo.po
Normal file
4761
i18n/fo.po
Normal file
File diff suppressed because it is too large
Load Diff
5530
i18n/fr.po
Normal file
5530
i18n/fr.po
Normal file
File diff suppressed because it is too large
Load Diff
4760
i18n/fr_BE.po
Normal file
4760
i18n/fr_BE.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/fr_CA.po
Normal file
4761
i18n/fr_CA.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/gl.po
Normal file
4761
i18n/gl.po
Normal file
File diff suppressed because it is too large
Load Diff
4766
i18n/gu.po
Normal file
4766
i18n/gu.po
Normal file
File diff suppressed because it is too large
Load Diff
5332
i18n/he.po
Normal file
5332
i18n/he.po
Normal file
File diff suppressed because it is too large
Load Diff
4781
i18n/hr.po
Normal file
4781
i18n/hr.po
Normal file
File diff suppressed because it is too large
Load Diff
5263
i18n/hu.po
Normal file
5263
i18n/hu.po
Normal file
File diff suppressed because it is too large
Load Diff
5495
i18n/id.po
Normal file
5495
i18n/id.po
Normal file
File diff suppressed because it is too large
Load Diff
4762
i18n/is.po
Normal file
4762
i18n/is.po
Normal file
File diff suppressed because it is too large
Load Diff
5514
i18n/it.po
Normal file
5514
i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
5345
i18n/ja.po
Normal file
5345
i18n/ja.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/ka.po
Normal file
4761
i18n/ka.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/kab.po
Normal file
4761
i18n/kab.po
Normal file
File diff suppressed because it is too large
Load Diff
4765
i18n/km.po
Normal file
4765
i18n/km.po
Normal file
File diff suppressed because it is too large
Load Diff
5368
i18n/ko.po
Normal file
5368
i18n/ko.po
Normal file
File diff suppressed because it is too large
Load Diff
4762
i18n/lb.po
Normal file
4762
i18n/lb.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/lo.po
Normal file
4761
i18n/lo.po
Normal file
File diff suppressed because it is too large
Load Diff
5282
i18n/lt.po
Normal file
5282
i18n/lt.po
Normal file
File diff suppressed because it is too large
Load Diff
5251
i18n/lv.po
Normal file
5251
i18n/lv.po
Normal file
File diff suppressed because it is too large
Load Diff
5227
i18n/mass_mailing.pot
Normal file
5227
i18n/mass_mailing.pot
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/mk.po
Normal file
4761
i18n/mk.po
Normal file
File diff suppressed because it is too large
Load Diff
4783
i18n/mn.po
Normal file
4783
i18n/mn.po
Normal file
File diff suppressed because it is too large
Load Diff
4772
i18n/nb.po
Normal file
4772
i18n/nb.po
Normal file
File diff suppressed because it is too large
Load Diff
4758
i18n/ne.po
Normal file
4758
i18n/ne.po
Normal file
File diff suppressed because it is too large
Load Diff
5520
i18n/nl.po
Normal file
5520
i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
5476
i18n/pl.po
Normal file
5476
i18n/pl.po
Normal file
File diff suppressed because it is too large
Load Diff
5239
i18n/pt.po
Normal file
5239
i18n/pt.po
Normal file
File diff suppressed because it is too large
Load Diff
5517
i18n/pt_BR.po
Normal file
5517
i18n/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
4811
i18n/ro.po
Normal file
4811
i18n/ro.po
Normal file
File diff suppressed because it is too large
Load Diff
5525
i18n/ru.po
Normal file
5525
i18n/ru.po
Normal file
File diff suppressed because it is too large
Load Diff
5288
i18n/sk.po
Normal file
5288
i18n/sk.po
Normal file
File diff suppressed because it is too large
Load Diff
5295
i18n/sl.po
Normal file
5295
i18n/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
4761
i18n/sq.po
Normal file
4761
i18n/sq.po
Normal file
File diff suppressed because it is too large
Load Diff
5422
i18n/sr.po
Normal file
5422
i18n/sr.po
Normal file
File diff suppressed because it is too large
Load Diff
4764
i18n/sr@latin.po
Normal file
4764
i18n/sr@latin.po
Normal file
File diff suppressed because it is too large
Load Diff
5304
i18n/sv.po
Normal file
5304
i18n/sv.po
Normal file
File diff suppressed because it is too large
Load Diff
5458
i18n/th.po
Normal file
5458
i18n/th.po
Normal file
File diff suppressed because it is too large
Load Diff
5459
i18n/tr.po
Normal file
5459
i18n/tr.po
Normal file
File diff suppressed because it is too large
Load Diff
5482
i18n/uk.po
Normal file
5482
i18n/uk.po
Normal file
File diff suppressed because it is too large
Load Diff
5470
i18n/vi.po
Normal file
5470
i18n/vi.po
Normal file
File diff suppressed because it is too large
Load Diff
5327
i18n/zh_CN.po
Normal file
5327
i18n/zh_CN.po
Normal file
File diff suppressed because it is too large
Load Diff
5326
i18n/zh_TW.po
Normal file
5326
i18n/zh_TW.po
Normal file
File diff suppressed because it is too large
Load Diff
25
models/__init__.py
Normal file
25
models/__init__.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import ir_http
|
||||||
|
from . import ir_mail_server
|
||||||
|
from . import ir_model
|
||||||
|
from . import link_tracker
|
||||||
|
from . import mail_blacklist
|
||||||
|
from . import mailing_subscription # keep before due to decorated m2m
|
||||||
|
from . import mailing_contact
|
||||||
|
from . import mailing_list
|
||||||
|
from . import mailing_subscription_optout
|
||||||
|
from . import mailing_trace
|
||||||
|
from . import mailing
|
||||||
|
from . import mailing_filter
|
||||||
|
from . import mail_mail
|
||||||
|
from . import mail_render_mixin
|
||||||
|
from . import mail_thread
|
||||||
|
from . import res_company
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import res_partner
|
||||||
|
from . import res_users
|
||||||
|
from . import utm_campaign
|
||||||
|
from . import utm_medium
|
||||||
|
from . import utm_source
|
12
models/ir_http.py
Normal file
12
models/ir_http.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import models
|
||||||
|
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
_inherit = "ir.http"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_translation_frontend_modules_name(cls):
|
||||||
|
mods = super()._get_translation_frontend_modules_name()
|
||||||
|
return mods + ["mass_mailing"]
|
36
models/ir_mail_server.py
Normal file
36
models/ir_mail_server.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
from odoo.tools.misc import format_date
|
||||||
|
|
||||||
|
|
||||||
|
class IrMailServer(models.Model):
|
||||||
|
_name = 'ir.mail_server'
|
||||||
|
_inherit = ['ir.mail_server']
|
||||||
|
|
||||||
|
active_mailing_ids = fields.One2many(
|
||||||
|
comodel_name='mailing.mailing',
|
||||||
|
inverse_name='mail_server_id',
|
||||||
|
string='Active mailing using this mail server',
|
||||||
|
readonly=True,
|
||||||
|
domain=[('state', '!=', 'done'), ('active', '=', True)])
|
||||||
|
|
||||||
|
def _active_usages_compute(self):
|
||||||
|
def format_usage(mailing_id):
|
||||||
|
base = _('Mass Mailing "%s"', mailing_id.display_name)
|
||||||
|
if not mailing_id.schedule_date:
|
||||||
|
return base
|
||||||
|
details = _('(scheduled for %s)', format_date(self.env, mailing_id.schedule_date))
|
||||||
|
return f'{base} {details}'
|
||||||
|
|
||||||
|
usages_super = super(IrMailServer, self)._active_usages_compute()
|
||||||
|
default_mail_server_id = self.env['mailing.mailing']._get_default_mail_server_id()
|
||||||
|
for record in self:
|
||||||
|
usages = []
|
||||||
|
if default_mail_server_id == record.id:
|
||||||
|
usages.append(_('Email Marketing uses it as its default mail server to send mass mailings'))
|
||||||
|
usages.extend(map(format_usage, record.active_mailing_ids))
|
||||||
|
if usages:
|
||||||
|
usages_super.setdefault(record.id, []).extend(usages)
|
||||||
|
return usages_super
|
34
models/ir_model.py
Normal file
34
models/ir_model.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models, _
|
||||||
|
|
||||||
|
|
||||||
|
class IrModel(models.Model):
|
||||||
|
_inherit = 'ir.model'
|
||||||
|
|
||||||
|
is_mailing_enabled = fields.Boolean(
|
||||||
|
string="Mailing Enabled",
|
||||||
|
compute='_compute_is_mailing_enabled', search='_search_is_mailing_enabled',
|
||||||
|
help="Whether this model supports marketing mailing capabilities (notably email and SMS).",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_is_mailing_enabled(self):
|
||||||
|
for model in self:
|
||||||
|
model.is_mailing_enabled = getattr(self.env[model.model], '_mailing_enabled', False)
|
||||||
|
|
||||||
|
def _search_is_mailing_enabled(self, operator, value):
|
||||||
|
if operator not in ('=', '!='):
|
||||||
|
raise ValueError(_("Searching Mailing Enabled models supports only direct search using '='' or '!='."))
|
||||||
|
|
||||||
|
valid_models = self.env['ir.model']
|
||||||
|
for model in self.search([]):
|
||||||
|
if model.model not in self.env or model.is_transient():
|
||||||
|
continue
|
||||||
|
if getattr(self.env[model.model], '_mailing_enabled', False):
|
||||||
|
valid_models |= model
|
||||||
|
|
||||||
|
search_is_mailing_enabled = (operator == '=' and value) or (operator == '!=' and not value)
|
||||||
|
if search_is_mailing_enabled:
|
||||||
|
return [('id', 'in', valid_models.ids)]
|
||||||
|
return [('id', 'not in', valid_models.ids)]
|
42
models/link_tracker.py
Normal file
42
models/link_tracker.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class LinkTracker(models.Model):
|
||||||
|
_inherit = "link.tracker"
|
||||||
|
|
||||||
|
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
|
||||||
|
|
||||||
|
|
||||||
|
class LinkTrackerClick(models.Model):
|
||||||
|
_inherit = "link.tracker.click"
|
||||||
|
|
||||||
|
mailing_trace_id = fields.Many2one('mailing.trace', string='Mail Statistics')
|
||||||
|
mass_mailing_id = fields.Many2one('mailing.mailing', string='Mass Mailing')
|
||||||
|
|
||||||
|
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('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('campaign_id'):
|
||||||
|
click_values['campaign_id'] = trace_sudo.campaign_id.id
|
||||||
|
if not click_values.get('mass_mailing_id'):
|
||||||
|
click_values['mass_mailing_id'] = trace_sudo.mass_mailing_id.id
|
||||||
|
|
||||||
|
return click_values
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def add_click(self, code, **route_values):
|
||||||
|
click = super(LinkTrackerClick, self).add_click(code, **route_values)
|
||||||
|
|
||||||
|
if click and click.mailing_trace_id:
|
||||||
|
click.mailing_trace_id.set_opened()
|
||||||
|
click.mailing_trace_id.set_clicked()
|
||||||
|
|
||||||
|
return click
|
20
models/mail_blacklist.py
Normal file
20
models/mail_blacklist.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class MailBlackList(models.Model):
|
||||||
|
""" Model of blacklisted email addresses to stop sending emails."""
|
||||||
|
_inherit = ['mail.blacklist']
|
||||||
|
|
||||||
|
opt_out_reason_id = fields.Many2one(
|
||||||
|
'mailing.subscription.optout', string='Reason',
|
||||||
|
ondelete='restrict',
|
||||||
|
tracking=10)
|
||||||
|
|
||||||
|
def _track_subtype(self, init_values):
|
||||||
|
self.ensure_one()
|
||||||
|
if 'opt_out_reason_id' in init_values and self.opt_out_reason_id:
|
||||||
|
return self.env.ref('mail.mt_comment')
|
||||||
|
return super()._track_subtype(init_values)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user