New Requirements Order
Create a Requirements Order — Technical Spec
Screen key ro_new; banner/tile name Create a Requirements Order (base.html screen_names).
Routes in app/ro_import.py: GET /ro/new, GET /ro/new/item/{sku}, GET /ro/new/item-search?q=, GET /ro/new/job-search?q=, POST /ro/new. Level2+ only on every one of them, the whole screen and not just Save (require_write(), WRITE_ROLES = admin/level1/level2).
### ⚠ Template/server drift — verify before trusting this page [Ver 2, 2026-09-06] The deployedapp/templates/ro_new.htmlis newer than the deployedapp/ro_import.py(checked 2026-09-06 against the live box). The template's header field is Class (<select id="roClass">populated from aclassescontext variable), it callsGET /ro/new/job-class, and it postsajax=1expecting a JSON reply. None of those three exist server-side yet: -manual_ro_form()passesro_typesonly — noclasses— so the Class dropdown renders with the(choose)option and nothing else until that context is added; - there is no/ro/new/job-classroute, so the class-from-job lookup 404s and the hint falls through to "No class on that job yet — choose one."; -POST /ro/newstill returnsRedirectResponse(303), so the template'sres.json()parse fails and the green "saved" panel rendersRO undefined saved.
>
The server contract documented below is whatro_import.pyactually does today. The parent thread owns deploying the matchingro_import.py(class list fromim2.setting_classes(), ajob-classroute, and anajaxJSON response); this doc must be re-checked the moment that lands.
Class list (the 2026-09-04/05 change)
The four RO classes are not a ro_class table. They live in app_settings under the ro_classes key (im2.py SETTING_DEFS, rendered as a textarea on Settings, one Class = QuickBooks class id per line; default Administration = 1202535, Commercial = 1202520, Residential = 1202519, Service = 1202540). im2.setting_classes() parses it into [{"name","id"}], sorted case-insensitively, de-duplicated by name; a line with no id is still offered but can carry no class onto a QuickBooks posting. Classes not on that list are ignored. Separately, ro_header.ro_type still has its own CHECK (Residential|Commercial|Service|Truck) and RO_TYPES is that list — Type and Class are different fields, and POST /ro/new currently validates the posted value against RO_TYPES, not against the class list.
SKU lookup / search
GET /ro/new/item/{sku} → lookup_item(sku): exact, case-insensitive, whitespace-tolerant match on IM2 items where archived_at is null, returning sku, item_name, description_po, uom, qty_on_hand, primary_bin, else 404. GET /ro/new/item-search?q= → search_items(): ILIKE across sku/item_name/description_po, min 2 characters, exact-SKU match first, limit 10. Both are best-effort: a failure returns an empty result set (or 502 on the single-SKU route), never a 500.
Job search
GET /ro/new/job-search?q= → search_jobs(): FieldPulse (search param on /v2.5/job, customer name attached via withCustomfieldsAndRelatedData) and QuickBooks (ro_pick.search_qb_jobs) run in parallel in a ThreadPoolExecutor, each cached for 120 s (_JOB_SEARCH_TTL, _cached(), errors never cached) — Dave 2026-09-06 called the uncached serial version "very slow". BuilderTrend is deliberately not searched (a live browser session, ~30 s). Result rows: {source: FP|QB, id, fp_job_id, name, detail}.
Save (POST /ro/new)
Form fields: job_name, bom_name, ro_type, date_required, notes, plus index-aligned repeated sku / description / qty / uom / line_type. (The live template additionally posts class_ref, job_source, job_source_id, qb_customer_id and ajax — all currently ignored by the handler.)
- Validates
job_name,bom_namenon-blank andro_type in RO_TYPES(400 otherwise);date_requiredparses toNoneif blank or unparseable (never defaulted — Ver 3 ruling C.2). - Skips rows with neither SKU nor description; a non-numeric qty is a 400 naming the line number; blank qty becomes 0.
line_typeanything butNONINVnormalises toINV. - Refuses (400) an
INVline with no SKU, and aNONINVline with neither SKU nor description. - Inserts one
ro_header(source_system/source_idleft NULL) then onero_detailper line withorigin='Manual',qty_source=qty, description falling back to the item master'sdescription_po/item_name/the SKU/"(no description)". - A SKU that does not resolve → line saved as
NONINV, typed SKU moved intosrc_ref(never discarded), plus an openro_issue(issue_type='SKU not in QB') so it lands on Issues to Resolve with the import-parser issues. A raise fromlookup_itemis treated exactly like "not found". - One
ro_auditrow (action='manual_ro_create', note "created manually, N line(s)"), then 303 redirect to/ro/{order_no}?created=1(which shows the green "created manually" banner on the Requirements Order screen).
Not implemented
- The brief's "warn if the job name doesn't match a QB customer": the job typeahead now searches QB customers/jobs, but the save path stores no
qb_customer_idand performs no match check. - Nothing here writes
priority_code, cost, or anything in QuickBooks.