From 839a7f0abc61610e35b6e001a912758d38ed4f66 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 10:47:01 -0400 Subject: [PATCH] fix(shopfloor): tablet tile grid includes shop-branch role holders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously only direct Technicians appeared on the lock-screen tile grid because env.ref('group_fp_technician').user_ids returns DIRECT members only — Odoo's implication chain (Owner -> ... -> Technician) is read-time only, not stored in res_groups_users_rel. Search res.users with ('groups_id', 'in', shop_branch_ids) where shop_branch_ids covers all 5 shop-branch role groups (Technician, Shop Manager v2, Manager, Quality Manager, Owner). Sales branch intentionally excluded — they don't operate the tablet. Verified on entech: 18 technicians + 1 shop_manager + 2 managers + 1 quality_manager + 2 owners = 24 tiles (was 18). CLAUDE.md rule 13l corrected — previous version wrongly claimed res.groups.user_ids surfaced implied members. Now documents the search-based query as the canonical 'enumerate role X or higher' pattern. Module version: 19.0.32.0.11 -> 19.0.32.0.12 Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/CLAUDE.md | 24 ++++++++++- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../controllers/tablet_controller.py | 40 +++++++++++++------ 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 8ceac116..447d0862 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -226,7 +226,29 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval ... ``` Caught 2026-05-24 when Technicians saw an empty Shop Floor kanban post-migration (log: `Access Denied by ACLs ... model: sale.order`). Same pattern likely needed in any controller returning a job-centric card payload to a non-Manager user. -13l. **Post-migration: `env.ref('old_group_xmlid').user_ids` returns empty** even though the old group still exists. Phase H's migration moves users OFF the old (now-`[DEPRECATED]`) groups onto the new ones. Old groups are still reachable via the new group's `implied_ids` (so old ACLs still resolve for backward-compat), but NO USER directly holds the old group anymore. Any code doing `env.ref('fusion_plating.group_fusion_plating_operator').user_ids` to enumerate "operators" gets an empty recordset. **Fix: point at the new group xmlid (`group_fp_technician`).** `res.groups.user_ids` includes both direct AND implied-via-other-group members, so higher roles (Shop Manager, Manager, QM, Owner) appear via implication. The Phase G `has_group()` sweep didn't catch these — `env.ref(...).user_ids` is a different access pattern. Caught 2026-05-24 when the tablet lock screen showed "No operators configured" post-migration. Audit for other instances: `grep -rn "env\.ref.*group_fusion_plating_" --include='*.py'` (skip test files which intentionally reference old xmlids to verify backward-compat). +13l. **`res.groups.user_ids` returns DIRECT members only — implied/transitive memberships are NOT stored in `user.groups_id`**. When you `user.write({'groups_id': [(4, owner_group.id)]})`, Odoo adds JUST the Owner group to the user — it does NOT cascade and write the implied Manager/Shop Manager/Technician group rows into res_groups_users_rel. The implication chain is resolved at READ time by `has_group()` and `_get_trans_implied_groups()`, but NOT materialized in storage. So: + - `env.ref('fusion_plating.group_fp_technician').user_ids` returns ONLY the 18 direct Technicians, NOT the Owners/QMs/Managers/Shop Managers who reach Technician via implication. + - `env.ref('fusion_plating.group_fusion_plating_operator').user_ids` (the deprecated group) returns EMPTY post-migration for the same reason — no user holds it directly. + + **Fix for "enumerate everyone with role X or higher":** search `res.users` directly with the union of group ids: + ```python + shop_branch_ids = [env.ref(x).id for x in ( + 'fusion_plating.group_fp_technician', + 'fusion_plating.group_fp_shop_manager_v2', + 'fusion_plating.group_fp_manager', + 'fusion_plating.group_fp_quality_manager', + 'fusion_plating.group_fp_owner', + )] + users = env['res.users'].sudo().search([ + ('groups_id', 'in', shop_branch_ids), + ('share', '=', False), ('active', '=', True), + ]) + ``` + `('groups_id', 'in', [...])` is the right operator — it matches the DIRECT membership rel table without trying to follow implications. Since each user has exactly one primary plating role (Phase F `x_fc_plating_role` Selection enforces this), this returns every shop-branch user with no duplicates. + + For `has_group`-style intent ("does this single user have role X or any role that implies X"), use `user.has_group('fusion_plating.group_fp_X')` — that DOES follow the implication chain at read time. + + Caught 2026-05-24 in two waves: (1) tablet lock screen showed "No operators configured" because it queried the deprecated `group_fusion_plating_operator.user_ids`; (2) after fixing to `group_fp_technician.user_ids`, it still missed Owners/Managers because implication chains don't populate `user_ids`. Final fix: search-based query across the 5 shop-branch role ids. Audit for other instances: `grep -rn "env\.ref.*\.user_ids\b" --include='*.py'` (skip test files which intentionally exercise backward-compat). 13k. **Custom fields on `res.users` must be added to `SELF_WRITEABLE_FIELDS` (and often `SELF_READABLE_FIELDS`) or non-admin users can't save their own Preferences dialog**. Odoo 19's User Preferences dialog goes through `res.users.write` on the user's own record — Odoo bypasses the standard write ACL ONLY IF every field being written is in `SELF_WRITEABLE_FIELDS`. Any unknown field forces fallback to the standard ACL (admin-only on entech) → `AccessError: You are not allowed to modify 'User' records. Required group: Access Rights`. **In Odoo 19, `SELF_WRITEABLE_FIELDS` and `SELF_READABLE_FIELDS` are `@property`-decorated methods, NOT class attributes.** Extend via super(), not list concatenation on `models.Model.SELF_*` (that AttributeErrors at module load — Model base doesn't define them, only res.users does). Canonical pattern (matches hr/res_users.py and mail/res_users.py): diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 3f534cb9..6c9ca722 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.32.0.11', + 'version': '19.0.32.0.12', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, ' 'first-piece inspection gates.', diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py index ac3eadaf..6b88ebc8 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/tablet_controller.py @@ -217,22 +217,36 @@ class FpTabletController(http.Controller): @http.route('/fp/tablet/tiles', type='jsonrpc', auth='user') def tiles(self, station_id=None): env = request.env - # Phase 1 permissions overhaul (2026-05-24): post-migration users - # hold the new group_fp_technician (old group_fusion_plating_operator - # is still reachable via implication but DIRECT memberships moved to - # the new group). Point the tile grid at Technician — higher roles - # (Shop Manager / Manager / QM / Owner) imply Technician and so - # are included automatically via res.groups.user_ids (which surfaces - # both direct AND implied memberships). - op_group = env.ref( + # Phase 1 permissions overhaul (2026-05-24): show everyone on the + # SHOP branch — Technician, Shop Manager, Manager, Quality Manager, + # Owner. Managers/Owners need to "chip in" on the floor occasionally. + # Search-based query because res.groups.user_ids returns DIRECT + # memberships only — implied groups (Owner → ... → Technician) don't + # get stored in user.groups_id by Odoo's group-write propagation. + # OR across the 5 shop-branch role group ids; sales-branch users + # (Sales Rep / Sales Manager directly held without any shop-branch + # role) are intentionally excluded — they don't operate the tablet. + shop_branch_xmlids = ( 'fusion_plating.group_fp_technician', - raise_if_not_found=False, + 'fusion_plating.group_fp_shop_manager_v2', + 'fusion_plating.group_fp_manager', + 'fusion_plating.group_fp_quality_manager', + 'fusion_plating.group_fp_owner', ) - if not op_group: - return {'ok': False, 'error': 'technician group missing'} + group_ids = [ + g.id for g in ( + env.ref(x, raise_if_not_found=False) for x in shop_branch_xmlids + ) if g + ] + if not group_ids: + return {'ok': False, 'error': 'shop-branch role groups missing'} - # Determine candidate users — station roster wins if non-empty - users = op_group.user_ids + Users = env['res.users'].sudo() + users = Users.search([ + ('groups_id', 'in', group_ids), + ('share', '=', False), + ('active', '=', True), + ]) if station_id: Station = env['fusion.plating.shopfloor.station'] station = Station.browse(int(station_id))