Skip to content

Commit

Permalink
Merge PR #1538 into 16.0
Browse files Browse the repository at this point in the history
Signed-off-by legalsylvain
  • Loading branch information
OCA-git-bot committed Mar 11, 2024
2 parents 31d4da2 + d53def1 commit d9f6d78
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 59 deletions.
22 changes: 16 additions & 6 deletions product_cost_security/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Product Cost Security
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f9848c664bbd913a926941e7f6e17388b48f1caa84dba2a0f4c041ecff1ad89c
!! source digest: sha256:21d263a4d1abd768f4117463d2e40665572ebb999e0ba9e016dab0b5074f5b34
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
Expand Down Expand Up @@ -42,15 +42,17 @@ Configuration
To use this module you need to:

#. Go to a *Setting > Users and Companies > Users*.
#. Select a user and add "Access to product costs" group.
#. Edit a user.
#. Grant some access level using the *Product costs* dropdown.

Usage
=====

To use this module you need to:

#. Go to product form view logged with this user and you will see the
standard_price field.
#. Go to product form view.
#. You will not see the *Cost* field unless you follow the *Configuration* steps and get read permissions.
#. You will not be able to edit it unless you are granted write permissions.

Bug Tracker
===========
Expand Down Expand Up @@ -84,6 +86,8 @@ Contributors

* Anjeel Haria

* Jairo Llopis (`Moduon <https://www.moduon.team>`__)

Maintainers
~~~~~~~~~~~

Expand All @@ -100,10 +104,16 @@ promote its widespread use.
.. |maintainer-sergio-teruel| image:: https://github.com/sergio-teruel.png?size=40px
:target: https://github.com/sergio-teruel
:alt: sergio-teruel
.. |maintainer-rafaelbn| image:: https://github.com/rafaelbn.png?size=40px
:target: https://github.com/rafaelbn
:alt: rafaelbn
.. |maintainer-yajo| image:: https://github.com/yajo.png?size=40px
:target: https://github.com/yajo
:alt: yajo

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-sergio-teruel|
|maintainer-sergio-teruel| |maintainer-rafaelbn| |maintainer-yajo|

This module is part of the `OCA/product-attribute <https://github.com/OCA/product-attribute/tree/16.0/product_cost_security>`_ project on GitHub.

Expand Down
2 changes: 1 addition & 1 deletion product_cost_security/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"summary": "Product cost security restriction view",
"version": "16.0.1.0.0",
"development_status": "Production/Stable",
"maintainers": ["sergio-teruel"],
"maintainers": ["sergio-teruel", "rafaelbn", "yajo"],
"category": "Product",
"website": "https://github.com/OCA/product-attribute",
"author": "Tecnativa, Odoo Community Association (OCA)",
Expand Down
1 change: 1 addition & 0 deletions product_cost_security/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from . import product_cost_security_mixin
from . import product_template
from . import product_product
97 changes: 97 additions & 0 deletions product_cost_security/models/product_cost_security_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2024 Moduon Team S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl-3.0)
from contextlib import suppress

from odoo import _, api, fields, models
from odoo.exceptions import AccessError


class ProductCostSecurityMixin(models.AbstractModel):
"""Automatic security for models related with product costs.
When you inherit from this mixin, make sure to add
`groups="product_cost_security.group_product_cost"` to the fields that
should be protected. Odoo will take care of hiding those fields to users
without that access, and this mixin will add an extra protection to prevent
editing if the user is not in the
`product_cost_security.group_product_edit_cost` group.
"""

_name = "product.cost.security.mixin"
_description = "Product cost access control mixin"

user_can_update_cost = fields.Boolean(compute="_compute_user_can_update_cost")

@api.depends_context("uid")
def _compute_user_can_update_cost(self):
"""Let views know if users can edit product costs.
A user could have full cost permissions but no product edition permissions.
We want to prevent those from updating costs.
"""
self.user_can_update_cost = self._user_can_update_cost()

@api.model
def _user_can_update_cost(self):
"""Know if current user can update product costs.
Just like `self.user_can_update_cost`, but once per model.
"""
return self.env.user.has_group("product_cost_security.group_product_edit_cost")

@api.model
def _product_cost_security_fields(self):
"""Fields that should be hidden if the user has no cost permissions.
Returns a list of field names where the security group is applied.
"""
return {
fname
for (fname, field) in self._fields.items()
if "product_cost_security.group_product_cost"
in str(field.groups).split(",")
}

