fix(shopfloor): sudo cross-module reads in Plant Kanban _render_card

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) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-24 10:19:39 -04:00
parent e99cf20887
commit 31740b3949
3 changed files with 16 additions and 2 deletions

View File

@@ -218,6 +218,14 @@ Use only: `name`, `model_id`, `state`, `code` (or `function`/`model`), `interval
</templates>
```
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`.

View File

@@ -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.',

View File

@@ -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