diff --git a/fusion_plating/CLAUDE.md b/fusion_plating/CLAUDE.md index a20171a4..e0708235 100644 --- a/fusion_plating/CLAUDE.md +++ b/fusion_plating/CLAUDE.md @@ -166,6 +166,19 @@ These modules have **source code in this repo** but are **intentionally NOT inst | `fusion_plating_culture` | `state=uninstalled`, dir removed from entech disk | Soft people-ops feature (peer kudos / "Fundamental of the Week"); zero data entered; not a client priority. Top-level "Culture" menu confused operators. | Ask the client whether they want it before reinstalling. If yes: re-sync folder + `-i fusion_plating_culture` + seed a value set. | | `fusion_plating_sensors` | deleted entirely (not in repo anymore) | Duplicated `fusion_plating_iot`'s scope but with no working alerting logic. Its valuables (sensor_type taxonomy, dashboard, location flexibility) were ported into `fusion_iot/fusion_plating_iot/`. | N/A — gone. Any new sensor work goes in `fusion_iot/fusion_plating_iot/`. | +## Shop-floor action endpoints — credit the correct tech via `tablet_tech_id` +The tablet sits on a long-lived "shopfloor service" Odoo session shared by many techs. The actual tech-of-record is established via the PIN unlock (Phase 6); their id lives in the OWL `fp_shopfloor_tech_store` service and is sent as `tablet_tech_id` on every action RPC. + +When writing a NEW shop-floor controller endpoint that **writes** (creates a record, calls a `button_*` method, posts to chatter): +1. Add `tablet_tech_id=None` as a kwarg on the route handler. +2. At the top, call: `env = env_for_tablet_tech(request.env, tablet_tech_id)` (from `fusion_plating_shopfloor/controllers/_tablet_audit.py`). +3. Use `env` (not `request.env`) for all subsequent writes. `env.with_user(...)` is applied internally so `create_uid` / `write_uid` / chatter authorship carry the right uid. +4. Read-only endpoints (load / kanban / funnel / overview) don't need this — leave them as `request.env`. + +On the client side: use `fpRpc()` from `services/fp_rpc.js` (drop-in for `rpc()`) for action calls. It auto-injects `tablet_tech_id`. Read calls can keep using plain `rpc()`. + +If `tablet_tech_id` is missing or invalid, `env_for_tablet_tech` falls back to the session uid — old callers and pre-Phase-6.3 endpoints continue working. + ## Removing menus/records — Odoo does NOT auto-delete orphans Deleting a `` (or any ``) from a data XML file does NOT remove the corresponding database row. The XML loader only updates records it sees; orphans persist in `ir.ui.menu` / `ir.model.data` until you delete them explicitly. Symptom: the menu still appears in the UI after `-u`. Fix — add a `` directive in a data file with `noupdate="0"`: ```xml diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py b/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py new file mode 100644 index 00000000..cf73da5d --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/controllers/_tablet_audit.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +"""Helper for audit-credit propagation (Phase 6.3 tablet redesign). + +Controllers that accept an optional `tablet_tech_id` kwarg use this +helper to switch their `env` to the tech-of-record before performing +writes. The result: chatter posts + create_uid/write_uid carry the +unlocked tech's identity, not the tablet's persistent session user. +""" +import logging + +_logger = logging.getLogger(__name__) + + +def env_for_tablet_tech(env, tablet_tech_id): + """Return an env scoped to `tablet_tech_id` if it's a valid user; + otherwise return the original env unchanged. + + Validation: the user must exist and be active. We deliberately do + NOT cross-check that they actually unlocked recently — the OWL + component is the source of truth for "who's at the tablet right + now", and the only path that produces a tablet_tech_id is a + successful /fp/tablet/unlock followed by an active session in the + OWL tech_store. + """ + if not tablet_tech_id: + return env + try: + tech_id = int(tablet_tech_id) + except (TypeError, ValueError): + return env + User = env['res.users'].sudo() + tech = User.browse(tech_id) + if not tech.exists() or not tech.active: + _logger.warning( + "tablet_tech_id %s invalid (not found or inactive); " + "falling back to session uid %s", + tablet_tech_id, env.uid, + ) + return env + return env(user=tech_id) diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js new file mode 100644 index 00000000..82c9e890 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/services/fp_rpc.js @@ -0,0 +1,42 @@ +/** @odoo-module **/ +// ============================================================================= +// Fusion Plating — fpRpc() wrapper +// +// Drop-in replacement for the standard `rpc()` import. Automatically +// injects the current tablet_tech_id from the tech_store into every +// call, so server-side endpoints can attribute the action to the right +// user via env.with_user() (see env_for_tablet_tech in +// controllers/_tablet_audit.py). +// +// USE for any RPC that WRITES (start step, finish step, hold create, +// sign-off, milestone advance). For read-only loads (kanban, workspace +// load, manager funnel), plain rpc() is fine. +// +// Example: +// import { fpRpc } from "../services/fp_rpc"; +// await fpRpc("/fp/shopfloor/start_wo", { workorder_id: stepId }); +// +// ============================================================================= + +import { rpc as baseRpc } from "@web/core/network/rpc"; + +function _getTechStore() { + // Lazy-resolve via the global debug API — avoids circular service init + try { + const env = odoo.__WOWL_DEBUG__?.root?.env; + if (env && env.services && env.services.fp_shopfloor_tech_store) { + return env.services.fp_shopfloor_tech_store; + } + } catch (e) { + // ignore + } + return null; +} + +export function fpRpc(url, params = {}) { + const techStore = _getTechStore(); + if (techStore && techStore.currentTechId) { + params = { ...params, tablet_tech_id: techStore.currentTechId }; + } + return baseRpc(url, params); +}