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

Issues to Resolve — One RO, All, Unattached

RO Import Issues to Resolve
Technical Spec Ver 3 Updated 2026-09-15 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, qty, sku (value kept for old callers)
POST /ro/issues/bulkissues_bulk()—form: action, note, issue_ids (CSV), qty

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)

NEEDS_SKU_TYPES/NO_IGNORE are duplicated in JS to drive the Resolve/Ignore modal and to hide the Ignore button for no qty rows; the server enforces the same rules, so the JS copy is cosmetic. The modal (#resolveModal) replaced the old prompt() chain 2026-09-15 — see the section above. 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.

Qty/SKU split on Resolve, reason-note syscon, bulk qty (2026-09-15, Dave)

Three related changes, all requested in the same message:

  1. apply_issue_action() now takes separate qty and sku form fields instead of one generic value. A NONINV line that is simultaneously unmatched (issue_type SKU not in QB) and missing a quantity no longer forces a SKU out of the operator — supplying only qty resolves the quantity and leaves the line NONINV; supplying sku (with or without qty) converts it to INV as before. needs (from RESOLVE_INPUT / _needs_qty) still decides what's required: qty required whenever the line needs one, sku never required. The old value param is still accepted (mapped onto whichever of qty/sku the issue type needed) so nothing already calling the old shape breaks.
  2. POST /ro/issues/bulk takes an optional qty, applied to every selected id via the same apply_issue_action() call — ids that don't need a qty simply ignore it. No bulk sku — a shared SKU across different lines isn't a safe default, so a row needing a SKU match still has to be resolved one at a time.
  3. New syscon ro_issues_ask_reason (im2.py SETTING_DEFS/BOOL_SETTINGS, default No). The client reads it as ask_reason in the three issues-screen routes (setting_bool("ro_issues_ask_reason")) and only shows the note textarea in the Resolve/Ignore modal when it's Yes. Off by default per Dave, 2026-09-15.

Client-side, the old prompt() chain (value prompt, then note prompt) is replaced by one modal (#resolveModal in ro_issues.html): Quantity box (shown/required per data-needsqty), SKU box (shown whenever the issue is SKU not in QB or the line is NONINV; always optional), and the reason textarea (shown only when ASK_REASON is true). Bulk resolve opens the same modal with just the Quantity box when any selected row still needs one, defaulting to blank (operator usually types 1).

RO Inbox back-link on the order screen (2026-09-15, Dave)

ro_order.html's top bar only had "← RO Inquiry", but Accept on the RO Inbox (ro_import.py / ro_inbox_detail.html) navigates straight to /ro/{order_no}?imported=1 with no way back to the Inbox queue. Added a second link, "← RO Inbox" → /ro/inbox, always shown (not gated on the imported banner) since any RO can legitimately have started life there.

Resolve wrongly demanding a qty that was already there (2026-09-15, Dave)

needs (what Resolve requires) came purely from RESOLVE_INPUT.get(issue["issue_type"]) -- fixed at import time. For a no qty/Qty problem issue, needs == "qty" never changed even after the line's quantity was fixed some other way (Fix-now on RO Import Inbox, a manual RO edit, an earlier resolve) while the issue itself stayed Open. The frontend already used the line's CURRENT quantity (_qty_missing(), refreshed every page load) to decide whether to show the Qty box as required — so the modal looked optional, but the backend still hard-blocked with "a quantity is required to resolve this issue" on submit. Fixed apply_issue_action() to only require it when the line's current qty is actually still missing (or the line itself can't be found) — if needs == "qty" and not qty_str and (line is None or _qty_missing(line)).

qb_skus() wasn't active-only — retired SKUs stopped being flagged (2026-09-15, Dave)

Found via "This one has an inactive record pointing to a good SKU": ro_parse.py::qb_skus() (the "known" set classify() checks a BOM's SKU against) had no qb_active filter. Before today's 926-row QB-inactive backfill this was harmless — IM2 held almost no inactive rows — but afterward it meant ANY sku with an inactive IM2 row counted as "known", so a BOM line referencing a retired SKU stopped getting flagged/held at all, let alone getting the "use X" suggestion. Fixed: qb_skus() now filters qb_active = true. Backfilled 3 already-open SKU not in QB issues (647, 693, 694) whose title/detail predated the fix and never picked up the "use X" hint their SKU's inactive record already had by the time this ran.

Resolve stopped showing the Quantity box when it isn't needed (2026-09-15, Dave)

Follow-up to the fix above: even after that fix, the Resolve modal still showed the Quantity box (as optional) on every row, including a SKU not in QB row that already had a perfectly good quantity — Dave: "the only issue is asking for a qty when it's not needed." showQty was hardcoded true for every Resolve regardless of type. Changed to showQty = qtyMissing || issueType in {'no qty', 'Qty problem'} — the box only appears when the line genuinely has no usable qty yet or the issue is qty-flavoured. When it does show but isn't required (line already has a qty, e.g. the no qty type after some other fix landed), it now defaults to the line's current quantity instead of blank, per Dave's own suggested fallback. Bulk resolve already had the equivalent anyNeedsQty-gated behavior and needed no change.

Bulk Resolve on "SKU not in QB" rows silently failed (2026-09-15, Dave)

Bulk resolve has no SKU field — each SKU not in QB row would need its OWN, different SKU, which a single bulk box can't supply. Selecting several and clicking "Resolve selected" posted qty="" sku="" for each; the backend correctly rejected every one ("enter a quantity, a SKU, or both") but that surfaced as a confusing "0 updated" plus a wall of per-issue errors. Now caught client-side before the modal even opens: if any selected row is SKU not in QB, an immediate message explains bulk Resolve can't do those and to resolve them individually (each row's own modal already has the SKU box) or bulk-Ignore them instead. Bulk Ignore is unaffected (no SKU needed there).

Bulk Ignore/Resolve had one dialog too many (2026-09-15, Dave)

Bulk Ignore/Resolve used to chain three separate boxes: a native confirm(), then the modal (with its own Cancel/Save), then a final alert("N updated"). Dave: "too many boxes to respond to." The native confirm() was pure redundancy — the modal's own Cancel/Save already is the confirmation step — so it's gone. The final alert now only fires when something needs attention (partial or zero success with per-issue errors); a clean full success reloads straight into the updated list with nothing to dismiss. Net: one dialog (the modal) for the common case instead of three.

"Ignore" wording only means Non-Inv for SKU-not-in-QB rows (2026-09-15, Dave)

"Ignore" is the shared close-this-issue action for every issue type (SKU not in QB, duplicate line, unmatched job, header only, unmatched NONINV refresh, unlinked QB PO) — it just marks the issue Ignored, no line change, except that a "SKU not in QB" line is already sitting as NONINV pending a QB item, so ignoring it in practice means "leave it non-inventory." The single-row button already said "Leave Non-Inv" for line_type == 'NONINV' rows; the bulk button/modal title now does the same (via each row's data-linetype) — "Leave Non-Inv selected" / "Leave N as Non-Inv" when every checked row is NONINV, otherwise the generic "Ignore selected" wording stays, since Ignore doesn't mean Non-Inv for the other issue types.

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.