diff --git a/automation_oca_activity_note_template/README.rst b/automation_oca_activity_note_template/README.rst new file mode 100644 index 0000000..4d272e3 --- /dev/null +++ b/automation_oca_activity_note_template/README.rst @@ -0,0 +1,151 @@ +======================================= +Automation OCA - Activity Note Template +======================================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6fac894cded1581e6da7ead1f6d5abe64054691afb727804263b27cb47eb0bff + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fautomation-lightgray.png?logo=github + :target: https://github.com/OCA/automation/tree/18.0/automation_oca_activity_note_template + :alt: OCA/automation +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/automation-18-0/automation-18-0-automation_oca_activity_note_template + :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/automation&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the **Activity** step type in ``automation_oca`` +with the ability to select a **mail template** as the source for the +activity's summary and note. + +When a mail template is selected on an automation step: + +- The template **subject** is rendered and used as the **activity + summary**. +- The template **body** is rendered and used as the **activity note**. + +Both fields are rendered using the ``inline_template`` engine, which +supports ``{{ object.field }}`` and ``{{ object.method() }}`` +expressions evaluated against the target record at the moment the +activity is created. + +This makes it straightforward to generate **dynamic, personalised +activity notes** by calling a method on the target model from inside the +template body. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +``automation_oca`` activity steps support a static **Note** and +**Summary** field, which are written as plain text or HTML at +configuration time and copied verbatim to every activity created by the +step. + +This is limiting when the note needs to reflect data specific to each +record — for example, a personalised LinkedIn outreach prompt. + +``automation_oca_activity_note_template`` solves this by delegating the +rendering to a **mail template**, which already has a well-established +mechanism for per-record dynamic content and is familiar to Odoo users. + +Usage +===== + +Configure a mail template +------------------------- + +1. Go to **Email** → **Templates** and create a new template. + +2. Set the **Applies To** model to match the model used in your + automation (e.g. *Contact*, *CRM Lead*). + +3. Write the **Subject** — it will become the **activity summary**. Use + ``{{ object.field }}`` expressions to include record-specific data. + +4. Write the **Body** — it will become the **activity note**. You may + call methods on ``object``, for example: + + :: + + {{ object.generate_note() }} + +Configure the automation step +----------------------------- + +1. Open an **Automation Configuration** and add or edit an **Activity** + step. +2. In the **Activity** tab, select the template in the **Note Template** + field. +3. The static **Summary** and **Note** fields are hidden when a template + is selected, as they are replaced by the rendered template output. +4. Save and start the automation as usual. + +When the step runs, the template subject and body are rendered +individually for each target record and set as the activity summary and +note respectively. + +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 +------- + +* ForgeFlow + +Contributors +------------ + +- Jordi Ballester (`ForgeFlow `__) + +Other credits +------------- + +The development of this module has been financially supported by: + +- `ForgeFlow `__ + +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/automation `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/automation_oca_activity_note_template/__init__.py b/automation_oca_activity_note_template/__init__.py new file mode 100644 index 0000000..1141406 --- /dev/null +++ b/automation_oca_activity_note_template/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import models diff --git a/automation_oca_activity_note_template/__manifest__.py b/automation_oca_activity_note_template/__manifest__.py new file mode 100644 index 0000000..37d517f --- /dev/null +++ b/automation_oca_activity_note_template/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Automation OCA - Activity Note Template", + "summary": "Render activity notes from a mail template in automation steps", + "version": "18.0.1.0.0", + "category": "Technical", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["automation_oca"], + "data": [ + "views/automation_configuration_step_views.xml", + ], + "installable": True, +} diff --git a/automation_oca_activity_note_template/models/__init__.py b/automation_oca_activity_note_template/models/__init__.py new file mode 100644 index 0000000..cc7ded8 --- /dev/null +++ b/automation_oca_activity_note_template/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import automation_configuration_step +from . import automation_record_step diff --git a/automation_oca_activity_note_template/models/automation_configuration_step.py b/automation_oca_activity_note_template/models/automation_configuration_step.py new file mode 100644 index 0000000..f1ddbfe --- /dev/null +++ b/automation_oca_activity_note_template/models/automation_configuration_step.py @@ -0,0 +1,16 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class AutomationConfigurationStep(models.Model): + _inherit = "automation.configuration.step" + + activity_note_template_id = fields.Many2one( + "mail.template", + string="Note Template", + domain="[('model_id', '=', model_id)]", + help="If set, the activity note is rendered from this mail template. " + "The template body may use {{ object.method() }} expressions. " + "Overrides the static Note field.", + ) diff --git a/automation_oca_activity_note_template/models/automation_record_step.py b/automation_oca_activity_note_template/models/automation_record_step.py new file mode 100644 index 0000000..f4f336c --- /dev/null +++ b/automation_oca_activity_note_template/models/automation_record_step.py @@ -0,0 +1,61 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from dateutil.relativedelta import relativedelta + +from odoo import fields, models + + +class AutomationRecordStep(models.Model): + _inherit = "automation.record.step" + + def _render_template_for_record( + self, template_src, res_id, engine="inline_template" + ): + rendered = self.env["mail.template"]._render_template( + template_src, + self.record_id.model, + [res_id], + engine=engine, + ) + return rendered.get(res_id, "") + + def _get_activity_summary(self): + template = self.configuration_step_id.activity_note_template_id + if not template: + return self.configuration_step_id.activity_summary or "" + return self._render_template_for_record(template.subject, self.record_id.res_id) + + def _get_activity_note(self): + template = self.configuration_step_id.activity_note_template_id + if not template: + return self.configuration_step_id.activity_note or "" + return self._render_template_for_record( + template.body_html, self.record_id.res_id + ) + + def _run_activity(self): + if not self.configuration_step_id.activity_note_template_id: + return super()._run_activity() + + record = self.env[self.record_id.model].browse(self.record_id.res_id) + step = self.configuration_step_id + vals = { + "summary": self._get_activity_summary(), + "note": self._get_activity_note(), + "activity_type_id": step.activity_type_id.id, + "automation_record_step_id": self.id, + } + if step.activity_date_deadline_range > 0: + range_type = step.activity_date_deadline_range_type + vals["date_deadline"] = fields.Date.context_today(self) + relativedelta( + **{range_type: step.activity_date_deadline_range} + ) + user = False + if step.activity_user_type == "specific": + user = step.activity_user_id + elif step.activity_user_type == "generic": + user = record[step.activity_user_field_id.name] + if user: + vals["user_id"] = user.id + record.activity_schedule(**vals) + return True diff --git a/automation_oca_activity_note_template/pyproject.toml b/automation_oca_activity_note_template/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/automation_oca_activity_note_template/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/automation_oca_activity_note_template/readme/CONTEXT.md b/automation_oca_activity_note_template/readme/CONTEXT.md new file mode 100644 index 0000000..387a2ad --- /dev/null +++ b/automation_oca_activity_note_template/readme/CONTEXT.md @@ -0,0 +1,10 @@ +`automation_oca` activity steps support a static **Note** and **Summary** field, +which are written as plain text or HTML at configuration time and copied verbatim +to every activity created by the step. + +This is limiting when the note needs to reflect data specific to each record — +for example, a personalised LinkedIn outreach prompt. + +`automation_oca_activity_note_template` solves this by delegating the rendering +to a **mail template**, which already has a well-established mechanism for +per-record dynamic content and is familiar to Odoo users. diff --git a/automation_oca_activity_note_template/readme/CONTRIBUTORS.md b/automation_oca_activity_note_template/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..d96eaa5 --- /dev/null +++ b/automation_oca_activity_note_template/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Jordi Ballester ([ForgeFlow](https://www.forgeflow.com/)) diff --git a/automation_oca_activity_note_template/readme/CREDITS.md b/automation_oca_activity_note_template/readme/CREDITS.md new file mode 100644 index 0000000..6c667a9 --- /dev/null +++ b/automation_oca_activity_note_template/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- [ForgeFlow](https://www.forgeflow.com/) diff --git a/automation_oca_activity_note_template/readme/DESCRIPTION.md b/automation_oca_activity_note_template/readme/DESCRIPTION.md new file mode 100644 index 0000000..f41d3af --- /dev/null +++ b/automation_oca_activity_note_template/readme/DESCRIPTION.md @@ -0,0 +1,14 @@ +This module extends the **Activity** step type in `automation_oca` with the ability +to select a **mail template** as the source for the activity's summary and note. + +When a mail template is selected on an automation step: + +- The template **subject** is rendered and used as the **activity summary**. +- The template **body** is rendered and used as the **activity note**. + +Both fields are rendered using the `inline_template` engine, which supports +`{{ object.field }}` and `{{ object.method() }}` expressions evaluated against +the target record at the moment the activity is created. + +This makes it straightforward to generate **dynamic, personalised activity notes** +by calling a method on the target model from inside the template body. diff --git a/automation_oca_activity_note_template/readme/USAGE.md b/automation_oca_activity_note_template/readme/USAGE.md new file mode 100644 index 0000000..959ba68 --- /dev/null +++ b/automation_oca_activity_note_template/readme/USAGE.md @@ -0,0 +1,26 @@ +Configure a mail template +-------------------------- + +1. Go to **Email** → **Templates** and create a new template. +2. Set the **Applies To** model to match the model used in your automation + (e.g. *Contact*, *CRM Lead*). +3. Write the **Subject** — it will become the **activity summary**. + Use `{{ object.field }}` expressions to include record-specific data. +4. Write the **Body** — it will become the **activity note**. + You may call methods on `object`, for example: + + ``` + {{ object.generate_note() }} + ``` + +Configure the automation step +------------------------------ + +1. Open an **Automation Configuration** and add or edit an **Activity** step. +2. In the **Activity** tab, select the template in the **Note Template** field. +3. The static **Summary** and **Note** fields are hidden when a template is selected, + as they are replaced by the rendered template output. +4. Save and start the automation as usual. + +When the step runs, the template subject and body are rendered individually for +each target record and set as the activity summary and note respectively. diff --git a/automation_oca_activity_note_template/static/description/icon.png b/automation_oca_activity_note_template/static/description/icon.png new file mode 100644 index 0000000..d4f84f3 Binary files /dev/null and b/automation_oca_activity_note_template/static/description/icon.png differ diff --git a/automation_oca_activity_note_template/static/description/index.html b/automation_oca_activity_note_template/static/description/index.html new file mode 100644 index 0000000..1be667f --- /dev/null +++ b/automation_oca_activity_note_template/static/description/index.html @@ -0,0 +1,501 @@ + + + + + +Automation OCA - Activity Note Template + + + +
+

