From 6601b5783319d5532d49b80804b0b71cf337de77 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 5 Jan 2026 12:03:16 +0700 Subject: [PATCH] first commit --- README.md | 38 ++++++++++++++ __init__.py | 1 + __manifest__.py | 26 ++++++++++ __pycache__/__init__.cpython-312.pyc | Bin 0 -> 214 bytes models/__init__.py | 2 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 265 bytes models/__pycache__/res_users.cpython-312.pyc | Bin 0 -> 1862 bytes .../restricted_models.cpython-312.pyc | Bin 0 -> 4624 bytes models/res_users.py | 49 ++++++++++++++++++ models/restricted_models.py | 47 +++++++++++++++++ views/res_users_views.xml | 25 +++++++++ 11 files changed, 188 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/res_users.cpython-312.pyc create mode 100644 models/__pycache__/restricted_models.cpython-312.pyc create mode 100644 models/res_users.py create mode 100644 models/restricted_models.py create mode 100644 views/res_users_views.xml 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 0000000000000000000000000000000000000000..06b8be22d262e4e694d9f0dd0561365902b60770 GIT binary patch literal 214 zcmXwzF$%&!5Jh(q2qFY5^bl!W>_o&Ph+wD1qPq)ONHVgsDfT3u!LxXS6gF0NLOLra zpud^-KQsKf=lNK$($&$;cs>X7SMq~?!f+Kv7^p-I2LKV1B7FhJ1$0@B##Xc~l1})P zvzzFL%gV4u47Bfv0znj1G?A#UVMT|ch$lp;FU5-Nu5>HiozZ%)*q7uDyoDbig@!5} z5*-!3Bup`$v22g`lgSeZ9$r5DO6sXMcVz$K@+{&DELg0e!4k38ndq#{5K>+Yeo#gV zJdA>4-LVTSW-2BIZ=$ohA?s>>EAj(`TxP>SXa^TzxR$D-#wqS97i?1RDOS`P*-&JO zmMdjCj?`?drIyvk7{{#d$33$I}91BV^p#E~AkqVb0yaUrL?dM_Yx;x?({vJ=m1$EjP?%jUzI=f~%L^7Z@U@$b4m zhv2Dy^3YwZBJ{U(@~1K}j<1996C#Kx9`cp0;+MK51(ncEL`qK)DO2S!%su-&D`%0e z8mpbkKho)xMAZ)n^_ZxB<5G_>r#MTSILAnle@IbRA*fp-O1E4>RDGOEElxlnfRu?! zDx^wge$}2$WOS?N^0Qz`sG3$Z~<#&gI`EE5O&uD!EYLXV2vu zWDWRrvO$^!elBixHidSVvU`k%ESU`{Gl!{JV^QGjn}>EtdjW(*3vxKk;WUM(DI8Ky z%*YgWiC8#^yChk@;d#L!CB}oi5KBHgv5lzbGDA)^;Mbn8QDE4PLm4xTThux0W>M(&9|^Tby+N{^2_Cr4J_OAu9#CPN)jY{!nN4NaPDMa)Jr{f@D`b2L zC`)dha$j9`vY5&A0>_Trpf8$dCFYq_-u%=9X-m1ZTa(Ew*_oQJBgqd3=3x--JG39s zuqX*K_#~4`(hEL_TFFH{fC9!&T7f0IMejAFE!l%X7#!H13Cl$v1z{}OCkvJ*ah}Z9 zZ0Z%)?8c-JOZIZp*vV?Tl)bvd+nsr#VcfTUim{l(7#h#ellB6}-wthWlF_i+@6phW zLKRwznBBAc@drK(q7KJdUXj9uatRZSJbdPaGO?0p3WtM<)jWD?f0$_H@&B(D{LL@u zSa!OX53OF{Q;Q8jBZ;j$anuX?4)jnfAVKi{$CfjMgX`Ob1bt>@w-Z)d-atp5WBhfT zHU}%~yVi7*%t0K=c9{;cRkPn-PZL1Z*9!tq*_R9bdXOx^#_e8(ixY zs(l66R&}Jl6}IzNyWOb>RjySiUozX|%AWiPWv{;}qbY5G^-}vZJ15t}i{Qd{2(bnX c&&!IU{E6BxR8y&bfAHgzAD;Y+z)WZU2ctkj2LJ#7 literal 0 HcmV?d00001 diff --git a/models/__pycache__/restricted_models.cpython-312.pyc b/models/__pycache__/restricted_models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4a26e384727cb5b02d8c81c2cbc4615e1f92c41 GIT binary patch literal 4624 zcmeHLL2T1j7=E^sG%2(x&~)8KmyUJXqDjf7<}hi~G{b-iL8(*(B12f7{hBy&;_&RW zNeO~UNVJnC*27ddG3`WE4m)xjCnoLUw1-R<(~x$WcA3^rLY(&hc3fx$jg+;wJ}JN7 z|NDP_|NH;%d-lsOJw4q7+F#-q+IX0dKTz>ou+>@p2RaKxB`T+rBA4Nce1_*x=XIeN z$OJgTljB4ct`jvtxxD*C{~_LWN${?jkn0%)&rloBF4waQJi~1~!>(rpJiFU?MqJN5 z;JLSrXLqe9(fcO`o#35N(NL*wI)Yr*5}eabt7S$_Q!`46w=7QvTFUBS=-hyeQIg?Q zlHpY@BhbK$@}df}WRg?)>yf3_;7n+iRJlyo1W5#(-U-W43g=`-b4JCaGf@-uzILa#?-}){X57XZ3nLLgs>X^39us)VVre7wUm~!cuEZ6J#+k z=J1*dzfwYQLdN8zNi8R+Yefw%22&|ZL>z&ZrX1d^I02{tb7Hct8`D&k){7u%s>!er zjv$VG<_MrVL9lGE;JkAtzu`ebNc3Mp4#E4A;PVP++7^M3NL;Hcd$~F}uUq zu02&!IipBZW+g|NT1$=BtQ;T?pqMgL!}#FClu`ja6lGO4N@hw{6u_hez_ggASO8Ax z%bJ8Sr#$$k{96#bXGxZ8Yyjq)8Q6%kWGn10xMw1Uo?Ii3qX%wQ=c+d^e|`Cub*Fl} zdgsdRE6w=f<@n)8vG~Hp`HSD5{o%r0_T!oR>7V)^4n7!sIP_qsDXI-oZHl>un6uNG zJ({;i3M(;v_SmCn|J?N4)mu|{C+{EqIW~KBC3@NppI(L8$fCdyq+?@?VJ9p}CAml? z$?29b?23;4y^?gPBI~U?VM!}Njx>v5ZJnUII(KDGPb;$u)pbc)lk4Qy=*#X)=$7=#HpFaGEHuP|o!0HqqCHYti5V}YmxtXU z4EPNcZ(|70vEdywqQJfB85`be#|9j-!))kr(|FQQWZc_kdOa%WEq#{;PyuM$N`ro% z27Z=1r9f;$?R@;d3h}nVzqlM1x5U5F5S6BgEU?o#dsMSW@++}I$M}B_LSiV8xvRq< z+_yoz9sY61PUHU+E1xr1L7^p!vYFl@D<`Md^7Lmi-PE6#^}ocrmP{B(B;_rcAj^bwC6WgEnwSdO3A z67hON)SF_tA(rj*C3}?FBj!rX>KO592#KM17h@X+;qLi|+U@x`WGu1A3E*jr37~n$ zk1*;oSZ+|#^R3p*L{vl^Zx)T#)1t1 literal 0 HcmV?d00001 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 + + + + + + + + + + + + + + + + + + + +