Browse Source

[ADD] mass_mailing_list_dynamic: Autopopulated mailing lists

pull/284/head
Jairo Llopis 7 years ago
committed by Pedro M. Baeza
parent
commit
ab554fe774
  1. 91
      mass_mailing_list_dynamic/README.rst
  2. 4
      mass_mailing_list_dynamic/__init__.py
  3. 20
      mass_mailing_list_dynamic/__manifest__.py
  4. 119
      mass_mailing_list_dynamic/i18n/es.po
  5. 6
      mass_mailing_list_dynamic/models/__init__.py
  6. 14
      mass_mailing_list_dynamic/models/mail_mass_mailing.py
  7. 20
      mass_mailing_list_dynamic/models/mail_mass_mailing_contact.py
  8. 57
      mass_mailing_list_dynamic/models/mail_mass_mailing_list.py
  9. BIN
      mass_mailing_list_dynamic/static/description/icon.png
  10. 4
      mass_mailing_list_dynamic/tests/__init__.py
  11. 90
      mass_mailing_list_dynamic/tests/test_dynamic_lists.py
  12. 60
      mass_mailing_list_dynamic/views/mail_mass_mailing_list_view.xml

91
mass_mailing_list_dynamic/README.rst

@ -0,0 +1,91 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-3
==========================
Dynamic Mass Mailing Lists
==========================
Without this addon you have to choose between providing a dynamic domain and
letting your mass mailings reach all partners that match it, or being able to
unsubscribe to certain mailing lists while still being subscribed to others.
This addon allows you to create dynamic mailing lists, so you can now benefit
from both things.
Configuration
=============
To create a dynamic mailing list, you need to:
#. Go to *Mass Mailing > Mailings > Mailing Lists > Create*.
#. Check the *Dynamic* box.
#. Choose a *Sync method*:
- Leave empty to use as a manual mailing list, the normal behavior.
- *Only add new records* to make sure no records disappear from the list
when partners stop matching the *Synchronization critera*.
- *Add and remove records as needed* to make the list be fully synchronized
with the *Synchronization critera*, even if that means removing contacts
from it.
#. Define a *Synchronization critera* that will be used to match the partners
that should go into the list as contacts. Only partners with emails will
be selected.
Usage
=====
When you hit the *Sync now* button or send a mass mailing to this list, its
contacts will be automatically updated.
Pay attention to the messages shown to you that tell you about some non-obvious
behaviour you could experience if you edit manually contacts from a dynamic
list.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/10.0
Known issues / Roadmap
======================
* Tests affected by https://github.com/odoo/odoo/issues/20603. Do not run them
in stateful databases.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* `Tecnativa <https://www.tecnativa.com>`_:
* Jairo Llopis <jairo.llopis@tecnativa.com>
Do not contact contributors directly about support or help with technical issues.
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

4
mass_mailing_list_dynamic/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import models

20
mass_mailing_list_dynamic/__manifest__.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Dynamic Mass Mailing Lists",
"summary": "Mass mailing lists that get autopopulated",
"version": "10.0.1.0.0",
"category": "Marketing",
"website": "https://github.com/OCA/social",
"author": "Tecnativa, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"installable": True,
"depends": [
"mass_mailing_partner",
],
"data": [
"views/mail_mass_mailing_list_view.xml",
],
}

119
mass_mailing_list_dynamic/i18n/es.po

