From 19f2101b5d7309c16b7ae99d56f533e402237a2b Mon Sep 17 00:00:00 2001 From: Fabien BOURGEOIS Date: Tue, 21 Jun 2022 16:48:00 +0200 Subject: [PATCH] [MIG]Mass Mailing Unique and Partner to v15 --- mass_mailing_partner/README.rst | 109 +++++ mass_mailing_partner/__init__.py | 5 + mass_mailing_partner/__manifest__.py | 25 + mass_mailing_partner/hooks.py | 33 ++ mass_mailing_partner/models/__init__.py | 7 + .../models/mailing_contact.py | 107 ++++ .../models/mailing_contact_subscription.py | 20 + mass_mailing_partner/models/mailing_list.py | 34 ++ mass_mailing_partner/models/mailing_trace.py | 37 ++ mass_mailing_partner/models/res_partner.py | 94 ++++ mass_mailing_partner/readme/CONFIGURE.rst | 2 + mass_mailing_partner/readme/CONTRIBUTORS.rst | 14 + mass_mailing_partner/readme/DESCRIPTION.rst | 11 + mass_mailing_partner/readme/USAGE.rst | 3 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 14912 bytes .../static/description/index.html | 455 ++++++++++++++++++ mass_mailing_partner/tests/__init__.py | 7 + mass_mailing_partner/tests/base.py | 67 +++ .../tests/test_mail_mail_statistics.py | 30 ++ .../tests/test_mail_mass_mailing_contact.py | 142 ++++++ .../tests/test_mail_mass_mailing_list.py | 39 ++ ...test_mail_mass_mailing_list_contact_rel.py | 22 + .../tests/test_partner_mail_list_wizard.py | 43 ++ .../tests/test_res_partner.py | 61 +++ .../views/mailing_contact_view.xml | 80 +++ .../views/mailing_trace_view.xml | 47 ++ mass_mailing_partner/views/mailing_view.xml | 25 + .../views/res_partner_view.xml | 72 +++ mass_mailing_partner/wizard/__init__.py | 4 + .../wizard/partner_mail_list_wizard.py | 44 ++ .../wizard/partner_mail_list_wizard.xml | 39 ++ mass_mailing_partner/wizard/partner_merge.py | 27 ++ mass_mailing_unique/README.rst | 93 ++++ mass_mailing_unique/__init__.py | 6 + mass_mailing_unique/__manifest__.py | 16 + mass_mailing_unique/hooks.py | 48 ++ mass_mailing_unique/models/__init__.py | 2 + mass_mailing_unique/models/mailing_contact.py | 17 + mass_mailing_unique/models/mailing_list.py | 18 + mass_mailing_unique/readme/CONTRIBUTORS.rst | 9 + mass_mailing_unique/readme/DESCRIPTION.rst | 5 + mass_mailing_unique/readme/INSTALL.rst | 4 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 445 +++++++++++++++++ mass_mailing_unique/tests/__init__.py | 3 + .../tests/test_mass_mailing_unique.py | 110 +++++ 47 files changed, 2483 insertions(+) create mode 100644 mass_mailing_partner/README.rst create mode 100644 mass_mailing_partner/__init__.py create mode 100644 mass_mailing_partner/__manifest__.py create mode 100644 mass_mailing_partner/hooks.py create mode 100644 mass_mailing_partner/models/__init__.py create mode 100644 mass_mailing_partner/models/mailing_contact.py create mode 100644 mass_mailing_partner/models/mailing_contact_subscription.py create mode 100644 mass_mailing_partner/models/mailing_list.py create mode 100644 mass_mailing_partner/models/mailing_trace.py create mode 100644 mass_mailing_partner/models/res_partner.py create mode 100644 mass_mailing_partner/readme/CONFIGURE.rst create mode 100644 mass_mailing_partner/readme/CONTRIBUTORS.rst create mode 100644 mass_mailing_partner/readme/DESCRIPTION.rst create mode 100644 mass_mailing_partner/readme/USAGE.rst create mode 100644 mass_mailing_partner/security/ir.model.access.csv create mode 100644 mass_mailing_partner/static/description/icon.png create mode 100644 mass_mailing_partner/static/description/index.html create mode 100644 mass_mailing_partner/tests/__init__.py create mode 100644 mass_mailing_partner/tests/base.py create mode 100644 mass_mailing_partner/tests/test_mail_mail_statistics.py create mode 100644 mass_mailing_partner/tests/test_mail_mass_mailing_contact.py create mode 100644 mass_mailing_partner/tests/test_mail_mass_mailing_list.py create mode 100644 mass_mailing_partner/tests/test_mail_mass_mailing_list_contact_rel.py create mode 100644 mass_mailing_partner/tests/test_partner_mail_list_wizard.py create mode 100644 mass_mailing_partner/tests/test_res_partner.py create mode 100644 mass_mailing_partner/views/mailing_contact_view.xml create mode 100644 mass_mailing_partner/views/mailing_trace_view.xml create mode 100644 mass_mailing_partner/views/mailing_view.xml create mode 100644 mass_mailing_partner/views/res_partner_view.xml create mode 100644 mass_mailing_partner/wizard/__init__.py create mode 100644 mass_mailing_partner/wizard/partner_mail_list_wizard.py create mode 100644 mass_mailing_partner/wizard/partner_mail_list_wizard.xml create mode 100644 mass_mailing_partner/wizard/partner_merge.py create mode 100644 mass_mailing_unique/README.rst create mode 100644 mass_mailing_unique/__init__.py create mode 100644 mass_mailing_unique/__manifest__.py create mode 100644 mass_mailing_unique/hooks.py create mode 100644 mass_mailing_unique/models/__init__.py create mode 100644 mass_mailing_unique/models/mailing_contact.py create mode 100644 mass_mailing_unique/models/mailing_list.py create mode 100644 mass_mailing_unique/readme/CONTRIBUTORS.rst create mode 100644 mass_mailing_unique/readme/DESCRIPTION.rst create mode 100644 mass_mailing_unique/readme/INSTALL.rst create mode 100644 mass_mailing_unique/static/description/icon.png create mode 100644 mass_mailing_unique/static/description/index.html create mode 100644 mass_mailing_unique/tests/__init__.py create mode 100644 mass_mailing_unique/tests/test_mass_mailing_unique.py 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 0000000000000000000000000000000000000000..b18b21a1f7fb7547bedeeb098bc451c9681d8f7a GIT binary patch literal 14912 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 + + + + + +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 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 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", + } + )