diff --git a/mail_template_conditional_report/README.rst b/mail_template_conditional_report/README.rst new file mode 100644 index 000000000..199d0b81b --- /dev/null +++ b/mail_template_conditional_report/README.rst @@ -0,0 +1,96 @@ +================================ +Mail template conditional report +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:4bd805708cf352d692d761056951ab35cbddf507666a72f42fa2bb37b13c0586 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fmail-lightgray.png?logo=github + :target: https://github.com/OCA/mail/tree/18.0/mail_template_conditional_report + :alt: OCA/mail +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/mail-18-0/mail-18-0-mail_template_conditional_report + :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=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module has been developed to enable you to conditionally select the +reports to be attached in an email template according to criteria in the +email template subject. + +To do this, we've added to the mail.template object the ability to +choose the ir.attchment that should be linked in the email template and +the ability to set conditions for each document. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module is useful for customising the attachments you want to +include according to the profile of the third party or the type of +transaction. + +Example: you could have one reports per type of account.move +(in_invoice, out_invoice,...). Instead of having one mail template per +type of account.move, you could use the same and add the domain on the +attachment to determine which should be generated. + +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 +------- + +* ACSONE SA/NV + +Contributors +------------ + +- `Acsone `__: + + - François Honoré francois.honore@acsone.eu + +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_conditional_report/__init__.py b/mail_template_conditional_report/__init__.py new file mode 100644 index 000000000..fe718badd --- /dev/null +++ b/mail_template_conditional_report/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks.pre_init_hook import _pre_init_hook diff --git a/mail_template_conditional_report/__manifest__.py b/mail_template_conditional_report/__manifest__.py new file mode 100644 index 000000000..b3f66deed --- /dev/null +++ b/mail_template_conditional_report/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Mail template conditional report", + "summary": "Add a domain to print dynamic attachments on mail template", + "version": "18.0.1.0.0", + "category": "Mail", + "website": "https://github.com/OCA/mail", + "author": "ACSONE SA/NV, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": ["mail"], + "data": [ + "views/mail_template.xml", + "security/mail_template_report.xml", + ], + "demo": [ + "demo/ir_actions_report.xml", + "demo/mail_template.xml", + ], + "pre_init_hook": "_pre_init_hook", +} diff --git a/mail_template_conditional_report/demo/ir_actions_report.xml b/mail_template_conditional_report/demo/ir_actions_report.xml new file mode 100644 index 000000000..d24868de3 --- /dev/null +++ b/mail_template_conditional_report/demo/ir_actions_report.xml @@ -0,0 +1,24 @@ + + + + + Model Overview 2 + ir.model + qweb-pdf + base.report_irmodeloverview + base.report_irmodeloverview + + report + + + + Model Overview 3 + ir.model + qweb-pdf + base.report_irmodeloverview + base.report_irmodeloverview + + report + + diff --git a/mail_template_conditional_report/demo/mail_template.xml b/mail_template_conditional_report/demo/mail_template.xml new file mode 100644 index 000000000..6b8cb9640 --- /dev/null +++ b/mail_template_conditional_report/demo/mail_template.xml @@ -0,0 +1,50 @@ + + + + + Hello world + + + You shouldn't receive this + +
But I think you receive it
+
+ + + + + + +
+ + Promotion + + + You have been promoted 🥳 + +
Sorry this email is not for you! But please continue your good job!💪
+
+ + + + [(1, '=', 1)] + + + + [(0, '=', 1)] + + + + [(1, '=', 1)] + + +
+
diff --git a/mail_template_conditional_report/hooks/__init__.py b/mail_template_conditional_report/hooks/__init__.py new file mode 100644 index 000000000..30e5640b1 --- /dev/null +++ b/mail_template_conditional_report/hooks/__init__.py @@ -0,0 +1 @@ +from . import pre_init_hook diff --git a/mail_template_conditional_report/hooks/pre_init_hook.py b/mail_template_conditional_report/hooks/pre_init_hook.py new file mode 100644 index 000000000..5c136f21c --- /dev/null +++ b/mail_template_conditional_report/hooks/pre_init_hook.py @@ -0,0 +1,16 @@ +# Copyright 2025 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.tools.sql import drop_constraint + + +def _pre_init_hook(env): + drop_constraint( + env.cr, + "mail_template_ir_actions_report_rel", + "mail_template_ir_actions_report_rel_pkey", + ) + query = """ + ALTER TABLE mail_template_ir_actions_report_rel + ADD COLUMN id SERIAL PRIMARY KEY; + """ + env.cr.execute(query) diff --git a/mail_template_conditional_report/models/__init__.py b/mail_template_conditional_report/models/__init__.py new file mode 100644 index 000000000..5d1012d3c --- /dev/null +++ b/mail_template_conditional_report/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_template +from . import mail_template_report diff --git a/mail_template_conditional_report/models/mail_template.py b/mail_template_conditional_report/models/mail_template.py new file mode 100644 index 000000000..5b194dc91 --- /dev/null +++ b/mail_template_conditional_report/models/mail_template.py @@ -0,0 +1,40 @@ +# Copyright 2025 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class MailTemplate(models.Model): + _inherit = "mail.template" + + mail_template_report_ids = fields.One2many( + comodel_name="mail.template.report", + inverse_name="mail_template_id", + string="Dynamic reports", + ) + + def _generate_template_attachments( + self, res_ids, render_fields, render_results=None + ): + results = super()._generate_template_attachments( + res_ids, render_fields, render_results=render_results + ) + self._filter_generated_attachments(results) + return results + + def _filter_generated_attachments(self, results): + """ + On given result, check the report should be removed or not. + :param results: dict + :return: + """ + for res_id in results.keys(): + values = results.setdefault(res_id, {}) + attachments = values.pop("attachments", []) + validated_attachments = [] + for template_report, attachment in zip( + self.mail_template_report_ids, attachments, strict=True + ): + record = self.env[template_report.model_name].browse(res_id) + if record.filtered_domain(template_report._get_eval_domain()): + validated_attachments.append(attachment) + values.update({"attachments": validated_attachments}) diff --git a/mail_template_conditional_report/models/mail_template_report.py b/mail_template_conditional_report/models/mail_template_report.py new file mode 100644 index 000000000..cb05679a6 --- /dev/null +++ b/mail_template_conditional_report/models/mail_template_report.py @@ -0,0 +1,65 @@ +# Copyright 2025 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models +from odoo.osv.expression import TRUE_DOMAIN +from odoo.tools.safe_eval import datetime, safe_eval + + +class MailTemplateReport(models.Model): + """ + Model used to represent existing relation between + report (ir.actions.report) and mail template (mail.template) + """ + + _name = "mail.template.report" + _table = "mail_template_ir_actions_report_rel" + _description = "Mail template report relation" + + mail_template_id = fields.Many2one( + comodel_name="mail.template", + string="Mail template", + required=True, + ondelete="cascade", + ) + ir_actions_report_id = fields.Many2one( + comodel_name="ir.actions.report", + string="Report", + required=True, + ondelete="cascade", + domain="[('model','=', model_name)]", + ) + model_name = fields.Char( + related="mail_template_id.model_id.model", + ) + filter_domain = fields.Char( + string="Domain", + help="Filter/Domain to apply on record to print to know if " + "the report must be added on the mail.\n" + "Technically, dynamics reports are always generated and then removed " + "from attachments if the record doesn't match to the domain.\n" + "Computed fields can be used (because filtered_domain(...) is used).", + default=False, + ) + + _sql_constraints = [ + ( + "unique_template_report", + "unique(mail_template_id, ir_actions_report_id)", + "The relation between the mail template and the report already exists!", + ), + ] + + def _get_eval_domain(self): + self.ensure_one() + if not self.filter_domain: + return TRUE_DOMAIN + return safe_eval( + self.filter_domain, + { + "user": self.env.user.with_context({}), # pylint: disable=context-overridden + "companies": self.env.companies, + "company": self.env.company, + "datetime": datetime, + "context_today": fields.Date.context_today(self), + }, + ) diff --git a/mail_template_conditional_report/pyproject.toml b/mail_template_conditional_report/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/mail_template_conditional_report/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_template_conditional_report/readme/CONTEXT.md b/mail_template_conditional_report/readme/CONTEXT.md new file mode 100644 index 000000000..5b835587d --- /dev/null +++ b/mail_template_conditional_report/readme/CONTEXT.md @@ -0,0 +1,4 @@ +This module is useful for customising the attachments you want to include according to the profile of the third party or the type of transaction. + +Example: you could have one reports per type of account.move (in_invoice, out_invoice,...). +Instead of having one mail template per type of account.move, you could use the same and add the domain on the attachment to determine which should be generated. diff --git a/mail_template_conditional_report/readme/CONTRIBUTORS.md b/mail_template_conditional_report/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..95c99866f --- /dev/null +++ b/mail_template_conditional_report/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Acsone](https://www.acsone.eu): + - François Honoré + diff --git a/mail_template_conditional_report/readme/DESCRIPTION.md b/mail_template_conditional_report/readme/DESCRIPTION.md new file mode 100644 index 000000000..d3ea554ee --- /dev/null +++ b/mail_template_conditional_report/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module has been developed to enable you to conditionally select the reports to be attached in an email template according to criteria in the email template subject. + +To do this, we've added to the mail.template object the ability to choose the ir.attchment that should be linked in the email template and the ability to set conditions for each document. diff --git a/mail_template_conditional_report/security/mail_template_report.xml b/mail_template_conditional_report/security/mail_template_report.xml new file mode 100644 index 000000000..c0490adc4 --- /dev/null +++ b/mail_template_conditional_report/security/mail_template_report.xml @@ -0,0 +1,14 @@ + + + + + access_mail_template_report + + + 1 + 1 + 1 + 1 + + diff --git a/mail_template_conditional_report/static/description/index.html b/mail_template_conditional_report/static/description/index.html new file mode 100644 index 000000000..8e6c49634 --- /dev/null +++ b/mail_template_conditional_report/static/description/index.html @@ -0,0 +1,442 @@ + + + + + +Mail template conditional report + + + +
+