@ -0,0 +1,119 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * mass_mailing_list_dynamic
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-11-03 10:58+0000\n"
"PO-Revision-Date: 2017-11-03 12:00+0100\n"
"Last-Translator: Jairo Llopis <yajo.sk8@gmail.com>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: \n"
"X-Generator: Poedit 2.0.3\n"
#. module: mass_mailing_list_dynamic
#: model:ir.ui.view,arch_db:mass_mailing_list_dynamic.view_mail_mass_mailing_list_form
msgid ""
"<i class=\"fa fa-info-circle\"/> If you want to remove contacts from a "
"dynamic list, just <b>set them as <i>Opt Out</i></b>."
msgstr ""
"<i class=\"fa fa-info-circle\"/> Si quiere eliminar contactos de una lista "
"dinámica, simplemente <b>márquelos como <i>Envío no deseado</i></b>."
#. module: mass_mailing_list_dynamic
#: model:ir.ui.view,arch_db:mass_mailing_list_dynamic.view_mail_mass_mailing_list_form
msgid ""
"<i class=\"fa fa-info-circle\"/> You cannot make manual editions of contacts "
"in fully synchronized lists."
msgstr ""
"<i class=\"fa fa-info-circle\"/> No puede editar manualmente los contactos "
"en las listas que están completamente sincronizadas."
#. module: mass_mailing_list_dynamic
#: selection:mail.mass_mailing.list,sync_method:0
msgid "Add and remove records as needed"
msgstr "Añade y elimina los registros conforme se necesite"
#. module: mass_mailing_list_dynamic
#: code:addons/mass_mailing_list_dynamic/models/mail_mass_mailing_contact.py:17
#, python-format
msgid ""
"Cannot edit manually contacts in a fully synchronized list. Change its sync "
"method or execute a manual sync instead."
msgstr ""
"No se pueden editar manualmente los contactos de una lista que está "
"completamente sincronizada. En vez de eso, cambie su método de "
"sincronización o ejecute una sincronización manual."
#. module: mass_mailing_list_dynamic
#: model:ir.model.fields,help:mass_mailing_list_dynamic.field_mail_mass_mailing_list_sync_method
msgid ""
"Choose the syncronization method for this list if you want to make it dynamic"
msgstr ""
"Escoja el método de sincronización para esta lista si quiere hacerla dinámica"
#. module: mass_mailing_list_dynamic
#: model:ir.model.fields,field_description:mass_mailing_list_dynamic.field_mail_mass_mailing_list_dynamic
msgid "Dynamic"
msgstr "Dinámica"
#. module: mass_mailing_list_dynamic
#: model:ir.ui.view,arch_db:mass_mailing_list_dynamic.view_mail_mass_mailing_list_form
msgid "Dynamic list"
msgstr "Lista dinámica"
#. module: mass_mailing_list_dynamic
#: model:ir.model.fields,help:mass_mailing_list_dynamic.field_mail_mass_mailing_list_sync_domain
msgid "Filter partners to sync in this list"
msgstr "Filtrar contactos a sincronizar en esta lista"
#. module: mass_mailing_list_dynamic
#: model:ir.model,name:mass_mailing_list_dynamic.model_mail_mass_mailing_list
msgid "Mailing List"
msgstr "Lista de correo"
#. module: mass_mailing_list_dynamic
#: model:ir.model,name:mass_mailing_list_dynamic.model_mail_mass_mailing
msgid "Mass Mailing"
msgstr "Correo masivo"
#. module: mass_mailing_list_dynamic
#: model:ir.model,name:mass_mailing_list_dynamic.model_mail_mass_mailing_contact
msgid "Mass Mailing Contact"
msgstr "Contacto de correo masivo"
#. module: mass_mailing_list_dynamic
#: selection:mail.mass_mailing.list,sync_method:0
msgid "Only add new records"
msgstr "Solamente añadir nuevos registros"
#. module: mass_mailing_list_dynamic
#: model:ir.model.fields,field_description:mass_mailing_list_dynamic.field_mail_mass_mailing_list_sync_method
msgid "Sync method"
msgstr "Método de sincronización"
#. module: mass_mailing_list_dynamic
#: model:ir.ui.view,arch_db:mass_mailing_list_dynamic.view_mail_mass_mailing_list_form
msgid "Sync now"
msgstr "Sincronizar ahora"
#. module: mass_mailing_list_dynamic
#: model:ir.model.fields,field_description:mass_mailing_list_dynamic.field_mail_mass_mailing_list_sync_domain
msgid "Synchronization critera"
msgstr "Criterios de sincronización"
#~ msgid ""
#~ "<i class=\"fa fa-exclamation-circle\"/> If you manually add contacts to "
#~ "the list, <b>they might get removed later automatically!</b>"
#~ msgstr ""
#~ "<i class=\"fa fa-exclamation-circle\"/> Si añade contactos a la lista "
#~ "manualmente, <b>¡podrían ser eliminados automáticamente más tarde!</b>"
#~ msgid "Sync domain"
#~ msgstr "Dominio de sincronización"

