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>
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_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 requestrequest.env.useris the technician — notablet_tech_idplumbing. - On SO confirm, two things auto-happen: one
fp.receiving(draft, quantities pre-filled) is created per order [fusion_plating_receiving/models/sale_order.pyaction_confirm], andfp.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/loadreturns areceivingspayload, 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 injob_workspace.js/.xml/.scss+fp_damage_dialog.js. These endpoints run as the current user (NOT sudo'd). fp.receivingstate machine:draft→counted(action_mark_counted, requiresbox_count_in, calls_update_so_receiving_statuswhich 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 onfusion.plating.delivery+ proof-of-delivery + chain-of-custody [fusion_plating_logistics/security/ir.model.access.csv:8,20,17]. A technician hitsAccessErroron every receiving write today. - Shipping internals: the outbound FedEx label is generated on the
fp.receivingrecord viaaction_generate_outbound_label→_fp_actually_generate_outbound_label(carrier.send_shipping,stock.pickingsynthesis,fusion.shipment,fp.outbound.package, ZPL/PDF) [fp_receiving.py:380-471]; it requiresx_fc_carrier_idx_fc_weightand takes a per-shipment service override viax_fc_outbound_service_type. There are no shipping endpoints on the tablet today.button_mark_shipped[fp_job.py:2161] flipsawaiting_ship→doneand has no internal group gate (it's view-gated only). Jobs reachawaiting_shipper2026-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
receivingspayload. - Tech sets Boxes Received (
box_count_in), adjusts per-line received qty/condition, optionally logs damage with photos, taps Mark Counted (action_mark_counted: statedraft → counted, setsreceived_by_id/received_date,_update_so_receiving_statusflips 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.userand chatter is authored by the tech. The receiving-record writes go through the now-granted ACL; only the internalsale.orderstatus write inside_update_so_receiving_statusissudo()'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_outif 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):
- Resolve the order's
fp.receiving(job.sale_order_id.x_fc_receiving_ids). - Re-check the readiness gate (all active jobs on the order
awaiting_ship) → return{ok: False, error, not_ready: [...]}if not. - Set
rec.x_fc_weight,rec.x_fc_outbound_service_type, andrec.x_fc_carrier_id(if provided/defaulted) — through the tech's now-grantedfp.receivingwrite. rec.sudo()._fp_actually_generate_outbound_label()— runscarrier.send_shipping, picking synthesis, and shipment/package/attachment creation under sudo.- 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. - 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-wizardact_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:
- Resolve all active
fp.jobon the order (samesale_order_id, state not cancelled). - Gate: every such job must be
awaiting_ship→ else{ok: False, error, not_ready: [...]}. jobs.button_mark_shipped()(as the tech; the method has no group gate) → eachawaiting_shipjob →done, firesjob_shipped, posts "Marked shipped by<tech>".- 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_shippeddoes 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_technicianonly —receiving_save_lines/mark_counted/close/damage_createsucceed (regression: wereAccessError); delivery / POD / chain-of-custody create+write succeed. - generate_label (mock carrier, or the
fixedcarrier manual path): returns tracking; the regen guard blocks a second call. - mark_shipped: blocked with a
not_readylist until all order jobs areawaiting_ship; once all ready, marks every jobdone. - 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 infp_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_idvs a company default vs always-pick. Read thefp.receivingcarrier/weight fields +fp.label.generate.wizarddefaults 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__.pyfusion_plating_logistics/security/ir.model.access.csv(3 rows) +__manifest__.pyfusion_plating_shopfloor/controllers/workspace_controller.py(+2 endpoints; extend/loadwith 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 == darkbranch)fusion_plating_shopfloor/__manifest__.py