← All manuals IM2

PO Alerts / change watch — technical specification

PO Alerts
Technical Spec Ver 3 Updated 2026-09-07 Matches the live screen

PO change watch (app/po_watch.py, P2 slice C)

Dave, 2026-09-06: alert when a PO or PO line an RO relies on is deleted, and when an RO line linked to a PO is deleted here — "CDC is the only foolproof way I can think of."

Read-only against QuickBooks. No creates, edits or deletes anywhere in this file.

How it runs

im2-po-watch.timer (systemd, hourly, RandomizedDelaySec=120, Persistent=true) → im2-po-watch.service/opt/im2/venv/bin/python /opt/im2/app/po_watch.py. --dry-run prints and writes nothing. Cursor: qb_cdc_cursor entity PurchaseOrderWatch — deliberately separate from PurchaseOrder, which live_qty advances on every workbench load and would otherwise consume this watcher's window.

Detections

Gotchas

Screens: GET /po/alerts (+ ?show=all), POST /po/alerts/{id}/ack in po_rel.py.

Ver 2 - 2026-09-07 - "PO not in QuickBooks"

Dave, while testing: "I've entered several fake POs for SKUs that don't exist in QB. What happens to those?" Nothing did. A hand-keyed or manually released link is stored as qb_po_id = 'PENDING:<doc>' and counts as supply on the workbench, and the nightly sync only ever matches links up - it never questions one. A typo'd or never-created PO number therefore suppressed a real requirement silently and forever.

po_watch.scan_pending(conn, qbo) now runs in the same hourly scan: every PENDING link older than PENDING_GRACE_HOURS (12) is looked up in QuickBooks by DocNumber, in chunks of 40, and anything QuickBooks does not have raises alert_type "PO not in QuickBooks". A failed lookup returns no alerts at all rather than reporting everything missing, so QuickBooks being down never floods /po/alerts.

Inventory rows are unaffected by design - their On PO is computed from live QuickBooks purchase orders, so a PO number that does not exist in QuickBooks contributes nothing.

Shared table sort (2026-09-07)

Column sorting is one implementation in templates/base.html (Dave, 2026-09-07: "all screens that have tables like this need sorts on the appropriate columns"). A table opts in with class="sorttable"; every thead th becomes sortable except those with class="nosort"/class="toggle", an empty heading, or a checkbox in the heading. Client-side only, over the rows already rendered. Details: tech/table_sorting.md.

Screen (Ver 2, 2026-09-07)

Dave, 2026-09-07: "let's put the alerts on a screen similar to the RO Import issues and let them resolve them from there with links to the RO and PO."

Migration: alter table po_alert add column if not exists resolution text — applied 2026-09-07. Rows closed under Ver 1 read as Resolved (the template falls back).

Stock Replenishment tiles removed from app/menu.html and the handheld menu (/var/www/im2docs/handheld.html, nginx alias for /handheld — note the copy in app/static/ is not the served one).

Closed-alert window (Ver 3, 2026-09-07)

Dave, 2026-09-07: "90 days is good with an older option." po_rel.CLOSED_WINDOW_DAYS = 90. GET /po/alerts adds older=0|1; when older is falsy the where-clause keeps acknowledged_at is null or acknowledged_at > now() - 90 days, so open alerts are never filtered by age. Chip counts use the same window and a second query supplies hidden (closed rows outside it) for the Show N older link. No purge — rows stay in po_alert indefinitely; this is display only.