diff --git a/mail_template_domain/README.rst b/mail_template_domain/README.rst new file mode 100644 index 000000000..4157409cd --- /dev/null +++ b/mail_template_domain/README.rst @@ -0,0 +1,105 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +==================== +Mail Template Domain +==================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0ad5a5b12ca98c2057110cf9b2856f809274ebb84075743c3861c6c5710bfd0e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/17.0/mail_template_domain + :alt: OCA/mail +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/mail-17-0/mail-17-0-mail_template_domain + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows setting an application domain on mail templates. + +When a domain is configured on a template, that template will only +appear in the email composer when the active record matches the domain. +Templates without a domain continue to appear for all records of the +applicable model. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module: + +1. Go to **Settings > Technical > Email > Templates**. +2. Open or create a mail template. +3. In the **Apply on** field (Settings tab), set the domain that records + must match for this template to appear in the composer (e.g. + ``[('is_company', '=', True)]`` to show the template only on contacts + where **Is a Company** is set). +4. Leave **Apply on** empty to keep the template available for all + records of the model (default behaviour). + +When a user opens the email composer from a record, only templates whose +**Apply on** domain matches that record will be listed in the *Load +template* dropdown. + +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 to smash 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 `__: + + - Cristina Hidalgo + +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/mail `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mail_template_domain/__init__.py b/mail_template_domain/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mail_template_domain/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mail_template_domain/__manifest__.py b/mail_template_domain/__manifest__.py new file mode 100644 index 000000000..11c2ca7bc --- /dev/null +++ b/mail_template_domain/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "Mail Template Domain", + "summary": "Filter mail templates by domain on the active record", + "version": "17.0.1.0.0", + "category": "Technical", + "website": "https://github.com/OCA/mail", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": ["mail"], + "data": [ + "views/mail_template_views.xml", + "views/mail_compose_message_views.xml", + ], +} diff --git a/mail_template_domain/i18n/es.po b/mail_template_domain/i18n/es.po new file mode 100644 index 000000000..81db4e37d --- /dev/null +++ b/mail_template_domain/i18n/es.po @@ -0,0 +1,56 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_template_domain +# +# Translators: +# Cristina Hidalgo , 2024 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-15 09:47+0000\n" +"PO-Revision-Date: 2026-04-15 09:47+0000\n" +"Last-Translator: Cristina Hidalgo \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: mail_template_domain +#: model:ir.model.fields,field_description:mail_template_domain.field_mail_template__filter_domain +#: model_terms:ir.ui.view,arch_db:mail_template_domain.email_template_form_filter_domain +msgid "Apply on" +msgstr "Aplicar en" + +#. module: mail_template_domain +#: model:ir.model,name:mail_template_domain.model_mail_compose_message +msgid "Email composition wizard" +msgstr "Asistente de redacción de correo electrónico" + +#. module: mail_template_domain +#: model:ir.model,name:mail_template_domain.model_mail_template +msgid "Email Templates" +msgstr "Plantillas de correo electrónico" + +#. module: mail_template_domain +#: model:ir.model.fields,help:mail_template_domain.field_mail_template__filter_domain +msgid "" +"If set, this template will only appear in the email composer when the active" +" record matches this domain." +msgstr "" +"Si se establece, esta plantilla solo aparecerá en el compositor de correo " +"cuando el registro activo cumpla este dominio." + +#. module: mail_template_domain +#: model:ir.model.fields,field_description:mail_template_domain.field_mail_compose_message__mail_template_res_ids +msgid "Mail Template Res Ids" +msgstr "" + +#. module: mail_template_domain +#: model:ir.model.fields,help:mail_template_domain.field_mail_compose_message__mail_template_res_ids +msgid "" +"Technical field: res_ids as JSON used to filter available templates by" +" domain." +msgstr "" diff --git a/mail_template_domain/i18n/mail_template_domain.pot b/mail_template_domain/i18n/mail_template_domain.pot new file mode 100644 index 000000000..7ea945e19 --- /dev/null +++ b/mail_template_domain/i18n/mail_template_domain.pot @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mail_template_domain +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-15 09:47+0000\n" +"PO-Revision-Date: 2026-04-15 09:47+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mail_template_domain +#: model:ir.model.fields,field_description:mail_template_domain.field_mail_template__filter_domain +#: model_terms:ir.ui.view,arch_db:mail_template_domain.email_template_form_filter_domain +msgid "Apply on" +msgstr "" + +#. module: mail_template_domain +#: model:ir.model,name:mail_template_domain.model_mail_compose_message +msgid "Email composition wizard" +msgstr "" + +#. module: mail_template_domain +#: model:ir.model,name:mail_template_domain.model_mail_template +msgid "Email Templates" +msgstr "" + +#. module: mail_template_domain +#: model:ir.model.fields,help:mail_template_domain.field_mail_template__filter_domain +msgid "" +"If set, this template will only appear in the email composer when the active" +" record matches this domain." +msgstr "" + +#. module: mail_template_domain +#: model:ir.model.fields,field_description:mail_template_domain.field_mail_compose_message__mail_template_res_ids +msgid "Mail Template Res Ids" +msgstr "" + +#. module: mail_template_domain +#: model:ir.model.fields,help:mail_template_domain.field_mail_compose_message__mail_template_res_ids +msgid "" +"Technical field: res_ids as JSON used to filter available templates by" +" domain." +msgstr "" diff --git a/mail_template_domain/models/__init__.py b/mail_template_domain/models/__init__.py new file mode 100644 index 000000000..33d581888 --- /dev/null +++ b/mail_template_domain/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_compose_message +from . import mail_template diff --git a/mail_template_domain/models/mail_compose_message.py b/mail_template_domain/models/mail_compose_message.py new file mode 100644 index 000000000..f5be2d21f --- /dev/null +++ b/mail_template_domain/models/mail_compose_message.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class MailComposeMessage(models.TransientModel): + _inherit = "mail.compose.message" + + mail_template_res_ids = fields.Char( + compute="_compute_mail_template_res_ids", + help="Technical field: res_ids as JSON used to filter available" + " templates by domain.", + ) + + @api.depends("res_ids") + def _compute_mail_template_res_ids(self): + for composer in self: + composer.mail_template_res_ids = composer.res_ids or "[]" diff --git a/mail_template_domain/models/mail_template.py b/mail_template_domain/models/mail_template.py new file mode 100644 index 000000000..8fc2e27b7 --- /dev/null +++ b/mail_template_domain/models/mail_template.py @@ -0,0 +1,45 @@ +import json + +from odoo import api, fields, models +from odoo.tools.safe_eval import safe_eval + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + filter_domain = fields.Char( + string="Apply on", + help="If set, this template will only appear in the email composer " + "when the active record matches this domain.", + ) + + @api.model + def _name_search( + self, name="", domain=None, operator="ilike", limit=None, order=None + ): + # Override _name_search (Many2one widget lookups) instead of + # search/_search so the filter only applies in the composer dropdown, + # not in list views or programmatic searches (fail-open by design). + ids = super()._name_search(name, domain, operator, limit, order) + res_ids_raw = self._context.get("mail_template_res_ids") + res_model = self._context.get("mail_template_res_model") + if not res_ids_raw or not res_model or res_model not in self.env: + return ids + try: + res_ids = json.loads(res_ids_raw) + except (ValueError, TypeError): + return ids + if not res_ids: + return ids + records = self.env[res_model].browse(res_ids) + if not records.exists(): + return ids + return ( + self.browse(ids) + .filtered( + lambda t: not t.filter_domain + or len(records.filtered_domain(safe_eval(t.filter_domain))) + == len(records) + ) + .ids + ) diff --git a/mail_template_domain/pyproject.toml b/mail_template_domain/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/mail_template_domain/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_template_domain/readme/CONTRIBUTORS.md b/mail_template_domain/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..689a1cdc4 --- /dev/null +++ b/mail_template_domain/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Tecnativa](https://www.tecnativa.com): + - Cristina Hidalgo diff --git a/mail_template_domain/readme/DESCRIPTION.md b/mail_template_domain/readme/DESCRIPTION.md new file mode 100644 index 000000000..910fc3c45 --- /dev/null +++ b/mail_template_domain/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module allows setting an application domain on mail templates. + +When a domain is configured on a template, that template will only appear +in the email composer when the active record matches the domain. Templates +without a domain continue to appear for all records of the applicable model. diff --git a/mail_template_domain/readme/USAGE.md b/mail_template_domain/readme/USAGE.md new file mode 100644 index 000000000..e635069a6 --- /dev/null +++ b/mail_template_domain/readme/USAGE.md @@ -0,0 +1,14 @@ +To use this module: + +1. Go to **Settings > Technical > Email > Templates**. +2. Open or create a mail template. +3. In the **Apply on** field (Settings tab), set the domain that records must + match for this template to appear in the composer + (e.g. `[('is_company', '=', True)]` to show the template only on contacts + where **Is a Company** is set). +4. Leave **Apply on** empty to keep the template available for all records of + the model (default behaviour). + +When a user opens the email composer from a record, only templates whose +**Apply on** domain matches that record will be listed in the *Load template* +dropdown. diff --git a/mail_template_domain/static/description/index.html b/mail_template_domain/static/description/index.html new file mode 100644 index 000000000..0651ae3bb --- /dev/null +++ b/mail_template_domain/static/description/index.html @@ -0,0 +1,454 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Mail Template Domain

