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"/>

Reply via email to