From e2618de352358bdd2519d0c08ba2ea99cd32f28f Mon Sep 17 00:00:00 2001 From: prayag Date: Mon, 24 Aug 2026 16:20:42 +0530 Subject: [PATCH] [ADD] base_report_manager --- base_report_manager/README.rst | 116 +++++ base_report_manager/__init__.py | 4 + base_report_manager/__manifest__.py | 17 + base_report_manager/models/__init__.py | 5 + .../models/ir_actions_actions.py | 35 ++ .../models/ir_actions_report.py | 36 ++ base_report_manager/models/res_groups.py | 17 + base_report_manager/pyproject.toml | 3 + base_report_manager/readme/CONFIGURE.md | 7 + base_report_manager/readme/CONTRIBUTORS.md | 2 + base_report_manager/readme/DESCRIPTION.md | 5 + base_report_manager/readme/ROADMAP.md | 1 + base_report_manager/readme/USAGE.md | 5 + .../static/description/index.html | 468 ++++++++++++++++++ base_report_manager/tests/__init__.py | 4 + base_report_manager/tests/test_base_report.py | 212 ++++++++ .../views/res_groups_views.xml | 28 ++ 17 files changed, 965 insertions(+) create mode 100644 base_report_manager/README.rst create mode 100644 base_report_manager/__init__.py create mode 100644 base_report_manager/__manifest__.py create mode 100644 base_report_manager/models/__init__.py create mode 100644 base_report_manager/models/ir_actions_actions.py create mode 100644 base_report_manager/models/ir_actions_report.py create mode 100644 base_report_manager/models/res_groups.py create mode 100644 base_report_manager/pyproject.toml create mode 100644 base_report_manager/readme/CONFIGURE.md create mode 100644 base_report_manager/readme/CONTRIBUTORS.md create mode 100644 base_report_manager/readme/DESCRIPTION.md create mode 100644 base_report_manager/readme/ROADMAP.md create mode 100644 base_report_manager/readme/USAGE.md create mode 100644 base_report_manager/static/description/index.html create mode 100644 base_report_manager/tests/__init__.py create mode 100644 base_report_manager/tests/test_base_report.py create mode 100644 base_report_manager/views/res_groups_views.xml diff --git a/base_report_manager/README.rst b/base_report_manager/README.rst new file mode 100644 index 0000000..f24e532 --- /dev/null +++ b/base_report_manager/README.rst @@ -0,0 +1,116 @@ +=================== +Base Report Manager +=================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:1e773c2fd37965ea5dfe074cf4596b46264cc1d4ba558180fa6377a20c0e1dca + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--ux-lightgray.png?logo=github + :target: https://github.com/OCA/server-ux/tree/18.0/base_report_manager + :alt: OCA/server-ux +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-ux-18-0/server-ux-18-0-base_report_manager + :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/server-ux&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides granular visibility control over PDF and QWeb +Reports based on User Groups: + +1. It introduces the ability to configure "Restricted Report Actions" + directly within Odoo Security Groups. +2. It enforces a "Restricted / Deny List" policy, ensuring that users + cannot view or print reports that are restricted for any of their + assigned groups (either directly or transitively inherited). + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure report restrictions: + +1. Go to **Settings > Users & Companies > Groups** and select the Group + you want to configure. +2. Under the **Restricted Report Actions** tab, select the report + actions you want to hide from members of this group. + +Note: Group privilege inheritance is transitive. If a report action is +restricted for a group, that restriction propagates to all users who +belong to the group, including those who inherit the group (e.g., +restricting a lower-privilege group like "User" will also restrict a +higher-privilege group like "Manager" that implies/inherits it). + +Usage +===== + +Once report restrictions are configured for user groups: + +1. Assign the appropriate groups to the user. +2. The user navigates to the target model view (e.g., Partners). +3. Under the **Print** action menu, any report that is restricted for + the user's groups will be hidden. All other reports remain visible. + +Known issues / Roadmap +====================== + +- None. + +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 +------- + +* CIT Services + +Contributors +------------ + +- `CIT-Services `__ + + - Prayag + +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/server-ux `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_report_manager/__init__.py b/base_report_manager/__init__.py new file mode 100644 index 0000000..4f12309 --- /dev/null +++ b/base_report_manager/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/base_report_manager/__manifest__.py b/base_report_manager/__manifest__.py new file mode 100644 index 0000000..1fc8491 --- /dev/null +++ b/base_report_manager/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Base Report Manager", + "summary": "Manage report actions visibility per user group", + "version": "18.0.1.0.0", + "category": "Tools", + "author": "CIT Services, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-ux", + "license": "AGPL-3", + "installable": True, + "depends": ["base"], + "data": [ + "views/res_groups_views.xml", + ], +} diff --git a/base_report_manager/models/__init__.py b/base_report_manager/models/__init__.py new file mode 100644 index 0000000..a31f022 --- /dev/null +++ b/base_report_manager/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import res_groups +from . import ir_actions_actions +from . import ir_actions_report diff --git a/base_report_manager/models/ir_actions_actions.py b/base_report_manager/models/ir_actions_actions.py new file mode 100644 index 0000000..a915c28 --- /dev/null +++ b/base_report_manager/models/ir_actions_actions.py @@ -0,0 +1,35 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class IrActionsActions(models.Model): + _inherit = "ir.actions.actions" + + @api.model + def get_bindings(self, model_name): + """Filter out restricted reports from print sidebar and bindings.""" + result = super().get_bindings(model_name) + if self.env.is_superuser(): + return result + if "report" in result: + report_ids = list(map(lambda rep: rep.get("id"), result["report"])) + if reports := self.env["ir.actions.report"].browse(report_ids): + restricted_report_ids = [ + report.id + for report in reports + if report._is_action_report_restricted() + ] + if restricted_report_ids: + result["report"] = list( + filter( + lambda rep: rep + if rep.get("id") not in restricted_report_ids + else {}, + result["report"], + ) + ) + if not result.get("report", False): + result.pop("report") + return result diff --git a/base_report_manager/models/ir_actions_report.py b/base_report_manager/models/ir_actions_report.py new file mode 100644 index 0000000..0a4a5f5 --- /dev/null +++ b/base_report_manager/models/ir_actions_report.py @@ -0,0 +1,36 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.exceptions import AccessError + + +class IrActionsReport(models.Model): + _inherit = "ir.actions.report" + + def _is_action_report_restricted(self): + """Returns True if the current action is restricted for the current user, + taking group privilege inheritance into account. + """ + self.ensure_one() + if self.env.is_superuser(): + return False + + user_groups = self.env.user.sudo().groups_id + return self.sudo().id in user_groups.restricted_report_action_ids.ids + + def _check_action_report_restrictions(self): + """Raises AccessError if any action in self is restricted.""" + for action in self: + if action._is_action_report_restricted(): + raise AccessError( + _( + "You are not allowed to access this action because it " + "is restricted for one of your user groups." + ) + ) + + def _get_action_dict(self): + """Verify restrictions before loading action details for execution.""" + self._check_action_report_restrictions() + return super()._get_action_dict() diff --git a/base_report_manager/models/res_groups.py b/base_report_manager/models/res_groups.py new file mode 100644 index 0000000..bd4f8ff --- /dev/null +++ b/base_report_manager/models/res_groups.py @@ -0,0 +1,17 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResGroups(models.Model): + _inherit = "res.groups" + + restricted_report_action_ids = fields.Many2many( + comodel_name="ir.actions.report", + relation="res_groups_restricted_report_action_rel", + column1="gid", + column2="act_id", + string="Restricted Report Actions", + help="Report actions restricted for members of this group.", + ) diff --git a/base_report_manager/pyproject.toml b/base_report_manager/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/base_report_manager/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/base_report_manager/readme/CONFIGURE.md b/base_report_manager/readme/CONFIGURE.md new file mode 100644 index 0000000..ef91317 --- /dev/null +++ b/base_report_manager/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +To configure report restrictions: + +1. Go to **Settings > Users & Companies > Groups** and select the Group you want to configure. +2. Under the **Restricted Report Actions** tab, select the report actions you want to hide from members of this group. + +Note: Group privilege inheritance is transitive. If a report action is restricted for a group, that restriction propagates to all users who belong to the group, including those who inherit the group (e.g., restricting a lower-privilege group like "User" will also restrict a higher-privilege group like "Manager" that implies/inherits it). + diff --git a/base_report_manager/readme/CONTRIBUTORS.md b/base_report_manager/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..4b54f31 --- /dev/null +++ b/base_report_manager/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [CIT-Services](cit-services.eu) + - Prayag \<\> diff --git a/base_report_manager/readme/DESCRIPTION.md b/base_report_manager/readme/DESCRIPTION.md new file mode 100644 index 0000000..c4ed360 --- /dev/null +++ b/base_report_manager/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module provides granular visibility control over PDF and QWeb Reports based on User Groups: + +1. It introduces the ability to configure "Restricted Report Actions" directly within Odoo Security Groups. +2. It enforces a "Restricted / Deny List" policy, ensuring that users cannot view or print reports that are restricted for any of their assigned groups (either directly or transitively inherited). + diff --git a/base_report_manager/readme/ROADMAP.md b/base_report_manager/readme/ROADMAP.md new file mode 100644 index 0000000..1ac5b36 --- /dev/null +++ b/base_report_manager/readme/ROADMAP.md @@ -0,0 +1 @@ +- None. diff --git a/base_report_manager/readme/USAGE.md b/base_report_manager/readme/USAGE.md new file mode 100644 index 0000000..741ccca --- /dev/null +++ b/base_report_manager/readme/USAGE.md @@ -0,0 +1,5 @@ +Once report restrictions are configured for user groups: + +1. Assign the appropriate groups to the user. +2. The user navigates to the target model view (e.g., Partners). +3. Under the **Print** action menu, any report that is restricted for the user's groups will be hidden. All other reports remain visible. diff --git a/base_report_manager/static/description/index.html b/base_report_manager/static/description/index.html new file mode 100644 index 0000000..269005e --- /dev/null +++ b/base_report_manager/static/description/index.html @@ -0,0 +1,468 @@ + + + + + +Base Report Manager + + + +
+

