first commit

This commit is contained in:
Suherdy Yacob 2025-10-29 13:50:40 +07:00
commit 01f4ad9971
10 changed files with 701 additions and 0 deletions

32
README.md Normal file
View File

@ -0,0 +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
Only members of the **Settings / Technical (System Administrator)** group can execute the export wizard.

2
__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import wizard

34
__manifest__.py Normal file
View File

@ -0,0 +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,
}

Binary file not shown.

View File

@ -0,0 +1,2 @@
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_user_access_rights_wizard user.access.rights.wizard model_user_access_rights_wizard base.group_system 1 1 1 1

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_user_access_rights_wizard_form" model="ir.ui.view">
<field name="name">user.access.rights.wizard.form</field>
<field name="model">user.access.rights.wizard</field>
<field name="arch" type="xml">
<form string="Export User Access Rights">
<sheet>
<group col="1" class="o_form_full_width">
<separator string="Overview" colspan="1"/>
<div class="o_form_description">
<p><strong>Generate a consolidated Excel workbook of user access rights.</strong></p>
<ul class="o_form_list">
<li>Includes model ACL permissions (Read, Write, Create, Delete) per user.</li>
<li>Captures applicable record rules and their domains.</li>
<li>Download starts automatically once the report is ready.</li>
</ul>
</div>
</group>
<group col="2" modifiers="{'invisible': [('excel_file', '=', False)]}">
<field name="filename" readonly="1" string="Generated File"/>
<field name="excel_file" filename="filename" invisible="1"/>
</group>
</sheet>
<footer>
<button string="Generate" type="object" name="action_generate_report" class="btn-primary"/>
<button string="Close" special="cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="action_user_access_rights_wizard" model="ir.actions.act_window">
<field name="name">Export User Access Rights</field>
<field name="res_model">user.access.rights.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_user_access_rights_export_root"
name="User Access Rights Export"
parent="base.menu_custom"
sequence="50"
action="action_user_access_rights_wizard"
groups="base.group_system"/>
</odoo>

2
wizard/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import user_access_rights_wizard

Binary file not shown.

View File

@ -0,0 +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):
return _("Yes") if value else _("No")