diff --git a/mass_mailing_partner/README.rst b/mass_mailing_partner/README.rst new file mode 100644 index 0000000..189f044 --- /dev/null +++ b/mass_mailing_partner/README.rst @@ -0,0 +1,109 @@ +=============================== +Link partners with mass-mailing +=============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/14.0/mass_mailing_partner + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-14-0/social-14-0-mass_mailing_partner + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/205/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module links mass-mailing contacts with partners. + +Features +~~~~~~~~ + +* When creating or saving a mass-mailing contact, partners are matched through + email, linking matched partner, or creating a new one if no match and the + maling list partner mandatory field is checked. +* Mailing contacts smart button in partner form. +* Mass mailing stats smart button in partner form. +* Filter and group by partner in mail statistics tree view + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +At first install, all existing mass mailing contacts are matched against +partners. And also mass mailing statistics are matched using model and res_id. + +Usage +===== + +In partner view, there is a new action called "Add to mailing list". This +action open a pop-up to select a mailing list. Selected partners will be added +as mailing list contacts. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Pedro M. Baeza + * Rafael Blasco + * Antonio Espinosa + * Javier Iniesta + * Jairo Llopis + * David Vidal + * Ernesto Tejeda + * Victor M.M. Torres + * Manuel Calero + * Víctor Martínez + +* `Hibou Corp. `_ + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mass_mailing_partner/__init__.py b/mass_mailing_partner/__init__.py new file mode 100644 index 0000000..4f890c3 --- /dev/null +++ b/mass_mailing_partner/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizard +from .hooks import post_init_hook diff --git a/mass_mailing_partner/__manifest__.py b/mass_mailing_partner/__manifest__.py new file mode 100644 index 0000000..f6bace1 --- /dev/null +++ b/mass_mailing_partner/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015-2016 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Link partners with mass-mailing", + "version": "14.0.1.1.0", + "author": "Tecnativa, " "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/social", + "license": "AGPL-3", + "category": "Marketing", + "depends": ["mass_mailing"], + "post_init_hook": "post_init_hook", + "data": [ + "security/ir.model.access.csv", + "views/mailing_trace_view.xml", + "views/mailing_contact_view.xml", + "views/mailing_view.xml", + "views/res_partner_view.xml", + "wizard/partner_mail_list_wizard.xml", + ], + "installable": True, +} diff --git a/mass_mailing_partner/hooks.py b/mass_mailing_partner/hooks.py new file mode 100644 index 0000000..f2fe4aa --- /dev/null +++ b/mass_mailing_partner/hooks.py @@ -0,0 +1,33 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2016 Antonio Espinosa - +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import SUPERUSER_ID, api + +_logger = logging.getLogger(__name__) + + +def post_init_hook(cr, registry): + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + # ACTION 1: Match existing contacts + contact_model = env["mailing.contact"] + partner_model = env["res.partner"] + contacts = contact_model.search([("email", "!=", False)]) + _logger.info("Trying to match %d contacts to partner by email", len(contacts)) + for contact in contacts: + partners = partner_model.search( + [("email", "=ilike", contact.email)], limit=1 + ) + if partners: + contact.write({"partner_id": partners.id}) + # ACTION 2: Match existing statistics + stat_model = env["mailing.trace"] + stats = stat_model.search([("model", "!=", False), ("res_id", "!=", False)]) + _logger.info("Trying to link %d mass mailing statistics to partner", len(stats)) + stats.partner_link() diff --git a/mass_mailing_partner/models/__init__.py b/mass_mailing_partner/models/__init__.py new file mode 100644 index 0000000..19ba29a --- /dev/null +++ b/mass_mailing_partner/models/__init__.py @@ -0,0 +1,7 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import mailing_contact +from . import mailing_list +from . import mailing_trace +from . import mailing_contact_subscription +from . import res_partner diff --git a/mass_mailing_partner/models/mailing_contact.py b/mass_mailing_partner/models/mailing_contact.py new file mode 100644 index 0000000..0c3457d --- /dev/null +++ b/mass_mailing_partner/models/mailing_contact.py @@ -0,0 +1,107 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2017 David Vidal +# Copyright 2020 Tecnativa - Manuel Calero +# Copyright 2020 Hibou Corp. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class MailingContact(models.Model): + _inherit = "mailing.contact" + + partner_id = fields.Many2one( + comodel_name="res.partner", string="Partner", domain=[("email", "!=", False)] + ) + + @api.constrains("partner_id", "list_ids") + def _check_partner_id_list_ids(self): + for contact in self: + if contact.partner_id: + other_contact = self.search( + [ + ("partner_id", "=", contact.partner_id.id), + ("id", "!=", contact.id), + ] + ) + if contact.list_ids & other_contact.mapped("list_ids"): + raise ValidationError( + _("Partner already exists in one of these mailing lists") + + ": %s" % contact.partner_id.display_name + ) + + @api.onchange("partner_id") + def _onchange_partner_mass_mailing_partner(self): + if self.partner_id: + self.name = self.partner_id.name + self.email = self.partner_id.email + self.title_id = self.partner_id.title + self.company_name = self.partner_id.company_id.name + self.country_id = self.partner_id.country_id + category_ids = self.partner_id.category_id + if category_ids: + self.tag_ids = category_ids + + @api.model + def create(self, vals): + record = self.new(vals) + if not record.partner_id: + record._set_partner() + record._onchange_partner_mass_mailing_partner() + new_vals = record._convert_to_write(record._cache) + new_vals.update( + subscription_list_ids=vals.get("subscription_list_ids", []), + list_ids=vals.get("list_ids", []), + ) + return super().create(new_vals) + + def write(self, vals): + for contact in self: + new_vals = contact.copy_data(vals)[0] + record = self.new(new_vals) + if not record.partner_id: + record._set_partner() + record._onchange_partner_mass_mailing_partner() + new_vals = record._convert_to_write(record._cache) + new_vals.update( + subscription_list_ids=vals.get("subscription_list_ids", []), + list_ids=vals.get("list_ids", []), + ) + super(MailingContact, contact).write(new_vals) + return True + + def _get_categories(self): + ca_ids = self.tag_ids.ids + self.list_ids.mapped("partner_category.id") + return [[6, 0, ca_ids]] + + def _prepare_partner(self): + return { + "name": self.name or self.email, + "email": self.email, + "country_id": self.country_id.id, + "title": self.title_id.id, + "company_name": self.company_name, + "company_id": False, + "category_id": self._get_categories(), + } + + def _set_partner(self): + self.ensure_one() + if not self.email: + return + m_partner = self.env["res.partner"] + # Look for a partner with that email + email = self.email.strip() + partner = m_partner.search([("email", "=ilike", email)], limit=1) + if partner: + # Partner found + self.partner_id = partner + else: + lts = self.subscription_list_ids.mapped("list_id") | self.list_ids + if lts.filtered("partner_mandatory"): + # Create partner + partner_vals = self._prepare_partner() + self.partner_id = m_partner.sudo().create(partner_vals) diff --git a/mass_mailing_partner/models/mailing_contact_subscription.py b/mass_mailing_partner/models/mailing_contact_subscription.py new file mode 100644 index 0000000..cec47dd --- /dev/null +++ b/mass_mailing_partner/models/mailing_contact_subscription.py @@ -0,0 +1,20 @@ +# Copyright 2018 Tecnativa - Ernesto Tejeda +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, models +from odoo.exceptions import ValidationError + + +class MailingContactSubscription(models.Model): + _inherit = "mailing.contact.subscription" + + @api.constrains("contact_id", "list_id") + def _check_contact_id_partner_id_list_id(self): + for rel in self: + if rel.contact_id.partner_id: + contacts = rel.list_id.contact_ids - rel.contact_id + if rel.contact_id.partner_id in contacts.mapped("partner_id"): + raise ValidationError( + _("A partner cannot be multiple times in the same list") + ) diff --git a/mass_mailing_partner/models/mailing_list.py b/mass_mailing_partner/models/mailing_list.py new file mode 100644 index 0000000..f6d9727 --- /dev/null +++ b/mass_mailing_partner/models/mailing_list.py @@ -0,0 +1,34 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class MailingList(models.Model): + _inherit = "mailing.list" + + partner_mandatory = fields.Boolean(string="Mandatory Partner", default=False) + partner_category = fields.Many2one( + comodel_name="res.partner.category", string="Partner Tag" + ) + + @api.constrains("contact_ids") + def _check_contact_ids_partner_id(self): + contact_obj = self.env["mailing.contact"] + for mailing_list in self: + data = contact_obj.read_group( + [ + ("id", "in", mailing_list.contact_ids.ids), + ("partner_id", "!=", False), + ], + ["partner_id"], + ["partner_id"], + ) + if len(list(filter(lambda r: r["partner_id_count"] > 1, data))): + raise ValidationError( + _("A partner cannot be multiple times " "in the same list") + ) diff --git a/mass_mailing_partner/models/mailing_trace.py b/mass_mailing_partner/models/mailing_trace.py new file mode 100644 index 0000000..57fd606 --- /dev/null +++ b/mass_mailing_partner/models/mailing_trace.py @@ -0,0 +1,37 @@ +# Copyright 2016 Antonio Espinosa - +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class MailingTrace(models.Model): + _inherit = "mailing.trace" + + partner_id = fields.Many2one( + string="Partner", comodel_name="res.partner", readonly=True + ) + + @api.model + def partner_id_from_obj(self, model, res_id): + partner_id = False + obj = self.env[model].browse(res_id) + if obj.exists(): + if model == "res.partner": + partner_id = res_id + elif "partner_id" in obj._fields: + partner_id = obj.partner_id.id + return partner_id + + def partner_link(self): + for stat in self.filtered(lambda r: r.model and r.res_id): + partner_id = self.partner_id_from_obj(stat.model, stat.res_id) + if partner_id != stat.partner_id.id: + stat.partner_id = partner_id + return True + + @api.model + def create(self, vals): + stat = super().create(vals) + stat.partner_link() + return stat diff --git a/mass_mailing_partner/models/res_partner.py b/mass_mailing_partner/models/res_partner.py new file mode 100644 index 0000000..266b3fa --- /dev/null +++ b/mass_mailing_partner/models/res_partner.py @@ -0,0 +1,94 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2017 David Vidal +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + mass_mailing_contact_ids = fields.One2many( + string="Mailing contacts", + comodel_name="mailing.contact", + inverse_name="partner_id", + ) + mass_mailing_contacts_count = fields.Integer( + string="Mailing contacts number", + compute="_compute_mass_mailing_contacts_count", + store=True, + compute_sudo=True, + ) + mass_mailing_stats_ids = fields.One2many( + string="Mass mailing stats", + comodel_name="mailing.trace", + inverse_name="partner_id", + ) + mass_mailing_stats_count = fields.Integer( + string="Mass mailing stats number", + compute="_compute_mass_mailing_stats_count", + store=True, + ) + + @api.constrains("email") + def _check_email_mass_mailing_contacts(self): + for partner in self: + if not partner.email and partner.sudo().mass_mailing_contact_ids: + raise ValidationError( + _( + "This partner '%s' is linked to one or more mass " + "mailing contact. Email must be assigned." + ) + % partner.name + ) + + @api.depends("mass_mailing_contact_ids") + def _compute_mass_mailing_contacts_count(self): + contact_data = self.env["mailing.contact"].read_group( + [("partner_id", "in", self.ids)], ["partner_id"], ["partner_id"] + ) + mapped_data = { + contact["partner_id"][0]: contact["partner_id_count"] + for contact in contact_data + } + for partner in self: + partner.mass_mailing_contacts_count = mapped_data.get(partner.id, 0) + + @api.depends("mass_mailing_stats_ids") + def _compute_mass_mailing_stats_count(self): + contact_data = self.env["mailing.trace"].read_group( + [("partner_id", "in", self.ids)], ["partner_id"], ["partner_id"] + ) + mapped_data = { + contact["partner_id"][0]: contact["partner_id_count"] + for contact in contact_data + } + for partner in self: + partner.mass_mailing_stats_count = mapped_data.get(partner.id, 0) + + def write(self, vals): + res = super().write(vals) + mm_vals = {} + if vals.get("name"): + mm_vals["name"] = vals["name"] + if vals.get("email"): + mm_vals["email"] = vals["email"] + if vals.get("title"): + mm_vals["title_id"] = vals["title"] + if vals.get("parent_id"): + parent = self.browse(vals.get("parent_id")) + mm_vals["company_name"] = parent.commercial_company_name + if vals.get("country_id"): + mm_vals["country_id"] = vals["country_id"] + if vals.get("category_id"): + mm_vals["tag_ids"] = vals["category_id"] + if mm_vals: + # Using sudo because ACLs shouldn't produce data inconsistency + self.env["mailing.contact"].sudo().search( + [("partner_id", "in", self.ids)] + ).write(mm_vals) + return res diff --git a/mass_mailing_partner/readme/CONFIGURE.rst b/mass_mailing_partner/readme/CONFIGURE.rst new file mode 100644 index 0000000..b8119b8 --- /dev/null +++ b/mass_mailing_partner/readme/CONFIGURE.rst @@ -0,0 +1,2 @@ +At first install, all existing mass mailing contacts are matched against +partners. And also mass mailing statistics are matched using model and res_id. diff --git a/mass_mailing_partner/readme/CONTRIBUTORS.rst b/mass_mailing_partner/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..d599115 --- /dev/null +++ b/mass_mailing_partner/readme/CONTRIBUTORS.rst @@ -0,0 +1,14 @@ +* `Tecnativa `_: + + * Pedro M. Baeza + * Rafael Blasco + * Antonio Espinosa + * Javier Iniesta + * Jairo Llopis + * David Vidal + * Ernesto Tejeda + * Victor M.M. Torres + * Manuel Calero + * Víctor Martínez + +* `Hibou Corp. `_ diff --git a/mass_mailing_partner/readme/DESCRIPTION.rst b/mass_mailing_partner/readme/DESCRIPTION.rst new file mode 100644 index 0000000..ee5e635 --- /dev/null +++ b/mass_mailing_partner/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +This module links mass-mailing contacts with partners. + +Features +~~~~~~~~ + +* When creating or saving a mass-mailing contact, partners are matched through + email, linking matched partner, or creating a new one if no match and the + maling list partner mandatory field is checked. +* Mailing contacts smart button in partner form. +* Mass mailing stats smart button in partner form. +* Filter and group by partner in mail statistics tree view diff --git a/mass_mailing_partner/readme/USAGE.rst b/mass_mailing_partner/readme/USAGE.rst new file mode 100644 index 0000000..1149221 --- /dev/null +++ b/mass_mailing_partner/readme/USAGE.rst @@ -0,0 +1,3 @@ +In partner view, there is a new action called "Add to mailing list". This +action open a pop-up to select a mailing list. Selected partners will be added +as mailing list contacts. diff --git a/mass_mailing_partner/security/ir.model.access.csv b/mass_mailing_partner/security/ir.model.access.csv new file mode 100644 index 0000000..4a7fbc6 --- /dev/null +++ b/mass_mailing_partner/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_partner_mail_list_wizard,access_partner_mail_list_wizard,model_partner_mail_list_wizard,base.group_user,1,1,1,0 diff --git a/mass_mailing_partner/static/description/icon.png b/mass_mailing_partner/static/description/icon.png new file mode 100644 index 0000000..b18b21a Binary files /dev/null and b/mass_mailing_partner/static/description/icon.png differ diff --git a/mass_mailing_partner/static/description/index.html b/mass_mailing_partner/static/description/index.html new file mode 100644 index 0000000..6c7f9a0 --- /dev/null +++ b/mass_mailing_partner/static/description/index.html @@ -0,0 +1,455 @@ + + + + + + +Link partners with mass-mailing + + + + + + diff --git a/mass_mailing_partner/tests/__init__.py b/mass_mailing_partner/tests/__init__.py new file mode 100644 index 0000000..cb4bd14 --- /dev/null +++ b/mass_mailing_partner/tests/__init__.py @@ -0,0 +1,7 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_mail_mass_mailing_contact, test_res_partner +from . import test_mail_mail_statistics +from . import test_partner_mail_list_wizard +from . import test_mail_mass_mailing_list +from . import test_mail_mass_mailing_list_contact_rel diff --git a/mass_mailing_partner/tests/base.py b/mass_mailing_partner/tests/base.py new file mode 100644 index 0000000..77b9e94 --- /dev/null +++ b/mass_mailing_partner/tests/base.py @@ -0,0 +1,67 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests.common import TransactionCase + + +class BaseCase(TransactionCase): + def setUp(self): + super(BaseCase, self).setUp() + + self.main_company = self.env.ref("base.main_company") + self.country_es = self.env.ref("base.es") + self.category_0 = self.env.ref("base.res_partner_category_0") + self.category_2 = self.env.ref("base.res_partner_category_2") + self.title_mister = self.env.ref("base.res_partner_title_mister") + self.partner = self.create_partner( + { + "name": "Partner test", + "email": "partner@test.com", + "title": self.title_mister.id, + "company_id": self.main_company.id, + "country_id": self.country_es.id, + "category_id": [(6, 0, (self.category_0 | self.category_2).ids)], + } + ) + + self.category_3 = self.env.ref("base.res_partner_category_3") + self.mailing_list = self.create_mailing_list({"name": "List test"}) + self.mailing_list2 = self.create_mailing_list( + { + "name": "List test 2", + "partner_mandatory": True, + "partner_category": self.category_3.id, + } + ) + + def create_partner(self, vals): + m_partner = self.env["res.partner"] + return m_partner.create(vals) + + def create_mailing_contact(self, vals): + m_mailing_contact = self.env["mailing.contact"] + return m_mailing_contact.create(vals) + + def create_mailing_list(self, vals): + m_mailing_list = self.env["mailing.list"] + return m_mailing_list.create(vals) + + def check_mailing_contact_partner(self, mailing_contact): + if mailing_contact.partner_id: + self.assertEqual(mailing_contact.partner_id.email, mailing_contact.email) + self.assertEqual(mailing_contact.partner_id.name, mailing_contact.name) + self.assertEqual(mailing_contact.partner_id.title, mailing_contact.title_id) + if mailing_contact.partner_id.company_id: + self.assertEqual( + mailing_contact.partner_id.company_id.name, + mailing_contact.company_name, + ) + self.assertEqual( + mailing_contact.partner_id.country_id, mailing_contact.country_id + ) + self.assertEqual( + mailing_contact.partner_id.category_id, mailing_contact.tag_ids + ) diff --git a/mass_mailing_partner/tests/test_mail_mail_statistics.py b/mass_mailing_partner/tests/test_mail_mail_statistics.py new file mode 100644 index 0000000..1ecdf2b --- /dev/null +++ b/mass_mailing_partner/tests/test_mail_mail_statistics.py @@ -0,0 +1,30 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import base + + +class MailMailStatisticsCase(base.BaseCase): + def test_link_partner(self): + partner = self.create_partner({"name": "Test partner"}) + stat = self.env["mailing.trace"].create( + {"model": "res.partner", "res_id": partner.id} + ) + self.assertEqual(partner.id, stat.partner_id.id) + + def test_link_mail_contact(self): + partner = self.create_partner( + {"name": "Test partner", "email": "test@domain.com"} + ) + contact_vals = { + "partner_id": partner.id, + "list_ids": [[6, 0, [self.mailing_list.id]]], + } + contact = self.create_mailing_contact(contact_vals) + stat = self.env["mailing.trace"].create( + {"model": "mailing.contact", "res_id": contact.id} + ) + self.assertEqual(partner.id, stat.partner_id.id) diff --git a/mass_mailing_partner/tests/test_mail_mass_mailing_contact.py b/mass_mailing_partner/tests/test_mail_mass_mailing_contact.py new file mode 100644 index 0000000..c67b282 --- /dev/null +++ b/mass_mailing_partner/tests/test_mail_mass_mailing_contact.py @@ -0,0 +1,142 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from ..hooks import post_init_hook +from . import base + + +class MailMassMailingContactCase(base.BaseCase): + def test_match_existing_contacts(self): + contact = self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [(6, 0, self.mailing_list.ids)]} + ) + post_init_hook(self.cr, self.registry) + self.assertEqual(contact.partner_id.id, self.partner.id) + self.check_mailing_contact_partner(contact) + + def test_create_mass_mailing_contact(self): + title_doctor = self.env.ref("base.res_partner_title_doctor") + country_cu = self.env.ref("base.cu") + category_8 = self.env.ref("base.res_partner_category_8") + category_11 = self.env.ref("base.res_partner_category_11") + contact_vals = { + "name": "Partner test 2", + "email": "partner2@test.com", + "title_id": title_doctor.id, + "company_name": "TestCompany", + "country_id": country_cu.id, + "tag_ids": [(6, 0, (category_8 | category_11).ids)], + "list_ids": [(6, 0, (self.mailing_list | self.mailing_list2).ids)], + } + contact = self.create_mailing_contact(contact_vals) + self.check_mailing_contact_partner(contact) + with self.assertRaises(ValidationError): + self.create_mailing_contact( + { + "email": "partner2@test.com", + "list_ids": [[6, 0, [self.mailing_list2.id]]], + } + ) + + def test_create_mass_mailing_contact_with_subscription(self): + title_doctor = self.env.ref("base.res_partner_title_doctor") + country_cu = self.env.ref("base.cu") + category_8 = self.env.ref("base.res_partner_category_8") + category_11 = self.env.ref("base.res_partner_category_11") + contact_vals = { + "name": "Partner test 2", + "email": "partner2@test.com", + "title_id": title_doctor.id, + "company_name": "TestCompany", + "country_id": country_cu.id, + "tag_ids": [(6, 0, (category_8 | category_11).ids)], + "subscription_list_ids": [ + (0, 0, {"list_id": self.mailing_list.id}), + (0, 0, {"list_id": self.mailing_list2.id}), + ], + } + contact = self.create_mailing_contact(contact_vals) + self.check_mailing_contact_partner(contact) + with self.assertRaises(ValidationError): + self.create_mailing_contact( + { + "email": "partner2@test.com", + "subscription_list_ids": [ + (0, 0, {"list_id": self.mailing_list2.id}) + ], + } + ) + + def test_write_mass_mailing_contact(self): + contact = self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [(6, 0, self.mailing_list.ids)]} + ) + contact.write({"partner_id": False}) + self.check_mailing_contact_partner(contact) + contact2 = self.create_mailing_contact( + { + "email": "partner2@test.com", + "name": "Partner test 2", + "list_ids": [(6, 0, self.mailing_list.ids)], + } + ) + contact2.write({"partner_id": False}) + self.assertFalse(contact2.partner_id) + + def test_onchange_partner(self): + contact = self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [[6, 0, [self.mailing_list.id]]]} + ) + title_doctor = self.env.ref("base.res_partner_title_doctor") + country_cu = self.env.ref("base.cu") + category_8 = self.env.ref("base.res_partner_category_8") + category_11 = self.env.ref("base.res_partner_category_11") + partner_vals = { + "name": "Partner test 2", + "email": "partner2@test.com", + "title": title_doctor.id, + "company_id": self.main_company.id, + "country_id": country_cu.id, + "category_id": [(6, 0, (category_8 | category_11).ids)], + } + partner = self.create_partner(partner_vals) + contact.partner_id = partner + contact._onchange_partner_mass_mailing_partner() + self.check_mailing_contact_partner(contact) + + def test_partners_merge(self): + partner_1 = self.create_partner({"name": "Demo 1", "email": "demo1@demo.com"}) + partner_2 = self.create_partner({"name": "Demo 2", "email": "demo2@demo.com"}) + list_1 = self.create_mailing_list({"name": "List test Partners Merge 1"}) + list_2 = self.create_mailing_list({"name": "List test Partners Merge 2"}) + contact_1 = self.create_mailing_contact( + { + "email": partner_1.email, + "name": partner_1.name, + "partner_id": partner_1.id, + "list_ids": [(6, 0, [list_1.id])], + } + ) + contact_2 = self.create_mailing_contact( + { + "email": partner_2.email, + "name": partner_2.name, + "partner_id": partner_2.id, + "list_ids": [(6, 0, [list_1.id, list_2.id])], + } + ) + # Wizard partner merge (partner_1 + partner_2) in partner_i1 + wizard = self.env["base.partner.merge.automatic.wizard"].create( + {"state": "option"} + ) + wizard._merge((partner_1 + partner_2).ids, partner_1) + contact = self.env["mailing.contact"].search( + [("id", "in", (contact_1 + contact_2).ids)] + ) + self.assertEqual(len(contact), 1) + self.assertEqual(contact.list_ids.ids, (list_1 + list_2).ids) diff --git a/mass_mailing_partner/tests/test_mail_mass_mailing_list.py b/mass_mailing_partner/tests/test_mail_mass_mailing_list.py new file mode 100644 index 0000000..570b700 --- /dev/null +++ b/mass_mailing_partner/tests/test_mail_mass_mailing_list.py @@ -0,0 +1,39 @@ +# Copyright 2018 Tecnativa - Ernesto tejeda +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from . import base + + +class MailMassMailingListCase(base.BaseCase): + def test_create_mass_mailing_list(self): + contact_test_1 = self.create_mailing_contact( + {"name": "Contact test 1", "partner_id": self.partner.id} + ) + contact_test_2 = self.create_mailing_contact( + {"name": "Contact test 2", "partner_id": self.partner.id} + ) + with self.assertRaises(ValidationError): + self.create_mailing_list( + { + "name": "List test Create Mailing List", + "contact_ids": [(6, 0, (contact_test_1 | contact_test_2).ids)], + } + ) + + def test_create_mass_mailing_list_with_subscription(self): + contact_test_1 = self.create_mailing_contact( + {"name": "Contact test 1", "partner_id": self.partner.id} + ) + contact_test_2 = self.create_mailing_contact( + {"name": "Contact test 2", "partner_id": self.partner.id} + ) + with self.assertRaises(ValidationError): + self.create_mailing_list( + { + "name": "List test Creat List With Subscription", + "contact_ids": [(4, contact_test_1.id), (4, contact_test_2.id)], + } + ) diff --git a/mass_mailing_partner/tests/test_mail_mass_mailing_list_contact_rel.py b/mass_mailing_partner/tests/test_mail_mass_mailing_list_contact_rel.py new file mode 100644 index 0000000..5d19b21 --- /dev/null +++ b/mass_mailing_partner/tests/test_mail_mass_mailing_list_contact_rel.py @@ -0,0 +1,22 @@ +# Copyright 2018 Tecnativa - Ernesto tejeda +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from . import base + + +class MailMassMailingListContactRelCase(base.BaseCase): + def test_create_mass_mailing_list(self): + contact_test_1 = self.create_mailing_contact( + {"name": "Contact test 1", "partner_id": self.partner.id} + ) + contact_test_2 = self.create_mailing_contact( + {"name": "Contact test 2", "partner_id": self.partner.id} + ) + list_3 = self.create_mailing_list( + {"name": "List test 3", "contact_ids": [(4, contact_test_1.id)]} + ) + with self.assertRaises(ValidationError): + list_3.contact_ids = [(4, contact_test_2.id)] diff --git a/mass_mailing_partner/tests/test_partner_mail_list_wizard.py b/mass_mailing_partner/tests/test_partner_mail_list_wizard.py new file mode 100644 index 0000000..20beef6 --- /dev/null +++ b/mass_mailing_partner/tests/test_partner_mail_list_wizard.py @@ -0,0 +1,43 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import UserError + +from . import base + + +class PartnerMailListWizardCase(base.BaseCase): + def test_add_to_mail_list(self): + wizard = self.env["partner.mail.list.wizard"].create( + {"mail_list_id": self.mailing_list.id} + ) + wizard.partner_ids = [self.partner.id] + wizard.add_to_mail_list() + contacts = self.env["mailing.contact"].search( + [("partner_id", "=", self.partner.id)] + ) + cont = contacts.filtered(lambda r: wizard.mail_list_id in r.list_ids) + self.assertEqual(len(cont), 1) + self.check_mailing_contact_partner(cont) + # This line does not create a new contact + wizard.add_to_mail_list() + self.assertEqual(len(self.partner.mass_mailing_contact_ids), 1) + self.assertEqual( + self.partner.mass_mailing_contact_ids.list_ids, self.mailing_list + ) + + list_2 = self.create_mailing_list({"name": "Test Add to List"}) + wizard.mail_list_id = list_2 + wizard.add_to_mail_list() + self.assertEqual(len(self.partner.mass_mailing_contact_ids), 1) + self.assertEqual( + self.partner.mass_mailing_contact_ids.list_ids, self.mailing_list | list_2 + ) + + partner = self.env["res.partner"].create({"name": "No email partner"}) + wizard.partner_ids = [partner.id] + with self.assertRaises(UserError): + wizard.add_to_mail_list() diff --git a/mass_mailing_partner/tests/test_res_partner.py b/mass_mailing_partner/tests/test_res_partner.py new file mode 100644 index 0000000..779ae99 --- /dev/null +++ b/mass_mailing_partner/tests/test_res_partner.py @@ -0,0 +1,61 @@ +# Copyright 2015 Tecnativa - Pedro M. Baeza +# Copyright 2015 Tecnativa - Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# Copyright 2021 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.exceptions import ValidationError + +from . import base + + +class ResPartnerCase(base.BaseCase): + def test_count_mass_mailing_contacts(self): + self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [[6, 0, [self.mailing_list.id]]]} + ) + self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [[6, 0, [self.mailing_list2.id]]]} + ) + self.assertEqual(self.partner.mass_mailing_contacts_count, 2) + + def test_write_res_partner(self): + contact = self.create_mailing_contact( + {"email": "partner@test.com", "list_ids": [[6, 0, [self.mailing_list.id]]]} + ) + self.assertEqual(self.partner, contact.partner_id) + + title_doctor = self.env.ref("base.res_partner_title_doctor") + country_cu = self.env.ref("base.cu") + category_8 = self.env.ref("base.res_partner_category_8") + category_11 = self.env.ref("base.res_partner_category_11") + self.partner.write( + { + "name": "Changed", + "email": "partner@changed.com", + "title": title_doctor.id, + "company_id": self.main_company.id, + "country_id": country_cu.id, + "category_id": [(6, 0, (category_8 | category_11).ids)], + } + ) + self.check_mailing_contact_partner(contact) + with self.assertRaises(ValidationError): + self.partner.write({"email": False}) + + def test_write_res_partner_multi(self): + self.assertEqual(len(self.partner.category_id.ids), 2) + partner2 = self.partner.copy({"name": "Partner test 2"}) + self.partner.write({"category_id": [(4, self.category_3.id)]}) + self.assertEqual(len(self.partner.category_id.ids), 3) + self.assertEqual(len(partner2.category_id.ids), 2) + for partner in [self.partner, partner2]: + self.create_mailing_contact( + {"partner_id": partner.id, "list_ids": [[6, 0, [self.mailing_list.id]]]} + ) + self.env["res.partner"].search( + [("id", "in", (self.partner.id, partner2.id))] + ).write({"category_id": [(4, self.category_3.id)]}) + self.assertEqual(len(self.partner.category_id.ids), 3) + self.assertEqual(len(partner2.category_id.ids), 3) diff --git a/mass_mailing_partner/views/mailing_contact_view.xml b/mass_mailing_partner/views/mailing_contact_view.xml new file mode 100644 index 0000000..0b2e8c3 --- /dev/null +++ b/mass_mailing_partner/views/mailing_contact_view.xml @@ -0,0 +1,80 @@ + + + + + + mailing.contact.tree.inherit + mailing.contact + + + + + + + + + + mailing.contact.form.partner + mailing.contact + + + + + + + {'readonly': [('partner_id', '!=', False)]} + + + {'readonly': [('partner_id', '!=', False)]} + + + {'readonly': [('partner_id', '!=', False)]} + + + {'readonly': [('partner_id', '!=', False)]} + + + {'readonly': [('partner_id', '!=', False)]} + + + {'readonly': [('partner_id', '!=', False)]} + + + + + + Add partner search field and group by + mailing.contact + + + + + + + + + + + + diff --git a/mass_mailing_partner/views/mailing_trace_view.xml b/mass_mailing_partner/views/mailing_trace_view.xml new file mode 100644 index 0000000..9d091bd --- /dev/null +++ b/mass_mailing_partner/views/mailing_trace_view.xml @@ -0,0 +1,47 @@ + + + + + + Add partner field + mailing.trace + + + + + + + + + + Add partner column + mailing.trace + + + + + + + + + + Add partner search field and group by + mailing.trace + + + + + + + + + + + + diff --git a/mass_mailing_partner/views/mailing_view.xml b/mass_mailing_partner/views/mailing_view.xml new file mode 100644 index 0000000..3ed5e65 --- /dev/null +++ b/mass_mailing_partner/views/mailing_view.xml @@ -0,0 +1,25 @@ + + + + + + mailing.list.form + mailing.list + + + + + + + + + + + + + + diff --git a/mass_mailing_partner/views/res_partner_view.xml b/mass_mailing_partner/views/res_partner_view.xml new file mode 100644 index 0000000..21afbfd --- /dev/null +++ b/mass_mailing_partner/views/res_partner_view.xml @@ -0,0 +1,72 @@ + + + + + + Partner Form with mailing contacts + res.partner + + + +
+ + +
+
+
+ + + Partner Search with mailing contacts + res.partner + + + 20 + + + + + + + +
diff --git a/mass_mailing_partner/wizard/__init__.py b/mass_mailing_partner/wizard/__init__.py new file mode 100644 index 0000000..e374d10 --- /dev/null +++ b/mass_mailing_partner/wizard/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import partner_mail_list_wizard +from . import partner_merge diff --git a/mass_mailing_partner/wizard/partner_mail_list_wizard.py b/mass_mailing_partner/wizard/partner_mail_list_wizard.py new file mode 100644 index 0000000..79b35e0 --- /dev/null +++ b/mass_mailing_partner/wizard/partner_mail_list_wizard.py @@ -0,0 +1,44 @@ +# Copyright 2015 Pedro M. Baeza +# Copyright 2015 Antonio Espinosa +# Copyright 2015 Javier Iniesta +# Copyright 2020 Tecnativa - Manuel Calero +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class PartnerMailListWizard(models.TransientModel): + _name = "partner.mail.list.wizard" + _description = "Create contact mailing list" + + mail_list_id = fields.Many2one(comodel_name="mailing.list", string="Mailing List") + partner_ids = fields.Many2many( + comodel_name="res.partner", + relation="mail_list_wizard_partner", + default=lambda self: self.env.context.get("active_ids"), + ) + + def add_to_mail_list(self): + contact_obj = self.env["mailing.contact"] + partners = self.partner_ids + + add_list = partners.filtered("mass_mailing_contact_ids") + for partner in add_list: + self.mail_list_id.contact_ids = [ + (4, partner.mass_mailing_contact_ids[0].id) + ] + + to_create = partners - add_list + for partner in to_create: + if not partner.email: + raise UserError(_("Partner '%s' has no email.") % partner.name) + contact_vals = { + "partner_id": partner.id, + "list_ids": [(4, self.mail_list_id.id)], + "title_id": partner.title or False, + "company_name": partner.company_id.name or False, + "country_id": partner.country_id or False, + "tag_ids": partner.category_id or False, + } + contact_obj.create(contact_vals) diff --git a/mass_mailing_partner/wizard/partner_mail_list_wizard.xml b/mass_mailing_partner/wizard/partner_mail_list_wizard.xml new file mode 100644 index 0000000..8584c2f --- /dev/null +++ b/mass_mailing_partner/wizard/partner_mail_list_wizard.xml @@ -0,0 +1,39 @@ + + + + + + Add to mailing list + partner.mail.list.wizard + new + form + + list + + + + partner.mail.list.form + partner.mail.list.wizard + +
+ + + +
+
+
+
+
+ +
diff --git a/mass_mailing_partner/wizard/partner_merge.py b/mass_mailing_partner/wizard/partner_merge.py new file mode 100644 index 0000000..97116cc --- /dev/null +++ b/mass_mailing_partner/wizard/partner_merge.py @@ -0,0 +1,27 @@ +# Copyright 2020 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class BasePartnerMergeAutomaticWizard(models.TransientModel): + _inherit = "base.partner.merge.automatic.wizard" + + def _merge(self, partner_ids, dst_partner=None, extra_checks=True): + if dst_partner: + contacts = ( + self.env["mailing.contact"] + .sudo() + .search([("partner_id", "in", partner_ids)]) + ) + if contacts: + contacts = contacts.sorted( + lambda x: 1 if x.partner_id == dst_partner else 0 + ) + list_ids = contacts.mapped("list_ids").ids + contacts[1:].unlink() + contacts[0].partner_id = dst_partner + contacts[0].list_ids = [(4, x) for x in list_ids] + return super()._merge( + partner_ids, dst_partner=dst_partner, extra_checks=extra_checks + ) diff --git a/mass_mailing_unique/README.rst b/mass_mailing_unique/README.rst new file mode 100644 index 0000000..4814b4c --- /dev/null +++ b/mass_mailing_unique/README.rst @@ -0,0 +1,93 @@ +=============================== +Unique records for mass mailing +=============================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsocial-lightgray.png?logo=github + :target: https://github.com/OCA/social/tree/14.0/mass_mailing_unique + :alt: OCA/social +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/social-14-0/social-14-0-mass_mailing_unique + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/205/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the functionality of mass mailing lists to disable +duplicate entries in list names and contact emails. + +This way you will avoid conflicts when importing contacts to a list that has a +duplicated name. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +Before installing this module, you need to: + +* Remove all duplicated list names. +* Remove all duplicated emails in mailing contacts. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `_: + + * Jairo Llopis + * Vicent Cubells + * Pedro M. Baeza + * Ernesto Tejeda +* `Camptocamp `_ + + * Iván Todorovich + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +This module is part of the `OCA/social `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mass_mailing_unique/__init__.py b/mass_mailing_unique/__init__.py new file mode 100644 index 0000000..50800f2 --- /dev/null +++ b/mass_mailing_unique/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis +# Copyright 2016 Tecnativa - Vicent Cubells +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from .hooks import pre_init_hook diff --git a/mass_mailing_unique/__manifest__.py b/mass_mailing_unique/__manifest__.py new file mode 100644 index 0000000..4cf83ab --- /dev/null +++ b/mass_mailing_unique/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis +# Copyright 2016 Tecnativa - Vicent Cubells +# Copyright 2018 Tecnativa - Ernesto Tejeda +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Unique records for mass mailing", + "summary": "Avoids duplicate mailing lists and contacts", + "version": "14.0.1.0.0", + "category": "Marketing", + "website": "https://github.com/OCA/social", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["mass_mailing"], + "pre_init_hook": "pre_init_hook", +} diff --git a/mass_mailing_unique/hooks.py b/mass_mailing_unique/hooks.py new file mode 100644 index 0000000..1c8089f --- /dev/null +++ b/mass_mailing_unique/hooks.py @@ -0,0 +1,48 @@ +# Copyright 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis +# Copyright 2016 Tecnativa - Vicent Cubells +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import _ +from odoo.exceptions import ValidationError + + +def pre_init_hook(cr): + """Make sure there are no duplicates before installing the module. + + If you define an unique key in Odoo that cannot be applied, Odoo will log a + warning and install the module without that constraint. Since this module + is useless without those constraints, we check here if all will work before + installing, and provide a user-friendly message in case of failure. + """ + errors = list() + # Search for duplicates in emails + cr.execute( + """ + SELECT email_normalized, COUNT(id) as count + FROM mailing_contact + GROUP BY email_normalized + HAVING COUNT(id) > 1 + """ + ) + for result in cr.fetchall(): + errors.append( + "There are {1} mailing contacts with the same email: {0}".format(*result) + ) + # Search for duplicates in list's name + cr.execute( + """ + SELECT name, COUNT(id) as count + FROM mailing_list + GROUP BY name + HAVING COUNT(id) > 1 + """ + ) + for result in cr.fetchall(): + errors.append( + "There are {1} mailing lists with the same name: {0}.".format(*result) + ) + # Abort if duplicates are found + if errors: + raise ValidationError( + _("Unable to install module mass_mailing_unique:\n%s", "\n".join(errors)) + ) diff --git a/mass_mailing_unique/models/__init__.py b/mass_mailing_unique/models/__init__.py new file mode 100644 index 0000000..805ade7 --- /dev/null +++ b/mass_mailing_unique/models/__init__.py @@ -0,0 +1,2 @@ +from . import mailing_contact +from . import mailing_list diff --git a/mass_mailing_unique/models/mailing_contact.py b/mass_mailing_unique/models/mailing_contact.py new file mode 100644 index 0000000..cca06a2 --- /dev/null +++ b/mass_mailing_unique/models/mailing_contact.py @@ -0,0 +1,17 @@ +# Copyright 2018 Tecnativa - Ernesto Tejeda +# Copyright 2021 Camptocamp - Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class MailingContact(models.Model): + _inherit = "mailing.contact" + + _sql_constraints = [ + ( + "unique_email", + "UNIQUE(email_normalized)", + "There's already a contact with this email address", + ) + ] diff --git a/mass_mailing_unique/models/mailing_list.py b/mass_mailing_unique/models/mailing_list.py new file mode 100644 index 0000000..a2511e1 --- /dev/null +++ b/mass_mailing_unique/models/mailing_list.py @@ -0,0 +1,18 @@ +# Copyright 2015 Grupo ESOC Ingeniería de Servicios, S.L.U. - Jairo Llopis +# Copyright 2016 Tecnativa - Vicent Cubells +# Copyright 2021 Camptocamp - Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import models + + +class MailingList(models.Model): + _inherit = "mailing.list" + + _sql_constraints = [ + ( + "unique_name", + "UNIQUE(name)", + "Cannot have more than one lists with the same name.", + ) + ] diff --git a/mass_mailing_unique/readme/CONTRIBUTORS.rst b/mass_mailing_unique/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..33b6216 --- /dev/null +++ b/mass_mailing_unique/readme/CONTRIBUTORS.rst @@ -0,0 +1,9 @@ +* `Tecnativa `_: + + * Jairo Llopis + * Vicent Cubells + * Pedro M. Baeza + * Ernesto Tejeda +* `Camptocamp `_ + + * Iván Todorovich diff --git a/mass_mailing_unique/readme/DESCRIPTION.rst b/mass_mailing_unique/readme/DESCRIPTION.rst new file mode 100644 index 0000000..cbb5ff5 --- /dev/null +++ b/mass_mailing_unique/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module extends the functionality of mass mailing lists to disable +duplicate entries in list names and contact emails. + +This way you will avoid conflicts when importing contacts to a list that has a +duplicated name. diff --git a/mass_mailing_unique/readme/INSTALL.rst b/mass_mailing_unique/readme/INSTALL.rst new file mode 100644 index 0000000..2860428 --- /dev/null +++ b/mass_mailing_unique/readme/INSTALL.rst @@ -0,0 +1,4 @@ +Before installing this module, you need to: + +* Remove all duplicated list names. +* Remove all duplicated emails in mailing contacts. diff --git a/mass_mailing_unique/static/description/icon.png b/mass_mailing_unique/static/description/icon.png new file mode 100644 index 0000000..3a0328b Binary files /dev/null and b/mass_mailing_unique/static/description/icon.png differ diff --git a/mass_mailing_unique/static/description/index.html b/mass_mailing_unique/static/description/index.html new file mode 100644 index 0000000..3bb45c8 --- /dev/null +++ b/mass_mailing_unique/static/description/index.html @@ -0,0 +1,445 @@ + + + + + + +Unique records for mass mailing + + + +
+

