diff --git a/CHANGELOG.md b/CHANGELOG.md index 563bfb8..202fa18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ All notable changes to this project are recorded here. The format follows [Keep ### Added +- Phase 6: Q.C. The Q.C. screen (the numbers Waiting for inspection, Accepted last 7 days and Rejected last 7 days; the boards Ready for inspection, Accepted and Rejected back with supervisor; the find bar; the Project folders grid with Active and All). The Q.C. tab on the packet page: the send details read-only as the supervisor set them (book, spool, location, note), inspector and stamp, the ten check items in two groups (Data book package, Physical spool) with the live hints (heats recorded, MTRs attached, welds on the map, checklist progress) ticked one key at a time, the visual weld inspection table with Accept and Reject per weld and a note, NDE method (None required, RT, UT, MT, PT), result (Acceptable, Rejectable) and report number, hardness labelled NACE when the WPS says so, inspection notes, and the earlier inspections. The check texts live in the `qc_check_items` settings row (migration 0007). +- Q.C. accept with a route (Clerical – Data Book, Shipping, or a shop department) and a PIN signature, blocked on the server until every check is done, every weld has a visual result and none is rejected, the NDE result is Acceptable when a method is set, and an inspector is named; the missing items are listed on the bar. Q.C. reject to the supervisor with a reason, an NCR number (NCR-YYYY-NNNN, linked to the NCR system) or both, with a signature; the inspection closes into the history with its snapshot and a fresh one opens when the supervisor sends the packet back to Q.C. The supervisor and the station see the rejection, its NCR and the rejected welds at the top of the packet. +- Q.C. project folders: documents per project and per unit (ITP, WPS / PQR, NDE procedure, Hydrotest procedure, PMI / material, Calibration certificate, Customer spec, Other), added by Q.C. or Clerical; a new revision supersedes the old one, which stays under Superseded; nothing is deleted. Project documents apply to every unit. A unit without an ITP is flagged. +- Packet documents for the data book on the Documents tab: NDE, hydrotest, dimensional, PMI, coating reports, certificates of conformance, customer inspection reports, NCRs and Other, with a note; "Take out of data book" keeps the row and the file with who, when and why; the unit's and project's Q.C. folder documents are listed as also in the data book. +- The digital ITP per unit, started from the Q.C. template ITP_Generation_Revised_2 (frozen into the ITP at creation: nine sections over the pages Weld Parts, Code and Final Assembly; items with the PESCO code, the customer code and the acceptance criteria; the legend O, R, A, I, AU, W, H, NA): line initials with a timestamp, undo by the same inspector or an admin before the block is signed, N/A per line or per section, customer code, witness and date, notes, rejects with an NCR number that must be entered before the line can be approved, signature blocks after sections 3 (covering 1 to 3), 4, 5, 6, 7, 8 and 9 signed with a PIN once every line is approved or N/A, QN number, remarks and the customer complete record per block, sign-off with a signature once every block is signed, reopen by a Q.C. supervisor or an admin with a reason, and the ITP log. Filing the signed ITP PDF is a hook that phase 7 implements. +- Hold-point warnings: an open H or W point, or an open rejection, on a line not yet done shows as a warning on the unit's packets, filtered by the department's page group (01, 02, 03, 04, 05 and 10 see Weld Parts; 06, 07 and 13 see Code; 09 sees both; 08, 11, 12 and 14 see Final Assembly). Nothing is blocked. +- New endpoints: `GET /api/qc/home`, `GET /api/qc/folders`, `GET /api/qc/folders/projects/{id}`, `GET /api/qc/folders/units/{id}`, `POST /api/qc/docs`, `PUT /api/packets/{id}/qc/checks/{key}`, `PUT /api/packets/{id}/qc/welds/{no}`, `PATCH /api/packets/{id}/qc` with field-level compare-and-set, `GET`/`POST /api/units/{id}/itp`, and the ITP writes under `/api/itps/{id}`. New policy actions `qc_doc_add`, `itp_start`, `itp_line_sign`, `itp_line_unsign`, `itp_block_unsign`; the scoped check for writes that belong to a project or unit rather than a packet. +- Packet history texts as the old build: "Q.C. checked: …", "VT accept · W3", "NDE method: RT", "Q.C. accepted → Clerical – Data Book", "Q.C. rejected → supervisor", "Document added", "Document taken out of the data book"; the ITP log: "ITP started", "ITP line 1.3 approved", "ITP line 1.3 rejected", "ITP block 3 signed", "ITP signed off", "ITP reopened"; the audit: "Q.C. document added", "Q.C. document revised". +- Tests: every Q.C. and ITP action as every role and status against a policy oracle, each `qc_missing` case, reject into history and the fresh inspection, Q.C. document supersession, document take-out, ITP line compare-and-set and unsign rules, block gates, the sign-off gate and the PDF hook, reopen, hold points per department page group; frontend tests for the inspection form, the Q.C. bar, the ITP page and the hold callout; Playwright scenarios for accept, reject with an NCR, the Q.C. folders, packet documents and the digital ITP through sign-off. +- docs/SCHEMA.md, POLICY.md and SECURITY.md updated; PLAN.md open questions 27 to 31. + - Phase 5: the shop. The Supervisor screen (department chips for people who lead several departments, the change-order headline that cannot be closed, the numbers Ready to assign, At stations, Waiting on review and Station hours vs target, the Change orders on hold table, the station grid with a tile per station showing the job, the welder and stamp, the ticking clock against the target hours and a CHANGE ORDER tag, and the boards Ready to assign, Waiting on review, At Q.C., With warehouse and Routed on) and the Stall screen (station picker, the station hero, My packets and Submitted, waiting on supervisor). - The shop steps on the packet page, driven by `can` and `blocked`: assign with the station select, a welder picker with search and the stamp filled from the welder's record; pull back to the queue (the clock freezes); Start work, Pause, Resume; Submit to supervisor with a PIN signature and the missing steps listed as links (checklist items, no welds on the drawing, filler metal); the supervisor's review with Data book package ready, Spool ready for inspection, the spool location and a note before Send to Q.C. (signature), Return to station with a return note, and Skip Q.C. with a route to another department (signature); a lead sees the review read-only. Send back to Warehouse and Send back to Clerical with reasons. - The Weld map tab: the drawing rendered with pdf.js at scale min(2.2, 4200/width), pan, pinch and wheel zoom, FIT, full screen; Mark a weld then tap the joint; welds drawn as a leader line and arrowhead in pink with the id in a white circle; drag the bubble or the arrow; the weld sheet (work order, piece, type from Fillet, Groove, Fill, Seal, Tack, size default 5/16", process from GMAW, FCAW, SMAW, GTAW, SAW, stamp, the BOM items it joins) with Log weld, Save changes and Remove weld; the welds-on-this-sheet list; the weld record (filler, filler lot, preheat, notes) with field-level compare-and-set. Marks from an earlier department show as carried. diff --git a/README.md b/README.md index 1518ad1..f7de906 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ This repo is the rebuild of Packet Station as a server application: Docker Compo ## Status -**Rebuild in progress. Phase 5 done: the Supervisor and Stall screens, the weld map, the heat table, the station clock and change-order verification.** +**Rebuild in progress. Phase 6 done: Q.C. inspection, accept and reject, Q.C. project folders, packet documents and the digital ITP.** -People can sign in, set up projects and units, make hand drafts, attach a drawing, and send a packet to the warehouse, a department or Q.C. The worker reads released work orders from VISUAL on a schedule, makes and updates drafts, opens change orders on packets already in the shop, looks drawings up in M-Files and attaches them at the rev VISUAL calls. Clerical sees it all on the From VISUAL panel and in the Flagged and Changes to approve columns; Admin sets the feed up on the Feed tab. Warehouse works its task board (waiting, being pulled, on hold, pulled, released), pulls parts line by line with heat numbers, shorts, receipts and MTRs, and releases to the supervisor once every line is pulled or short and every pulled line has a heat. A supervisor assigns the packet to a station and a welder, the welder starts the clock, marks welds on the drawing, places the heat table, finishes the checklist and submits with a PIN signature, and the supervisor sends it to Q.C. or routes it to another department with the marks carried. Department supervisors verify change orders with a signature. Every packet write goes through the server policy. The Q.C. screen is still a placeholder; its API exists. The old build on `M:\Production\Packet Station` stays the live system until go-live. The new system starts empty; nothing is migrated from M:. +People can sign in, set up projects and units, make hand drafts, attach a drawing, and send a packet to the warehouse, a department or Q.C. The worker reads released work orders from VISUAL on a schedule, makes and updates drafts, opens change orders on packets already in the shop, looks drawings up in M-Files and attaches them at the rev VISUAL calls. Clerical sees it all on the From VISUAL panel and in the Flagged and Changes to approve columns; Admin sets the feed up on the Feed tab. Warehouse works its task board (waiting, being pulled, on hold, pulled, released), pulls parts line by line with heat numbers, shorts, receipts and MTRs, and releases to the supervisor once every line is pulled or short and every pulled line has a heat. A supervisor assigns the packet to a station and a welder, the welder starts the clock, marks welds on the drawing, places the heat table, finishes the checklist and submits with a PIN signature, and the supervisor sends it to Q.C. or routes it to another department with the marks carried. Department supervisors verify change orders with a signature. Q.C. inspects the packet on its own tab (the check items, a visual result per weld, NDE, hardness, notes), accepts it to the data book, to Shipping or on to another department with a PIN signature, or rejects it back to the supervisor with a reason or an NCR number. Q.C. keeps project folders of documents per project and unit (ITP, WPS / PQR, procedures, certificates) where a new revision supersedes the old one and nothing is deleted, adds reports to a packet for the data book and takes them out with a reason, and works each unit's digital ITP from the Q.C. template through line initials, N/A, rejects with NCR numbers, section block signatures and a sign-off. Open hold and witness points show as a warning on the unit's packets. Every packet write goes through the server policy. The old build on `M:\Production\Packet Station` stays the live system until go-live. The new system starts empty; nothing is migrated from M:. | Area | Exists yet? | |---|---| @@ -19,7 +19,8 @@ People can sign in, set up projects and units, make hand drafts, attach a drawin | VISUAL feed, M-Files drawings, change orders from the feed, From VISUAL panel, Admin Feed tab and resource map | Yes (phase 3, run once against VISUAL DEV and the vault from a developer machine; fixture-driven in CI) | | Warehouse screen, pull table, shorts and receipts, MTRs, release gate, upload kinds, optional ClamAV, storage alert | Yes (phase 4, verified in the CI stack on docker-1) | | Supervisor and Stall screens, weld map, heat table, station clock, change-order verification | Yes (phase 5, verified in the CI stack on docker-1) | -| Q.C., close and file, PDFs | API only (phase 2); screens in phases 6 and 7 | +| Q.C. screen, inspection, Q.C. folders, packet documents, digital ITP, hold-point warnings | Yes (phase 6, verified in the CI stack on docker-1) | +| Close and file, PDFs | API only (phase 2); phase 7 | | Project Management, Admin, notifications, backups | No (phases 8 and 9) | | Regression bench and go-live | No (phase 10) | @@ -84,6 +85,8 @@ The `packetstation` command has `doctor`, `migrate`, `app`, `worker`, `job run < Phase 3 adds the worker (`packetstation worker`: APScheduler, the `visual-feed`, `drawing-lookup` and `housekeeping` jobs, settings re-read on every fire, run now through `job_requests`), the VISUAL and drawing cache tables (migration 0004), `/api/feed/*`, `/api/jobs/*`, `/api/admin/feed/*`, rev approval and hand uploads under `/api/packets/{id}`, and the synthetic fixture generator (`python -m tests.fixtures.generator --out DIR`) that CI feeds the worker with. See [docs/FEED.md](docs/FEED.md). +Phase 6 adds the Q.C. screen (`/qc`: numbers, boards, project folders), `GET /api/qc/home`, `GET /api/qc/folders` with the project and unit folder reads, `POST /api/qc/docs` (a new revision supersedes the old row), the per-key Q.C. endpoints under `/api/packets/{id}/qc`, document kinds as a closed set with take-out reasons, and the digital ITP under `/api/units/{id}/itp` and `/api/itps/{id}` (start, line sign and unsign, N/A, reject with NCR, block sign, sign-off, reopen, log). The ITP PDF filed on sign-off is a hook for phase 7. See [docs/POLICY.md](docs/POLICY.md) "Q.C. (phase 6)". + CI stack with the Samba domain controller and the Playwright scenario, on a Docker host: ```bash diff --git a/docs/PLAN.md b/docs/PLAN.md index d4253da..d2ca3c6 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -251,6 +251,14 @@ From phase 5 (2026-10-16), for Seth to veto: 25. The old build's manual "copy marks to the new sheet" button on a change order is not ported. On a page replacement only the heat table placement is copied to the new page. Welds and heat tags stay on the VOID page. Default: not ported in phase 5; a follow-up. 26. Checklist templates live in the `checklist_templates` settings row, with the old lists as defaults. Default: no screen edits them in phase 5; Admin editing comes in phase 9. +From phase 6 (2026-10-17), for Seth and Q.C. to veto: + +27. Q.C. reject: the old build took any non-empty reason. Default: a reason of at least 8 characters, or a well-formed NCR number (`NCR-YYYY-NNNN`) in its place, or both. The NCR is stored on the inspection and linked to pesco-ncr. Applied in phase 6. +28. A filed packet in a unit and the unit's ITP: the ITP belongs to the unit, not to a packet, so ITP writes never look at a packet's status. Default: a filed packet does not lock the ITP. This resolves the phase 5 follow-up. Applied in phase 6. +29. ITP status: the 0003 enum had `open`, `signed` and `reopened`. Default: `itp_status` gains `complete` for a signed-off ITP; a reopened ITP is `open` again with the reopen recorded. `signed` and `reopened` stay in the enum unused, since PostgreSQL cannot drop an enum value. Applied in phase 6. +30. Removing an ITP block signature: the old build let any Q.C. editor undo it. Default: the signer or an admin, while the ITP is open. The block's QN, remarks and customer fields stay. Applied in phase 6. +31. The signed ITP PDF on sign-off: default a hook (`pdf.itp.file_signed_itp_pdf`) that phase 7 builds. Today it files nothing, the sign-off log says `PDF filing reserved for phase 7`, and the automatic `qc_docs` ITP row is written only when the hook returns a file. Applied in phase 6. + ## Verification - Every phase: `pytest` server suite, `npm test` and `npm run build` for the frontend, `docker compose -f deploy/compose.ci.yml up` with the Samba DC and the Playwright scenarios for that phase, `packetstation doctor` inside the image. diff --git a/docs/POLICY.md b/docs/POLICY.md index 9430968..252bea5 100644 --- a/docs/POLICY.md +++ b/docs/POLICY.md @@ -4,7 +4,7 @@ Who may do what to a packet, and when. This file is written from the code in `se ## Purpose -The policy is a Python port of the old `guard.js`. It runs only on the server. Every packet write calls `policy.check()` inside the write transaction, under the packet row lock. Every packet read returns `can`, `blocked` and `holder` from the same rules, so the screens only show what the server will accept. +The policy is a Python port of the old `guard.js`. It runs only on the server. Every packet write calls `policy.check()` inside the write transaction, under the packet row lock. Every packet read returns `can`, `blocked` and `holder` from the same rules, so the screens only show what the server will accept. The Q.C. folder and ITP writes belong to a project or a unit, not to a packet; they call `policy.check_scoped()` under their own row lock (see "Scoped checks"). The package is pure functions with no I/O: @@ -12,7 +12,7 @@ The package is pure functions with no I/O: - `principal.py`: `Principal`, `PacketState`, `principal_from_roles()`. - `custody.py`: `holds()`, `works_on()`, `holder_name()` and the Who predicates. - `preconditions.py`: the named preconditions and their messages. -- `engine.py`: `check()`, `evaluate()`, `can()` and `Decision`. +- `engine.py`: `check()`, `check_scoped()`, `evaluate()`, `can()` and `Decision`. ## Roles @@ -101,8 +101,6 @@ Statuses in workflow order: draft, warehouse, ready, assigned, in_progress, revi | edit_wos | CL | draft | | | | | | | | edit_bom | HOLDER | draft, warehouse, SHOP | | | | | | | | wh_receive_short | WH | any | | | | short_open | | | - -A holder of the packet who is not in the warehouse receives a short through edit_bom (see "Receive by custody"). | edit_heats | HOLDER | warehouse, SHOP | | | | | | | | edit_heat_table | HOLDER | warehouse, SHOP | | | | | | | | edit_welds | HOLDER | SHOP | | | | | | | @@ -123,17 +121,28 @@ A holder of the packet who is not in the warehouse receives a short through edit | co_reject | CL | any | | | | reason_given | Reject a change order | yes | | co_hold_toggle | CL | any | | | | | | | | co_verify | CO_VERIFIER | any | | | | | Verify a change order for the department | | -| itp_edit_line | QC | any | | | | | | | -| itp_block_sign | QC | any | | | | | Sign an ITP block | | -| itp_sign_off | QC | any | | | | | Sign off the ITP | | -| itp_reopen | QC_SUP (Admin through the bypass) | any | | | | reason_given | | yes | +| qc_doc_add | CL_OR_QC | no packet (scoped) | | | | | | | +| itp_start | QC | no packet (scoped) | | | | | | | +| itp_line_sign | QC | no packet (scoped) | | | | | | | +| itp_line_unsign | QC | no packet (scoped) | | | | | | | +| itp_edit_line | QC | no packet (scoped) | | | | | | | +| itp_block_sign | QC | no packet (scoped) | | | | | Sign an ITP block | | +| itp_block_unsign | QC | no packet (scoped) | | | | | | | +| itp_sign_off | QC | no packet (scoped) | | | | | Sign off the ITP | | +| itp_reopen | QC_SUP (Admin through the bypass) | no packet (scoped) | | | | reason_given | | yes | | vis_update_draft | SYSTEM | draft | | | | | | | | vis_retire_draft | SYSTEM | draft | | | | | | | | vis_open_co | SYSTEM | any but draft and filed | | | | | | | | vis_auto_release | SYSTEM | draft | warehouse | | | heat_map_answered, drawing_attached | | | +A holder of the packet who is not in the warehouse receives a short through edit_bom (see "Receive by custody"). + The `vis_*` actions are system only. People can never do them, and the worker can do nothing else. +The Q.C. folder and ITP actions ("no packet (scoped)") belong to a project or a unit, not to a packet. They are checked with `check_scoped()` (see "Scoped checks"), never with the packet status, custody or the filed lock. + +qc_reject takes an NCR number in place of the reason text (see "Q.C. (phase 6)", "Reject"). + ## Preconditions Each precondition is a named predicate over the packet state and the request input. They are never bypassed, Admin included. The first one that fails gives the message. @@ -176,6 +185,17 @@ Each precondition is a named predicate over the packet state and the request inp The packet API answers 403 with the code for most failures, and 400 for `it_reason_required` and `reason_required`. +### Scoped checks + +`check_scoped(principal, action, *, extra=None, it_reason=None) -> Decision` runs the same steps as `check()` without a packet, for the actions that belong to a project or a unit (the Q.C. folder and the ITP): + +1. **System.** A system principal fails (`forbidden`). A system-only action fails every person (`system_only`). +2. **Admin bypass.** An admin not allowed by the rule's Who passes with an IT reason of at least 8 characters, else `it_reason_required`. The decision is marked `admin_bypass`. +3. **Who.** Code `forbidden`, "Your access doesn't allow this." +4. **Preconditions,** the input ones in rule order. `reason_given` fails as `reason_required`. + +There is no removed, status, change-order hold, filed lock or custody step: there is no packet and no holder. The ITP and Q.C. folder writes call it inside their own transaction, under the `itps` row lock or the lock on the Q.C. document being replaced. They never take a packet row lock, because they change no packet and must not wait on, or block, the packets of the unit. + ## Signatures These actions need a signature: submit, send_to_qc, route_from_review, qc_reject, qc_accept, qc_accept_and_route, file, admin_reopen, edit_document_set (attach only), rev_approve, co_issue, co_approve, co_reject, co_verify, itp_block_sign and itp_sign_off. The rule's signature text is the meaning that is signed. @@ -190,7 +210,8 @@ The request carries `sign: {factor, secret}`. The write pipeline calls the phase - **Header scalars** use field-level compare-and-set: the body sends `set` and `base`. A key whose base differs from the current value is 409 `field_conflict` with the current value. A key with no base is written as is. - **Work orders** are a draft-only full replace with If-Match on the packet version. - **Shorts and change-order actions** are state compare-and-set. Checklist keys are written one key at a time. The heat table position is an idempotent upsert. A change-order verification is an insert, so a second one is "already verified". -- **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, project_renumbers, itp_rejects and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them. +- **ITP lines** are approved under the `itps` row lock, with a state check and an optional `If-Match: `. ITP header fields use the same `set` and `base` field compare-and-set as packet headers. +- **Append-only tables** never conflict: packet_transitions, packet_log, qc_history, short_receipts, project_renumbers, itp_rejects, itp_log and co_verifications. Triggers refuse UPDATE, DELETE and TRUNCATE, and the app role has no grant for them. ## Reads: can, blocked and holder @@ -433,6 +454,175 @@ The `action` and `note` columns of `packet_log` for each shop write. A departmen The submit hours are the stage's `clock_elapsed_ms` after the stop, with one decimal. +## Q.C. (phase 6) + +### Who inspects + +Q.C. holds a packet in status qc. EDIT_QC (QC, status qc) covers every write to the open inspection: the check items, the visual result per weld, the NDE fields, hardness, the inspector name, the stamp and the notes. A write with no open inspection is 409 `no_inspection`. + +| Write | Endpoint | Concurrency | +|---|---|---| +| One check item | `PUT /api/packets/{id}/qc/checks/{key}` `{done}` | one key by `jsonb_set` under the packet row lock, no If-Match. Unknown key 422 `invalid_key`. | +| One weld's visual result | `PUT /api/packets/{id}/qc/welds/{no}` `{result?, note?}` | one weld by `jsonb_set`. The weld must be live on the packet (422 `invalid_weld`). `result` is `accept`, `reject` or empty. | +| The other fields | `PATCH /api/packets/{id}/qc` `{set, base}` | field-level compare-and-set over `inspector`, `stamp`, `nde_method`, `nde_result`, `nde_report`, `hardness`, `notes`; 409 `field_conflict` with the current value. | + +`nde_method` must be one of `None required`, `RT`, `UT`, `MT`, `PT` (422 `invalid_field` "Pick an NDE method from the list."). `nde_result` is empty, `Acceptable` or `Rejectable`. Setting the method to `None required` clears the result and the report number. The phase 2 flat body (with `If-Match` on the inspection's `row_version`, merging `checks` and `weld_exam`) still works; a body with `set` selects the field compare-and-set. The NDE rules apply to both. Hardness is labelled `Hardness (NACE)` when the packet's WPS contains `NACE`. The server computes this; no request sets it. + +### Accept + +Two actions accept. Both are Q.C. from status qc, held by an open change order, gated by `qc_missing_empty`, and signed. + +| Route | Action | Body | Result | +|---|---|---|---| +| Clerical – Data Book | qc_accept | `routed_to: "databook"` | complete, routed to the data book | +| Shipping | qc_accept | `routed_to: "shipping"` | complete, routed to Shipping | +| A shop department | qc_accept_and_route | `dept: "09"` | ready in that department, a new stage opened | + +The signature meanings are "Q.C. accept" and "Q.C. accept and route". The accept closes the inspection into `qc_history` and closes the stage with the inspection as its `qc_snapshot`. The note in the log counts every live weld of the packet. + +### What Q.C. still needs + +`qc_missing_empty` reads the list built by `checklists.qc_missing()`. Its message is `Q.C. still needs: ` followed by the parts joined by `, ` and a full stop. `blocked.qc_accept` and `blocked.qc_accept_and_route` carry it, so the screen shows Accept disabled with the reason. + +| Part | When | +|---|---| +| `3 inspection items` | that many check items are not done | +| `2 welds without a visual result` | that many live welds have no accept or reject | +| `a rejected weld (reject the packet instead)` | any weld is rejected | +| `NDE result` | an NDE method other than `None required` is set and the result is not `Acceptable` | +| `inspector name` | the inspector is empty | + +### Reject + +qc_reject is Q.C. from status qc back to review, signed ("Q.C. reject"). It takes a reason, an NCR number, or both: + +- An NCR number goes through `validate_ncr`: trimmed, upper case, then the pattern `NCR-` four digits `-` four digits. A bad one is 422 `invalid_ncr` "Enter an NCR number like NCR-2026-0021." +- The reason checked by `reason_given` is the NCR alone (`NCR-2026-0021`) when no text is given, else `NCR-2026-0021 · `. So an NCR number stands in for the reason, and the text has no minimum when an NCR is given. +- With neither, the answer is 400 `reason_required` with the rule's message. A reason without an NCR needs 8 characters. + +The reject stores the NCR on the inspection, sets the packet's `return_note` to the reason, and closes the inspection into `qc_history` at once. The packet in review has no open inspection; the read model gives the reject as `qc_last`, and the screens show the "Q.C. rejected" callout from it. The supervisor's next send to Q.C. opens a fresh inspection. + +The change-order hold applies to both accepts and to file. It does not apply to the reject: Q.C. can always send a packet back. + +The NCR link is `https://ncr.pescoinc.biz/ncrs/`. `domain/ncr.py` `validate_ncr` is the hook for checking the number against pesco-ncr. Today it accepts any well-formed number. The check against pesco-ncr is a later phase. + +### Packet documents + +DOC_ADD and DOC_REMOVE are Clerical or Q.C. (CL_OR_QC), in any status but filed (the filed lock applies). + +- `POST /api/packets/{id}/docs` `{asset_id, kind, title?, note?}`. `kind` is one of the nine kinds (see [SCHEMA.md](SCHEMA.md), "Packet documents"); any other is 422 `invalid_kind` "Pick a document type from the list." The asset must be a PDF, PNG or JPEG (422 `invalid_asset`). The title defaults to the kind's label and is up to 160 characters. The note is up to 300 characters. +- `DELETE /api/packets/{id}/docs/{doc_id}` `{reason}` takes a document out of the data book. The reason needs 3 characters after cleaning (400 `reason_required` "Say why it comes out of the data book."). The row is kept with who, when and why. A second take-out is 409 `already_removed`. + +Drawings are never packet documents. They come from M-Files or the hand attach only. + +### Q.C. folder documents + +QC_DOC_ADD is Clerical or Q.C. (CL_OR_QC), checked with `check_scoped()`. `POST /api/qc/docs` `{project_id | unit_id, type, title?, rev?, note?, asset_id, replaces?, it_reason?}`: + +- Exactly one of `project_id` and `unit_id` (422 `invalid_request`). +- `type` is one of `ITP`, `WPS / PQR`, `NDE procedure`, `Hydrotest procedure`, `PMI / material`, `Calibration certificate`, `Customer spec`, `Other` (422 `invalid_type` "Pick a document type from the list."). +- The asset must be a PDF, PNG or JPEG (422 `invalid_asset`). Limits: title 140 (defaults to the type), rev 12, note 600. +- `replaces` files a new revision. The replaced row must be a current document of the same folder (422 `invalid_replace`). One that already points at a newer row is 409 `already_superseded`. The old row is kept and points at the new one. Nothing is deleted. + +Reads (`GET /api/qc/home`, `/api/qc/folders`, the project and unit folder reads) need any signed-in session. PM reads and changes nothing. + +Clerical may add Q.C. folder documents. The old build allowed Q.C. and Admin only. This is wider than the old build; Seth can narrow it to QC. + +### The ITP + +Each unit has at most one digital ITP, started from the code template `ITP_Generation_Revised_2` and frozen at start. Reads (`GET /api/units/{unit_id}/itp`) need any session. Every write runs `check_scoped()` with its action, then finds the ITP (404), then refuses a complete one (409 `itp_complete`), locks the `itps` row, bumps `itps.version` once, and writes one `itp_log` row per change, one audit row for each (`itp.`, so `itp.itp_start`, `itp.itp_line_sign`; subject `itp`; detail `{text, note, unit_id}`, plus `signature_id` on a signed one) and a change event, in one transaction. Block sign and sign-off check their gates before the signature, so a refused step costs no PIN attempt, and check them again under the lock. + +Every line and block row is made at start. A Q.C. user's packet read still lists the scoped actions in `can`; they mean nothing on a packet. + +| Action | Who | Endpoint | Gate | +|---|---|---|---| +| itp_start | QC | `POST /api/units/{unit_id}/itp` | 409 `itp_exists` when the unit has one | +| itp_edit_line | QC | `PATCH /api/itps/{id}` (header), `PATCH .../lines/{key}`, `POST .../sections/{n}/na`, `POST .../lines/{key}/reject`, `POST .../lines/{key}/ncr`, `PATCH .../blocks/{key}` | see below | +| itp_line_sign | QC | `POST /api/itps/{id}/lines/{key}/sign` | the line is not approved (409 `already_signed`) or N/A (409 `line_na`); a rejected line needs its NCR (409 `ncr_required` "Enter the NCR # before approving this line.") | +| itp_line_unsign | QC | `POST /api/itps/{id}/lines/{key}/unsign` | the line is approved (409 `not_signed`); only the inspector who initialed it, or an admin (403 `forbidden` "Only the inspector who initialed this line, or an admin, can remove it."); refused while the block is signed (409 `block_signed` "Undo the section signature first.") | +| itp_block_sign | QC, signature "Sign an ITP block" | `POST /api/itps/{id}/blocks/{key}/sign` | every line of the block done (409 `block_not_ready` "Every line must be initialed or N/A first."); not already signed (409 `already_signed`) | +| itp_block_unsign | QC | `DELETE /api/itps/{id}/blocks/{key}/sign` | the block is signed (409 `not_signed`); the signer or an admin (403 `forbidden` "Only the inspector who signed this block, or an admin, can remove it."); clears the signature and keeps the block's fields | +| itp_sign_off | QC, signature "Sign off the ITP" | `POST /api/itps/{id}/signoff` | every block signed (409 `blocks_unsigned` "Every section must be signed first.") | +| itp_reopen | QC_SUP, or an admin through the bypass | `POST /api/itps/{id}/reopen` `{reason, it_reason?}` | a reason of 8 characters; the ITP is complete (409 `itp_open` "The ITP is open already."). An admin who gives no `it_reason` has the reason used as both. | + +Line approval is a compare-and-set: the `itps` row lock, the state check above, and an optional `If-Match: ` (409 `row_conflict`). Approval sets `result approve`, `by`, `at` and the initials (the first letter of each word of the display name, upper case, at most three). Unsign clears the result, the approver, the time and the initials, so the line is open again. The line's `ncr` and every `itp_rejects` row stay. + +The itp_edit_line writes: + +- Header (`no`, `client`, `pname`, `pno`, `po`, `unit`): field compare-and-set with `{set, base}` (409 `field_conflict`), 120 characters each. `no` cannot be empty (422 `invalid_field`). +- Line details: `na` is refused on an approved line (409 `already_signed`) or one with an open reject (409 `line_rejected`). `cust_code` is one of `R`, `R/AU`, `AU`, `W`, `H`, `NA`, or empty for the template's (422 `invalid_field` "Pick a customer code from the list."). `cust_by` 80, `note` 500. `cust_at` defaults to today when a name is given. A change that changes nothing writes no log and no version. +- Section N/A: sets or clears N/A on every line of the section. Marking N/A is refused when any line of the section is approved (409 `already_signed`) or has an open reject (409 `line_rejected`). +- Reject: refused on a line that is approved (409 `already_signed`), N/A (409 `line_na`) or already rejected (409 `line_rejected`). The NCR is optional and validated when given. An `itp_rejects` row (kind `reject`) is added. +- NCR entered later: the line must have an open reject without an NCR (409 `no_open_reject`). An `itp_rejects` row (kind `ncr`) is added. +- Block fields: `qn` 40, `remarks` 400, `cust_name` 80, `cust_at` (today when a name is given), `cust_clear` clears the customer entry. An empty `cust_name` without `cust_clear` is 422 `invalid_field` "Enter the customer rep's name." No signature. + +A write to a complete ITP, other than the reopen, is 409 `itp_complete` "The ITP is signed off and locked. An admin can reopen it." The sign-off sets status `complete` and calls the phase 7 hook `pdf.itp.file_signed_itp_pdf()`. Today the hook returns nothing. When it returns an asset, the sign-off sets `signoff_asset_id` and files a `qc_docs` row on the unit (type `ITP`, `auto`), superseding any earlier automatic ITP document of the unit. A reopen sets status `open` with the reason. Block and line signatures stay; the inspector undoes what must change. + +ITP actions never look at a packet. A filed packet in the unit does not lock the ITP, because the ITP belongs to the unit (the phase 5 follow-up, open question 28). + +### Blocks + +A signature block closes at every template section marked to sign, and covers the sections since the last block. The template gives blocks `3` (sections 1 to 3), `4`, `5`, `6`, `7`, `8` and `9` (the final block). A line is done when it is N/A or approved. A block is ready when every line in it is done, so a block of N/A lines only can be signed. + +### Hold points + +A hold code is `H` or `W`, alone or after a slash (`R/H`), on the PESCO code or the customer code (the line's own customer code when set, else the template's). An open hold point is a line with a hold code that is not done, or any rejected line. + +The packet read gives `itp_hold` while the unit's ITP is open: the open hold and rejected points, filtered by the packet's department page group. It is a warning only. No precondition reads it, and no step is blocked by it. Q.C. and the shop check with each other before moving past a point. + +| Department | Pages it sees | +|---|---| +| 01, 02, 03, 04, 05, 10 | WP (Weld Parts) | +| 06, 07, 13 | C (Code) | +| 09 | WP and C | +| 08, 11, 12, 14 | FA (Final Assembly) | +| CL, WH, QC, or an unknown code | every page | + +### Log texts + +The `action` and `note` of `packet_log` for each Q.C. packet write. + +| Write | action | note | +|---|---|---| +| Check ticked | `Q.C. checked: ` | | +| Check unticked | `Q.C. unchecked: ` | | +| Visual result set | `VT accept · W3`, `VT reject · W3` | the note when given | +| Visual result cleared | `VT cleared · W3` | | +| Weld note only | `Q.C. record changed` | `W3 note` | +| NDE method | `NDE method: RT`, `NDE method: -` when cleared | | +| NDE result | `NDE result: Acceptable`, `NDE result: -` when cleared (no line of its own when `None required` cleared it) | | +| Other Q.C. fields | `Q.C. record changed` | the changed fields, `inspector Quinn QC · stamp Q1 · hardness 198 HB · report RPT-1 · notes changed` (` cleared`, `notes cleared`) | +| Flat-body checks or weld exam merge | `Q.C. record changed` | | +| qc_accept to the data book | `Q.C. accepted → Clerical – Data Book` | `Quinn QC [Q1] · 3 welds VT accepted`, plus ` · RT Acceptable` when an NDE method other than None required is set, plus ` · ` when the step carries a note | +| qc_accept to Shipping | `Q.C. accepted → Shipping` | the same | +| qc_accept_and_route | `Q.C. accepted → 09 Hydro` | `Quinn QC [Q1] · from 03 Weld Parts` | +| qc_reject | `Q.C. rejected → supervisor` | `NCR-2026-0021 · · welds W1, W3` (parts present only; the rejected welds by number) | +| Document added | `Document added` | `