From 7cc9f961f0f7e71671c69d441f4cb7cacad96065 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Wed, 27 May 2026 09:29:00 +0700 Subject: [PATCH] first commit --- .gitignore | 5 ++++ README.md | 57 +++++++++++++++++++++++++++++++++++++ __init__.py | 1 + __manifest__.py | 21 ++++++++++++++ models/__init__.py | 2 ++ models/ir_ui_menu.py | 60 +++++++++++++++++++++++++++++++++++++++ models/res_users.py | 42 +++++++++++++++++++++++++++ views/res_users_views.xml | 22 ++++++++++++++ 8 files changed, 210 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 models/__init__.py create mode 100644 models/ir_ui_menu.py create mode 100644 models/res_users.py create mode 100644 views/res_users_views.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92baa50 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.py[cod] +*$py.class +.DS_Store +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..23e4411 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# POS & Kitchen Custom Access + +A custom Odoo 19 module designed to restrict menu visibility and streamline the interface for **Point of Sale (POS)** and **Kitchen Display (KDS)** users. It cleans up the user interface by hiding unrelated apps and administrative menus while ensuring standard security rules are respected. + +## 🌟 Key Features + +1. **User Settings Integration**: Adds **"Is POS User?"** and **"Is Kitchen User?"** checkboxes directly inside Odoo's default User form (under the *Access Rights* tab). +2. **Standard App Hiding**: When either restriction checkbox is checked, Odoo hides all unrelated root menus (such as *Discuss*, *Documents*, *CRM*, *Inventory*, etc.) from their navigation bar. +3. **POS Administrative Submenu Hiding**: When **"Is POS User?"** is checked: + * Only the Point of Sale **Dashboard** is visible. + * Administrative submenus like *Orders*, *Products*, *Reporting*, and *Configuration* are completely hidden. +4. **Kitchen Display Overview**: When **"Is Kitchen User?"** is checked: + * The **Kitchen Display** root menu is made accessible. + * We automatically reactivate the standard Odoo 19 Kitchen Display root menu (`pos_enterprise.menu_point_kitchen_display_root`) and open it up to the standard internal user group (`base.group_user`). +5. **Smart Group Provisioning**: + * To prevent "Access Error" messages or blank screens when a Kitchen User loads the KDS screen, our module automatically assigns the required underlying **"Point of Sale/User"** security group when **"Is Kitchen User?"** is checked. + * If both checkboxes are subsequently unchecked, the group is automatically removed to keep your security configuration pristine. +6. **Vanilla Behavior Preservation**: Standard, unrestricted non-manager users in the database still do *not* see the Kitchen Display root menu, strictly preserving default Odoo behavior. + +--- + +## 🛠️ Installation & Setup + +1. **Copy Module**: Ensure this directory (`pos_kitchen_custom_access`) is located within your Odoo custom addons directory. +2. **Update Addons List**: + * Activate developer mode in Odoo. + * Navigate to **Apps** and click **Update Apps List**. +3. **Install**: Search for `pos_kitchen_custom_access` and click **Activate** / **Upgrade**. +4. **Configure Users**: + * Navigate to **Settings > Users & Companies > Users**. + * Open the target user, scroll down to the **Access Rights** tab, and check **"Is POS User?"** and/or **"Is Kitchen User?"**. + * Save the form. The module automatically syncs group permissions in the background. + +--- + +## 📂 Codebase Architecture + +```bash +pos_kitchen_custom_access/ +├── __init__.py # Python initialization +├── __manifest__.py # Module manifest metadata and dependencies +├── .gitignore # Git ignore files +├── README.md # Documentation +├── models/ +│ ├── __init__.py # Python models registration +│ ├── ir_ui_menu.py # Extends _filter_visible_menus() to apply dynamic menu filtering rules +│ └── res_users.py # Extends res.users with boolean checkboxes & auto-syncing of POS group +└── views/ + └── res_users_views.xml # Inherits base.view_users_form to add UI fields and reactivates KDS root menu +``` + +--- + +## 💡 Technical Notes + +* **No Access Blocking**: This module strictly performs menu filtering to streamline the UI. It does not block backend controllers or standard API accesses, allowing background tasks and POS operations to run seamlessly. +* **ORM Cache Invalidation**: The write and create operations on user records automatically flush Odoo's registry and menu cache to ensure that changes in access rights are reflected on the next page refresh without requiring a server reboot. 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..49c4b73 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'POS & Kitchen User Custom Access', + 'version': '19.0.1.0.0', + 'category': 'Point of Sale', + 'summary': 'Restrict menu visibility for POS and Kitchen users', + 'description': """ + Hides all top navigation menus except: + - Point of Sale (for users marked as POS Users) + - Kitchen Display (for users marked as Kitchen Users) + + Does not block backend or API access. + """, + 'author': 'Suherdy Yacob', + 'depends': ['base', 'point_of_sale', 'pos_enterprise'], + 'data': [ + 'views/res_users_views.xml', + ], + 'installable': True, + 'application': False, + 'license': 'LGPL-3', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..716b818 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_users +from . import ir_ui_menu diff --git a/models/ir_ui_menu.py b/models/ir_ui_menu.py new file mode 100644 index 0000000..e0405e9 --- /dev/null +++ b/models/ir_ui_menu.py @@ -0,0 +1,60 @@ +from odoo import models, api + +class IrUiMenu(models.Model): + _inherit = 'ir.ui.menu' + + def _filter_visible_menus(self): + res = super()._filter_visible_menus() + user = self.env.user + + # Avoid restricting the superuser (id = 1) or public/portal users + if user.id == 1 or user.share: + return res + + is_pos = user.is_pos_user + is_kitchen = user.is_kitchen_user + + if not is_pos and not is_kitchen: + if not user.has_group('point_of_sale.group_pos_manager'): + kitchen_root = self.env.ref('pos_enterprise.menu_point_kitchen_display_root', raise_if_not_found=False) + if kitchen_root: + kitchen_menus = self.env['ir.ui.menu'].sudo().search([('parent_path', 'like', kitchen_root.parent_path + '%')]) + kitchen_menu_ids = set(kitchen_menus.ids) + kitchen_menu_ids.add(kitchen_root.id) + return res.filtered(lambda menu: menu.id not in kitchen_menu_ids) + return res + + allowed_ids = set() + + if is_pos: + pos_root = self.env.ref('point_of_sale.menu_point_root', raise_if_not_found=False) + if pos_root: + pos_menus = self.env['ir.ui.menu'].sudo().search([('parent_path', 'like', pos_root.parent_path + '%')]) + pos_menu_ids = set(pos_menus.ids) + pos_menu_ids.add(pos_root.id) + + # Submenus to hide: Orders, Products, Reporting, Configuration + exclude_xml_ids = [ + 'point_of_sale.menu_point_of_sale', + 'point_of_sale.pos_config_menu_catalog', + 'point_of_sale.menu_point_rep', + 'point_of_sale.menu_point_config_product' + ] + exclude_ids = set() + for xml_id in exclude_xml_ids: + menu = self.env.ref(xml_id, raise_if_not_found=False) + if menu: + descendants = self.env['ir.ui.menu'].sudo().search([('parent_path', 'like', menu.parent_path + '%')]) + exclude_ids.update(descendants.ids) + exclude_ids.add(menu.id) + + allowed_ids.update(pos_menu_ids - exclude_ids) + + if is_kitchen: + kitchen_root = self.env.ref('pos_enterprise.menu_point_kitchen_display_root', raise_if_not_found=False) + if kitchen_root: + kitchen_menus = self.env['ir.ui.menu'].sudo().search([('parent_path', 'like', kitchen_root.parent_path + '%')]) + allowed_ids.update(kitchen_menus.ids) + allowed_ids.add(kitchen_root.id) + + return res.filtered(lambda menu: menu.id in allowed_ids) diff --git a/models/res_users.py b/models/res_users.py new file mode 100644 index 0000000..02f4bd8 --- /dev/null +++ b/models/res_users.py @@ -0,0 +1,42 @@ +from odoo import models, fields, api + +class ResUsers(models.Model): + _inherit = 'res.users' + + is_pos_user = fields.Boolean(string="Is POS User?", default=False) + is_kitchen_user = fields.Boolean(string="Is Kitchen User?", default=False) + + @api.model_create_multi + def create(self, vals_list): + pos_user_group = self.env.ref('point_of_sale.group_pos_user', raise_if_not_found=False) + if pos_user_group: + for vals in vals_list: + if vals.get('is_kitchen_user') or vals.get('is_pos_user'): + group_commands = vals.get('group_ids', []) + if isinstance(group_commands, list): + group_commands.append((4, pos_user_group.id)) + vals['group_ids'] = group_commands + + res = super().create(vals_list) + if any(vals.get('is_pos_user') or vals.get('is_kitchen_user') for vals in vals_list): + self.env.registry.clear_cache() + return res + + def write(self, vals): + pos_user_group = self.env.ref('point_of_sale.group_pos_user', raise_if_not_found=False) + if pos_user_group and ('is_kitchen_user' in vals or 'is_pos_user' in vals): + for user in self: + is_kitchen = vals.get('is_kitchen_user', user.is_kitchen_user) + is_pos = vals.get('is_pos_user', user.is_pos_user) + if (is_kitchen or is_pos): + if pos_user_group not in user.group_ids: + super(ResUsers, user).write({'group_ids': [(4, pos_user_group.id)]}) + else: + if pos_user_group in user.group_ids: + super(ResUsers, user).write({'group_ids': [(3, pos_user_group.id)]}) + + res = super().write(vals) + if 'is_pos_user' in vals or 'is_kitchen_user' in vals: + self.env.registry.clear_cache() + return res + diff --git a/views/res_users_views.xml b/views/res_users_views.xml new file mode 100644 index 0000000..692224b --- /dev/null +++ b/views/res_users_views.xml @@ -0,0 +1,22 @@ + + + + res.users.form.inherit.pos.kitchen + res.users + + + + + + + + + + + + + + + + +