Unique records for mass mailing

+ + +

Beta License: AGPL-3 OCA/social Translate me on Weblate Try me on Runbot

+

This module extends the functionality of mass mailing lists to disable +duplicate entries in list names and contact emails.

+

This way you will avoid conflicts when importing contacts to a list that has a +duplicated name.

+

Table of contents

+ +
+

Installation

+

Before installing this module, you need to:

+
    +
  • Remove all duplicated list names.
  • +
  • Remove all duplicated emails in mailing contacts.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/social project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/mass_mailing_unique/tests/__init__.py b/mass_mailing_unique/tests/__init__.py new file mode 100644 index 0000000..8df8686 --- /dev/null +++ b/mass_mailing_unique/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_mass_mailing_unique diff --git a/mass_mailing_unique/tests/test_mass_mailing_unique.py b/mass_mailing_unique/tests/test_mass_mailing_unique.py new file mode 100644 index 0000000..bdd4aec --- /dev/null +++ b/mass_mailing_unique/tests/test_mass_mailing_unique.py @@ -0,0 +1,110 @@ +# Copyright 2016 Tecnativa - Pedro M. Baeza +# Copyright 2021 Camptocamp - Iván Todorovich +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from psycopg2 import IntegrityError + +from odoo.exceptions import ValidationError +from odoo.tests import common +from odoo.tools import mute_logger + +from ..hooks import pre_init_hook + + +class TestMassMailingUnique(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.mailing_list = cls.env.ref("mass_mailing.mailing_list_data") + cls.mailing_contact = cls.env["mailing.contact"].create( + { + "name": "John Doe", + "email": "john.doe@example.com", + "list_ids": [(6, 0, cls.mailing_list.ids)], + } + ) + + def test_init_hook_list_mailing_list(self): + # Disable temporarily the constraint + self.env.cr.execute( + """ + ALTER TABLE mailing_list + DROP CONSTRAINT mailing_list_unique_name + """ + ) + # Create another list with the same exact name + self.env["mailing.list"].create({"name": self.mailing_list.name}) + self.env["mailing.list"].flush() + with self.assertRaises(ValidationError): + pre_init_hook(self.env.cr) + + def test_init_hook_list_mailing_contact(self): + # Disable temporarily the constraint + self.env.cr.execute( + """ + ALTER TABLE mailing_contact + DROP CONSTRAINT mailing_contact_unique_email + """ + ) + # Create another list with the same exact name + self.env["mailing.contact"].create( + { + "name": f"{self.mailing_contact.name} (2)", + "email": self.mailing_contact.email, + } + ) + self.env["mailing.contact"].flush() + with self.assertRaises(ValidationError): + pre_init_hook(self.env.cr) + + def test_mailing_contact_unique_email_exact(self): + """Create a contact with the same exact email""" + with mute_logger("odoo.sql_db"): + with self.assertRaisesRegex(IntegrityError, "mailing_contact_unique_email"): + self.env["mailing.contact"].create( + { + "name": "John Doe (2)", + "email": "john.doe@example.com", + } + ) + self.env["mailing.contact"].flush() + + def test_mailing_contact_unique_email_same(self): + """Create a contact with the same email (not exact though)""" + with mute_logger("odoo.sql_db"): + with self.assertRaisesRegex(IntegrityError, "mailing_contact_unique_email"): + self.env["mailing.contact"].create( + { + "name": "John Doe (2)", + "email": " John.DOE@example.com", + } + ) + self.env["mailing.contact"].flush() + + def test_mailing_contact_unique_email_ok(self): + """Create a contact with another email""" + self.env["mailing.contact"].create( + { + "name": "Jane Doe", + "email": "jane.doe@example.com", + } + ) + + def test_mailing_list_unique_name_duplicated(self): + """Create a mailing list with the same name""" + with mute_logger("odoo.sql_db"): + with self.assertRaisesRegex(IntegrityError, "mailing_list_unique_name"): + self.env["mailing.list"].create( + { + "name": self.mailing_list.name, + } + ) + self.env["mailing.list"].flush() + + def test_mailing_list_unique_name_ok(self): + """Create a mailing list with another name""" + self.env["mailing.list"].create( + { + "name": "Another mailing list", + } + )