Pick Item — Technical Spec
1. Routes
app/ro_pick.py, same module and same session tables as Pick List (see pick_list tech spec §2).
| Method | Path | Purpose | Permission |
|---|---|---|---|
| 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/find | Resolve job text, set the cookie | signed in |
| POST | /ro/pick-item/save | Save one line (sku, optional line_key, qty, confirm) | require_write |
| POST | /ro/pick-item/save-batch | Desktop grid "Save all" — whole grid, one transaction | require_write |
| GET | /ro/pick-item/lookup?sku= | JSON item attrs (404 if unknown) | signed in |
| POST | /ro/pick/find-job | Shared with Pick List; redirect_to=/ro/pick-item* | require_write |
| POST | /ro/pick/{pick_id}/class | Shared with Pick List | require_write |
| POST | /ro/pick/leave | Shared; 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:
ro_pick_item_order— theorder_nofor an RO pick.ro_pick_item_pick— thepick_idfor a no-RO (direct issue) session.
_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
- RO path:
get_or_open_pick(order_no, SCREEN_ITEM, user)(seeds lines on first open),ensure_pick_class(conn, pick, order). - If
skuequals the order number or itssource_ref, it is discarded with"That's the RO # — this box wants a SKU."(browser field restore guard). _find_pick_line(pick_id, sku=…)→line(pre-seeded BOM line or an existing addition); elselookup_item(sku)→add_item(may be{}= unknown SKU).gen_locis overlaid whenprimary_binis blank._pick_item_recent(pick_id, limit)=ro_pick_linerows withqty_picked is not null, newest first byupdated_at;limit=Noneon the desktop screen. No per-line operator exists in the schema (attribution isro_pick.operator), so no "by <name>" is shown.- Desktop only:
bom_lines_json— a{sku: {line_key, qty_required, qty_oh, qty_picked, description, uom, primary_bin, gen_loc}}map,</escaped, so the grid can route a typed SKU onto its BOM line. Serialized with_json_default(Decimal → float). - No-RO render (
_pick_item_render_noro) builds a stuborder={order_no: None, job_name: qb_customer_name}; every SKU is an addition;bom_lines_jsonis"{}".
4. Save (POST /ro/pick-item/save)
require_write.- 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.
- Class is required either way (Dave, 2026-09-06). A no-RO session must still be
OPEN(400) and haveclass_ref→ else400 {"ok": false, "error": "Set the class for this job first."}; an RO pick is checked withresolve_class(conn, pick, None)→ else400 {"ok": false, "error": "Set the class for this RO first."}. Nothing on this screen saves without a class. line_keypresent →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).touch_pick(pick_id, SCREEN_ITEM)updatesupdated_atandscreen.- 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).
require_write; emptyrows→400 {"ok": false, "errors": {"": "nothing to save"}}.- The same two cookies decide RO vs no-RO; no usable cookie → 400. A no-RO session must still be
OPEN. resolve_classmust return a class →400 … "Set the class for this job first.".- Rows are applied through
_apply_form_qtyin one transaction: blank SKU skipped, duplicate SKU within the batch rejected,line_keypresent → order line, absent → addition. - Any error raises
_PickSaveRollback→ the whole batch rolls back and the response is400 {"ok": false, "saved": 0, "errors": {"<sku>": "reason"}}. Success:{"ok": true, "saved": n}. touch_pick(pick_id, SCREEN_ITEM)inside the same transaction.
5. External calls
GET /ro/jobs/suggest → search_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
- Cookies are not scoped per user. A shared handheld signed in as a different operator inherits the previous operator's remembered job (the pick session it writes to still records the current user, but the job on screen carries over).
- Re-saving a SKU replaces the quantity; there is no way to add to it, and these screens have no delete-line endpoint. A wrong addition is corrected on Pick Review (
POST /ro/picks/{pick_id}/line-qty, qty 0 deletes a manual line) or by discarding the session. - The handheld screen has no Help link by convention (Help lives on the handheld menu).