Metadata-Version: 2.4
Name: superslurp
Version: 0.0.4
Summary: A parser for SuperU receipts
Author-email: Pierre Sassoulas <pierre.sassoulas@gmail.com>
License: MIT
Project-URL: Bug Tracker, https://github.com/Pierre-Sassoulas/superslurp/issues
Project-URL: homepage, https://github.com/Pierre-Sassoulas/superslurp
Project-URL: Source Code, https://github.com/Pierre-Sassoulas/superslurp
Keywords: parser,receipt,superu
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pypdf
Provides-Extra: dev
Requires-Dist: pre-commit; extra == "dev"
Requires-Dist: pylint; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# superslurp : Super, Sublime, Light, and Unprecedented Receipt Parser

Parser for [SuperU](https://fr.wikipedia.org/wiki/Coop%C3%A9rative_U) receipts. Take the
PDF receipt sent by mail as input and return a json.

Useful when you want to display the instantaneous cheese consumption intensity of your
home in € inside grafana.

## 1. Parse a receipt

```python
from superslurp import parse_superu_receipt

result = parse_superu_receipt("Ticket de caisse_01032022-165652.pdf")
```

The parser understands the intricacies of French cheese: AOP designation, _fermier_ vs
_laitier_ production, milk treatment, as defined by the
[official AOP specification](https://agriculture.gouv.fr/le-reblochon-aop-le-fromage-onctueux-de-savoie).

The receipt line `REBL.SAVE.AOP.FRM.LC BIO BQT.X12 450G 32%MG  8,61 €  11` is parsed as:

```json
{
  "name": "REBLOCHON",
  "price": 8.61,
  "bought": 1,
  "units": 12,
  "grams": 450.0,
  "volume_ml": null,
  "fat_pct": 32.0,
  "tr": false,
  "way_of_paying": "11",
  "discount": null,
  "properties": {
    "bio": true,
    "milk_treatment": "cru",
    "production": "fermier",
    "label": "AOP",
    "packaging": "BARQUETTE",
    "origin": "SAVOIE"
  }
}
```

Pass `debug=True` to include the original receipt line (`"raw"` field):

```python
from superslurp import parse_superu_receipt

result = parse_superu_receipt("receipt.pdf", debug=True)
```

The parser ships with built-in synonyms that expand common receipt abbreviations (e.g.
`TABS` → `TABLETTES`, `VAISS` → `VAISSELLE`). You can provide extra synonyms that are
merged on top of the defaults:

```python
from superslurp import parse_superu_receipt

extra = {"CUSTOM_ABBREV": "CUSTOM EXPANSION"}
result = parse_superu_receipt("receipt.pdf", synonyms=extra)
```

CLI:

```bash
# Uses built-in synonyms (default)
superu-receipt-parser receipt.pdf

# Merge extra synonyms on top of built-in defaults
superu-receipt-parser receipt.pdf --synonyms extra.json

# Disable built-in synonyms entirely — only use your own file
superu-receipt-parser receipt.pdf --no-default-synonyms --synonyms my_synonyms.json
```

## 2. Aggregate receipts

Compare items across multiple parsed receipts. Products are grouped under a canonical
name using fuzzy matching (via difflib).

```python
from pathlib import Path

from superslurp.compare.aggregate import compare_receipt_files

result = compare_receipt_files(
    paths=[Path("receipt1.json"), Path("receipt2.json")],
    threshold=0.90,       # difflib threshold (default: 0.90)
)
```

Synonyms are applied at parse time (step 1), so the JSON files fed to
`compare_receipt_files` already contain expanded names.

The result contains stores, sessions, per-session totals, a rolling weekly average, and
products with their observations:

```json
{
  "stores": [{ "id": "123_456", "store_name": "...", "location": "..." }],
  "sessions": [{ "id": 1, "date": "2025-01-15 10:00:00", "store_id": "123_456" }],
  "session_totals": [{ "session_id": 1, "date": "2025-01-15", "total": 42.5 }],
  "rolling_average": [{ "date": "2025-01-13", "value": 85.3 }, "..."],
  "products": [
    {
      "canonical_name": "OEUFS",
      "observations": [
        {
          "original_name": "OEUFS PLEIN AIR MOYEN",
          "session_id": 1,
          "price": 3.15,
          "quantity": 1,
          "grams": null,
          "discount": null,
          "price_per_kg": null,
          "volume_ml": null,
          "price_per_liter": null,
          "unit_count": 12,
          "price_per_unit": 0.2625,
          "bio": true
        }
      ]
    }
  ]
}
```

CLI:

```bash
superu-aggregate-parsed-receipt receipts/ --output aggregate.json
```

## 3. Generate an HTML report

### From PDFs directly

Parse receipt PDFs and generate a self-contained HTML dashboard in one step:

```python
from pathlib import Path

from superslurp import generate_report

synonyms = {"TABS": "TABLETTES", "VAISS": "VAISSELLE"}
html = generate_report(
    ["receipt1.pdf", "receipt2.pdf", "receipt3.pdf"],
    synonyms=synonyms,    # optional
    threshold=0.90,       # fuzzy matching threshold (default: 0.90)
)
Path("report.html").write_text(html)
```

CLI:

```bash
superu-report receipts/*.pdf --output report.html
superu-report receipts/*.pdf --synonyms extra.json --output report.html
```

### From an existing aggregate JSON

If you already have an aggregate JSON (from step 2):

```python
from pathlib import Path

from superslurp.compare.html_report import generate_html

html = generate_html(aggregate_result)
Path("report.html").write_text(html)
```

```bash
superu-report-from-aggregate aggregate.json --output report.html
```

Or pipe directly from aggregate:

```bash
superu-aggregate-parsed-receipt receipts/ \
  | superu-report-from-aggregate - --output report.html
```

## Synonyms

Synonyms are applied during **parsing** (step 1) — the aggregate step (step 2) only does
fuzzy matching on already-expanded names.

The package ships with built-in synonyms for ~200 common Super U receipt abbreviations.
Extra synonyms passed via `--synonyms` are merged on top (user entries take precedence
on conflict). Use `--no-default-synonyms` to disable the built-in set entirely — this is
useful when you need full control over expansion order, since **insertion order
matters** (see below).

Synonyms is an ordered `dict[str, str]`. Entries are applied sequentially with
word-boundary matching — **insertion order matters**. Earlier entries are replaced
first, so later entries won't match words already consumed.

Dots in both names and keys are normalized to spaces before matching, so `"FROM.BLC"`
matches `FROM.BLC` on the receipt.

```python
synonyms = {
    "FROM.BLC": "FROMAGE BLANC",          # applied 1st: consumes FROM and BLC
    "CHOCO PATIS": "CHOCOLAT PATISSIER",  # applied 2nd: consumes CHOCO and PATIS
    "CHOCO": "CHOCOLAT",                  # applied 3rd: only if CHOCO still present
    "FROM": "FROMAGE",                    # applied 4th: only if FROM still present
    "PATIS": "PATISSERIE",               # applied 5th: only if PATIS still present
}
# "FROM.BLC NAT"         → "FROMAGE BLANC NAT"            (FROM.BLC consumed by 1st)
# "FROM.RAPE"            → "FROMAGE RAPE"                 (FROM consumed by 4th)
# "CHOCO.PATIS.NOIR 52%" → "CHOCOLAT PATISSIER NOIR 52%"  (CHOCO PATIS consumed by 2nd)
# "CHOCO.NOIR"           → "CHOCOLAT NOIR"                (CHOCO consumed by 3rd)
```

The JSON file is a standard object (key order is preserved since Python 3.7):

```json
{
  "FROM.BLC": "FROMAGE BLANC",
  "CHOCO PATIS": "CHOCOLAT PATISSIER",
  "CHOCO": "CHOCOLAT",
  "FROM": "FROMAGE",
  "PATIS": "PATISSERIE"
}
```