+ +

Beta License: AGPL-3 OCA/mail Translate me on Weblate Try me on Runboat

+

This module allows setting an application domain on mail templates.

+

When a domain is configured on a template, that template will only +appear in the email composer when the active record matches the domain. +Templates without a domain continue to appear for all records of the +applicable model.

+

Table of contents

+ +
+

Usage

+

To use this module:

+
    +
  1. Go to Settings > Technical > Email > Templates.
  2. +
  3. Open or create a mail template.
  4. +
  5. In the Apply on field (Settings tab), set the domain that records +must match for this template to appear in the composer (e.g. +[('is_company', '=', True)] to show the template only on contacts +where Is a Company is set).
  6. +
  7. Leave Apply on empty to keep the template available for all +records of the model (default behaviour).
  8. +
+

When a user opens the email composer from a record, only templates whose +Apply on domain matches that record will be listed in the Load +template dropdown.

+
+
+

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 to smash 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/mail project on GitHub.

+

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

+
+
+
+
+ + diff --git a/mail_template_domain/tests/__init__.py b/mail_template_domain/tests/__init__.py new file mode 100644 index 000000000..36d237371 --- /dev/null +++ b/mail_template_domain/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_template_domain diff --git a/mail_template_domain/tests/test_mail_template_domain.py b/mail_template_domain/tests/test_mail_template_domain.py new file mode 100644 index 000000000..6346a0308 --- /dev/null +++ b/mail_template_domain/tests/test_mail_template_domain.py @@ -0,0 +1,121 @@ +import json + +from odoo.addons.base.tests.common import BaseCommon + + +class TestMailTemplateDomain(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner_company = cls.env["res.partner"].create( + {"name": "Test company partner", "is_company": True} + ) + cls.partner_individual = cls.env["res.partner"].create( + {"name": "Test individual partner", "is_company": False} + ) + model_id = cls.env["ir.model"]._get("res.partner").id + cls.template_no_domain = cls.env["mail.template"].create( + { + "name": "Template no domain", + "model_id": model_id, + } + ) + cls.template_company = cls.env["mail.template"].create( + { + "name": "Template company", + "model_id": model_id, + "filter_domain": "[('is_company', '=', True)]", + } + ) + cls.template_no_match = cls.env["mail.template"].create( + { + "name": "Template no match", + "model_id": model_id, + "filter_domain": "[('id', '<', 0)]", + } + ) + + def _search_templates(self, records): + """Run name_search on mail.template with the records as context. + + name_search is the code path used by Many2one fields in the UI, + which is what _name_search overrides. + """ + template_env = self.env["mail.template"].with_context( + mail_template_res_ids=json.dumps(records.ids), + mail_template_res_model=records._name, + ) + result = template_env.name_search("", args=[("model", "=", "res.partner")]) + return template_env.browse([r[0] for r in result]) + + def test_template_without_domain_always_visible(self): + """A template with no filter_domain appears for any record.""" + templates = self._search_templates(self.partner_company) + self.assertIn(self.template_no_domain, templates) + + templates = self._search_templates(self.partner_individual) + self.assertIn(self.template_no_domain, templates) + + def test_template_domain_matches_record(self): + """A template whose filter_domain matches the record is visible.""" + templates = self._search_templates(self.partner_company) + self.assertIn(self.template_company, templates) + + def test_template_domain_does_not_match_record(self): + """A template whose filter_domain does not match the record is hidden.""" + templates = self._search_templates(self.partner_individual) + self.assertNotIn(self.template_company, templates) + + def test_template_domain_all_records_match(self): + """Template appears only when ALL selected records match the domain.""" + partner_company2 = self.env["res.partner"].create( + {"name": "Test company 2", "is_company": True} + ) + templates = self._search_templates(self.partner_company | partner_company2) + self.assertIn(self.template_company, templates) + + def test_template_domain_not_all_records_match(self): + """Template hidden when at least one selected record does not match.""" + templates = self._search_templates( + self.partner_company | self.partner_individual + ) + self.assertNotIn(self.template_company, templates) + + def test_template_no_match_domain_always_hidden(self): + """A template with an impossible domain never appears.""" + templates = self._search_templates(self.partner_company) + self.assertNotIn(self.template_no_match, templates) + + templates = self._search_templates(self.partner_individual) + self.assertNotIn(self.template_no_match, templates) + + def test_composer_mail_template_res_ids(self): + """mail_template_res_ids mirrors res_ids on the composer.""" + composer = self.env["mail.compose.message"].create( + { + "model": "res.partner", + "res_ids": json.dumps([self.partner_company.id]), + "composition_mode": "comment", + } + ) + self.assertEqual( + composer.mail_template_res_ids, + json.dumps([self.partner_company.id]), + ) + + def test_composer_mail_template_res_ids_empty(self): + """mail_template_res_ids is '[]' when res_ids is not set.""" + composer = self.env["mail.compose.message"].create( + { + "model": "res.partner", + "composition_mode": "mass_mail", + } + ) + self.assertEqual(composer.mail_template_res_ids, "[]") + + def test_no_context_no_filter_applied(self): + """Without context the full list is returned (fail-open).""" + templates = self.env["mail.template"].search([("model", "=", "res.partner")]) + self.assertIn(self.template_no_domain, templates) + self.assertIn(self.template_company, templates) + self.assertIn(self.template_no_match, templates) diff --git a/mail_template_domain/views/mail_compose_message_views.xml b/mail_template_domain/views/mail_compose_message_views.xml new file mode 100644 index 000000000..ffe092e4f --- /dev/null +++ b/mail_template_domain/views/mail_compose_message_views.xml @@ -0,0 +1,37 @@ + + + + mail.compose.message.form.filter.domain + mail.compose.message + + + + + + + + {'default_model': model, + 'default_body_html': body, + 'default_subject': subject, + 'mail_template_res_ids': mail_template_res_ids, + 'mail_template_res_model': model} + + + + + {'default_model': model, + 'default_body_html': body, + 'default_subject': subject, + 'mail_template_res_ids': mail_template_res_ids, + 'mail_template_res_model': model} + + + + + diff --git a/mail_template_domain/views/mail_template_views.xml b/mail_template_domain/views/mail_template_views.xml new file mode 100644 index 000000000..65706398b --- /dev/null +++ b/mail_template_domain/views/mail_template_views.xml @@ -0,0 +1,21 @@ + + + + mail.template.form.filter.domain + mail.template + + + + + + + + + +