← All manuals Operator Guide for this screen IM2

Pick Item — Technical Spec

Pick Item (handheld) + Pick Item Full Screen (PC)
Technical Spec Ver 2 Updated 2026-09-06 Matches the live screen

1. Routes

app/ro_pick.py, same module and same session tables as Pick List (see pick_list tech spec §2).

MethodPathPurposePermission
GET/ro/pick-item?sku=&saved=Handheld (ro_pick_item.html, recent_limit=5)signed in, Save gated by can_write
GET/ro/pick-item-full?sku=&saved=Desktop (ro_pick_item_full.html, recent_limit=None, include_bom_lines=True)same
POST/ro/pick-item/findResolve job text, set the cookiesigned in
POST/ro/pick-item/saveSave one line (sku, optional line_key, qty, confirm)require_write
POST/ro/pick-item/save-batchDesktop grid "Save all" — whole grid, one transactionrequire_write
GET/ro/pick-item/lookup?sku=JSON item attrs (404 if unknown)signed in
POST/ro/pick/find-jobShared with Pick List; redirect_to=/ro/pick-item*require_write
POST/ro/pick/{pick_id}/classShared with Pick Listrequire_write
POST/ro/pick/leaveShared; clears both cookies. Also the handheld Menu button (redirect_to=/handheld)require_write

There is deliberately no post path here (pick_item_save never posts).

2. Job persistence (cookies)

Two unsigned cookies, max_age = 3 days, exactly one set at a time:

_pick_item_render prefers the no-RO cookie only when the order cookie is absent/non-numeric. A missing or invalid cookie renders the find form. A cookie pointing at a missing RO renders RO … not found and deletes the cookie; a no-RO cookie whose session is no longer OPEN renders the "no longer open" error and deletes the cookie. Cookies are UX only — never identity.

3. Render logic

4. Save (POST /ro/pick-item/save)

  1. require_write.
  2. Cookie decides RO vs no-RO. No usable order cookie and no no-RO cookie → 400 no RO selected — scan or enter the RO # again.
  3. Class is required either way (Dave, 2026-09-06). A no-RO session must still be OPEN (400) and have class_ref → else 400 {"ok": false, "error": "Set the class for this job first."}; an RO pick is checked with resolve_class(conn, pick, None) → else 400 {"ok": false, "error": "Set the class for this RO first."}. Nothing on this screen saves without a class.
  4. line_key present → save_order_line_qty (update of the pre-seeded row); absent → save_addition_qty (upsert-by-SKU, manual=true, line_key=NULL, line_type='INV', IM2 snapshot at insert).
  5. touch_pick(pick_id, SCREEN_ITEM) updates updated_at and screen.
  6. Response is primitives only: {"ok": true, "sku": …} (Decimal/datetime previously 500'd here). Errors: 400 {"ok": false, "error": …}.

Quantity semantics are apply_qty_entry — identical to Pick List (blank = untouched, confirmed 0 = requirement dropped at post time, negative = return, non-numeric = error).

4a. Save all (POST /ro/pick-item/save-batch)

Desktop grid only. Body {"rows": [{"sku", "qty", "line_key": int|null, "confirm": bool}, …]}; one request for the whole grid instead of one per row (Dave, 2026-09-06).

  1. require_write; empty rows400 {"ok": false, "errors": {"": "nothing to save"}}.
  2. The same two cookies decide RO vs no-RO; no usable cookie → 400. A no-RO session must still be OPEN.
  3. resolve_class must return a class → 400 … "Set the class for this job first.".
  4. Rows are applied through _apply_form_qty in one transaction: blank SKU skipped, duplicate SKU within the batch rejected, line_key present → order line, absent → addition.
  5. Any error raises _PickSaveRollback → the whole batch rolls back and the response is 400 {"ok": false, "saved": 0, "errors": {"<sku>": "reason"}}. Success: {"ok": true, "saved": n}.
  6. touch_pick(pick_id, SCREEN_ITEM) inside the same transaction.

5. External calls

GET /ro/jobs/suggestsearch_qb_jobs() → QuickBooks Customer query, cached by ro_import._cached("qb", q, 8, …); failures are returned as a row containing error, never a 500. ensure_pick_class may call ro_import.class_for_job (live QB) once per session per process.

6. Known limits / gaps