From efc79d193a266d762d288a0838e6a2f5e6bf6d59 Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Sat, 6 Dec 2025 19:11:08 +0700 Subject: [PATCH] feat: Implement user access rights export wizard. --- README.md | 62 +- __init__.py | 2 +- __manifest__.py | 66 +- __pycache__/__init__.cpython-310.pyc | Bin 0 -> 224 bytes security/ir.model.access.csv | 2 +- views/user_access_rights_wizard_views.xml | 90 +- wizard/__init__.py | 2 +- wizard/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 250 bytes .../user_access_rights_wizard.cpython-310.pyc | Bin 0 -> 13384 bytes wizard/user_access_rights_wizard.py | 1164 ++++++++--------- 10 files changed, 694 insertions(+), 694 deletions(-) create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100644 wizard/__pycache__/__init__.cpython-310.pyc create mode 100644 wizard/__pycache__/user_access_rights_wizard.cpython-310.pyc diff --git a/README.md b/README.md index 8e0ac48..b1fa7b5 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,32 @@ -# User Access Rights Export - -Generate an Excel workbook that consolidates the access rights of every user in your Odoo 17 instance. - -## Features - -* One-click wizard accessible to system administrators. -* Summary worksheet listing all users with counts of granted ACLs and record rules. -* Summary worksheet listing each security group with user counts, ACL totals, and record rule totals. -* Dedicated worksheet per user including: - * Model access control list (CRUD) permissions derived from `ir.model.access`. - * Record rule visibility and domain definitions from `ir.rule`. -* Dedicated worksheet per security group including: - * Group-specific ACL permissions. - * Record rules that apply to the group. -* Workbook generated entirely in-memory using `xlsxwriter`. - -## Installation - -1. Copy the `user_access_rights_export` directory into your Odoo addons path. -2. Update your addons list and install the module via Apps. -3. Requires the Python package `xlsxwriter` (bundled with standard Odoo installations). - -## Usage - -1. Navigate to **Settings → Technical → User Access Rights Export**. -2. Click **Generate**. The module will produce and download an `.xlsx` file. -3. Open the Excel file to inspect summary metrics and per-user details. - -## Security - +# User Access Rights Export + +Generate an Excel workbook that consolidates the access rights of every user in your Odoo 17 instance. + +## Features + +* One-click wizard accessible to system administrators. +* Summary worksheet listing all users with counts of granted ACLs and record rules. +* Summary worksheet listing each security group with user counts, ACL totals, and record rule totals. +* Dedicated worksheet per user including: + * Model access control list (CRUD) permissions derived from `ir.model.access`. + * Record rule visibility and domain definitions from `ir.rule`. +* Dedicated worksheet per security group including: + * Group-specific ACL permissions. + * Record rules that apply to the group. +* Workbook generated entirely in-memory using `xlsxwriter`. + +## Installation + +1. Copy the `user_access_rights_export` directory into your Odoo addons path. +2. Update your addons list and install the module via Apps. +3. Requires the Python package `xlsxwriter` (bundled with standard Odoo installations). + +## Usage + +1. Navigate to **Settings → Technical → User Access Rights Export**. +2. Click **Generate**. The module will produce and download an `.xlsx` file. +3. Open the Excel file to inspect summary metrics and per-user details. + +## Security + Only members of the **Settings / Technical (System Administrator)** group can execute the export wizard. \ No newline at end of file diff --git a/__init__.py b/__init__.py index 83e278c..e50c250 100644 --- a/__init__.py +++ b/__init__.py @@ -1,2 +1,2 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from . import wizard \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py index d4d885f..71bb0d4 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,34 +1,34 @@ -# -*- coding: utf-8 -*- -{ - "name": "User Access Rights Export", - "version": "17.0.1.0.0", - "category": "Settings/Technical", - "summary": "Export detailed user access rights (model ACLs and record rules) to Excel", - "description": """ -User Access Rights Export -========================= - -Generate an Excel workbook detailing the access rights of all internal users. - -Features --------- -* Summary worksheet with key metrics per user. -* Dedicated worksheet per user including: - - Model access rights (CRUD permissions). - - Record rules with domains and permissions. -* XLSX output generated in-memory via xlsxwriter. -""", - "author": "Suherdy Yacob", - "website": "https://www.example.com", - "license": "LGPL-3", - "depends": [ - "base", - ], - "data": [ - "security/ir.model.access.csv", - "views/user_access_rights_wizard_views.xml", - ], - "installable": True, - "application": False, - "auto_install": False, +# -*- coding: utf-8 -*- +{ + "name": "User Access Rights Export", + "version": "17.0.1.0.0", + "category": "Settings/Technical", + "summary": "Export detailed user access rights (model ACLs and record rules) to Excel", + "description": """ +User Access Rights Export +========================= + +Generate an Excel workbook detailing the access rights of all internal users. + +Features +-------- +* Summary worksheet with key metrics per user. +* Dedicated worksheet per user including: + - Model access rights (CRUD permissions). + - Record rules with domains and permissions. +* XLSX output generated in-memory via xlsxwriter. +""", + "author": "Suherdy Yacob", + "website": "https://www.example.com", + "license": "LGPL-3", + "depends": [ + "base", + ], + "data": [ + "security/ir.model.access.csv", + "views/user_access_rights_wizard_views.xml", + ], + "installable": True, + "application": False, + "auto_install": False, } \ No newline at end of file diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc847f1b8a3f2c54c39dbf0ab1061c9d02b70e80 GIT binary patch literal 224 zcmYjLK?=e!5KO9w2!(#52OB(j5$Ov=@a84NY_Nf*Te7KIpW!e3Qm>x;f+tf?4$KTQ zFff>=3l?#CaJ!8D8pA)57|KY>q5uP4GslmdGe{0BpS*djb3V+j_O3ka@?>Cs+&r0| zM*GPJM1{@>>MG-*FM6f%R65nFrqEbe5c@t<*k}YJ5D-^up*srb6aY3PIx=1=tqqa% h_FM&$=GLMQLQ31%AZ5|^!+hfqy4q~VPc)v$_!l66I`aSk literal 0 HcmV?d00001 diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv index fcf214f..a9584fb 100644 --- a/security/ir.model.access.csv +++ b/security/ir.model.access.csv @@ -1,2 +1,2 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_user_access_rights_wizard,user.access.rights.wizard,model_user_access_rights_wizard,base.group_system,1,1,1,1 \ No newline at end of file diff --git a/views/user_access_rights_wizard_views.xml b/views/user_access_rights_wizard_views.xml index 44360cd..ff92a24 100644 --- a/views/user_access_rights_wizard_views.xml +++ b/views/user_access_rights_wizard_views.xml @@ -1,46 +1,46 @@ - - - - user.access.rights.wizard.form - user.access.rights.wizard - -
- - - -
-

