Create Purchase Invoice from Extraction
POST/api/purchase-invoices/ocr/:purchase_extraction_id/create-invoice
Accept the reviewed/edited extraction data (header + lines + cost lines) and create a PurchaseInvoice record, marking the extraction as 'confirmed'. The extraction must be in pending_review or duplicate_detected status (a duplicate-detected extraction is allowed because the user may confirm it is not actually a duplicate and proceed to create a new invoice).
The payload supports two kinds of line items:
lines— product line items, attached to PurchaseOrderLines on the target PO. Each may setpurchase_order_line_idto bind to a specific PO line, orignored: trueto skip persisting (kept for audit but not invoiced).cost_lines— non-product COST-classified financial lines (freight, duty, handling). These create FinancialLine records on the PurchaseInvoice keyed by a COST-classificationfinancial_line_type_id.allocate_to_productsis alwaystruefor OCR-created cost lines.
At least one of lines or cost_lines is required (lines is required_without:cost_lines).
Request body fields:
| Field | Type | Required | Notes |
|---|---|---|---|
| purchase_order_id | integer | yes | Must exist in purchase_orders. |
| invoice_number | string | yes | Max 255 chars. |
| invoice_date | date | yes | |
| due_date | date | no | |
| lines | array | required_without:cost_lines | Product lines. |
| lines[].description | string | no | |
| lines[].qty | numeric | yes | Negative values are accepted (e.g. PO-level discount rows surfaced as standalone negative lines). The manager filters out ignored: true and match_status: discount_confirmation lines before persisting. |
| lines[].unit_price | numeric | yes | Negative values are accepted (e.g. PO-level discount rows surfaced as standalone negative lines). The manager filters out ignored: true and match_status: discount_confirmation lines before persisting. |
| lines[].purchase_order_line_id | integer | no | Must exist in purchase_order_lines. |
| lines[].ignored | boolean | no | Skip persisting this line. |
| cost_lines | array | no | Non-product COST FinancialLines. |
| cost_lines[].financial_line_type_id | integer | yes (per cost line) | Must exist in financial_line_types and be COST classification. |
| cost_lines[].description | string | no | Max 500 chars. |
| cost_lines[].quantity | numeric | yes (per cost line) | Min 0. |
| cost_lines[].amount | numeric | yes (per cost line) | Min 0. Per-line amount (not total). |
| remember_mappings | boolean | no | When true, upserts (supplier_id, description) → financial_line_type_id mappings into supplier_description_mappings so future OCR imports auto-tag matching descriptions as cost lines (Tier 0 match). |
Response 201:
{
"data": {
"purchase_invoice_id": 312,
"extraction_id": 7
},
"message": "Purchase invoice created from OCR extraction."
}
Response 422 — wrong status (e.g. confirmed or failed):
{ "message": "Only extractions pending review can be used to create an invoice." }
Response 422 — validation failure (no lines or cost lines):
{
"message": "The lines field is required when cost lines is not present.",
"errors": { "lines": ["The lines field is required when cost lines is not present."] }
}
Request
Responses
- 200
Successful response