← All manuals Operator Guide for this screen IM2

Item Master Display — Technical Spec

Item Master Display & Maintenance
Technical Spec Ver 5 Updated 2026-09-06 Matches the live screen

1. Routes

MethodPathPurposePermission
GET/Render the gridany signed-in user
POST/api/item/{sku}Save one field on one itemrole_field_perms for that field
POST/api/bulkSave one field across many SKUssame, per row
GET/api/item/{sku}JSON for one itemany signed-in user
GET/api/audit/{sku}Audit trail for one itemany signed-in user
GET/exportCSV of the current filtered setany signed-in user
POST/api/uploadSpreadsheet update of warehouse fieldsaction item_upload (Admin)

Template: app/templates/grid.html. Handler: grid() in app/im2.py.

2. Inputs (GET /)

Query string, all optional: search, gen_loc, item_group, manufacturer, flag, archived (0/1), page (1-based), sort, dir (asc/desc). Page size is fixed at 200; offset is (page-1)*200. Unknown sort values fall back to sku; dir anything other than desc is asc. Pager UI: _page_url(request) rebuilds the current query string with a new page, and the template gets page_count (ceil(total/limit)), first_row, last_row for the First/Prev/Next/Last controls and the "Showing 401–600 of 4,182" readout. The filter form and sort links omit page, so any filter or sort change restarts at page 1. [Ver 2, 2026-09-04]

3. Query logic (query_items)

Ordering (order_clause) is whitelisted against SORT_COLUMNS — no user string reaches SQL. Numeric columns (qty_on_hand, purchase_cost, min_qty, max_qty, lead_time_days) sort numerically with nulls last; everything else sorts as nullif(col::text,'') with nulls last, then sku as tiebreak. Blank-last is deliberate in both directions.

4. Data read

Table items, all columns (select *). Supporting reads per render:

5. Field ownership

QB_FIELDS = item_name, description_po, description_so, qty_on_hand, purchase_cost, primary_vendor, qb_active — rendered read-only/grey, rejected server-side by clean_value with "controlled by QuickBooks" regardless of role. QB is the book of record; these arrive on sync.

WH_FIELDS = primary_bin, secondary_bin, gen_loc, item_group, manufacturer, mfr_part_no, uom, min_qty, max_qty, lead_time_days, count_group, notes, abc_code — editable subject to role_field_perms. no_cost_expected / no_cost_reason are not in WH_FIELDS and are not rendered as columns on this grid; they exist only as grantable rows in EDITABLE_FIELD_LABELS on the Permissions screen and are set elsewhere (ABC/no-cost reporting). [Ver 4, 2026-09-06]

6. Write path (apply_change)

  1. clean_value(field, value, user) — reject QB fields; reject fields not in editable_fields(user["role"]) (403); trim strings, empty → NULL; NUMERIC_FIELDS (min_qty, max_qty, lead_time_days) cast to float, negatives rejected; uom upper-cased and validated against the Settings list; abc_code upper-cased and restricted to A/B/C.
  2. Read current value; if unchanged as text, return without writing (no empty audit rows).
  3. Cross-field rule: min_qty must not exceed max_qty, checked whichever side is being edited.
  4. update items set {field} = …, updated_by, updated_at = now().
  5. Insert into audit_log (sku, field_name, old_value, new_value, changed_by, source).

POST /api/item/{sku} re-reads the stored value and returns it, so the cell shows what the database actually holds (normalised case, trimmed) rather than what was typed. POST /api/bulk splits skus on commas and calls apply_change per SKU, accumulating changed and a per-SKU errors list — one failure does not abort the batch.

7. Audit trail

GET /api/audit/{sku} returns audit_log rows for that SKU, newest first. source distinguishes origin: app (grid edit), bulk, upload, recount, counts, cycle count {batch_code}, perms_admin. No delete or update path exists for audit_log anywhere in the app.

8. Permission gates

Field-level rights come from role_field_perms via editable_fields(role), cached in _PERM_CACHE and cleared on any save from /api/perms. The template only renders inputs for fields in editable, and clean_value enforces the same list server-side — the UI hiding a field is convenience, not security. Level 3 can never hold field rights (blocked at /api/perms).

Action-level keys live in role_action_perms (ACTION_DEFS / ACTION_DEFAULTS in im2.py) and are editable on the Permissions screen: item_create, item_qb_sync, item_upload, count_record, count_post, abc_run, abc_write, ro_view, ro_write, pick_do, pick_post, and rush_priority_set (added 2026-09-05 for the Rush toggle on the PO Workbench; default admin/level1). [Ver 4, 2026-09-06]

8a. Decided business rules carried in this screen

Rules settled in design that the code enforces here, restated so the logic can be checked without reading the code. [Ver 3, 2026-09-04]

9. Errors

All failures are HTTPException with a plain-English detail rendered next to the cell: 400 for validation and QB-owned fields, 403 for insufficient level, 404 for an unknown SKU. Bulk edits report per-SKU errors in the response body.

10. Limits and dependencies

Uppercase fields are converted, not just styled (2026-09-06)

Dave, 2026-09-06 ("do the boxes requiring upper case auto convert?"). They were styled with text-transform: uppercase only, so a typed a12 displayed as A12 and was stored as a12 — two spellings of one bin that no longer match or sort together. Now clean_value() upper-cases primary_bin, secondary_bin and gen_loc (as it already did uom and abc_code), create_item upper-cases both bins and gen-loc on insert, and the grid's inline editor upper-cases the value before it is sent (data-upper="1" on bin and ABC cells). Existing data was already clean: 0 of 917 primary bins and 0 of 1,517 gen-locs were mixed case, so no backfill was needed. item_group and count_group are deliberately left in mixed case — they are pick lists of proper names ("Inverter Parts").