summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstd-odoo <std@odoo.com>2020-04-17 07:37:45 +0000
committerDebauche Stéphane <std@odoo.com>2020-06-12 11:49:45 +0000
commitb16843230d9c62606550c061934f319fee23d3c5 (patch)
tree2b7608860889186fffa902ae2cf9cbb466d4281f
parent5227f956da1b64293f3e1301ff03b4efb28db80b (diff)
[IMP] mass_mailing: send a statistics email to the mailing responsible
Purpose ======= Provide a follow-up/overview of the Mailing 24 hours after its been sent. Specifications ============== Send an email to the responsible of the mailing 24 hours the last email of the mass mailing. During the link trackers creation, extract the button label if exists (so we can display them in the statistics email). Technical remarks ================= The statistics email is sent to the responsible of each mailing in the CRON of mass mailing. Task-2227411 closes odoo/odoo#49836 Related: odoo/upgrade#1094 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
-rw-r--r--addons/digest/data/digest_data.xml26
-rw-r--r--addons/digest/models/digest.py17
-rw-r--r--addons/link_tracker/__manifest__.py2
-rw-r--r--addons/link_tracker/models/link_tracker.py20
-rw-r--r--addons/link_tracker/models/mail_render_mixin.py5
-rw-r--r--addons/link_tracker/tests/__init__.py1
-rw-r--r--addons/link_tracker/tests/test_mail_render_mixin.py52
-rw-r--r--addons/link_tracker/views/link_tracker_views.xml3
-rw-r--r--addons/mass_mailing/__manifest__.py2
-rw-r--r--addons/mass_mailing/data/mailing_data_templates.xml46
-rw-r--r--addons/mass_mailing/models/mailing.py124
-rw-r--r--addons/mass_mailing_crm/models/mailing_mailing.py13
-rw-r--r--addons/mass_mailing_sale/models/mailing_mailing.py22
-rw-r--r--odoo/tools/mail.py2
-rw-r--r--odoo/tools/misc.py40
15 files changed, 345 insertions, 30 deletions
diff --git a/addons/digest/data/digest_data.xml b/addons/digest/data/digest_data.xml
index d4f29288418..30a8316f469 100644
--- a/addons/digest/data/digest_data.xml
+++ b/addons/digest/data/digest_data.xml
@@ -69,11 +69,14 @@
<td class="mobile-w-75" style="text-align: left;">
<span class="font-16" style="color:#8F8F8F;font-weight:bold;font-size: 20px; line-height: 30px;" t-field="company.name"/>
<br/>
- <span class="font-18" style="color:#282f33;font-weight:bold;font-size: 25px; line-height: 35px; margin-right: 5px;" t-field="object.name"/>
- <span class="d-block font-12" style="color:#8F8F8F;font-size: 15px;"><t t-esc="datetime.today().strftime('%B %d, %Y')"/></span>
+ <div class="font-18" style="color:#282f33;font-weight:bold;font-size: 25px; line-height: 35px; margin-right: 5px; display: inline-block; max-width: 500px;word-break: break-all;" t-esc="title"/>
+ <div t-if="sub_title" class="font-18" style="color:#282f33;font-weight:bold;font-size: 25px; line-height: 35px; margin-right: 5px; display: inline-block; max-width: 500px;word-break: break-all;" t-esc="sub_title"/>
+ <div class="font-12" style="color:#8F8F8F;font-size: 15px;display: inline-block;"><t t-esc="formatted_date"/></div>
</td>
<td valign="top" style="text-align:right;">
- <a href="/web/login" target="_blank" style="display:block;background-color:#875a7b;line-height:40px;text-decoration: none;color: #ffffff;border-radius: 3px;font-size:13px;font-weight:bold;text-align:center;">Connect</a>
+ <a t-att-href="top_button_url" target="_blank" style="text-align: center; color: #fff; background-color: #9a5b82; border-radius: 8px; font-size: 15px; padding: 5px 10px; display: inline-block; text-decoration: none; min-width: 65px;">
+ <span t-esc="top_button_label"/>
+ </a>
</td>
</tr>
</table>
@@ -107,7 +110,7 @@
<td align="center" valign="top">
<table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee; border-bottom: none;width: 100%; max-width: 650px; padding:30px 30px">
<tr t-foreach="kpi_data" t-as="kpi_info">
- <td>
+ <td t-if="kpi_info.get('kpi_col1') or kpi_info.get('kpi_col2') or kpi_info.get('kpi_col3')">
<table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
<tr>
<td align="left" style="border-bottom: 1px solid #eeeeee;">
@@ -122,24 +125,24 @@
<td>
<table cellspacing="0" cellpadding="0" align="center" border="0" style="width:100%; padding: 20px 0;">
<tr valign="top">
- <td class="mobile-w-33" width="196" style=" text-align: center;">
+ <td t-if="kpi_info.get('kpi_col1')" class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col1']['value']"/>
- <t t-set="kpi_margin" t-value="kpi_info['kpi_col1']['margin']"/>
+ <t t-set="kpi_margin" t-value="kpi_info['kpi_col1'].get('margin')"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col1']['col_subtitle']"/>
</t>
</td>
- <td class="mobile-w-33" width="196" style=" text-align: center;">
+ <td t-if="kpi_info.get('kpi_col2')" class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col2']['value']"/>
- <t t-set="kpi_margin" t-value="kpi_info['kpi_col2']['margin']"/>
+ <t t-set="kpi_margin" t-value="kpi_info['kpi_col2'].get('margin')"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col2']['col_subtitle']"/>
</t>
</td>
- <td class="mobile-w-33" width="196" style=" text-align: center;">
+ <td t-if="kpi_info.get('kpi_col3')" class="mobile-w-33" width="196" style=" text-align: center;">
<t t-call="digest.digest_tool_kpi">
<t t-set="kpi_value" t-value="kpi_info['kpi_col3']['value']"/>
- <t t-set="kpi_margin" t-value="kpi_info['kpi_col3']['margin']"/>
+ <t t-set="kpi_margin" t-value="kpi_info['kpi_col3'].get('margin')"/>
<t t-set="kpi_subtitle" t-value="kpi_info['kpi_col3']['col_subtitle']"/>
</t>
</td>
@@ -163,6 +166,7 @@
</td>
</tr>
</table>
+ <t t-if="body" t-raw="body"/>
<table t-if="display_mobile_banner" cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
<tr>
<td align="center" valign="top">
@@ -228,7 +232,7 @@
<span style="color:#888888;display: inline-block; font-size: 12px; line-height: 18px; text-transform: uppercase;">
<t t-esc="kpi_subtitle"/>
</span>
-<t t-if="kpi_margin != 0.0">
+<t t-if="kpi_margin">
<t t-if="kpi_margin &gt; 0.0">
<div style="background-color:#d5f1e2; border: 1px solid #c4ecd7; margin: 10px auto;width: 67px; line-height:27px; text-decoration: none; color: #17613a; border-radius: 5px; font-size:13px;">
▲ <t t-esc="'%.2f' % kpi_margin"/>
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py
index c6e51a4fb9a..b9ff6bf0829 100644
--- a/addons/digest/models/digest.py
+++ b/addons/digest/models/digest.py
@@ -6,6 +6,7 @@ import pytz
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
+from werkzeug.urls import url_join
from odoo import api, fields, models, tools, _
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
@@ -112,16 +113,21 @@ class Digest(models.Model):
digest.next_run_date = digest._get_next_run_date()
def _action_send_to_user(self, user, tips_count=1):
+ web_base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
rendered_body = self.env['mail.render.mixin']._render_template(
'digest.digest_mail_main',
'digest.digest',
self.ids,
engine='qweb',
add_context={
+ 'title': self.name,
+ 'top_button_label': _('Connect'),
+ 'top_button_url': url_join(web_base_url, '/web/login'),
'company': user.company_id,
'user': user,
'tips_count': tips_count,
- 'datetime': datetime, # TDE TEMP FIXME
+ 'formatted_date': datetime.today().strftime('%B %d, %Y'),
'display_mobile_banner': True,
'kpi_data': self.compute_kpis(user.company_id, user),
'tips': self.compute_tips(user.company_id, user, tips_count=tips_count),
@@ -211,7 +217,7 @@ class Digest(models.Model):
continue
margin = self._get_margin_value(compute_value, previous_value)
if self._fields['%s_value' % field_name].type == 'monetary':
- converted_amount = self._format_human_readable_amount(compute_value)
+ converted_amount = tools.format_decimalized_amount(compute_value)
compute_value = self._format_currency_amount(converted_amount, company.currency_id)
kpi_values['kpi_col%s' % (col_index + 1)].update({
'value': compute_value,
@@ -328,10 +334,3 @@ class Digest(models.Model):
pre = currency_id.position == 'before'
symbol = u'{symbol}'.format(symbol=currency_id.symbol or '')
return u'{pre}{0}{post}'.format(amount, pre=symbol if pre else '', post=symbol if not pre else '')
-
- def _format_human_readable_amount(self, amount, suffix=''):
- for unit in ['', 'K', 'M', 'G']:
- if abs(amount) < 1000.0:
- return "%3.2f%s%s" % (amount, unit, suffix)
- amount /= 1000.0
- return "%.2f%s%s" % (amount, 'T', suffix)
diff --git a/addons/link_tracker/__manifest__.py b/addons/link_tracker/__manifest__.py
index 038fe807173..73bb6a78b1a 100644
--- a/addons/link_tracker/__manifest__.py
+++ b/addons/link_tracker/__manifest__.py
@@ -7,7 +7,7 @@
'description': """
Shorten URLs and use them to track clicks and UTMs
""",
- 'version': '1.0',
+ 'version': '1.1',
'depends': ['utm', 'mail'],
'data': [
'views/link_tracker_views.xml',
diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py
index 925c613c0c6..2ee2fad31ab 100644
--- a/addons/link_tracker/models/link_tracker.py
+++ b/addons/link_tracker/models/link_tracker.py
@@ -27,16 +27,28 @@ class LinkTracker(models.Model):
# URL info
url = fields.Char(string='Target URL', required=True)
+ absolute_url = fields.Char("Absolute URL", compute="_compute_absolute_url")
short_url = fields.Char(string='Tracked URL', compute='_compute_short_url')
redirected_url = fields.Char(string='Redirected URL', compute='_compute_redirected_url')
short_url_host = fields.Char(string='Host of the short URL', compute='_compute_short_url_host')
title = fields.Char(string='Page Title', store=True)
+ label = fields.Char(string='Button label')
# Tracking
link_code_ids = fields.One2many('link.tracker.code', 'link_id', string='Codes')
code = fields.Char(string='Short URL code', compute='_compute_code')
link_click_ids = fields.One2many('link.tracker.click', 'link_id', string='Clicks')
count = fields.Integer(string='Number of Clicks', compute='_compute_count', store=True)
+ @api.depends("url")
+ def _compute_absolute_url(self):
+ web_base_url = urls.url_parse(self.env['ir.config_parameter'].sudo().get_param('web.base.url'))
+ for tracker in self:
+ url = urls.url_parse(tracker.url)
+ if url.scheme:
+ tracker.absolute_url = tracker.url
+ else:
+ tracker.absolute_url = web_base_url.join(url).to_url()
+
@api.depends('link_click_ids.link_id')
def _compute_count(self):
if self.ids:
@@ -99,9 +111,11 @@ class LinkTracker(models.Model):
else:
create_vals['url'] = tools.validate_url(vals['url'])
- search_domain = []
- for fname, value in create_vals.items():
- search_domain.append((fname, '=', value))
+ search_domain = [
+ (fname, '=', value)
+ for fname, value in create_vals.items()
+ if fname in ['url', 'campaign_id', 'medium_id', 'source_id']
+ ]
result = self.search(search_domain, limit=1)
diff --git a/addons/link_tracker/models/mail_render_mixin.py b/addons/link_tracker/models/mail_render_mixin.py
index 0bc30de960b..c848f3979e1 100644
--- a/addons/link_tracker/models/mail_render_mixin.py
+++ b/addons/link_tracker/models/mail_render_mixin.py
@@ -27,12 +27,13 @@ class MailRenderMixin(models.AbstractModel):
"""
base_url = base_url or self.env['ir.config_parameter'].sudo().get_param('web.base.url')
short_schema = base_url + '/r/'
- for match in re.findall(tools.URL_REGEX, html):
+ for match in re.findall(tools.HTML_TAG_URL_REGEX, html):
href = match[0]
long_url = match[1]
+ label = (match[3] or '').strip()
if not blacklist or not [s for s in blacklist if s in long_url] and not long_url.startswith(short_schema):
- create_vals = dict(link_tracker_vals, url=utils.unescape(long_url))
+ create_vals = dict(link_tracker_vals, url=utils.unescape(long_url), label=utils.unescape(label))
link = self.env['link.tracker'].create(create_vals)
if link.short_url:
new_href = href.replace(long_url, link.short_url)
diff --git a/addons/link_tracker/tests/__init__.py b/addons/link_tracker/tests/__init__.py
index 511e649c9e2..e8c7d8453ad 100644
--- a/addons/link_tracker/tests/__init__.py
+++ b/addons/link_tracker/tests/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
+from . import test_mail_render_mixin
diff --git a/addons/link_tracker/tests/test_mail_render_mixin.py b/addons/link_tracker/tests/test_mail_render_mixin.py
new file mode 100644
index 00000000000..23a74b5aae0
--- /dev/null
+++ b/addons/link_tracker/tests/test_mail_render_mixin.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import common
+
+
+class TestMailRenderMixin(common.TransactionCase):
+ def test_shorten_links(self):
+ test_links = [
+ '<a href="https://gitlab.com" title="title" fake="fake">test_label</a>',
+ '<a href="https://test_542152qsdqsd.com"/>',
+ """<a href="https://third_test_54212.com">
+ <img src="imagesrc"/>
+ </a>
+ """,
+ """<a
+ href="https://test_strange_html.com" title="title"
+ fake='fake'
+ > test_strange_html_label
+ </a>
+ """,
+ '<a href="https://test_escaped.com" title="title" fake="fake"> test_escaped &lt; &gt; </a>',
+ '<a href="https://url_with_params.com?a=b&c=d">label</a>',
+ ]
+
+ self.env["mail.render.mixin"]._shorten_links("".join(test_links), {})
+
+ trackers_to_find = [
+ [("url", "=", "https://gitlab.com"), ("label", "=", "test_label")],
+ [("url", "=", "https://test_542152qsdqsd.com")],
+ [
+ ("url", "=", "https://test_strange_html.com"),
+ ("label", "=", "test_strange_html_label"),
+ ],
+ [
+ ("url", "=", "https://test_escaped.com"),
+ ("label", "=", "test_escaped < >"),
+ ],
+ [
+ ("url", "=", "https://url_with_params.com?a=b&c=d"),
+ ("label", "=", "label"),
+ ],
+ ]
+ trackers_to_fail = [
+ [("url", "=", "https://test_542152qsdqsd.com"), ("label", "ilike", "_")]
+ ]
+
+ for tracker_to_find in trackers_to_find:
+ self.assertTrue(self.env["link.tracker"].search(tracker_to_find))
+
+ for tracker_to_fail in trackers_to_fail:
+ self.assertFalse(self.env["link.tracker"].search(tracker_to_fail))
diff --git a/addons/link_tracker/views/link_tracker_views.xml b/addons/link_tracker/views/link_tracker_views.xml
index 40c4d75d593..f4d1273d5de 100644
--- a/addons/link_tracker/views/link_tracker_views.xml
+++ b/addons/link_tracker/views/link_tracker_views.xml
@@ -9,6 +9,7 @@
<search string="Links">
<field name="url" string="Title and URL" filter_domain="['|', ('title', 'ilike', self), ('url', 'ilike', self)]"/>
<field name="title"/>
+ <field name="label"/>
<field name="campaign_id"/>
<field name="medium_id"/>
<field name="source_id"/>
@@ -38,6 +39,7 @@
<group>
<group name="url" string="URL">
<field name="title"/>
+ <field name="label"/>
<field name="url"/>
<field name="short_url"/>
</group>
@@ -59,6 +61,7 @@
<tree string="Links">
<field name="create_date"/>
<field name="title"/>
+ <field name="label"/>
<field name="url"/>
<field name="short_url" optional="hide"/>
<field name="count"/>
diff --git a/addons/mass_mailing/__manifest__.py b/addons/mass_mailing/__manifest__.py
index 9cb082042d6..9c94b16e238 100644
--- a/addons/mass_mailing/__manifest__.py
+++ b/addons/mass_mailing/__manifest__.py
@@ -18,11 +18,13 @@
'web_kanban_gauge',
'social_media',
'web_tour',
+ 'digest',
],
'data': [
'security/mass_mailing_security.xml',
'security/ir.model.access.csv',
'data/mail_data.xml',
+ 'data/mailing_data_templates.xml',
'data/mass_mailing_data.xml',
'wizard/mail_compose_message_views.xml',
'wizard/mailing_list_merge_views.xml',
diff --git a/addons/mass_mailing/data/mailing_data_templates.xml b/addons/mass_mailing/data/mailing_data_templates.xml
new file mode 100644
index 00000000000..5f16120534e
--- /dev/null
+++ b/addons/mass_mailing/data/mailing_data_templates.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+<data noupdate="1">
+ <template id="mass_mailing.mass_mailing_kpi_link_trackers" name="Email Marketing: mailing link trackers statistic">
+ <table t-if="link_trackers" cellspacing="0" cellpadding="0" align="center" border="0" bgcolor="#eeeeee" style="width:100%; font-family: Arial,Helvetica,Verdana,sans-serif;">
+ <tr>
+ <td align="center" valign="top">
+ <table bgcolor="#ffffff" cellspacing="0" cellpadding="0" width="650" align="center" border="0" style="border: 1px solid #eeeeee; border-bottom: none;border-top: none; width: 100%; max-width: 650px; padding:0 30px 30px 30px">
+ <tr>
+ <td style="width: 100%;">
+ <table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
+ <tr>
+ <td align="left" style="border-bottom: 1px solid #eeeeee;">
+ <span style="color:#282f33; font-size: 15px; font-weight: bold; line-height: 30px">
+ <t t-esc="'Click Rate Report on %i Emails Sent' % object.sent"/>
+ </span>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr>
+ <td style="margin: 0; padding:0;">
+ <table cellspacing="0" cellpadding="0" border="0" width="580" align="center" style="width:100%; max-width:580px;">
+ <tr style="color: #875a7b; font-size: 16px; font-weight: 500; border-bottom: 1px solid #e7e7e7;">
+ <td style="width: 70%;padding: 10px 0; text-align: left;">Label</td>
+ <td style="width: 30%;padding: 10px 0; text-align: right;">%Click (Total)</td>
+ </tr>
+ <tr t-foreach="link_trackers" t-as="link_tracker" style="color: #888888; font-size: 15px; font-weight: 300;">
+ <td style="width: 70%;padding: 10px 0; text-align: left;">
+ <a t-att-href="link_tracker.absolute_url" target="_blank" style="color: #56b3b5; text-decoration: none;" t-esc="link_tracker.label or link_tracker.url"/>
+ </td>
+ <td style="width: 30%;padding: 10px 0; text-align: right;">
+ <t t-esc="int(link_tracker.count * 100 / object.sent) if object.sent else 0"/>% (<t t-esc="link_tracker.count"/>)
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ </table>
+ </template>
+</data>
+</odoo>
diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py
index 7b5b5e65954..31c2acc8dc3 100644
--- a/addons/mass_mailing/models/mailing.py
+++ b/addons/mass_mailing/models/mailing.py
@@ -10,6 +10,8 @@ import re
import threading
from ast import literal_eval
from datetime import datetime
+from dateutil.relativedelta import relativedelta
+from werkzeug.urls import url_join
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
@@ -117,6 +119,7 @@ class MassMailing(models.Model):
help='If checked, recipients will be mailed only once for the whole campaign. '
'This lets you send different mailings to randomly selected recipients and test '
'the effectiveness of the mailings, without causing duplicate messages.')
+ kpi_mail_required = fields.Boolean('KPI mail required', copy=False)
# statistics data
mailing_trace_ids = fields.One2many('mailing.trace', 'mass_mailing_id', string='Emails Statistics')
total = fields.Integer(compute="_compute_total")
@@ -558,7 +561,12 @@ class MassMailing(models.Model):
# auto-commit except in testing mode
auto_commit = not getattr(threading.currentThread(), 'testing', False)
composer.send_mail(auto_commit=auto_commit)
- mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
+ mailing.write({
+ 'state': 'done',
+ 'sent_date': fields.Datetime.now(),
+ # send the KPI mail only if it's the first sending
+ 'kpi_mail_required': not mailing.sent_date,
+ })
return True
def convert_links(self):
@@ -589,7 +597,119 @@ class MassMailing(models.Model):
mass_mailing.state = 'sending'
mass_mailing.action_send_mail()
else:
- mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
+ mass_mailing.write({
+ 'state': 'done',
+ 'sent_date': fields.Datetime.now(),
+ # send the KPI mail only if it's the first sending
+ 'kpi_mail_required': not mass_mailing.sent_date,
+ })
+
+ mailings = self.env['mailing.mailing'].search([
+ ('kpi_mail_required', '=', True),
+ ('state', '=', 'done'),
+ ('sent_date', '<=', fields.Datetime.now() - relativedelta(days=1)),
+ ('sent_date', '>=', fields.Datetime.now() - relativedelta(days=5)),
+ ])
+ if mailings:
+ mailings._action_send_statistics()
+
+ # ------------------------------------------------------
+ # STATISTICS
+ # ------------------------------------------------------
+ def _action_send_statistics(self):
+ """Send an email to the responsible of each finished mailing with the statistics."""
+ self.kpi_mail_required = False
+
+ for mailing in self:
+ user = mailing.user_id
+ mailing = mailing.with_context(lang=user.lang or self._context.get('lang'))
+
+ link_trackers = self.env['link.tracker'].search(
+ [('mass_mailing_id', '=', mailing.id)]
+ ).sorted('count', reverse=True)
+ link_trackers_body = self.env['ir.qweb']._render(
+ 'mass_mailing.mass_mailing_kpi_link_trackers',
+ {'object': mailing, 'link_trackers': link_trackers},
+ )
+
+ rendered_body = self.env['ir.qweb']._render(
+ 'digest.digest_mail_main',
+ {
+ 'body': tools.html_sanitize(link_trackers_body),
+ 'company': user.company_id,
+ 'user': user,
+ 'display_mobile_banner': True,
+ ** mailing._prepare_statistics_email_values()
+ },
+ )
+
+ full_mail = self.env['mail.render.mixin']._render_encapsulate(
+ 'digest.digest_mail_layout',
+ rendered_body,
+ )
+
+ mail_values = {
+ 'subject': _('24H Stats of mailing "%s"') % 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)
+
+ def _prepare_statistics_email_values(self):
+ """Return some statistics that will be displayed in the mailing statistics email.
+
+ Each item in the returned list will be displayed as a table, with a title and
+ 1, 2 or 3 columns.
+ """
+ self.ensure_one()
+
+ random_tip = self.env['digest.tip'].search(
+ [('group_id.category_id', '=', self.env.ref('base.module_category_marketing_email_marketing').id)]
+ )
+ if random_tip:
+ random_tip = random.choice(random_tip).tip_description
+
+ formatted_date = tools.format_datetime(
+ self.env, self.sent_date, self.user_id.tz, 'MMM dd, YYYY', self.user_id.lang
+ ) if self.sent_date else False
+
+ web_base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
+
+ return {
+ 'title': _('24H Stats of mailing'),
+ 'sub_title': '"%s"' % self.subject,
+ 'top_button_label': _('More Info'),
+ 'top_button_url': url_join(web_base_url, f'/web#id={self.id}&model=mailing.mailing&view_type=form'),
+ 'kpi_data': [
+ {
+ 'kpi_fullname': _('Engagement on %i Emails Sent') % self.sent,
+ 'kpi_action': None,
+ 'kpi_col1': {
+ 'value': f'{self.received_ratio}%',
+ 'col_subtitle': '%s (%i)' % (_('RECEIVED'), self.delivered),
+ },
+ 'kpi_col2': {
+ 'value': f'{self.opened_ratio}%',
+ 'col_subtitle': '%s (%i)' % (_('OPENED'), self.opened),
+ },
+ 'kpi_col3': {
+ 'value': f'{self.replied_ratio}%',
+ 'col_subtitle': '%s (%i)' % (_('REPLIED'), self.replied),
+ },
+ }, {
+ 'kpi_fullname': _('Business Benefits on %i Emails Sent') % self.sent,
+ 'kpi_action': None,
+ 'kpi_col1': {},
+ 'kpi_col2': {},
+ 'kpi_col3': {},
+ },
+ ],
+ 'tips': [random_tip] if random_tip else False,
+ 'formatted_date': formatted_date,
+ }
# ------------------------------------------------------
# TOOLS
diff --git a/addons/mass_mailing_crm/models/mailing_mailing.py b/addons/mass_mailing_crm/models/mailing_mailing.py
index 97de181c798..19af7cff904 100644
--- a/addons/mass_mailing_crm/models/mailing_mailing.py
+++ b/addons/mass_mailing_crm/models/mailing_mailing.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, fields, models, _, tools
from odoo.osv import expression
@@ -33,3 +33,14 @@ class MassMailing(models.Model):
action['domain'] = [('source_id', 'in', self.source_id.ids)]
action['context'] = {'active_test': False, 'create': False}
return action
+
+ def _prepare_statistics_email_values(self):
+ self.ensure_one()
+ values = super(MassMailing, self)._prepare_statistics_email_values()
+ if not self.user_id:
+ return values
+ values['kpi_data'][1]['kpi_col1'] = {
+ 'value': tools.format_decimalized_number(self.crm_lead_count, decimal=0),
+ 'col_subtitle': _('LEADS'),
+ }
+ return values
diff --git a/addons/mass_mailing_sale/models/mailing_mailing.py b/addons/mass_mailing_sale/models/mailing_mailing.py
index 858159d2286..290875290bf 100644
--- a/addons/mass_mailing_sale/models/mailing_mailing.py
+++ b/addons/mass_mailing_sale/models/mailing_mailing.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, fields, models, _, tools
class MassMailing(models.Model):
@@ -61,3 +61,23 @@ class MassMailing(models.Model):
if not res:
res.append((0, '=', 1))
return res
+
+ def _prepare_statistics_email_values(self):
+ self.ensure_one()
+ values = super(MassMailing, self)._prepare_statistics_email_values()
+ if not self.user_id:
+ return values
+
+ self_with_company = self.with_company(self.user_id.company_id)
+ currency = self.user_id.company_id.currency_id
+ formated_amount = tools.format_decimalized_amount(self_with_company.sale_invoiced_amount, currency)
+
+ values['kpi_data'][1]['kpi_col2'] = {
+ 'value': self.sale_quotation_count,
+ 'col_subtitle': _('QUOTATIONS'),
+ }
+ values['kpi_data'][1]['kpi_col3'] = {
+ 'value': formated_amount,
+ 'col_subtitle': _('INVOICED'),
+ }
+ return values
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index 6e552a6962d..c37f6dcc369 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -255,6 +255,8 @@ def html_sanitize(src, silent=True, sanitize_tags=True, sanitize_attributes=Fals
URL_REGEX = r'(\bhref=[\'"](?!mailto:|tel:|sms:)([^\'"]+)[\'"])'
TEXT_URL_REGEX = r'https?://[a-zA-Z0-9@:%._\+~#=/-]+(?:\?\S+)?'
+# retrieve inner content of the link
+HTML_TAG_URL_REGEX = URL_REGEX + r'([^<>]*>([^<>]+)<\/)?'
def validate_url(url):
diff --git a/odoo/tools/misc.py b/odoo/tools/misc.py
index 24a58ba1922..e7f087f1636 100644
--- a/odoo/tools/misc.py
+++ b/odoo/tools/misc.py
@@ -1380,6 +1380,46 @@ def _format_time_ago(env, time_delta, lang_code=False, add_direction=True):
return babel.dates.format_timedelta(-time_delta, add_direction=add_direction, locale=locale)
+def format_decimalized_number(number, decimal=1):
+ """Format a number to display to nearest metrics unit next to it.
+
+ Do not display digits if all visible digits are null.
+ Do not display units higher then "Tera" because most of people don't know what
+ a "Yotta" is.
+
+ >>> format_decimalized_number(123_456.789)
+ 123.5k
+ >>> format_decimalized_number(123_000.789)
+ 123k
+ >>> format_decimalized_number(-123_456.789)
+ -123.5k
+ >>> format_decimalized_number(0.789)
+ 0.8
+ """
+ for unit in ['', 'k', 'M', 'G']:
+ if abs(number) < 1000.0:
+ return "%g%s" % (round(number, decimal), unit)
+ number /= 1000.0
+ return "%g%s" % (round(number, decimal), 'T')
+
+
+def format_decimalized_amount(amount, currency=None):
+ """Format a amount to display the currency and also display the metric unit of the amount.
+
+ >>> format_decimalized_amount(123_456.789, res.currency("$"))
+ $123.5k
+ """
+ formated_amount = format_decimalized_number(amount)
+
+ if not currency:
+ return formated_amount
+
+ if currency.position == 'before':
+ return "%s%s" % (currency.symbol or '', formated_amount)
+
+ return "%s %s" % (formated_amount, currency.symbol or '')
+
+
def format_amount(env, amount, currency, lang_code=False):
fmt = "%.{0}f".format(currency.decimal_places)
lang = get_lang(env, lang_code)