Issues to Resolve — One RO, All, Unattached
Issues to Resolve — flat list routes — Technical Spec
app/ro.py. One template, ro_issues.html, screen key ro_issues, serves three GET routes that differ only in the filter they add and the title_suffix / chip_url they pass. The summary screen at /ro/issues is documented separately (help/tech ro_issues_summary).
Routes
| Route | Function | Extra filter | title_suffix |
|---|---|---|---|
GET /ro/issues/all | issues_screen() | none | "" |
GET /ro/issues/unattached | issues_unattached_screen() | order_no is null | " — Unattached" |
GET /ro/issues/{order_no:int} | issues_by_order_screen() | order_no = %s | " — RO {order_no}" |
POST /ro/issues/{issue_id}/action | issue_action() | — | form: action, note, value |
POST /ro/issues/bulk | issues_bulk() | — | form: action, note, issue_ids (CSV) |
All three GETs take status (default Open), issue_type, search. _query_url() builds chip links that merge the current filters with one override, so chips compose. All three pass back_url="/ro/issues", back_label="Issues Summary".
{order_no:int} is declared after /all and /unattached, so the literal paths win.
Query
query_issues(conn, status, issue_type, search, order_no, unattached, limit=LIST_LIMIT) against ro_issue:
statusunlessAll;issue_typeexact;searchILIKE acrosstitle, sku, source_ref, detail.order_noandunattachedare mutually exclusive by construction (no caller passes both).order by created_at asc limit 1000, plus a separatecount(*)for the total.- Adds computed
age_days = now()::date - created_at::date; the template styles>= 7red.
issue_status_counts(conn) is global (no filters at all) — the N open badge counts every Open issue in the system, even on a per-order list. Intentional but easy to misread.
ISSUE_TYPES: SKU not in QB, no qty, unmatched job, duplicate line, header only, unmatched NONINV refresh, unlinked QB PO. ISSUE_STATUSES: Open / Resolved / Ignored / All.
Write path — apply_issue_action()
Single chokepoint for both the row action and bulk. Order of operations:
require_write(user)(level3 rejected) — the routes themselves first call_ro_write(user)→require_action(user, "ro_write"), default admin + level1.actionmust beresolveorignore;ignoreis rejected for types inNO_IGNORE = {"no qty"}; an issue not in statusOpenis rejected 400.- Typed resolves, from
RESOLVE_INPUT = {"SKU not in QB": "sku", "no qty": "qty"}:
- the target line is found by
_issue_line()—line_keyfirst, elseorder_no + coalesce(src_ref, sku)and only when that matches exactly one row; no line → 400 "resolve it on the RO Maintenance screen instead". - sku: value required, must exist in IM2
items(_sku_exists(), cross-database viaim2_db()); setsro_detail.line_type='INV',sku,adj_by,adj_at; writes aro_auditrowissue_resolve_sku; also stampsro_issue.sku. - qty: must parse as float and be
> 0; setsro_detail.qty_source(+adj_by,adj_at);ro_auditrowissue_resolve_qty; stampsro_issue.qty.
ro_issue.status/resolved_at/resolved_byupdated.- One
ro_auditrowissue_resolve/issue_ignore.ro_audithas no issue column, so the id is carried innoteas a parseable prefix:issue_id=<id>; resolved as …/issue_id=<id>; ignored: …. sync_line_hold(cur, line_key, operator)— if the line has no Open issue left,ro_detail.line_statusHOLD → NULL plus aro_audithold_clearrow.Cancelled/Closed Shortare left alone.
Bulk calls the same function per id with no value, collecting HTTPException.detail per failure into errors and returning {ok, changed, errors}. All ids share one ro_db() transaction, committed once when the request ends, so a rejected id (validation raises before any write) does not undo the ones that succeeded.
Client behaviour (ro_issues.html)
RESOLVE_INPUT and NO_IGNORE are duplicated in JS to drive the prompt()s and to hide the Ignore button for no qty rows; the server enforces the same rules, so the JS copy is cosmetic. After a successful action the row is removed from the DOM and the counts decremented only when the active status filter is Open; otherwise location.reload().
The Help button on this template links to /docs/help/ro_issues_summary. See Known limits.
Known limits / gaps
- Help button points at the summary guide, not this one.
ro_issues.htmlhard-codes/docs/help/ro_issues_summary; it should be/docs/help/ro_issues. Patch noted ingroup5_helplinks.md. [found 2026-09-06] - The open badge is global, not scoped to the current list (see above).
- The JS duplicates the server's typed-resolve rules — change both together.
- No
ro_viewgate on the GETs (Depends(current_user)only); see techro_inquiry.
Permission gate (added 2026-09-06)
Every route in ro.py now depends on ro_user() instead of current_user directly. ro_user() calls require_action(user, "ro_view"), so revoking View Requirements Orders screens on the Field Permissions screen actually blocks the RO screens (403) — before this the key existed and nothing read it. All four roles hold ro_view by default, so nothing changed for anyone until someone revokes it. Pick and Rush screens live in ro_pick.py / rush.py and keep their own ro_write / rush_* checks.
Use Surplus + UOM (2026-09-07)
apply_issue_action() takes a third action, surplus, alongside resolve/ignore. It is refused unless the RO line is NONINV. It sets ro_detail.line_type = 'SURPLUS', stamps adj_note, writes two ro_audit rows (line change + issue status) and calls sync_line_hold() like the other paths.
po._demand_lines() excludes line_type = 'SURPLUS', so a surplus line never reaches the workbench or a buy list. Surplus lines are not yet on the pick screens — ro_pick still selects line_type = 'INV' only; the "SURPLUS row on the pick list" work is separate.
query_issues() is now aliased (ro_issue i) and left-joins ro_detail on line_key to bring uom and line_type onto the row. Every filter is i.-qualified — the join made sku and order_no ambiguous.
Quantity-required issues are decided by the line, not the label (2026-09-07)
The old rule was NO_IGNORE = {"no qty"} keyed on issue_type. The importer files the same defect under several labels (no qty, Qty problem for an unparseable source qty, sometimes Review), so a SKU line with qty 0 could be resolved with no quantity and Ignore was still offered. Dave, 2026-09-07: "SKU items must have a qty, no ignore option."
ro._needs_qty(issue, line) is now the single test: an INV line (or any line with a SKU) whose ro_detail.qty_source is null or <= 0 requires a quantity. It gates all three paths:
query_issues()stampsneeds_qtyon every row (the query also returnsline_qty/line_skufor it); the template rendersdata-needsqtyand hides the Ignore button on those rows.apply_issue_action()refusesignore(400) and forcesneeds = "qty"onresolveregardless of issue_type, so the API and the bulk actions are covered too, not just the buttons.- Client-side, Resolve prompts for the qty and rejects <= 0 before the round trip.
RESOLVE_INPUT also gained "Qty problem": "qty" so the label-driven path agrees with the line-driven one.
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.