mirror of
https://github.com/bringout/oca-ocb-mail.git
synced 2026-04-18 16:42:05 +02:00
Initial commit: Mail packages
This commit is contained in:
commit
4e53507711
1948 changed files with 751201 additions and 0 deletions
|
|
@ -0,0 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_mailing_ab_testing
|
||||
from . import test_mailing_internals
|
||||
from . import test_mailing_list
|
||||
from . import test_mailing_controllers
|
||||
from . import test_mailing_mailing_schedule_date
|
||||
from . import test_mailing_ui
|
||||
from . import test_utm
|
||||
from . import test_mailing_retry
|
||||
347
odoo-bringout-oca-ocb-mass_mailing/mass_mailing/tests/common.py
Normal file
347
odoo-bringout-oca-ocb-mass_mailing/mass_mailing/tests/common.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
import werkzeug
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo import tools
|
||||
from odoo.addons.link_tracker.tests.common import MockLinkTracker
|
||||
from odoo.addons.mail.tests.common import MailCase, MailCommon, mail_new_test_user
|
||||
from odoo.sql_db import Cursor
|
||||
|
||||
|
||||
class MassMailCase(MailCase, MockLinkTracker):
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# ASSERTS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def assertMailingStatistics(self, mailing, **kwargs):
|
||||
""" Helper to assert mailing statistics fields. As we have many of them
|
||||
it helps lessening test asserts. """
|
||||
if not kwargs.get('expected'):
|
||||
kwargs['expected'] = len(mailing.mailing_trace_ids)
|
||||
if not kwargs.get('delivered'):
|
||||
kwargs['delivered'] = len(mailing.mailing_trace_ids)
|
||||
for fname in ['scheduled', 'expected', 'sent', 'delivered',
|
||||
'opened', 'replied', 'clicked',
|
||||
'canceled', 'failed', 'bounced']:
|
||||
self.assertEqual(
|
||||
mailing[fname], kwargs.get(fname, 0),
|
||||
'Mailing %s statistics failed: got %s instead of %s' % (fname, mailing[fname], kwargs.get(fname, 0))
|
||||
)
|
||||
|
||||
def assertMailTraces(self, recipients_info, mailing, records,
|
||||
check_mail=True, sent_unlink=False,
|
||||
author=None, mail_links_info=None):
|
||||
""" Check content of traces. Traces are fetched based on a given mailing
|
||||
and records. Their content is compared to recipients_info structure that
|
||||
holds expected information. Links content may be checked, notably to
|
||||
assert shortening or unsubscribe links. Mail.mail records may optionally
|
||||
be checked.
|
||||
|
||||
:param recipients_info: list[{
|
||||
# TRACE
|
||||
'partner': res.partner record (may be empty),
|
||||
'email': email used when sending email (may be empty, computed based on partner),
|
||||
'trace_status': outgoing / sent / open / reply / bounce / error / cancel (sent by default),
|
||||
'record: linked record,
|
||||
# MAIL.MAIL
|
||||
'content': optional content that should be present in mail.mail body_html;
|
||||
'email_to_mail': optional email used for the mail, when different from the
|
||||
one stored on the trace itself;
|
||||
'email_to_recipients': optional, see '_assertMailMail';
|
||||
'failure_type': optional failure reason;
|
||||
}, { ... }]
|
||||
|
||||
:param mailing: a mailing.mailing record from which traces have been
|
||||
generated;
|
||||
:param records: records given to mailing that generated traces. It is
|
||||
used notably to find traces using their IDs;
|
||||
:param check_mail: if True, also check mail.mail records that should be
|
||||
linked to traces;
|
||||
:param sent_unlink: it True, sent mail.mail are deleted and we check gateway
|
||||
output result instead of actual mail.mail records;
|
||||
:param mail_links_info: if given, should follow order of ``recipients_info``
|
||||
and give details about links. See ``assertLinkShortenedHtml`` helper for
|
||||
more details about content to give;
|
||||
:param author: author of sent mail.mail;
|
||||
"""
|
||||
# map trace state to email state
|
||||
state_mapping = {
|
||||
'sent': 'sent',
|
||||
'open': 'sent', # opened implies something has been sent
|
||||
'reply': 'sent', # replied implies something has been sent
|
||||
'error': 'exception',
|
||||
'cancel': 'cancel',
|
||||
'bounce': 'cancel',
|
||||
}
|
||||
|
||||
traces = self.env['mailing.trace'].search([
|
||||
('mass_mailing_id', 'in', mailing.ids),
|
||||
('res_id', 'in', records.ids)
|
||||
])
|
||||
debug_info = '\n'.join(
|
||||
(
|
||||
f'Trace: to {t.email} - state {t.trace_status} - res_id {t.res_id}'
|
||||
for t in traces
|
||||
)
|
||||
)
|
||||
|
||||
# ensure trace coherency
|
||||
self.assertTrue(all(s.model == records._name for s in traces))
|
||||
self.assertEqual(set(s.res_id for s in traces), set(records.ids))
|
||||
|
||||
# check each traces
|
||||
if not mail_links_info:
|
||||
mail_links_info = [None] * len(recipients_info)
|
||||
for recipient_info, link_info, record in zip(recipients_info, mail_links_info, records):
|
||||
partner = recipient_info.get('partner', self.env['res.partner'])
|
||||
email = recipient_info.get('email')
|
||||
email_to_mail = recipient_info.get('email_to_mail') or email
|
||||
email_to_recipients = recipient_info.get('email_to_recipients')
|
||||
status = recipient_info.get('trace_status', 'sent')
|
||||
record = record or recipient_info.get('record')
|
||||
content = recipient_info.get('content')
|
||||
if email is None and partner:
|
||||
email = partner.email_normalized
|
||||
|
||||
recipient_trace = traces.filtered(
|
||||
lambda t: (t.email == email or (not email and not t.email)) and \
|
||||
t.trace_status == status and \
|
||||
(t.res_id == record.id if record else True)
|
||||
)
|
||||
self.assertTrue(
|
||||
len(recipient_trace) == 1,
|
||||
'MailTrace: email %s (recipient %s, status: %s, record: %s): found %s records (1 expected)\n%s' % (
|
||||
email, partner, status, record,
|
||||
len(recipient_trace), debug_info)
|
||||
)
|
||||
self.assertTrue(bool(recipient_trace.mail_mail_id_int))
|
||||
if 'failure_type' in recipient_info or status in ('error', 'cancel', 'bounce'):
|
||||
self.assertEqual(recipient_trace.failure_type, recipient_info['failure_type'])
|
||||
|
||||
if check_mail:
|
||||
if author is None:
|
||||
author = self.env.user.partner_id
|
||||
|
||||
# mail.mail specific values to check
|
||||
fields_values = {'mailing_id': mailing}
|
||||
if recipient_info.get('mail_values'):
|
||||
fields_values.update(recipient_info['mail_values'])
|
||||
if 'failure_reason' in recipient_info:
|
||||
fields_values['failure_reason'] = recipient_info['failure_reason']
|
||||
if 'email_to_mail' in recipient_info:
|
||||
fields_values['email_to'] = recipient_info['email_to_mail']
|
||||
|
||||
# specific for partner: email_formatted is used
|
||||
if partner:
|
||||
if status == 'sent' and sent_unlink:
|
||||
self.assertSentEmail(author, [partner])
|
||||
else:
|
||||
self.assertMailMail(
|
||||
partner, state_mapping[status],
|
||||
author=author,
|
||||
content=content,
|
||||
email_to_recipients=email_to_recipients,
|
||||
fields_values=fields_values,
|
||||
)
|
||||
# specific if email is False -> could have troubles finding it if several falsy traces
|
||||
elif not email and status in ('cancel', 'bounce'):
|
||||
self.assertMailMailWId(
|
||||
recipient_trace.mail_mail_id_int, state_mapping[status],
|
||||
author=author,
|
||||
content=content,
|
||||
email_to_recipients=email_to_recipients,
|
||||
fields_values=fields_values,
|
||||
)
|
||||
else:
|
||||
self.assertMailMailWEmails(
|
||||
[email_to_mail], state_mapping[status],
|
||||
author=author,
|
||||
content=content,
|
||||
email_to_recipients=email_to_recipients,
|
||||
fields_values=fields_values,
|
||||
)
|
||||
|
||||
if link_info:
|
||||
trace_mail = self._find_mail_mail_wrecord(record)
|
||||
for (anchor_id, url, is_shortened, add_link_params) in link_info:
|
||||
link_params = {'utm_medium': 'Email', 'utm_source': mailing.name}
|
||||
if add_link_params:
|
||||
link_params.update(**add_link_params)
|
||||
self.assertLinkShortenedHtml(
|
||||
trace_mail.body_html,
|
||||
(anchor_id, url, is_shortened),
|
||||
link_params=link_params,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# TOOLS
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def gateway_mail_bounce(self, mailing, record, bounce_base_values=None):
|
||||
""" Generate a bounce at mailgateway level.
|
||||
|
||||
:param mailing: a ``mailing.mailing`` record on which we find a trace
|
||||
to bounce;
|
||||
:param record: record which should bounce;
|
||||
:param bounce_base_values: optional values given to routing;
|
||||
"""
|
||||
trace = mailing.mailing_trace_ids.filtered(
|
||||
lambda t: t.model == record._name and t.res_id == record.id
|
||||
)
|
||||
|
||||
parsed_bounce_values = {
|
||||
'email_from': 'some.email@external.example.com', # TDE check: email_from -> trace email ?
|
||||
'to': 'bounce@test.example.com', # TDE check: bounce alias ?
|
||||
'message_id': tools.generate_tracking_message_id('MailTest'),
|
||||
'bounced_partner': self.env['res.partner'].sudo(),
|
||||
'bounced_message': self.env['mail.message'].sudo()
|
||||
}
|
||||
if bounce_base_values:
|
||||
parsed_bounce_values.update(bounce_base_values)
|
||||
parsed_bounce_values.update({
|
||||
'bounced_email': trace.email,
|
||||
'bounced_msg_id': [trace.message_id],
|
||||
})
|
||||
self.env['mail.thread']._routing_handle_bounce(False, parsed_bounce_values)
|
||||
return trace
|
||||
|
||||
def gateway_mail_click(self, mailing, record, click_label):
|
||||
""" Simulate a click on a sent email.
|
||||
|
||||
:param mailing: a ``mailing.mailing`` record on which we find a trace
|
||||
to click;
|
||||
:param record: record which should click;
|
||||
:param click_label: label of link on which we should click;
|
||||
"""
|
||||
trace = mailing.mailing_trace_ids.filtered(
|
||||
lambda t: t.model == record._name and t.res_id == record.id
|
||||
)
|
||||
|
||||
email = self._find_sent_mail_wemail(trace.email)
|
||||
self.assertTrue(bool(email))
|
||||
for (_url_href, link_url, _dummy, label) in re.findall(tools.HTML_TAG_URL_REGEX, email['body']):
|
||||
if label == click_label and '/r/' in link_url: # shortened link, like 'http://localhost:8069/r/LBG/m/53'
|
||||
parsed_url = werkzeug.urls.url_parse(link_url)
|
||||
path_items = parsed_url.path.split('/')
|
||||
code, trace_id = path_items[2], int(path_items[4])
|
||||
self.assertEqual(trace.id, trace_id)
|
||||
|
||||
self.env['link.tracker.click'].sudo().add_click(
|
||||
code,
|
||||
ip='100.200.300.%3f' % random.random(),
|
||||
country_code='BE',
|
||||
mailing_trace_id=trace.id
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise AssertionError('url %s not found in mailing %s for record %s' % (click_label, mailing, record))
|
||||
return trace
|
||||
|
||||
def gateway_mail_open(self, mailing, record):
|
||||
""" Simulate opening an email through blank.gif icon access. As we
|
||||
don't want to use the whole Http layer just for that we will just
|
||||
call 'set_opened()' on trace, until having a better option.
|
||||
|
||||
:param mailing: a ``mailing.mailing`` record on which we find a trace
|
||||
to open;
|
||||
:param record: record which should open;
|
||||
"""
|
||||
trace = mailing.mailing_trace_ids.filtered(
|
||||
lambda t: t.model == record._name and t.res_id == record.id
|
||||
)
|
||||
trace.set_opened()
|
||||
return trace
|
||||
|
||||
@classmethod
|
||||
def _create_bounce_trace(cls, mailing, records, dt=None):
|
||||
if dt is None:
|
||||
dt = datetime.datetime.now() - datetime.timedelta(days=1)
|
||||
return cls._create_traces(mailing, records, dt, trace_status='bounce')
|
||||
|
||||
@classmethod
|
||||
def _create_sent_traces(cls, mailing, records, dt=None):
|
||||
if dt is None:
|
||||
dt = datetime.datetime.now() - datetime.timedelta(days=1)
|
||||
return cls._create_traces(mailing, records, dt, trace_status='sent')
|
||||
|
||||
@classmethod
|
||||
def _create_traces(cls, mailing, records, dt, **values):
|
||||
if 'email_normalized' in records:
|
||||
fname = 'email_normalized'
|
||||
elif 'email_from' in records:
|
||||
fname = 'email_from'
|
||||
else:
|
||||
fname = 'email'
|
||||
randomized = random.random()
|
||||
# Cursor.now() uses transaction's timestamp and not datetime lib -> freeze_time
|
||||
# is not sufficient
|
||||
with patch.object(Cursor, 'now', lambda *args, **kwargs: dt):
|
||||
traces = cls.env['mailing.trace'].sudo().create([
|
||||
dict({'mass_mailing_id': mailing.id,
|
||||
'model': record._name,
|
||||
'res_id': record.id,
|
||||
'trace_status': values.get('trace_status', 'bounce'),
|
||||
# TDE FIXME: improve this with a mail-enabled heuristics
|
||||
'email': record[fname],
|
||||
'message_id': '<%5f@gilbert.boitempomils>' % randomized,
|
||||
}, **values)
|
||||
for record in records
|
||||
])
|
||||
return traces
|
||||
|
||||
@classmethod
|
||||
def _create_mailing_list(cls):
|
||||
""" Shortcut to create mailing lists. Currently hardcoded, maybe evolve
|
||||
in a near future. """
|
||||
cls.mailing_list_1 = cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'List1',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Déboulonneur', 'email': 'fleurus@example.com'}),
|
||||
(0, 0, {'name': 'Gorramts', 'email': 'gorramts@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
]
|
||||
})
|
||||
cls.mailing_list_2 = cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'List2',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Gilberte', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Gilberte En Mieux', 'email': 'gilberte@example.com'}),
|
||||
(0, 0, {'name': 'Norbert', 'email': 'norbert@example.com'}),
|
||||
(0, 0, {'name': 'Ybrant', 'email': 'ybrant@example.com'}),
|
||||
]
|
||||
})
|
||||
|
||||
@classmethod
|
||||
def _create_mailing_list_of_x_contacts(cls, contacts_nbr):
|
||||
""" Shortcut to create a mailing list that contains a defined number
|
||||
of contacts. """
|
||||
return cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'Test List',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Contact %s' % i, 'email': 'contact%s@example.com' % i})
|
||||
for i in range(contacts_nbr)
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
class MassMailCommon(MailCommon, MassMailCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(MassMailCommon, cls).setUpClass()
|
||||
|
||||
cls.user_marketing = mail_new_test_user(
|
||||
cls.env, login='user_marketing',
|
||||
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
|
||||
name='Martial Marketing', signature='--\nMartial')
|
||||
|
||||
cls.email_reply_to = 'MyCompany SomehowAlias <test.alias@test.mycompany.com>'
|
||||
|
||||
cls.env.flush_all()
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.tests import users, tagged
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tests.common import Form
|
||||
from odoo import fields
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestMailingABTestingCommon(MassMailCommon):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.mailing_list = self._create_mailing_list_of_x_contacts(150)
|
||||
self.ab_testing_mailing_1 = self.env['mailing.mailing'].create({
|
||||
'subject': 'A/B Testing V1',
|
||||
'contact_list_ids': self.mailing_list.ids,
|
||||
'ab_testing_enabled': True,
|
||||
'ab_testing_pc': 10,
|
||||
'ab_testing_schedule_datetime': datetime.now(),
|
||||
})
|
||||
self.ab_testing_mailing_2 = self.ab_testing_mailing_1.copy({
|
||||
'subject': 'A/B Testing V2',
|
||||
'ab_testing_pc': 20,
|
||||
})
|
||||
self.ab_testing_campaign = self.ab_testing_mailing_1.campaign_id
|
||||
self.ab_testing_mailing_ids = self.ab_testing_mailing_1 + self.ab_testing_mailing_2
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
|
||||
class TestMailingABTesting(TestMailingABTestingCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_auto_flow(self):
|
||||
with self.mock_mail_gateway():
|
||||
self.ab_testing_mailing_ids.action_send_mail()
|
||||
self.assertEqual(self.ab_testing_mailing_1.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_2.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 0)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 0)
|
||||
|
||||
total_trace_ids = self.ab_testing_mailing_ids.mailing_trace_ids
|
||||
unique_recipients_used = set(map(lambda mail: mail.res_id, total_trace_ids.mail_mail_id))
|
||||
self.assertEqual(len(self.ab_testing_mailing_1.mailing_trace_ids), 15)
|
||||
self.assertEqual(len(self.ab_testing_mailing_2.mailing_trace_ids), 30)
|
||||
self.assertEqual(len(unique_recipients_used), 45)
|
||||
|
||||
self.ab_testing_mailing_1.mailing_trace_ids[:10].set_opened()
|
||||
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.ab_testing_mailing_2.action_send_winner_mailing()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
winner_mailing = self.ab_testing_campaign.mailing_mail_ids.filtered(lambda mailing: mailing.ab_testing_pc == 100)
|
||||
self.assertEqual(winner_mailing.subject, 'A/B Testing V1')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_auto_flow_cron(self):
|
||||
self.ab_testing_mailing_1.write({
|
||||
'ab_testing_schedule_datetime': datetime.now() + timedelta(days=-1),
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
self.ab_testing_mailing_ids.action_send_mail()
|
||||
self.assertEqual(self.ab_testing_mailing_1.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_2.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 0)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 0)
|
||||
|
||||
total_trace_ids = self.ab_testing_mailing_ids.mailing_trace_ids
|
||||
unique_recipients_used = set(map(lambda mail: mail.res_id, total_trace_ids.mail_mail_id))
|
||||
self.assertEqual(len(self.ab_testing_mailing_1.mailing_trace_ids), 15)
|
||||
self.assertEqual(len(self.ab_testing_mailing_2.mailing_trace_ids), 30)
|
||||
self.assertEqual(len(unique_recipients_used), 45)
|
||||
|
||||
self.ab_testing_mailing_1.mailing_trace_ids[:10].set_opened()
|
||||
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.env.ref('mass_mailing.ir_cron_mass_mailing_ab_testing').sudo().method_direct_trigger()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
winner_mailing = self.ab_testing_campaign.mailing_mail_ids.filtered(lambda mailing: mailing.ab_testing_pc == 100)
|
||||
self.assertEqual(winner_mailing.subject, 'A/B Testing V1')
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_campaign(self):
|
||||
schedule_datetime = datetime.now() + timedelta(days=30)
|
||||
ab_mailing = self.env['mailing.mailing'].create({
|
||||
'subject': 'A/B Testing V1',
|
||||
'contact_list_ids': self.mailing_list.ids,
|
||||
'ab_testing_enabled': True,
|
||||
'ab_testing_winner_selection': 'manual',
|
||||
'ab_testing_schedule_datetime': schedule_datetime,
|
||||
})
|
||||
ab_mailing.invalidate_recordset()
|
||||
|
||||
# Check if the campaign is correctly created and the values set on the mailing are still the same
|
||||
self.assertTrue(ab_mailing.campaign_id, "A campaign id is present for the A/B test mailing")
|
||||
self.assertEqual(ab_mailing.ab_testing_winner_selection, 'manual', "The selection winner has been propagated correctly")
|
||||
self.assertEqual(ab_mailing.ab_testing_schedule_datetime, schedule_datetime, "The schedule date has been propagated correctly")
|
||||
|
||||
# Check that while enabling the A/B testing, if campaign is already set, new one should not be created
|
||||
created_mailing_campaign = ab_mailing.campaign_id
|
||||
ab_mailing.ab_testing_enabled = False
|
||||
ab_mailing.ab_testing_enabled = True
|
||||
self.assertEqual(ab_mailing.campaign_id, created_mailing_campaign, "No new campaign should have been created")
|
||||
|
||||
# Check that while enabling the A/B testing, if user manually selects a campaign, it should be saved
|
||||
# rather than being replaced with the automatically created one
|
||||
ab_mailing.write({'ab_testing_enabled': False, 'campaign_id': False})
|
||||
ab_mailing.write({'ab_testing_enabled': True, 'campaign_id': created_mailing_campaign})
|
||||
self.assertEqual(ab_mailing.campaign_id, created_mailing_campaign, "No new campaign should have been created")
|
||||
|
||||
ab_mailing_2 = self.env['mailing.mailing'].create({
|
||||
'subject': 'A/B Testing V2',
|
||||
'contact_list_ids': self.mailing_list.ids,
|
||||
})
|
||||
ab_mailing_2.invalidate_recordset()
|
||||
|
||||
ab_mailing_2.ab_testing_enabled = True
|
||||
# Check if the campaign is correctly created with default values
|
||||
self.assertTrue(ab_mailing.campaign_id, "A campaign id is present for the A/B test mailing")
|
||||
self.assertTrue(ab_mailing.ab_testing_winner_selection, "The selection winner has been set to default value")
|
||||
self.assertTrue(ab_mailing.ab_testing_schedule_datetime, "The schedule date has been set to default value")
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_compare(self):
|
||||
# compare version feature should returns all mailings of the same
|
||||
# campaign having a/b testing enabled.
|
||||
compare_version = self.ab_testing_mailing_1.action_compare_versions()
|
||||
self.assertEqual(
|
||||
self.env['mailing.mailing'].search(compare_version.get('domain')),
|
||||
self.ab_testing_mailing_1 + self.ab_testing_mailing_2
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_manual_flow(self):
|
||||
self.ab_testing_mailing_1.write({
|
||||
'ab_testing_winner_selection': 'manual',
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
self.ab_testing_mailing_ids.action_send_mail()
|
||||
self.assertEqual(self.ab_testing_mailing_1.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_2.state, 'done')
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 0)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 0)
|
||||
|
||||
total_trace_ids = self.ab_testing_mailing_ids.mailing_trace_ids
|
||||
unique_recipients_used = set(map(lambda mail: mail.res_id, total_trace_ids.mail_mail_id))
|
||||
self.assertEqual(len(self.ab_testing_mailing_1.mailing_trace_ids), 15)
|
||||
self.assertEqual(len(self.ab_testing_mailing_2.mailing_trace_ids), 30)
|
||||
self.assertEqual(len(unique_recipients_used), 45)
|
||||
|
||||
self.ab_testing_mailing_1.mailing_trace_ids[:10].set_opened()
|
||||
self.ab_testing_mailing_2.mailing_trace_ids[:15].set_opened()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
|
||||
self.assertEqual(self.ab_testing_mailing_1.opened_ratio, 66)
|
||||
self.assertEqual(self.ab_testing_mailing_2.opened_ratio, 50)
|
||||
|
||||
with self.mock_mail_gateway():
|
||||
self.ab_testing_mailing_2.action_send_winner_mailing()
|
||||
self.ab_testing_mailing_ids.invalidate_recordset()
|
||||
winner_mailing = self.ab_testing_campaign.mailing_mail_ids.filtered(lambda mailing: mailing.ab_testing_pc == 100)
|
||||
self.assertEqual(winner_mailing.subject, 'A/B Testing V2')
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_ab_testing_minimum_participants(self):
|
||||
""" Test that it should send minimum one mail(if possible) when ab_testing_pc is too small compared to the amount of targeted records."""
|
||||
mailing_list = self._create_mailing_list_of_x_contacts(10)
|
||||
ab_testing = self.env['mailing.mailing'].create({
|
||||
'subject': 'A/B Testing SMS V1',
|
||||
'contact_list_ids': mailing_list.ids,
|
||||
'ab_testing_enabled': True,
|
||||
'ab_testing_pc': 2,
|
||||
'ab_testing_schedule_datetime': datetime.now(),
|
||||
'mailing_type': 'mail',
|
||||
'campaign_id': self.ab_testing_campaign.id,
|
||||
})
|
||||
with self.mock_mail_gateway():
|
||||
ab_testing.action_send_mail()
|
||||
self.assertEqual(ab_testing.state, 'done')
|
||||
self.assertEqual(len(self._mails), 1)
|
||||
|
||||
def test_mailing_ab_testing_duplicate_date(self):
|
||||
""" Test that "Send final on" date value should be copied in new mass_mailing """
|
||||
ab_testing_mail_1 = Form(self.ab_testing_mailing_1)
|
||||
ab_testing_mail_1.ab_testing_schedule_datetime = datetime.now() + timedelta(days=10)
|
||||
action = ab_testing_mail_1.save().action_duplicate()
|
||||
ab_testing_mailing_2 = self.env[action['res_model']].browse(action['res_id'])
|
||||
self.assertEqual(fields.Datetime.to_string(ab_testing_mailing_2.ab_testing_schedule_datetime), ab_testing_mail_1.ab_testing_schedule_datetime)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from freezegun import freeze_time
|
||||
|
||||
import datetime
|
||||
import werkzeug
|
||||
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.tests.common import HttpCase
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('link_tracker')
|
||||
class TestMailingControllers(MassMailCommon, HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingControllers, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
cls.test_mailing = cls.env['mailing.mailing'].create({
|
||||
'body_html': '<p>Hello <t t-out="object.name"/><br />Go to <a id="url" href="https://www.example.com/foo/bar?baz=qux">this link</a></p>',
|
||||
'contact_list_ids': [(4, cls.mailing_list_1.id), (4, cls.mailing_list_2.id)],
|
||||
'mailing_model_id': cls.env['ir.model']._get('mailing.list').id,
|
||||
'mailing_type': 'mail',
|
||||
'name': 'TestMailing',
|
||||
'reply_to': cls.email_reply_to,
|
||||
'subject': 'Test',
|
||||
})
|
||||
|
||||
cls.test_contact = cls.mailing_list_1.contact_ids[0]
|
||||
|
||||
# freeze time base value
|
||||
cls._reference_now = datetime.datetime(2022, 6, 14, 10, 0, 0)
|
||||
|
||||
def test_tracking_short_code(self):
|
||||
""" Test opening short code linked to a mailing trace: should set the
|
||||
trace as opened and clicked, create a click record. """
|
||||
mailing = self.test_mailing.with_env(self.env)
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
mail = self._find_mail_mail_wrecord(self.test_contact)
|
||||
mailing_trace = mail.mailing_trace_ids
|
||||
link_tracker_code = self._get_code_from_short_url(
|
||||
self._get_href_from_anchor_id(mail.body, 'url')
|
||||
)
|
||||
self.assertEqual(len(link_tracker_code), 1)
|
||||
self.assertEqual(link_tracker_code.link_id.count, 0)
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertEqual(len(mailing_trace), 1)
|
||||
self.assertFalse(mailing_trace.links_click_datetime)
|
||||
self.assertFalse(mailing_trace.open_datetime)
|
||||
self.assertEqual(mailing_trace.trace_status, 'sent')
|
||||
|
||||
short_link_url = werkzeug.urls.url_join(
|
||||
mail.get_base_url(),
|
||||
f'r/{link_tracker_code.code}/m/{mailing_trace.id}'
|
||||
)
|
||||
with freeze_time(self._reference_now):
|
||||
response = self.url_open(short_link_url, allow_redirects=False)
|
||||
self.assertEqual(response.headers['Location'], 'https://www.example.com/foo/bar?baz=qux&utm_source=TestMailing&utm_medium=Email')
|
||||
|
||||
self.assertEqual(link_tracker_code.link_id.count, 1)
|
||||
self.assertEqual(mailing_trace.links_click_datetime, self._reference_now)
|
||||
self.assertEqual(mailing_trace.open_datetime, self._reference_now)
|
||||
self.assertEqual(mailing_trace.trace_status, 'open')
|
||||
|
||||
def test_tracking_url_token(self):
|
||||
""" Test tracking of mails linked to a mailing trace: should set the
|
||||
trace as opened. """
|
||||
mailing = self.test_mailing.with_env(self.env)
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
mail = self._find_mail_mail_wrecord(self.test_contact)
|
||||
mailing_trace = mail.mailing_trace_ids
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertEqual(len(mailing_trace), 1)
|
||||
self.assertFalse(mailing_trace.open_datetime)
|
||||
self.assertEqual(mailing_trace.trace_status, 'sent')
|
||||
|
||||
with freeze_time(self._reference_now):
|
||||
response = self.url_open(mail._get_tracking_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(mail.state, 'sent')
|
||||
self.assertEqual(mailing_trace.open_datetime, self._reference_now)
|
||||
self.assertEqual(mailing_trace.trace_status, 'open')
|
||||
|
||||
track_url = werkzeug.urls.url_join(
|
||||
mail.get_base_url(),
|
||||
'mail/track/%s/fake_token/blank.gif' % mail.id
|
||||
)
|
||||
response = self.url_open(track_url)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
|
@ -0,0 +1,856 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
import re
|
||||
from ast import literal_eval
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
from psycopg2 import IntegrityError
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.sql_db import Cursor
|
||||
from odoo.tests.common import users, Form, HttpCase, tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
BASE_64_STRING = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
||||
|
||||
|
||||
@tagged("mass_mailing")
|
||||
class TestMassMailValues(MassMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMassMailValues, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_body_cropped_vml_image(self):
|
||||
""" Testing mail mailing responsive bg-image cropping for Outlook.
|
||||
|
||||
Outlook needs background images to be converted to VML but there is no
|
||||
way to emulate `background-size: cover` that works for Windows Mail as
|
||||
well. We therefore need to crop the image in the VML version to mimick
|
||||
the style of other email clients.
|
||||
"""
|
||||
attachment = {}
|
||||
def patched_get_image(self, url, session):
|
||||
return base64.b64decode(BASE_64_STRING)
|
||||
original_images_to_urls = self.env['mailing.mailing']._create_attachments_from_inline_images
|
||||
def patched_images_to_urls(self, b64images):
|
||||
urls = original_images_to_urls(b64images)
|
||||
if len(urls) == 1:
|
||||
(attachment_id, attachment_token) = re.search(r'/web/image/(?P<id>[0-9]+)\?access_token=(?P<token>.*)', urls[0]).groups()
|
||||
attachment['id'] = attachment_id
|
||||
attachment['token'] = attachment_token
|
||||
return urls
|
||||
else:
|
||||
return []
|
||||
with patch("odoo.addons.mass_mailing.models.mailing.MassMailing._get_image_by_url",
|
||||
new=patched_get_image), \
|
||||
patch("odoo.addons.mass_mailing.models.mailing.MassMailing._create_attachments_from_inline_images",
|
||||
new=patched_images_to_urls):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'Test',
|
||||
'subject': 'Test',
|
||||
'state': 'draft',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'body_html': """
|
||||
<html>
|
||||
<!--[if mso]>
|
||||
<v:image src="https://www.example.com/image" style="width:100px;height:100px;"/>
|
||||
<![endif]-->
|
||||
</html>
|
||||
""",
|
||||
})
|
||||
self.assertEqual(str(mailing.body_html), f"""
|
||||
<html>
|
||||
<!--[if mso]>
|
||||
<v:image src="/web/image/{attachment['id']}?access_token={attachment['token']}" style="width:100px;height:100px;"/>
|
||||
<![endif]-->
|
||||
</html>
|
||||
""".strip())
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_body_inline_image(self):
|
||||
""" Testing mail mailing base64 image conversion to attachment.
|
||||
|
||||
This test ensures that the base64 images are correctly converted to
|
||||
attachments, even when they appear in MSO conditional comments.
|
||||
"""
|
||||
attachments = []
|
||||
original_images_to_urls = self.env['mailing.mailing']._create_attachments_from_inline_images
|
||||
def patched_images_to_urls(self, b64images):
|
||||
urls = original_images_to_urls(b64images)
|
||||
for url in urls:
|
||||
(attachment_id, attachment_token) = re.search(r'/web/image/(?P<id>[0-9]+)\?access_token=(?P<token>.*)', url).groups()
|
||||
attachments.append({
|
||||
'id': attachment_id,
|
||||
'token': attachment_token,
|
||||
})
|
||||
return urls
|
||||
with patch("odoo.addons.mass_mailing.models.mailing.MassMailing._create_attachments_from_inline_images",
|
||||
new=patched_images_to_urls):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'Test',
|
||||
'subject': 'Test',
|
||||
'state': 'draft',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'body_html': f"""
|
||||
<html><body>
|
||||
<img src="data:image/png;base64,{BASE_64_STRING}0">
|
||||
<img src="data:image/jpg;base64,{BASE_64_STRING}1">
|
||||
<div style='color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}2"); display: block;'/>
|
||||
<div style="color: red; background-image:url('data:image/jpg;base64,{BASE_64_STRING}3'); display: block;"/>
|
||||
<div style="color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}4"); display: block;"/>
|
||||
<div style="color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}5"); display: block;"/>
|
||||
<div style="color: red; background-image:url(data:image/jpg;base64,{BASE_64_STRING}6); display: block;"/>
|
||||
<div style="color: red; background-image: url(data:image/jpg;base64,{BASE_64_STRING}7); background: url('data:image/jpg;base64,{BASE_64_STRING}8'); display: block;"/>
|
||||
<!--[if mso]>
|
||||
<img src="data:image/png;base64,{BASE_64_STRING}9">Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
|
||||
Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
|
||||
<img src="data:image/jpg;base64,{BASE_64_STRING}10">
|
||||
<div style='color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}11"); display: block;'>Fake url, in text: style="background-image:url('data:image/png;base64,{BASE_64_STRING}');"
|
||||
Fake url, in text: style="background-image:url('data:image/png;base64,{BASE_64_STRING}');"</div>
|
||||
<div style="color: red; background-image:url('data:image/jpg;base64,{BASE_64_STRING}12'); display: block;"/>
|
||||
<div style="color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}13"); display: block;"/>
|
||||
<div style="color: red; background-image:url("data:image/jpg;base64,{BASE_64_STRING}14"); display: block;"/>
|
||||
<div style="color: red; background-image:url(data:image/jpg;base64,{BASE_64_STRING}15); display: block;"/>
|
||||
<div style="color: red; background-image: url(data:image/jpg;base64,{BASE_64_STRING}16); background: url('data:image/jpg;base64,{BASE_64_STRING}17'); display: block;"/>
|
||||
<![endif]-->
|
||||
<img src="data:image/png;base64,{BASE_64_STRING}0">
|
||||
</body></html>
|
||||
""",
|
||||
})
|
||||
self.assertEqual(len(attachments), 19)
|
||||
self.assertEqual(attachments[0]['id'], attachments[18]['id'])
|
||||
self.assertEqual(str(mailing.body_html), f"""
|
||||
<html><body>
|
||||
<img src="/web/image/{attachments[0]['id']}?access_token={attachments[0]['token']}">
|
||||
<img src="/web/image/{attachments[1]['id']}?access_token={attachments[1]['token']}">
|
||||
<div style='color: red; background-image:url("/web/image/{attachments[2]['id']}?access_token={attachments[2]['token']}"); display: block;'></div>
|
||||
<div style="color: red; background-image:url('/web/image/{attachments[3]['id']}?access_token={attachments[3]['token']}'); display: block;"></div>
|
||||
<div style='color: red; background-image:url("/web/image/{attachments[4]['id']}?access_token={attachments[4]['token']}"); display: block;'></div>
|
||||
<div style='color: red; background-image:url("/web/image/{attachments[5]['id']}?access_token={attachments[5]['token']}"); display: block;'></div>
|
||||
<div style="color: red; background-image:url(/web/image/{attachments[6]['id']}?access_token={attachments[6]['token']}); display: block;"></div>
|
||||
<div style="color: red; background-image: url(/web/image/{attachments[7]['id']}?access_token={attachments[7]['token']}); background: url('/web/image/{attachments[8]['id']}?access_token={attachments[8]['token']}'); display: block;"></div>
|
||||
<!--[if mso]>
|
||||
<img src="/web/image/{attachments[9]['id']}?access_token={attachments[9]['token']}">Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
|
||||
Fake url, in text: img src="data:image/png;base64,{BASE_64_STRING}"
|
||||
<img src="/web/image/{attachments[10]['id']}?access_token={attachments[10]['token']}">
|
||||
<div style='color: red; background-image:url("/web/image/{attachments[11]['id']}?access_token={attachments[11]['token']}"); display: block;'>Fake url, in text: style="background-image:url('data:image/png;base64,{BASE_64_STRING}');"
|
||||
Fake url, in text: style="background-image:url('data:image/png;base64,{BASE_64_STRING}');"</div>
|
||||
<div style="color: red; background-image:url('/web/image/{attachments[12]['id']}?access_token={attachments[12]['token']}'); display: block;"/>
|
||||
<div style="color: red; background-image:url("/web/image/{attachments[13]['id']}?access_token={attachments[13]['token']}"); display: block;"/>
|
||||
<div style="color: red; background-image:url("/web/image/{attachments[14]['id']}?access_token={attachments[14]['token']}"); display: block;"/>
|
||||
<div style="color: red; background-image:url(/web/image/{attachments[15]['id']}?access_token={attachments[15]['token']}); display: block;"/>
|
||||
<div style="color: red; background-image: url(/web/image/{attachments[16]['id']}?access_token={attachments[16]['token']}); background: url('/web/image/{attachments[17]['id']}?access_token={attachments[17]['token']}'); display: block;"/>
|
||||
<![endif]-->
|
||||
<img src="/web/image/{attachments[18]['id']}?access_token={attachments[18]['token']}">
|
||||
</body></html>
|
||||
""".strip())
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_body_responsive(self):
|
||||
""" Testing mail mailing responsive mail body
|
||||
|
||||
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/
|
||||
|
||||
This template is meant to put inline CSS into an email's head
|
||||
"""
|
||||
recipient = self.env['res.partner'].create({
|
||||
'name': 'Mass Mail Partner',
|
||||
'email': 'Customer <test.customer@example.com>',
|
||||
})
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'Test',
|
||||
'subject': 'Test',
|
||||
'state': 'draft',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
|
||||
composer = self.env['mail.compose.message'].with_user(self.user_marketing).with_context({
|
||||
'default_composition_mode': 'mass_mail',
|
||||
'default_model': 'res.partner',
|
||||
'default_res_id': recipient.id,
|
||||
}).create({
|
||||
'subject': 'Mass Mail Responsive',
|
||||
'body': 'I am Responsive body',
|
||||
'mass_mailing_id': mailing.id
|
||||
})
|
||||
|
||||
mail_values = composer.get_mail_values([recipient.id])
|
||||
body_html = mail_values[recipient.id]['body_html']
|
||||
|
||||
self.assertIn('<!DOCTYPE html>', body_html)
|
||||
self.assertIn('<head>', body_html)
|
||||
self.assertIn('viewport', body_html)
|
||||
# This is important: we need inline css, and not <link/>
|
||||
self.assertIn('@media', body_html)
|
||||
self.assertIn('I am Responsive body', body_html)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_computed_fields(self):
|
||||
# Create on res.partner, with default values for computed fields
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestMailing',
|
||||
'subject': 'Test',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
self.assertEqual(mailing.user_id, self.user_marketing)
|
||||
self.assertEqual(mailing.medium_id, self.env.ref('utm.utm_medium_email'))
|
||||
self.assertEqual(mailing.mailing_model_name, 'res.partner')
|
||||
self.assertEqual(mailing.mailing_model_real, 'res.partner')
|
||||
self.assertEqual(mailing.reply_to_mode, 'new')
|
||||
self.assertEqual(mailing.reply_to, self.user_marketing.email_formatted)
|
||||
# default for partner: remove blacklisted
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('is_blacklisted', '=', False)])
|
||||
# update domain
|
||||
mailing.write({
|
||||
'mailing_domain': [('email', 'ilike', 'test.example.com')]
|
||||
})
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('email', 'ilike', 'test.example.com')])
|
||||
|
||||
# reset mailing model -> reset domain; set reply_to -> keep it
|
||||
mailing.write({
|
||||
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
|
||||
'reply_to': self.email_reply_to,
|
||||
})
|
||||
self.assertEqual(mailing.mailing_model_name, 'mailing.list')
|
||||
self.assertEqual(mailing.mailing_model_real, 'mailing.contact')
|
||||
self.assertEqual(mailing.reply_to_mode, 'new')
|
||||
self.assertEqual(mailing.reply_to, self.email_reply_to)
|
||||
# default for mailing list: depends upon contact_list_ids
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('list_ids', 'in', [])])
|
||||
mailing.write({
|
||||
'contact_list_ids': [(4, self.mailing_list_1.id), (4, self.mailing_list_2.id)]
|
||||
})
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('list_ids', 'in', (self.mailing_list_1 | self.mailing_list_2).ids)])
|
||||
|
||||
# reset mailing model -> reset domain and reply to mode
|
||||
mailing.write({
|
||||
'mailing_model_id': self.env['ir.model']._get('mail.channel').id,
|
||||
})
|
||||
self.assertEqual(mailing.mailing_model_name, 'mail.channel')
|
||||
self.assertEqual(mailing.mailing_model_real, 'mail.channel')
|
||||
self.assertEqual(mailing.reply_to_mode, 'update')
|
||||
self.assertFalse(mailing.reply_to)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_computed_fields_domain_w_filter(self):
|
||||
""" Test domain update, involving mailing.filters added in 15.1. """
|
||||
# Create on res.partner, with default values for computed fields
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestMailing',
|
||||
'subject': 'Test',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
# default for partner: remove blacklisted
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('is_blacklisted', '=', False)])
|
||||
|
||||
# prepare initial data
|
||||
filter_1, filter_2, filter_3 = self.env['mailing.filter'].create([
|
||||
{'name': 'General channel',
|
||||
'mailing_domain' : [('name', '=', 'general')],
|
||||
'mailing_model_id': self.env['ir.model']._get('mail.channel').id,
|
||||
},
|
||||
{'name': 'LLN City',
|
||||
'mailing_domain' : [('city', 'ilike', 'LLN')],
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
},
|
||||
{'name': 'Email based',
|
||||
'mailing_domain' : [('email', 'ilike', 'info@odoo.com')],
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
}
|
||||
])
|
||||
|
||||
# check that adding mailing_filter_id updates domain correctly
|
||||
mailing.mailing_filter_id = filter_2
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), literal_eval(filter_2.mailing_domain))
|
||||
|
||||
# cannot set a filter linked to another model
|
||||
with self.assertRaises(ValidationError):
|
||||
mailing.mailing_filter_id = filter_1
|
||||
|
||||
# resetting model should reset domain, even if filter was chosen previously
|
||||
mailing.mailing_model_id = self.env['ir.model']._get('mail.channel').id
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [])
|
||||
|
||||
# changing the filter should update the mailing domain correctly
|
||||
mailing.mailing_filter_id = filter_1
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), literal_eval(filter_1.mailing_domain))
|
||||
|
||||
# changing the domain should not empty the mailing_filter_id
|
||||
mailing.mailing_domain = "[('email', 'ilike', 'info_be@odoo.com')]"
|
||||
self.assertEqual(mailing.mailing_filter_id, filter_1, "Filter should not be unset even if domain is changed")
|
||||
|
||||
# deleting the filter record should not delete the domain on mailing
|
||||
mailing.mailing_model_id = self.env['ir.model']._get('res.partner').id
|
||||
mailing.mailing_filter_id = filter_3
|
||||
filter_3_domain = filter_3.mailing_domain
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), literal_eval(filter_3_domain))
|
||||
filter_3.unlink() # delete the filter record
|
||||
self.assertFalse(mailing.mailing_filter_id, "Should unset filter if it is deleted")
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), literal_eval(filter_3_domain), "Should still have the same domain")
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_computed_fields_default(self):
|
||||
mailing = self.env['mailing.mailing'].with_context(
|
||||
default_mailing_domain=repr([('email', 'ilike', 'test.example.com')])
|
||||
).create({
|
||||
'name': 'TestMailing',
|
||||
'subject': 'Test',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<p>Hello <t t-out="object.name"/></p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
self.assertEqual(literal_eval(mailing.mailing_domain), [('email', 'ilike', 'test.example.com')])
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_computed_fields_default_email_from(self):
|
||||
# Testing if the email_from is correctly computed when an
|
||||
# alias domain for the company is set
|
||||
|
||||
# Setup mail outgoing server for use cases
|
||||
|
||||
from_filter_match, from_filter_missmatch = self.env['ir.mail_server'].sudo().create([
|
||||
# Case where alias domain is set and there is a default outgoing email server
|
||||
# for mass mailing. from_filter matches domain of company alias domain
|
||||
# before record creation
|
||||
{
|
||||
'name': 'mass_mailing_test_match_from_filter',
|
||||
'from_filter': self.alias_domain,
|
||||
'smtp_host': 'not_real@smtp.com',
|
||||
},
|
||||
# Case where alias domain is set and there is a default outgoing email server
|
||||
# for mass mailing. from_filter DOES NOT match domain of company alias domain
|
||||
# before record creation
|
||||
{
|
||||
'name': 'mass_mailing_test_from_missmatch',
|
||||
'from_filter': 'notcompanydomain.com',
|
||||
'smtp_host': 'not_real@smtp.com',
|
||||
},
|
||||
])
|
||||
|
||||
# Expected combos of server vs FROM values
|
||||
|
||||
servers = [
|
||||
self.env['ir.mail_server'],
|
||||
from_filter_match,
|
||||
from_filter_missmatch,
|
||||
]
|
||||
expected_from_all = [
|
||||
self.env.user.email_formatted, # default when no server
|
||||
self.env['ir.mail_server']._get_default_from_address(), # matches company alias domain
|
||||
self.env.user.email_formatted, # not matching from filter -> back to user from
|
||||
]
|
||||
|
||||
for mail_server, expected_from in zip(servers, expected_from_all):
|
||||
with self.subTest(server_name=mail_server.name):
|
||||
# When a mail server is set, we update the mass mailing
|
||||
# settings to designate a dedicated outgoing email server
|
||||
if mail_server:
|
||||
self.env['res.config.settings'].sudo().create({
|
||||
'mass_mailing_mail_server_id': mail_server.id,
|
||||
'mass_mailing_outgoing_mail_server': mail_server,
|
||||
}).execute()
|
||||
|
||||
# Create mailing
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': f'TestMailing {mail_server.name}',
|
||||
'subject': f'Test {mail_server.name}',
|
||||
})
|
||||
|
||||
# Check email_from
|
||||
self.assertEqual(mailing.email_from, expected_from)
|
||||
|
||||
# If configured, check if dedicated email outgoing server is
|
||||
# on mailing record
|
||||
self.assertEqual(mailing.mail_server_id, mail_server)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_computed_fields_form(self):
|
||||
mailing_form = Form(self.env['mailing.mailing'].with_context(
|
||||
default_mailing_domain="[('email', 'ilike', 'test.example.com')]",
|
||||
default_mailing_model_id=self.env['ir.model']._get('res.partner').id,
|
||||
))
|
||||
self.assertEqual(
|
||||
literal_eval(mailing_form.mailing_domain),
|
||||
[('email', 'ilike', 'test.example.com')],
|
||||
)
|
||||
self.assertEqual(mailing_form.mailing_model_real, 'res.partner')
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
@users('user_marketing')
|
||||
def test_mailing_trace_values(self):
|
||||
recipient = self.partner_employee
|
||||
|
||||
# both void and 0 are invalid, document should have an id != 0
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mailing.trace'].create({
|
||||
'model': recipient._name,
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mailing.trace'].create({
|
||||
'model': recipient._name,
|
||||
'res_id': 0,
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
self.env['mailing.trace'].create({
|
||||
'res_id': 3,
|
||||
})
|
||||
|
||||
activity = self.env['mailing.trace'].create({
|
||||
'model': recipient._name,
|
||||
'res_id': recipient.id,
|
||||
})
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'model': False})
|
||||
self.env.flush_all()
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'res_id': False})
|
||||
self.env.flush_all()
|
||||
with self.assertRaises(IntegrityError):
|
||||
activity.write({'res_id': 0})
|
||||
self.env.flush_all()
|
||||
|
||||
def test_mailing_editor_created_attachments(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestMailing',
|
||||
'subject': 'Test',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<p>Hello</p>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
blob_b64 = base64.b64encode(b'blob1')
|
||||
|
||||
# Created when uploading an image
|
||||
original_svg_attachment = self.env['ir.attachment'].create({
|
||||
"name": "test SVG",
|
||||
"mimetype": "image/svg+xml",
|
||||
"datas": blob_b64,
|
||||
"public": True,
|
||||
"res_model": "mailing.mailing",
|
||||
"res_id": mailing.id,
|
||||
})
|
||||
|
||||
# Created when saving the mass_mailing
|
||||
png_duplicate_of_svg_attachment = self.env['ir.attachment'].create({
|
||||
"name": "test SVG duplicate",
|
||||
"mimetype": "image/png",
|
||||
"datas": blob_b64,
|
||||
"public": True,
|
||||
"res_model": "mailing.mailing",
|
||||
"res_id": mailing.id,
|
||||
"original_id": original_svg_attachment.id
|
||||
})
|
||||
|
||||
# Created by uploading new image
|
||||
original_png_attachment = self.env['ir.attachment'].create({
|
||||
"name": "test PNG",
|
||||
"mimetype": "image/png",
|
||||
"datas": blob_b64,
|
||||
"public": True,
|
||||
"res_model": "mailing.mailing",
|
||||
"res_id": mailing.id,
|
||||
})
|
||||
|
||||
# Created by modify_image in editor controller
|
||||
self.env['ir.attachment'].create({
|
||||
"name": "test PNG duplicate",
|
||||
"mimetype": "image/png",
|
||||
"datas": blob_b64,
|
||||
"public": True,
|
||||
"res_model": "mailing.mailing",
|
||||
"res_id": mailing.id,
|
||||
"original_id": original_png_attachment.id
|
||||
})
|
||||
|
||||
mail_thread_attachments = mailing._get_mail_thread_data_attachments()
|
||||
self.assertSetEqual(set(mail_thread_attachments.ids), {png_duplicate_of_svg_attachment.id, original_png_attachment.id})
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_process_mailing_queue_without_html_body(self):
|
||||
""" Test mailing with past schedule date and without any html body """
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'preview': "Check it out before its too late",
|
||||
'body_html': False,
|
||||
'schedule_date': datetime(2023, 2, 17, 11, 0),
|
||||
})
|
||||
mailing.action_put_in_queue()
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing._process_mass_mailing_queue()
|
||||
|
||||
self.assertFalse(mailing.body_html)
|
||||
self.assertEqual(mailing.mailing_model_name, 'res.partner')
|
||||
|
||||
|
||||
@tagged("mass_mailing", "utm")
|
||||
class TestMassMailUTM(MassMailCommon):
|
||||
|
||||
@freeze_time('2022-01-02')
|
||||
@patch.object(Cursor, 'now', lambda *args, **kwargs: datetime(2022, 1, 2))
|
||||
@users('user_marketing')
|
||||
def test_mailing_unique_name(self):
|
||||
"""Test that the names are generated and unique for each mailing.
|
||||
|
||||
If the name is missing, it's generated from the subject. Then we should ensure
|
||||
that this generated name is unique.
|
||||
"""
|
||||
mailing_0 = self.env['mailing.mailing'].create({'subject': 'First subject'})
|
||||
self.assertEqual(mailing_0.name, 'First subject (Mass Mailing created on 2022-01-02)')
|
||||
|
||||
mailing_1, mailing_2, mailing_3, mailing_4, mailing_5, mailing_6 = self.env['mailing.mailing'].create([{
|
||||
'subject': 'First subject',
|
||||
}, {
|
||||
'subject': 'First subject',
|
||||
}, {
|
||||
'subject': 'First subject',
|
||||
'source_id': self.env['utm.source'].create({'name': 'Custom Source'}).id,
|
||||
}, {
|
||||
'subject': 'First subject',
|
||||
'name': 'Mailing',
|
||||
}, {
|
||||
'subject': 'Second subject',
|
||||
'name': 'Mailing',
|
||||
}, {
|
||||
'subject': 'Second subject',
|
||||
}])
|
||||
|
||||
self.assertEqual(mailing_0.name, 'First subject (Mass Mailing created on 2022-01-02)')
|
||||
self.assertEqual(mailing_1.name, 'First subject (Mass Mailing created on 2022-01-02) [2]')
|
||||
self.assertEqual(mailing_2.name, 'First subject (Mass Mailing created on 2022-01-02) [3]')
|
||||
self.assertEqual(mailing_3.name, 'Custom Source')
|
||||
self.assertEqual(mailing_4.name, 'Mailing')
|
||||
self.assertEqual(mailing_5.name, 'Mailing [2]')
|
||||
self.assertEqual(mailing_6.name, 'Second subject (Mass Mailing created on 2022-01-02)')
|
||||
|
||||
# should generate same name (coming from same subject)
|
||||
mailing_0.subject = 'First subject'
|
||||
self.assertEqual(mailing_0.name, 'First subject (Mass Mailing created on 2022-01-02)',
|
||||
msg='The name should not be updated')
|
||||
|
||||
# take a (long) existing name -> should increment
|
||||
mailing_0.name = 'Second subject (Mass Mailing created on 2022-01-02)'
|
||||
self.assertEqual(mailing_0.name, 'Second subject (Mass Mailing created on 2022-01-02) [2]',
|
||||
msg='The name must be unique, it was already taken')
|
||||
|
||||
# back to first subject: not linked to any record so should take it back
|
||||
mailing_0.subject = 'First subject'
|
||||
self.assertEqual(mailing_0.name, 'First subject (Mass Mailing created on 2022-01-02)',
|
||||
msg='The name should be back to first one')
|
||||
|
||||
def test_mailing_create_with_context(self):
|
||||
""" Test that the default_name provided via context is ignored to prevent constraint violations."""
|
||||
mailing_1, mailing_2 = self.env["mailing.mailing"].create([
|
||||
{
|
||||
"subject": "First subject",
|
||||
"name": "Mailing",
|
||||
},
|
||||
{
|
||||
"subject": "Second subject",
|
||||
"name": "Mailing",
|
||||
},
|
||||
])
|
||||
self.assertEqual(mailing_1.name, "Mailing")
|
||||
self.assertEqual(mailing_2.name, "Mailing [2]")
|
||||
mailing_3 = self.env["mailing.mailing"].with_context({"default_name": "Mailing"}).create({"subject": "Third subject"})
|
||||
self.assertEqual(mailing_3.name, "Mailing [3]")
|
||||
|
||||
|
||||
@tagged('mass_mailing')
|
||||
class TestMassMailFeatures(MassMailCommon, CronMixinCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMassMailFeatures, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mailing_cron_trigger(self):
|
||||
""" Technical test to ensure the cron is triggered at the correct
|
||||
time """
|
||||
|
||||
cron_id = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').id
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': 'Jean-Alphonce',
|
||||
'email': 'jeanalph@example.com',
|
||||
})
|
||||
common_mailing_values = {
|
||||
'name': 'Knock knock',
|
||||
'subject': "Who's there?",
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'mailing_domain': [('id', '=', partner.id)],
|
||||
'body_html': 'The marketing mailing test.',
|
||||
'schedule_type': 'scheduled',
|
||||
}
|
||||
|
||||
now = datetime(2021, 2, 5, 16, 43, 20)
|
||||
then = datetime(2021, 2, 7, 12, 0, 0)
|
||||
|
||||
with freeze_time(now):
|
||||
for (test, truth) in [(False, now), (then, then)]:
|
||||
with self.subTest(schedule_date=test):
|
||||
with self.capture_triggers(cron_id) as capt:
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
**common_mailing_values,
|
||||
'schedule_date': test,
|
||||
})
|
||||
mailing.action_put_in_queue()
|
||||
capt.records.ensure_one()
|
||||
self.assertLessEqual(capt.records.call_at, truth)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mailing_deletion(self):
|
||||
""" Test deletion in various use case, depending on reply-to """
|
||||
# 1- Keep archives and reply-to set to 'answer = new thread'
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestSource',
|
||||
'subject': 'TestDeletion',
|
||||
'body_html': "<div>Hello {object.name}</div>",
|
||||
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
|
||||
'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
|
||||
'keep_archives': True,
|
||||
'reply_to_mode': 'new',
|
||||
'reply_to': self.email_reply_to,
|
||||
})
|
||||
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(len(self._mails), 3)
|
||||
self.assertEqual(len(self._new_mails.exists()), 3)
|
||||
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
|
||||
|
||||
# 2- Keep archives and reply-to set to 'answer = update thread'
|
||||
self.mailing_list_1.contact_ids.message_ids.unlink()
|
||||
mailing = mailing.copy()
|
||||
mailing.write({
|
||||
'reply_to_mode': 'update',
|
||||
})
|
||||
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(len(self._mails), 3)
|
||||
self.assertEqual(len(self._new_mails.exists()), 3)
|
||||
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
|
||||
|
||||
# 3- Remove archives and reply-to set to 'answer = new thread'
|
||||
self.mailing_list_1.contact_ids.message_ids.unlink()
|
||||
mailing = mailing.copy()
|
||||
mailing.write({
|
||||
'keep_archives': False,
|
||||
'reply_to_mode': 'new',
|
||||
'reply_to': self.email_reply_to,
|
||||
})
|
||||
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(len(self._mails), 3)
|
||||
self.assertEqual(len(self._new_mails.exists()), 0)
|
||||
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
|
||||
|
||||
# 4- Remove archives and reply-to set to 'answer = update thread'
|
||||
# Imply keeping mail.message for gateway reply)
|
||||
self.mailing_list_1.contact_ids.message_ids.unlink()
|
||||
mailing = mailing.copy()
|
||||
mailing.write({
|
||||
'keep_archives': False,
|
||||
'reply_to_mode': 'update',
|
||||
})
|
||||
self.assertEqual(self.mailing_list_1.contact_ids.message_ids, self.env['mail.message'])
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing.action_send_mail()
|
||||
|
||||
self.assertEqual(len(self._mails), 3)
|
||||
self.assertEqual(len(self._new_mails.exists()), 0)
|
||||
self.assertEqual(len(self.mailing_list_1.contact_ids.message_ids), 3)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mailing_on_res_partner(self):
|
||||
""" Test mailing on res.partner model: ensure default recipients are
|
||||
correctly computed """
|
||||
partner_a = self.env['res.partner'].create({
|
||||
'name': 'test email 1',
|
||||
'email': 'test1@example.com',
|
||||
})
|
||||
partner_b = self.env['res.partner'].create({
|
||||
'name': 'test email 2',
|
||||
'email': 'test2@example.com',
|
||||
})
|
||||
self.env['mail.blacklist'].create({'email': 'Test2@example.com',})
|
||||
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'One',
|
||||
'subject': 'One',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'mailing_domain': [('id', 'in', (partner_a | partner_b).ids)],
|
||||
'body_html': 'This is mass mail marketing demo'
|
||||
})
|
||||
mailing.action_put_in_queue()
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing._process_mass_mailing_queue()
|
||||
|
||||
self.assertMailTraces(
|
||||
[{'partner': partner_a},
|
||||
{'partner': partner_b, 'trace_status': 'cancel', 'failure_type': 'mail_bl'}],
|
||||
mailing, partner_a + partner_b, check_mail=True
|
||||
)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_mailing_shortener(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestSource',
|
||||
'subject': 'TestShortener',
|
||||
'body_html': """<div>
|
||||
Hi,
|
||||
<t t-set="url" t-value="'www.odoo.com'"/>
|
||||
<t t-set="httpurl" t-value="'https://www.odoo.eu'"/>
|
||||
Website0: <a id="url0" t-attf-href="https://www.odoo.tz/my/{{object.name}}">https://www.odoo.tz/my/<t t-esc="object.name"/></a>
|
||||
Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</a>
|
||||
Website2: <a id="url2" t-attf-href="https://{{url}}">https://<t t-esc="url"/></a>
|
||||
Website3: <a id="url3" t-att-href="httpurl"><t t-esc="httpurl"/></a>
|
||||
External1: <a id="url4" href="https://www.example.com/foo/bar?baz=qux">Youpie</a>
|
||||
Email: <a id="url5" href="mailto:test@odoo.com">test@odoo.com</a></div>""",
|
||||
'mailing_model_id': self.env['ir.model']._get('mailing.list').id,
|
||||
'reply_to_mode': 'new',
|
||||
'reply_to': self.email_reply_to,
|
||||
'contact_list_ids': [(6, 0, self.mailing_list_1.ids)],
|
||||
'keep_archives': True,
|
||||
})
|
||||
|
||||
mailing.action_put_in_queue()
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing._process_mass_mailing_queue()
|
||||
|
||||
self.assertMailTraces(
|
||||
[{'email': 'fleurus@example.com'},
|
||||
{'email': 'gorramts@example.com'},
|
||||
{'email': 'ybrant@example.com'}],
|
||||
mailing, self.mailing_list_1.contact_ids, check_mail=True
|
||||
)
|
||||
|
||||
for contact in self.mailing_list_1.contact_ids:
|
||||
new_mail = self._find_mail_mail_wrecord(contact)
|
||||
for link_info in [('url0', 'https://www.odoo.tz/my/%s' % contact.name, True),
|
||||
('url1', 'https://www.odoo.be', True),
|
||||
('url2', 'https://www.odoo.com', True),
|
||||
('url3', 'https://www.odoo.eu', True),
|
||||
('url4', 'https://www.example.com/foo/bar?baz=qux', True),
|
||||
('url5', 'mailto:test@odoo.com', False)]:
|
||||
# TDE FIXME: why going to mail message id ? mail.body_html seems to fail, check
|
||||
link_params = {'utm_medium': 'Email', 'utm_source': mailing.name}
|
||||
if link_info[0] == 'url4':
|
||||
link_params['baz'] = 'qux'
|
||||
self.assertLinkShortenedHtml(
|
||||
new_mail.mail_message_id.body,
|
||||
link_info,
|
||||
link_params=link_params,
|
||||
)
|
||||
|
||||
|
||||
@tagged("mail_mail")
|
||||
class TestMailingHeaders(MassMailCommon, HttpCase):
|
||||
""" Test headers + linked controllers """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls._create_mailing_list()
|
||||
cls.test_mailing = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
|
||||
"body_html": """
|
||||
<p>Hello <t t-out="object.name"/>
|
||||
<a href="/unsubscribe_from_list">UNSUBSCRIBE</a>
|
||||
<a href="/view">VIEW</a>
|
||||
</p>""",
|
||||
"contact_list_ids": [(4, cls.mailing_list_1.id)],
|
||||
"mailing_model_id": cls.env["ir.model"]._get("mailing.list").id,
|
||||
"mailing_type": "mail",
|
||||
"name": "TestMailing",
|
||||
"subject": "Test for {{ object.name }}",
|
||||
})
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_unsubscribe_headers(self):
|
||||
""" Check unsubscribe headers are present in outgoing emails and work
|
||||
as one-click """
|
||||
test_mailing = self.test_mailing.with_env(self.env)
|
||||
test_mailing.action_put_in_queue()
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
test_mailing.action_send_mail()
|
||||
|
||||
for contact in self.mailing_list_1.contact_ids:
|
||||
new_mail = self._find_mail_mail_wrecord(contact)
|
||||
# check mail.mail still have default links
|
||||
self.assertIn("/unsubscribe_from_list", new_mail.body)
|
||||
self.assertIn("/view", new_mail.body)
|
||||
|
||||
# check outgoing email headers (those are put into outgoing email
|
||||
# not in the mail.mail record)
|
||||
email = self._find_sent_mail_wemail(contact.email)
|
||||
headers = email.get("headers")
|
||||
unsubscribe_oneclick_url = test_mailing._get_unsubscribe_oneclick_url(contact.email, contact.id)
|
||||
self.assertTrue(headers, "Mass mailing emails should have headers for unsubscribe")
|
||||
self.assertEqual(headers.get("List-Unsubscribe"), f"<{unsubscribe_oneclick_url}>")
|
||||
self.assertEqual(headers.get("List-Unsubscribe-Post"), "List-Unsubscribe=One-Click")
|
||||
self.assertEqual(headers.get("Precedence"), "list")
|
||||
|
||||
# check outgoing email has real links
|
||||
self.assertNotIn("/unsubscribe_from_list", email["body"])
|
||||
|
||||
# unsubscribe in one-click
|
||||
unsubscribe_oneclick_url = headers["List-Unsubscribe"].strip("<>")
|
||||
self.opener.post(unsubscribe_oneclick_url)
|
||||
|
||||
# should be unsubscribed
|
||||
self.assertTrue(contact.subscription_list_ids.opt_out)
|
||||
|
||||
|
||||
class TestMailingScheduleDateWizard(MassMailCommon):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_schedule_date(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject'
|
||||
})
|
||||
# create a schedule date wizard
|
||||
wizard_form = Form(
|
||||
self.env['mailing.mailing.schedule.date'].with_context(default_mass_mailing_id=mailing.id))
|
||||
|
||||
# set a schedule date
|
||||
wizard_form.schedule_date = datetime(2021, 4, 30, 9, 0)
|
||||
wizard = wizard_form.save()
|
||||
wizard.action_schedule_date()
|
||||
|
||||
# assert that the schedule_date and schedule_type fields are correct and that the mailing is put in queue
|
||||
self.assertEqual(mailing.schedule_date, datetime(2021, 4, 30, 9, 0))
|
||||
self.assertEqual(mailing.schedule_type, 'scheduled')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo import exceptions
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.tests.common import Form, users
|
||||
|
||||
|
||||
class TestMailingContactToList(MassMailCommon):
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_contact_to_list(self):
|
||||
contacts = self.env['mailing.contact'].create([{
|
||||
'name': 'Contact %02d',
|
||||
'email': 'contact_%02d@test.example.com',
|
||||
} for x in range(30)])
|
||||
|
||||
self.assertEqual(len(contacts), 30)
|
||||
self.assertEqual(contacts.list_ids, self.env['mailing.list'])
|
||||
|
||||
mailing = self.env['mailing.list'].create({
|
||||
'name': 'Contacts Agregator',
|
||||
})
|
||||
|
||||
# create wizard with context values
|
||||
wizard_form = Form(self.env['mailing.contact.to.list'].with_context(default_contact_ids=contacts.ids))
|
||||
self.assertEqual(wizard_form.contact_ids._get_ids(), contacts.ids)
|
||||
|
||||
# set mailing list and add contacts
|
||||
wizard_form.mailing_list_id = mailing
|
||||
wizard = wizard_form.save()
|
||||
action = wizard.action_add_contacts()
|
||||
self.assertEqual(contacts.list_ids, mailing)
|
||||
self.assertEqual(action["type"], "ir.actions.client")
|
||||
self.assertTrue(action.get("params", {}).get("next"), "Should return a notification with a next action")
|
||||
subaction = action["params"]["next"]
|
||||
self.assertEqual(subaction["type"], "ir.actions.act_window_close")
|
||||
|
||||
# set mailing list, add contacts and redirect to mailing view
|
||||
mailing2 = self.env['mailing.list'].create({
|
||||
'name': 'Contacts Sublimator',
|
||||
})
|
||||
|
||||
wizard_form.mailing_list_id = mailing2
|
||||
wizard = wizard_form.save()
|
||||
action = wizard.action_add_contacts_and_send_mailing()
|
||||
self.assertEqual(contacts.list_ids, mailing + mailing2)
|
||||
self.assertEqual(action["type"], "ir.actions.client")
|
||||
self.assertTrue(action.get("params", {}).get("next"), "Should return a notification with a next action")
|
||||
subaction = action["params"]["next"]
|
||||
self.assertEqual(subaction["type"], "ir.actions.act_window")
|
||||
self.assertEqual(subaction["context"]["default_contact_list_ids"], [mailing2.id])
|
||||
|
||||
|
||||
class TestMailingListMerge(MassMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingListMerge, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
cls.mailing_list_3 = cls.env['mailing.list'].with_context(cls._test_context).create({
|
||||
'name': 'ListC',
|
||||
'contact_ids': [
|
||||
(0, 0, {'name': 'Norberto', 'email': 'norbert@example.com'}),
|
||||
]
|
||||
})
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_contact_create(self):
|
||||
default_list_ids = (self.mailing_list_2 | self.mailing_list_3).ids
|
||||
|
||||
# simply set default list in context
|
||||
new = self.env['mailing.contact'].with_context(default_list_ids=default_list_ids).create([{
|
||||
'name': 'Contact_%d' % x,
|
||||
'email': 'contact_%d@test.example.com' % x,
|
||||
} for x in range(0, 5)])
|
||||
self.assertEqual(new.list_ids, (self.mailing_list_2 | self.mailing_list_3))
|
||||
|
||||
# default list and subscriptions should be merged
|
||||
new = self.env['mailing.contact'].with_context(default_list_ids=default_list_ids).create([{
|
||||
'name': 'Contact_%d' % x,
|
||||
'email': 'contact_%d@test.example.com' % x,
|
||||
'subscription_list_ids': [(0, 0, {
|
||||
'list_id': self.mailing_list_1.id,
|
||||
'opt_out': True,
|
||||
}), (0, 0, {
|
||||
'list_id': self.mailing_list_2.id,
|
||||
'opt_out': True,
|
||||
})],
|
||||
} for x in range(0, 5)])
|
||||
self.assertEqual(new.list_ids, (self.mailing_list_1 | self.mailing_list_2 | self.mailing_list_3))
|
||||
# should correctly take subscription opt_out value
|
||||
for list_id in (self.mailing_list_1 | self.mailing_list_2).ids:
|
||||
new = new.with_context(default_list_ids=[list_id])
|
||||
self.assertTrue(all(contact.opt_out for contact in new))
|
||||
# not opt_out for new subscription without specific create values
|
||||
for list_id in self.mailing_list_3.ids:
|
||||
new = new.with_context(default_list_ids=[list_id])
|
||||
self.assertFalse(any(contact.opt_out for contact in new))
|
||||
|
||||
with freeze_time('2022-01-01 12:00'):
|
||||
contact_form = Form(self.env['mailing.contact'])
|
||||
contact_form.name = 'Contact_test'
|
||||
with contact_form.subscription_list_ids.new() as subscription:
|
||||
subscription.list_id = self.mailing_list_1
|
||||
subscription.opt_out = True
|
||||
with contact_form.subscription_list_ids.new() as subscription:
|
||||
subscription.list_id = self.mailing_list_2
|
||||
subscription.opt_out = False
|
||||
contact = contact_form.save()
|
||||
self.assertEqual(contact.subscription_list_ids[0].unsubscription_date, datetime(2022, 1, 1, 12, 0, 0))
|
||||
self.assertFalse(contact.subscription_list_ids[1].unsubscription_date)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_contact_copy_in_context_of_mailing_list(self):
|
||||
MailingContact = self.env['mailing.contact']
|
||||
contact_1 = MailingContact.create({
|
||||
'name': 'Sam',
|
||||
'email': 'gamgee@shire.com',
|
||||
'subscription_list_ids': [(0, 0, {'list_id': self.mailing_list_3.id})],
|
||||
})
|
||||
# Copy the contact with default_list_ids in context, which should not raise anything
|
||||
contact_2 = contact_1.with_context(default_list_ids=self.mailing_list_3.ids).copy()
|
||||
self.assertEqual(contact_1.list_ids, contact_2.list_ids, 'Should copy the existing mailing list(s)')
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_merge(self):
|
||||
# TEST CASE: Merge A,B into the existing mailing list C
|
||||
# The mailing list C contains the same email address than 'Norbert' in list B
|
||||
# This test ensure that the mailing lists are correctly merged and no
|
||||
# duplicates are appearing in C
|
||||
merge_form = Form(self.env['mailing.list.merge'].with_context(
|
||||
active_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
|
||||
active_model='mailing.list'
|
||||
))
|
||||
merge_form.new_list_name = False
|
||||
merge_form.merge_options = 'existing'
|
||||
# Need to set `merge_options` before `dest_lid_id` so `dest_list_id` is visible
|
||||
# `'invisible': [('merge_options', '=', 'new')]`
|
||||
merge_form.dest_list_id = self.mailing_list_3
|
||||
merge_form.archive_src_lists = False
|
||||
result_list = merge_form.save().action_mailing_lists_merge()
|
||||
|
||||
# Assert the number of contacts is correct
|
||||
self.assertEqual(
|
||||
len(result_list.contact_ids.ids), 5,
|
||||
'The number of contacts on the mailing list C is not equal to 5')
|
||||
|
||||
# Assert there's no duplicated email address
|
||||
self.assertEqual(
|
||||
len(list(set(result_list.contact_ids.mapped('email')))), 5,
|
||||
'Duplicates have been merged into the destination mailing list. Check %s' % (result_list.contact_ids.mapped('email')))
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_list_merge_cornercase(self):
|
||||
""" Check wrong use of merge wizard """
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
merge_form = Form(self.env['mailing.list.merge'].with_context(
|
||||
active_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
|
||||
))
|
||||
|
||||
merge_form = Form(self.env['mailing.list.merge'].with_context(
|
||||
active_ids=[self.mailing_list_1.id],
|
||||
active_model='mailing.list',
|
||||
default_src_list_ids=[self.mailing_list_1.id, self.mailing_list_2.id],
|
||||
default_dest_list_id=self.mailing_list_3.id,
|
||||
default_merge_options='existing',
|
||||
))
|
||||
merge = merge_form.save()
|
||||
self.assertEqual(merge.src_list_ids, self.mailing_list_1 + self.mailing_list_2)
|
||||
self.assertEqual(merge.dest_list_id, self.mailing_list_3)
|
||||
|
||||
|
||||
class TestMailingContactImport(MassMailCommon):
|
||||
"""Test the transient <mailing.contact.import>."""
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_contact_import(self):
|
||||
first_list, second_list, third_list = self.env['mailing.list'].create([
|
||||
{'name': 'First mailing list'},
|
||||
{'name': 'Second mailing list'},
|
||||
{'name': 'Third mailing list'},
|
||||
])
|
||||
|
||||
self.env['mailing.contact'].create([
|
||||
{
|
||||
'name': 'Already Exists',
|
||||
'email': 'already_exists_list_1@example.com',
|
||||
'list_ids': first_list.ids,
|
||||
}, {
|
||||
'email': 'already_exists_list_2@example.com',
|
||||
'list_ids': second_list.ids,
|
||||
}, {
|
||||
'email': 'already_exists_list_1_and_2@example.com',
|
||||
'list_ids': (first_list | second_list).ids,
|
||||
},
|
||||
])
|
||||
|
||||
self.env['mailing.mailing'].create({
|
||||
'name': 'Test',
|
||||
'subject': 'Test',
|
||||
'contact_list_ids': (first_list | second_list).ids,
|
||||
})
|
||||
|
||||
contact_import = Form(self.env['mailing.contact.import'].with_context(
|
||||
default_mailing_list_ids=first_list.ids,
|
||||
))
|
||||
|
||||
contact_import.contact_list = '''
|
||||
invalid line1
|
||||
alice@example.com
|
||||
bob@example.com
|
||||
invalid line2
|
||||
"Bob" <bob@EXAMPLE.com>
|
||||
"Test" <bob@example.com>
|
||||
|
||||
invalid line3, with a comma
|
||||
already_exists_list_1@example.com
|
||||
already_exists_list_2@example.com
|
||||
"Test" <already_exists_list_1_and_2@example.com>
|
||||
invalid line4
|
||||
'''
|
||||
contact_import = contact_import.save()
|
||||
|
||||
self.assertEqual(contact_import.mailing_list_ids, first_list)
|
||||
|
||||
# Can not add many2many directly on a Form
|
||||
contact_import.mailing_list_ids |= third_list
|
||||
|
||||
self.assertEqual(len(first_list.contact_ids), 2, 'Should not yet create the contact')
|
||||
self.assertEqual(len(second_list.contact_ids), 2, 'Should not yet create the contact')
|
||||
self.assertEqual(len(third_list.contact_ids), 0, 'Should not yet create the contact')
|
||||
|
||||
# Test that the context key "default_list_ids" is ignored (because we manually set list_ids)
|
||||
contact_import.with_context(default_list_ids=(first_list | second_list).ids).action_import()
|
||||
|
||||
# Check the contact of the first mailing list
|
||||
contacts = [
|
||||
(contact.name, contact.email)
|
||||
for contact in first_list.contact_ids
|
||||
]
|
||||
self.assertIn(('', 'alice@example.com'), contacts, 'Should have imported the right email address')
|
||||
self.assertIn(('Bob', 'bob@example.com'), contacts, 'Should have imported the name of the contact')
|
||||
self.assertIn(
|
||||
('', 'already_exists_list_2@example.com'), contacts,
|
||||
'The email already exists but in a different list. The contact must be imported.')
|
||||
self.assertEqual(len(second_list.contact_ids), 2, 'Should have ignored default_list_ids')
|
||||
self.assertNotIn(('Test', 'bob@example.com'), contacts, 'Should have ignored duplicated')
|
||||
self.assertNotIn(('', 'bob@example.com'), contacts, 'Should have ignored duplicated')
|
||||
self.assertNotIn(('Test', 'already_exists_list_1_and_2@example.com'), contacts, 'Should have ignored duplicated')
|
||||
self.assertEqual(len(contacts), 5, 'Should have imported 2 new contacts')
|
||||
|
||||
# Check the contact of the third mailing list
|
||||
contacts = [
|
||||
(contact.name, contact.email)
|
||||
for contact in third_list.contact_ids
|
||||
]
|
||||
self.assertIn(('', 'alice@example.com'), contacts, 'Should have imported the right email address')
|
||||
self.assertIn(('Bob', 'bob@example.com'), contacts, 'Should have imported the name of the contact')
|
||||
self.assertIn(
|
||||
('', 'already_exists_list_2@example.com'), contacts,
|
||||
'The email already exists but in a different list. The contact must be imported.')
|
||||
self.assertIn(('Already Exists', 'already_exists_list_1@example.com'), contacts, 'This contact exists in the first mailing list, but not in the third mailing list')
|
||||
self.assertNotIn(('Test', 'already_exists_list_1_and_2@example.com'), contacts, 'Should have ignored duplicated')
|
||||
|
||||
contact = self.env['mailing.contact'].search([('email', '=', 'already_exists_list_1@example.com')])
|
||||
self.assertEqual(len(contact), 1, 'Should have updated the existing contact instead of creating a new one')
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from freezegun import freeze_time
|
||||
|
||||
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.tests import users, Form
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
class TestMailingScheduleDateWizard(MassMailCommon, CronMixinCase):
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
@users('user_marketing')
|
||||
def test_mailing_next_departure(self):
|
||||
# test if mailing.mailing.next_departure is correctly set taking into account
|
||||
# presence of implicitly created cron triggers (since odoo v15). These should
|
||||
# launch cron job before its schedule nextcall datetime (if scheduled_date < nextcall)
|
||||
|
||||
cron_job = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo()
|
||||
cron_job.write({'nextcall' : datetime(2023, 2, 18, 9, 0)})
|
||||
cron_job_id = cron_job.id
|
||||
|
||||
# case where user click on "Send" button (action_launch)
|
||||
with freeze_time(datetime(2023, 2, 17, 9, 0)):
|
||||
with self.capture_triggers(cron_job_id) as capt:
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'state' : 'draft'
|
||||
})
|
||||
mailing.action_launch()
|
||||
capt.records.ensure_one()
|
||||
|
||||
# assert that the schedule_date and schedule_type fields are correct and that the mailing is put in queue
|
||||
self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 9, 0))
|
||||
self.assertIsNot(mailing.schedule_date, cron_job.nextcall)
|
||||
self.assertEqual(mailing.schedule_type, 'now')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
self.assertEqual(capt.records.call_at, datetime(2023, 2, 17, 9, 0)) #verify that cron.trigger exists
|
||||
|
||||
# case where client uses schedule wizard to chose a date between now and cron.job nextcall
|
||||
with freeze_time(datetime(2023, 2, 17, 9, 0)):
|
||||
with self.capture_triggers(cron_job_id) as capt:
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'state' : 'draft',
|
||||
'schedule_date' : datetime(2023, 2, 17, 11, 0),
|
||||
'schedule_type' : 'scheduled'
|
||||
})
|
||||
mailing.action_schedule()
|
||||
capt.records.ensure_one()
|
||||
|
||||
self.assertEqual(mailing.schedule_date, datetime(2023, 2, 17, 11, 0))
|
||||
self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 11, 0))
|
||||
self.assertEqual(mailing.schedule_type, 'scheduled')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
self.assertEqual(capt.records.call_at, datetime(2023, 2, 17, 11, 0)) #verify that cron.trigger exists
|
||||
|
||||
# case where client uses schedule wizard to chose a date after cron.job nextcall
|
||||
# which means mails will get send after that date (datetime(2023, 2, 18, 9, 0))
|
||||
with freeze_time(datetime(2023, 2, 17, 9, 0)):
|
||||
with self.capture_triggers(cron_job_id) as capt:
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'state' : 'draft',
|
||||
'schedule_date' : datetime(2024, 2, 17, 11, 0),
|
||||
'schedule_type' : 'scheduled'
|
||||
})
|
||||
mailing.action_schedule()
|
||||
capt.records.ensure_one()
|
||||
|
||||
self.assertEqual(mailing.schedule_date, datetime(2024, 2, 17, 11, 0))
|
||||
self.assertEqual(mailing.next_departure, datetime(2024, 2, 17, 11, 0))
|
||||
self.assertEqual(mailing.schedule_type, 'scheduled')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
self.assertEqual(capt.records.call_at, datetime(2024, 2, 17, 11, 0)) #verify that cron.trigger exists
|
||||
|
||||
# case where client uses schedule wizard to chose a date in the past
|
||||
with freeze_time(datetime(2023, 2, 17, 9, 0)):
|
||||
with self.capture_triggers(cron_job_id) as capt:
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'state' : 'draft',
|
||||
'schedule_date' : datetime(2024, 2, 17, 11, 0),
|
||||
'schedule_type' : 'scheduled'
|
||||
})
|
||||
# create a schedule date wizard
|
||||
# Have to use wizard for this case to simulate schedule date in the past
|
||||
# Otherwise "state" doesn't get update from draft to'in_queue'
|
||||
# in test env vs production env (see mailing.mailing.schedule.date wizard)
|
||||
|
||||
wizard_form = Form(
|
||||
self.env['mailing.mailing.schedule.date'].with_context(default_mass_mailing_id=mailing.id))
|
||||
|
||||
# set a schedule date
|
||||
wizard_form.schedule_date = datetime(2022, 2, 17, 11, 0)
|
||||
wizard = wizard_form.save()
|
||||
wizard.action_schedule_date()
|
||||
capt.records.ensure_one()
|
||||
|
||||
self.assertEqual(mailing.schedule_date, datetime(2022, 2, 17, 11, 0))
|
||||
self.assertEqual(mailing.next_departure, datetime(2023, 2, 17, 9, 0)) #now
|
||||
self.assertEqual(mailing.schedule_type, 'scheduled')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
self.assertEqual(capt.records.call_at, datetime(2022, 2, 17, 11, 0)) #verify that cron.trigger exists
|
||||
|
||||
def test_mailing_schedule_date(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'mailing',
|
||||
'subject': 'some subject',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
})
|
||||
# create a schedule date wizard
|
||||
wizard_form = Form(
|
||||
self.env['mailing.mailing.schedule.date'].with_context(default_mass_mailing_id=mailing.id))
|
||||
|
||||
# set a schedule date
|
||||
wizard_form.schedule_date = datetime(2021, 4, 30, 9, 0)
|
||||
wizard = wizard_form.save()
|
||||
wizard.action_schedule_date()
|
||||
|
||||
# assert that the schedule_date and schedule_type fields are correct and that the mailing is put in queue
|
||||
self.assertEqual(mailing.schedule_date, datetime(2021, 4, 30, 9, 0))
|
||||
self.assertEqual(mailing.schedule_type, 'scheduled')
|
||||
self.assertEqual(mailing.state, 'in_queue')
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
|
||||
from odoo.tests.common import users
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
class TestMailingRetry(MassMailCommon, CronMixinCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingRetry, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mailing_retry_immediate_trigger(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'name': 'TestMailing',
|
||||
'subject': 'Test',
|
||||
'mailing_type': 'mail',
|
||||
'body_html': '<div>Hello</div>',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id,
|
||||
'contact_list_ids': [(4, self.mailing_list_1.id)],
|
||||
})
|
||||
mailing.action_launch()
|
||||
|
||||
# force email sending to fail to test our retry mechanism
|
||||
def patched_mail_mail_send(mail_records, auto_commit=False, raise_exception=False, smtp_session=None):
|
||||
mail_records.write({'state': 'exception', 'failure_reason': 'forced_failure'})
|
||||
|
||||
with patch('odoo.addons.mail.models.mail_mail.MailMail._send', patched_mail_mail_send):
|
||||
self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().method_direct_trigger()
|
||||
|
||||
with self.capture_triggers('mass_mailing.ir_cron_mass_mailing_queue') as captured_triggers:
|
||||
mailing.action_retry_failed()
|
||||
|
||||
self.assertEqual(len(captured_triggers.records), 1, "Should have created an additional trigger immediately")
|
||||
captured_trigger = captured_triggers.records[0]
|
||||
self.assertEqual(captured_trigger.cron_id, self.env.ref('mass_mailing.ir_cron_mass_mailing_queue'))
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import odoo.tests
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
|
||||
|
||||
@odoo.tests.tagged('-at_install', 'post_install')
|
||||
class TestUi(HttpCaseWithUserDemo):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user_demo.groups_id |= self.env.ref('mass_mailing.group_mass_mailing_user')
|
||||
self.user_demo.groups_id |= self.env.ref('mail.group_mail_template_editor')
|
||||
self.user_demo.groups_id |= self.env.ref('mass_mailing.group_mass_mailing_campaign')
|
||||
|
||||
def test_01_mass_mailing_editor_tour(self):
|
||||
self.start_tour("/web", 'mass_mailing_editor_tour', login="demo")
|
||||
mail = self.env['mailing.mailing'].search([('subject', '=', 'Test')])[0]
|
||||
# The tour created and saved an email. The edited version should be
|
||||
# saved in body_arch, and its transpiled version (see convert_inline)
|
||||
# for email client compatibility should be saved in body_html. This
|
||||
# ensures both fields have different values (the mailing body should
|
||||
# have been converted to a table in body_html).
|
||||
self.assertIn('data-snippet="s_title"', mail.body_arch)
|
||||
self.assertTrue(mail.body_arch.startswith('<div'))
|
||||
self.assertIn('data-snippet="s_title"', mail.body_html)
|
||||
self.assertTrue(mail.body_html.startswith('<table'))
|
||||
|
||||
def test_02_mass_mailing_snippets_menu_tabs(self):
|
||||
self.start_tour("/web", 'mass_mailing_snippets_menu_tabs', login="demo")
|
||||
|
||||
def test_03_mass_mailing_snippets_toolbar_mobile_hide(self):
|
||||
self.start_tour("/web", 'mass_mailing_snippets_menu_toolbar_new_mailing_mobile', login="demo")
|
||||
|
||||
def test_04_mass_mailing_snippets_menu_hide(self):
|
||||
self.start_tour("/web", 'mass_mailing_snippets_menu_toolbar', login="demo")
|
||||
|
||||
def test_05_mass_mailing_basic_theme_toolbar(self):
|
||||
self.start_tour('/web', 'mass_mailing_basic_theme_toolbar', login="demo")
|
||||
|
||||
def test_06_mass_mailing_campaign_new_mailing(self):
|
||||
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('mass_mailing.group_mass_mailing_campaign').id)]})
|
||||
campaign = self.env['utm.campaign'].create({
|
||||
'name': 'Test Newsletter',
|
||||
'user_id': self.env.ref("base.user_admin").id,
|
||||
'tag_ids': [(4, self.env.ref('utm.utm_tag_1').id)],
|
||||
})
|
||||
self.env['mailing.mailing'].create({
|
||||
'name': 'First Mailing to disply x2many',
|
||||
'subject': 'Bioutifoul mailing',
|
||||
'state': 'draft',
|
||||
'campaign_id': campaign.id,
|
||||
})
|
||||
self.env['mailing.list'].create({
|
||||
'name': 'Test Newsletter',
|
||||
})
|
||||
self.start_tour("/web", 'mass_mailing_campaing_new_mailing', login="demo")
|
||||
|
||||
def test_07_mass_mailing_code_view_tour(self):
|
||||
self.start_tour("/web?debug=tests", 'mass_mailing_code_view_tour', login="demo")
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.mass_mailing.tests.common import MassMailCommon
|
||||
from odoo.addons.utm.tests.common import TestUTMCommon
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import tagged, users
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install', 'utm_consistency')
|
||||
class TestUTMConsistencyMassMailing(TestUTMCommon, MassMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestUTMConsistencyMassMailing, cls).setUpClass()
|
||||
cls._create_mailing_list()
|
||||
|
||||
@users('__system__')
|
||||
def test_utm_consistency(self):
|
||||
mailing = self.env['mailing.mailing'].create({
|
||||
'subject': 'Newsletter',
|
||||
'mailing_model_id': self.env['ir.model']._get('res.partner').id
|
||||
})
|
||||
# the source is automatically created when creating a mailing
|
||||
utm_source = mailing.source_id
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
# can't unlink the source as it's used by a mailing.mailing as its source
|
||||
# unlinking the source would break all the mailing statistics
|
||||
utm_source.unlink()
|
||||
|
||||
# the medium "Email" (from module XML data) is automatically assigned
|
||||
# when creating a mailing
|
||||
utm_medium = mailing.medium_id
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
# can't unlink the medium as it's used by a mailing.mailing as its medium
|
||||
# unlinking the medium would break all the mailing statistics
|
||||
utm_medium.unlink()
|
||||
|
||||
@users('user_marketing')
|
||||
def test_utm_consistency_mass_mailing_user(self):
|
||||
# mass mailing user should be able to unlink all UTM models
|
||||
self.utm_campaign.unlink()
|
||||
self.utm_medium.unlink()
|
||||
self.utm_source.unlink()
|
||||
Loading…
Add table
Add a link
Reference in a new issue