# -*- 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")