112 lines
5.7 KiB
Markdown
112 lines
5.7 KiB
Markdown
|
|
# Invoice Parser (desktop)
|
||
|
|
|
||
|
|
Windows desktop replacement for the `invoiceProject` Classic ASP web app.
|
||
|
|
One exe: WPF window hosting WebView2, an embedded local web server (loopback
|
||
|
|
only), and a single background worker that parses queued invoices with the
|
||
|
|
configured LLM. No IIS, no browser needed for parsing.
|
||
|
|
|
||
|
|
## What changed vs the web app
|
||
|
|
|
||
|
|
- Upload -> prep -> parse is fully automatic. Drop files on the dashboard;
|
||
|
|
the server extracts the PDF text layer (PdfPig) or renders pages (PDFium),
|
||
|
|
queues the invoice, and the in-process worker calls the LLM. The UI gets
|
||
|
|
live updates over SSE - no polling, no "open it and press Scan Invoice".
|
||
|
|
- Scanned PDFs no longer dead-end: pages are rendered server-side on demand.
|
||
|
|
- One worker owns the queue (replaces pending_process.asp, parse_queue.ps1
|
||
|
|
and the browser-side parser), so the claim/steal/stale logic is gone.
|
||
|
|
- All SQL is parameterized.
|
||
|
|
- `pending/{id}/state.json` keeps the same format - existing drafts can be
|
||
|
|
migrated by copying `invoiceProject\pending\*` into this app's `pending\`.
|
||
|
|
|
||
|
|
## Configuration (appsettings.json next to the exe)
|
||
|
|
|
||
|
|
Secrets live in `appsettings.local.json` (same shape, git-ignored, loaded on
|
||
|
|
top of `appsettings.json`). Put `ConnectionStrings:Pos` and the two `ApiKey`
|
||
|
|
values there; `appsettings.json` ships with those blank. Use a SQL login
|
||
|
|
limited to the posbdat/posinv tables rather than `sa`.
|
||
|
|
|
||
|
|
- `ConnectionStrings:Pos` - SQL Server connection string.
|
||
|
|
- `Llm:TextRoute` / `Llm:ImageRoute` - `{ Url, Model, ApiKey }` per customer.
|
||
|
|
- Local llama.cpp: `Url: "http://host:8080"`, empty ApiKey.
|
||
|
|
- DeepSeek/OpenAI-compatible: full base URL + ApiKey (sends Bearer header).
|
||
|
|
- Gemini: `Url: "https://generativelanguage.googleapis.com"`, Model
|
||
|
|
`gemini-...`, ApiKey (sends x-goog-api-key).
|
||
|
|
- `Llm:Prompt` - override the extraction prompt; empty = built-in default.
|
||
|
|
- `Llm:TimeoutSeconds`, `Llm:MaxImageDim` - LLM call timeout, vision image cap.
|
||
|
|
- `Llm:AttachPageImages` - text-layer PDFs: send each rendered page image with
|
||
|
|
its extracted text (default true). Fixes vendors whose text layer is garbled
|
||
|
|
by the form overlay. Needs a vision model on `TextRoute`; set false for
|
||
|
|
text-only models (DeepSeek, a text-only local llama.cpp).
|
||
|
|
- `Paths:Pending` / `Paths:Archive` - relative to the exe or absolute.
|
||
|
|
- `Port` - local UI port (falls back to a random port if taken).
|
||
|
|
- `Logging` - `Enabled`, `Directory` (default `logs`), `RetainDays` (14),
|
||
|
|
`MaxFileMB` (20), `LogRequests`. See "Log" below.
|
||
|
|
- `Batch` - `Enabled`, `NamePrefix` (`INV_`), `CreatedBy` (`INVOICE`),
|
||
|
|
`MinCostDelta` (0.01), `MarginThreshold` (15). See "Cost batches" below.
|
||
|
|
- `Tables:ProductBatchesHeaderTable` / `Tables:ProductBatchesTable` - the
|
||
|
|
posbdat batch tables a cost batch is written to.
|
||
|
|
|
||
|
|
## Log
|
||
|
|
|
||
|
|
`logs\invoiceparser-YYYYMMDD.log`, one file per day, pruned after
|
||
|
|
`Logging:RetainDays`. Copy the folder or read it over a share to see what a
|
||
|
|
machine did without sitting at it. One line per event:
|
||
|
|
|
||
|
|
2026-09-11 14:22:31.104 INFO [http] POST /api/save 200 412ms
|
||
|
|
|
||
|
|
Categories: `startup`, `shutdown`, `crash` (unhandled WPF/AppDomain/task
|
||
|
|
exceptions), `http` (every request; 4xx logs WARN, 5xx ERROR), `upload`,
|
||
|
|
`prep`, `worker` (queue and per-invoice stage changes), `llm` (endpoint, status,
|
||
|
|
elapsed, bytes; failures carry the response body), `match`, `db`, `catalog`,
|
||
|
|
`save`, `batch`, `product`, `pending`, `archive`. Connection-string passwords
|
||
|
|
and API keys are stripped before anything is written. Set
|
||
|
|
`Logging:LogRequests` to false to keep only warnings and errors.
|
||
|
|
|
||
|
|
## Cost batches
|
||
|
|
|
||
|
|
Saving an invoice also writes a posbdat price/cost batch for the items whose
|
||
|
|
cost moved. The invoice bills by the case, the product file costs by the unit,
|
||
|
|
so the comparison is `case cost / pack` against `Products.cost`; a gap of at
|
||
|
|
least `Batch:MinCostDelta` is a change, and an item with no cost on file counts
|
||
|
|
as one. Rows with no invoiced cost, and UPCs that are not in the product file,
|
||
|
|
are left out.
|
||
|
|
|
||
|
|
The batch is written inside the same transaction as InvoiceHeader and
|
||
|
|
InvoiceDetail - if it cannot be written, nothing is saved - and
|
||
|
|
`InvoiceHeader.Price_Cost_Change` is set to 1 when the invoice produced one.
|
||
|
|
It is named `INV_<invoice number>` (`_1`, `_2`... if that name is taken) and
|
||
|
|
carries the new cost only; retail is left alone, and items whose gross margin
|
||
|
|
at the new cost falls under `Batch:MarginThreshold` are flagged on the archive
|
||
|
|
screen for manual repricing. Apply it the usual way (`sp_ApplyBatch`).
|
||
|
|
|
||
|
|
The batch tables' column lists are read from INFORMATION_SCHEMA on first use and
|
||
|
|
whatever `Products` and `ProductBatches` genuinely have in common is copied
|
||
|
|
across, so a differently shaped install does not need a code change. If the
|
||
|
|
batch tables are missing or have no `cost` column the invoice still saves and
|
||
|
|
the reason is logged under `batch`.
|
||
|
|
|
||
|
|
## Build / run (dev)
|
||
|
|
|
||
|
|
dotnet build
|
||
|
|
bin\Debug\net8.0-windows\InvoiceParser.exe
|
||
|
|
|
||
|
|
## Deploy (copy-paste, no install)
|
||
|
|
|
||
|
|
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true
|
||
|
|
|
||
|
|
Copy the publish folder (exe + appsettings.json + appsettings.local.json +
|
||
|
|
wwwroot) to the target machine and run the exe. Requirements on the target: the WebView2 Runtime
|
||
|
|
(preinstalled on updated Windows 10/11; otherwise the 2 MB Evergreen
|
||
|
|
bootstrapper from Microsoft), network access to SQL Server and the LLM host.
|
||
|
|
|
||
|
|
## Layout at runtime
|
||
|
|
|
||
|
|
InvoiceParser.exe
|
||
|
|
appsettings.json
|
||
|
|
appsettings.local.json connection string + API keys (not in git)
|
||
|
|
wwwroot\ UI (HTML/JS/CSS, local jQuery)
|
||
|
|
pending\{id}\ state.json, source.<ext>, pages\page_N.jpg
|
||
|
|
archive\ {vendorId}_{invoice}.<ext> + .preview cache
|
||
|
|
logs\ invoiceparser-YYYYMMDD.log
|
||
|
|
webview2_data\ WebView2 profile (created on first run)
|