Truck Pick — Technical Spec
1. Routes
app/ro_pick.py, reuses Pick Item's session tables and save/lookup endpoints unchanged. Only the find step and the post dispatch are new.
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /ro/pick-truck?sku=&saved= | Handheld (ro_pick_truck.html) | signed in, Save gated by can_write |
| GET | /ro/pick-truck-full?sku=&saved= | Desktop (ro_pick_truck_full.html) | same |
| POST | /ro/pick-truck/find | Resolve job (RO/FP/customer text or qb_customer_id+qb_customer_name) AND truck_customer_id; stamps both onto the ro_pick row | require_write |
| GET | /ro/trucks/suggest?q= | Truck # typeahead — SA Vehicles sub-jobs only | signed in |
| POST | /ro/pick-item/save, /save-batch, GET /ro/pick-item/lookup | Shared, unchanged | require_write / signed in |
| POST | /ro/pick/leave, /ro/pick/{pick_id}/class | Shared, unchanged (redirect_to whitelist includes /ro/pick-truck[-full]) | require_write |
Truck picks are always no-RO (ro_pick.order_no IS NULL) — pick-truck/find never opens an RO session's truck fields; if job_number resolves to an RO, get_or_open_pick is used and the truck columns are stamped onto that RO-backed pick row too (allowed, but not the expected path).
2. Data model
ro_pick gained two nullable columns (migration run 2026-09-20, no DDL file — ad hoc ALTER TABLE via the RO_DSN connection):
``sql ALTER TABLE ro_pick ADD COLUMN truck_customer_id text; ALTER TABLE ro_pick ADD COLUMN truck_customer_name text; ``
A pick is a "Truck pick" purely by truck_customer_id IS NOT NULL — no separate type/flag column. load_session() in ro_post.py already SELECT p.* so both columns are available for free.
Trucks are QuickBooks sub-jobs under customer SA Vehicles (Id 100000431), seeded 2026-09-20 with the 11 active vehicles; Notes carries the description ("2026 Ford F-250"). search_trucks() / resolve_truck() in ro_pick.py match on FullyQualifiedName LIKE 'SA Vehicles:%' — QuickBooks' query language rejects filtering on ParentRef directly (QueryValidationError), same constraint search_qb_jobs() already worked around for the general job typeahead. resolve_truck() re-checks the client-supplied id server-side before it is trusted (never assume the client's typeahead selection wasn't tampered with).
3. Posting (ro_post.py::post_truck)
Dispatched from post_pick_to_qb() in ro_pick.py, which reads truck_customer_id off the pick row before choosing post() (normal picks) vs post_truck().
Because the truck's "stock" is never really on hand, every touched line becomes a mirrored pair of $0 QuickBooks documents instead of one document against a single customer:
| Line qty | Truck-side document | Job-side document |
|---|---|---|
> 0 (issued) | CreditMemo on truck_customer_id | Invoice on qb_customer_id |
< 0 (return) | Invoice on truck_customer_id | Invoice→CreditMemo swap: CreditMemo on qb_customer_id |
Posting order per pair is truck-side document first. If the job-side document then fails, the truck-side document is deleted via qbo.delete_doc() before the error is raised — mirrors the existing CM-before-invoice rollback rule in post() (Dave, 2026-09-06) so nothing is ever left half-posted across two customers. ro_txn rows are tagged source='Workbench-Truck' so truck movements are distinguishable from ordinary pick movements in the ledger; the unique index (qb_doc_type, qb_doc_id, sku) is respected because every document has its own qb_doc_id.
zeroed/surplus handling is unchanged from post() (truck picks have no ro_detail row to update in practice, since they are always no-RO, but the code path is identical for parity).
4. Review screen
_pending_pick_rows() and the posted-status query in picks_review_screen() both select p.truck_customer_name; ro_picks.html renders a small "TRUCK <name>" tag ahead of the job name when it is set.
5. Truck-tech menu
Static page /var/www/im2docs/truck.html, served by nginx at https://logistics.solalt.com/truck (location = /truck added to /etc/nginx/sites-enabled/im2, alongside the existing /handheld alias). Lists only Truck Pick and Rush Material Request — deliberately not merged into the warehouse handheld.html (Dave, 2026-09-20: "The techs in trucks will need their own menu with just this truck pick and the rush material request options").
6. Verified 2026-09-20
search_trucks/resolve_truck tested live against QuickBooks (11 trucks resolve, bogus id rejected). post_truck(dry_run=True) tested against a temporary ro_pick/ro_pick_line fixture (inserted and deleted directly in the ro database, never posted to QuickBooks) — correctly classified 1 issued line. End-to-end /ro/pick-truck/find → /ro/pick-truck tested authenticated as dcantin, confirmed the job bar reads "Truck HT6 · SA:Admin" and the ro_pick row carries both customer ids; test session deleted afterward (no lines were saved). Live QuickBooks posting itself (post_truck without dry_run) has NOT been exercised against production — first real Truck pick should be watched end-to-end before relying on it unattended.