From df0de97a6840ef34457f09796bf16119332e9959 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 29 May 2026 00:35:40 -0400 Subject: [PATCH] =?UTF-8?q?docs(plating):=20spec=20=E2=80=94=20technician?= =?UTF-8?q?=20receiving=20+=20shipping=20from=20the=20workstation=20tablet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design for letting Technicians receive a confirmed order and ship a finished order from the fp_job_workspace tablet surface. Receiving is ACL-only (the panel + endpoints already exist); shipping adds a workspace panel + two sudo-backed endpoints (generate label, mark shipped) gated on all order jobs being awaiting_ship. Co-Authored-By: Claude Opus 4.7 --- ...nician-receiving-shipping-tablet-design.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md b/fusion_plating/docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md new file mode 100644 index 00000000..e3c36ef9 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md @@ -0,0 +1,219 @@ +# Technician Receiving + Shipping from the Workstation (Tablet) + +**Date:** 2026-05-29 +**Status:** Approved design — pending spec review → implementation plan +**Modules touched:** `fusion_plating_receiving`, `fusion_plating_logistics`, `fusion_plating_shopfloor` + +## Goal + +Let **Technicians** (`group_fp_technician`) receive a confirmed order and ship a finished +order directly from the workstation tablet (the `fp_plant_kanban` → `fp_job_workspace` +surface), instead of those actions being Shop-Manager+/back-office only. + +## Background — current state (verified 2026-05-29) + +- **Roles** (per `2026-05-23-permissions-overhaul`): Owner → Quality Manager → Manager → + Shop Manager v2 → Technician → Operator. Tablet writes are attributed via real per-tech + PIN sessions (`2026-05-24-tablet-pin-session-redesign`), so inside a tablet request + `request.env.user` **is the technician** — no `tablet_tech_id` plumbing. +- **On SO confirm, two things auto-happen:** one `fp.receiving` (draft, quantities + pre-filled) is created **per order** [`fusion_plating_receiving/models/sale_order.py` + `action_confirm`], and `fp.job`(s) are created grouped by part/recipe + [`fusion_plating_jobs/models/sale_order.py` `_fp_auto_create_job`]. Receiving is + **per-order**; the kanban shows **one card per job**. +- **Receiving is already built on the tablet.** `/fp/workspace/load` returns a `receivings` + payload, and endpoints already exist — `receiving_save_lines`, `receiving_mark_counted`, + `receiving_close`, `damage_create`, `damage_delete` + [`fusion_plating_shopfloor/controllers/workspace_controller.py:140-190, 482-624`]. The + OWL panel lives in `job_workspace.js/.xml/.scss` + `fp_damage_dialog.js`. **These + endpoints run as the current user (NOT sudo'd).** +- **`fp.receiving` state machine:** `draft` → `counted` (`action_mark_counted`, requires + `box_count_in`, calls `_update_so_receiving_status` which unblocks the order's workflow) → + `closed` (`action_close`) [`fp_receiving.py:1185-1231`]. +- **The blocker = ACL.** Technician is **read-only** on `fp.receiving` / `fp.receiving.line` + / `fp.receiving.damage` [`fusion_plating_receiving/security/ir.model.access.csv:2,5,8`] + and on `fusion.plating.delivery` + proof-of-delivery + chain-of-custody + [`fusion_plating_logistics/security/ir.model.access.csv:8,20,17`]. A technician hits + `AccessError` on every receiving write today. +- **Shipping internals:** the outbound FedEx label is generated **on the `fp.receiving` + record** via `action_generate_outbound_label` → `_fp_actually_generate_outbound_label` + (`carrier.send_shipping`, `stock.picking` synthesis, `fusion.shipment`, + `fp.outbound.package`, ZPL/PDF) [`fp_receiving.py:380-471`]; it requires `x_fc_carrier_id` + + `x_fc_weight` and takes a per-shipment service override via `x_fc_outbound_service_type`. + There are **no shipping endpoints on the tablet today.** `button_mark_shipped` + [`fp_job.py:2161`] flips `awaiting_ship` → `done` and has **no internal group gate** (it's + view-gated only). Jobs reach `awaiting_ship` per `2026-05-25-post-shop-cert-shipping`. + +## Locked decisions + +| # | Decision | +|---|---| +| D1 | **Receive UX:** detailed receiving panel inside the Job Workspace (reuse the existing one). | +| D2 | **Scope:** receiving **and** shipping tablet flows, both in this build. | +| D3 | **Ship action:** full self-serve — pick FedEx service → generate label → mark shipped. | +| D4 | **Multi-job order:** **shared** receiving (one per order); **ship only when ALL active jobs on the order are `awaiting_ship`**, then label + mark shipped together ("wait for all, ship together"). | +| D5 | **Delivery ACL:** also grant Technician write/create on the delivery-*completion* set (`fusion.plating.delivery`, `fp.proof.of.delivery`, `fp.chain.of.custody`). Route/vehicle/pickup *dispatch* records stay read-only for techs. | +| — | **Approach:** extend `fp_job_workspace` — no new client action, no kanban dialogs, no standalone dock app. | +| — | **Station scoping:** any technician may receive/ship; not gated to a paired station. ACL is the gate. | + +## Design + +### 1. Permissions (ACL changes only — no new groups) + +`fusion_plating_receiving/security/ir.model.access.csv` — flip `perm_write` + `perm_create` +`0 → 1` on the three Technician rows: +- `access_fp_receiving_operator` (`fp.receiving`) +- `access_fp_receiving_line_operator` (`fp.receiving.line`) +- `access_fp_receiving_damage_operator` (`fp.receiving.damage`) + +`fusion_plating_logistics/security/ir.model.access.csv` — flip `perm_write` + `perm_create` +`0 → 1` on: +- `access_fp_delivery_operator` (`fusion.plating.delivery`) +- `access_fp_proof_of_delivery_operator` (`fp.proof.of.delivery`) +- `access_fp_chain_of_custody_operator` (`fp.chain.of.custody`) + +Leave the vehicle / pickup / route / route-stop Technician rows **read-only**. + +**No** Technician ACL rows for `fusion.shipment` / `fp.outbound.package` / +`fp.label.generate.wizard` / `stock.*` — the shipping endpoint `sudo()`s that machinery +(see §3). `button_mark_shipped` writes `fp.job`, which Technicians already hold. + +**Version bumps:** `fusion_plating_receiving` + `fusion_plating_logistics` manifest versions +(ACL rows only reload on `-u` when the version changes). + +### 2. Receiving on the tablet (ACL-only; UI + endpoints already exist) + +**No code change beyond §1.** After the ACL flip: + +- Tech taps a card in the **Receiving** column → workspace opens → the existing receiving + panel renders from the `receivings` payload. +- Tech sets **Boxes Received** (`box_count_in`), adjusts per-line received qty/condition, + optionally logs damage with photos, taps **Mark Counted** (`action_mark_counted`: state + `draft → counted`, sets `received_by_id`/`received_date`, `_update_so_receiving_status` + flips the order to received so plating can begin) and **Close** when done. +- **Multi-job order (D4):** receivings come from `so.x_fc_receiving_ids` (one record per + order), so the panel is identical regardless of which sibling job's card was tapped; + counting/closing clears all sibling job cards from Receiving together. +- **Attribution:** the endpoints already run as `request.env.user` (the tech); + `received_by_id = env.user` and chatter is authored by the tech. No `sudo` — the + now-granted ACL is what makes the write legal. + +**Acceptance:** a user holding only `group_fp_technician` can call `receiving_save_lines` / +`receiving_mark_counted` / `receiving_close` / `damage_create` without `AccessError`. + +### 3. Shipping on the tablet (net-new) + +#### 3a. New OWL Shipping panel (`job_workspace.js` / `.xml` / `.scss`) + +Shown when `job.state == 'awaiting_ship'`. Sections: + +- **Order summary:** parts on the order, `x_fc_box_count_out` if set, existing + tracking #/label if already generated. +- **Readiness banner:** computed "are *all* active jobs on the order `awaiting_ship`?" If + not, the panel is **read-only** and lists the blocking jobs ("Waiting on: WO-00031 — + Plating"); both action buttons are disabled. +- **When ready — inputs:** Weight (numeric) + FedEx **Service tier** (selection from the + carrier's service list). Carrier resolves from a default (customer-level, else company + default), shown read-only with a manager-only override picker. *Exact default source is an + Open Item (§9).* +- **Buttons:** Generate Label, Mark Shipped. + +#### 3b. New endpoint `/fp/workspace/generate_label` + +Args: `job_id`, `weight`, `service_type` (+ optional `carrier_id` override). Logic (writes +as the tech; carrier machinery `sudo`'d): + +1. Resolve the order's `fp.receiving` (`job.sale_order_id.x_fc_receiving_ids`). +2. **Re-check the readiness gate** (all active jobs on the order `awaiting_ship`) → return + `{ok: False, error, not_ready: [...]}` if not. +3. Set `rec.x_fc_weight`, `rec.x_fc_outbound_service_type`, and `rec.x_fc_carrier_id` (if + provided/defaulted) — through the tech's now-granted `fp.receiving` write. +4. `rec.sudo()._fp_actually_generate_outbound_label()` — runs `carrier.send_shipping`, + picking synthesis, and shipment/package/attachment creation under sudo. +5. On success return the tracking # and label attachment id(s); route the PDF through + `fusion_pdf_preview` (`att.action_fusion_preview`) and expose the ZPL attachment for the + Zebra. +6. On API failure: catch and return + `{ok: False, error: 'Label generation failed: . Ask the office to generate it.'}` + — do **not** return the model's backend manual-wizard `act_window` (it can't render on + the tablet). The existing regen guard (label already attached) returns a clear + "label already exists" error. + +#### 3c. New endpoint `/fp/workspace/mark_shipped` + +Args: `job_id`. Logic: + +1. Resolve all active `fp.job` on the order (same `sale_order_id`, state not cancelled). +2. **Gate:** every such job must be `awaiting_ship` → else `{ok: False, error, not_ready: [...]}`. +3. `jobs.button_mark_shipped()` (as the tech; the method has no group gate) → each + `awaiting_ship` job → `done`, fires `job_shipped`, posts "Marked shipped by ``". +4. Return `{ok: True, shipped: [wo names]}`. + +Per D4, **both** label-gen and mark-shipped are gated on all-ready, so a multi-job order is +labelled and shipped as one unit; single-job orders pass the gate trivially. + +### 4. Kanban — no structural change + +Receiving + Shipping columns already populate (`no_parts` → receiving; `awaiting_ship` → +shipping). Tapping a card opens the workspace where the relevant panel now appears. +*(Deferred nicety: a "ready to ship" badge when all siblings are `awaiting_ship` — out of +scope.)* + +### 5. Attribution & security + +- PIN session ⇒ tech is `request.env.user`; receiving writes, `mark_shipped`, and chatter + are attributed to the tech. +- Only the carrier/shipment/picking/attachment machinery is `sudo`'d (privileged + inventory/shipping ops techs intentionally don't hold ACLs for). User-facing record + changes (receiving fields, job state) go through the tech's own granted ACL. +- `mark_shipped` does **not** trust any client bypass flag; the readiness gate is enforced + server-side. + +### 6. Error handling + +- New endpoints return `{ok, error}` (matches existing workspace endpoints); `UserError` → + `{ok: False, error: str}`. +- Label API failure surfaces a plain operator message; no backend wizard. +- The not-ready gate returns the list of blocking jobs so the panel can name them. + +### 7. Testing + +- **ACL:** as `group_fp_technician` only — `receiving_save_lines` / `mark_counted` / + `close` / `damage_create` succeed (regression: were `AccessError`); delivery / POD / + chain-of-custody create+write succeed. +- **generate_label** (mock carrier, or the `fixed` carrier manual path): returns tracking; + the regen guard blocks a second call. +- **mark_shipped:** blocked with a `not_ready` list until all order jobs are + `awaiting_ship`; once all ready, marks every job `done`. +- **Multi-job order, end-to-end:** receive once (clears all sibling cards) → finish all + jobs → ship-together. +- **Single-job order:** the readiness gate passes trivially. + +### 8. Out of scope / deferred + +- Standalone receiving-dock / shipping tablet apps (S30 / S32). +- Auto-linking `delivery.action_mark_delivered` → `button_mark_shipped` (noted as a future + hook in `fp_job.py`). +- Broadening tech ACL to route / vehicle / pickup *dispatch* records. +- Kanban "ready to ship" badge. + +### 9. Open items to confirm during planning + +- **Carrier default source** for the shipping panel: per-customer `x_fc_carrier_id` vs a + company default vs always-pick. Read the `fp.receiving` carrier/weight fields + + `fp.label.generate.wizard` defaults to decide. +- **Weight UoM** + whether a default weight can be pre-filled from the part catalog. +- Whether a single-job order should allow **generate + mark-shipped in one combined tap** + (UX nicety). + +### Files to touch + +- `fusion_plating_receiving/security/ir.model.access.csv` (3 rows) + `__manifest__.py` +- `fusion_plating_logistics/security/ir.model.access.csv` (3 rows) + `__manifest__.py` +- `fusion_plating_shopfloor/controllers/workspace_controller.py` (+2 endpoints; extend + `/load` with shipping payload + readiness) +- `fusion_plating_shopfloor/static/src/js/job_workspace.js` (shipping panel state/actions) +- `fusion_plating_shopfloor/static/src/xml/job_workspace.xml` (shipping panel markup) +- `fusion_plating_shopfloor/static/src/scss/job_workspace.scss` (panel styles, dark-mode + `@if $o-webclient-color-scheme == dark` branch) +- `fusion_plating_shopfloor/__manifest__.py`