Skip to main content

receipt-report-generator

Turn receipts that land in your inbox into a single, tidy expense-report PDF — a cover sheet that summarises every receipt with a running total, followed by each original receipt as an indexed appendix. Optionally emails the finished PDF onwards and files the processed messages away so they are never counted twice.

Originally built to collect public-transport receipts for monthly bookkeeping, it is metadata-driven: point it at any sender, any IMAP mailbox, and any currency.

What it does

  1. Connects to your mailbox over IMAP.
  2. Finds every email from a configured sender that has a PDF attachment.
  3. Reads the amount and date out of each receipt PDF.
  4. Builds one PDF:
    • Cover sheet — a title, today's date, and a table of # | Date | Description | (Purpose) | Amount, ending in a Total.
    • Appendix — every original receipt, stamped Appendix N to match its table row, with PDF bookmarks.
  5. Moves the processed emails to a folder so the next run only sees new receipts.
  6. (Optional) Emails the PDF to an archive address and saves a copy in your Sent folder.
  7. (Optional, in CI) Emails you if a scheduled run fails.

Because "what's already processed" is represented by moving the emails out of the inbox, there is no database or local state to maintain — the mailbox is the single source of truth, which is what makes running it in the cloud trivial.

Requirements

Installation

git clone https://github.com/<you>/receipt-report-generator.git
cd receipt-report-generator
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env      # then edit .env

Or install it as a package to get the receipt-report command on your PATH:

pip install git+https://github.com/<you>/receipt-report-generator.git
# then, in a directory containing your .env:
receipt-report --dry-run

Configuration

All configuration is via environment variables (read from .env locally, or injected by your host in the cloud). The most important ones:

Variable Description
MAIL_PROVIDER A preset from providers.json (gmail, outlook, icloud, …). Fills in IMAP/SMTP host, port and security.
IMAP_USER / IMAP_PASSWORD Mailbox login. Use an app password where the provider supports one.
RECEIPT_SENDER Sender address of the receipts. A substring works (e.g. sl.se).
PROCESSED_FOLDER Where processed emails are moved (default Processed/Receipts, created if missing).
RECIPIENT_NAME / REPORT_TITLE Name and cover title ({name} is substituted).
CURRENCY Label after each amount (default kr).
PURPOSE Optional value for a "Purpose" column; empty hides the column.
DESCRIPTION_MATCH Optional regex to extract a description from the receipt text.
ARCHIVE_RECIPIENT If set, the report is emailed here on a real run.
NOTIFY_RECIPIENT Where CI failure notices go (defaults to IMAP_USER).

To use a server not in the presets, leave MAIL_PROVIDER empty and set IMAP_HOST, IMAP_PORT, SMTP_HOST, SMTP_PORT and SMTP_SECURITY (ssl or starttls) directly. See .env.example for the full list.

Supported mail servers

Presets live in providers.json and are easy to extend.

MAIL_PROVIDER Service IMAP SMTP
gmail Gmail / Google Workspace imap.gmail.com:993 smtp.gmail.com:465 (SSL)
outlook Outlook.com / Hotmail outlook.office365.com:993 smtp-mail.outlook.com:587 (STARTTLS)
office365 Microsoft 365 (work/school) outlook.office365.com:993 smtp.office365.com:587 (STARTTLS)
icloud iCloud Mail imap.mail.me.com:993 smtp.mail.me.com:587 (STARTTLS)
yahoo Yahoo Mail imap.mail.yahoo.com:993 smtp.mail.yahoo.com:465 (SSL)
fastmail Fastmail imap.fastmail.com:993 smtp.fastmail.com:465 (SSL)
gmx GMX imap.gmx.com:993 mail.gmx.com:465 (SSL)
zoho Zoho Mail imap.zoho.com:993 smtp.zoho.com:465 (SSL)
opensrs OpenSRS / Tucows Hosted Email imap.hostedemail.com:993 smtp.hostedemail.com:465 (SSL)

Any IMAP/SMTP server works via explicit host settings — the presets are just conveniences.

Setting up your mail account

Most providers need two things:

  1. IMAP access enabled. On by default for many; for some (Yahoo, GMX, Zoho) you toggle it in settings.
  2. An app password if the account uses 2-factor authentication. The generator logs in with a plain username + password, so create a provider-specific app password and use it as IMAP_PASSWORD:
    • Gmail: Google Account → Security → App passwords (requires 2-Step Verification).
    • iCloud: appleid.apple.com → Sign-In and Security → App-Specific Passwords.
    • Yahoo / Fastmail / Zoho: Account security → generate an app password.
    • OpenSRS/hosted email: usually the normal mailbox password works.