@api.model
def check_field_access_rights(self, operation, fields):
"""Forbid users from updating product costs if they have no permissions.
The field's `groups` attribute restricts always R/W access. We apply an
extra protection to prevent only editing if the user is not in the
`product_cost_security.group_product_edit_cost` group.
"""
valid_fields = super().check_field_access_rights(operation, fields)
if self.env.su:
return valid_fields
product_cost_fields = self._product_cost_security_fields().intersection(
valid_fields
)
if (
operation != "read"
and product_cost_fields
and not self._user_can_update_cost()
):
description = self.env["ir.model"]._get(self._name).name
raise AccessError(
_(
'You do not have enough rights to access the fields "%(fields)s"'
" on %(document_kind)s (%(document_model)s). "
"Please contact your system administrator."
"\n\n(Operation: %(operation)s)",
fields=",".join(sorted(product_cost_fields)),
document_kind=description,
document_model=self._name,
operation=operation,
)
)
return valid_fields

@api.model
def fields_get(self, allfields=None, attributes=None):
"""Make product cost fields readonly for non-editors."""
result = super().fields_get(allfields, attributes)
if not self._user_can_update_cost():
for field_name in self._product_cost_security_fields():
with suppress(KeyError):
result[field_name]["readonly"] = True
return result
4 changes: 3 additions & 1 deletion product_cost_security/models/product_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@


class ProductProduct(models.Model):
_inherit = "product.product"
_name = "product.product"
_inherit = ["product.product", "product.cost.security.mixin"]

# Inherited fields
standard_price = fields.Float(groups="product_cost_security.group_product_cost")
16 changes: 4 additions & 12 deletions product_cost_security/models/product_template.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
# Copyright 2018 Sergio Teruel - Tecnativa <sergio.teruel@tecnativa.com>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
from odoo import fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"
_name = "product.template"
_inherit = ["product.template", "product.cost.security.mixin"]

# Inherited fields
standard_price = fields.Float(groups="product_cost_security.group_product_cost")
user_can_update_cost = fields.Boolean(compute="_compute_user_can_update_cost")

@api.depends_context("uid")
def _compute_user_can_update_cost(self):
"""A user could have full cost permissions but no product edition permissions.
We want to prevent those from updating costs."""
for product in self:
product.user_can_update_cost = self.env.user.has_group(
"product_cost_security.group_product_edit_cost"
)
3 changes: 2 additions & 1 deletion product_cost_security/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
To use this module you need to:

#. Go to a *Setting > Users and Companies > Users*.
#. Select a user and add "Access to product costs" group.
#. Edit a user.
#. Grant some access level using the *Product costs* dropdown.
2 changes: 2 additions & 0 deletions product_cost_security/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
* `Onestein <https://www.onestein.eu>`_:

* Anjeel Haria

* Jairo Llopis (`Moduon <https://www.moduon.team>`__)
5 changes: 3 additions & 2 deletions product_cost_security/readme/USAGE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
To use this module you need to:

#. Go to product form view logged with this user and you will see the
standard_price field.
#. Go to product form view.
#. You will not see the *Cost* field unless you follow the *Configuration* steps and get read permissions.
#. You will not be able to edit it unless you are granted write permissions.
8 changes: 6 additions & 2 deletions product_cost_security/security/product_cost_security.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<?xml version="1.0" ?>
<odoo>
<record model="ir.module.category" id="module_category">
<field name="name">Product costs</field>
</record>

