From 2df8bb339a0669523b3e24751e4910c32c7222ef Mon Sep 17 00:00:00 2001 From: Christopher Rogos Date: Fri, 27 Mar 2026 16:36:36 +0000 Subject: [PATCH 1/2] [ADD] mail_activity_restrict: Implement mail activity write restrictions based on assigned user --- mail_activity_restrict/README.rst | 84 ++++ mail_activity_restrict/__init__.py | 1 + mail_activity_restrict/__manifest__.py | 16 + mail_activity_restrict/models/__init__.py | 2 + .../models/mail_activity.py | 49 ++ .../models/mail_activity_type.py | 18 + mail_activity_restrict/pyproject.toml | 3 + mail_activity_restrict/readme/DESCRIPTION.md | 5 + mail_activity_restrict/readme/USAGE.md | 8 + .../static/description/index.html | 426 ++++++++++++++++++ mail_activity_restrict/tests/__init__.py | 1 + .../tests/test_mail_activity_restrict.py | 134 ++++++ .../views/mail_activity_type_views.xml | 13 + 13 files changed, 760 insertions(+) create mode 100644 mail_activity_restrict/README.rst create mode 100644 mail_activity_restrict/__init__.py create mode 100644 mail_activity_restrict/__manifest__.py create mode 100644 mail_activity_restrict/models/__init__.py create mode 100644 mail_activity_restrict/models/mail_activity.py create mode 100644 mail_activity_restrict/models/mail_activity_type.py create mode 100644 mail_activity_restrict/pyproject.toml create mode 100644 mail_activity_restrict/readme/DESCRIPTION.md create mode 100644 mail_activity_restrict/readme/USAGE.md create mode 100644 mail_activity_restrict/static/description/index.html create mode 100644 mail_activity_restrict/tests/__init__.py create mode 100644 mail_activity_restrict/tests/test_mail_activity_restrict.py create mode 100644 mail_activity_restrict/views/mail_activity_type_views.xml diff --git a/mail_activity_restrict/README.rst b/mail_activity_restrict/README.rst new file mode 100644 index 000000000..9e9ca36e0 --- /dev/null +++ b/mail_activity_restrict/README.rst @@ -0,0 +1,84 @@ +=============================== +Mail Activity Write Restriction +=============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:fca1d7f4b556efb27cc73e10028731aabfc09055e8de4512727b28352a6361e3 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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_activity_restrict + :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_activity_restrict + :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| + +When the activity type option "Restrict Write to Assigned User" on an +activity type is enabled, only the assigned user can edit or mark the +activity as done. + +The restriction is enforced through the activity access check, so it +applies consistently to write and done flows. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +To use this module: + +1. Go to Activity Types. +2. Open the activity type you want to protect. +3. Enable "Restrict Write to Assigned User". +4. Save. + +From now on, only the assigned user can modify or complete activities of +this type. + +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 +======= + +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_activity_restrict/__init__.py b/mail_activity_restrict/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/mail_activity_restrict/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mail_activity_restrict/__manifest__.py b/mail_activity_restrict/__manifest__.py new file mode 100644 index 000000000..91aa97202 --- /dev/null +++ b/mail_activity_restrict/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Mail Activity Write Restriction", + "version": "18.0.1.0.0", + "category": "Productivity/Mail", + "license": "AGPL-3", + "author": "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/mail", + "depends": [ + "mail", + ], + "data": [ + "views/mail_activity_type_views.xml", + ], + "installable": True, + "application": False, +} diff --git a/mail_activity_restrict/models/__init__.py b/mail_activity_restrict/models/__init__.py new file mode 100644 index 000000000..1beace17c --- /dev/null +++ b/mail_activity_restrict/models/__init__.py @@ -0,0 +1,2 @@ +from . import mail_activity_type +from . import mail_activity diff --git a/mail_activity_restrict/models/mail_activity.py b/mail_activity_restrict/models/mail_activity.py new file mode 100644 index 000000000..87a8fcbdd --- /dev/null +++ b/mail_activity_restrict/models/mail_activity.py @@ -0,0 +1,49 @@ +"""Restrict mail.activity modifications to the assigned user.""" + +from odoo import models +from odoo.exceptions import AccessError + + +class MailActivity(models.Model): + """Extend mail.activity access checks for restricted activity types.""" + + _inherit = "mail.activity" + + def _get_can_write_restrict_allowed_activities(self): + """Return restricted activities the current user may modify.""" + return self.sudo().filtered_domain([("user_id", "=", self.env.uid)]) + + def _make_can_write_restrict_error(self, operation: str) -> AccessError: + """Build the access error raised for restricted activity types.""" + if len(self) == 1: + return AccessError( + self.env._( + "You cannot %(operation)s this activity because only " + "the assigned user can modify activities of this type.", + operation=operation, + ) + ) + return AccessError( + self.env._( + "You cannot %(operation)s some activities because only their " + "assigned user can modify activities of this type.", + operation=operation, + ) + ) + + def _check_access(self, operation: str) -> tuple | None: + """Restrict write-like operations when the activity type requires it.""" + result = super()._check_access(operation) + if operation not in ("write", "unlink") or not self or self.env.su: + return result + + restricted = self.filtered("activity_type_id.can_write_restrict") + forbidden = restricted - restricted._get_can_write_restrict_allowed_activities() + if not forbidden: + return result + + if result and not (forbidden - result[0]): + return result + + forbidden = result[0] | forbidden if result else forbidden + return forbidden, lambda: forbidden._make_can_write_restrict_error(operation) diff --git a/mail_activity_restrict/models/mail_activity_type.py b/mail_activity_restrict/models/mail_activity_type.py new file mode 100644 index 000000000..72c2341b4 --- /dev/null +++ b/mail_activity_restrict/models/mail_activity_type.py @@ -0,0 +1,18 @@ +"""Extension to mail.activity.type model for write restriction.""" + +from odoo import fields, models + + +class MailActivityType(models.Model): + """Extend mail.activity.type with write restriction control.""" + + _inherit = "mail.activity.type" + + can_write_restrict = fields.Boolean( + string="Restrict Write to Assigned User", + default=False, + help=( + "If checked, only the assigned user (user_id field) can write/edit " + "activities of this type. Other users cannot modify restricted activities." + ), + ) diff --git a/mail_activity_restrict/pyproject.toml b/mail_activity_restrict/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/mail_activity_restrict/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/mail_activity_restrict/readme/DESCRIPTION.md b/mail_activity_restrict/readme/DESCRIPTION.md new file mode 100644 index 000000000..10ab780e3 --- /dev/null +++ b/mail_activity_restrict/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +When the activity type option "Restrict Write to Assigned User" on an activity type is enabled, +only the assigned user can edit or mark the activity as done. + +The restriction is enforced through the activity access check, so it applies +consistently to write and done flows. \ No newline at end of file diff --git a/mail_activity_restrict/readme/USAGE.md b/mail_activity_restrict/readme/USAGE.md new file mode 100644 index 000000000..cc78c90ce --- /dev/null +++ b/mail_activity_restrict/readme/USAGE.md @@ -0,0 +1,8 @@ +To use this module: + +1. Go to Activity Types. +2. Open the activity type you want to protect. +3. Enable "Restrict Write to Assigned User". +4. Save. + +From now on, only the assigned user can modify or complete activities of this type. \ No newline at end of file diff --git a/mail_activity_restrict/static/description/index.html b/mail_activity_restrict/static/description/index.html new file mode 100644 index 000000000..9cfe936cf --- /dev/null +++ b/mail_activity_restrict/static/description/index.html @@ -0,0 +1,426 @@ + + + + + +Mail Activity Write Restriction + + + +
+

Mail Activity Write Restriction

+ + +

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

+

When the activity type option “Restrict Write to Assigned User” on an +activity type is enabled, only the assigned user can edit or mark the +activity as done.

+

The restriction is enforced through the activity access check, so it +applies consistently to write and done flows.

+

Table of contents

+ +
+

Usage

+

To use this module:

+
    +
  1. Go to Activity Types.
  2. +
  3. Open the activity type you want to protect.
  4. +
  5. Enable “Restrict Write to Assigned User”.
  6. +
  7. Save.
  8. +
+

From now on, only the assigned user can modify or complete activities of +this type.

+
+
+

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

+
+

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_activity_restrict/tests/__init__.py b/mail_activity_restrict/tests/__init__.py new file mode 100644 index 000000000..c1e523861 --- /dev/null +++ b/mail_activity_restrict/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mail_activity_restrict diff --git a/mail_activity_restrict/tests/test_mail_activity_restrict.py b/mail_activity_restrict/tests/test_mail_activity_restrict.py new file mode 100644 index 000000000..1cfc35125 --- /dev/null +++ b/mail_activity_restrict/tests/test_mail_activity_restrict.py @@ -0,0 +1,134 @@ +"""Tests for mail_activity_restrict.""" + +from odoo.exceptions import AccessError +from odoo.tests import TransactionCase, new_test_user + + +class TestMailActivityRestrict(TransactionCase): + """Test access restrictions on mail.activity.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Partner"}) + cls.partner_model = cls.env["ir.model"]._get("res.partner") + + cls.activity_type_unrestricted = cls.env["mail.activity.type"].create( + { + "name": "Unrestricted Activity", + "can_write_restrict": False, + } + ) + cls.activity_type_restricted = cls.env["mail.activity.type"].create( + { + "name": "Restricted Activity", + "can_write_restrict": True, + } + ) + cls.user_assigned = new_test_user( + cls.env, + login="user_assigned@example.com", + name="Assigned User", + groups="base.group_user", + ) + cls.user_other = new_test_user( + cls.env, + login="user_other@example.com", + name="Other User", + groups="base.group_user", + ) + + @classmethod + def _create_activity(cls, activity_type, user): + return cls.env["mail.activity"].create( + { + "activity_type_id": activity_type.id, + "res_model_id": cls.partner_model.id, + "res_id": cls.partner.id, + "user_id": user.id, + "summary": activity_type.name, + "date_deadline": "2026-12-31", + } + ) + + def test_unrestricted_activity_assigned_user_can_write(self): + """Unrestricted activities keep the assignee's default write behavior.""" + activity = self._create_activity( + self.activity_type_unrestricted, self.user_assigned + ) + + activity.with_user(self.user_assigned).write( + {"summary": "Updated by Assigned User"} + ) + + self.assertEqual(activity.summary, "Updated by Assigned User") + + def test_restricted_activity_assigned_user_can_write(self): + """Assigned users may still edit restricted activities.""" + activity = self._create_activity( + self.activity_type_restricted, self.user_assigned + ) + + activity.with_user(self.user_assigned).write( + {"summary": "Updated by Assigned User"} + ) + + self.assertEqual(activity.summary, "Updated by Assigned User") + + def test_restricted_activity_other_user_cannot_write(self): + """Non-assigned users cannot edit restricted activities.""" + activity = self._create_activity( + self.activity_type_restricted, self.user_assigned + ) + + with self.assertRaises(AccessError): + activity.with_user(self.user_other).write( + {"summary": "Updated by Other User"} + ) + + def test_restricted_activity_other_user_cannot_mark_done(self): + """Restricted activities also block mark done for other users.""" + activity = self._create_activity( + self.activity_type_restricted, self.user_assigned + ) + + with self.assertRaises(AccessError): + activity.with_user(self.user_other).action_done() + + def test_restricted_activity_assigned_user_can_mark_done(self): + """Assigned users may mark restricted activities as done.""" + activity = self._create_activity( + self.activity_type_restricted, self.user_assigned + ) + activity_id = activity.id + + activity.with_user(self.user_assigned).action_done() + + self.assertFalse(self.env["mail.activity"].browse(activity_id).exists()) + + def test_restrict_field_default_value(self): + """The restriction flag defaults to False.""" + activity_type = self.env["mail.activity.type"].create( + { + "name": "Default Activity Type", + } + ) + self.assertFalse(activity_type.can_write_restrict) + + def test_restrict_field_can_be_toggled(self): + """The restriction flag can be toggled.""" + activity_type = self.env["mail.activity.type"].create( + { + "name": "Toggleable Activity Type", + "can_write_restrict": False, + } + ) + self.assertFalse(activity_type.can_write_restrict) + + # Toggle on + activity_type.write({"can_write_restrict": True}) + self.assertTrue(activity_type.can_write_restrict) + + # Toggle off + activity_type.write({"can_write_restrict": False}) + self.assertFalse(activity_type.can_write_restrict) diff --git a/mail_activity_restrict/views/mail_activity_type_views.xml b/mail_activity_restrict/views/mail_activity_type_views.xml new file mode 100644 index 000000000..d3ab5e335 --- /dev/null +++ b/mail_activity_restrict/views/mail_activity_type_views.xml @@ -0,0 +1,13 @@ + + + + mail.activity.type.view.form.restrict + mail.activity.type + + + + + + + + From 5f4571fa061d122de5b267032d6d9e253b25682a Mon Sep 17 00:00:00 2001 From: Christopher Rogos Date: Thu, 9 Apr 2026 12:40:18 +0000 Subject: [PATCH 2/2] [MIG] mail_activity_restrict: Migration to 19.0 --- mail_activity_restrict/README.rst | 16 +++++++----- mail_activity_restrict/__manifest__.py | 2 +- .../static/description/index.html | 26 ++++++++++++------- .../tests/test_mail_activity_restrict.py | 5 +--- .../views/mail_activity_type_views.xml | 4 +-- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/mail_activity_restrict/README.rst b/mail_activity_restrict/README.rst index 9e9ca36e0..342e79f1a 100644 --- a/mail_activity_restrict/README.rst +++ b/mail_activity_restrict/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + =============================== Mail Activity Write Restriction =============================== @@ -13,17 +17,17 @@ Mail Activity Write Restriction .. |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 +.. |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/18.0/mail_activity_restrict + :target: https://github.com/OCA/mail/tree/19.0/mail_activity_restrict :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_activity_restrict + :target: https://translation.odoo-community.org/projects/mail-19-0/mail-19-0-mail_activity_restrict :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 + :target: https://runboat.odoo-community.org/builds?repo=OCA/mail&target_branch=19.0 :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -59,7 +63,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -79,6 +83,6 @@ 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. +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_activity_restrict/__manifest__.py b/mail_activity_restrict/__manifest__.py index 91aa97202..0df0a6301 100644 --- a/mail_activity_restrict/__manifest__.py +++ b/mail_activity_restrict/__manifest__.py @@ -1,6 +1,6 @@ { "name": "Mail Activity Write Restriction", - "version": "18.0.1.0.0", + "version": "19.0.1.0.0", "category": "Productivity/Mail", "license": "AGPL-3", "author": "Odoo Community Association (OCA)", diff --git a/mail_activity_restrict/static/description/index.html b/mail_activity_restrict/static/description/index.html index 9cfe936cf..fc63ba8d3 100644 --- a/mail_activity_restrict/static/description/index.html +++ b/mail_activity_restrict/static/description/index.html @@ -3,7 +3,7 @@ -Mail Activity Write Restriction +README.rst -
-

