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

RO Import Inbox

RO Import Inbox
Technical Spec Ver 5 Updated 2026-09-18 matches build 2026-09-18

RO Import Inbox — Technical Spec

Bulk reject (2026-09-18)

The bulk bar now includes Reject selected. No new backend endpoint: after one confirmation and, when configured, one optional shared reason prompt, rejectSelected() sequentially calls existing POST /ro/inbox/{id}/reject for each rendered/checked id. This preserves the existing write-role, nonterminal-row, status update and per-row inbox_reject audit behavior. Failures are collected without stopping later rows; the summary is shown once, then the browser returns to /ro/inbox. Accepted/Rejected rows do not render checkboxes. No source record, BOM, RO, polling setting, or re-import rule changes.

Clean-first release and per-import counts (2026-09-18)

status=No%20Errors is a derived filter, not a stored status. It includes only Awaiting Review rows with error_count=0. INBOX_REVIEW_CTE is shared by the list, chip counts and optional clean_only=true acceptance guard. Reads only; no schema, polling, source-system or QuickBooks changes.

Select-all covers rendered checkboxes only. Both individual and bulk Accept from No Errors send clean_only=true; the server rechecks the stored row and returns 409 before promotion if it is no longer clean/Awaiting Review. Existing acceptance semantics elsewhere (including intentional acceptance with HOLD lines), permission gates and sequential bulk processing remain unchanged. This is staged-data readiness, not live source validation or a guarantee against runtime failure. There is no automatic acceptance during polling or page load.

Screen title (2026-09-15)

Renamed from "RO Inbox" to "RO Import Inbox" everywhere it's user-facing (Dave) — matches the menu tile label, which already said "RO Import Inbox". Changed: base.html's screen_names['ro_inbox']/['ro_inbox_detail'], the ro_inbox_detail.html title block, and every in-app link/back-button text ("← RO Inbox" -> "← RO Import Inbox" on ro_inbox_detail.html/ro_order.html, the RO Inquiry link label). Routes/URLs (/ro/inbox), table names (ro_inbox, ro_inbox_line) and code identifiers are unchanged — this is a display-text-only rename.

RO family jump-nav (2026-09-15)