Automation OCA - Activity Note Template

+ + +

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

+

This module extends the Activity step type in automation_oca +with the ability to select a mail template as the source for the +activity’s summary and note.

+

When a mail template is selected on an automation step:

+
    +
  • The template subject is rendered and used as the activity +summary.
  • +
  • The template body is rendered and used as the activity note.
  • +
+

Both fields are rendered using the inline_template engine, which +supports {{ object.field }} and {{ object.method() }} +expressions evaluated against the target record at the moment the +activity is created.

+

This makes it straightforward to generate dynamic, personalised +activity notes by calling a method on the target model from inside the +template body.

+

Table of contents

+ +
+

Use Cases / Context

+

automation_oca activity steps support a static Note and +Summary field, which are written as plain text or HTML at +configuration time and copied verbatim to every activity created by the +step.

+

This is limiting when the note needs to reflect data specific to each +record — for example, a personalised LinkedIn outreach prompt.

+

automation_oca_activity_note_template solves this by delegating the +rendering to a mail template, which already has a well-established +mechanism for per-record dynamic content and is familiar to Odoo users.

+
+
+

Usage

+
+

Configure a mail template

+
    +
  1. Go to EmailTemplates and create a new template.

    +
  2. +
  3. Set the Applies To model to match the model used in your +automation (e.g. Contact, CRM Lead).

    +
  4. +
  5. Write the Subject — it will become the activity summary. Use +{{ object.field }} expressions to include record-specific data.

    +
  6. +
  7. Write the Body — it will become the activity note. You may +call methods on object, for example:

    +
    +{{ object.generate_note() }}
    +
    +
  8. +
