changeset 1f606673f56a in modules/marketing_automation:default
details:
https://hg.tryton.org/modules/marketing_automation?cmd=changeset&node=1f606673f56a
description:
Allow party to be unsubscribed from scenario
issue11426
review403391045
diffstat:
CHANGELOG | 2 +
__init__.py | 6 +-
marketing_automation.py | 83 ++++++------
marketing_automation.xml | 6 +
mixin.py | 13 ++
party.py | 38 +++++
party.xml | 12 +
sale.py | 8 +
setup.py | 5 +-
tests/email.html | 2 +-
tests/scenario_marketing_automation.rst | 4 +-
tests/scenario_marketing_automation_unsubscribable.rst | 110 +++++++++++++++++
tests/test_module.py | 1 +
tryton.cfg | 5 +-
view/party_form.xml | 10 +
view/record_form.xml | 2 +
view/record_list.xml | 1 +
view/scenario_form.xml | 5 +-
view/scenario_list.xml | 1 +
19 files changed, 264 insertions(+), 50 deletions(-)
diffs (547 lines):
diff -r 96de6a719ee3 -r 1f606673f56a CHANGELOG
--- a/CHANGELOG Mon May 02 17:24:45 2022 +0200
+++ b/CHANGELOG Tue Jun 07 18:03:53 2022 +0200
@@ -1,3 +1,5 @@
+* Allow party to be unsubscribed from scenario
+
Version 6.4.0 - 2022-05-02
* Bug fixes (see mercurial logs for details)
* Add support for Python 3.10
diff -r 96de6a719ee3 -r 1f606673f56a __init__.py
--- a/__init__.py Mon May 02 17:24:45 2022 +0200
+++ b/__init__.py Tue Jun 07 18:03:53 2022 +0200
@@ -15,13 +15,11 @@
marketing_automation.Activity,
marketing_automation.Record,
marketing_automation.RecordActivity,
+ party.Party,
+ party.PartyUnsubscribedScenario,
web.ShortenedURL,
module='marketing_automation', type_='model')
Pool.register(
- party.Party,
- module='marketing_automation', type_='model',
- depends=['party'])
- Pool.register(
sale.Sale,
module='marketing_automation', type_='model',
depends=['sale'])
diff -r 96de6a719ee3 -r 1f606673f56a marketing_automation.py
--- a/marketing_automation.py Mon May 02 17:24:45 2022 +0200
+++ b/marketing_automation.py Tue Jun 07 18:03:53 2022 +0200
@@ -4,6 +4,7 @@
import logging
import time
import uuid
+from collections import defaultdict
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
@@ -72,6 +73,9 @@
fields.Integer("Records"), 'get_record_count')
record_count_blocked = fields.Function(
fields.Integer("Records Blocked"), 'get_record_count')
+ unsubscribable = fields.Boolean(
+ "Unsubscribable",
+ help="If checked parties are also unsubscribed from the scenario.")
state = fields.Selection([
('draft', "Draft"),
('running', "Running"),
@@ -109,6 +113,10 @@
return '[]'
@classmethod
+ def default_unsubscribable(cls):
+ return False
+
+ @classmethod
def get_models(cls):
pool = Pool()
Model = pool.get('ir.model')
@@ -212,6 +220,11 @@
record = Record.__table__()
cursor = Transaction().connection.cursor()
domain = PYSONDecoder({}).decode(scenario.domain)
+ domain = [
+ domain,
+ ('marketing_party.marketing_scenario_unsubscribed',
+ 'not where', [('id', '=', scenario.id)]),
+ ]
try:
query = Model.search(domain, query=True, order=[])
except Exception:
@@ -506,28 +519,12 @@
for child in self.children])
def _email_recipient(self, record):
- pool = Pool()
- try:
- Party = pool.get('party.party')
- except KeyError:
- Party = None
- try:
- Sale = pool.get('sale.sale')
- except KeyError:
- Sale = None
-
- def get_party_email(party):
- contact = party.contact_mechanism_get('email')
- if contact and contact.email:
- return _formataddr(
- contact.name or party.rec_name,
- contact.email)
- return None
-
- if Party and isinstance(record, Party):
- return get_party_email(record)
- elif Sale and isinstance(record, Sale):
- return get_party_email(record.party)
+ party = record.marketing_party
+ contact = party.contact_mechanism_get('email')
+ if contact and contact.email:
+ return _formataddr(
+ contact.name or party.rec_name,
+ contact.email)
def execute_send_email(
self, record_activity, smtpd_datamanager=None, **kwargs):
@@ -638,7 +635,11 @@
required=True, ondelete='CASCADE')
record = fields.Reference(
"Record", selection='get_models', required=True)
- blocked = fields.Boolean("Blocked")
+ blocked = fields.Boolean(
+ "Blocked",
+ states={
+ 'readonly': ~Eval('blocked', False),
+ })
uuid = fields.Char("UUID", readonly=True)
@classmethod
@@ -652,6 +653,11 @@
('uuid_unique', Unique(t, t.uuid),
'marketing_automation.msg_record_uuid_unique'),
]
+ cls._buttons.update({
+ 'block': {
+ 'invisible': Eval('blocked', False),
+ },
+ })
@classmethod
def default_uuid(cls):
@@ -683,27 +689,26 @@
@property
def language(self):
- pool = Pool()
- try:
- Party = pool.get('party.party')
- except KeyError:
- Party = None
- try:
- Sale = pool.get('sale.sale')
- except KeyError:
- Sale = None
-
- if Party and isinstance(self.record, Party):
- if self.record.lang:
- return self.record.lang.code
- elif Sale and isinstance(self.record, Sale):
- if self.record.party.lang:
- return self.record.party.lang.code
+ lang = self.record.marketing_party.lang
+ if lang:
+ return lang.code
@dualmethod
+ @ModelView.button
def block(cls, records):
+ pool = Pool()
+ Party = pool.get('party.party')
+
cls.write(records, {'blocked': True})
+ parties = defaultdict(set)
+ for record in records:
+ if record.scenario.unsubscribable:
+ parties[record.record.marketing_party].add(record.scenario.id)
+ Party.write(*sum((
+ ([p], {'marketing_scenario_unsubscribed': [('add', s)]})
+ for p, s in parties.items()), ()))
+
def get_rec_name(self, name):
if self.record:
return self.record.rec_name
diff -r 96de6a719ee3 -r 1f606673f56a marketing_automation.xml
--- a/marketing_automation.xml Mon May 02 17:24:45 2022 +0200
+++ b/marketing_automation.xml Tue Jun 07 18:03:53 2022 +0200
@@ -205,6 +205,12 @@
<field name="perm_delete" eval="True"/>
</record>
+ <record model="ir.model.button" id="record_block_button">
+ <field name="name">block</field>
+ <field name="string">Block</field>
+ <field name="model" search="[('model', '=',
'marketing.automation.record')]"/>
+ </record>
+
<record model="ir.ui.view" id="record_activity_view_list">
<field name="model">marketing.automation.record.activity</field>
<field name="type">tree</field>
diff -r 96de6a719ee3 -r 1f606673f56a mixin.py
--- a/mixin.py Mon May 02 17:24:45 2022 +0200
+++ b/mixin.py Tue Jun 07 18:03:53 2022 +0200
@@ -1,6 +1,19 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+from trytond.model import fields
+
class MarketingAutomationMixin:
__slots__ = ()
+
+ marketing_party = fields.Function(
+ fields.Many2One('party.party', "Marketing Party"),
+ 'get_marketing_party', searcher='search_marketing_party')
+
+ def get_marketing_party(self, name):
+ raise NotImplementedError
+
+ @classmethod
+ def search_marketing_party(cls, name, clause):
+ raise NotImplementedError
diff -r 96de6a719ee3 -r 1f606673f56a party.py
--- a/party.py Mon May 02 17:24:45 2022 +0200
+++ b/party.py Tue Jun 07 18:03:53 2022 +0200
@@ -1,6 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
+from trytond.model import ModelSQL, fields
from trytond.pool import PoolMeta
from .mixin import MarketingAutomationMixin
@@ -8,3 +9,40 @@
class Party(MarketingAutomationMixin, metaclass=PoolMeta):
__name__ = 'party.party'
+
+ marketing_scenario_unsubscribed = fields.Many2Many(
+ 'party.party-unsubscribed-marketing.automation.scenario',
+ 'party', 'scenario', "Marketing Automation Scenario Unsubscribed")
+
+ def get_marketing_party(self, name):
+ return self.id
+
+ @classmethod
+ def search_marketing_party(cls, name, clause):
+ operand, operator, value = clause[:3]
+ nested = operand.lstrip(name)
+ if not nested:
+ if operator.endswith('where'):
+ query = cls.search(value, order=[], query=True)
+ if operator.startswith('not'):
+ return [('id', 'not in', query)]
+ else:
+ return [('id', 'in', query)]
+ elif isinstance(value, str):
+ nested = 'rec_name'
+ else:
+ nested = 'id'
+ else:
+ nested = nested.lstrip('.')
+ return [(nested,) + tuple(clause[1:])]
+
+
+class PartyUnsubscribedScenario(ModelSQL):
+ "Party Unsubscribed Scenario"
+ __name__ = 'party.party-unsubscribed-marketing.automation.scenario'
+
+ party = fields.Many2One(
+ 'party.party', "Party", required=True, select=True, ondelete='CASCADE')
+ scenario = fields.Many2One(
+ 'marketing.automation.scenario', "Scenario",
+ required=True, ondelete='CASCADE')
diff -r 96de6a719ee3 -r 1f606673f56a party.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/party.xml Tue Jun 07 18:03:53 2022 +0200
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+ <data>
+ <record model="ir.ui.view" id="party_view_form">
+ <field name="model">party.party</field>
+ <field name="inherit" ref="party.party_view_form"/>
+ <field name="name">party_form</field>
+ </record>
+ </data>
+</tryton>
diff -r 96de6a719ee3 -r 1f606673f56a sale.py
--- a/sale.py Mon May 02 17:24:45 2022 +0200
+++ b/sale.py Tue Jun 07 18:03:53 2022 +0200
@@ -8,3 +8,11 @@
class Sale(MarketingAutomationMixin, metaclass=PoolMeta):
__name__ = 'sale.sale'
+
+ def get_marketing_party(self, name):
+ return self.party.id
+
+ @classmethod
+ def search_marketing_party(cls, name, clause):
+ nested = clause[0].lstrip(name)
+ return [('party' + nested,) + tuple(clause[1:])]
diff -r 96de6a719ee3 -r 1f606673f56a setup.py
--- a/setup.py Mon May 02 17:24:45 2022 +0200
+++ b/setup.py Tue Jun 07 18:03:53 2022 +0200
@@ -64,8 +64,9 @@
requires.append(get_require_version('trytond_%s' % dep))
requires.append(get_require_version('trytond'))
-tests_require = [get_require_version('proteus'),
- get_require_version('trytond_party')]
+tests_require = [
+ get_require_version('proteus'),
+ get_require_version('trytond_sale')]
dependency_links = []
if minor_version % 2:
dependency_links.append(
diff -r 96de6a719ee3 -r 1f606673f56a tests/email.html
--- a/tests/email.html Mon May 02 17:24:45 2022 +0200
+++ b/tests/email.html Tue Jun 07 18:03:53 2022 +0200
@@ -4,7 +4,7 @@
<title>Hello!</title>
</head>
<body>
- <p>Hello, ${record.name}!</p>
+ <p>Hello, ${record.rec_name}!</p>
<p>
Here is a
<a href="http://example.com/action">
diff -r 96de6a719ee3 -r 1f606673f56a tests/scenario_marketing_automation.rst
--- a/tests/scenario_marketing_automation.rst Mon May 02 17:24:45 2022 +0200
+++ b/tests/scenario_marketing_automation.rst Tue Jun 07 18:03:53 2022 +0200
@@ -17,10 +17,12 @@
>>> from trytond.modules.marketing_automation import marketing_automation
>>> smtp_calls = patch.object(
... marketing_automation, 'sendmail_transactional').start()
+ >>> manager = patch.object(
+ ... marketing_automation, 'SMTPDataManager').start()
Activate modules::
- >>> config = activate_modules(['marketing_automation', 'party'])
+ >>> config = activate_modules('marketing_automation')
Create a party::
diff -r 96de6a719ee3 -r 1f606673f56a
tests/scenario_marketing_automation_unsubscribable.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_marketing_automation_unsubscribable.rst Tue Jun 07
18:03:53 2022 +0200
@@ -0,0 +1,110 @@
+Marketing Automation Unsubscribable Scenario
+============================================
+
+Imports::
+
+ >>> import datetime
+ >>> import re
+
+ >>> from proteus import Model, Wizard
+ >>> from proteus.config import get_config
+ >>> from trytond.modules.company.tests.tools import create_company
+ >>> from trytond.pyson import Eval, PYSONEncoder
+ >>> from trytond.tests.tools import activate_modules
+ >>> from trytond.tools import file_open
+
+Patch sendmail_transactional::
+
+ >>> from unittest.mock import patch
+ >>> from trytond.modules.marketing_automation import marketing_automation
+ >>> smtp_calls = patch.object(
+ ... marketing_automation, 'sendmail_transactional').start()
+ >>> manager = patch.object(
+ ... marketing_automation, 'SMTPDataManager').start()
+
+Activate modules::
+
+ >>> config = activate_modules(['marketing_automation', 'sale'])
+
+ >>> Activity = Model.get('marketing.automation.activity')
+ >>> Cron = Model.get('ir.cron')
+ >>> Party = Model.get('party.party')
+ >>> Record = Model.get('marketing.automation.record')
+ >>> Sale = Model.get('sale.sale')
+ >>> Scenario = Model.get('marketing.automation.scenario')
+
+ >>> cron_trigger, = Cron.find([
+ ... ('method', '=', 'marketing.automation.scenario|trigger'),
+ ... ])
+ >>> cron_process, = Cron.find([
+ ... ('method', '=', 'marketing.automation.record.activity|process'),
+ ... ])
+
+Create a party::
+
+ >>> party = Party()
+ >>> party.name = "Michael Scott"
+ >>> contact = party.contact_mechanisms.new()
+ >>> contact.type = 'email'
+ >>> contact.value = '[email protected]'
+ >>> party.save()
+
+Create company::
+
+ >>> _ = create_company()
+
+Create a sale::
+
+ >>> sale = Sale(party=party)
+ >>> sale.save()
+
+Create the running scenario::
+
+ >>> scenario = Scenario()
+ >>> scenario.name = "Sale Scenario"
+ >>> scenario.model = 'sale.sale'
+ >>> scenario.domain = '[["state", "=", "draft"]]'
+ >>> scenario.unsubscribable = True
+ >>> scenario.save()
+
+ >>> activity = scenario.activities.new()
+ >>> activity.name = "First E-Mail"
+ >>> activity.action = 'send_email'
+ >>> activity.email_title = "Pending Sale"
+ >>> activity.condition = PYSONEncoder().encode(
+ ... Eval('self', {}).get('active'))
+ >>> with file_open('marketing_automation/tests/email.html', mode='r') as
fp:
+ ... activity.email_template = fp.read()
+
+ >>> scenario.click('run')
+
+Trigger scenario::
+
+ >>> cron_trigger.click('run_once')
+ >>> cron_process.click('run_once')
+
+ >>> record, = Record.find([])
+ >>> record.record == sale
+ True
+
+Block and unsubscribe::
+
+ >>> record.click('block')
+ >>> bool(record.blocked)
+ True
+ >>> party.reload()
+ >>> party.marketing_scenario_unsubscribed == [scenario]
+ True
+
+Create a new sale::
+
+ >>> sale = Sale(party=party)
+ >>> sale.save()
+
+Trigger scenario::
+
+ >>> cron_trigger.click('run_once')
+ >>> cron_process.click('run_once')
+
+ >>> Record.find([('blocked', '=', False)])
+ []
diff -r 96de6a719ee3 -r 1f606673f56a tests/test_module.py
--- a/tests/test_module.py Mon May 02 17:24:45 2022 +0200
+++ b/tests/test_module.py Tue Jun 07 18:03:53 2022 +0200
@@ -7,6 +7,7 @@
class MarketingAutomationTestCase(ModuleTestCase):
'Test Marketing Automation module'
module = 'marketing_automation'
+ extras = ['sale']
del ModuleTestCase
diff -r 96de6a719ee3 -r 1f606673f56a tryton.cfg
--- a/tryton.cfg Mon May 02 17:24:45 2022 +0200
+++ b/tryton.cfg Tue Jun 07 18:03:53 2022 +0200
@@ -2,12 +2,13 @@
version=6.5.0
depends:
ir
+ marketing
+ party
res
- marketing
web_shortener
extras_depend:
sale
- party
xml:
marketing_automation.xml
+ party.xml
message.xml
diff -r 96de6a719ee3 -r 1f606673f56a view/party_form.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/view/party_form.xml Tue Jun 07 18:03:53 2022 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<data>
+ <xpath expr="/form/notebook" position="inside">
+ <page string="Marketing Automation" id="marketing_automation">
+ <field name="marketing_scenario_unsubscribed"
widget="multiselection"/>
+ </page>
+ </xpath>
+</data>
diff -r 96de6a719ee3 -r 1f606673f56a view/record_form.xml
--- a/view/record_form.xml Mon May 02 17:24:45 2022 +0200
+++ b/view/record_form.xml Tue Jun 07 18:03:53 2022 +0200
@@ -6,6 +6,8 @@
<field name="record"/>
<label name="scenario"/>
<field name="scenario"/>
+
<label name="blocked"/>
<field name="blocked"/>
+ <button name="block" colspan="2"/>
</form>
diff -r 96de6a719ee3 -r 1f606673f56a view/record_list.xml
--- a/view/record_list.xml Mon May 02 17:24:45 2022 +0200
+++ b/view/record_list.xml Tue Jun 07 18:03:53 2022 +0200
@@ -5,4 +5,5 @@
<field name="scenario" expand="1"/>
<field name="record" expand="1"/>
<field name="blocked"/>
+ <button name="block" tree_invisible="1"/>
</tree>
diff -r 96de6a719ee3 -r 1f606673f56a view/scenario_form.xml
--- a/view/scenario_form.xml Mon May 02 17:24:45 2022 +0200
+++ b/view/scenario_form.xml Tue Jun 07 18:03:53 2022 +0200
@@ -7,8 +7,11 @@
<label name="model"/>
<field name="model"/>
+ <label name="unsubscribable"/>
+ <field name="unsubscribable"/>
+
<label name="domain"/>
- <field name="domain" widget="pyson"/>
+ <field name="domain" widget="pyson" colspan="3"/>
<field name="activities" colspan="4"
view_ids="marketing_automation.activity_view_list"/>
diff -r 96de6a719ee3 -r 1f606673f56a view/scenario_list.xml
--- a/view/scenario_list.xml Mon May 02 17:24:45 2022 +0200
+++ b/view/scenario_list.xml Tue Jun 07 18:03:53 2022 +0200
@@ -4,6 +4,7 @@
<tree keyword_open="1">
<field name="name" expand="2"/>
<field name="model" expand="1"/>
+ <field name="unsubscribable" optional="0"/>
<field name="record_count"/>
<field name="record_count_blocked"/>
<field name="state"/>