# Suppliers Module

**Folder:** `CRUD/suppliers/` · **Main table:** `suppliers` (575 rows) · **Reached from:** navbar → Suppliers · **Entry page:** `supplier_navigation.php`

Everything about the companies you **buy from** — the supplier records, their product/parts lists, and the largest sub-system in the module: **supplier invoices**, which are scanned, OCR-extracted against a per-supplier template, matched to hires, and pushed through to Sage.

> Documented to the format set by [sites.md](sites.md).

---

## How the supplier invoice pipeline works

```mermaid
flowchart TD
    UP[Upload invoice PDF<br/>upload_supplier_invoice_form.php] --> EX[Extract via OCR<br/>supplier_invoice_run_extract.php]
    TPL[Per-supplier template<br/>supplier_template_builder.php] -.defines field positions.-> EX
    EX --> RV[Review extracted values<br/>supplier_invoice_review.php]
    RV --> LINK[Link to a hire<br/>supplier_invoice_hire_link_save.php]
    LINK --> CO{Recharge?}
    CO -->|charge on| CHG[Charge On<br/>charge_on_index.php]
    CO -->|cross-hire| XH[Cross-Hire Manager<br/>cross_hire_manager.php]
    RV --> BURN[Stamp / burn onto the PDF<br/>supplier_invoice_burn.php]
    BURN --> SAGE[Sage entry<br/>sage_entry.php]
    RV --> UNP[Unpaid invoices<br/>unpaid_invoices.php]
```

---

## Suppliers

![Supplier Navigation](../screenshots/suppliers__navigation.png)

| Page | Purpose |
|---|---|
| `supplier_navigation.php` | Module hub |
| `suppliers_index.php` | All suppliers (575) |
| `suppliers_create.php` / `suppliers_update.php` / `suppliers_delete.php` | Maintain a supplier (`?id_supplier=`) |
| `suppliers_read.php` | View one supplier — **unreferenced** |
| `supplier_documents_*.php` | Documents against a supplier (upload validated via the shared upload guard) |

![Suppliers Index](../screenshots/suppliers__index.png)

## Products & parts

Two parallel catalogues:

- **`products/`** — the supplier's own product list (`suppliers_productlists`, 658 rows), with stock take (`product_stock_index.php`, `product_update_stock_take.php`).
- **`products_hickeys/`** — Hickey's own parts catalogue (`suppliers_product_hickey`, 128 rows), including **machine compatibility** (`supplier_product_machine_compatibility`, 201 rows), service-by-machine lookup, preferred suppliers (41) and stock summary (40).

![Products](../screenshots/suppliers__products.png)

---

## Supplier invoices — `supplier_invoices/` (36 files)

The heart of the module. **469 invoices**, **615 line items**, **94 per-supplier templates**.

### Upload & extract

![Upload Invoice](../screenshots/suppliers__upload_invoice.png)

| File | Purpose |
|---|---|
| `upload_supplier_invoice_form.php` | Upload the invoice PDF |
| `upload_supplier_invoice.php` | Store it (API, 596 lines) |
| `supplier_invoice_run_extract.php` | Run OCR extraction (API) |
| `supplier_invoice_file.php` | Serve the stored PDF |

### Per-supplier templates

![Template Overview](../screenshots/suppliers__template_overview.png)

Each supplier's invoice layout differs, so a **template defines where each field sits** on the page. OCR then reads those positions.

| File | Purpose |
|---|---|
| `supplier_template_overview.php` | All templates (94) |
| `supplier_template_builder.php` | Define field positions — **requires both `?id_supplier_invoice=` and `?id_supplier=`** |
| `supplier_template_overlay_preview.php` | Preview the template overlaid on a real invoice (same two params) |
| `supplier_template_copy.php` | Copy a template to another supplier (needs both source and target ids) |
| `supplier_template_save.php` / `supplier_template_preview.php` | Save / render |

### Review & matching

![Invoice Review](../screenshots/suppliers__invoice_review.png)

| File | Purpose |
|---|---|
| `supplier_invoice_overview.php` | All invoices (1,100 lines) |
| `supplier_invoice_review.php` | Review one invoice (`?id=`; 2,071 lines — the biggest page in the module) |
| `supplier_invoice_save_review.php` | Save the reviewed values (handler) |
| `supplier_invoice_quick_update.php` / `supplier_invoice_flag_save.php` | Quick edits and flags |
| `supplier_invoice_annotate.php` | Annotate the PDF (`?id=`) — annotations in `supplier_invoice_annotations` (160) |
| `save_supplier_invoice_annotations.php` | Persist annotations |
| `supplier_invoice_hire_link_save.php` | Link an invoice to a hire (`supplier_invoice_hire_link`, 4 rows) |
| `api_get_unlinked_invoices.php` / `api_invoice_hire_links.php` / `api_search_all_hires.php` | Matching APIs |
| `supplier_invoice_burn.php` | **Stamp/burn** the reviewed detail onto the PDF itself |
| `supplier_invoice_delete.php` | Remove an invoice |

