[FW][MERGE] digest, mass_mailing: fix usage of digests / statistics emails
Provide fixes related to digest and mass mailing statistics email after some
testing.
Fixes notably
* use queue to send emails;
* ease unsubscribe for non admin people using a route and a token to allow
unsubscribe when being not logged;
* improve tone down of digests when users have no activity, gradually going
to quarterly digests instead of sticking on weekly;
* fix statistics emails being broken due to some change in digest layout;
* fix statistics emails author / from / to computation;
* fix unsubscribe display and usage;
* fix 'body' part of digest location;
Tests are added in digest and mass mailing to test a bit more in-depth those
features.
See sub commits for more details.
Task-2641394 (Digest emails sending improvement)
Task-2582128 (Digest onbarding and usage improvement)
Task-2686586 (Repair mailing statistics email)
Task-2688856 (Digest tone down improvement)
Task-2692316 (Mailing trace status overrides)
closes odoo/odoo#79877
Forward-port-of: odoo/odoo#77609
Forward-port-of: odoo/odoo#76591
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
@@ -1,21 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.exceptions import Forbidden
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import _
|
||||
from odoo.http import Controller, request, route
|
||||
from odoo.tools import consteq
|
||||
|
||||
|
||||
class DigestController(Controller):
|
||||
|
||||
@route('/digest/<int:digest_id>/unsubscribe', type='http', website=True, auth='user')
|
||||
def digest_unsubscribe(self, digest_id):
|
||||
digest = request.env['digest.digest'].browse(digest_id).exists()
|
||||
digest.action_unsubcribe()
|
||||
@route('/digest/<int:digest_id>/unsubscribe', type='http', website=True, auth='public')
|
||||
def digest_unsubscribe(self, digest_id, token=None, user_id=None):
|
||||
digest_sudo = request.env['digest.digest'].sudo().browse(digest_id).exists()
|
||||
|
||||
# new route parameters
|
||||
if digest_sudo and token and user_id:
|
||||
correct_token = digest_sudo._get_unsubscribe_token(int(user_id))
|
||||
if not consteq(correct_token, token):
|
||||
raise NotFound()
|
||||
digest_sudo._action_unsubscribe_users(request.env['res.users'].sudo().browse(int(user_id)))
|
||||
# old route was given without any token or user_id but only for auth users
|
||||
elif digest_sudo and not token and not user_id and not request.env.user.share:
|
||||
digest_sudo.action_unsubcribe()
|
||||
else:
|
||||
raise NotFound()
|
||||
|
||||
return request.render('digest.portal_digest_unsubscribed', {
|
||||
'digest': digest,
|
||||
'digest': digest_sudo,
|
||||
})
|
||||
|
||||
@route('/digest/<int:digest_id>/set_periodicity', type='http', website=True, auth='user')
|
||||
|
||||
@@ -391,21 +391,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="preferences">
|
||||
<t t-if="body" t-out="body"/>
|
||||
|
||||
<div>
|
||||
<div class="global_layout">
|
||||
<div class="preference_div">
|
||||
<div t-foreach="preferences" t-as="preference" class="preference">
|
||||
<div t-if="preferences" t-foreach="preferences" t-as="preference"
|
||||
class="preference">
|
||||
<t t-out="preference"/>
|
||||
</div>
|
||||
<div class="by_odoo">
|
||||
Sent by <a href="https://www.odoo.com" target="_blank" class="odoo_link"><span class="odoo_link_text">Odoo</span></a> –
|
||||
<a t-att-href="'/web#view_type=form&model=digest.digest&id=%s' % object.id" target="_blank" style="text-decoration: none;"><span style="color: #8f8f8f;">Unsubscribe</span></a>
|
||||
Sent by <a href="https://www.odoo.com" target="_blank" class="odoo_link"><span class="odoo_link_text">Odoo</span></a>
|
||||
<t t-if="unsubscribe_token">
|
||||
–
|
||||
<a t-attf-href="/digest/#{object.id}/unsubscribe?token=#{unsubscribe_token}&user_id=#{user.id}"
|
||||
target="_blank" style="text-decoration: none;">
|
||||
<span style="color: #8f8f8f;">Unsubscribe</span>
|
||||
</a>
|
||||
</t>
|
||||
<t t-elif="object and object._name == 'digest.digest'">
|
||||
–
|
||||
<a t-att-href="'/web#view_type=form&model=digest.digest&id=%s' % object.id"
|
||||
target="_blank" style="text-decoration: none;">
|
||||
<span style="color: #8f8f8f;">Unsubscribe</span>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<t t-if="body" t-out="body"/>
|
||||
<div t-if="display_mobile_banner" t-call="digest.digest_section_mobile" />
|
||||
|
||||
<div class="global_layout" id="footer">
|
||||
|
||||
@@ -7,7 +7,6 @@ import pytz
|
||||
from datetime import datetime, date
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from markupsafe import Markup
|
||||
from werkzeug.urls import url_join
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
@@ -41,6 +40,7 @@ class Digest(models.Model):
|
||||
kpi_mail_message_total = fields.Boolean('Messages')
|
||||
kpi_mail_message_total_value = fields.Integer(compute='_compute_kpi_mail_message_total_value')
|
||||
|
||||
@api.depends('user_ids')
|
||||
def _compute_is_subscribed(self):
|
||||
for digest in self:
|
||||
digest.is_subscribed = self.env.user in digest.user_ids
|
||||
@@ -87,11 +87,21 @@ class Digest(models.Model):
|
||||
|
||||
def action_subscribe(self):
|
||||
if self.env.user.has_group('base.group_user') and self.env.user not in self.user_ids:
|
||||
self.sudo().user_ids |= self.env.user
|
||||
self._action_subscribe_users(self.env.user)
|
||||
|
||||
def _action_subscribe_users(self, users):
|
||||
""" Private method to manage subscriptions. Done as sudo() to speedup
|
||||
computation and avoid ACLs issues. """
|
||||
self.sudo().user_ids |= users
|
||||
|
||||
def action_unsubcribe(self):
|
||||
if self.env.user.has_group('base.group_user') and self.env.user in self.user_ids:
|
||||
self.sudo().user_ids -= self.env.user
|
||||
self._action_unsubscribe_users(self.env.user)
|
||||
|
||||
def _action_unsubscribe_users(self, users):
|
||||
""" Private method to manage subscriptions. Done as sudo() to speedup
|
||||
computation and avoid ACLs issues. """
|
||||
self.sudo().user_ids -= users
|
||||
|
||||
def action_activate(self):
|
||||
self.state = 'activated'
|
||||
@@ -111,7 +121,7 @@ class Digest(models.Model):
|
||||
lang=user.lang
|
||||
)._action_send_to_user(user, tips_count=1)
|
||||
if digest in to_slowdown:
|
||||
digest.write({'periodicity': 'weekly'})
|
||||
digest.write({'periodicity': self._get_next_periodicity()[0]})
|
||||
digest.next_run_date = digest._get_next_run_date()
|
||||
|
||||
def _action_send_to_user(self, user, tips_count=1, consum_tips=True):
|
||||
@@ -126,6 +136,7 @@ class Digest(models.Model):
|
||||
'top_button_url': self.get_base_url(),
|
||||
'company': user.company_id,
|
||||
'user': user,
|
||||
'unsubscribe_token': self._get_unsubscribe_token(user.id),
|
||||
'tips_count': tips_count,
|
||||
'formatted_date': datetime.today().strftime('%B %d, %Y'),
|
||||
'display_mobile_banner': True,
|
||||
@@ -145,14 +156,15 @@ class Digest(models.Model):
|
||||
)
|
||||
# create a mail_mail based on values, without attachments
|
||||
mail_values = {
|
||||
'subject': '%s: %s' % (user.company_id.name, self.name),
|
||||
'auto_delete': True,
|
||||
'author_id': self.env.user.partner_id.id,
|
||||
'email_from': self.company_id.partner_id.email_formatted if self.company_id else self.env.user.email_formatted,
|
||||
'email_to': user.email_formatted,
|
||||
'body_html': full_mail,
|
||||
'auto_delete': True,
|
||||
'state': 'outgoing',
|
||||
'subject': '%s: %s' % (user.company_id.name, self.name),
|
||||
}
|
||||
mail = self.env['mail.mail'].sudo().create(mail_values)
|
||||
mail.send(raise_exception=False)
|
||||
self.env['mail.mail'].sudo().create(mail_values)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
@@ -164,6 +176,14 @@ class Digest(models.Model):
|
||||
except MailDeliveryException as e:
|
||||
_logger.warning('MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.', digest.id)
|
||||
|
||||
def _get_unsubscribe_token(self, user_id):
|
||||
"""Generate a secure hash for this digest and user. It allows to
|
||||
unsubscribe from a digest while keeping some security in that process.
|
||||
|
||||
:param int user_id: ID of the user to unsubscribe
|
||||
"""
|
||||
return tools.hmac(self.env(su=True), 'digest-unsubscribe', (self.id, user_id))
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# KPIS
|
||||
# ------------------------------------------------------------
|
||||
@@ -256,7 +276,11 @@ class Digest(models.Model):
|
||||
"""
|
||||
preferences = []
|
||||
if self._context.get('digest_slowdown'):
|
||||
preferences.append(_("We have noticed you did not connect these last few days so we've automatically switched your preference to weekly Digests."))
|
||||
_dummy, new_perioridicy_str = self._get_next_periodicity()
|
||||
preferences.append(
|
||||
_("We have noticed you did not connect these last few days. We have automatically switched your preference to %(new_perioridicy_str)s Digests.",
|
||||
new_perioridicy_str=new_perioridicy_str)
|
||||
)
|
||||
elif self.periodicity == 'daily' and user.has_group('base.group_erp_manager'):
|
||||
preferences.append(Markup('<p>%s<br /><a href="%s" target="_blank" style="color:#875A7B; font-weight: bold;">%s</a></p>') % (
|
||||
_('Prefer a broader overview ?'),
|
||||
@@ -318,17 +342,34 @@ class Digest(models.Model):
|
||||
return margin
|
||||
|
||||
def _check_daily_logs(self):
|
||||
three_days_ago = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - relativedelta(days=3)
|
||||
""" Badly named method that checks user logs and slowdown the sending
|
||||
of digest emails based on recipients being away. """
|
||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
to_slowdown = self.env['digest.digest']
|
||||
for digest in self.filtered(lambda digest: digest.periodicity == 'daily'):
|
||||
for digest in self:
|
||||
if digest.periodicity == 'daily': # 3 days ago
|
||||
limit_dt = today - relativedelta(days=3)
|
||||
elif digest.periodicity == 'weekly': # 2 weeks ago
|
||||
limit_dt = today - relativedelta(days=14)
|
||||
elif digest.periodicity == 'monthly': # 1 month ago
|
||||
limit_dt = today - relativedelta(months=1)
|
||||
elif digest.periodicity == 'quarterly': # 3 month ago
|
||||
limit_dt = today - relativedelta(months=3)
|
||||
users_logs = self.env['res.users.log'].sudo().search_count([
|
||||
('create_uid', 'in', digest.user_ids.ids),
|
||||
('create_date', '>=', three_days_ago)
|
||||
('create_date', '>=', limit_dt)
|
||||
])
|
||||
if not users_logs:
|
||||
to_slowdown += digest
|
||||
return to_slowdown
|
||||
|
||||
def _get_next_periodicity(self):
|
||||
if self.periodicity == 'weekly':
|
||||
return 'monthly', _('monthly')
|
||||
if self.periodicity == 'monthly':
|
||||
return 'quarterly', _('quarterly')
|
||||
return 'weekly', _('weekly')
|
||||
|
||||
def _format_currency_amount(self, amount, currency_id):
|
||||
pre = currency_id.position == 'before'
|
||||
symbol = u'{symbol}'.format(symbol=currency_id.symbol or '')
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_digest
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from lxml import html
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import fields
|
||||
from odoo import fields, SUPERUSER_ID
|
||||
from odoo.addons.base.tests.common import HttpCaseWithUserDemo
|
||||
from odoo.addons.mail.tests import common as mail_test
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import users
|
||||
|
||||
|
||||
class TestDigest(mail_test.MailCommon):
|
||||
def test_digest_numbers(self):
|
||||
self._setup_messages()
|
||||
|
||||
digest = self.env['digest.digest'].create({
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestDigest, cls).setUpClass()
|
||||
cls._activate_multi_company()
|
||||
|
||||
# clean messages
|
||||
cls.env['mail.message'].search([
|
||||
('subtype_id', '=', cls.env.ref('mail.mt_comment').id),
|
||||
('message_type', 'in', ['comment', 'email']),
|
||||
]).unlink()
|
||||
cls._setup_messages()
|
||||
|
||||
# clean demo users so that we keep only the test users
|
||||
cls.env['res.users'].search([('login', 'in', ['demo', 'portal'])]).action_archive()
|
||||
# clean logs so that town down is activated
|
||||
cls.env['res.users.log'].search([('create_uid', 'in', (cls.user_admin + cls.user_employee).ids)]).unlink()
|
||||
|
||||
cls.test_digest = cls.env['digest.digest'].create({
|
||||
'kpi_mail_message_total': True,
|
||||
'kpi_res_users_connected': True,
|
||||
'name': "My Digest",
|
||||
'kpi_mail_message_total': True
|
||||
'periodicity': 'daily',
|
||||
})
|
||||
|
||||
digest_user = digest.with_user(self.user_employee)
|
||||
# subscribe a user so at least one mail gets sent
|
||||
digest_user.action_subscribe()
|
||||
self.assertTrue(
|
||||
digest_user.is_subscribed,
|
||||
"check the user was subscribed as action_subscribe will silently "
|
||||
"ignore subs of non-employees"
|
||||
)
|
||||
|
||||
# digest creates its mails in auto_delete mode so we need to capture
|
||||
# the formatted body during the sending process
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
self.assertEqual(len(self._mails), 1, "a mail has been created for the digest")
|
||||
body = self._mails[0]['body']
|
||||
|
||||
kpi_message_values = html.fromstring(body).xpath('//div[@data-field="kpi_mail_message_total"]//*[hasclass("kpi_value")]/text()')
|
||||
self.assertEqual(
|
||||
[t.strip() for t in kpi_message_values],
|
||||
['3', '8', '15']
|
||||
)
|
||||
|
||||
def _setup_messages(self):
|
||||
@classmethod
|
||||
def _setup_messages(cls):
|
||||
""" Remove all existing messages, then create a bunch of them on random
|
||||
partners with the correct types in correct time-bucket:
|
||||
|
||||
@@ -50,25 +52,199 @@ class TestDigest(mail_test.MailCommon):
|
||||
based around weeks and months not days), for a total of 15 in the
|
||||
previous month
|
||||
"""
|
||||
self.env['mail.message'].search([]).unlink()
|
||||
now = fields.Datetime.now()
|
||||
# regular employee can't necessarily access "private" addresses
|
||||
partners = self.env['res.partner'].search([('type', '!=', 'private')])
|
||||
partners = cls.env['res.partner'].search([('type', '!=', 'private')])
|
||||
messages = cls.env['mail.message']
|
||||
counter = itertools.count()
|
||||
|
||||
# pylint: disable=bad-whitespace
|
||||
for count, (low, high) in [
|
||||
(3, (0 * 24, 1 * 24)),
|
||||
(5, (1 * 24, 7 * 24)),
|
||||
(7, (7 * 24, 27 * 24)),
|
||||
]:
|
||||
now = fields.Datetime.now()
|
||||
for count, (low, high) in [(3, (0 * 24, 1 * 24)),
|
||||
(5, (1 * 24, 7 * 24)),
|
||||
(7, (7 * 24, 27 * 24)),
|
||||
]:
|
||||
for _ in range(count):
|
||||
create_date = now - relativedelta(hours=random.randint(low + 1, high - 1))
|
||||
random.choice(partners).message_post(
|
||||
messages += random.choice(partners).message_post(
|
||||
author_id=cls.partner_admin.id,
|
||||
body=f"Awesome Partner! ({next(counter)})",
|
||||
email_from=cls.partner_admin.email_formatted,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
# adjust top and bottom by 1h to avoid overlapping with the
|
||||
# range limit and dropping out of the digest's selection thing
|
||||
create_date=create_date
|
||||
create_date=create_date,
|
||||
)
|
||||
messages.flush()
|
||||
|
||||
@users('admin')
|
||||
def test_digest_numbers(self):
|
||||
digest = self.env['digest.digest'].browse(self.test_digest.ids)
|
||||
digest._action_subscribe_users(self.user_employee)
|
||||
|
||||
# digest creates its mails in auto_delete mode so we need to capture
|
||||
# the formatted body during the sending process
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1, "A new mail.mail should have been created")
|
||||
mail = self._new_mails[0]
|
||||
# check mail.mail content
|
||||
self.assertEqual(mail.author_id, self.partner_admin)
|
||||
self.assertEqual(mail.email_from, self.company_admin.email_formatted)
|
||||
self.assertEqual(mail.state, 'outgoing', 'Mail should use the queue')
|
||||
|
||||
kpi_message_values = html.fromstring(mail.body_html).xpath('//div[@data-field="kpi_mail_message_total"]//*[hasclass("kpi_value")]/text()')
|
||||
self.assertEqual(
|
||||
[t.strip() for t in kpi_message_values],
|
||||
['3', '8', '15']
|
||||
)
|
||||
|
||||
@users('admin')
|
||||
def test_digest_subscribe(self):
|
||||
digest_user = self.test_digest.with_user(self.user_employee)
|
||||
self.assertFalse(digest_user.is_subscribed)
|
||||
|
||||
# subscribe a user so at least one mail gets sent
|
||||
digest_user.action_subscribe()
|
||||
self.assertTrue(
|
||||
digest_user.is_subscribed,
|
||||
"check the user was subscribed as action_subscribe will silently "
|
||||
"ignore subs of non-employees"
|
||||
)
|
||||
|
||||
@users('admin')
|
||||
def test_digest_tone_down(self):
|
||||
digest = self.env['digest.digest'].browse(self.test_digest.ids)
|
||||
digest._action_subscribe_users(self.user_employee)
|
||||
|
||||
# initial data
|
||||
self.assertEqual(digest.periodicity, 'daily')
|
||||
|
||||
# no logs for employee -> should tone down periodicity
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
self.assertEqual(digest.periodicity, 'weekly')
|
||||
|
||||
# no logs for employee -> should tone down periodicity
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
self.assertEqual(digest.periodicity, 'monthly')
|
||||
|
||||
# no logs for employee -> should tone down periodicity
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
self.assertEqual(digest.periodicity, 'quarterly')
|
||||
|
||||
@users('admin')
|
||||
def test_digest_tone_down_wlogs(self):
|
||||
digest = self.env['digest.digest'].browse(self.test_digest.ids)
|
||||
digest._action_subscribe_users(self.user_employee)
|
||||
|
||||
# initial data
|
||||
self.assertEqual(digest.periodicity, 'daily')
|
||||
|
||||
# logs for employee -> should not tone down
|
||||
logs = self.env['res.users.log'].with_user(SUPERUSER_ID).create({'create_uid': self.user_employee.id})
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
|
||||
logs.unlink()
|
||||
logs = self.env['res.users.log'].with_user(SUPERUSER_ID).create({
|
||||
'create_uid': self.user_employee.id,
|
||||
'create_date': fields.Datetime.now() - relativedelta(days=20),
|
||||
})
|
||||
|
||||
# logs for employee are more than 3 days old -> should tone down
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
self.assertEqual(digest.periodicity, 'weekly')
|
||||
|
||||
# logs for employee are more than 2 weeks old -> should tone down
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
self.assertEqual(digest.periodicity, 'monthly')
|
||||
|
||||
# logs for employee are less than 1 month old -> should not tone down
|
||||
digest.flush()
|
||||
with self.mock_mail_gateway():
|
||||
digest.action_send()
|
||||
self.assertEqual(digest.periodicity, 'monthly')
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestUnsubscribe(HttpCaseWithUserDemo):
|
||||
|
||||
def setUp(self):
|
||||
super(TestUnsubscribe, self).setUp()
|
||||
|
||||
self.test_digest = self.env['digest.digest'].create({
|
||||
'kpi_mail_message_total': True,
|
||||
'kpi_res_users_connected': True,
|
||||
'name': "My Digest",
|
||||
'periodicity': 'daily',
|
||||
'user_ids': self.user_demo.ids,
|
||||
})
|
||||
self.test_digest._action_subscribe_users(self.user_demo)
|
||||
self.base_url = self.test_digest.get_base_url()
|
||||
self.user_demo_unsubscribe_token = self.test_digest._get_unsubscribe_token(self.user_demo.id)
|
||||
|
||||
@users('demo')
|
||||
def test_unsubscribe_classic(self):
|
||||
self.assertIn(self.user_demo, self.test_digest.user_ids)
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
|
||||
response = self._url_unsubscribe()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
|
||||
|
||||
@users('demo')
|
||||
def test_unsubscribe_issues(self):
|
||||
""" Test when not being member """
|
||||
self.test_digest.write({'user_ids': [(3, self.user_demo.id)]})
|
||||
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
|
||||
|
||||
# unsubscribe
|
||||
self.authenticate(self.env.user.login, self.env.user.login)
|
||||
response = self._url_unsubscribe()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
|
||||
|
||||
def test_unsubscribe_token(self):
|
||||
self.assertIn(self.user_demo, self.test_digest.user_ids)
|
||||
self.authenticate(None, None)
|
||||
response = self._url_unsubscribe(token=self.user_demo_unsubscribe_token, user_id=self.user_demo.id)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.test_digest.invalidate_cache()
|
||||
self.assertNotIn(self.user_demo, self.test_digest.user_ids)
|
||||
|
||||
def test_unsubscribe_public(self):
|
||||
""" Check public users are redirected when trying to catch unsubscribe
|
||||
route. """
|
||||
self.authenticate(None, None)
|
||||
|
||||
response = self._url_unsubscribe()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def _url_unsubscribe(self, token=None, user_id=None):
|
||||
url_params = {}
|
||||
if token is not None:
|
||||
url_params['token'] = token
|
||||
if user_id is not None:
|
||||
url_params['user_id'] = user_id
|
||||
|
||||
url = "%s/digest/%s/unsubscribe?%s" % (
|
||||
self.base_url,
|
||||
self.test_digest.id,
|
||||
url_encode(url_params)
|
||||
)
|
||||
return self.url_open(url)
|
||||
|
||||
@@ -931,10 +931,15 @@ class MassMailing(models.Model):
|
||||
"""Send an email to the responsible of each finished mailing with the statistics."""
|
||||
self.kpi_mail_required = False
|
||||
|
||||
mails_sudo = self.env['mail.mail'].sudo()
|
||||
for mailing in self:
|
||||
user = mailing.user_id
|
||||
mailing = mailing.with_context(lang=user.lang or self._context.get('lang'))
|
||||
if mailing.user_id:
|
||||
mailing = mailing.with_user(mailing.user_id).with_context(
|
||||
lang=mailing.user_id.lang or self._context.get('lang')
|
||||
)
|
||||
mailing_type = mailing._get_pretty_mailing_type()
|
||||
mail_user = mailing.user_id or self.env.user
|
||||
mail_company = mail_user.company_id
|
||||
|
||||
link_trackers = self.env['link.tracker'].search(
|
||||
[('mass_mailing_id', '=', mailing.id)]
|
||||
@@ -952,8 +957,8 @@ class MassMailing(models.Model):
|
||||
'digest.digest_mail_main',
|
||||
{
|
||||
'body': tools.html_sanitize(link_trackers_body),
|
||||
'company': user.company_id,
|
||||
'user': user,
|
||||
'company': mail_company,
|
||||
'user': mail_user,
|
||||
'display_mobile_banner': True,
|
||||
** mailing._prepare_statistics_email_values()
|
||||
},
|
||||
@@ -965,17 +970,20 @@ class MassMailing(models.Model):
|
||||
)
|
||||
|
||||
mail_values = {
|
||||
'auto_delete': True,
|
||||
'author_id': mail_user.partner_id.id,
|
||||
'email_from': mail_user.email_formatted,
|
||||
'email_to': mail_user.email_formatted,
|
||||
'body_html': full_mail,
|
||||
'reply_to': mail_company.email_formatted or mail_user.email_formatted,
|
||||
'state': 'outgoing',
|
||||
'subject': _('24H Stats of %(mailing_type)s "%(mailing_name)s"',
|
||||
mailing_type=mailing._get_pretty_mailing_type(),
|
||||
mailing_name=mailing.subject
|
||||
),
|
||||
'email_from': user.email_formatted,
|
||||
'email_to': user.email_formatted,
|
||||
'body_html': full_mail,
|
||||
'auto_delete': True,
|
||||
}
|
||||
mail = self.env['mail.mail'].sudo().create(mail_values)
|
||||
mail.send(raise_exception=False)
|
||||
mails_sudo += self.env['mail.mail'].sudo().create(mail_values)
|
||||
return mails_sudo
|
||||
|
||||
def _prepare_statistics_email_values(self):
|
||||
"""Return some statistics that will be displayed in the mailing statistics email.
|
||||
@@ -1005,6 +1013,7 @@ class MassMailing(models.Model):
|
||||
'col_subtitle': _('REPLIED (%i)', self.replied),
|
||||
},
|
||||
'kpi_action': None,
|
||||
'kpi_name': self.mailing_type,
|
||||
}
|
||||
|
||||
random_tip = self.env['digest.tip'].search(
|
||||
@@ -1037,6 +1046,7 @@ class MassMailing(models.Model):
|
||||
'kpi_col1': {},
|
||||
'kpi_col2': {},
|
||||
'kpi_col3': {},
|
||||
'kpi_name': 'trace',
|
||||
},
|
||||
],
|
||||
'tips': [random_tip] if random_tip else False,
|
||||
|
||||
@@ -131,8 +131,11 @@ class MailingTrace(models.Model):
|
||||
return traces
|
||||
|
||||
def set_opened(self, domain=None):
|
||||
""" Reply / Open are a bit shared in various processes: reply implies
|
||||
open, click implies open. Let us avoid status override by skipping traces
|
||||
that are not already opened or replied. """
|
||||
traces = self + (self.search(domain) if domain else self.env['mailing.trace'])
|
||||
traces.write({'trace_status': 'open', 'open_datetime': fields.Datetime.now()})
|
||||
traces.filtered(lambda t: t.trace_status not in ('open', 'reply')).write({'trace_status': 'open', 'open_datetime': fields.Datetime.now()})
|
||||
return traces
|
||||
|
||||
def set_clicked(self, domain=None):
|
||||
|
||||
@@ -40,4 +40,5 @@ class MassMailing(models.Model):
|
||||
'value': tools.format_decimalized_number(self.crm_lead_count, decimal=0),
|
||||
'col_subtitle': _('LEADS'),
|
||||
}
|
||||
values['kpi_data'][1]['kpi_name'] = 'lead'
|
||||
return values
|
||||
|
||||
@@ -87,4 +87,5 @@ class MassMailing(models.Model):
|
||||
'value': formated_amount,
|
||||
'col_subtitle': _('INVOICED'),
|
||||
}
|
||||
values['kpi_data'][1]['kpi_name'] = 'sale'
|
||||
return values
|
||||
|
||||
@@ -291,6 +291,7 @@ class Mailing(models.Model):
|
||||
'col_subtitle': _('BOUNCED (%i)', self.bounced),
|
||||
},
|
||||
'kpi_action': None,
|
||||
'kpi_name': self.mailing_type,
|
||||
}
|
||||
return values
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import random
|
||||
import re
|
||||
import werkzeug
|
||||
|
||||
@@ -147,7 +149,7 @@ class MassSMSCase(SMSCase):
|
||||
|
||||
self.env['link.tracker.click'].sudo().add_click(
|
||||
code,
|
||||
ip='100.200.300.400',
|
||||
ip='100.200.300.%3f' % random.random(),
|
||||
country_code='BE',
|
||||
mailing_trace_id=trace_id
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from . import common
|
||||
from . import test_odoobot
|
||||
from . import test_phone_blacklist
|
||||
from . import test_mass_mailing
|
||||
from . import test_mass_mailing_statistics
|
||||
from . import test_mass_sms
|
||||
from . import test_sms_composer
|
||||
from . import test_sms_management
|
||||
|
||||
@@ -18,8 +18,8 @@ class TestMailFullCommon(TestMassMailCommon, MassSMSCommon):
|
||||
'subject': 'Xmas SMS for {object.name}',
|
||||
'mailing_model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||
'mailing_type': 'sms',
|
||||
'mailing_domain': '%s' % repr([('name', 'ilike', 'SMSTest')]),
|
||||
'body_plaintext': 'Dear {{ object.display_name }} this is a mass SMS with two links http://www.odoo.com/smstest and http://www.odoo.com/smstest/{{ object.id }}',
|
||||
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
|
||||
'body_plaintext': 'Dear {{object.display_name}} this is a mass SMS with two links http://www.odoo.com/smstest and http://www.odoo.com/smstest/{{object.id}}',
|
||||
'sms_force_send': True,
|
||||
'sms_allow_unsubscribe': True,
|
||||
})
|
||||
@@ -34,7 +34,7 @@ class TestMailFullCommon(TestMassMailCommon, MassSMSCommon):
|
||||
vals_list = []
|
||||
for idx in range(count):
|
||||
vals = {
|
||||
'name': 'SMSTestRecord_%02d' % idx,
|
||||
'name': 'MassSMSTestRecord_%02d' % idx,
|
||||
phone_field: '045600%02d%02d' % (idx, idx)
|
||||
}
|
||||
if partners:
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml import html
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mail_full.tests.test_mass_sms import TestMassSMSCommon
|
||||
from odoo.tests.common import users
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('digest', 'mass_mailing')
|
||||
class TestMailingStatistics(TestMassSMSCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingStatistics, cls).setUpClass()
|
||||
|
||||
cls.user_marketing_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
|
||||
login='user_marketing_2',
|
||||
name='Marie Marketing',
|
||||
signature='--\nMarie'
|
||||
)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mass_mailing_sms.models.mailing_mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
|
||||
def test_mailing_statistics_sms(self):
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
|
||||
target_records = self.env['mail.test.sms'].browse(self.records.ids)
|
||||
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
|
||||
mailing.action_put_in_queue()
|
||||
with self.mockSMSGateway():
|
||||
mailing.action_send_sms()
|
||||
|
||||
# simulate some replies and clicks
|
||||
self.gateway_sms_click(mailing, target_records[0])
|
||||
self.gateway_sms_click(mailing, target_records[2])
|
||||
self.gateway_sms_click(mailing, target_records[3])
|
||||
|
||||
# check mailing statistics
|
||||
self.assertEqual(mailing.clicked, 3)
|
||||
self.assertEqual(mailing.delivered, 10)
|
||||
self.assertEqual(mailing.opened, 3)
|
||||
self.assertEqual(mailing.opened_ratio, 30)
|
||||
self.assertEqual(mailing.sent, 10)
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing._action_send_statistics()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
|
||||
mail = self._new_mails[0]
|
||||
# test email values
|
||||
self.assertEqual(mail.author_id, self.user_marketing_2.partner_id)
|
||||
self.assertEqual(mail.email_from, self.user_marketing_2.email_formatted)
|
||||
self.assertEqual(mail.email_to, self.user_marketing_2.email_formatted)
|
||||
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
# test body content: KPIs
|
||||
body_html = html.fromstring(mail.body_html)
|
||||
kpi_values = body_html.xpath('//div[@data-field="sms"]//*[hasclass("kpi_value")]/text()')
|
||||
self.assertEqual(
|
||||
[t.strip().strip('%') for t in kpi_values],
|
||||
['100', str(mailing.opened_ratio), str(mailing.replied_ratio)]
|
||||
)
|
||||
# test body content: clicks (a bit hackish but hey we are in stable)
|
||||
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
|
||||
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
|
||||
self.assertEqual(first_link_value, mailing.clicked)
|
||||
@@ -51,16 +51,6 @@ class TestMassSMSCommon(TestMailFullCommon):
|
||||
for partner in partners
|
||||
]
|
||||
|
||||
cls.mailing = cls.env['mailing.mailing'].with_user(cls.user_marketing).create({
|
||||
'name': 'Xmas Spam',
|
||||
'subject': 'Xmas Spam',
|
||||
'mailing_model_id': cls.env['ir.model']._get('mail.test.sms').id,
|
||||
'mailing_type': 'sms',
|
||||
'mailing_domain': '%s' % repr([('name', 'ilike', 'MassSMSTest')]),
|
||||
'sms_template_id': cls.sms_template.id,
|
||||
'sms_allow_unsubscribe': False,
|
||||
})
|
||||
|
||||
|
||||
@tagged('mass_mailing')
|
||||
class TestMassSMSInternals(TestMassSMSCommon):
|
||||
@@ -114,6 +104,7 @@ class TestMassSMSInternals(TestMassSMSCommon):
|
||||
mailing, self.records,
|
||||
)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mass_sms_internals_errors(self):
|
||||
# same customer, specific different number on record -> should be valid
|
||||
new_record_1 = self.env['mail.test.sms'].create({
|
||||
@@ -141,7 +132,7 @@ class TestMassSMSInternals(TestMassSMSCommon):
|
||||
'customer_id': False,
|
||||
'phone_nbr': '0456110011',
|
||||
})
|
||||
self.env['phone.blacklist'].create({'number': '0456110011'})
|
||||
self.env['phone.blacklist'].sudo().create({'number': '0456110011'})
|
||||
# new customer, number already on record -> should be ignored
|
||||
country_be_id = self.env.ref('base.be').id
|
||||
nr2_partner = self.env['res.partner'].create({
|
||||
@@ -156,81 +147,87 @@ class TestMassSMSInternals(TestMassSMSCommon):
|
||||
})
|
||||
records_numbers = self.records_numbers + ['+32456999999']
|
||||
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
|
||||
mailing.write({'sms_force_send': False}) # force outgoing sms, not sent
|
||||
with self.with_user('user_marketing'):
|
||||
with self.mockSMSGateway():
|
||||
self.mailing.action_send_sms()
|
||||
mailing.action_send_sms()
|
||||
|
||||
self.assertSMSTraces(
|
||||
[{'partner': record.customer_id, 'number': records_numbers[i],
|
||||
'content': 'Dear %s this is a mass SMS.' % record.display_name}
|
||||
'content': 'Dear %s this is a mass SMS' % record.display_name}
|
||||
for i, record in enumerate(self.records | new_record_1)],
|
||||
self.mailing, self.records | new_record_1,
|
||||
mailing, self.records | new_record_1,
|
||||
)
|
||||
# duplicates
|
||||
self.assertSMSTraces(
|
||||
[{'partner': new_record_2.customer_id, 'number': self.records_numbers[0],
|
||||
'content': 'Dear %s this is a mass SMS.' % new_record_2.display_name, 'trace_status': 'cancel',
|
||||
'content': 'Dear %s this is a mass SMS' % new_record_2.display_name, 'trace_status': 'cancel',
|
||||
'failure_type': 'sms_duplicate'}],
|
||||
self.mailing, new_record_2,
|
||||
mailing, new_record_2,
|
||||
)
|
||||
# blacklist
|
||||
self.assertSMSTraces(
|
||||
[{'partner': self.env['res.partner'], 'number': phone_validation.phone_format(bl_record_1.phone_nbr, 'BE', '32', force_format='E164'),
|
||||
'content': 'Dear %s this is a mass SMS.' % bl_record_1.display_name, 'trace_status': 'cancel',
|
||||
'content': 'Dear %s this is a mass SMS' % bl_record_1.display_name, 'trace_status': 'cancel',
|
||||
'failure_type': 'sms_blacklist'}],
|
||||
self.mailing, bl_record_1,
|
||||
mailing, bl_record_1,
|
||||
)
|
||||
# missing number
|
||||
self.assertSMSTraces(
|
||||
[{'partner': self.env['res.partner'], 'number': False,
|
||||
'content': 'Dear %s this is a mass SMS.' % void_record.display_name, 'trace_status': 'cancel',
|
||||
'content': 'Dear %s this is a mass SMS' % void_record.display_name, 'trace_status': 'cancel',
|
||||
'failure_type': 'sms_number_missing'}],
|
||||
self.mailing, void_record,
|
||||
mailing, void_record,
|
||||
)
|
||||
# wrong values
|
||||
self.assertSMSTraces(
|
||||
[{'partner': self.env['res.partner'], 'number': record.phone_nbr,
|
||||
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'trace_status': 'cancel',
|
||||
'content': 'Dear %s this is a mass SMS' % record.display_name, 'trace_status': 'cancel',
|
||||
'failure_type': 'sms_number_format'}
|
||||
for record in falsy_record_1 + falsy_record_2],
|
||||
self.mailing, falsy_record_1 + falsy_record_2,
|
||||
mailing, falsy_record_1 + falsy_record_2,
|
||||
)
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mass_sms_internals_done_ids(self):
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
|
||||
mailing.write({'sms_force_send': False}) # check with outgoing traces, not already sent
|
||||
|
||||
with self.with_user('user_marketing'):
|
||||
with self.mockSMSGateway():
|
||||
self.mailing.action_send_sms(res_ids=self.records[:5].ids)
|
||||
mailing.action_send_sms(res_ids=self.records[:5].ids)
|
||||
|
||||
traces = self.env['mailing.trace'].search([('mass_mailing_id', 'in', self.mailing.ids)])
|
||||
traces = self.env['mailing.trace'].search([('mass_mailing_id', 'in', mailing.ids)])
|
||||
self.assertEqual(len(traces), 5)
|
||||
# new traces generated
|
||||
self.assertSMSTraces(
|
||||
[{'partner': record.customer_id, 'number': self.records_numbers[i],
|
||||
'content': 'Dear %s this is a mass SMS.' % record.display_name}
|
||||
'content': 'Dear %s this is a mass SMS' % record.display_name}
|
||||
for i, record in enumerate(self.records[:5])],
|
||||
self.mailing, self.records[:5],
|
||||
mailing, self.records[:5],
|
||||
)
|
||||
|
||||
with self.with_user('user_marketing'):
|
||||
with self.mockSMSGateway():
|
||||
self.mailing.action_send_sms(res_ids=self.records.ids)
|
||||
mailing.action_send_sms(res_ids=self.records.ids)
|
||||
|
||||
# delete old traces (for testing purpose: ease check by deleting old ones)
|
||||
traces.unlink()
|
||||
# new failed traces generated for duplicates
|
||||
self.assertSMSTraces(
|
||||
[{'partner': record.customer_id, 'number': self.records_numbers[i],
|
||||
'content': 'Dear %s this is a mass SMS.' % record.display_name, 'trace_status': 'cancel',
|
||||
'content': 'Dear %s this is a mass SMS' % record.display_name, 'trace_status': 'cancel',
|
||||
'failure_type': 'sms_duplicate'}
|
||||
for i, record in enumerate(self.records[:5])],
|
||||
self.mailing, self.records[:5],
|
||||
mailing, self.records[:5],
|
||||
)
|
||||
# new traces generated
|
||||
self.assertSMSTraces(
|
||||
[{'partner': record.customer_id, 'number': self.records_numbers[i+5],
|
||||
'content': 'Dear %s this is a mass SMS.' % record.display_name}
|
||||
'content': 'Dear %s this is a mass SMS' % record.display_name}
|
||||
for i, record in enumerate(self.records[5:])],
|
||||
self.mailing, self.records[5:],
|
||||
mailing, self.records[5:],
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_render_mixin')
|
||||
@@ -268,7 +265,7 @@ class TestMassSMS(TestMassSMSCommon):
|
||||
|
||||
@users('user_marketing')
|
||||
def test_mass_sms_links(self):
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing.ids)
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_sms.ids)
|
||||
mailing.write({
|
||||
'body_plaintext': 'Dear {{ object.display_name }} this is a mass SMS with two links http://www.odoo.com/smstest and http://www.odoo.com/smstest/{{ object.name }}',
|
||||
'sms_template_id': False,
|
||||
@@ -305,7 +302,7 @@ class TestMassSMS(TestMassSMSCommon):
|
||||
})
|
||||
|
||||
records = self.env['mail.test.sms.partner'].create([
|
||||
{'name': 'SMSTest on %s' % partner.name,
|
||||
{'name': 'MassSMSTest on %s' % partner.name,
|
||||
'customer_id': partner.id,
|
||||
} for partner in self.partners
|
||||
])
|
||||
@@ -365,7 +362,7 @@ class TestMassSMS(TestMassSMSCommon):
|
||||
})
|
||||
|
||||
self.env['mail.test.sms.partner.2many'].create([
|
||||
{'name': 'SMSTest on %s' % partner.name,
|
||||
{'name': 'MassSMSTest on %s' % partner.name,
|
||||
'customer_ids': [(4, partner.id)],
|
||||
} for partner in self.partners
|
||||
])
|
||||
|
||||
@@ -7,5 +7,6 @@ from . import test_blacklist_mixin
|
||||
from . import test_link_tracker
|
||||
from . import test_mailing
|
||||
from . import test_mailing_server
|
||||
from . import test_mailing_statistics
|
||||
from . import test_mailing_test
|
||||
from . import test_performance
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml import html
|
||||
|
||||
from odoo.addons.mail.tests.common import mail_new_test_user
|
||||
from odoo.addons.test_mass_mailing.data.mail_test_data import MAIL_TEMPLATE
|
||||
from odoo.addons.test_mass_mailing.tests.common import TestMassMailCommon
|
||||
from odoo.tests.common import users
|
||||
from odoo.tests import tagged
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
|
||||
@tagged('digest')
|
||||
class TestMailingStatistics(TestMassMailCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestMailingStatistics, cls).setUpClass()
|
||||
|
||||
cls.user_marketing_2 = mail_new_test_user(
|
||||
cls.env,
|
||||
groups='base.group_user,base.group_partner_manager,mass_mailing.group_mass_mailing_user',
|
||||
login='user_marketing_2',
|
||||
name='Marie Marketing',
|
||||
signature='--\nMarie'
|
||||
)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mass_mailing.models.mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
|
||||
def test_mailing_statistics(self):
|
||||
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=10)
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
|
||||
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': self.user_marketing_2.id})
|
||||
mailing.action_put_in_queue()
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
# simulate some replies and clicks
|
||||
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[0], use_in_reply_to=True)
|
||||
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[1], use_in_reply_to=True)
|
||||
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, target_records[2], use_in_reply_to=True)
|
||||
self.gateway_mail_click(mailing, target_records[0], 'https://www.odoo.be')
|
||||
self.gateway_mail_click(mailing, target_records[2], 'https://www.odoo.be')
|
||||
self.gateway_mail_click(mailing, target_records[3], 'https://www.odoo.be')
|
||||
|
||||
# check mailing statistics
|
||||
self.assertEqual(mailing.clicked, 3)
|
||||
self.assertEqual(mailing.delivered, 10)
|
||||
self.assertEqual(mailing.opened, 4)
|
||||
self.assertEqual(mailing.opened_ratio, 40)
|
||||
self.assertEqual(mailing.replied, 3)
|
||||
self.assertEqual(mailing.replied_ratio, 30)
|
||||
self.assertEqual(mailing.sent, 10)
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=True):
|
||||
mailing._action_send_statistics()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
|
||||
mail = self._new_mails[0]
|
||||
# test email values
|
||||
self.assertEqual(mail.author_id, self.user_marketing_2.partner_id)
|
||||
self.assertEqual(mail.email_from, self.user_marketing_2.email_formatted)
|
||||
self.assertEqual(mail.email_to, self.user_marketing_2.email_formatted)
|
||||
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
# test body content: KPIs
|
||||
body_html = html.fromstring(mail.body_html)
|
||||
kpi_values = body_html.xpath('//div[@data-field="mail"]//*[hasclass("kpi_value")]/text()')
|
||||
self.assertEqual(
|
||||
[t.strip().strip('%') for t in kpi_values],
|
||||
['100', str(mailing.opened_ratio), str(mailing.replied_ratio)]
|
||||
)
|
||||
# test body content: clicks (a bit hackish but hey we are in stable)
|
||||
kpi_click_values = body_html.xpath('//div[hasclass("global_layout")]/table//tr[contains(@style,"color: #888888")]/td[contains(@style,"width: 30%")]/text()')
|
||||
first_link_value = int(kpi_click_values[0].strip().split()[1].strip('()'))
|
||||
self.assertEqual(first_link_value, mailing.clicked)
|
||||
|
||||
@users('user_marketing')
|
||||
@mute_logger('odoo.addons.mass_mailing.models.mailing', 'odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
|
||||
def test_mailing_statistics_wo_user(self):
|
||||
target_records = self._create_mailing_test_records(model='mailing.test.blacklist', count=10)
|
||||
mailing = self.env['mailing.mailing'].browse(self.mailing_bl.ids)
|
||||
mailing.write({'mailing_domain': [('id', 'in', target_records.ids)], 'user_id': False})
|
||||
mailing.action_put_in_queue()
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing.action_send_mail()
|
||||
|
||||
with self.mock_mail_gateway(mail_unlink_sent=False):
|
||||
mailing._action_send_statistics()
|
||||
|
||||
self.assertEqual(len(self._new_mails), 1, "Mailing: a mail should have been created for statistics")
|
||||
mail = self._new_mails[0]
|
||||
# test email values
|
||||
self.assertEqual(mail.author_id, self.user_marketing.partner_id)
|
||||
self.assertEqual(mail.email_from, self.user_marketing.email_formatted)
|
||||
self.assertEqual(mail.email_to, self.user_marketing.email_formatted)
|
||||
self.assertEqual(mail.reply_to, self.company_admin.partner_id.email_formatted)
|
||||
self.assertEqual(mail.state, 'outgoing')
|
||||
Reference in New Issue
Block a user