diff --git a/modules/marketing_automation/CHANGELOG b/modules/marketing_automation/CHANGELOG
index b1db5db8fa..495b5dbf0b 100644
--- a/modules/marketing_automation/CHANGELOG
+++ b/modules/marketing_automation/CHANGELOG
@@ -1,3 +1,4 @@
+* Add trend spark lines
* Add reporting on scenario and activity per period
* Display block rate instead of absolute value
* Display email rates instead of absolute value
diff --git a/modules/marketing_automation/marketing_automation.py b/modules/marketing_automation/marketing_automation.py
index e8c76dd5a4..0d3e2b19e2 100644
--- a/modules/marketing_automation/marketing_automation.py
+++ b/modules/marketing_automation/marketing_automation.py
@@ -13,6 +13,8 @@ from functools import partial
from urllib.parse import (
parse_qsl, quote, urlencode, urljoin, urlsplit, urlunsplit)
+from dateutil.relativedelta import relativedelta
+
try:
import html2text
except ImportError:
@@ -23,6 +25,11 @@ from genshi.template import MarkupTemplate, TextTemplate
from sql import Literal
from sql.aggregate import Count
+try:
+ import pygal
+except ImportError:
+ pygal = None
+
from trytond.config import config
from trytond.i18n import gettext
from trytond.model import (
@@ -32,7 +39,7 @@ from trytond.pool import Pool
from trytond.pyson import Eval, If, PYSONDecoder, TimeDelta
from trytond.report import Report
from trytond.sendmail import SMTPDataManager, sendmail_transactional
-from trytond.tools import grouped_slice, reduce_ids
+from trytond.tools import grouped_slice, pairwise_longest, reduce_ids
from trytond.tools.email_ import convert_ascii_email, set_from_header
from trytond.transaction import Transaction
from trytond.url import http_host
@@ -59,7 +66,56 @@ def _formataddr(name, email):
return formataddr((name, convert_ascii_email(email)))
-class Scenario(Workflow, ModelSQL, ModelView):
+def trend_mixin(model_name, field_name):
+ class TrendMixin:
+ __slots__ = ()
+
+ def get_trend(self, name):
+ name = name[:-len('_trend')]
+ if pygal:
+ chart = pygal.Line()
+ chart.add('', [
+ getattr(ts, name, 0) or 0
+ for ts in self.trends])
+ return chart.render_sparktext()
+
+ @property
+ def trends(self):
+ pool = Pool()
+ Model = pool.get(model_name)
+ Date = pool.get('ir.date')
+
+ delta = self._trend_period_delta()
+ date = Date.today()
+ date -= 10 * delta
+
+ records = Model.search([
+ ('date', '>=', date),
+ (field_name, '=', self.id),
+ ],
+ order=[('date', 'ASC')])
+ for record, next_record in pairwise_longest(records):
+ yield record
+ if delta and next_record:
+ date = record.date + delta
+ while date < next_record.date:
+ yield None
+ date += delta
+
+ @classmethod
+ def _trend_period_delta(cls):
+ context = Transaction().context
+ return {
+ 'year': relativedelta(years=1),
+ 'month': relativedelta(months=1),
+ 'day': relativedelta(days=1),
+ }.get(context.get('period', 'month'))
+ return TrendMixin
+
+
+class Scenario(
+ trend_mixin('marketing.automation.reporting.scenario', 'scenario'),
+ Workflow, ModelSQL, ModelView):
"Marketing Scenario"
__name__ = 'marketing.automation.scenario'
@@ -77,6 +133,9 @@ class Scenario(Workflow, ModelSQL, ModelView):
block_rate = fields.Function(
fields.Float("Block Rate"),
'get_rate')
+ block_rate_trend = fields.Function(
+ fields.Char("Block Rate Trend"),
+ 'get_trend')
unsubscribable = fields.Boolean(
"Unsubscribable",
help="If checked parties are also unsubscribed from the scenario.")
@@ -273,7 +332,9 @@ class Scenario(Workflow, ModelSQL, ModelView):
RecordActivity.save(record_activities)
-class Activity(ModelSQL, ModelView):
+class Activity(
+ trend_mixin('marketing.automation.reporting.activity', 'activity'),
+ ModelSQL, ModelView):
"Marketing Activity"
__name__ = 'marketing.automation.activity'
@@ -380,6 +441,13 @@ class Activity(ModelSQL, ModelView):
'invisible': Eval('action') != 'send_email',
}),
'get_rate')
+ email_open_rate_trend = fields.Function(
+ fields.Char(
+ "E-mail Open Rate Trend",
+ states={
+ 'invisible': Eval('action') != 'send_email',
+ }),
+ 'get_trend')
email_click_rate = fields.Function(
fields.Float(
"E-mail Click Rate",
@@ -387,6 +455,13 @@ class Activity(ModelSQL, ModelView):
'invisible': Eval('action') != 'send_email',
}),
'get_rate')
+ email_click_rate_trend = fields.Function(
+ fields.Char(
+ "E-mail Click Rate Trend",
+ states={
+ 'invisible': Eval('action') != 'send_email',
+ }),
+ 'get_trend')
email_click_through_rate = fields.Function(
fields.Float(
"E-mail Click-Through Rate",
@@ -394,6 +469,13 @@ class Activity(ModelSQL, ModelView):
'invisible': Eval('action') != 'send_email',
}),
'get_rate')
+ email_click_through_rate_trend = fields.Function(
+ fields.Char(
+ "E-mail Click-Through Rate Trend",
+ states={
+ 'invisible': Eval('action') != 'send_email',
+ }),
+ 'get_trend')
@classmethod
def __setup__(cls):
diff --git a/modules/marketing_automation/setup.py b/modules/marketing_automation/setup.py
index 6540e85a32..3f1625925b 100755
--- a/modules/marketing_automation/setup.py
+++ b/modules/marketing_automation/setup.py
@@ -49,7 +49,9 @@ requires.append(get_require_version('trytond'))
tests_require = [
get_require_version('proteus'),
- get_require_version('trytond_sale')]
+ get_require_version('trytond_sale'),
+ 'pygal',
+ ]
setup(name=name,
version=version,
@@ -123,6 +125,7 @@ setup(name=name,
python_requires='>=3.8',
install_requires=requires,
extras_require={
+ 'sparklines': ['pygal'],
'test': tests_require,
},
zip_safe=False,
diff --git a/modules/marketing_automation/tests/scenario_marketing_automation.rst b/modules/marketing_automation/tests/scenario_marketing_automation.rst
index b4bbdddc05..731823ed00 100644
--- a/modules/marketing_automation/tests/scenario_marketing_automation.rst
+++ b/modules/marketing_automation/tests/scenario_marketing_automation.rst
@@ -115,6 +115,8 @@ Trigger scenario::
0
>>> scenario.block_rate
0.0
+ >>> bool(scenario.block_rate_trend)
+ True
>>> reporting, = ReportingScenario.find([])
>>> reporting.record_count
@@ -201,10 +203,16 @@ Trigger open email and reminder after delay::
0
>>> root_activity.email_open_rate
1.0
+ >>> bool(root_activity.email_open_rate_trend)
+ True
>>> root_activity.email_click_rate
0.0
+ >>> bool(root_activity.email_click_rate_trend)
+ True
>>> root_activity.email_click_through_rate
0.0
+ >>> bool(root_activity.email_click_through_rate_trend)
+ True
>>> reporting, = ReportingActivity.find([
... ('activity', '=', root_activity.id),
diff --git a/modules/marketing_automation/view/activity_tree.xml b/modules/marketing_automation/view/activity_tree.xml
index bb3d0e29d1..c15a1a07e0 100644
--- a/modules/marketing_automation/view/activity_tree.xml
+++ b/modules/marketing_automation/view/activity_tree.xml
@@ -9,10 +9,13 @@ this repository contains the full copyright notices and license terms. -->
+
+
+
diff --git a/modules/marketing_automation/view/scenario_list.xml b/modules/marketing_automation/view/scenario_list.xml
index 9bc4da6651..f88a5cc3ad 100644
--- a/modules/marketing_automation/view/scenario_list.xml
+++ b/modules/marketing_automation/view/scenario_list.xml
@@ -8,5 +8,6 @@ this repository contains the full copyright notices and license terms. -->
+