Generate a consolidated Excel workbook of user access rights.

-
    -
  • Includes model ACL permissions (Read, Write, Create, Delete) per user.
  • -
  • Captures applicable record rules and their domains.
  • -
  • Download starts automatically once the report is ready.
  • -
-
-
- - - - -
-
-
-
-
-
- - - Export User Access Rights - user.access.rights.wizard - form - new - - - + + + + user.access.rights.wizard.form + user.access.rights.wizard + +
+ + + +
+

Generate a consolidated Excel workbook of user access rights.

+
    +
  • Includes model ACL permissions (Read, Write, Create, Delete) per user.
  • +
  • Captures applicable record rules and their domains.
  • +
  • Download starts automatically once the report is ready.
  • +
+
+
+ + + + +
+
+
+
+
+
+ + + Export User Access Rights + user.access.rights.wizard + form + new + + +
\ No newline at end of file diff --git a/wizard/__init__.py b/wizard/__init__.py index e10cd50..6b744f2 100644 --- a/wizard/__init__.py +++ b/wizard/__init__.py @@ -1,2 +1,2 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from . import user_access_rights_wizard \ No newline at end of file diff --git a/wizard/__pycache__/__init__.cpython-310.pyc b/wizard/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66c332de6e7c8e3735908167bffa276e97e73982 GIT binary patch literal 250 zcmZuru?oU46iljv2!gAh(51o2MWkOKf}2Z-dBFyozLJ-sb?_tn3xBDrlfU3(s*8gM zcgMZq-Qi?1W)asLw|VqOAAXUgK6k2CRi-hsAU16%u+j)dARx{c qLN^r9DFCcU{OL328hsGoS|O#aY>+am+itXU2u*2L{aWg`O!*t$UPZ?M literal 0 HcmV?d00001 diff --git a/wizard/__pycache__/user_access_rights_wizard.cpython-310.pyc b/wizard/__pycache__/user_access_rights_wizard.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6db837b9b522d6693f0c405d816677cf508ca048 GIT binary patch literal 13384 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 Y82=!Ym*!?cAY}NZ^eKp3O3z3B2Qh`^xc~qF literal 0 HcmV?d00001 diff --git a/wizard/user_access_rights_wizard.py b/wizard/user_access_rights_wizard.py index 8e770a5..4ffabdf 100644 --- a/wizard/user_access_rights_wizard.py +++ b/wizard/user_access_rights_wizard.py @@ -1,583 +1,583 @@ -# -*- coding: utf-8 -*- -import base64 -import io -import re -from datetime import datetime - -from odoo import _, api, fields, models -from odoo.exceptions import UserError -from odoo.tools.misc import xlsxwriter - - -class UserAccessRightsWizard(models.TransientModel): - _name = "user.access.rights.wizard" - _description = "User Access Rights Export Wizard" - - excel_file = fields.Binary(string="Excel File", readonly=True) - filename = fields.Char(string="File Name", readonly=True) - - def action_generate_report(self): - self.ensure_one() - if not xlsxwriter: - raise UserError(_("The python library xlsxwriter is required to export Excel files.")) - - users = self._get_users() - user_data = [] - user_summary_rows = [] - - for user in users: - group_names = ", ".join(user.groups_id.mapped("display_name")) or _("No Groups") - model_access = self._collect_model_access(user) - record_rules = self._collect_record_rules(user) - - user_summary_rows.append({ - "name": user.display_name, - "login": user.login or "", - "groups": group_names, - "active": self._bool_to_str(user.active), - "model_count": len(model_access), - "rule_count": len(record_rules), - }) - - user_data.append({ - "user": user, - "groups": group_names, - "model_access": model_access, - "record_rules": record_rules, - }) - - groups = self._get_groups() - group_data = [] - group_summary_rows = [] - - for group in groups: - model_access = self._collect_group_model_access(group) - record_rules = self._collect_group_record_rules(group) - users_in_group = group.users - - group_summary_rows.append({ - "name": group.display_name, - "technical_name": group.full_name, - "category": group.category_id.display_name or _("Uncategorized"), - "user_count": len(users_in_group), - "model_count": len(model_access), - "rule_count": len(record_rules), - }) - - group_data.append({ - "group": group, - "users": users_in_group, - "model_access": model_access, - "record_rules": record_rules, - }) - - output = io.BytesIO() - workbook = xlsxwriter.Workbook(output, {"in_memory": True}) - formats = self._build_formats(workbook) - used_sheet_names = set() - - try: - user_overview_sheet = self._make_unique_sheet_name(_("Users Overview"), used_sheet_names) - used_sheet_names.add(user_overview_sheet) - self._write_user_summary_sheet(workbook, formats, user_overview_sheet, user_summary_rows) - - if group_summary_rows: - group_overview_sheet = self._make_unique_sheet_name(_("Groups Overview"), used_sheet_names) - used_sheet_names.add(group_overview_sheet) - self._write_group_summary_sheet(workbook, formats, group_overview_sheet, group_summary_rows) - - for data in user_data: - sheet_name = self._make_unique_sheet_name( - data["user"].display_name or _("User"), - used_sheet_names, - ) - used_sheet_names.add(sheet_name) - self._write_user_sheet(workbook, formats, sheet_name, data) - - for data in group_data: - sheet_name = self._make_unique_sheet_name( - data["group"].display_name or _("Group"), - used_sheet_names, - ) - used_sheet_names.add(sheet_name) - self._write_group_sheet(workbook, formats, sheet_name, data) - finally: - workbook.close() - - file_content = output.getvalue() - filename = "user_access_rights_%s.xlsx" % datetime.now().strftime("%Y%m%d_%H%M%S") - - self.write({ - "excel_file": base64.b64encode(file_content), - "filename": filename, - }) - - return { - "type": "ir.actions.act_url", - "url": "/web/content/?model=%s&id=%s&field=excel_file&filename_field=filename&download=true" - % (self._name, self.id), - "target": "self", - } - - def _get_users(self): - return self.env["res.users"].sudo().with_context(active_test=False).search([], order="name") - - def _get_groups(self): - return self.env["res.groups"].sudo().with_context(active_test=False).search([], order="category_id, name") - - @api.model - def _collect_model_access(self, user): - user_groups = user.groups_id - acl_model = self.env["ir.model.access"].sudo().with_context(active_test=False) - acl_records = acl_model.search([], order="model_id, id") - result = [] - - for acl in acl_records: - applies = not acl.group_id or acl.group_id in user_groups - if not applies: - continue - - result.append({ - "model": acl.model_id.model, - "model_name": acl.model_id.name, - "group": acl.group_id.display_name if acl.group_id else _("All Users"), - "perm_read": self._bool_to_str(acl.perm_read), - "perm_write": self._bool_to_str(acl.perm_write), - "perm_create": self._bool_to_str(acl.perm_create), - "perm_unlink": self._bool_to_str(acl.perm_unlink), - }) - - return result - - @api.model - def _collect_group_model_access(self, group): - acl_model = self.env["ir.model.access"].sudo().with_context(active_test=False) - acl_records = acl_model.search([("group_id", "=", group.id)], order="model_id, id") - result = [] - for acl in acl_records: - result.append({ - "model": acl.model_id.model, - "model_name": acl.model_id.name, - "perm_read": self._bool_to_str(acl.perm_read), - "perm_write": self._bool_to_str(acl.perm_write), - "perm_create": self._bool_to_str(acl.perm_create), - "perm_unlink": self._bool_to_str(acl.perm_unlink), - }) - return result - - @api.model - def _collect_record_rules(self, user): - user_groups = user.groups_id - rule_model = self.env["ir.rule"].sudo().with_context(active_test=False) - rules = rule_model.search([], order="model_id, id") - result = [] - - for rule in rules: - is_global = bool(getattr(rule, "global", False)) - applies = is_global or (rule.groups and any(g in user_groups for g in rule.groups)) - if not applies: - continue - - group_names = ", ".join(rule.groups.mapped("display_name")) if rule.groups else _("All Users") - domain = rule.domain_force or "[]" - domain = re.sub(r"\s+", " ", domain).strip() - - result.append({ - "name": rule.name or _("Unnamed Rule"), - "model": rule.model_id.model, - "model_name": rule.model_id.name, - "domain": domain, - "group": group_names, - "global": self._bool_to_str(is_global), - "perm_read": self._bool_to_str(rule.perm_read), - "perm_write": self._bool_to_str(rule.perm_write), - "perm_create": self._bool_to_str(rule.perm_create), - "perm_unlink": self._bool_to_str(rule.perm_unlink), - }) - - return result - - @api.model - def _collect_group_record_rules(self, group): - rule_model = self.env["ir.rule"].sudo().with_context(active_test=False) - rules = rule_model.search([], order="model_id, id") - result = [] - - for rule in rules: - if group not in rule.groups: - continue - - domain = rule.domain_force or "[]" - domain = re.sub(r"\s+", " ", domain).strip() - - result.append({ - "name": rule.name or _("Unnamed Rule"), - "model": rule.model_id.model, - "model_name": rule.model_id.name, - "domain": domain, - "perm_read": self._bool_to_str(rule.perm_read), - "perm_write": self._bool_to_str(rule.perm_write), - "perm_create": self._bool_to_str(rule.perm_create), - "perm_unlink": self._bool_to_str(rule.perm_unlink), - }) - - return result - - @api.model - def _build_formats(self, workbook): - return { - "header": workbook.add_format({ - "bold": True, - "bg_color": "#F2F2F2", - "border": 1, - "text_wrap": True, - }), - "section": workbook.add_format({ - "bold": True, - "font_size": 12, - "bottom": 1, - }), - "text": workbook.add_format({ - "border": 1, - }), - "wrap": workbook.add_format({ - "border": 1, - "text_wrap": True, - }), - "center": workbook.add_format({ - "border": 1, - "align": "center", - }), - "title": workbook.add_format({ - "bold": True, - "font_size": 14, - }), - } - - def _write_user_summary_sheet(self, workbook, formats, sheet_name, rows): - worksheet = workbook.add_worksheet(sheet_name) - headers = [ - _("User"), - _("Login"), - _("Groups"), - _("Active"), - _("Model ACLs"), - _("Record Rules"), - ] - column_widths = [len(header) + 2 for header in headers] - - worksheet.freeze_panes(1, 0) - - for col, header in enumerate(headers): - worksheet.write(0, col, header, formats["header"]) - - for row_idx, data in enumerate(rows, start=1): - values = [ - data["name"], - data["login"], - data["groups"], - data["active"], - data["model_count"], - data["rule_count"], - ] - for col_idx, value in enumerate(values): - fmt = formats["wrap"] if col_idx == 2 else formats["text"] - worksheet.write(row_idx, col_idx, value, fmt) - column_widths[col_idx] = min( - max(column_widths[col_idx], len(str(value)) + 2), - 80, - ) - - for col_idx, width in enumerate(column_widths): - worksheet.set_column(col_idx, col_idx, width) - - def _write_group_summary_sheet(self, workbook, formats, sheet_name, rows): - worksheet = workbook.add_worksheet(sheet_name) - headers = [ - _("Group"), - _("Technical Name"), - _("Category"), - _("Users"), - _("Model ACLs"), - _("Record Rules"), - ] - column_widths = [len(header) + 2 for header in headers] - - worksheet.freeze_panes(1, 0) - - for col, header in enumerate(headers): - worksheet.write(0, col, header, formats["header"]) - - for row_idx, data in enumerate(rows, start=1): - values = [ - data["name"], - data["technical_name"], - data["category"], - data["user_count"], - data["model_count"], - data["rule_count"], - ] - for col_idx, value in enumerate(values): - fmt = formats["text"] - worksheet.write(row_idx, col_idx, value, fmt) - column_widths[col_idx] = min( - max(column_widths[col_idx], len(str(value)) + 2), - 80, - ) - - for col_idx, width in enumerate(column_widths): - worksheet.set_column(col_idx, col_idx, width) - - def _write_user_sheet(self, workbook, formats, sheet_name, data): - worksheet = workbook.add_worksheet(sheet_name) - row = 0 - - user = data["user"] - worksheet.write(row, 0, _("User Access Rights: %s") % (user.display_name,), formats["title"]) - row += 2 - - info_pairs = [ - (_("Name"), user.display_name or ""), - (_("Login"), user.login or ""), - (_("Email"), user.email or ""), - (_("Active"), self._bool_to_str(user.active)), - (_("Groups"), data["groups"]), - ] - - column_widths = [0, 0] - for label, value in info_pairs: - worksheet.write(row, 0, label, formats["header"]) - worksheet.write(row, 1, value, formats["wrap"]) - column_widths[0] = min(max(column_widths[0], len(label) + 2), 40) - column_widths[1] = min(max(column_widths[1], len(value) + 2), 80) - row += 1 - - worksheet.set_column(0, 0, column_widths[0] or 18) - worksheet.set_column(1, 1, column_widths[1] or 50) - - row += 1 - worksheet.write(row, 0, _("Model Access Rights"), formats["section"]) - row += 1 - - headers = [ - _("Model Technical Name"), - _("Model"), - _("Applies To Group"), - _("Read"), - _("Write"), - _("Create"), - _("Delete"), - ] - for col, header in enumerate(headers): - worksheet.write(row, col, header, formats["header"]) - row += 1 - - model_column_widths = [len(header) + 2 for header in headers] - for record in data["model_access"]: - values = [ - record["model"], - record["model_name"], - record["group"], - record["perm_read"], - record["perm_write"], - record["perm_create"], - record["perm_unlink"], - ] - for col_idx, value in enumerate(values): - fmt = formats["wrap"] if col_idx in (1, 2) else formats["center"] if col_idx >= 3 else formats["text"] - worksheet.write(row, col_idx, value, fmt) - model_column_widths[col_idx] = min( - max(model_column_widths[col_idx], len(str(value)) + 2), - 70, - ) - row += 1 - - for col_idx, width in enumerate(model_column_widths): - worksheet.set_column(col_idx, col_idx, width) - - row += 1 - worksheet.write(row, 0, _("Record Rules"), formats["section"]) - row += 1 - - rule_headers = [ - _("Rule Name"), - _("Model Technical Name"), - _("Model"), - _("Domain"), - _("Applies To Group"), - _("Global"), - _("Read"), - _("Write"), - _("Create"), - _("Delete"), - ] - for col, header in enumerate(rule_headers): - worksheet.write(row, col, header, formats["header"]) - row += 1 - - rule_column_widths = [len(header) + 2 for header in rule_headers] - for record in data["record_rules"]: - values = [ - record["name"], - record["model"], - record["model_name"], - record["domain"], - record["group"], - record["global"], - record["perm_read"], - record["perm_write"], - record["perm_create"], - record["perm_unlink"], - ] - for col_idx, value in enumerate(values): - if col_idx in (3, 4): - fmt = formats["wrap"] - elif col_idx >= 5: - fmt = formats["center"] - else: - fmt = formats["text"] - worksheet.write(row, col_idx, value, fmt) - rule_column_widths[col_idx] = min( - max(rule_column_widths[col_idx], len(str(value)) + 2), - 90 if col_idx == 3 else 70, - ) - row += 1 - - for col_idx, width in enumerate(rule_column_widths): - worksheet.set_column(col_idx, col_idx, width) - - worksheet.freeze_panes(4 + len(data["model_access"]), 0) - - def _write_group_sheet(self, workbook, formats, sheet_name, data): - worksheet = workbook.add_worksheet(sheet_name) - row = 0 - - group = data["group"] - worksheet.write(row, 0, _("Group Access Rights: %s") % (group.display_name,), formats["title"]) - row += 2 - - user_names = ", ".join(data["users"].mapped("display_name")) or _("No Users") - - info_pairs = [ - (_("Name"), group.display_name or ""), - (_("Technical Name"), group.full_name or ""), - (_("Category"), group.category_id.display_name or _("Uncategorized")), - (_("Users"), user_names), - ] - - column_widths = [0, 0] - for label, value in info_pairs: - worksheet.write(row, 0, label, formats["header"]) - worksheet.write(row, 1, value, formats["wrap"]) - column_widths[0] = min(max(column_widths[0], len(label) + 2), 40) - column_widths[1] = min(max(column_widths[1], len(value) + 2), 80) - row += 1 - - worksheet.set_column(0, 0, column_widths[0] or 18) - worksheet.set_column(1, 1, column_widths[1] or 50) - - row += 1 - worksheet.write(row, 0, _("Model Access Rights"), formats["section"]) - row += 1 - - headers = [ - _("Model Technical Name"), - _("Model"), - _("Read"), - _("Write"), - _("Create"), - _("Delete"), - ] - for col, header in enumerate(headers): - worksheet.write(row, col, header, formats["header"]) - row += 1 - - model_column_widths = [len(header) + 2 for header in headers] - for record in data["model_access"]: - values = [ - record["model"], - record["model_name"], - record["perm_read"], - record["perm_write"], - record["perm_create"], - record["perm_unlink"], - ] - for col_idx, value in enumerate(values): - fmt = formats["wrap"] if col_idx == 1 else formats["center"] if col_idx >= 2 else formats["text"] - worksheet.write(row, col_idx, value, fmt) - model_column_widths[col_idx] = min( - max(model_column_widths[col_idx], len(str(value)) + 2), - 70, - ) - row += 1 - - for col_idx, width in enumerate(model_column_widths): - worksheet.set_column(col_idx, col_idx, width) - - row += 1 - worksheet.write(row, 0, _("Record Rules"), formats["section"]) - row += 1 - - rule_headers = [ - _("Rule Name"), - _("Model Technical Name"), - _("Model"), - _("Domain"), - _("Read"), - _("Write"), - _("Create"), - _("Delete"), - ] - for col, header in enumerate(rule_headers): - worksheet.write(row, col, header, formats["header"]) - row += 1 - - rule_column_widths = [len(header) + 2 for header in rule_headers] - for record in data["record_rules"]: - values = [ - record["name"], - record["model"], - record["model_name"], - record["domain"], - record["perm_read"], - record["perm_write"], - record["perm_create"], - record["perm_unlink"], - ] - for col_idx, value in enumerate(values): - if col_idx == 3: - fmt = formats["wrap"] - elif col_idx >= 4: - fmt = formats["center"] - else: - fmt = formats["text"] - worksheet.write(row, col_idx, value, fmt) - rule_column_widths[col_idx] = min( - max(rule_column_widths[col_idx], len(str(value)) + 2), - 90 if col_idx == 3 else 70, - ) - row += 1 - - for col_idx, width in enumerate(rule_column_widths): - worksheet.set_column(col_idx, col_idx, width) - - worksheet.freeze_panes(4 + len(data["model_access"]), 0) - - @api.model - def _make_unique_sheet_name(self, base_name, used_names): - sanitized = re.sub(r"[\[\]\*\?:\\/]", "", base_name or _("Sheet")) - sanitized = sanitized.strip() or _("Sheet") - sanitized = sanitized[:31] - candidate = sanitized - index = 2 - - while candidate in used_names: - suffix = f" ({index})" - candidate = (sanitized[:31 - len(suffix)] + suffix) if len(sanitized) + len(suffix) > 31 else sanitized + suffix - index += 1 - - return candidate - - @api.model - def _bool_to_str(self, value): +# -*- coding: utf-8 -*- +import base64 +import io +import re +from datetime import datetime + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools.misc import xlsxwriter + + +class UserAccessRightsWizard(models.TransientModel): + _name = "user.access.rights.wizard" + _description = "User Access Rights Export Wizard" + + excel_file = fields.Binary(string="Excel File", readonly=True) + filename = fields.Char(string="File Name", readonly=True) + + def action_generate_report(self): + self.ensure_one() + if not xlsxwriter: + raise UserError(_("The python library xlsxwriter is required to export Excel files.")) + + users = self._get_users() + user_data = [] + user_summary_rows = [] + + for user in users: + group_names = ", ".join(user.groups_id.mapped("display_name")) or _("No Groups") + model_access = self._collect_model_access(user) + record_rules = self._collect_record_rules(user) + + user_summary_rows.append({ + "name": user.display_name, + "login": user.login or "", + "groups": group_names, + "active": self._bool_to_str(user.active), + "model_count": len(model_access), + "rule_count": len(record_rules), + }) + + user_data.append({ + "user": user, + "groups": group_names, + "model_access": model_access, + "record_rules": record_rules, + }) + + groups = self._get_groups() + group_data = [] + group_summary_rows = [] + + for group in groups: + model_access = self._collect_group_model_access(group) + record_rules = self._collect_group_record_rules(group) + users_in_group = group.users + + group_summary_rows.append({ + "name": group.display_name, + "technical_name": group.full_name, + "category": group.category_id.display_name or _("Uncategorized"), + "user_count": len(users_in_group), + "model_count": len(model_access), + "rule_count": len(record_rules), + }) + + group_data.append({ + "group": group, + "users": users_in_group, + "model_access": model_access, + "record_rules": record_rules, + }) + + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {"in_memory": True}) + formats = self._build_formats(workbook) + used_sheet_names = set() + + try: + user_overview_sheet = self._make_unique_sheet_name(_("Users Overview"), used_sheet_names) + used_sheet_names.add(user_overview_sheet) + self._write_user_summary_sheet(workbook, formats, user_overview_sheet, user_summary_rows) + + if group_summary_rows: + group_overview_sheet = self._make_unique_sheet_name(_("Groups Overview"), used_sheet_names) + used_sheet_names.add(group_overview_sheet) + self._write_group_summary_sheet(workbook, formats, group_overview_sheet, group_summary_rows) + + for data in user_data: + sheet_name = self._make_unique_sheet_name( + data["user"].display_name or _("User"), + used_sheet_names, + ) + used_sheet_names.add(sheet_name) + self._write_user_sheet(workbook, formats, sheet_name, data) + + for data in group_data: + sheet_name = self._make_unique_sheet_name( + data["group"].display_name or _("Group"), + used_sheet_names, + ) + used_sheet_names.add(sheet_name) + self._write_group_sheet(workbook, formats, sheet_name, data) + finally: + workbook.close() + + file_content = output.getvalue() + filename = "user_access_rights_%s.xlsx" % datetime.now().strftime("%Y%m%d_%H%M%S") + + self.write({ + "excel_file": base64.b64encode(file_content), + "filename": filename, + }) + + return { + "type": "ir.actions.act_url", + "url": "/web/content/?model=%s&id=%s&field=excel_file&filename_field=filename&download=true" + % (self._name, self.id), + "target": "self", + } + + def _get_users(self): + return self.env["res.users"].sudo().with_context(active_test=False).search([], order="name") + + def _get_groups(self): + return self.env["res.groups"].sudo().with_context(active_test=False).search([], order="category_id, name") + + @api.model + def _collect_model_access(self, user): + user_groups = user.groups_id + acl_model = self.env["ir.model.access"].sudo().with_context(active_test=False) + acl_records = acl_model.search([], order="model_id, id") + result = [] + + for acl in acl_records: + applies = not acl.group_id or acl.group_id in user_groups + if not applies: + continue + + result.append({ + "model": acl.model_id.model, + "model_name": acl.model_id.name, + "group": acl.group_id.display_name if acl.group_id else _("All Users"), + "perm_read": self._bool_to_str(acl.perm_read), + "perm_write": self._bool_to_str(acl.perm_write), + "perm_create": self._bool_to_str(acl.perm_create), + "perm_unlink": self._bool_to_str(acl.perm_unlink), + }) + + return result + + @api.model + def _collect_group_model_access(self, group): + acl_model = self.env["ir.model.access"].sudo().with_context(active_test=False) + acl_records = acl_model.search([("group_id", "=", group.id)], order="model_id, id") + result = [] + for acl in acl_records: + result.append({ + "model": acl.model_id.model, + "model_name": acl.model_id.name, + "perm_read": self._bool_to_str(acl.perm_read), + "perm_write": self._bool_to_str(acl.perm_write), + "perm_create": self._bool_to_str(acl.perm_create), + "perm_unlink": self._bool_to_str(acl.perm_unlink), + }) + return result + + @api.model + def _collect_record_rules(self, user): + user_groups = user.groups_id + rule_model = self.env["ir.rule"].sudo().with_context(active_test=False) + rules = rule_model.search([], order="model_id, id") + result = [] + + for rule in rules: + is_global = bool(getattr(rule, "global", False)) + applies = is_global or (rule.groups and any(g in user_groups for g in rule.groups)) + if not applies: + continue + + group_names = ", ".join(rule.groups.mapped("display_name")) if rule.groups else _("All Users") + domain = rule.domain_force or "[]" + domain = re.sub(r"\s+", " ", domain).strip() + + result.append({ + "name": rule.name or _("Unnamed Rule"), + "model": rule.model_id.model, + "model_name": rule.model_id.name, + "domain": domain, + "group": group_names, + "global": self._bool_to_str(is_global), + "perm_read": self._bool_to_str(rule.perm_read), + "perm_write": self._bool_to_str(rule.perm_write), + "perm_create": self._bool_to_str(rule.perm_create), + "perm_unlink": self._bool_to_str(rule.perm_unlink), + }) + + return result + + @api.model + def _collect_group_record_rules(self, group): + rule_model = self.env["ir.rule"].sudo().with_context(active_test=False) + rules = rule_model.search([], order="model_id, id") + result = [] + + for rule in rules: + if group not in rule.groups: + continue + + domain = rule.domain_force or "[]" + domain = re.sub(r"\s+", " ", domain).strip() + + result.append({ + "name": rule.name or _("Unnamed Rule"), + "model": rule.model_id.model, + "model_name": rule.model_id.name, + "domain": domain, + "perm_read": self._bool_to_str(rule.perm_read), + "perm_write": self._bool_to_str(rule.perm_write), + "perm_create": self._bool_to_str(rule.perm_create), + "perm_unlink": self._bool_to_str(rule.perm_unlink), + }) + + return result + + @api.model + def _build_formats(self, workbook): + return { + "header": workbook.add_format({ + "bold": True, + "bg_color": "#F2F2F2", + "border": 1, + "text_wrap": True, + }), + "section": workbook.add_format({ + "bold": True, + "font_size": 12, + "bottom": 1, + }), + "text": workbook.add_format({ + "border": 1, + }), + "wrap": workbook.add_format({ + "border": 1, + "text_wrap": True, + }), + "center": workbook.add_format({ + "border": 1, + "align": "center", + }), + "title": workbook.add_format({ + "bold": True, + "font_size": 14, + }), + } + + def _write_user_summary_sheet(self, workbook, formats, sheet_name, rows): + worksheet = workbook.add_worksheet(sheet_name) + headers = [ + _("User"), + _("Login"), + _("Groups"), + _("Active"), + _("Model ACLs"), + _("Record Rules"), + ] + column_widths = [len(header) + 2 for header in headers] + + worksheet.freeze_panes(1, 0) + + for col, header in enumerate(headers): + worksheet.write(0, col, header, formats["header"]) + + for row_idx, data in enumerate(rows, start=1): + values = [ + data["name"], + data["login"], + data["groups"], + data["active"], + data["model_count"], + data["rule_count"], + ] + for col_idx, value in enumerate(values): + fmt = formats["wrap"] if col_idx == 2 else formats["text"] + worksheet.write(row_idx, col_idx, value, fmt) + column_widths[col_idx] = min( + max(column_widths[col_idx], len(str(value)) + 2), + 80, + ) + + for col_idx, width in enumerate(column_widths): + worksheet.set_column(col_idx, col_idx, width) + + def _write_group_summary_sheet(self, workbook, formats, sheet_name, rows): + worksheet = workbook.add_worksheet(sheet_name) + headers = [ + _("Group"), + _("Technical Name"), + _("Category"), + _("Users"), + _("Model ACLs"), + _("Record Rules"), + ] + column_widths = [len(header) + 2 for header in headers] + + worksheet.freeze_panes(1, 0) + + for col, header in enumerate(headers): + worksheet.write(0, col, header, formats["header"]) + + for row_idx, data in enumerate(rows, start=1): + values = [ + data["name"], + data["technical_name"], + data["category"], + data["user_count"], + data["model_count"], + data["rule_count"], + ] + for col_idx, value in enumerate(values): + fmt = formats["text"] + worksheet.write(row_idx, col_idx, value, fmt) + column_widths[col_idx] = min( + max(column_widths[col_idx], len(str(value)) + 2), + 80, + ) + + for col_idx, width in enumerate(column_widths): + worksheet.set_column(col_idx, col_idx, width) + + def _write_user_sheet(self, workbook, formats, sheet_name, data): + worksheet = workbook.add_worksheet(sheet_name) + row = 0 + + user = data["user"] + worksheet.write(row, 0, _("User Access Rights: %s") % (user.display_name,), formats["title"]) + row += 2 + + info_pairs = [ + (_("Name"), user.display_name or ""), + (_("Login"), user.login or ""), + (_("Email"), user.email or ""), + (_("Active"), self._bool_to_str(user.active)), + (_("Groups"), data["groups"]), + ] + + column_widths = [0, 0] + for label, value in info_pairs: + worksheet.write(row, 0, label, formats["header"]) + worksheet.write(row, 1, value, formats["wrap"]) + column_widths[0] = min(max(column_widths[0], len(label) + 2), 40) + column_widths[1] = min(max(column_widths[1], len(value) + 2), 80) + row += 1 + + worksheet.set_column(0, 0, column_widths[0] or 18) + worksheet.set_column(1, 1, column_widths[1] or 50) + + row += 1 + worksheet.write(row, 0, _("Model Access Rights"), formats["section"]) + row += 1 + + headers = [ + _("Model Technical Name"), + _("Model"), + _("Applies To Group"), + _("Read"), + _("Write"), + _("Create"), + _("Delete"), + ] + for col, header in enumerate(headers): + worksheet.write(row, col, header, formats["header"]) + row += 1 + + model_column_widths = [len(header) + 2 for header in headers] + for record in data["model_access"]: + values = [ + record["model"], + record["model_name"], + record["group"], + record["perm_read"], + record["perm_write"], + record["perm_create"], + record["perm_unlink"], + ] + for col_idx, value in enumerate(values): + fmt = formats["wrap"] if col_idx in (1, 2) else formats["center"] if col_idx >= 3 else formats["text"] + worksheet.write(row, col_idx, value, fmt) + model_column_widths[col_idx] = min( + max(model_column_widths[col_idx], len(str(value)) + 2), + 70, + ) + row += 1 + + for col_idx, width in enumerate(model_column_widths): + worksheet.set_column(col_idx, col_idx, width) + + row += 1 + worksheet.write(row, 0, _("Record Rules"), formats["section"]) + row += 1 + + rule_headers = [ + _("Rule Name"), + _("Model Technical Name"), + _("Model"), + _("Domain"), + _("Applies To Group"), + _("Global"), + _("Read"), + _("Write"), + _("Create"), + _("Delete"), + ] + for col, header in enumerate(rule_headers): + worksheet.write(row, col, header, formats["header"]) + row += 1 + + rule_column_widths = [len(header) + 2 for header in rule_headers] + for record in data["record_rules"]: + values = [ + record["name"], + record["model"], + record["model_name"], + record["domain"], + record["group"], + record["global"], + record["perm_read"], + record["perm_write"], + record["perm_create"], + record["perm_unlink"], + ] + for col_idx, value in enumerate(values): + if col_idx in (3, 4): + fmt = formats["wrap"] + elif col_idx >= 5: + fmt = formats["center"] + else: + fmt = formats["text"] + worksheet.write(row, col_idx, value, fmt) + rule_column_widths[col_idx] = min( + max(rule_column_widths[col_idx], len(str(value)) + 2), + 90 if col_idx == 3 else 70, + ) + row += 1 + + for col_idx, width in enumerate(rule_column_widths): + worksheet.set_column(col_idx, col_idx, width) + + worksheet.freeze_panes(4 + len(data["model_access"]), 0) + + def _write_group_sheet(self, workbook, formats, sheet_name, data): + worksheet = workbook.add_worksheet(sheet_name) + row = 0 + + group = data["group"] + worksheet.write(row, 0, _("Group Access Rights: %s") % (group.display_name,), formats["title"]) + row += 2 + + user_names = ", ".join(data["users"].mapped("display_name")) or _("No Users") + + info_pairs = [ + (_("Name"), group.display_name or ""), + (_("Technical Name"), group.full_name or ""), + (_("Category"), group.category_id.display_name or _("Uncategorized")), + (_("Users"), user_names), + ] + + column_widths = [0, 0] + for label, value in info_pairs: + worksheet.write(row, 0, label, formats["header"]) + worksheet.write(row, 1, value, formats["wrap"]) + column_widths[0] = min(max(column_widths[0], len(label) + 2), 40) + column_widths[1] = min(max(column_widths[1], len(value) + 2), 80) + row += 1 + + worksheet.set_column(0, 0, column_widths[0] or 18) + worksheet.set_column(1, 1, column_widths[1] or 50) + + row += 1 + worksheet.write(row, 0, _("Model Access Rights"), formats["section"]) + row += 1 + + headers = [ + _("Model Technical Name"), + _("Model"), + _("Read"), + _("Write"), + _("Create"), + _("Delete"), + ] + for col, header in enumerate(headers): + worksheet.write(row, col, header, formats["header"]) + row += 1 + + model_column_widths = [len(header) + 2 for header in headers] + for record in data["model_access"]: + values = [ + record["model"], + record["model_name"], + record["perm_read"], + record["perm_write"], + record["perm_create"], + record["perm_unlink"], + ] + for col_idx, value in enumerate(values): + fmt = formats["wrap"] if col_idx == 1 else formats["center"] if col_idx >= 2 else formats["text"] + worksheet.write(row, col_idx, value, fmt) + model_column_widths[col_idx] = min( + max(model_column_widths[col_idx], len(str(value)) + 2), + 70, + ) + row += 1 + + for col_idx, width in enumerate(model_column_widths): + worksheet.set_column(col_idx, col_idx, width) + + row += 1 + worksheet.write(row, 0, _("Record Rules"), formats["section"]) + row += 1 + + rule_headers = [ + _("Rule Name"), + _("Model Technical Name"), + _("Model"), + _("Domain"), + _("Read"), + _("Write"), + _("Create"), + _("Delete"), + ] + for col, header in enumerate(rule_headers): + worksheet.write(row, col, header, formats["header"]) + row += 1 + + rule_column_widths = [len(header) + 2 for header in rule_headers] + for record in data["record_rules"]: + values = [ + record["name"], + record["model"], + record["model_name"], + record["domain"], + record["perm_read"], + record["perm_write"], + record["perm_create"], + record["perm_unlink"], + ] + for col_idx, value in enumerate(values): + if col_idx == 3: + fmt = formats["wrap"] + elif col_idx >= 4: + fmt = formats["center"] + else: + fmt = formats["text"] + worksheet.write(row, col_idx, value, fmt) + rule_column_widths[col_idx] = min( + max(rule_column_widths[col_idx], len(str(value)) + 2), + 90 if col_idx == 3 else 70, + ) + row += 1 + + for col_idx, width in enumerate(rule_column_widths): + worksheet.set_column(col_idx, col_idx, width) + + worksheet.freeze_panes(4 + len(data["model_access"]), 0) + + @api.model + def _make_unique_sheet_name(self, base_name, used_names): + sanitized = re.sub(r"[\[\]\*\?:\\/]", "", base_name or _("Sheet")) + sanitized = sanitized.strip() or _("Sheet") + sanitized = sanitized[:31] + candidate = sanitized + index = 2 + + while candidate in used_names: + suffix = f" ({index})" + candidate = (sanitized[:31 - len(suffix)] + suffix) if len(sanitized) + len(suffix) > 31 else sanitized + suffix + index += 1 + + return candidate + + @api.model + def _bool_to_str(self, value): return _("Yes") if value else _("No") \ No newline at end of file