Added a small nav bar (RO Inquiry / RO Import Inbox / RO Import Issues to Resolve) at the top of every RO screen, so an operator can jump between the three without returning to Main Menu — one change in base.html: ro_nav_screens (a fixed list of screen values) gates whether the bar renders at all, and the active tab highlights based on screen. Covers: ro_inquiry, ro_new, ro_order, ro_inbox, ro_inbox_detail, ro_issues, ro_issues_summary, ro_imports, ro_pick_full, ro_pick_item_full, ro_picks. Deliberately excludes the handheld ro_pick/ro_pick_item screens (phone-optimized, already skip the Main Menu link for the same reason). Existing single-purpose back-links on individual screens (e.g. ro_order.html's "← RO Inquiry"/"← RO Import Inbox") were left in place, not removed — this nav is additional, not a replacement.

Bulk accept (2026-09-15)

Added checkboxes (.pick-one, header #pickAll) and a bulk bar matching RO Import Issues to Resolve's pattern (.bulk/.pick shared styles from base.html). A checkbox only renders on rows still status not in ('Accepted','Rejected') — those are the only rows with an Accept button at all. No new backend endpoint — the bulk bar's acceptSelected() calls the existing single-row POST /ro/inbox/{id}/accept once per selected id, sequentially (not in parallel, so two staged rows for the same job/BOM can't both read "no RO yet" at the same instant and each try to create one). Each row becomes its own RO, same as accepting it alone. Failures (e.g. "choose a Class before accepting") are collected per-row and shown in one summary alert; a failure on one row does not stop the rest from being processed. Page reloads after the loop finishes.

Router: app/ro_import.py, mounted at /ro before ro_router (see the im2.py mount patch — this ordering is load-bearing: ro_router's /{order_no:int} and /{screen:str} catch-alls would otherwise swallow /ro/inbox).

Data

Both tables must be applied via create_inbox_tables.sql before this router is deployed — they do not exist in the schema this package shipped against.

Routes

MethodPathNotes
GET/ro/inboxList screen. status: Awaiting Review (default) / No Errors / Held / Rejected / Error / All.
POST/ro/inbox/importsource (FP\BT\XLSX), source_id, optional bom_ref, optional file. See "Import flow" below.
GET/ro/inbox/{inbox_id}Detail screen (Screen B).
POST/ro/inbox/{inbox_id}/typero_type — sets the operator-chosen RO type before Accept.
POST/ro/inbox/{inbox_id}/holdnote optional; prompt controlled by ro_issues_ask_reason.
POST/ro/inbox/{inbox_id}/rejectnote optional; returns to Inbox after success.
POST/ro/inbox/{inbox_id}/acceptCalls promote_inbox_to_ro(). Requires Class; optional clean_only=true rechecks readiness.

All POST routes require level2+ (ro_import.can_write / require_write — a fixed role check, admin/level1/level2, per Ver 3 addendum ruling C.3, not a role_action_perms lookup).

Import flow (stage_source_job)

No longer a stub [Ver 2, 2026-09-06]: stage_source_job() now dispatches into app/ro_fp.py (stage_fp_job(), stage_bt_job(), stage_upload()); lookup_item() / search_items() query the IM2 items table directly; promote_inbox_to_ro() is implemented in this file. The contract below still holds — every caller treats a raise as "nothing was written".

One entry point covers all three sources:

  1. stage_source_job("BT", source_id, user) (no bom_ref) → {"choices": [...]}, nothing staged yet.
  2. stage_source_job("BT", source_id, user, bom_ref=<picked>) → {"inbox_id": N}.

A raise from stage_source_job at any point means nothing was written — the route returns {"ok": false, "error": ...} with HTTP 502, and the operator can just retry.

Accept: promote or refresh (promote_inbox_to_ro) [Ver 2, 2026-09-06]

Dave, 2026-09-05: a re-import must refresh the RO the source job already produced, not create a second one. promote_inbox_to_ro() first matches source_id plus BOM name (newest first); if found it hands off to refresh_ro() and returns that existing order_no. refresh_ro():

On the fresh path it inserts ro_header (job_name falling back to customer_name / FP <id>, bom_name falling back to source_ref / Inbox <id>, carrying fp_material_list_id, fp_job_id, fp_job_cuid, source_system, source_id, note) then one ro_detail per ro_inbox_line, mapping would_hold to line_status='HOLD' and raising a ro_issue ('SKU not in QB' when the reason mentions QB, else 'no qty'). ro_header_job_bom_uniq (job_name, bom_name) makes a duplicate Accept fail cleanly.

A held line is non-blocking; the rest of the order picks, posts and orders normally. Once the last Open issue on a line is closed, ro.sync_line_hold() clears the HOLD by itself (Dave, 2026-09-05).

Status machine

Awaiting Review / Held / Error → (Accept) → Accepted, or → (Reject) → Rejected. Accepted and Rejected are terminal — every action route 400s on a terminal row. One exception, added 2026-09-05: POST /ro/{order_no}/delete (admin only, in ro.py) sets any ro_inbox row pointing at that RO back to status='Awaiting Review' with order_no, reviewed_by and reviewed_at cleared and a note — a deleted RO's source job returns to this queue.

Transaction rules (unchanged)

See the docstrings on stage_source_job() and promote_inbox_to_ro() in app/ro_import.py: a raise from either must never leave a half-written row. POST /ro/inbox/{id}/accept still updates ro_inbox in its own follow-up transaction, and on a raise sets status='Error' + parse_error and returns 502.

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.

Accept looked hung with no feedback (2026-09-15, Dave)

Accept on Import Inbox Detail can take ~10s (flush any dirty Fix-now edits, save the header, then create the RO) but the "Working…" message next to the buttons only got set right before the final fetch — the whole flush/save phase before it gave zero feedback, and the buttons stayed clickable the entire time. inboxAction() now sets Working… and disables all .actions-bar buttons before any of that starts, and clears/re-enables in a finally so a failed step still leaves the screen usable.

Fix-now Save gave "line 0 not found on inbox row N" (2026-09-15, Dave)

flushRow() computed the line number from row.children[0].textContent — the LINE # cell only when there's no pick-checkbox column. Once any line on the row is held (held_count > 0), a checkbox <td> is inserted as children[0] instead, which has no text, so Number('') came out 0 and the save failed. Every <tr> in the lines table now carries data-line="{{ ln.line_no }}"; flushRow() reads that instead of counting cells.

Qty box spinner arrows removed (2026-09-15, Dave)

"Remove the v^ in the qty box" — the native up/down spinner on <input type=number> is now hidden site-wide via a rule in base.html (::-webkit-outer/inner-spin-button + Firefox -moz-appearance:textfield), not just on this screen.