<!--
Only users with this group can view the fields with this security group.
Add group to user_root to avoid errors when other modules install and
add fields to views with this group defined as field attribute.
-->
<record model="res.groups" id="group_product_cost">
<field name="name">Access to product costs</field>
<field name="category_id" ref="base.module_category_hidden" />
<field name="category_id" ref="module_category" />
<field name="users" eval="[(4, ref('base.user_root'))]" />
</record>
<!--
Only users within this group can edit costs.
-->
<record model="res.groups" id="group_product_edit_cost">
<field name="name">Modify product costs</field>
<field name="category_id" ref="base.module_category_hidden" />
<field name="category_id" ref="module_category" />
<field
name="implied_ids"
eval="[(4, ref('product_cost_security.group_product_cost'))]"
Expand Down
15 changes: 9 additions & 6 deletions product_cost_security/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ <h1 class="title">Product Cost Security</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f9848c664bbd913a926941e7f6e17388b48f1caa84dba2a0f4c041ecff1ad89c
!! source digest: sha256:21d263a4d1abd768f4117463d2e40665572ebb999e0ba9e016dab0b5074f5b34
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" 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 image-reference" href="https://github.com/OCA/product-attribute/tree/16.0/product_cost_security"><img alt="OCA/product-attribute" src="https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_cost_security"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>It adds two security groups, one for viewing the product cost price, and the other for
Expand All @@ -391,15 +391,17 @@ <h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
<p>To use this module you need to:</p>
<ol class="arabic simple">
<li>Go to a <em>Setting &gt; Users and Companies &gt; Users</em>.</li>
<li>Select a user and add “Access to product costs” group.</li>
<li>Edit a user.</li>
<li>Grant some access level using the <em>Product costs</em> dropdown.</li>
</ol>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
<p>To use this module you need to:</p>
<ol class="arabic simple">
<li>Go to product form view logged with this user and you will see the
standard_price field.</li>
<li>Go to product form view.</li>
<li>You will not see the <em>Cost</em> field unless you follow the <em>Configuration</em> steps and get read permissions.</li>
<li>You will not be able to edit it unless you are granted write permissions.</li>
</ol>
</div>
<div class="section" id="bug-tracker">
Expand Down Expand Up @@ -432,6 +434,7 @@ <h2><a class="toc-backref" href="#toc-entry-6">Contributors</a></h2>
<li>Anjeel Haria</li>
</ul>
</li>
<li>Jairo Llopis (<a class="reference external" href="https://www.moduon.team">Moduon</a>)</li>
</ul>
</div>
<div class="section" id="maintainers">
Expand All @@ -441,8 +444,8 @@ <h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2>
<p>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.</p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/sergio-teruel"><img alt="sergio-teruel" src="https://github.com/sergio-teruel.png?size=40px" /></a></p>
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
<p><a class="reference external image-reference" href="https://github.com/sergio-teruel"><img alt="sergio-teruel" src="https://github.com/sergio-teruel.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/rafaelbn"><img alt="rafaelbn" src="https://github.com/rafaelbn.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/yajo"><img alt="yajo" src="https://github.com/yajo.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/product-attribute/tree/16.0/product_cost_security">OCA/product-attribute</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
Expand Down
59 changes: 58 additions & 1 deletion product_cost_security/tests/test_product_cost_security.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Copyright 2023 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo.tests.common import Form, TransactionCase
from odoo.exceptions import AccessError
from odoo.tests.common import Form, TransactionCase, new_test_user


class TestProductCostSecurity(TransactionCase):
Expand Down Expand Up @@ -76,3 +77,59 @@ def test_with_access_to_modify_product_costs_group(self):
sheet_form.standard_price = 5.0 # It would not raise any error now as the user
# has the required group to modify the costs
self.assertEqual(sheet_form.standard_price, 5.0)

def test_api_modification(self):
"""Test that a direct API call respects the security groups."""
# Using base.group_system because it's the only group in this module's
# dependency graph with CRUD access to products
editor = new_test_user(
self.env,
"editor",
groups="base.group_system,product_cost_security.group_product_edit_cost",
)
reader = new_test_user(
self.env,
"reader",
groups="base.group_system,product_cost_security.group_product_cost",
)
user = new_test_user(self.env, "user", groups="base.group_system")
# Editor can write and read
product = (
self.env["product.product"]
.with_user(editor)
.create(
{
"name": "Test product",
"standard_price": 10.0,
}
)
)
self.assertEqual(
product.read(["standard_price"]),
[{"id": product.id, "standard_price": 10.0}],
)
product.standard_price = 20.0
self.assertEqual(
product.read(["standard_price"]),
[{"id": product.id, "standard_price": 20.0}],
)
# Reader can read but not write
product = product.with_user(reader)
with self.assertRaises(AccessError):
product.standard_price = 30.0
self.assertEqual(
product.read(["standard_price"]),
[{"id": product.id, "standard_price": 20.0}],
)
# User can't read or write (standard Odoo when setting field groups)
product = product.with_user(user)
with self.assertRaises(AccessError):
product.standard_price = 30.0
with self.assertRaises(AccessError):
product.read(["standard_price"])
# Sudo still works
product.sudo().standard_price = 30.0
self.assertEqual(
product.sudo().read(["standard_price"]),
[{"id": product.id, "standard_price": 30.0}],
)
Loading

0 comments on commit d9f6d78

Please sign in to comment.