← All manuals Operator Guide for this screen IM2

Issues to Resolve — One RO, All, Unattached

RO Import Issues to Resolve
Technical Spec Ver 2 Updated 2026-09-06 Matches the live screen

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

RouteFunctionExtra filtertitle_suffix
GET /ro/issues/allissues_screen()none""
GET /ro/issues/unattachedissues_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}/actionissue_action()form: action, note, value
POST /ro/issues/bulkissues_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:

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:

  1. require_write(user) (level3 rejected) — the routes themselves first call _ro_write(user)require_action(user, "ro_write"), default admin + level1.
  2. action must be resolve or ignore; ignore is rejected for types in NO_IGNORE = {"no qty"}; an issue not in status Open is rejected 400.
  3. Typed resolves, from RESOLVE_INPUT = {"SKU not in QB": "sku", "no qty": "qty"}:
  1. ro_issue.status/resolved_at/resolved_by updated.
  2. One ro_audit row issue_resolve / issue_ignore. ro_audit has no issue column, so the id is carried in note as a parseable prefix: issue_id=<id>; resolved as … / issue_id=<id>; ignored: ….
  3. sync_line_hold(cur, line_key, operator) — if the line has no Open issue left, ro_detail.line_status HOLD → NULL plus a ro_audit hold_clear row. Cancelled / Closed Short are 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

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:

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.