## Overview

The Purchase Orders importer creates or updates Purchase Orders **and** their Purchase Order Lines from a single CSV. The CSV is denormalized: one row per PO line, with the PO header columns repeated across every row that belongs to the same PO. Example: two POs, three lines.

| po_number | supplier | currency | part_number | quantity | cost |
| --- | --- | --- | --- | --- | --- |
| PO-001 | Acme | USD | WIDGET-A | 10 | 5.00 |
| PO-001 | Acme | USD | WIDGET-B | 5 | 12.00 |
| PO-002 | Globex | EUR | GADGET-X | 1 | 100.00 |

Rows sharing the same `po_number` are merged into a single PO header. If a header column is included in your CSV, it must be populated on **every row of that PO** and all values must match. Any blank cell or any value that disagrees with another row of the same PO fails the import with a per-conflict error before any data is written.

## Header columns

These describe the PO itself. If you include a header column in your CSV, it must be present and identical on every row that shares a `po_number`. Otherwise the import fails.

| Column | Required | Description |
| --- | --- | --- |
| `po_number` | Yes | PO identifier. The grouping key. |
| `original_po_number` | No | Existing PO number when renaming. **Lookup column**: an empty cell means “no rename,” not `""`. |
| `po_description` | No | Free-text PO description. |
| `supplier` | No | Supplier name. Must match an existing supplier. |
| `ship_to_location` | No | Ship-to location name. Must match an existing location. |
| `bill_to_location` | No | Bill-to location name. Must match an existing location. |
| `currency` | No | Currency type, such as `USD` or `EUR`. Must match an existing currency. |
| `ordered_at` | No | Order date. Parsed flexibly (any pandas-recognized date format). |
| `assigned_to` | No | Email of the user the PO is assigned to. Must match an existing user. |
| `po_intent_option` | No | Intent option value at the PO level. Prefixed with `po_` to disambiguate from the line-level `intent_option`. |
| `po_labels` | No | Semicolon-delimited list of label values to attach to the PO. |
| `terms_and_conditions` | No | Title of a Requirement with type `TERMS_AND_CONDITIONS`. Overrides the org’s default. |
| `fees` | No | Semicolon-delimited list of fee records. See [Fees](https://docs.firstresonance.io/administration/ion-importers/purchase-orders#fees) below. |

## Line columns

These describe a single PO line. Every row contributes one line if any line column is populated.

| Column | Required | Description |
| --- | --- | --- |
| `part_number` | No | Part number for the line. Combined with `revision` to look up the part. |
| `revision` | No | Part revision. If omitted, the latest revision of `part_number` is used. |
| `description` | No | Line description. |
| `quantity` | No | Line quantity. Must be non-negative. |
| `cost` | No | Line unit cost. Must be non-negative. |
| `need_date` | No | Date needed. Parsed flexibly. |
| `estimated_arrival_date` | No | Estimated arrival date. Parsed flexibly. |
| `paid` | No | Marks the line as paid. Boolean field (see [accepted values](https://docs.firstresonance.io/administration/ion-importers#accepted-boolean-values)). |
| `percentage_fee_exempt` | No | Excludes this line from percentage-based PO fees. Boolean field (see [accepted values](https://docs.firstresonance.io/administration/ion-importers#accepted-boolean-values)). |
| `labels` | No | Semicolon-delimited list of label values to attach to the line. |
| `quality_clauses` | No | Semicolon-delimited list of quality clause titles. Must match Requirements with `requirement_type = QUALITY_CLAUSE`. |
| `intent_option` | No | Intent option value at the line level.

A row that populates only header columns (no `part_number` or other line column) contributes to the PO header but does not create a line. Use this to create or update a PO without lines.

## Fees

The `fees` column accepts a semicolon-delimited list of fee records. Each record uses two bracketed fields after the fee name:

```
Name[value][type];Name[value][type]
```

| Field | Notes |
| --- | --- |
| `Name` | Required. Must be unique within the PO. |
| `value` | Required. Must parse as a number. |
| `type` | Required. One of `currency` or `percentage`. |

Example: `Shipping[50.00][currency];VAT[8.5][percentage]`

Fee names must be unique within a PO. Duplicate fee names in the same row’s `fees` cell are rejected at validation.

## Custom attributes

Any column not in the standard set above is treated as a custom attribute. The importer routes each column to either PO-level or PO-line-level attributes based on which table defines it in your organization’s attribute configuration:

- A column matching a PO custom attribute is attached to the PO.
- A column matching a PO-line custom attribute is attached to the line.
- A column matching **both** is **ambiguous** and rejected. Rename one side to disambiguate.
- A column matching neither is rejected as unrecognized.

Matching is case- and format-sensitive, so a CSV header `BatchID` matches an attribute keyed `BatchID` only.

## Header conflicts

Header-column conflicts are collected across all POs and reported together, so you can fix everything in one pass.
