# 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 + one `sudo()` — 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 + one `sudo()`; UI + endpoints already exist) **ACL flip (§1) + one `sudo()`.** `action_mark_counted` / `action_close` call `_update_so_receiving_status`, which writes `sale.order.x_fc_receiving_status` directly — a technician lacks `sale.order` write, so that internal denormalized-status write must be elevated (`rec.sale_order_id.sudo()…`). Without it the ACL flip alone still AccessErrors inside mark-counted. (Discovered during planning; the rest of the receiving UI + endpoints are untouched.) After both changes: - 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. The receiving-record writes go through the now-granted ACL; only the internal `sale.order` status write inside `_update_so_receiving_status` is `sudo()`'d (denormalized status, never user-entered). **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`