fix(shopfloor): tablet tile grid includes shop-branch role holders
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user