Base Report Manager

+ + +

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

+

This module provides granular visibility control over PDF and QWeb +Reports based on User Groups:

+
    +
  1. It introduces the ability to configure “Restricted Report Actions” +directly within Odoo Security Groups.
  2. +
  3. It enforces a “Restricted / Deny List” policy, ensuring that users +cannot view or print reports that are restricted for any of their +assigned groups (either directly or transitively inherited).
  4. +
+

Table of contents

+ +
+

Configuration

+

To configure report restrictions:

+
    +
  1. Go to Settings > Users & Companies > Groups and select the Group +you want to configure.
  2. +
  3. Under the Restricted Report Actions tab, select the report +actions you want to hide from members of this group.
  4. +
+

Note: Group privilege inheritance is transitive. If a report action is +restricted for a group, that restriction propagates to all users who +belong to the group, including those who inherit the group (e.g., +restricting a lower-privilege group like “User” will also restrict a +higher-privilege group like “Manager” that implies/inherits it).

+
+
+

Usage

+

Once report restrictions are configured for user groups:

+
    +
  1. Assign the appropriate groups to the user.
  2. +
  3. The user navigates to the target model view (e.g., Partners).
  4. +
  5. Under the Print action menu, any report that is restricted for +the user’s groups will be hidden. All other reports remain visible.
  6. +