Microsoft note: Outlook.com and many Microsoft 365 tenants have disabled basic IMAP/SMTP auth. If password login is refused, an admin may need to allow it, or the account may require OAuth (not handled by this tool).

The same account is used both to read receipts and to send the report, so make sure SMTP submission is allowed too. The per-provider notes in providers.json summarise this.

Usage

Dry run (build the PDF; touch nothing in the mailbox):

python -m receipt_report.main --dry-run

Real run (build the PDF, move processed emails, and email the report if ARCHIVE_RECIPIENT is set):

python -m receipt_report.main

Real run without emailing:

python -m receipt_report.main --no-email

Output lands in output/expense-report_<YYYY-MM-DD>.pdf.

Run it in the cloud (GitHub Actions)

.github/workflows/monthly.yml runs the generator on the last day of each month — no machine of your own needs to be on.

Set these in your repository (Settings → Secrets and variables → Actions):

Secrets (sensitive):

Secret Value
IMAP_USER Your mailbox address
IMAP_PASSWORD App password / mailbox password
ARCHIVE_RECIPIENT (optional) where to email the report

Variables (non-sensitive):

Variable Example
MAIL_PROVIDER gmail
RECEIPT_SENDER receipts@vendor.com
RECIPIENT_NAME Jane Doe
CURRENCY kr
PURPOSE (optional)

Then:

  • Test anytime: Actions tab → Monthly expense report → Run workflow. A manual run skips the last-day guard and runs immediately.
  • Test the failure alert: run the workflow with the test_fail input checked — it forces a failure and emails NOTIFY_RECIPIENT (or IMAP_USER).
  • The generated PDF is also kept as a run artifact for 90 days.
  • Scheduled workflows only run on the repository's default branch.

Customising the parsing

Receipt layouts vary. The parser (receipt_parser.py) is built to be tuned:

  • Amounts are matched in both 1,234.50 and 1 234,50 styles; lines containing keywords like total, amount due, summa are preferred, otherwise the largest amount wins.
  • Dates are read as ISO, dd.mm.yyyy, or 12 June 2026 (English + Swedish month names), falling back to the email's date.
  • Descriptions default to the first meaningful text line; set DESCRIPTION_MATCH to a regex to grab something specific (group 1 if present).

Receipts where an amount or date can't be read are still included and listed as warnings so you can check them.

Security

  • Credentials live only in .env (git-ignored) or your host's secret store. Nothing sensitive is committed.
  • The processed-email move happens after the PDF is written successfully, so an interrupted run never loses receipts.

Publishing to PyPI

.github/workflows/publish.yml publishes the package to PyPI whenever you create a GitHub Release, authenticating with a PyPI API token. One-time setup:

  1. Create an API token. On pypi.org → Account settings → API tokens → Add API token (scope "Entire account" for the first upload; you can scope it to the project afterwards).
  2. Store it as a secret. Repo → Settings → Secrets and variables → Actions → new secret named PYPI_API_TOKEN with the token value. Or via CLI: gh secret set PYPI_API_TOKEN --repo <owner>/<repo>
  3. Release. Bump version in pyproject.toml, then create a GitHub Release (e.g. tag v1.0.0). The workflow builds and uploads automatically.

Prefer no stored token? PyPI also supports OIDC trusted publishing — remove the password: line from the workflow, add permissions: id-token: write to the publish job, and register a trusted publisher on PyPI (owner, repo, workflow publish.yml, no environment).

License

MIT

Metadata

Release files for receipt-report-generator 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for receipt-report-generator 1.0.0
File Size Uploaded
receipt_report_generator-1.0.0.tar.gz 22.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for receipt-report-generator 1.0.0
File Interpreter ABI Platform
receipt_report_generator-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 43.7 kB

Release files / receipt_report_generator-1.0.0.tar.gz

Download URL receipt_report_generator-1.0.0.tar.gz
Size 22.0 kB
Tags Source
SHA-256 checksum
How to use checksums
c0332f541aa66ec601663cd505e72f0b585e885c091837be04b959dc55a36dfe
BLAKE2b-256 checksum
How to use checksums
46c40667b43327012a419828dd4c4ea3d95a1c36bb31332a4bb52ba22a064311
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release files / receipt_report_generator-1.0.0-py3-none-any.whl

Download URL receipt_report_generator-1.0.0-py3-none-any.whl
Size 21.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75f9508fe330767c8cf07fa744df21cfe939fe4957c0985c65227e6f7a7ac381
BLAKE2b-256 checksum
How to use checksums
35f7509bee913ffd7fa614cde7a6090f17d86a2d6a1dad32c1ac5b84387b79dd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page