Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions base_user_group_mgmt/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
==========================
Base User Group Management
==========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:e748bc9c4f16b21030d04b2cf7329ca503abe8181583a411ce302521fee08c6b
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |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--backend-lightgray.png?logo=github
:target: https://github.com/OCA/server-backend/tree/16.0/base_user_group_mgmt
:alt: OCA/server-backend
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-backend-16-0/server-backend-16-0-base_user_group_mgmt
: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-backend&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This modules adds a new models which contains the security updates a
user wants to do. Theses security updates are then validated through a
workflow. If the updates are approved, the changes are done one the
users/groups. The request must be approved twice by different users to
be approved. Only users with the manager access can approve all steps.

Lists of actions:

- add a group to a user
- remove a group from a user
- add a group to a group
- remove a group from a group

This module also makes readonly user groups views, inherited groups of a
group, model access and security rules.

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

[ This file is optional but strongly suggested to allow end-users to
evaluate the module's usefulness in their context. ]

BUSINESS NEED: This modules has been created in order to validate what
groups is assigned to a user, or to a group.

USEFUL INFORMATION: This modules can be inherited with
base_user_group_mgmt_role which enables new actions on roles.

Usage
=====

[ This file is required and contains the instructions on **“how”** to
use the module for end-users.

To use this module, you need to:

- Go to *Settings* > Users & companies > Security Update Requests
- Create a new request
- Creates lines and select an action for each line
- Process the workflow to apply each action

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-backend/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 <https://github.com/OCA/server-backend/issues/new?body=module:%20base_user_group_mgmt%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* ACSONE SA/NV

Contributors
------------

- Benjamin Willig [email protected] (https://acsone.eu)

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-backend <https://github.com/OCA/server-backend/tree/16.0/base_user_group_mgmt>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions base_user_group_mgmt/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from .hooks import _post_init_hook
30 changes: 30 additions & 0 deletions base_user_group_mgmt/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Base User Group Management",
"summary": """
This modules allows to have a validation of users and groups update.
Views related to security models become readonly and each update
of user's groups or groups is done after the workflow of a
dedicated model is approved.""",
"version": "16.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-backend",
"depends": [
"base",
"mail",
],
"data": [
"security/res_groups.xml",
"security/base_security_update_request.xml",
"security/base_security_update_request_line.xml",
"views/base_security_update_request_line.xml",
"views/base_security_update_request.xml",
"views/ir_model_access.xml",
"views/ir_rule.xml",
],
"demo": [],
"post_init_hook": "_post_init_hook",
}
18 changes: 18 additions & 0 deletions base_user_group_mgmt/hooks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import SUPERUSER_ID, api, Command


def _post_init_hook(cr, registry):
"""Give group to admin at first install"""
env = api.Environment(cr, SUPERUSER_ID, {})
env.ref("base.user_admin").write(
{
"groups_id": [
Command.link(
env.ref("base_user_group_mgmt.security_management_manager").id
)
]
}
)
4 changes: 4 additions & 0 deletions base_user_group_mgmt/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import base_security_update_request
from . import base_security_update_request_line
from . import res_groups
from . import res_users
225 changes: 225 additions & 0 deletions base_user_group_mgmt/models/base_security_update_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Copyright 2025 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import SUPERUSER_ID, _, api, fields, models
from odoo.exceptions import UserError

READONLY_STATES = {
"draft": [("readonly", False)],
}


class BaseSecurityUpdateRequest(models.Model):

_name = "base.security.update.request"
_description = "Base Security Update"
_inherit = [
"mail.thread",
"mail.activity.mixin",
]
_mail_post_access = "read"
_order = "date desc"

user_id = fields.Many2one(
comodel_name="res.users",
required=True,
default=lambda self: self.env.user.id,
readonly=True,
states=READONLY_STATES,
)
date = fields.Datetime(
default=lambda self: fields.Datetime.now(),
copy=False,
readonly=True,
states=READONLY_STATES,
)
line_ids = fields.One2many(
comodel_name="base.security.update.request.line",
inverse_name="request_id",
string="Lines",
readonly=True,
states=READONLY_STATES,
copy=True,
)
state = fields.Selection(
selection="_selection_state",
default="draft",
required=True,
readonly=True,
states=READONLY_STATES,
copy=False,
tracking=True,
)
approver_1_user_id = fields.Many2one(
comodel_name="res.users",
string="Approver 1",
readonly=True,
copy=False,
)
approver_2_user_id = fields.Many2one(
comodel_name="res.users",
string="Approver 2",
readonly=True,
copy=False,
)
action_confirm_allowed = fields.Boolean(
compute="_compute_action_confirm_allowed",
)
action_first_approval_allowed = fields.Boolean(
compute="_compute_action_first_approval_allowed",
)
action_second_approval_allowed = fields.Boolean(
compute="_compute_action_second_approval_allowed",
)
action_reject_allowed = fields.Boolean(
compute="_compute_action_reject_allowed",
)

def name_get(self):
res = []
for rec in self:
res.append((rec.id, f"#{rec.id}"))
return res

def _compute_action_confirm_allowed(self):
is_super_approver = self._is_user_super_approver()
for rec in self:
rec.action_confirm_allowed = rec.state == "draft" and (
rec._is_user_request_creator() or is_super_approver
)

@api.depends_context("uid")
@api.depends("user_id", "state")
def _compute_action_first_approval_allowed(self):
is_approver = self._is_user_approver()
is_super_approver = self._is_user_super_approver()
for rec in self:
rec.action_first_approval_allowed = rec.state == "first_approval" and (
(is_approver and not rec._is_user_request_creator())
or is_super_approver
)

@api.depends_context("uid")
@api.depends("approver_1_user_id", "user_id", "state")
def _compute_action_second_approval_allowed(self):
current_user = self.env.user
is_approver = self._is_user_approver()
is_manager = self._is_user_manager()
for rec in self:
rec.action_second_approval_allowed = rec.state == "second_approval" and (
(
is_approver
and current_user != rec.approver_1_user_id
and not rec._is_user_request_creator()
)
or is_manager
)

@api.depends(
"action_first_approval_allowed",
"action_second_approval_allowed",
)
def _compute_action_reject_allowed(self):
for rec in self:
rec.action_reject_allowed = (
rec.action_first_approval_allowed or rec.action_second_approval_allowed
)

@api.model
def _selection_state(self):
return [
("draft", _("Draft")),
("first_approval", _("1st Approval")),
("second_approval", _("2nd Approval")),
("approved", _("Approved")),
("rejected", _("Rejected")),
]

@api.model
def _is_user_super_approver(self):
return self.env.user.has_group(
"base_user_group_mgmt.security_management_super_approver"
)

@api.model
def _is_user_approver(self):
return self.env.user.has_group(
"base_user_group_mgmt.security_management_approver"
)

@api.model
def _is_user_manager(self):
return self.env.user.has_group(
"base_user_group_mgmt.security_management_manager"
)

def _is_user_request_creator(self):
self.ensure_one()
return self.env.user == self.user_id

def _check_action_confirm_allowed(self):
for rec in self:
if not rec.action_confirm_allowed:
raise UserError(

Check warning on line 163 in base_user_group_mgmt/models/base_security_update_request.py

View check run for this annotation

Codecov / codecov/patch

base_user_group_mgmt/models/base_security_update_request.py#L163

Added line #L163 was not covered by tests
_("You can't confirm this request. %(name)s", name=rec.display_name)
)

def action_confirm(self):
self.ensure_one()
self._check_action_confirm_allowed()
self.write({"state": "first_approval"})
if self.action_first_approval_allowed:
self.action_first_approval()

def _check_action_first_approval_allowed(self):
for rec in self:
if not rec.action_first_approval_allowed:
raise UserError(
_("You can't approve this request. %(name)s", name=rec.display_name)
)

def action_first_approval(self):
self.ensure_one()
self._check_action_first_approval_allowed()
self.sudo().write(
{
"state": "second_approval",
"approver_1_user_id": self.env.user.id,
}
)

def _check_action_second_approval_allowed(self):
for rec in self:
if not rec.action_second_approval_allowed:
raise UserError(
_("You can't approve this request. %(name)s", name=rec.display_name)
)

def action_second_approval(self):
self.ensure_one()
self._check_action_second_approval_allowed()
self_sudo = self.sudo()
self_sudo.write(
{
"state": "approved",
"approver_2_user_id": self.env.user.id,
}
)
self_sudo.with_user(SUPERUSER_ID).line_ids._do_updates()

def _check_action_reject_allowed(self):
for rec in self:
if not rec.action_reject_allowed:
raise UserError(

Check warning on line 213 in base_user_group_mgmt/models/base_security_update_request.py

View check run for this annotation

Codecov / codecov/patch

base_user_group_mgmt/models/base_security_update_request.py#L213

Added line #L213 was not covered by tests
_("You can't reject this request. %(name)s", name=rec.display_name)
)

def action_reject(self):
self.ensure_one()
self._check_action_reject_allowed()
self.sudo().write(
{
"state": "rejected",
"approver_2_user_id": self.env.user.id,
}
)
Loading