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. --> +