commit 6601b5783319d5532d49b80804b0b71cf337de77 Author: Suherdy Yacob Date: Mon Jan 5 12:03:16 2026 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac82a6b --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Access Restriction By User + +This module allows administrators to restrict user access to specific records in Inventory, Manufacturing, and Approvals without using complex Record Rules. + +## Features + +Restrict visibility of the following records based on User configuration: +* **Warehouses** (`stock.warehouse`) +* **Picking Types** (`stock.picking.type`) +* **Locations** (`stock.location`) +* **Work Centers** (`mrp.workcenter`) +* **Approval Categories** (`approval.category`) + +## Configuration + +1. Navigate to **Settings > Users & Companies > Users**. +2. Select the user you want to restrict. +3. Go to the **Access Restrictions** tab. +4. Add records to the following fields: + * **Allowed Warehouses** + * **Allowed Picking Types** + * **Allowed Locations** + * **Allowed Work Centers** + * **Allowed Approvals** + +## Important Usage Notes + +* **Empty List = Unrestricted**: If an "Allowed" field is left empty for a user, they will have access to **ALL** records of that type. +* **Populated List = Restricted**: If one or more records are added, the user will **ONLY** see those specific records. +* **Superuser**: The Superuser (OdooBot) and administrators bypassing access rights are not affected by these restrictions. + +## Technical Details + +This module overrides the `_search` method on the target models to apply a domain filter based on the current user's allowed list. This ensures consistency across views (list, kanban, many2one dropdowns) and avoids common issues associated with Record Rules. + +## Author + +Suherdy Yacob diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..00ce14b --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'Access Restriction By User', + 'version': '18.0.1.0.0', + 'summary': 'Restrict access to Warehouses, Picking Types, Locations, Work Centers, and Approvals by User', + 'description': """ + Restricts visibility of: + - Warehouses + - Picking Types + - Locations + - Work Centers + - Approval Categories + + Files are filtered based on "Allowed" lists in the User Settings. + If the allowed list is empty, the user sees all records (default behavior). + Does NOT use Record Rules. + """, + 'category': 'Extra Tools', + 'author': 'Suherdy Yacob', + 'depends': ['base', 'stock', 'mrp', 'approvals'], + 'data': [ + 'views/res_users_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..06b8be2 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..c118021 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_users +from . import restricted_models diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..634e7a8 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/res_users.cpython-312.pyc b/models/__pycache__/res_users.cpython-312.pyc new file mode 100644 index 0000000..5d3af7a Binary files /dev/null and b/models/__pycache__/res_users.cpython-312.pyc differ diff --git a/models/__pycache__/restricted_models.cpython-312.pyc b/models/__pycache__/restricted_models.cpython-312.pyc new file mode 100644 index 0000000..b4a26e3 Binary files /dev/null and b/models/__pycache__/restricted_models.cpython-312.pyc differ diff --git a/models/res_users.py b/models/res_users.py new file mode 100644 index 0000000..abd2dd7 --- /dev/null +++ b/models/res_users.py @@ -0,0 +1,49 @@ +from odoo import models, fields + +class ResUsers(models.Model): + _inherit = 'res.users' + + allowed_warehouse_ids = fields.Many2many( + 'stock.warehouse', + 'res_users_stock_warehouse_rel', + 'user_id', + 'warehouse_id', + string="Allowed Warehouses", + help="Warehouses this user is allowed to access. Leave empty to allow all." + ) + + allowed_picking_type_ids = fields.Many2many( + 'stock.picking.type', + 'res_users_stock_picking_type_rel', + 'user_id', + 'picking_type_id', + string="Allowed Picking Types", + help="Picking Types this user is allowed to access. Leave empty to allow all." + ) + + allowed_location_ids = fields.Many2many( + 'stock.location', + 'res_users_stock_location_rel', + 'user_id', + 'location_id', + string="Allowed Locations", + help="Locations this user is allowed to access. Leave empty to allow all." + ) + + allowed_workcenter_ids = fields.Many2many( + 'mrp.workcenter', + 'res_users_mrp_workcenter_rel', + 'user_id', + 'workcenter_id', + string="Allowed Work Centers", + help="Work Centers this user is allowed to access. Leave empty to allow all." + ) + + allowed_approval_category_ids = fields.Many2many( + 'approval.category', + 'res_users_approval_category_rel', + 'user_id', + 'category_id', + string="Allowed Approvals", + help="Approval Categories this user is allowed to access. Leave empty to allow all." + ) diff --git a/models/restricted_models.py b/models/restricted_models.py new file mode 100644 index 0000000..b0df31b --- /dev/null +++ b/models/restricted_models.py @@ -0,0 +1,47 @@ +from odoo import models, api +from odoo.osv import expression + +class StockWarehouse(models.Model): + _inherit = 'stock.warehouse' + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + if not self.env.su and self.env.user.allowed_warehouse_ids: + domain = expression.AND([domain or [], [('id', 'in', self.env.user.allowed_warehouse_ids.ids)]]) + return super()._search(domain, offset=offset, limit=limit, order=order) + +class StockPickingType(models.Model): + _inherit = 'stock.picking.type' + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + if not self.env.su and self.env.user.allowed_picking_type_ids: + domain = expression.AND([domain or [], [('id', 'in', self.env.user.allowed_picking_type_ids.ids)]]) + return super()._search(domain, offset=offset, limit=limit, order=order) + +class StockLocation(models.Model): + _inherit = 'stock.location' + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + if not self.env.su and self.env.user.allowed_location_ids: + domain = expression.AND([domain or [], [('id', 'in', self.env.user.allowed_location_ids.ids)]]) + return super()._search(domain, offset=offset, limit=limit, order=order) + +class MrpWorkcenter(models.Model): + _inherit = 'mrp.workcenter' + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + if not self.env.su and self.env.user.allowed_workcenter_ids: + domain = expression.AND([domain or [], [('id', 'in', self.env.user.allowed_workcenter_ids.ids)]]) + return super()._search(domain, offset=offset, limit=limit, order=order) + +class ApprovalCategory(models.Model): + _inherit = 'approval.category' + + @api.model + def _search(self, domain, offset=0, limit=None, order=None): + if not self.env.su and self.env.user.allowed_approval_category_ids: + domain = expression.AND([domain or [], [('id', 'in', self.env.user.allowed_approval_category_ids.ids)]]) + return super()._search(domain, offset=offset, limit=limit, order=order) diff --git a/views/res_users_views.xml b/views/res_users_views.xml new file mode 100644 index 0000000..6ca0e38 --- /dev/null +++ b/views/res_users_views.xml @@ -0,0 +1,25 @@ + + + + res.users.form.inherit.access.restriction + res.users + + + + + + + + + + + + + + + + + + + +