+
+
+

Configure the automation step

+
    +
  1. Open an Automation Configuration and add or edit an Activity +step.
  2. +
  3. In the Activity tab, select the template in the Note Template +field.
  4. +
  5. The static Summary and Note fields are hidden when a template +is selected, as they are replaced by the rendered template output.
  6. +
  7. Save and start the automation as usual.
  8. +
+

When the step runs, the template subject and body are rendered +individually for each target record and set as the activity summary and +note respectively.

+
+
+
+

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

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

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/automation project on GitHub.

+

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

+
+
+
+ + diff --git a/automation_oca_activity_note_template/tests/__init__.py b/automation_oca_activity_note_template/tests/__init__.py new file mode 100644 index 0000000..7e57d6a --- /dev/null +++ b/automation_oca_activity_note_template/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from . import test_automation_activity_note_template diff --git a/automation_oca_activity_note_template/tests/test_automation_activity_note_template.py b/automation_oca_activity_note_template/tests/test_automation_activity_note_template.py new file mode 100644 index 0000000..e473cae --- /dev/null +++ b/automation_oca_activity_note_template/tests/test_automation_activity_note_template.py @@ -0,0 +1,87 @@ +# Copyright 2026 ForgeFlow +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.automation_oca.tests.common import AutomationTestCase + + +class TestAutomationActivityNoteTemplate(AutomationTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.note_template = cls.env["mail.template"].create( + { + "name": "Activity note template", + "model_id": cls.env.ref("base.model_res_partner").id, + "subject": "Follow up with {{ object.name }}", + "body_html": "

