Developer Documentation

PaintCo Invoice Classifier — system architecture and API reference

Architecture

Single Python server using Flask. No external AI APIs — all classification is done locally with rule-based keyword matching.

File Structure

paintco-mia-wizard/
├── server.py — Flask web server, routes, email polling, SharePoint sync
├── classifier.py — PDF parsing + paint/materials classification engine
├── excel_writer.py — Invoice_Register.xlsx generation
├── email_monitor.py — IMAP + Graph API email polling
├── sharepoint_client.py — Microsoft Graph API SharePoint operations
├── config.py — Settings persistence (JSON file)
├── help_pages.py — Help + DevDoc HTML pages
└── start.sh — Startup script

Classification Engine

classifier.py uses pdfplumber for PDF text extraction and keyword matching for classification. No LLM, no external API calls.

How it works

  1. extract_pdf_text() — extracts text from PDF using pdfplumber
  2. detect_format() — identifies supplier from text (wattyl / dulux / haymes / resene / unknown)
  3. _parse_wattyl() / _parse_dulux() / _parse_haymes() / _parse_resene() — supplier-specific table parser
  4. _parse_unknown() — generic fallback for unrecognised formats
  5. _classify_item() — checks description against PAINT_KEYWORDS and MATERIALS_KEYWORDS
  6. _confidence_check() — flags items with Review status based on configurable threshold

Each supplier has its own parser due to different table layouts. To add a new supplier: implement a _parse_NEWSUPPLIER() function, register it in the parsers dict inside classify_invoice(), and add detection to detect_format().

API Endpoints

GET/
Web UI — invoice upload and classification
GET/history
Web UI — classification history
GET/settings
Web UI — email and SharePoint configuration
GET/help
Web UI — user help guide
GET/devdoc
Web UI — developer documentation
POST/api/classify
Upload and classify a PDF invoice (multipart form, field: file)
GET/api/health
Service health check
GET/api/stats
Aggregate classification statistics
GET/api/register/download
Download Invoice_Register.xlsx (creates empty one if none exists)
POST/api/register/reset
Clear register, history, failed log, and push empty to SharePoint
POST/api/register/download-and-reset
Download current register then reset everything (including SharePoint)
GET/api/config
Get current settings (secrets masked)
POST/api/config/email
Save email configuration
POST/api/config/sharepoint
Save SharePoint configuration
POST/api/config/test/email
Test email connection
POST/api/config/test/sharepoint
Test SharePoint connection
POST/api/config/classification
Update classification settings (review_threshold)
GET/api/email/status
Email polling status
POST/api/email/start
Start email polling thread
POST/api/email/stop
Stop email polling thread
POST/api/watch/process
Process all PDFs in the inbox watch directory
GET/download/<entry_id>
Download JSON result for a specific classification

Email Monitoring

email_monitor.py supports two modes:

Polling runs in a background daemon thread. Configurable interval (default 60s). Found emails are processed then moved to a "Processed" folder. Duplicate detection uses a persistent set of processed message IDs.

SharePoint Integration

sharepoint_client.py uses Microsoft Graph API with MSAL client credentials flow.

Required Graph permissions:

The Excel register is updated locally with openpyxl and uploaded to SharePoint. Uses PUT /drives/{id}/items/{id}/content for updates. Upload retries up to 5 times with a 3s delay if the file is locked (HTTP 423).

Running Locally

pip install flask pdfplumber openpyxl msal
export PORT=5000
python3 server.py