diff --git a/product_cost_security/README.rst b/product_cost_security/README.rst index 832a28630298..62fa96519493 100644 --- a/product_cost_security/README.rst +++ b/product_cost_security/README.rst @@ -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 @@ -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 =========== @@ -84,6 +86,8 @@ Contributors * Anjeel Haria +* Jairo Llopis (`Moduon `__) + Maintainers ~~~~~~~~~~~ @@ -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 `__: +Current `maintainers `__: -|maintainer-sergio-teruel| +|maintainer-sergio-teruel| |maintainer-rafaelbn| |maintainer-yajo| This module is part of the `OCA/product-attribute `_ project on GitHub. diff --git a/product_cost_security/__manifest__.py b/product_cost_security/__manifest__.py index 249f58bab449..6035621a0483 100644 --- a/product_cost_security/__manifest__.py +++ b/product_cost_security/__manifest__.py @@ -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)", diff --git a/product_cost_security/models/__init__.py b/product_cost_security/models/__init__.py index b117c3f7eba6..1d2a193096ea 100644 --- a/product_cost_security/models/__init__.py +++ b/product_cost_security/models/__init__.py @@ -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 diff --git a/product_cost_security/models/product_cost_security_mixin.py b/product_cost_security/models/product_cost_security_mixin.py new file mode 100644 index 000000000000..3803217ef330 --- /dev/null +++ b/product_cost_security/models/product_cost_security_mixin.py @@ -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 diff --git a/product_cost_security/models/product_product.py b/product_cost_security/models/product_product.py index 55786a00d4fd..c4c0486cd39c 100644 --- a/product_cost_security/models/product_product.py +++ b/product_cost_security/models/product_product.py @@ -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") diff --git a/product_cost_security/models/product_template.py b/product_cost_security/models/product_template.py index b7abc7eca743..35736fc017ad 100644 --- a/product_cost_security/models/product_template.py +++ b/product_cost_security/models/product_template.py @@ -1,19 +1,11 @@ # Copyright 2018 Sergio Teruel - Tecnativa # 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" - ) diff --git a/product_cost_security/readme/CONFIGURE.rst b/product_cost_security/readme/CONFIGURE.rst index d4e76b200708..46db376472f5 100644 --- a/product_cost_security/readme/CONFIGURE.rst +++ b/product_cost_security/readme/CONFIGURE.rst @@ -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. diff --git a/product_cost_security/readme/CONTRIBUTORS.rst b/product_cost_security/readme/CONTRIBUTORS.rst index 3d46b822d1a6..108c66a7536e 100644 --- a/product_cost_security/readme/CONTRIBUTORS.rst +++ b/product_cost_security/readme/CONTRIBUTORS.rst @@ -8,3 +8,5 @@ * `Onestein `_: * Anjeel Haria + +* Jairo Llopis (`Moduon `__) diff --git a/product_cost_security/readme/USAGE.rst b/product_cost_security/readme/USAGE.rst index bf4f7c65bdae..c619230f3ba4 100644 --- a/product_cost_security/readme/USAGE.rst +++ b/product_cost_security/readme/USAGE.rst @@ -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. diff --git a/product_cost_security/security/product_cost_security.xml b/product_cost_security/security/product_cost_security.xml index 89463c282a13..bbdbc579daba 100644 --- a/product_cost_security/security/product_cost_security.xml +++ b/product_cost_security/security/product_cost_security.xml @@ -1,5 +1,9 @@ + + Product costs + + Access to product costs - + Modify product costs - + Product Cost Security !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:f9848c664bbd913a926941e7f6e17388b48f1caa84dba2a0f4c041ecff1ad89c +!! source digest: sha256:21d263a4d1abd768f4117463d2e40665572ebb999e0ba9e016dab0b5074f5b34 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Production/Stable License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

It adds two security groups, one for viewing the product cost price, and the other for @@ -391,15 +391,17 @@

Configuration

To use this module you need to:

  1. Go to a Setting > Users and Companies > Users.
  2. -
  3. Select a user and add “Access to product costs” group.
  4. +
  5. Edit a user.
  6. +
  7. Grant some access level using the Product costs dropdown.

Usage

To use this module you need to:

    -
  1. Go to product form view logged with this user and you will see the -standard_price field.
  2. +
  3. Go to product form view.
  4. +
  5. You will not see the Cost field unless you follow the Configuration steps and get read permissions.
  6. +
  7. You will not be able to edit it unless you are granted write permissions.
@@ -432,6 +434,7 @@

Contributors

  • Anjeel Haria
  • +
  • Jairo Llopis (Moduon)
  • @@ -441,8 +444,8 @@

    Maintainers

    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.

    -

    Current maintainer:

    -

    sergio-teruel

    +

    Current maintainers:

    +

    sergio-teruel rafaelbn yajo

    This module is part of the OCA/product-attribute project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    diff --git a/product_cost_security/tests/test_product_cost_security.py b/product_cost_security/tests/test_product_cost_security.py index e5713cf81ee5..e3f906ec528e 100644 --- a/product_cost_security/tests/test_product_cost_security.py +++ b/product_cost_security/tests/test_product_cost_security.py @@ -1,7 +1,8 @@ # Copyright 2023 Onestein () # 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): @@ -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}], + ) diff --git a/product_cost_security/views/product_views.xml b/product_cost_security/views/product_views.xml index fe793b4f263e..998b10e75d7b 100644 --- a/product_cost_security/views/product_views.xml +++ b/product_cost_security/views/product_views.xml @@ -4,9 +4,6 @@ product.template - - - - - {'readonly': [('user_can_update_cost', '=', False)]} - - -
    - - - - product.product - - - - - - - {'readonly': [('user_can_update_cost', '=', False)]} -