Item Display & Bin Lookup / Cycle Count (handheld)
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
| Route | Function | Notes |
|---|---|---|
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():
require_action(user, "count_record")— level3 rejected even though the button would be hidden anyway (server is the gate, not the template).qtymust parse as float and be>= 0; anything else is 400.- 404 if the SKU is not in
items. - One pending count per SKU: the oldest
cycle_countsrow withposted_at is nullis looked up; if it exists it is updated (counted_qty,counted_by,counted_at=now(),post_statusandpost_errorcleared) and anaudit_logrow is written withfield_name='cycle_count',source='recount', old → new. Otherwise a newcycle_countsrow is inserted (noaudit_logrow for a first count). - 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
| Capability | Gate |
|---|---|
| View item / on hand | any logged-in role |
| Edit primary/secondary bin | role_field_perms for that field (level2 and up by default) |
| Record a count | action count_record (admin, level1, level2) |
| Post counts to QB | action count_post, on /counts — not on this screen |
Known limits / gaps
- No Help link on the screen. By convention the handheld carries Help on the Handheld Menu;
scan.htmlhas no Help button (base.htmldoes not add one). - No link to a Count List.
/countknows nothing aboutpo_count_list— the printed sheet and the handheld are only connected by the operator. See techcount_list. - The SKU box is
inputmode="numeric"— fine for numeric SA SKUs, awkward for anything alphanumeric typed by hand on a phone. - Quantity input is
step="1"; fractional counts must be typed and are accepted server-side (float), but the spinner will not produce them. - The SKU search is exact match only — no partial/description lookup on the handheld.