6
mass_mailing_list_dynamic/models/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import mail_mass_mailing
from . import mail_mass_mailing_contact
from . import mail_mass_mailing_list

14
mass_mailing_list_dynamic/models/mail_mass_mailing.py

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
class MassMailing(models.Model):
_inherit = "mail.mass_mailing"
def send_mail(self):
"""Sync dynamic lists before sending mailings to them."""
self.contact_list_ids.action_sync()
return super(MassMailing, self).send_mail()

20
mass_mailing_list_dynamic/models/mail_mass_mailing_contact.py

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, models
from odoo.exceptions import ValidationError
class MassMailingContact(models.Model):
_inherit = "mail.mass_mailing.contact"
@api.constrains("partner_id", "list_id", "name", "email")
def _check_no_manual_edits_on_fully_synced_lists(self):
if self.env.context.get("syncing"):
return
if any(one.list_id.sync_method == "full" for one in self):
raise ValidationError(
_("Cannot edit manually contacts in a fully "
"synchronized list. Change its sync method or execute "
"a manual sync instead."))

57
mass_mailing_list_dynamic/models/mail_mass_mailing_list.py

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models
from odoo.tools import safe_eval
class MassMailingList(models.Model):
_inherit = "mail.mass_mailing.list"
dynamic = fields.Boolean(
help="Set this list as dynamic, to make it autosynchronized with "
"partners from a given criteria.",
)
sync_method = fields.Selection(
[
("add", "Only add new records"),
("full", "Add and remove records as needed"),
],
default="add",
required=True,
help="Choose the syncronization method for this list if you want to "
"make it dynamic",
)
sync_domain = fields.Char(
string="Synchronization critera",
default="[('opt_out', '=', False), ('email', '!=', False)]",
required=True,
help="Filter partners to sync in this list",
)
def action_sync(self):
"""Sync contacts in dynamic lists."""
Contact = self.env["mail.mass_mailing.contact"]
Partner = self.env["res.partner"]
# Skip non-dynamic lists
dynamic = self.filtered("dynamic").with_context(syncing=True)
for one in dynamic:
sync_domain = safe_eval(one.sync_domain) + [("email", "!=", False)]
desired_partners = Partner.search(sync_domain)
# Remove undesired contacts when synchronization is full
if one.sync_method == "full":
Contact.search([
("list_id", "=", one.id),
("partner_id", "not in", desired_partners.ids),
]).unlink()
current_contacts = Contact.search([("list_id", "=", one.id)])
current_partners = current_contacts.mapped("partner_id")
# Add new contacts
for partner in desired_partners - current_partners:
Contact.create({
"list_id": one.id,
"partner_id": partner.id,
})
# Invalidate cached contact count
self.invalidate_cache(["contact_nbr"], dynamic.ids)

BIN
mass_mailing_list_dynamic/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

4
mass_mailing_list_dynamic/tests/__init__.py

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import test_dynamic_lists

90
mass_mailing_list_dynamic/tests/test_dynamic_lists.py

