← Main Menu All manuals Tips & Tricks for this screen IM2

System Settings

System Settings
Technical Spec Ver 4 Updated 2026-10-02 Matches the live screen

System Settings — Technical Spec

app/im2.py, template app/templates/settings.html.

Routes

MethodPathAuthNotes
GET/settingsrequire_action(user, 'settings_edit')Non-list settings only. Shares _settings_page() with /tables.
GET/tablesrequire_action(user, 'tables_edit')LIST_SETTINGS textareas — see the Tables spec.
POST/api/settingsper key: list keys need tables_edit, everything else settings_editWhole-form save, used by both screens. Returns {ok:true, changed:N}.

Split 2026-09-10 (Dave). Action keys settings_edit (default admin) and tables_edit (default admin+level1) are editable on /perms; ADMIN_LOCKED_ACTIONS unions admin back in so no permission edit can lock admins out. can() is a Jinja global and gates the two nav links independently.

Storage and resolution

Save semantics (POST /api/settings)

Per key in SETTING_KEYS, skipped entirely if absent from the form (so a partial form only touches what it posts):

  1. trim; list keys re-serialized through setting_list; bool keys coerced to Yes/No; uoms upper-cased; default_lead_time_days must parse as a number → stored as int, else 400 "Default lead time must be a whole number of days".
  2. Unchanged vs settings(conn) → skipped, not counted.
  3. val == "" → delete from app_settings where key = %s (revert to code default; an empty override is never stored).
  4. Otherwise upsert with updated_by, updated_at = now().
  5. One audit_log row per change: sku = NULL, field_name = key, old→new, source='settings'.

Consumers

fp_poll_hours / bt_poll_hours (HOURS_SETTINGS, whole hours 0-24, 0 = off, rendered as a number box and range-checked on save) → ro_poll_gate.py, read by the hourly /logistics/ro-intake-poll cron; allow_auto_po + wb_count_before_release → po.py workbench/buy list; ro_classes → RO screens and ro_post.py; uoms → grid, new item, RO lines; income_gl/expense_gl + hidden account ids → qb.py new-item push; default_lead_time_days → Purchasing Workbench; rush_notify_* → notify.py.

Known limits / defects

  1. Account names are free text and never validated against QuickBooks; a typo surfaces later as a rejected item push.
  2. ro_classes ids are not validated either — a wrong id posts to the wrong QB class silently.
  3. settings() / setting_bool() open a fresh connection per call when no conn is passed (notify.py and several route paths do exactly that). Cheap per request, but it is a real DB round trip inside best-effort notification code.
  4. No CSRF token; cookie auth + samesite=lax only. A single POST can change every setting, including turning allow_auto_po on.
  5. scripts/schema.sql in this package does not contain app_settings; the live DB has it. Replaying that DDL clean would not produce a working Settings screen.
  6. updated_at.strftime is called unguarded in the template — a row with a NULL updated_at (only reachable by hand-written SQL) would 500 the screen.

FP Status Trigger (Ver 4, 2026-10-02)

fp_status_trigger (default Pending Material). Save validates via im2._fp_job_status() against FP /status rows with object_type='job', not deleted, case-insensitive; stores the canonical name. ro_watch.py / sandbox ro_inbox_watcher.py trigger_status_id() resolve name→id each sweep and filter /job on it; an unresolvable name raises (poll failure alert), never falls back silently. PM_STATUS 1096195 is only the default. Manual Load FP Material (ro_fp.py, by job #) is not status-filtered. BT has no status trigger (all open jobs from /api/Jobsites/Grid).