From 31740b3949b9e9c1bc40962ff755e79e8b34a5f7 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 24 May 2026 10:19:39 -0400 Subject: [PATCH] fix(shopfloor): sudo cross-module reads in Plant Kanban _render_card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-migration, Technicians (now group_fp_technician) have read on fp.job but NOT on sale.order / fp.part.catalog / fusion.plating.customer.spec. The kanban render path tries to access job.sale_order_id.x_fc_po_number and AccessErrors silently — kanban returns empty, user sees blank 'Shop Floor' page. Fix: `job = job.sudo()` at the top of _render_card. The output is denormalized display data, no security concerns; ACL gating is still enforced by the caller's access to fp.job (which Technician does have). CLAUDE.md rule 13m documents the broader pattern: any dashboard / tablet / kanban controller surfacing cross-module data to low-priv roles needs this sudo at the helper top. Module version: 19.0.32.0.8 -> 19.0.32.0.9 Co-Authored-By: Claude Opus 4.7 (1M context) --- fusion_plating/CLAUDE.md | 8 ++++++++ fusion_plating/fusion_plating_shopfloor/__manifest__.py | 2 +- .../fusion_plating_shopfloor/controllers/plant_kanban.py | 8 +++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index 660973b5..8ceac116 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -218,6 +218,14 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval ``` Reference: `/usr/lib/python3/dist-packages/odoo/addons/web/static/src/views/kanban/kanban_arch_parser.js`. Pre-existing `fp_rack_views.xml` still uses the old name and would also fail at render — fix when next touched. Caught 2026-05-24 by final reviewer of permissions-overhaul branch. +13m. **Tablet / kanban / dashboard controllers that surface DENORMALIZED cross-module data must `sudo()` the source recordset** at the top of the rendering helper. Low-privilege roles (Technician / Sales Rep) can read `fp.job` but NOT the cross-module fields it links to (sale.order, fp.part.catalog, fusion.plating.customer.spec, etc.) — naive `job.sale_order_id.x_fc_po_number` AccessErrors at render time and the kanban returns empty. The output is safe-to-expose display data; ACL gating is enforced by the CALLER's access to fp.job itself. Pattern: + ```python + def _render_card(job, paired): + job = job.sudo() # cross-module reads now bypass ACL + so = job.sale_order_id # was AccessError for Technician + ... + ``` + 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). 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`. diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index a7dd2455..7158e071 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.8', + 'version': '19.0.32.0.9', '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/plant_kanban.py b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py index c0f06ac2..fa477012 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/plant_kanban.py @@ -172,13 +172,19 @@ def _resolve_card_area(job): def _render_card(job, paired): """Build the full card payload for one fp.job.""" + # Sudo the job recordset so cross-module field reads (sale.order, + # fp.part.catalog, fusion.plating.customer.spec) don't AccessError + # for low-privilege roles like Technician. The output is denormalized + # display data; the underlying record visibility is controlled by the + # caller's fp.job ACL (Technician can read all jobs). + job = job.sudo() step = job.active_step_id try: timeline = json.loads(job.mini_timeline_json or '[]') except (TypeError, ValueError): timeline = [] - # Cross-module field probes + # Cross-module field probes (sudo'd via job.sudo() above) part = job.part_catalog_id if 'part_catalog_id' in job._fields else None spec = job.customer_spec_id if 'customer_spec_id' in job._fields else None so = job.sale_order_id