+
+
+

Known issues / Roadmap

+
    +
  • None.
  • +
+
+
+

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

+
    +
  • CIT Services
  • +
+
+ +
+

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

+

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

+
+
+
+ + diff --git a/base_report_manager/tests/__init__.py b/base_report_manager/tests/__init__.py new file mode 100644 index 0000000..4c55a93 --- /dev/null +++ b/base_report_manager/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_base_report diff --git a/base_report_manager/tests/test_base_report.py b/base_report_manager/tests/test_base_report.py new file mode 100644 index 0000000..e86d419 --- /dev/null +++ b/base_report_manager/tests/test_base_report.py @@ -0,0 +1,212 @@ +# Copyright 2026 CIT Services +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.exceptions import AccessError +from odoo.tests.common import TransactionCase + + +class TestBaseReport(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + + polluted_columns = [ + ("res_partner", "autopost_bills"), + ("res_users", "notification_type"), + ] + for table, column in polluted_columns: + cls.env.cr.execute(f""" + SELECT column_name + FROM information_schema.columns + WHERE table_name='{table}' AND column_name='{column}' + """) + if cls.env.cr.fetchone(): + cls.env.cr.execute( + f"ALTER TABLE {table} ALTER COLUMN {column} DROP NOT NULL" + ) + + cls.group_user = cls.env.ref("base.group_user") + + cls.child_group = cls.env["res.groups"].create( + { + "name": "User: Own Documents Only", + "implied_ids": [Command.link(cls.group_user.id)], + } + ) + cls.parent_group = cls.env["res.groups"].create( + { + "name": "User: All Documents", + "implied_ids": [Command.link(cls.child_group.id)], + } + ) + + cls.unrelated_group = cls.env["res.groups"].create( + { + "name": "Project / User", + "implied_ids": [Command.link(cls.group_user.id)], + } + ) + + cls.test_user = cls.env["res.users"].create( + { + "name": "Test Report User", + "login": "test_report_user", + "groups_id": [Command.set([cls.env.ref("base.group_system").id])], + } + ) + + cls.model_res_partner = cls.env["ir.model"].search( + [("model", "=", "res.partner")], limit=1 + ) + + cls.report_action = cls.env["ir.actions.report"].create( + { + "name": "Test Report", + "model": "res.partner", + "binding_model_id": cls.model_res_partner.id, + "report_name": "test_report", + "report_type": "qweb-pdf", + } + ) + + def test_restricted_report_inheritance(self): + """Test that restricting a report hides it from bindings.""" + self.child_group.write( + {"restricted_report_action_ids": [Command.link(self.report_action.id)]} + ) + self.test_user.write( + { + "groups_id": [ + Command.set( + [self.parent_group.id, self.env.ref("base.group_system").id] + ) + ] + } + ) + with self.assertRaises(AccessError): + self.report_action.with_user(self.test_user)._get_action_dict() + + bindings = ( + self.env["ir.actions.actions"] + .with_user(self.test_user) + .get_bindings("res.partner") + ) + report_bindings = [r["id"] for r in bindings.get("report", [])] + self.assertNotIn(self.report_action.id, report_bindings) + + self.child_group.write( + {"restricted_report_action_ids": [Command.unlink(self.report_action.id)]} + ) + self.parent_group.write( + {"restricted_report_action_ids": [Command.link(self.report_action.id)]} + ) + with self.assertRaises(AccessError): + self.report_action.with_user(self.test_user)._get_action_dict() + + bindings = ( + self.env["ir.actions.actions"] + .with_user(self.test_user) + .get_bindings("res.partner") + ) + report_bindings = [r["id"] for r in bindings.get("report", [])] + self.assertNotIn(self.report_action.id, report_bindings) + + self.test_user.write( + { + "groups_id": [ + Command.set( + [ + self.child_group.id, + self.unrelated_group.id, + self.env.ref("base.group_system").id, + ] + ) + ] + } + ) + + self.report_action.with_user(self.test_user)._get_action_dict() + + bindings = ( + self.env["ir.actions.actions"] + .with_user(self.test_user) + .get_bindings("res.partner") + ) + report_bindings = [r["id"] for r in bindings.get("report", [])] + self.assertIn(self.report_action.id, report_bindings) + + def test_superuser_bypass(self): + """Superuser bypasses restrictions entirely.""" + self.child_group.write( + {"restricted_report_action_ids": [Command.link(self.report_action.id)]} + ) + root_user = self.env.ref("base.user_root") or self.env.user.browse(1) + self.report_action.with_user(root_user)._check_action_report_restrictions() + self.assertFalse( + self.report_action.with_user(root_user)._is_action_report_restricted() + ) + bindings = ( + self.env["ir.actions.actions"] + .with_user(root_user) + .get_bindings("res.partner") + ) + report_bindings = [r["id"] for r in bindings.get("report", [])] + self.assertIn(self.report_action.id, report_bindings) + + def test_no_restriction(self): + """Test that a report action without any group restriction returns False.""" + self.assertFalse( + self.report_action.with_user(self.test_user)._is_action_report_restricted() + ) + + def test_no_group_overlap(self): + """User has no group overlap with the restricted group.""" + self.child_group.write( + {"restricted_report_action_ids": [Command.link(self.report_action.id)]} + ) + self.test_user.write( + { + "groups_id": [ + Command.set( + [ + self.unrelated_group.id, + self.env.ref("base.group_system").id, + ] + ) + ] + } + ) + self.assertFalse( + self.report_action.with_user(self.test_user)._is_action_report_restricted() + ) + + def test_get_bindings_filtering_pop(self): + """Test that report key is popped when all report bindings are restricted.""" + self.child_group.write( + {"restricted_report_action_ids": [Command.link(self.report_action.id)]} + ) + self.test_user.write( + { + "groups_id": [ + Command.set( + [self.child_group.id, self.env.ref("base.group_system").id] + ) + ] + } + ) + + import unittest.mock as mock + + mock_result = {"report": [{"id": self.report_action.id, "name": "Test Report"}]} + with mock.patch( + "odoo.addons.base.models.ir_actions.IrActions.get_bindings", + return_value=mock_result, + ): + bindings = ( + self.env["ir.actions.actions"] + .with_user(self.test_user) + .get_bindings("res.partner") + ) + self.assertNotIn("report", bindings) diff --git a/base_report_manager/views/res_groups_views.xml b/base_report_manager/views/res_groups_views.xml new file mode 100644 index 0000000..b997f16 --- /dev/null +++ b/base_report_manager/views/res_groups_views.xml @@ -0,0 +1,28 @@ + + + + + res.groups.form.inherit + res.groups + + + + + + + + + + + + + + +