feat(fusion_plating_shopfloor): fpRpc wrapper + env_for_tablet_tech helper (P6.3.1)
Client-side fpRpc() is a drop-in for rpc() that automatically injects tablet_tech_id from the tech_store into every action call. Read-only endpoints can keep using plain rpc(). Server-side env_for_tablet_tech(env, tablet_tech_id) returns an env scoped via with_user() when the id is a valid active user; otherwise returns the original env unchanged. Controllers call this at the top of action methods so all subsequent writes carry the right uid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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_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/`. |
|
| `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
|
## Removing menus/records — Odoo does NOT auto-delete orphans
|
||||||
Deleting a `<menuitem>` (or any `<record>`) 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 `<delete>` directive in a data file with `noupdate="0"`:
|
Deleting a `<menuitem>` (or any `<record>`) 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 `<delete>` directive in a data file with `noupdate="0"`:
|
||||||
```xml
|
```xml
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user