From 257254b4af0797190dbaf37bde5cb4fc640b3fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Todorovich?= Date: Thu, 7 Nov 2024 12:37:12 -0300 Subject: [PATCH] [ADD] stock_inventory_security --- .../odoo/addons/stock_inventory_security | 1 + setup/stock_inventory_security/setup.py | 6 + stock_inventory_security/README.rst | 1 + stock_inventory_security/__init__.py | 1 + stock_inventory_security/__manifest__.py | 23 +++ stock_inventory_security/demo/demo.xml | 15 ++ stock_inventory_security/models/__init__.py | 4 + .../models/product_product.py | 23 +++ .../models/stock_location.py | 22 +++ stock_inventory_security/models/stock_lot.py | 24 +++ .../models/stock_quant.py | 43 +++++ .../readme/CONTRIBUTORS.md | 1 + stock_inventory_security/readme/README.md | 4 + .../security/ir.model.access.csv | 3 + .../security/security.xml | 19 +++ stock_inventory_security/tests/__init__.py | 1 + .../tests/test_stock_inventory_security.py | 156 ++++++++++++++++++ stock_inventory_security/views/product.xml | 59 +++++++ .../views/stock_quant.xml | 34 ++++ 19 files changed, 440 insertions(+) create mode 120000 setup/stock_inventory_security/odoo/addons/stock_inventory_security create mode 100644 setup/stock_inventory_security/setup.py create mode 100644 stock_inventory_security/README.rst create mode 100644 stock_inventory_security/__init__.py create mode 100644 stock_inventory_security/__manifest__.py create mode 100644 stock_inventory_security/demo/demo.xml create mode 100644 stock_inventory_security/models/__init__.py create mode 100644 stock_inventory_security/models/product_product.py create mode 100644 stock_inventory_security/models/stock_location.py create mode 100644 stock_inventory_security/models/stock_lot.py create mode 100644 stock_inventory_security/models/stock_quant.py create mode 100644 stock_inventory_security/readme/CONTRIBUTORS.md create mode 100644 stock_inventory_security/readme/README.md create mode 100644 stock_inventory_security/security/ir.model.access.csv create mode 100644 stock_inventory_security/security/security.xml create mode 100644 stock_inventory_security/tests/__init__.py create mode 100644 stock_inventory_security/tests/test_stock_inventory_security.py create mode 100644 stock_inventory_security/views/product.xml create mode 100644 stock_inventory_security/views/stock_quant.xml diff --git a/setup/stock_inventory_security/odoo/addons/stock_inventory_security b/setup/stock_inventory_security/odoo/addons/stock_inventory_security new file mode 120000 index 000000000000..f9e25cd7746f --- /dev/null +++ b/setup/stock_inventory_security/odoo/addons/stock_inventory_security @@ -0,0 +1 @@ +../../../../stock_inventory_security \ No newline at end of file diff --git a/setup/stock_inventory_security/setup.py b/setup/stock_inventory_security/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/stock_inventory_security/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/stock_inventory_security/README.rst b/stock_inventory_security/README.rst new file mode 100644 index 000000000000..c26961f4e147 --- /dev/null +++ b/stock_inventory_security/README.rst @@ -0,0 +1 @@ +# To be generated by bot diff --git a/stock_inventory_security/__init__.py b/stock_inventory_security/__init__.py new file mode 100644 index 000000000000..0650744f6bc6 --- /dev/null +++ b/stock_inventory_security/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_inventory_security/__manifest__.py b/stock_inventory_security/__manifest__.py new file mode 100644 index 000000000000..6f1e552ede74 --- /dev/null +++ b/stock_inventory_security/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Stock Quant Inventory Security", + "summary": "Dedicated security group to apply inventory adjustments", + "version": "16.0.1.0.0", + "author": "Camptocamp, Odoo Community Association (OCA)", + "maintainers": ["ivantodorovich"], + "website": "https://github.com/OCA/stock-logistics-warehouse", + "license": "AGPL-3", + "category": "Inventory", + "depends": ["stock"], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "views/product.xml", + "views/stock_quant.xml", + ], + "demo": [ + "demo/demo.xml", + ], +} diff --git a/stock_inventory_security/demo/demo.xml b/stock_inventory_security/demo/demo.xml new file mode 100644 index 000000000000..c5dd2f94d928 --- /dev/null +++ b/stock_inventory_security/demo/demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/stock_inventory_security/models/__init__.py b/stock_inventory_security/models/__init__.py new file mode 100644 index 000000000000..d979564a36df --- /dev/null +++ b/stock_inventory_security/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_product +from . import stock_location +from . import stock_lot +from . import stock_quant diff --git a/stock_inventory_security/models/product_product.py b/stock_inventory_security/models/product_product.py new file mode 100644 index 000000000000..390465aaa8d1 --- /dev/null +++ b/stock_inventory_security/models/product_product.py @@ -0,0 +1,23 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + def user_has_groups(self, groups): + # Most inventory adjustment operations are limited to users having + # the Inventory Manager group. + # OVERRIDE: Hijack the check to replace it with our own group. + if groups == "stock.group_stock_manager" and self.env.context.get( + "_stock_inventory_security" + ): + groups = "stock_inventory_security.group_inventory_adjustment" + return super().user_has_groups(groups) + + def action_open_quants(self): + if self.user_has_groups("stock_inventory_security.group_inventory_adjustment"): + self = self.with_context(_stock_inventory_security=True) + return super().action_open_quants() diff --git a/stock_inventory_security/models/stock_location.py b/stock_inventory_security/models/stock_location.py new file mode 100644 index 000000000000..1ff8deef8e11 --- /dev/null +++ b/stock_inventory_security/models/stock_location.py @@ -0,0 +1,22 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockLocation(models.Model): + _inherit = "stock.location" + + def write(self, vals): + # OVERRIDE: Allow the inventory user to set the last inventory date. + # https://github.com/odoo/odoo/blob/534220ee/addons/stock/models/stock_quant.py#L775 + if ( + self.env.context.get("_stock_inventory_security") + and len(vals) == 1 + and "last_inventory_date" in vals + and self.user_has_groups( + "stock_inventory_security.group_inventory_adjustment" + ) + ): + self = self.sudo() + return super().write(vals) diff --git a/stock_inventory_security/models/stock_lot.py b/stock_inventory_security/models/stock_lot.py new file mode 100644 index 000000000000..75812fecfb2a --- /dev/null +++ b/stock_inventory_security/models/stock_lot.py @@ -0,0 +1,24 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models + + +class StockLot(models.Model): + _inherit = "stock.lot" + + def user_has_groups(self, groups): + # Most inventory adjustment operations are limited to users having + # the Inventory Manager group. + # OVERRIDE: Hijack the check to replace it with our own group.) + if groups == "stock.group_stock_manager" and self.env.context.get( + "_stock_inventory_security" + ): + groups = "stock_inventory_security.group_inventory_adjustment" + return super().user_has_groups(groups) + + def action_lot_open_quants(self): + # OVERRIDE: Add the inventory_mode context + if self.user_has_groups("stock_inventory_security.group_inventory_adjustment"): + self = self.with_context(_stock_inventory_security=True) + return super().action_lot_open_quants() diff --git a/stock_inventory_security/models/stock_quant.py b/stock_inventory_security/models/stock_quant.py new file mode 100644 index 000000000000..452c13fe3417 --- /dev/null +++ b/stock_inventory_security/models/stock_quant.py @@ -0,0 +1,43 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class StockQuant(models.Model): + _inherit = "stock.quant" + + inventory_quantity_auto_apply = fields.Float( + # Change stock.group_stock_manager to our own group. + groups="stock_inventory_security.group_inventory_adjustment", + ) + + def user_has_groups(self, groups): + # Most inventory adjustment operations are limited to users having + # the Inventory Manager group. + # OVERRIDE: Hijack the check to replace it with our own group. + if groups == "stock.group_stock_manager" and self.env.context.get( + "_stock_inventory_security" + ): + groups = "stock_inventory_security.group_inventory_adjustment" + return super().user_has_groups(groups) + + def _get_quants_action(self, domain=None, extend=False): + # OVERRIDE: Show the editable quants view for users having the Inventory + # Adjustments group. + # The original method would only do it for Stock Managers. + if self.user_has_groups("stock_inventory_security.group_inventory_adjustment"): + self = self.with_context(_stock_inventory_security=True) + return super()._get_quants_action(domain=domain, extend=extend) + + def action_view_inventory(self): + # OVERRIDE: Disable the "My count" filter for users having the Inventory + # Adjustments group. + if self.user_has_groups("stock_inventory_security.group_inventory_adjustment"): + self = self.with_context(_stock_inventory_security=True) + return super().action_view_inventory() + + def _apply_inventory(self): + if self.user_has_groups("stock_inventory_security.group_inventory_adjustment"): + self = self.with_context(_stock_inventory_security=True) + return super()._apply_inventory() diff --git a/stock_inventory_security/readme/CONTRIBUTORS.md b/stock_inventory_security/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..7992345c3cce --- /dev/null +++ b/stock_inventory_security/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +* Iván Todorovich diff --git a/stock_inventory_security/readme/README.md b/stock_inventory_security/readme/README.md new file mode 100644 index 000000000000..c60cf67ac8f6 --- /dev/null +++ b/stock_inventory_security/readme/README.md @@ -0,0 +1,4 @@ +In standard, the inventory adjustments can only be applied by **Inventory / Manager** users. + +This module introduces a new security group named **Stock: Inventory Adjustments**, which +grants regular stock users the ability to apply inventory adjustments. diff --git a/stock_inventory_security/security/ir.model.access.csv b/stock_inventory_security/security/ir.model.access.csv new file mode 100644 index 000000000000..cafc1a2b43ae --- /dev/null +++ b/stock_inventory_security/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_inventory_adjustment_name,stock.inventory.adjustment.name,stock.model_stock_inventory_adjustment_name,group_inventory_adjustment,1,1,1,0 +access_stock_request_count,stock.request.count,stock.model_stock_request_count,group_inventory_adjustment,1,1,1,0 diff --git a/stock_inventory_security/security/security.xml b/stock_inventory_security/security/security.xml new file mode 100644 index 000000000000..6f28befc83ea --- /dev/null +++ b/stock_inventory_security/security/security.xml @@ -0,0 +1,19 @@ + + + + + Stock: Inventory Adjustments + + + + + + + + diff --git a/stock_inventory_security/tests/__init__.py b/stock_inventory_security/tests/__init__.py new file mode 100644 index 000000000000..acaff09facdd --- /dev/null +++ b/stock_inventory_security/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_inventory_security diff --git a/stock_inventory_security/tests/test_stock_inventory_security.py b/stock_inventory_security/tests/test_stock_inventory_security.py new file mode 100644 index 000000000000..76e78153a8d9 --- /dev/null +++ b/stock_inventory_security/tests/test_stock_inventory_security.py @@ -0,0 +1,156 @@ +# Copyright 2024 Camptocamp SA (https://www.camptocamp.com). +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError +from odoo.tests import Form, TransactionCase, new_test_user, users + +from odoo.addons.base.tests.common import DISABLED_MAIL_CONTEXT + + +class TestStockInventorySecurity(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, **DISABLED_MAIL_CONTEXT)) + # Lazy tests compatibility with `stock_inventory_discrepancy` + cls.env = cls.env(context=dict(cls.env.context, skip_exceeded_discrepancy=True)) + # Create test records + cls.inventory_user = new_test_user( + cls.env, + login="inventory", + groups="stock_inventory_security.group_inventory_adjustment", + ) + cls.stock_user = new_test_user( + cls.env, + login="stock", + groups="stock.group_stock_user", + ) + cls.stock_location = cls.env.ref("stock.stock_location_stock") + cls.product = cls.env["product.product"].create( + { + "name": "Test product", + "type": "product", + } + ) + cls.product_lot = cls.env["stock.lot"].create( + { + "product_id": cls.product.id, + "company_id": cls.env.company.id, + } + ) + cls.product_quants = ( + cls.env["stock.quant"] + .with_context(inventory_mode=True) + .create( + { + "product_id": cls.product.id, + "inventory_quantity": 4, + "lot_id": cls.product_lot.id, + "location_id": cls.stock_location.id, + } + ) + ) + cls.product_quants.action_apply_inventory() + + @users("inventory", "admin") + def test_inventory_user_product_action_open_quants(self): + """Test that the inventory user gets into inventory mode from products""" + res = self.product.with_user(self.env.user).action_open_quants() + self.assertFalse(res["context"].get("search_default_my_count")) + + @users("stock") + def test_stock_user_product_action_open_quants(self): + """Test that the stock user does not get into inventory mode from products""" + res = self.product.with_user(self.env.user).action_open_quants() + self.assertTrue(res["context"].get("search_default_my_count")) + + @users("inventory", "admin") + def test_inventory_user_quant_action(self): + """Test that the inventory user gets into inventory mode from quants""" + res = self.product_quants.with_user(self.env.user).action_view_inventory() + self.assertFalse(res["context"].get("search_default_my_count")) + + @users("stock") + def test_stock_user_quant_action(self): + """Test that the stock user does not get into inventory mode from quants""" + res = self.product_quants.with_user(self.env.user).action_view_inventory() + self.assertTrue(res["context"].get("search_default_my_count")) + + @users("inventory", "admin") + def test_inventory_user_lot_action(self): + """Test that the inventory user gets into inventory mode from lots""" + res = self.product_lot.with_user(self.env.user).action_lot_open_quants() + self.assertEqual( + res["view_id"], self.env.ref("stock.view_stock_quant_tree_editable").id + ) + self.assertTrue(res["context"].get("inventory_mode")) + + @users("stock") + def test_stock_user_lot_action(self): + """Test that the stock user does not get into inventory mode from lots""" + res = self.product_lot.with_user(self.stock_user).action_lot_open_quants() + self.assertEqual(res["view_id"], self.env.ref("stock.view_stock_quant_tree").id) + self.assertFalse(res["context"].get("inventory_mode")) + + @users("inventory", "admin") + def test_inventory_user_apply_inventory(self): + """Test that the inventory user can apply inventory""" + quant = ( + self.env["stock.quant"] + .with_context(inventory_mode=True) + .create( + { + "product_id": self.product.id, + "inventory_quantity": 10, + "lot_id": self.product_lot.id, + "location_id": self.stock_location.id, + } + ) + ) + quant.action_apply_inventory() + self.assertEqual(self.product.qty_available, 10) + + @users("stock") + def test_stock_user_apply_inventory(self): + """Test that the stock user cannot apply inventory""" + with self.assertRaisesRegex( + UserError, "Only a stock manager can validate an inventory adjustment." + ): + quant = self.env["stock.quant"].create( + { + "product_id": self.product.id, + "inventory_quantity": 10, + "lot_id": self.product_lot.id, + "location_id": self.stock_location.id, + } + ) + quant.action_apply_inventory() + + @users("inventory", "admin") + def test_inventory_user_apply_inventory_reason(self): + """Test that the inventory user can apply inventory with a reason""" + quant = ( + self.env["stock.quant"] + .with_context(inventory_mode=True) + .create( + { + "product_id": self.product.id, + "lot_id": self.product_lot.id, + "location_id": self.stock_location.id, + "inventory_quantity": 10, + } + ) + ) + form_wizard = Form( + self.env["stock.inventory.adjustment.name"].with_context( + default_quant_ids=quant.ids + ) + ) + form_wizard.inventory_adjustment_name = "Inventory Adjustment - Test" + form_wizard.save().action_apply() + self.assertTrue( + self.env["stock.move"].search( + [("reference", "=", "Inventory Adjustment - Test")], limit=1 + ) + ) + self.assertEqual(self.product.qty_available, 10) diff --git a/stock_inventory_security/views/product.xml b/stock_inventory_security/views/product.xml new file mode 100644 index 000000000000..455a26c50e87 --- /dev/null +++ b/stock_inventory_security/views/product.xml @@ -0,0 +1,59 @@ + + + + + + product.product + + + + + + + + product.product + + + + + + + + product.template + + + + + + + diff --git a/stock_inventory_security/views/stock_quant.xml b/stock_inventory_security/views/stock_quant.xml new file mode 100644 index 000000000000..92fb1562f7d4 --- /dev/null +++ b/stock_inventory_security/views/stock_quant.xml @@ -0,0 +1,34 @@ + + + + + + stock.quant + + + + + + + +