Mail Activity Write Restriction

+
+ + +Odoo Community Association + +
+

Mail Activity Write Restriction

-

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

+

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

When the activity type option “Restrict Write to Assigned User” on an activity type is enabled, only the assigned user can edit or mark the activity as done.

@@ -387,7 +392,7 @@

Mail Activity Write Restriction

-

Usage

+

Usage

To use this module:

  1. Go to Activity Types.
  2. @@ -399,17 +404,17 @@

    Usage

    this type.

-

Bug Tracker

+

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.

+feedback.

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

-

Credits

+

Credits

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -417,10 +422,11 @@

Maintainers

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.

+

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_activity_restrict/tests/test_mail_activity_restrict.py b/mail_activity_restrict/tests/test_mail_activity_restrict.py index 1cfc35125..e4178e593 100644 --- a/mail_activity_restrict/tests/test_mail_activity_restrict.py +++ b/mail_activity_restrict/tests/test_mail_activity_restrict.py @@ -100,11 +100,8 @@ def test_restricted_activity_assigned_user_can_mark_done(self): activity = self._create_activity( self.activity_type_restricted, self.user_assigned ) - activity_id = activity.id - activity.with_user(self.user_assigned).action_done() - - self.assertFalse(self.env["mail.activity"].browse(activity_id).exists()) + self.assertEqual(activity.state, "done") def test_restrict_field_default_value(self): """The restriction flag defaults to False.""" diff --git a/mail_activity_restrict/views/mail_activity_type_views.xml b/mail_activity_restrict/views/mail_activity_type_views.xml index d3ab5e335..27e8a54bd 100644 --- a/mail_activity_restrict/views/mail_activity_type_views.xml +++ b/mail_activity_restrict/views/mail_activity_type_views.xml @@ -5,9 +5,9 @@ mail.activity.type - + - +