Files
Odoo-Modules/fusion_plating/docs/superpowers/specs/2026-05-29-technician-receiving-shipping-tablet-design.md
gsinghpal b98ee8a6fb docs(plating): implementation plan for technician receiving + shipping tablet
Bite-sized TDD plan across receiving ACL+sudo, delivery ACL, fp.job
ship-readiness helpers, shipping endpoints, and the workspace shipping
panel. Also patches the spec to record the sale.order status-write sudo
fix found during planning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 00:50:23 -04:00

13 KiB

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_kanbanfp_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: draftcounted (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_shipdone 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: <reason>. 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 <tech>".
  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_deliveredbutton_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