Contact: {{ object.name }}

", + } + ) + + def _run_automation(self, domain): + self.configuration.editable_domain = domain + self.configuration.start_automation() + self.env["automation.configuration"].cron_automation() + self.env["automation.record.step"]._cron_automation_steps() + + def test_summary_and_note_rendered_from_template(self): + """Both subject and body_html of the template are rendered into summary and note.""" + activity = self.create_activity_action( + activity_note_template_id=self.note_template.id, + ) + self._run_automation(f"[('id', '=', {self.partner_01.id})]") + self.assertTrue(self.partner_01.activity_ids) + act = self.partner_01.activity_ids + self.assertIn(self.partner_01.name, act.summary) + self.assertIn(self.partner_01.name, act.note) + activity.unlink() + + def test_static_note_without_template(self): + """Without a note template, static activity_note is used unchanged.""" + activity = self.create_activity_action( + activity_note="

Static note

", + ) + self._run_automation(f"[('id', '=', {self.partner_01.id})]") + self.assertTrue(self.partner_01.activity_ids) + self.assertEqual(self.partner_01.activity_ids.note, "

Static note

") + activity.unlink() + + def test_note_rendered_from_template(self): + """When a note template is set, body_html is rendered against the record.""" + activity = self.create_activity_action( + activity_note_template_id=self.note_template.id, + ) + self._run_automation(f"[('id', '=', {self.partner_01.id})]") + self.assertTrue(self.partner_01.activity_ids) + self.assertIn(self.partner_01.name, self.partner_01.activity_ids.note) + activity.unlink() + + def test_template_overrides_static_note(self): + """When both note template and static note are set, template takes precedence.""" + activity = self.create_activity_action( + activity_note="

Static note

", + activity_note_template_id=self.note_template.id, + ) + self._run_automation(f"[('id', '=', {self.partner_01.id})]") + self.assertTrue(self.partner_01.activity_ids) + note = self.partner_01.activity_ids.note + self.assertIn(self.partner_01.name, note) + self.assertNotIn("Static note", note) + activity.unlink() + + def test_note_rendered_per_record(self): + """Template is rendered individually for each record.""" + activity = self.create_activity_action( + activity_note_template_id=self.note_template.id, + ) + self._run_automation( + f"[('id', 'in', [{self.partner_01.id}, {self.partner_02.id}])]" + ) + self.assertTrue(self.partner_01.activity_ids) + self.assertTrue(self.partner_02.activity_ids) + note_01 = self.partner_01.activity_ids.note + note_02 = self.partner_02.activity_ids.note + self.assertIn(self.partner_01.name, note_01) + self.assertIn(self.partner_02.name, note_02) + self.assertNotEqual(note_01, note_02) + self.assertNotIn(self.partner_02.name, note_01) + activity.unlink() diff --git a/automation_oca_activity_note_template/views/automation_configuration_step_views.xml b/automation_oca_activity_note_template/views/automation_configuration_step_views.xml new file mode 100644 index 0000000..e6a53d1 --- /dev/null +++ b/automation_oca_activity_note_template/views/automation_configuration_step_views.xml @@ -0,0 +1,35 @@ + + + + + automation.configuration.step + + + + + + + activity_note_template_id != False + Summary (static) + + + activity_note_template_id != False + Note (static) + + + +