Allow party to be unsubscribed from scenario
issue11426 review403391045
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-4
@@ -15,12 +15,10 @@ def register():
|
||||
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',
|
||||
|
||||
+44
-39
@@ -4,6 +4,7 @@ import datetime
|
||||
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 @@ class Scenario(Workflow, ModelSQL, ModelView):
|
||||
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"),
|
||||
@@ -108,6 +112,10 @@ class Scenario(Workflow, ModelSQL, ModelView):
|
||||
def default_domain(cls):
|
||||
return '[]'
|
||||
|
||||
@classmethod
|
||||
def default_unsubscribable(cls):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_models(cls):
|
||||
pool = Pool()
|
||||
@@ -212,6 +220,11 @@ class Scenario(Workflow, ModelSQL, ModelView):
|
||||
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 @@ class Activity(ModelSQL, ModelView):
|
||||
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 @@ class Record(ModelSQL, ModelView):
|
||||
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 @@ class Record(ModelSQL, ModelView):
|
||||
('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 @@ class Record(ModelSQL, ModelView):
|
||||
|
||||
@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
|
||||
|
||||
@@ -205,6 +205,12 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 @@ from .mixin import MarketingAutomationMixin
|
||||
|
||||
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')
|
||||
|
||||
@@ -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>
|
||||
@@ -8,3 +8,11 @@ from .mixin import MarketingAutomationMixin
|
||||
|
||||
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:])]
|
||||
|
||||
@@ -64,8 +64,9 @@ for dep in info.get('depends', []):
|
||||
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(
|
||||
|
||||
+1
-1
@@ -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">
|
||||
|
||||
@@ -17,10 +17,12 @@ Patch sendmail_transactional::
|
||||
>>> 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::
|
||||
|
||||
|
||||
@@ -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 = 'michael@example.com'
|
||||
>>> 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)])
|
||||
[]
|
||||
@@ -7,6 +7,7 @@ from trytond.tests.test_tryton import ModuleTestCase
|
||||
class MarketingAutomationTestCase(ModuleTestCase):
|
||||
'Test Marketing Automation module'
|
||||
module = 'marketing_automation'
|
||||
extras = ['sale']
|
||||
|
||||
|
||||
del ModuleTestCase
|
||||
|
||||
+3
-2
@@ -2,12 +2,13 @@
|
||||
version=6.5.0
|
||||
depends:
|
||||
ir
|
||||
res
|
||||
marketing
|
||||
party
|
||||
res
|
||||
web_shortener
|
||||
extras_depend:
|
||||
sale
|
||||
party
|
||||
xml:
|
||||
marketing_automation.xml
|
||||
party.xml
|
||||
message.xml
|
||||
|
||||
@@ -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>
|
||||
@@ -6,6 +6,8 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="record"/>
|
||||
<label name="scenario"/>
|
||||
<field name="scenario"/>
|
||||
|
||||
<label name="blocked"/>
|
||||
<field name="blocked"/>
|
||||
<button name="block" colspan="2"/>
|
||||
</form>
|
||||
|
||||
@@ -5,4 +5,5 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="scenario" expand="1"/>
|
||||
<field name="record" expand="1"/>
|
||||
<field name="blocked"/>
|
||||
<button name="block" tree_invisible="1"/>
|
||||
</tree>
|
||||
|
||||
@@ -7,8 +7,11 @@ this repository contains the full copyright notices and license terms. -->
|
||||
|
||||
<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"/>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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"/>
|
||||
|
||||
Reference in New Issue
Block a user