Mail template conditional report

+ + +

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

+

This module has been developed to enable you to conditionally select the +reports to be attached in an email template according to criteria in the +email template subject.

+

To do this, we’ve added to the mail.template object the ability to +choose the ir.attchment that should be linked in the email template and +the ability to set conditions for each document.

+

Table of contents

+ +
+

Use Cases / Context

+

This module is useful for customising the attachments you want to +include according to the profile of the third party or the type of +transaction.

+

Example: you could have one reports per type of account.move +(in_invoice, out_invoice,…). Instead of having one mail template per +type of account.move, you could use the same and add the domain on the +attachment to determine which should be generated.

+
+
+

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

+
    +
  • ACSONE SA/NV
  • +
+
+
+

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_conditional_report/tests/__init__.py b/mail_template_conditional_report/tests/__init__.py new file mode 100644 index 000000000..b3ad65b46 --- /dev/null +++ b/mail_template_conditional_report/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_template diff --git a/mail_template_conditional_report/tests/test_mail_template.py b/mail_template_conditional_report/tests/test_mail_template.py new file mode 100644 index 000000000..9adb68e27 --- /dev/null +++ b/mail_template_conditional_report/tests/test_mail_template.py @@ -0,0 +1,95 @@ +# Copyright 2025 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo.osv.expression import FALSE_DOMAIN, TRUE_DOMAIN +from odoo.tests.common import TransactionCase + + +class TestMailTemplate(TransactionCase): + """ + Tests for mail.template + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.Mail = cls.env["mail.mail"] + cls.report = cls.env.ref("base.report_ir_model_overview") + cls.mail_template = cls.env.ref( + "mail_template_conditional_report.mail_template_demo_hello_world" + ) + cls.mail_template2 = cls.env.ref( + "mail_template_conditional_report.mail_template_demo_promote" + ) + + def test_email_generation_no_domain(self): + """ + Ensure the email generation is working without any domain + :return: + """ + self.assertFalse(self.mail_template.mail_template_report_ids.filter_domain) + target_model = self.env.ref("base.model_res_partner") + mail_mail_id = self.mail_template.send_mail(target_model.id) + mail = self.Mail.browse(mail_mail_id).exists() + self.assertTrue(mail) + self.assertEqual(len(mail.attachment_ids), 1) + + def test_email_generation_with_domain_valid1(self): + """ + Ensure the email generation is working with a True leaf domain + :return: + """ + self.mail_template.mail_template_report_ids.write( + { + "filter_domain": TRUE_DOMAIN, + } + ) + target_model = self.env.ref("base.model_res_partner") + mail_mail_id = self.mail_template.send_mail(target_model.id) + mail = self.Mail.browse(mail_mail_id).exists() + self.assertTrue(mail) + self.assertEqual(len(mail.attachment_ids), 1) + + def test_email_generation_with_domain_valid2(self): + """ + Ensure the email generation is working with a custom valid domain + :return: + """ + target_model = self.env.ref("base.model_res_partner") + self.mail_template.mail_template_report_ids.write( + { + "filter_domain": [("name", "=", target_model.name)], + } + ) + mail_mail_id = self.mail_template.send_mail(target_model.id) + mail = self.Mail.browse(mail_mail_id).exists() + self.assertTrue(mail) + self.assertEqual(len(mail.attachment_ids), 1) + + def test_email_generation_with_domain_invalid(self): + """ + Ensure the email generation is working with False leaf domain + :return: + """ + self.mail_template.mail_template_report_ids.write( + { + "filter_domain": FALSE_DOMAIN, + } + ) + target_model = self.env.ref("base.model_res_partner") + mail_mail_id = self.mail_template.send_mail(target_model.id) + mail = self.Mail.browse(mail_mail_id).exists() + self.assertTrue(mail) + self.assertEqual(len(mail.attachment_ids), 0) + + def test_email_generation_with_domain_mixed(self): + """ + Ensure the email generation is working with multiple domains + :return: + """ + target_model = self.env.ref("base.model_res_partner") + mail_mail_id = self.mail_template2.send_mail(target_model.id) + mail = self.Mail.browse(mail_mail_id).exists() + self.assertTrue(mail) + # We should have 3 but the second is dropped by the domain + self.assertEqual(len(mail.attachment_ids), 3 - 1) diff --git a/mail_template_conditional_report/views/mail_template.xml b/mail_template_conditional_report/views/mail_template.xml new file mode 100644 index 000000000..5286e4793 --- /dev/null +++ b/mail_template_conditional_report/views/mail_template.xml @@ -0,0 +1,58 @@ + + + + + mail.template.form (in mail_template_conditional_report) + mail.template + + + + + True + + + + + + + + + + + +
+ + + + + + + + + +
+
+ + +
+
+
+
+