Skip to content

Commit

Permalink
[ADD] stock_inventory_security
Browse files Browse the repository at this point in the history
  • Loading branch information
ivantodorovich committed Nov 12, 2024
1 parent b94a8a1 commit 257254b
Show file tree
Hide file tree
Showing 19 changed files with 440 additions and 0 deletions.
6 changes: 6 additions & 0 deletions setup/stock_inventory_security/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
1 change: 1 addition & 0 deletions stock_inventory_security/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# To be generated by bot
1 change: 1 addition & 0 deletions stock_inventory_security/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
23 changes: 23 additions & 0 deletions stock_inventory_security/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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",
],
}
15 changes: 15 additions & 0 deletions stock_inventory_security/demo/demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<!--
Copyright 2024 Camptocamp SA (https://www.camptocamp.com).
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo>

<record id="base.user_demo" model="res.users">
<field
name="groups_id"
eval="[Command.link(ref('group_inventory_adjustment'))]"
/>
</record>

</odoo>
4 changes: 4 additions & 0 deletions stock_inventory_security/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import product_product
from . import stock_location
from . import stock_lot
from . import stock_quant
23 changes: 23 additions & 0 deletions stock_inventory_security/models/product_product.py
Original file line number Diff line number Diff line change
@@ -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()
22 changes: 22 additions & 0 deletions stock_inventory_security/models/stock_location.py
Original file line number Diff line number Diff line change
@@ -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)
24 changes: 24 additions & 0 deletions stock_inventory_security/models/stock_lot.py
Original file line number Diff line number Diff line change
@@ -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()
43 changes: 43 additions & 0 deletions stock_inventory_security/models/stock_quant.py
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions stock_inventory_security/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Iván Todorovich <[email protected]>
4 changes: 4 additions & 0 deletions stock_inventory_security/readme/README.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions stock_inventory_security/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions stock_inventory_security/security/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">

<record id="group_inventory_adjustment" model="res.groups">
<field name="name">Stock: Inventory Adjustments</field>
<field
name="implied_ids"
eval="[Command.link(ref('stock.group_stock_user'))]"
/>
</record>

<record id="stock.group_stock_manager" model="res.groups">
<field
name="implied_ids"
eval="[Command.link(ref('group_inventory_adjustment'))]"
/>
</record>

</odoo>
1 change: 1 addition & 0 deletions stock_inventory_security/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_stock_inventory_security
156 changes: 156 additions & 0 deletions stock_inventory_security/tests/test_stock_inventory_security.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 257254b

Please sign in to comment.