RO Maintenance & Attachments
RO Maintenance & Attachments — Technical Spec
All routes in app/ro.py, extending /ro/{order_no:int}. Level2+ for the inline edits and attachments (ro.require_write — the file's existing level3-view-only rule); the two routes added 2026-09-05 have their own, stricter gates (see below). Template ro_order.html, screen key ro_order, banner Requirements Order.
order_detail() accepts line_filter (LINE_FILTERS: all / open / adjusted / noninv / cancelled; anything else falls back to all) and the one-shot banner flags ?imported=1 (from Accept) and ?created=1 (from Create a Requirements Order). It also passes held_count, imports (import history), header_attachments and per-line attachments.
Inline edits
| Route | Fields | Notes |
|---|---|---|
POST /{order_no}/line/{line_key}/adjust | qty_adj, adj_note (optional) | Writes ro_detail.qty_adj + qty_source_at_adj/adj_by/adj_at/adj_note. Never writes qty_source or qty_required (GENERATED). Also writes one ro_bom_audit row (match_basis='Manual Adjustment') and one ro_audit row. |
POST /{order_no}/date_required | date_required (blank clears it) | Writes ro_header.date_required + ro_audit. No default is ever applied server-side (Ver 3 ruling C.2). |
adj_note is optional per Ver 3 ruling C.1 — the original brief's "mandatory adj_note" is superseded.
Routes added 2026-09-05 [Ver 2, 2026-09-06]
| Route | Gate | Notes |
|---|---|---|
POST /{order_no}/priority | require_action(user, "rush_priority_set") — admin/level1 by default | Writes ro_header.priority_code = exactly 'R' or NULL, 400 on anything else; no-op returns changed: false; one ro_audit row (rush_priority_set/rush_priority_clear). Called from the PO Workbench demand drill-down, not from this screen. FP-sourced 1–5 priorities stay read-only app-wide. |
POST /{order_no}/delete | user["role"] == "admin" (hard-coded, not a perms key) | 400 if sum(ro_detail.qty_issued) > 0 or any ro_pick row exists ("cancel it instead"). Then: sets any ro_inbox row for this order back to status='Awaiting Review' (order_no / reviewed_by / reviewed_at cleared, note written); deletes ro_issue, ro_bom_audit, ro_attachment, ro_detail for the order; update ro_audit set order_no = NULL; deletes ro_header; inserts one ro_audit row (action='ro_delete', order_no NULL) naming the job/BOM and line count. The button renders only for user.role == 'admin'. |
Held-line badge
order_detail() now also counts ro_detail rows with line_status = 'HOLD' for the order and passes held_count to the template; the badge links to /ro/issues/{order_no} (Screen C's per-order drill-down).
Attachments
Table: ro_attachment (create_inbox_tables.sql). line_key IS NULL = header document.
| Route | Notes |
|---|---|
POST /{order_no}/attachments | Multipart, one file per call (file, optional line_key). 25 MB cap, any content type, filename sanitised (_safe_filename) before it touches disk. Writes the file under RO_ATTACH_DIR (env var, default /var/lib/im2/ro_attachments) as {order_no}/{uuid}_{name}, written to disk before the DB row so a failed insert never leaves a dangling reference to a missing file. |
POST /{order_no}/attachments/{attachment_id}/remove | Sets deleted_at. Never deletes the file — same no-physical-delete rule as everywhere else in this schema. |
GET /{order_no}/attachments/{attachment_id}/download | Available to every role — only Remove is level2+. Streams the file with its original filename. |
order_detail() loads all active attachments for the order in one query (fetch_attachments()) and splits them into header_attachments and a line_key → [attachments] map, attached to each line as line["attachments"] before rendering — no per-line lazy fetch, unlike the PO-links/movement/refresh History panel (order sizes here are small enough that eager loading is simpler and just as fast).
Manual PO reference (added Ver 1, 2026-09-06)
POST /ro/{order_no}/line/{line_key}/po_ref and POST /ro/{order_no}/line/{line_key}/po_ref/{po_link_id}/remove in app/ro.py. Dave, 2026-09-06: "They can run the buy report, enter the PO manually. They need to be able to enter the pieces that come back to the RO manually."
- Writes
ro_po_linkwithlink_source = 'Manual entry',po_status 'Open',qty_received/qty_billed0, andqb_po_id = 'PENDING:<doc>'— the nightly QuickBooks sync replaces that placeholder with the real PO id once it sees the document. - NONINV lines only (
_line_for_po_refraises 400 otherwise). An inventory requirement is satisfied by a pick; its PO replenishes stock in total and is not tied to an RO line. Same rule aspo_rel.release_manual— if one changes, change both. - Rejected: blank PO number, non-numeric or ≤ 0 quantity, non-numeric cost, a date that is not
YYYY-MM-DD, and a PO number already recorded against the same line. - Remove refuses anything whose
link_sourceis not'Manual entry', so a release-written or sync-written link cannot be deleted from the UI. - Both write
ro_audit(po_ref_add/po_ref_remove). line_expandnow also returnslink_sourceper link and the line'sline_type; the template uses those to decide whether to show the form and the remove links.- Gotchas: the app's CSRF check rejects posts without an
Origin/Refererfrom the site — a TestClient harness must send them. The handlers are delegated at document level because the panel is re-rendered after every add or remove.
RO number, line sorting, per-line issue flags (2026-09-07)
order_detail() loads open issues grouped by line_key and stamps open_issues / issue_types on each line, plus open_issue_count for the header. Line sorting is client-side and moves each tr.line-row together with its hidden #expand-{line_key} row; only columns 1–8 (Line # … Req Date) are sortable.
Status was reviewed against live data on 2026-09-07 and is behaving as the views define it (ro_detail_v.line_state, ro_header_v.ro_status) — every current RO reads Open because nothing has been picked or PO'd yet. The one gap worth knowing: an RO whose only exception is a HOLD line still rolls up to "Open", which is why the issue badge exists.
Ver 2 - 2026-09-07 - header Class
- POST /ro/{order_no}/class_ref sets ro_header.class_ref. Only ids on the Settings "RO classes" list are accepted; blank clears it. Audit row header_edit / class_ref.
- ro_header_v does not expose class_ref, so the order screen reads it from ro_header directly rather than widening a view that several screens select * from.
- Rush ROs were inserted with no class (rush.py never set it) although the manual RO screen derives it from the type - _rush_class_ref() now does the same. Ten existing ROs were backfilled from their type on 2026-09-07, each with an audit row.
- Pre-flight text for a class-less RO now says where to fix it and that Type is not the class.
Ver 2 — 2026-09-07 — header Class
- sets . Only ids on the Settings RO classes list are accepted; blank clears it. Audit row .
- does not expose , so the order screen reads it from directly rather than widening a view several screens from.
- Rush ROs were inserted with no class ( did not set it) even though the manual RO screen derives it from the type — now does the same. Ten existing ROs were backfilled from their type on 2026-09-07 with audit rows.
- Pre-flight text for a class-less RO now says where to fix it and that Type is not the class.
Ver 3 - 2026-09-07 - PO reference removal opened up
remove_po_ref() no longer rejects link_source <> 'Manual entry' (Dave: "I need to also be able to remove one"). Any ro_po_link row on the line can be deleted; the audit row records the source it was removed from in new_value, and the response carries may_resync=True for QB-sourced links, which the confirm dialog surfaces. Deleting a link changes workbench supply for that line immediately (see tech/po_workbench Ver 3).