### Recharging: charge-on vs cross-hire

![Charge On](../screenshots/suppliers__charge_on.png)

Two different ways a supplier cost reaches a customer:

- **Charge on** (`charge_on_index.php`, `charge_on_lib.php`, `charge_on_confirmed.php`) — a supplier cost recharged onto a hire.
- **Cross-hire** (`cross_hire_manager.php`, `cross_hire_history.php`, `cross_hire_archive_save.php`) — kit hired **in** from another company and re-hired out. See also `xh_list.php` in [hire-list.md](hire-list.md).

![Cross-Hire Manager](../screenshots/suppliers__cross_hire.png)

### Payment & Sage

![Unpaid Invoices](../screenshots/suppliers__unpaid.png)

`unpaid_invoices.php` tracks what's outstanding; `sage_entry.php` prepares the entry for the accounts system.

![Sage Entry](../screenshots/suppliers__sage_entry.png)

### OCR service control

![OCR Status](../screenshots/suppliers__ocr_status.png)

The OCR extractor is a **separate Python service on `127.0.0.1:8012`** (source under `supplier_invoices/extractor/`). The same service backs the [capture flows](capture-flows.md) and [invoice review](invoice-review.md).

| File | Purpose |
|---|---|
| `ocr_status.php` | Service health page |
| `ocr_service_status.php` / `ocr_service_diag.php` | Status + diagnostics (API) |
| `ocr_service_restart.php` | Restart the service (API) |
| `ocr_service_control.php` | Control panel — **unreferenced** |

## Database

| Table | Rows | Notes |
|---|---|---|
| `suppliers` | 575 | Master supplier record |
| `supplier_invoices` | 469 | Uploaded invoices |
| `supplier_invoice_items` | 615 | Extracted line items |
| `supplier_invoice_templates` | 94 | Per-supplier field-position templates |
| `supplier_invoice_annotations` | 160 | PDF annotations |
| `supplier_invoice_hire_link` | 4 | Invoice → hire links |
| `suppliers_productlists` | 658 | Supplier product catalogue |
| `suppliers_product_hickey` | 128 | Hickey parts catalogue |
| `supplier_product_machine_compatibility` | 201 | Which part fits which machine |
| `supplier_product_preferred` / `_stock_summary` | 41 / 40 | Preferred supplier + stock |

> **Local caveat:** uploaded invoice PDFs do **not** exist on a dev machine — `supplier_invoices.stored_path` holds absolute **live** paths, so the PDF viewer will report the file missing locally. Environmental, not a bug.

---

## Known issues

Reviewed 2026-07-21.

**No functional bugs found.** All 33 tested pages returned HTTP 200 with no PHP errors or warnings. The 302s and 400s were correct parameter validation, and the 400s return helpful plain-text messages rather than erroring.

| # | Issue | Status |
|---|---|---|
| 1 | Three validation messages said *"Missing X **or** Y"* while the condition required **both** (`$a <= 0 \|\| $b <= 0`). Passing either one alone still returned 400 with a message implying it should have worked — genuinely misleading when debugging. Affected `supplier_template_builder.php`, `supplier_template_overlay_preview.php` and `supplier_template_copy.php`. | **Fixed** — all three now state that both parameters are required |
| 2 | Unreferenced: `suppliers_read.php`, `products/product_read.php`, `products_hickeys/product_hickeys_compatability_index.php`, `supplier_invoice_annotate.php`, `ocr_service_control.php`. Note `supplier_invoice_annotate.php` (456 lines) works fine when given `?id=` — it looks **live but unlinked**. | **Open** — annotate in particular may just need a link |
| 3 | `charge_on_lib.php` runs `CREATE TABLE`/`ALTER TABLE` on load (the self-migrating pattern seen in 12 files portal-wide). | **Open** — architectural, see [fleet.md](fleet.md#known-issues) |
| 4 | Only **4 rows** in `supplier_invoice_hire_link` against 469 invoices. | **Open** — worth confirming whether invoice→hire matching is actually being used in practice, or whether the feature is new/unadopted |
