← All manuals Operator Guide for this screen IM2

Item Display & Bin Lookup / Cycle Count (handheld)

Item Display
Technical Spec Ver 1 Updated 2026-09-06 Matches the live screen

Handheld Item Display / Cycle Count — Technical Spec

app/im2.py, "handheld" section. One template, templates/scan.html, serves both screens; it sets screen = 'count' if mode == 'count' else 'scan' so base.html renders the right banner. Layout is capped at 520px with large tap targets.

Routes

RouteFunctionNotes
GET /scan?sku=scan()mode='display'. Loads select * from items where sku = %s (exact match, trimmed). No item → error = "SKU … not found".
GET /count?sku=count_screen()mode='count'. Same item load, plus the last 5 cycle_counts rows for the SKU (order by counted_at desc limit 5) and pending_count = the first of those with posted_at is null.
POST /api/count/{sku}save_count()Form field qty. Gated by require_action(user, "count_record") (default admin/level1/level2).
POST /api/item/{sku}edit_item()Shared with the desktop grid; the bin fields post here. Field-level permission is enforced inside apply_change().

Both GETs are Depends(current_user) — any logged-in role can look an item up.

Template context

item, error, user, mode, can_edit_bin ("primary_bin" in editable_fields(role)), can_edit_secondary (same for secondary_bin), and for count mode recent, pending_count, can_count (can(user, "count_record")).

The count box renders only when mode == 'count' and can_count; the bin inputs render always but carry disabled and a "(view only)" label when the role lacks the field.

Bin save

scan.html binds blur (and Enter → blur()) on #bin / #sbin, skips the post when the value is unchanged against dataset.original, and posts field + value to /api/item/{sku}. On success it takes the stored value back from the response (so server-side normalisation such as upper-casing shows immediately) and writes "Saved." to the hint; on failure it prints detail and restores the previous value. There is no optimistic state — the field always reflects what the DB returned.

Count save

save_count():

  1. require_action(user, "count_record") — level3 rejected even though the button would be hidden anyway (server is the gate, not the template).
  2. qty must parse as float and be >= 0; anything else is 400.
  3. 404 if the SKU is not in items.
  4. One pending count per SKU: the oldest cycle_counts row with posted_at is null is looked up; if it exists it is updated (counted_qty, counted_by, counted_at=now(), post_status and post_error cleared) and an audit_log row is written with field_name='cycle_count', source='recount', old → new. Otherwise a new cycle_counts row is inserted (no audit_log row for a first count).
  5. Returns {ok, id, counted_qty, qb_on_hand, variance, replaced_qty, replaced_by} — the client renders the green confirmation from that, then refocuses the SKU input.

items.qty_on_hand is not touched here; it only changes when the count is posted to QuickBooks from /counts (count_post, default admin/level1).

Permissions summary

CapabilityGate
View item / on handany logged-in role
Edit primary/secondary binrole_field_perms for that field (level2 and up by default)
Record a countaction count_record (admin, level1, level2)
Post counts to QBaction count_post, on /counts — not on this screen

Known limits / gaps