@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from mock import patch
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
class DynamicListCase(TransactionCase):
def setUp(self):
super(DynamicListCase, self).setUp()
self.tag = self.env["res.partner.category"].create({
"name": "testing tag",
})
self.partners = self.env["res.partner"]
for number in range(5):
self.partners |= self.partners.create({
"name": "partner %d" % number,
"category_id": [(4, self.tag.id, False)],
"email": "%d@example.com" % number,
})
self.list = self.env["mail.mass_mailing.list"].create({
"name": "test list",
"dynamic": True,
"sync_domain": repr([("category_id", "in", self.tag.ids)]),
})
self.mail = self.env["mail.mass_mailing"].create({
"name": "test mass mailing",
"contact_list_ids": [(4, self.list.id, False)],
})
self.mail._onchange_model_and_list()
def test_list_sync(self):
"""List is synced correctly."""
Contact = self.env["mail.mass_mailing.contact"]
# Partner 0 is not categorized
self.partners[0].category_id = False
# Partner 1 has no email
self.partners[1].email = False
# Set list as unsynced
self.list.dynamic = False
# Create contact for partner 0 in unsynced list
contact0 = Contact.create({
"list_id": self.list.id,
"partner_id": self.partners[0].id,
})
self.assertEqual(self.list.contact_nbr, 1)
# Set list as add-synced
self.list.dynamic = True
self.list.action_sync()
self.assertEqual(self.list.contact_nbr, 4)
self.assertTrue(contact0.exists())
# Set list as full-synced
self.list.sync_method = "full"
self.list.action_sync()
self.assertEqual(self.list.contact_nbr, 3)
self.assertFalse(contact0.exists())
# Cannot add or edit contacts in fully synced lists
with self.assertRaises(ValidationError):
Contact.create({
"list_id": self.list.id,
"partner_id": self.partners[0].id,
})
contact1 = Contact.search([
("list_id", "=", self.list.id),
], limit=1)
with self.assertRaises(ValidationError):
contact1.name = "other"
with self.assertRaises(ValidationError):
contact1.email = "other@example.com"
with self.assertRaises(ValidationError):
contact1.partner_id = self.partners[0]
def test_sync_when_sending_mail(self):
"""Dynamic list is synced before mailing to it."""
self.list.action_sync()
self.assertEqual(self.list.contact_nbr, 5)
# Create a new partner
self.partners.create({
"name": "extra partner",
"category_id": [(4, self.tag.id, False)],
"email": "extra@example.com",
})
# Before sending the mail, the list is updated
with patch("odoo.addons.base.ir.ir_mail_server"
".IrMailServer.send_email") as send_email:
self.mail.send_mail()
self.assertEqual(6, send_email.call_count)
self.assertEqual(6, self.list.contact_nbr)

60
mass_mailing_list_dynamic/views/mail_mass_mailing_list_view.xml

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Tecnativa - Jairo Llopis
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_mail_mass_mailing_list_form" model="ir.ui.view">
<field name="name">Add sync stuff</field>
<field name="model">mail.mass_mailing.list</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_list_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@class='oe_title']" position="after">
<group name="dynamic" string="Dynamic list">
<group colspan="4">
<field
name="dynamic"
/>
</group>
<group attrs="{'invisible': [('dynamic', '=', False)]}">
<label for="sync_method"/>
<div>
<field
name="sync_method"
/>
<button
name="action_sync"
type="object"
string="Sync now"
icon="fa-refresh"
/>
</div>
</group>
<group attrs="{'invisible': [('dynamic', '=', False)]}">
<div
class="alert alert-info"
role="alert"
>
<i class="fa fa-info-circle"/> If you want to remove contacts from a dynamic list, just <b>set them as <i>Opt Out</i></b>.
</div>
<div
class="alert alert-warning"
role="alert"
>
<i class="fa fa-info-circle"/> You cannot make manual editions of contacts in fully synchronized lists.
</div>
</group>
<group colspan="4"
attrs="{'invisible': [('dynamic', '=', False)]}">
<field
name="sync_domain"
widget="char_domain"
options='{"model": "res.partner"}'
/>
</group>
</group>
</xpath>
</field>
</record>
</odoo>
Loading…
Cancel
Save