mirror of
https://github.com/OCA/server-backend.git
synced 2025-02-18 09:52:42 +02:00
[ADD][12.0] base_user_role_profile: Add to 12.0
fixup! Logic and permissions fixes, new demo module, changes JS-side that reloads in a cleaner way on profile change fixup! removed unused imports, beautified JS fixup! Test coverage increase [FIX] Use write instead of assignment operator on create function: assignment on multiple records raises error fixup! Removed leftover copyright Apply suggestions from code review Co-Authored-By: David Beal <david.beal@akretion.com>
This commit is contained in:
committed by
Sébastien BEAU
parent
efe0a694df
commit
0422b83c31
@@ -43,7 +43,7 @@ class ResUsers(models.Model):
|
||||
{},
|
||||
new_role_line_values_by_user
|
||||
)
|
||||
res.last_role_line_modification = fields.Datetime.now()
|
||||
res.write({'last_role_line_modification': fields.Datetime.now()})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
|
||||
73
base_user_role_profile/README.rst
Normal file
73
base_user_role_profile/README.rst
Normal file
@@ -0,0 +1,73 @@
|
||||
=============
|
||||
User profiles
|
||||
=============
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/12.0/base_user_role_profile
|
||||
:alt: oca/server-backend
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
Extending the base_user_role module, this one adds the notion of profiles. Effectively profiles act as an additional filter to how the roles are used. Through the new widget, much in the same way that a user can switch companies when they are part of the multi company group, users have the possibility to change profiles when they are part of the multi profiles group.
|
||||
|
||||
This allows users to switch their permission groups dynamically. This can be useful for example to:
|
||||
- finer grain control on menu and model permissions (with record rules this becomes very flexible)
|
||||
- break down complicated menus into simpler ones
|
||||
- easily restrict users accidentally editing or creating records in O2M fields and in general misusing the interface, instead of excessively explaining things to them
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Go to Configuration / Users / Profiles and create a profile. Go to Configuration / Users / Roles and define some role lines with profiles.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Once you have set up at least one profile for a user, use the widget in the top bar to switch user profiles. Note that it is possible to use no profile; in this case, the user will only get the roles that always apply (i.e the ones with no profile_id).
|
||||
|
||||
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 smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/oca/server-backend/issues/new?body=module:%20base_user_role_profile%0Aversion:%2012.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
|
||||
~~~~~~~
|
||||
|
||||
* Akretion
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Kevin Khao <kevin.khao@akretion.com>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is part of the `oca/server-backend <https://github.com/oca/server-backend/tree/12.0/base_user_role_profile>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute.
|
||||
1
base_user_role_profile/__init__.py
Normal file
1
base_user_role_profile/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
21
base_user_role_profile/__manifest__.py
Normal file
21
base_user_role_profile/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "User profiles",
|
||||
"version": "12.0.1.0.0",
|
||||
"category": "Tools",
|
||||
"author": "Akretion, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"website": "https://github.com/OCA/server-backend",
|
||||
"depends": ["base_user_role", "web"],
|
||||
"data": [
|
||||
"data/data.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/user.xml",
|
||||
"views/role.xml",
|
||||
"views/profile.xml",
|
||||
"views/assets.xml",
|
||||
],
|
||||
"qweb": ["static/src/xml/templates.xml"],
|
||||
"installable": True,
|
||||
}
|
||||
7
base_user_role_profile/data/data.xml
Normal file
7
base_user_role_profile/data/data.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<record id="default_profile" model="res.users.profile">
|
||||
<field name="name">No profile</field>
|
||||
</record>
|
||||
</odoo>
|
||||
4
base_user_role_profile/models/__init__.py
Normal file
4
base_user_role_profile/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import profile
|
||||
from . import user
|
||||
from . import role
|
||||
from . import ir_http
|
||||
26
base_user_role_profile/models/ir_http.py
Normal file
26
base_user_role_profile/models/ir_http.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class Http(models.AbstractModel):
|
||||
_inherit = "ir.http"
|
||||
|
||||
def session_info(self): # pragma: no cover
|
||||
result = super().session_info()
|
||||
user = request.env.user
|
||||
allowed_profiles = [
|
||||
(profile.id, profile.name) for profile in user.profile_ids
|
||||
]
|
||||
if len(allowed_profiles) > 1:
|
||||
current_profile = (user.profile_id.id, user.profile_id.name)
|
||||
result["user_profiles"] = {
|
||||
"current_profile": current_profile,
|
||||
"allowed_profiles": allowed_profiles,
|
||||
}
|
||||
else:
|
||||
result["user_profiles"] = False
|
||||
result["profile_id"] = (
|
||||
user.profile_id.id if request.session.uid else None
|
||||
)
|
||||
return result
|
||||
19
base_user_role_profile/models/profile.py
Normal file
19
base_user_role_profile/models/profile.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersProfile(models.Model):
|
||||
_name = "res.users.profile"
|
||||
_description = "Role profile"
|
||||
|
||||
name = fields.Char("Name")
|
||||
user_ids = fields.Many2many(
|
||||
"res.users", string="Allowed users", compute="_compute_user_ids"
|
||||
)
|
||||
role_ids = fields.One2many("res.users.role", "profile_id", string="Roles")
|
||||
|
||||
def _compute_user_ids(self):
|
||||
for rec in self:
|
||||
rec.user_ids = self.env["res.users"].search(
|
||||
[("profile_ids", "in", rec.ids)]
|
||||
)
|
||||
14
base_user_role_profile/models/role.py
Normal file
14
base_user_role_profile/models/role.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersRole(models.Model):
|
||||
_inherit = "res.users.role"
|
||||
|
||||
profile_id = fields.Many2one("res.users.profile", "Profile",)
|
||||
|
||||
|
||||
class ResUsersRoleLine(models.Model):
|
||||
_inherit = "res.users.role.line"
|
||||
|
||||
profile_id = fields.Many2one(related="role_id.profile_id")
|
||||
81
base_user_role_profile/models/user.py
Normal file
81
base_user_role_profile/models/user.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
def _get_default_profile(self):
|
||||
return self.env.ref("base_user_role_profile.default_profile")
|
||||
|
||||
profile_id = fields.Many2one(
|
||||
"res.users.profile",
|
||||
"Current profile",
|
||||
default=lambda self: self._get_default_profile,
|
||||
)
|
||||
|
||||
profile_ids = fields.Many2many(
|
||||
"res.users.profile", string="Currently allowed profiles",
|
||||
)
|
||||
|
||||
def _get_action_root_menu(self):
|
||||
# used JS-side. Reload the client; open the first available root menu
|
||||
menu = self.env["ir.ui.menu"].search([("parent_id", "=", False)])[:1]
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "reload",
|
||||
"params": {"menu_id": menu.id},
|
||||
}
|
||||
|
||||
def action_profile_change(self, vals):
|
||||
self.write(vals)
|
||||
return self._get_action_root_menu()
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
new_record = super().create(vals)
|
||||
if vals.get("company_id") or vals.get("role_line_ids"):
|
||||
new_record.sudo()._compute_profile_ids()
|
||||
return new_record
|
||||
|
||||
def write(self, vals):
|
||||
# inspired by base/models/res_users.py l. 491
|
||||
if self == self.env.user and vals.get("profile_id"):
|
||||
self.sudo().write({"profile_id": vals["profile_id"]})
|
||||
del vals["profile_id"]
|
||||
res = super().write(vals)
|
||||
if (
|
||||
vals.get("company_id")
|
||||
or vals.get("profile_id")
|
||||
or vals.get("role_line_ids")
|
||||
):
|
||||
self.sudo()._compute_profile_ids()
|
||||
return res
|
||||
|
||||
def _get_applicable_roles(self):
|
||||
res = super()._get_applicable_roles()
|
||||
res = res.filtered(
|
||||
lambda r: not r.profile_id
|
||||
or (r.profile_id.id == r.user_id.profile_id.id)
|
||||
)
|
||||
return res
|
||||
|
||||
def _update_profile_id(self):
|
||||
default_profile = self.env.ref(
|
||||
"base_user_role_profile.default_profile"
|
||||
)
|
||||
if not self.profile_ids:
|
||||
if self.profile_id != default_profile:
|
||||
self.profile_id = default_profile
|
||||
elif self.profile_id not in self.profile_ids:
|
||||
self.write({"profile_id": self.profile_ids[0].id})
|
||||
|
||||
def _compute_profile_ids(self):
|
||||
for rec in self:
|
||||
role_lines = rec.role_line_ids
|
||||
profiles = role_lines.filtered(
|
||||
lambda r: r.company_id == rec.company_id
|
||||
).mapped("profile_id")
|
||||
rec.profile_ids = profiles
|
||||
# set defaults in case applicable profile changes
|
||||
rec._update_profile_id()
|
||||
2
base_user_role_profile/readme/CONFIGURE.rst
Normal file
2
base_user_role_profile/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Go to Configuration / Users / Profiles and create a profile. Go to Configuration / Users / Roles and define some role lines with profiles.
|
||||
Be careful when defining role lines that company ids and profiles are correctly configured.
|
||||
2
base_user_role_profile/readme/CONTRIBUTORS.rst
Normal file
2
base_user_role_profile/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
* Kevin Khao <kevin.khao@akretion.com>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
12
base_user_role_profile/readme/DESCRIPTION.rst
Normal file
12
base_user_role_profile/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
Extending the base_user_role module, this one adds the notion of profiles. Effectively profiles act as an additional filter to how the roles are used. Through the new widget, much in the same way that a user can switch companies when they are part of the multi company group, users have the possibility to change profiles when they are part of the multi profiles group.
|
||||
|
||||
This allows users to switch their permission groups dynamically. This can be useful for example to:
|
||||
- finer grain control on menu and model permissions (with record rules this becomes very flexible)
|
||||
- break down complicated menus into simpler ones
|
||||
- easily restrict users accidentally editing or creating records in O2M fields and in general misusing the interface, instead of excessively explaining things to them
|
||||
|
||||
When you define a role, you have the possibility to link it to a profile. Roles are applied to users in the following way:
|
||||
- Apply user's roles without profiles in any case
|
||||
- Apply user's roles that are linked to the currently selected profile
|
||||
|
||||
Note that this module assumes a multicompany environment
|
||||
1
base_user_role_profile/readme/USAGE.rst
Normal file
1
base_user_role_profile/readme/USAGE.rst
Normal file
@@ -0,0 +1 @@
|
||||
Once you have set up at least one profile for a user, use the widget in the top bar to switch user profiles. Note that it is possible to use no profile; in this case, the user will only get the roles that always apply (i.e the ones with no profile_id).
|
||||
3
base_user_role_profile/security/ir.model.access.csv
Normal file
3
base_user_role_profile/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_res_users_profile,access_res_users_profile,model_res_users_profile,base.group_user,1,0,0,0
|
||||
access_res_users_role_line_users,access_res_users_role_line,model_res_users_role_line,base.group_user,1,0,0,0
|
||||
|
BIN
base_user_role_profile/static/description/icon.png
Normal file
BIN
base_user_role_profile/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
434
base_user_role_profile/static/description/index.html
Normal file
434
base_user_role_profile/static/description/index.html
Normal file
@@ -0,0 +1,434 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
||||
<title>User profiles</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="user-profiles">
|
||||
<h1 class="title">User profiles</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/oca/server-backend/tree/12.0/base_user_role_profile"><img alt="oca/server-backend" src="https://img.shields.io/badge/github-oca%2Fserver--backend-lightgray.png?logo=github" /></a></p>
|
||||
<p>Extending the base_user_role module, this one adds the notion of profiles. Effectively profiles act as an additional filter to how the roles are used. Through the new widget, much in the same way that a user can switch companies when they are part of the multi company group, users have the possibility to change profiles when they are part of the multi profiles group.</p>
|
||||
<dl class="docutils">
|
||||
<dt>This allows users to switch their permission groups dynamically. This can be useful for example to:</dt>
|
||||
<dd><ul class="first last simple">
|
||||
<li>finer grain control on menu and model permissions (with record rules this becomes very flexible)</li>
|
||||
<li>break down complicated menus into simpler ones</li>
|
||||
<li>easily restrict users accidentally editing or creating records in O2M fields and in general misusing the interface, instead of excessively explaining things to them</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
|
||||
<p>Go to Configuration / Users / Profiles and create a profile. Go to Configuration / Users / Roles and define some role lines with profiles.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
|
||||
<p>Once you have set up at least one profile for a user, use the widget in the top bar to switch user profiles. Note that it is possible to use no profile; in this case, the user will only get the roles that always apply (i.e the ones with no profile_id).</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/oca/server-backend/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/oca/server-backend/issues/new?body=module:%20base_user_role_profile%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Akretion</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Kevin Khao <<a class="reference external" href="mailto:kevin.khao@akretion.com">kevin.khao@akretion.com</a>></li>
|
||||
<li>Sébastien Beau <<a class="reference external" href="mailto:sebastien.beau@akretion.com">sebastien.beau@akretion.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/oca/server-backend/tree/12.0/base_user_role_profile">oca/server-backend</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
79
base_user_role_profile/static/src/js/switch_profile_menu.js
Normal file
79
base_user_role_profile/static/src/js/switch_profile_menu.js
Normal file
@@ -0,0 +1,79 @@
|
||||
odoo.define('web.SwitchProfileMenu', function(require) {
|
||||
"use strict";
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var SystrayMenu = require('web.SystrayMenu');
|
||||
var Widget = require('web.Widget')
|
||||
var _t = core._t;
|
||||
|
||||
var SwitchProfileMenu = Widget.extend({
|
||||
template: 'SwitchProfileMenu',
|
||||
events: {
|
||||
'click .dropdown-item[data-menu]': '_onClick',
|
||||
},
|
||||
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.isMobile = config.device.isMobile;
|
||||
this._onClick = _.debounce(this._onClick, 1500, true);
|
||||
},
|
||||
|
||||
willStart: function() {
|
||||
return session.user_profiles ? this._super() : $.Deferred().reject();
|
||||
},
|
||||
|
||||
start: function() {
|
||||
var profilesList = '';
|
||||
if (this.isMobile) {
|
||||
profilesList = '<li class="bg-info">' +
|
||||
_t('Tap on the list to change profile') + '</li>';
|
||||
} else {
|
||||
this.$('.oe_topbar_name').text(session.user_profiles.current_profile[1]);
|
||||
}
|
||||
_.each(session.user_profiles.allowed_profiles, function(profile) {
|
||||
var a = '';
|
||||
if (profile[0] == session.user_profiles.current_profile[0]) {
|
||||
a = '<i class="fa fa-check mr8"></i>';
|
||||
} else {
|
||||
a = '<span style="margin-right: 24px;"/>';
|
||||
}
|
||||
profilesList += '<a role="menuitem" href="#" class="dropdown-item" data-menu="profile" data-profile-id="' +
|
||||
profile[0] + '">' + a + profile[1] + '</a>';
|
||||
});
|
||||
this.$('.dropdown-menu').html(profilesList);
|
||||
return this._super();
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
var self = this;
|
||||
ev.preventDefault();
|
||||
var profileID = $(ev.currentTarget).data('profile-id');
|
||||
// We use this instead of the location.reload() because permissions change
|
||||
// and we might land on a menu that we don't have permissions for. Thus it
|
||||
// is cleaner to reload any root menu
|
||||
this._rpc({
|
||||
model: 'res.users',
|
||||
method: 'action_profile_change',
|
||||
args: [
|
||||
[session.uid], {
|
||||
'profile_id': profileID
|
||||
}
|
||||
],
|
||||
})
|
||||
.done(
|
||||
function(result) {
|
||||
self.trigger_up('do_action', {
|
||||
action: result,
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
});
|
||||
|
||||
SystrayMenu.Items.push(SwitchProfileMenu);
|
||||
|
||||
return SwitchProfileMenu;
|
||||
|
||||
});
|
||||
13
base_user_role_profile/static/src/xml/templates.xml
Normal file
13
base_user_role_profile/static/src/xml/templates.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SwitchProfileMenu">
|
||||
<li class="o_switch_profile_menu">
|
||||
<a role="button" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||
<span t-attf-class="#{widget.isMobile ? 'fa fa-building-o' : 'oe_topbar_name'}"/>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right" role="menu"/>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
1
base_user_role_profile/tests/__init__.py
Normal file
1
base_user_role_profile/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_user_role
|
||||
161
base_user_role_profile/tests/test_user_role.py
Normal file
161
base_user_role_profile/tests/test_user_role.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestUserProfile(TransactionCase):
|
||||
def _helper_unpack_groups_role(self, role):
|
||||
role_group_ids = role.trans_implied_ids.ids
|
||||
role_group_ids.append(role.group_id.id)
|
||||
return sorted(set(role_group_ids))
|
||||
|
||||
def _helper_unpack_groups_group(self, group):
|
||||
group_ids = group.trans_implied_ids.ids
|
||||
group_ids.append(group.id)
|
||||
return sorted(set(group_ids))
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user_model = self.env["res.users"]
|
||||
self.role_model = self.env["res.users.role"]
|
||||
|
||||
self.company1 = self.env.ref("base.main_company")
|
||||
self.company2 = self.env["res.company"].create({"name": "company2"})
|
||||
|
||||
self.default_user = self.env.ref("base.default_user")
|
||||
user_vals = {
|
||||
"name": "USER TEST (ROLES)",
|
||||
"login": "user_test_roles",
|
||||
"company_ids": [(6, 0, [self.company1.id, self.company2.id])],
|
||||
"company_id": self.company1.id,
|
||||
}
|
||||
self.user_id = self.user_model.create(user_vals)
|
||||
|
||||
self.profile1_id = self.env["res.users.profile"].create(
|
||||
{"name": "profile1"}
|
||||
)
|
||||
self.profile2_id = self.env["res.users.profile"].create(
|
||||
{"name": "profile2"}
|
||||
)
|
||||
|
||||
# role 1
|
||||
self.group_user_id = self.env.ref("base.group_user")
|
||||
self.group_no_one_id = self.env.ref("base.group_no_one")
|
||||
|
||||
# role 2
|
||||
self.group_system_id = self.env.ref("base.group_system")
|
||||
self.group_multi_company_id = self.env.ref("base.group_multi_company")
|
||||
|
||||
# role 3
|
||||
self.group_erp_manager_id = self.env.ref("base.group_erp_manager")
|
||||
self.group_partner_manager_id = self.env.ref(
|
||||
"base.group_partner_manager"
|
||||
)
|
||||
|
||||
# roles 1 and 2 have a profile, role 3 no profile
|
||||
vals = {
|
||||
"name": "ROLE_1",
|
||||
"implied_ids": [
|
||||
(6, 0, [self.group_user_id.id, self.group_no_one_id.id])
|
||||
],
|
||||
"profile_id": self.profile1_id.id,
|
||||
}
|
||||
self.role1_id = self.role_model.create(vals)
|
||||
self.role1_group_ids = self._helper_unpack_groups_role(self.role1_id)
|
||||
|
||||
vals = {
|
||||
"name": "ROLE_2",
|
||||
"implied_ids": [
|
||||
(
|
||||
6,
|
||||
0,
|
||||
[self.group_system_id.id, self.group_multi_company_id.id],
|
||||
)
|
||||
],
|
||||
"profile_id": self.profile2_id.id,
|
||||
}
|
||||
self.role2_id = self.role_model.create(vals)
|
||||
self.role2_group_ids = self._helper_unpack_groups_role(self.role2_id)
|
||||
|
||||
vals = {
|
||||
"name": "ROLE_3",
|
||||
"implied_ids": [
|
||||
(
|
||||
6,
|
||||
0,
|
||||
[
|
||||
self.group_erp_manager_id.id,
|
||||
self.group_partner_manager_id.id,
|
||||
],
|
||||
)
|
||||
],
|
||||
}
|
||||
self.role3_id = self.role_model.create(vals)
|
||||
self.role3_group_ids = self._helper_unpack_groups_role(self.role3_id)
|
||||
|
||||
def test_filter_by_profile(self):
|
||||
line1_vals = {"role_id": self.role1_id.id, "user_id": self.user_id.id}
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line1_vals)]})
|
||||
line2_vals = {"role_id": self.role2_id.id, "user_id": self.user_id.id}
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line2_vals)]})
|
||||
self.assertEqual(
|
||||
self.user_id.profile_ids, self.profile1_id + self.profile2_id
|
||||
)
|
||||
self.assertEqual(self.user_id.profile_id, self.profile1_id)
|
||||
self.user_id.action_profile_change({"profile_id": self.profile1_id.id})
|
||||
|
||||
user_group_ids = sorted(
|
||||
set([group.id for group in self.user_id.groups_id])
|
||||
)
|
||||
expected_group_ids = sorted(set(self.role1_group_ids))
|
||||
self.assertEqual(user_group_ids, expected_group_ids)
|
||||
|
||||
self.user_id.action_profile_change({"profile_id": self.profile2_id.id})
|
||||
|
||||
user_group_ids = sorted(
|
||||
set([group.id for group in self.user_id.groups_id])
|
||||
)
|
||||
expected_group_ids = sorted(set(self.role2_group_ids))
|
||||
self.assertEqual(user_group_ids, expected_group_ids)
|
||||
|
||||
def test_allow_by_noprofile(self):
|
||||
line1_vals = {"role_id": self.role1_id.id, "user_id": self.user_id.id}
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line1_vals)]})
|
||||
line2_vals = {"role_id": self.role3_id.id, "user_id": self.user_id.id}
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line2_vals)]})
|
||||
self.assertEqual(self.user_id.profile_ids, self.profile1_id)
|
||||
user_group_ids = []
|
||||
for group in self.user_id.groups_id:
|
||||
user_group_ids += self._helper_unpack_groups_group(group)
|
||||
user_group_ids = set(user_group_ids)
|
||||
expected_groups = set(self.role1_group_ids + self.role3_group_ids)
|
||||
self.assertEqual(user_group_ids, expected_groups)
|
||||
|
||||
def test_sync_profile_change_company(self):
|
||||
line1_vals = {
|
||||
"role_id": self.role1_id.id,
|
||||
"user_id": self.user_id.id,
|
||||
"company_id": self.company1.id,
|
||||
}
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line1_vals)]})
|
||||
line2_vals = {
|
||||
"role_id": self.role2_id.id,
|
||||
"user_id": self.user_id.id,
|
||||
"company_id": self.company2.id,
|
||||
}
|
||||
|
||||
self.user_id.write({"role_line_ids": [(0, 0, line2_vals)]})
|
||||
self.assertEqual(self.user_id.profile_ids, self.profile1_id)
|
||||
|
||||
user_group_ids = sorted(
|
||||
set([group.id for group in self.user_id.groups_id])
|
||||
)
|
||||
expected_group_ids = sorted(set(self.role1_group_ids))
|
||||
self.assertEqual(user_group_ids, expected_group_ids)
|
||||
|
||||
self.user_id.company_id = self.company2
|
||||
self.assertEqual(self.user_id.profile_ids, self.profile2_id)
|
||||
user_group_ids = sorted(
|
||||
set([group.id for group in self.user_id.groups_id])
|
||||
)
|
||||
expected_group_ids = sorted(set(self.role2_group_ids))
|
||||
self.assertEqual(user_group_ids, expected_group_ids)
|
||||
9
base_user_role_profile/views/assets.xml
Normal file
9
base_user_role_profile/views/assets.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<template id="assets_backend" name="User Role Profile assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/base_user_role_profile/static/src/js/switch_profile_menu.js"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
42
base_user_role_profile/views/profile.xml
Normal file
42
base_user_role_profile/views/profile.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_users_profile_form" model="ir.ui.view">
|
||||
<field name="name">res.users.profile.form</field>
|
||||
<field name="model">res.users.profile</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="user_ids"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_res_users_profile_tree" model="ir.ui.view">
|
||||
<field name="name">res.users.profile.tree</field>
|
||||
<field name="model">res.users.profile</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_res_users_profile_tree">
|
||||
<field name="name">User Profiles</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">res.users.profile</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="view_res_users_profile_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_action_res_users_profile_tree"
|
||||
parent="base.menu_users"
|
||||
action="action_res_users_profile_tree"/>
|
||||
|
||||
</odoo>
|
||||
19
base_user_role_profile/views/role.xml
Normal file
19
base_user_role_profile/views/role.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_users_form_inherit" model="ir.ui.view">
|
||||
<field name="name">res.users.role.form.inherit</field>
|
||||
<field name="model">res.users.role</field>
|
||||
<field name="inherit_id" ref="base_user_role.view_res_users_role_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='line_ids']//field[@name='company_id']" position="after">
|
||||
<field name="profile_id"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='group_id']" position="after">
|
||||
<field name="profile_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
35
base_user_role_profile/views/user.xml
Normal file
35
base_user_role_profile/views/user.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).-->
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_users_form_inherit_profile" model="ir.ui.view">
|
||||
<field name="name">res.users.form.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='role_line_ids']" position="before">
|
||||
<group>
|
||||
<field name="profile_ids" widget="many2many_tags" attrs="{'readonly': True}"/>
|
||||
<field name="profile_id" domain="[('id', 'in', profile_ids)]"
|
||||
options="{'no_create_edit': True, 'no_open': True}"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='role_line_ids']//field[@name='company_id']" position="after">
|
||||
<field name="profile_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_res_users_form_show_company" model="ir.ui.view">
|
||||
<field name="name">res.users.form.inherit</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base_user_role.view_res_users_form_inherit"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='role_line_ids']//field[@name='company_id']" position="attributes">
|
||||
<attribute name="groups" eval=""/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
||||
73
base_user_role_profile_example/README.rst
Normal file
73
base_user_role_profile_example/README.rst
Normal file
@@ -0,0 +1,73 @@
|
||||
=============
|
||||
User profiles
|
||||
=============
|
||||
|
||||
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/12.0/base_user_role_profile
|
||||
:alt: oca/server-backend
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
Extending the base_user_role module, this one adds the notion of profiles. Effectively profiles act as an additional filter to how the roles are used. Through the new widget, much in the same way that a user can switch companies when they are part of the multi company group, users have the possibility to change profiles when they are part of the multi profiles group.
|
||||
|
||||
This allows users to switch their permission groups dynamically. This can be useful for example to:
|
||||
- finer grain control on menu and model permissions (with record rules this becomes very flexible)
|
||||
- break down complicated menus into simpler ones
|
||||
- easily restrict users accidentally editing or creating records in O2M fields and in general misusing the interface, instead of excessively explaining things to them
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Go to Configuration / Users / Profiles and create a profile. Go to Configuration / Users / Roles and define some role lines with profiles.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Once you have set up at least one profile for a user, use the widget in the top bar to switch user profiles. Note that it is possible to use no profile; in this case, the user will only get the roles that always apply (i.e the ones with no profile_id).
|
||||
|
||||
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 smashing it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/oca/server-backend/issues/new?body=module:%20base_user_role_profile%0Aversion:%2012.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
|
||||
~~~~~~~
|
||||
|
||||
* Akretion
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Kevin Khao <kevin.khao@akretion.com>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is part of the `oca/server-backend <https://github.com/oca/server-backend/tree/12.0/base_user_role_profile>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute.
|
||||
0
base_user_role_profile_example/__init__.py
Normal file
0
base_user_role_profile_example/__init__.py
Normal file
20
base_user_role_profile_example/__manifest__.py
Normal file
20
base_user_role_profile_example/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2014 ABF OSIELL <http://osiell.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
"name": "User profiles example",
|
||||
"version": "12.0.1.0.0",
|
||||
"category": "Tools",
|
||||
"author": "Akretion, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"website": "https://github.com/OCA/server-backend",
|
||||
"depends": [
|
||||
"base_user_role_profile",
|
||||
"purchase",
|
||||
"sales_team",
|
||||
"sale_management",
|
||||
"hr",
|
||||
],
|
||||
"demo": ["demo/demo.xml"],
|
||||
"installable": True,
|
||||
}
|
||||
95
base_user_role_profile_example/demo/demo.xml
Normal file
95
base_user_role_profile_example/demo/demo.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<odoo>
|
||||
<record id="demo_profile_company" model="res.company">
|
||||
<field name="name">Company, The Second</field>
|
||||
</record>
|
||||
|
||||
<!--Profiles-->
|
||||
<!--Merchant: Purchase and/or Sales-->
|
||||
<record id="demo_profile_1" model="res.users.profile">
|
||||
<field name="name">Merchant Profile</field>
|
||||
</record>
|
||||
<record id="demo_profile_2" model="res.users.profile">
|
||||
<field name="name">HR profile</field>
|
||||
</record>
|
||||
<record id="demo_profile_3" model="res.users.profile">
|
||||
<field name="name">ERP Settings profile</field>
|
||||
</record>
|
||||
|
||||
<!--Roles-->
|
||||
<record id="role_purchaser" model="res.users.role">
|
||||
<field name="name">Role 1: Purchaser permissions</field>
|
||||
<field name="implied_ids" eval="[
|
||||
(6, 0, [ref('purchase.group_purchase_manager'), ref('base.group_user'), ref('base.group_multi_company')])
|
||||
]"/>
|
||||
<field name="profile_id" eval="ref('base_user_role_profile_example.demo_profile_1')"/>
|
||||
</record>
|
||||
<record id="role_sale" model="res.users.role">
|
||||
<field name="name">Role 2: Sales permissions</field>
|
||||
<field name="implied_ids" eval="[
|
||||
(6, 0, [ref('sales_team.group_sale_manager'), ref('base.group_user'), ref('base.group_multi_company')])
|
||||
]"/>
|
||||
<field name="profile_id" eval="ref('base_user_role_profile_example.demo_profile_1')"/>
|
||||
</record>
|
||||
<record id="role_erp_manager" model="res.users.role">
|
||||
<field name="name">Role 3: System permissions</field>
|
||||
<field name="implied_ids" eval="[
|
||||
(6, 0, [ref('base.group_erp_manager'), ref('base.group_user'), ref('base.group_multi_company')])
|
||||
]"/>
|
||||
<field name="profile_id" eval="ref('base_user_role_profile_example.demo_profile_3')"/>
|
||||
</record>
|
||||
<record id="role_hr_manager" model="res.users.role">
|
||||
<field name="name">Role 4: HR permissions</field>
|
||||
<field name="implied_ids" eval="[
|
||||
(6, 0, [ref('hr.group_hr_manager'), ref('base.group_user'), ref('base.group_multi_company')])
|
||||
]"/>
|
||||
<field name="profile_id" eval="ref('base_user_role_profile_example.demo_profile_2')"/>
|
||||
</record>
|
||||
|
||||
<!--Role lines-->
|
||||
<record id="role_line_1" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_purchaser')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base.main_company')"/>
|
||||
</record>
|
||||
<record id="role_line_2" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_sale')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base.main_company')"/>
|
||||
</record>
|
||||
<record id="role_line_3" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_hr_manager')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base.main_company')"/>
|
||||
</record>
|
||||
<record id="role_line_4" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_sale')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base_user_role_profile_example.demo_profile_company')"/>
|
||||
</record>
|
||||
<record id="role_line_5" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_hr_manager')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base_user_role_profile_example.demo_profile_company')"/>
|
||||
</record>
|
||||
<record id="role_line_6" model="res.users.role.line">
|
||||
<field name="role_id" eval="ref('base_user_role_profile_example.role_erp_manager')"/>
|
||||
<field name="user_id" eval="ref('base.user_demo')"/>
|
||||
<field name="company_id" eval="ref('base_user_role_profile_example.demo_profile_company')"/>
|
||||
</record>
|
||||
|
||||
<!--Demo user-->
|
||||
<record id="base.user_demo" model="res.users">
|
||||
<field name="role_line_ids"
|
||||
eval="[
|
||||
(4, ref('base_user_role_profile_example.role_line_1'), 0),
|
||||
(4, ref('base_user_role_profile_example.role_line_2'), 0),
|
||||
(4, ref('base_user_role_profile_example.role_line_3'), 0),
|
||||
(4, ref('base_user_role_profile_example.role_line_4'), 0),
|
||||
(4, ref('base_user_role_profile_example.role_line_5'), 0),
|
||||
(4, ref('base_user_role_profile_example.role_line_6'), 0),
|
||||
]"/>
|
||||
<field name="company_ids" eval="[(4, ref('base_user_role_profile_example.demo_profile_company'), 0)]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
1
base_user_role_profile_example/readme/CONFIGURE.rst
Normal file
1
base_user_role_profile_example/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1 @@
|
||||
Nothing to configure, just check the demo user.
|
||||
2
base_user_role_profile_example/readme/CONTRIBUTORS.rst
Normal file
2
base_user_role_profile_example/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
* Kevin Khao <kevin.khao@akretion.com>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
2
base_user_role_profile_example/readme/DESCRIPTION.rst
Normal file
2
base_user_role_profile_example/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
This shows an example of base_user_role_profile in use.
|
||||
|
||||
12
base_user_role_profile_example/readme/USAGE.rst
Normal file
12
base_user_role_profile_example/readme/USAGE.rst
Normal file
@@ -0,0 +1,12 @@
|
||||
Log in as the demo user, and observe on the upper right of the screen the widgets for profile selection and company selection.
|
||||
Use the widgets to manipulate user profile and companies for dynamic permissions/roles editing.
|
||||
|
||||
Note: "Merchant Profile" means the user is interested in sales and purchases, and thus has access only to those menus. Note that through configuration of roles and role lines, a merchant can be a sales user AND a purchase user in one company, or just a sales user (NOT a purchase user) in another company.
|
||||
|
||||
Here is a walkthrough:
|
||||
|
||||
* Demo user starts in "YourCompany" company. Observe permissions and access to the root menus for sales and purchases.
|
||||
* Switch profile to HR profile, which gives access only to HR permissions. Observe that you can create new employees.
|
||||
* Switch company to "Company, The Second". Observe the menu has been reset, profile options have changed, one has been picked automatically from the available ones.
|
||||
* Switch profile to "Merchant Profile". Observe that for this company, you can only access Sales, because only a Sales role line has been defined for this company and user.
|
||||
* Switch profile to "ERP Settings profile". Observe that as expected you have ERP manager permissions.
|
||||
Reference in New Issue
Block a user