receipt-report-generator 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- receipt_report_generator-1.0.0/LICENSE +21 -0
- receipt_report_generator-1.0.0/PKG-INFO +260 -0
- receipt_report_generator-1.0.0/README.md +213 -0
- receipt_report_generator-1.0.0/pyproject.toml +42 -0
- receipt_report_generator-1.0.0/receipt_report/__init__.py +3 -0
- receipt_report_generator-1.0.0/receipt_report/__main__.py +8 -0
- receipt_report_generator-1.0.0/receipt_report/config.py +107 -0
- receipt_report_generator-1.0.0/receipt_report/mail_client.py +149 -0
- receipt_report_generator-1.0.0/receipt_report/mailer.py +60 -0
- receipt_report_generator-1.0.0/receipt_report/main.py +135 -0
- receipt_report_generator-1.0.0/receipt_report/notify.py +41 -0
- receipt_report_generator-1.0.0/receipt_report/pdf_builder.py +171 -0
- receipt_report_generator-1.0.0/receipt_report/providers.json +93 -0
- receipt_report_generator-1.0.0/receipt_report/providers.py +48 -0
- receipt_report_generator-1.0.0/receipt_report/receipt_parser.py +157 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/PKG-INFO +260 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/SOURCES.txt +20 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/dependency_links.txt +1 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/entry_points.txt +2 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/requires.txt +5 -0
- receipt_report_generator-1.0.0/receipt_report_generator.egg-info/top_level.txt +1 -0
- receipt_report_generator-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Barton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: receipt-report-generator
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Turn emailed receipts into a single expense-report PDF (cover summary + indexed appendix).
|
|
5
|
+
Author: David Barton
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 David Barton
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/dbarton1974/receipt-report-generator
|
|
29
|
+
Project-URL: Repository, https://github.com/dbarton1974/receipt-report-generator
|
|
30
|
+
Project-URL: Issues, https://github.com/dbarton1974/receipt-report-generator/issues
|
|
31
|
+
Keywords: imap,receipts,pdf,expense-report,email,invoice,bookkeeping
|
|
32
|
+
Classifier: Programming Language :: Python :: 3
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Operating System :: OS Independent
|
|
35
|
+
Classifier: Environment :: Console
|
|
36
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
37
|
+
Classifier: Topic :: Communications :: Email
|
|
38
|
+
Requires-Python: >=3.10
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
License-File: LICENSE
|
|
41
|
+
Requires-Dist: imap-tools>=1.7.0
|
|
42
|
+
Requires-Dist: pdfplumber>=0.11.0
|
|
43
|
+
Requires-Dist: reportlab>=4.1.0
|
|
44
|
+
Requires-Dist: pypdf>=4.2.0
|
|
45
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# receipt-report-generator
|
|
49
|
+
|
|
50
|
+
Turn receipts that land in your inbox into a single, tidy **expense-report PDF** — a cover
|
|
51
|
+
sheet that summarises every receipt with a running total, followed by each original receipt
|
|
52
|
+
as an indexed appendix. Optionally emails the finished PDF onwards and files the processed
|
|
53
|
+
messages away so they are never counted twice.
|
|
54
|
+
|
|
55
|
+
Originally built to collect public-transport receipts for monthly bookkeeping, it is
|
|
56
|
+
**metadata-driven**: point it at any sender, any IMAP mailbox, and any currency.
|
|
57
|
+
|
|
58
|
+
## What it does
|
|
59
|
+
|
|
60
|
+
1. Connects to your mailbox over **IMAP**.
|
|
61
|
+
2. Finds every email from a configured **sender** that has a **PDF attachment**.
|
|
62
|
+
3. Reads the **amount** and **date** out of each receipt PDF.
|
|
63
|
+
4. Builds one PDF:
|
|
64
|
+
- **Cover sheet** — a title, today's date, and a table of `# | Date | Description |
|
|
65
|
+
(Purpose) | Amount`, ending in a **Total**.
|
|
66
|
+
- **Appendix** — every original receipt, stamped `Appendix N` to match its table row,
|
|
67
|
+
with PDF bookmarks.
|
|
68
|
+
5. **Moves** the processed emails to a folder so the next run only sees new receipts.
|
|
69
|
+
6. *(Optional)* **Emails** the PDF to an archive address and saves a copy in your Sent folder.
|
|
70
|
+
7. *(Optional, in CI)* **Emails you** if a scheduled run fails.
|
|
71
|
+
|
|
72
|
+
Because "what's already processed" is represented by *moving the emails out of the inbox*,
|
|
73
|
+
there is no database or local state to maintain — the mailbox is the single source of truth,
|
|
74
|
+
which is what makes running it in the cloud trivial.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Python 3.10+
|
|
79
|
+
- An email account reachable over IMAP + SMTP (see [supported servers](#supported-mail-servers))
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/<you>/receipt-report-generator.git
|
|
85
|
+
cd receipt-report-generator
|
|
86
|
+
python -m venv .venv && source .venv/bin/activate
|
|
87
|
+
pip install -r requirements.txt
|
|
88
|
+
cp .env.example .env # then edit .env
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or install it as a package to get the `receipt-report` command on your PATH:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install git+https://github.com/<you>/receipt-report-generator.git
|
|
95
|
+
# then, in a directory containing your .env:
|
|
96
|
+
receipt-report --dry-run
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
All configuration is via environment variables (read from `.env` locally, or injected by
|
|
102
|
+
your host in the cloud). The most important ones:
|
|
103
|
+
|
|
104
|
+
| Variable | Description |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| `MAIL_PROVIDER` | A preset from [`providers.json`](receipt_report/providers.json) (`gmail`, `outlook`, `icloud`, …). Fills in IMAP/SMTP host, port and security. |
|
|
107
|
+
| `IMAP_USER` / `IMAP_PASSWORD` | Mailbox login. Use an **app password** where the provider supports one. |
|
|
108
|
+
| `RECEIPT_SENDER` | Sender address of the receipts. A substring works (e.g. `sl.se`). |
|
|
109
|
+
| `PROCESSED_FOLDER` | Where processed emails are moved (default `Processed/Receipts`, created if missing). |
|
|
110
|
+
| `RECIPIENT_NAME` / `REPORT_TITLE` | Name and cover title (`{name}` is substituted). |
|
|
111
|
+
| `CURRENCY` | Label after each amount (default `kr`). |
|
|
112
|
+
| `PURPOSE` | Optional value for a "Purpose" column; empty hides the column. |
|
|
113
|
+
| `DESCRIPTION_MATCH` | Optional regex to extract a description from the receipt text. |
|
|
114
|
+
| `ARCHIVE_RECIPIENT` | If set, the report is emailed here on a real run. |
|
|
115
|
+
| `NOTIFY_RECIPIENT` | Where CI failure notices go (defaults to `IMAP_USER`). |
|
|
116
|
+
|
|
117
|
+
To use a server not in the presets, leave `MAIL_PROVIDER` empty and set `IMAP_HOST`,
|
|
118
|
+
`IMAP_PORT`, `SMTP_HOST`, `SMTP_PORT` and `SMTP_SECURITY` (`ssl` or `starttls`) directly.
|
|
119
|
+
See [`.env.example`](.env.example) for the full list.
|
|
120
|
+
|
|
121
|
+
## Supported mail servers
|
|
122
|
+
|
|
123
|
+
Presets live in [`providers.json`](receipt_report/providers.json) and are easy to extend.
|
|
124
|
+
|
|
125
|
+
| `MAIL_PROVIDER` | Service | IMAP | SMTP |
|
|
126
|
+
| --- | --- | --- | --- |
|
|
127
|
+
| `gmail` | Gmail / Google Workspace | imap.gmail.com:993 | smtp.gmail.com:465 (SSL) |
|
|
128
|
+
| `outlook` | Outlook.com / Hotmail | outlook.office365.com:993 | smtp-mail.outlook.com:587 (STARTTLS) |
|
|
129
|
+
| `office365` | Microsoft 365 (work/school) | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
|
|
130
|
+
| `icloud` | iCloud Mail | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
|
|
131
|
+
| `yahoo` | Yahoo Mail | imap.mail.yahoo.com:993 | smtp.mail.yahoo.com:465 (SSL) |
|
|
132
|
+
| `fastmail` | Fastmail | imap.fastmail.com:993 | smtp.fastmail.com:465 (SSL) |
|
|
133
|
+
| `gmx` | GMX | imap.gmx.com:993 | mail.gmx.com:465 (SSL) |
|
|
134
|
+
| `zoho` | Zoho Mail | imap.zoho.com:993 | smtp.zoho.com:465 (SSL) |
|
|
135
|
+
| `opensrs` | OpenSRS / Tucows Hosted Email | imap.hostedemail.com:993 | smtp.hostedemail.com:465 (SSL) |
|
|
136
|
+
|
|
137
|
+
Any IMAP/SMTP server works via explicit host settings — the presets are just conveniences.
|
|
138
|
+
|
|
139
|
+
## Setting up your mail account
|
|
140
|
+
|
|
141
|
+
Most providers need two things:
|
|
142
|
+
|
|
143
|
+
1. **IMAP access enabled.** On by default for many; for some (Yahoo, GMX, Zoho) you toggle
|
|
144
|
+
it in settings.
|
|
145
|
+
2. **An app password** if the account uses 2-factor authentication. The generator logs in
|
|
146
|
+
with a plain username + password, so create a provider-specific *app password* and use it
|
|
147
|
+
as `IMAP_PASSWORD`:
|
|
148
|
+
- **Gmail:** Google Account → Security → App passwords (requires 2-Step Verification).
|
|
149
|
+
- **iCloud:** appleid.apple.com → Sign-In and Security → App-Specific Passwords.
|
|
150
|
+
- **Yahoo / Fastmail / Zoho:** Account security → generate an app password.
|
|
151
|
+
- **OpenSRS/hosted email:** usually the normal mailbox password works.
|
|
152
|
+
|
|
153
|
+
> **Microsoft note:** Outlook.com and many Microsoft 365 tenants have disabled basic
|
|
154
|
+
> IMAP/SMTP auth. If password login is refused, an admin may need to allow it, or the account
|
|
155
|
+
> may require OAuth (not handled by this tool).
|
|
156
|
+
|
|
157
|
+
The same account is used both to **read** receipts and to **send** the report, so make sure
|
|
158
|
+
SMTP submission is allowed too. The per-provider notes in `providers.json` summarise this.
|
|
159
|
+
|
|
160
|
+
## Usage
|
|
161
|
+
|
|
162
|
+
Dry run (build the PDF; touch nothing in the mailbox):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
python -m receipt_report.main --dry-run
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Real run (build the PDF, move processed emails, and email the report if `ARCHIVE_RECIPIENT`
|
|
169
|
+
is set):
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
python -m receipt_report.main
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Real run without emailing:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python -m receipt_report.main --no-email
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Output lands in `output/expense-report_<YYYY-MM-DD>.pdf`.
|
|
182
|
+
|
|
183
|
+
## Run it in the cloud (GitHub Actions)
|
|
184
|
+
|
|
185
|
+
[`.github/workflows/monthly.yml`](.github/workflows/monthly.yml) runs the generator on the
|
|
186
|
+
**last day of each month** — no machine of your own needs to be on.
|
|
187
|
+
|
|
188
|
+
Set these in your repository (**Settings → Secrets and variables → Actions**):
|
|
189
|
+
|
|
190
|
+
**Secrets** (sensitive):
|
|
191
|
+
|
|
192
|
+
| Secret | Value |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `IMAP_USER` | Your mailbox address |
|
|
195
|
+
| `IMAP_PASSWORD` | App password / mailbox password |
|
|
196
|
+
| `ARCHIVE_RECIPIENT` | *(optional)* where to email the report |
|
|
197
|
+
|
|
198
|
+
**Variables** (non-sensitive):
|
|
199
|
+
|
|
200
|
+
| Variable | Example |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `MAIL_PROVIDER` | `gmail` |
|
|
203
|
+
| `RECEIPT_SENDER` | `receipts@vendor.com` |
|
|
204
|
+
| `RECIPIENT_NAME` | `Jane Doe` |
|
|
205
|
+
| `CURRENCY` | `kr` |
|
|
206
|
+
| `PURPOSE` | *(optional)* |
|
|
207
|
+
|
|
208
|
+
Then:
|
|
209
|
+
|
|
210
|
+
- **Test anytime:** Actions tab → *Monthly expense report* → **Run workflow**. A manual run
|
|
211
|
+
skips the last-day guard and runs immediately.
|
|
212
|
+
- **Test the failure alert:** run the workflow with the **test_fail** input checked — it forces
|
|
213
|
+
a failure and emails `NOTIFY_RECIPIENT` (or `IMAP_USER`).
|
|
214
|
+
- The generated PDF is also kept as a **run artifact** for 90 days.
|
|
215
|
+
- Scheduled workflows only run on the repository's default branch.
|
|
216
|
+
|
|
217
|
+
## Customising the parsing
|
|
218
|
+
|
|
219
|
+
Receipt layouts vary. The parser ([`receipt_parser.py`](receipt_report/receipt_parser.py)) is
|
|
220
|
+
built to be tuned:
|
|
221
|
+
|
|
222
|
+
- **Amounts** are matched in both `1,234.50` and `1 234,50` styles; lines containing keywords
|
|
223
|
+
like *total*, *amount due*, *summa* are preferred, otherwise the largest amount wins.
|
|
224
|
+
- **Dates** are read as ISO, `dd.mm.yyyy`, or `12 June 2026` (English + Swedish month names),
|
|
225
|
+
falling back to the email's date.
|
|
226
|
+
- **Descriptions** default to the first meaningful text line; set `DESCRIPTION_MATCH` to a
|
|
227
|
+
regex to grab something specific (group 1 if present).
|
|
228
|
+
|
|
229
|
+
Receipts where an amount or date can't be read are still included and listed as warnings so
|
|
230
|
+
you can check them.
|
|
231
|
+
|
|
232
|
+
## Security
|
|
233
|
+
|
|
234
|
+
- Credentials live only in `.env` (git-ignored) or your host's secret store. Nothing sensitive
|
|
235
|
+
is committed.
|
|
236
|
+
- The processed-email move happens **after** the PDF is written successfully, so an interrupted
|
|
237
|
+
run never loses receipts.
|
|
238
|
+
|
|
239
|
+
## Publishing to PyPI
|
|
240
|
+
|
|
241
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) publishes the package to
|
|
242
|
+
PyPI whenever you create a **GitHub Release**, authenticating with a PyPI API token. One-time
|
|
243
|
+
setup:
|
|
244
|
+
|
|
245
|
+
1. **Create an API token.** On [pypi.org](https://pypi.org) → *Account settings → API tokens*
|
|
246
|
+
→ *Add API token* (scope "Entire account" for the first upload; you can scope it to the
|
|
247
|
+
project afterwards).
|
|
248
|
+
2. **Store it as a secret.** Repo → *Settings → Secrets and variables → Actions* → new secret
|
|
249
|
+
named `PYPI_API_TOKEN` with the token value. Or via CLI:
|
|
250
|
+
`gh secret set PYPI_API_TOKEN --repo <owner>/<repo>`
|
|
251
|
+
3. **Release.** Bump `version` in [`pyproject.toml`](pyproject.toml), then create a GitHub
|
|
252
|
+
Release (e.g. tag `v1.0.0`). The workflow builds and uploads automatically.
|
|
253
|
+
|
|
254
|
+
Prefer no stored token? PyPI also supports OIDC *trusted publishing* — remove the `password:`
|
|
255
|
+
line from the workflow, add `permissions: id-token: write` to the publish job, and register a
|
|
256
|
+
trusted publisher on PyPI (owner, repo, workflow `publish.yml`, no environment).
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# receipt-report-generator
|
|
2
|
+
|
|
3
|
+
Turn receipts that land in your inbox into a single, tidy **expense-report PDF** — a cover
|
|
4
|
+
sheet that summarises every receipt with a running total, followed by each original receipt
|
|
5
|
+
as an indexed appendix. Optionally emails the finished PDF onwards and files the processed
|
|
6
|
+
messages away so they are never counted twice.
|
|
7
|
+
|
|
8
|
+
Originally built to collect public-transport receipts for monthly bookkeeping, it is
|
|
9
|
+
**metadata-driven**: point it at any sender, any IMAP mailbox, and any currency.
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
1. Connects to your mailbox over **IMAP**.
|
|
14
|
+
2. Finds every email from a configured **sender** that has a **PDF attachment**.
|
|
15
|
+
3. Reads the **amount** and **date** out of each receipt PDF.
|
|
16
|
+
4. Builds one PDF:
|
|
17
|
+
- **Cover sheet** — a title, today's date, and a table of `# | Date | Description |
|
|
18
|
+
(Purpose) | Amount`, ending in a **Total**.
|
|
19
|
+
- **Appendix** — every original receipt, stamped `Appendix N` to match its table row,
|
|
20
|
+
with PDF bookmarks.
|
|
21
|
+
5. **Moves** the processed emails to a folder so the next run only sees new receipts.
|
|
22
|
+
6. *(Optional)* **Emails** the PDF to an archive address and saves a copy in your Sent folder.
|
|
23
|
+
7. *(Optional, in CI)* **Emails you** if a scheduled run fails.
|
|
24
|
+
|
|
25
|
+
Because "what's already processed" is represented by *moving the emails out of the inbox*,
|
|
26
|
+
there is no database or local state to maintain — the mailbox is the single source of truth,
|
|
27
|
+
which is what makes running it in the cloud trivial.
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.10+
|
|
32
|
+
- An email account reachable over IMAP + SMTP (see [supported servers](#supported-mail-servers))
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/<you>/receipt-report-generator.git
|
|
38
|
+
cd receipt-report-generator
|
|
39
|
+
python -m venv .venv && source .venv/bin/activate
|
|
40
|
+
pip install -r requirements.txt
|
|
41
|
+
cp .env.example .env # then edit .env
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install it as a package to get the `receipt-report` command on your PATH:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install git+https://github.com/<you>/receipt-report-generator.git
|
|
48
|
+
# then, in a directory containing your .env:
|
|
49
|
+
receipt-report --dry-run
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
All configuration is via environment variables (read from `.env` locally, or injected by
|
|
55
|
+
your host in the cloud). The most important ones:
|
|
56
|
+
|
|
57
|
+
| Variable | Description |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `MAIL_PROVIDER` | A preset from [`providers.json`](receipt_report/providers.json) (`gmail`, `outlook`, `icloud`, …). Fills in IMAP/SMTP host, port and security. |
|
|
60
|
+
| `IMAP_USER` / `IMAP_PASSWORD` | Mailbox login. Use an **app password** where the provider supports one. |
|
|
61
|
+
| `RECEIPT_SENDER` | Sender address of the receipts. A substring works (e.g. `sl.se`). |
|
|
62
|
+
| `PROCESSED_FOLDER` | Where processed emails are moved (default `Processed/Receipts`, created if missing). |
|
|
63
|
+
| `RECIPIENT_NAME` / `REPORT_TITLE` | Name and cover title (`{name}` is substituted). |
|
|
64
|
+
| `CURRENCY` | Label after each amount (default `kr`). |
|
|
65
|
+
| `PURPOSE` | Optional value for a "Purpose" column; empty hides the column. |
|
|
66
|
+
| `DESCRIPTION_MATCH` | Optional regex to extract a description from the receipt text. |
|
|
67
|
+
| `ARCHIVE_RECIPIENT` | If set, the report is emailed here on a real run. |
|
|
68
|
+
| `NOTIFY_RECIPIENT` | Where CI failure notices go (defaults to `IMAP_USER`). |
|
|
69
|
+
|
|
70
|
+
To use a server not in the presets, leave `MAIL_PROVIDER` empty and set `IMAP_HOST`,
|
|
71
|
+
`IMAP_PORT`, `SMTP_HOST`, `SMTP_PORT` and `SMTP_SECURITY` (`ssl` or `starttls`) directly.
|
|
72
|
+
See [`.env.example`](.env.example) for the full list.
|
|
73
|
+
|
|
74
|
+
## Supported mail servers
|
|
75
|
+
|
|
76
|
+
Presets live in [`providers.json`](receipt_report/providers.json) and are easy to extend.
|
|
77
|
+
|
|
78
|
+
| `MAIL_PROVIDER` | Service | IMAP | SMTP |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `gmail` | Gmail / Google Workspace | imap.gmail.com:993 | smtp.gmail.com:465 (SSL) |
|
|
81
|
+
| `outlook` | Outlook.com / Hotmail | outlook.office365.com:993 | smtp-mail.outlook.com:587 (STARTTLS) |
|
|
82
|
+
| `office365` | Microsoft 365 (work/school) | outlook.office365.com:993 | smtp.office365.com:587 (STARTTLS) |
|
|
83
|
+
| `icloud` | iCloud Mail | imap.mail.me.com:993 | smtp.mail.me.com:587 (STARTTLS) |
|
|
84
|
+
| `yahoo` | Yahoo Mail | imap.mail.yahoo.com:993 | smtp.mail.yahoo.com:465 (SSL) |
|
|
85
|
+
| `fastmail` | Fastmail | imap.fastmail.com:993 | smtp.fastmail.com:465 (SSL) |
|
|
86
|
+
| `gmx` | GMX | imap.gmx.com:993 | mail.gmx.com:465 (SSL) |
|
|
87
|
+
| `zoho` | Zoho Mail | imap.zoho.com:993 | smtp.zoho.com:465 (SSL) |
|
|
88
|
+
| `opensrs` | OpenSRS / Tucows Hosted Email | imap.hostedemail.com:993 | smtp.hostedemail.com:465 (SSL) |
|
|
89
|
+
|
|
90
|
+
Any IMAP/SMTP server works via explicit host settings — the presets are just conveniences.
|
|
91
|
+
|
|
92
|
+
## Setting up your mail account
|
|
93
|
+
|
|
94
|
+
Most providers need two things:
|
|
95
|
+
|
|
96
|
+
1. **IMAP access enabled.** On by default for many; for some (Yahoo, GMX, Zoho) you toggle
|
|
97
|
+
it in settings.
|
|
98
|
+
2. **An app password** if the account uses 2-factor authentication. The generator logs in
|
|
99
|
+
with a plain username + password, so create a provider-specific *app password* and use it
|
|
100
|
+
as `IMAP_PASSWORD`:
|
|
101
|
+
- **Gmail:** Google Account → Security → App passwords (requires 2-Step Verification).
|
|
102
|
+
- **iCloud:** appleid.apple.com → Sign-In and Security → App-Specific Passwords.
|
|
103
|
+
- **Yahoo / Fastmail / Zoho:** Account security → generate an app password.
|
|
104
|
+
- **OpenSRS/hosted email:** usually the normal mailbox password works.
|
|
105
|
+
|
|
106
|
+
> **Microsoft note:** Outlook.com and many Microsoft 365 tenants have disabled basic
|
|
107
|
+
> IMAP/SMTP auth. If password login is refused, an admin may need to allow it, or the account
|
|
108
|
+
> may require OAuth (not handled by this tool).
|
|
109
|
+
|
|
110
|
+
The same account is used both to **read** receipts and to **send** the report, so make sure
|
|
111
|
+
SMTP submission is allowed too. The per-provider notes in `providers.json` summarise this.
|
|
112
|
+
|
|
113
|
+
## Usage
|
|
114
|
+
|
|
115
|
+
Dry run (build the PDF; touch nothing in the mailbox):
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python -m receipt_report.main --dry-run
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Real run (build the PDF, move processed emails, and email the report if `ARCHIVE_RECIPIENT`
|
|
122
|
+
is set):
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
python -m receipt_report.main
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Real run without emailing:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python -m receipt_report.main --no-email
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Output lands in `output/expense-report_<YYYY-MM-DD>.pdf`.
|
|
135
|
+
|
|
136
|
+
## Run it in the cloud (GitHub Actions)
|
|
137
|
+
|
|
138
|
+
[`.github/workflows/monthly.yml`](.github/workflows/monthly.yml) runs the generator on the
|
|
139
|
+
**last day of each month** — no machine of your own needs to be on.
|
|
140
|
+
|
|
141
|
+
Set these in your repository (**Settings → Secrets and variables → Actions**):
|
|
142
|
+
|
|
143
|
+
**Secrets** (sensitive):
|
|
144
|
+
|
|
145
|
+
| Secret | Value |
|
|
146
|
+
| --- | --- |
|
|
147
|
+
| `IMAP_USER` | Your mailbox address |
|
|
148
|
+
| `IMAP_PASSWORD` | App password / mailbox password |
|
|
149
|
+
| `ARCHIVE_RECIPIENT` | *(optional)* where to email the report |
|
|
150
|
+
|
|
151
|
+
**Variables** (non-sensitive):
|
|
152
|
+
|
|
153
|
+
| Variable | Example |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `MAIL_PROVIDER` | `gmail` |
|
|
156
|
+
| `RECEIPT_SENDER` | `receipts@vendor.com` |
|
|
157
|
+
| `RECIPIENT_NAME` | `Jane Doe` |
|
|
158
|
+
| `CURRENCY` | `kr` |
|
|
159
|
+
| `PURPOSE` | *(optional)* |
|
|
160
|
+
|
|
161
|
+
Then:
|
|
162
|
+
|
|
163
|
+
- **Test anytime:** Actions tab → *Monthly expense report* → **Run workflow**. A manual run
|
|
164
|
+
skips the last-day guard and runs immediately.
|
|
165
|
+
- **Test the failure alert:** run the workflow with the **test_fail** input checked — it forces
|
|
166
|
+
a failure and emails `NOTIFY_RECIPIENT` (or `IMAP_USER`).
|
|
167
|
+
- The generated PDF is also kept as a **run artifact** for 90 days.
|
|
168
|
+
- Scheduled workflows only run on the repository's default branch.
|
|
169
|
+
|
|
170
|
+
## Customising the parsing
|
|
171
|
+
|
|
172
|
+
Receipt layouts vary. The parser ([`receipt_parser.py`](receipt_report/receipt_parser.py)) is
|
|
173
|
+
built to be tuned:
|
|
174
|
+
|
|
175
|
+
- **Amounts** are matched in both `1,234.50` and `1 234,50` styles; lines containing keywords
|
|
176
|
+
like *total*, *amount due*, *summa* are preferred, otherwise the largest amount wins.
|
|
177
|
+
- **Dates** are read as ISO, `dd.mm.yyyy`, or `12 June 2026` (English + Swedish month names),
|
|
178
|
+
falling back to the email's date.
|
|
179
|
+
- **Descriptions** default to the first meaningful text line; set `DESCRIPTION_MATCH` to a
|
|
180
|
+
regex to grab something specific (group 1 if present).
|
|
181
|
+
|
|
182
|
+
Receipts where an amount or date can't be read are still included and listed as warnings so
|
|
183
|
+
you can check them.
|
|
184
|
+
|
|
185
|
+
## Security
|
|
186
|
+
|
|
187
|
+
- Credentials live only in `.env` (git-ignored) or your host's secret store. Nothing sensitive
|
|
188
|
+
is committed.
|
|
189
|
+
- The processed-email move happens **after** the PDF is written successfully, so an interrupted
|
|
190
|
+
run never loses receipts.
|
|
191
|
+
|
|
192
|
+
## Publishing to PyPI
|
|
193
|
+
|
|
194
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) publishes the package to
|
|
195
|
+
PyPI whenever you create a **GitHub Release**, authenticating with a PyPI API token. One-time
|
|
196
|
+
setup:
|
|
197
|
+
|
|
198
|
+
1. **Create an API token.** On [pypi.org](https://pypi.org) → *Account settings → API tokens*
|
|
199
|
+
→ *Add API token* (scope "Entire account" for the first upload; you can scope it to the
|
|
200
|
+
project afterwards).
|
|
201
|
+
2. **Store it as a secret.** Repo → *Settings → Secrets and variables → Actions* → new secret
|
|
202
|
+
named `PYPI_API_TOKEN` with the token value. Or via CLI:
|
|
203
|
+
`gh secret set PYPI_API_TOKEN --repo <owner>/<repo>`
|
|
204
|
+
3. **Release.** Bump `version` in [`pyproject.toml`](pyproject.toml), then create a GitHub
|
|
205
|
+
Release (e.g. tag `v1.0.0`). The workflow builds and uploads automatically.
|
|
206
|
+
|
|
207
|
+
Prefer no stored token? PyPI also supports OIDC *trusted publishing* — remove the `password:`
|
|
208
|
+
line from the workflow, add `permissions: id-token: write` to the publish job, and register a
|
|
209
|
+
trusted publisher on PyPI (owner, repo, workflow `publish.yml`, no environment).
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "receipt-report-generator"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Turn emailed receipts into a single expense-report PDF (cover summary + indexed appendix)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "David Barton" }]
|
|
13
|
+
keywords = ["imap", "receipts", "pdf", "expense-report", "email", "invoice", "bookkeeping"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Topic :: Office/Business :: Financial :: Accounting",
|
|
20
|
+
"Topic :: Communications :: Email",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"imap-tools>=1.7.0",
|
|
24
|
+
"pdfplumber>=0.11.0",
|
|
25
|
+
"reportlab>=4.1.0",
|
|
26
|
+
"pypdf>=4.2.0",
|
|
27
|
+
"python-dotenv>=1.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/dbarton1974/receipt-report-generator"
|
|
32
|
+
Repository = "https://github.com/dbarton1974/receipt-report-generator"
|
|
33
|
+
Issues = "https://github.com/dbarton1974/receipt-report-generator/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
receipt-report = "receipt_report.main:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools]
|
|
39
|
+
packages = ["receipt_report"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
receipt_report = ["providers.json"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Configuration, loaded from environment variables (12-factor / metadata-driven).
|
|
2
|
+
|
|
3
|
+
A named preset from providers.json can be selected with MAIL_PROVIDER, which fills in
|
|
4
|
+
IMAP/SMTP host, port and security. Any explicit host/port/security env var overrides it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
except ImportError: # python-dotenv is optional; in CI the vars are set directly.
|
|
16
|
+
def load_dotenv(*args, **kwargs): # type: ignore[misc]
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
from .providers import get_provider
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Config:
|
|
24
|
+
# IMAP
|
|
25
|
+
imap_host: str
|
|
26
|
+
imap_port: int
|
|
27
|
+
imap_user: str
|
|
28
|
+
imap_password: str
|
|
29
|
+
# SMTP
|
|
30
|
+
smtp_host: str
|
|
31
|
+
smtp_port: int
|
|
32
|
+
smtp_security: str # "ssl" or "starttls"
|
|
33
|
+
# What to collect
|
|
34
|
+
receipt_sender: str
|
|
35
|
+
inbox_folder: str
|
|
36
|
+
processed_folder: str
|
|
37
|
+
# Report
|
|
38
|
+
output_dir: Path
|
|
39
|
+
recipient_name: str
|
|
40
|
+
report_title: str
|
|
41
|
+
purpose: str # empty string => no "Purpose" column
|
|
42
|
+
currency: str
|
|
43
|
+
description_match: str # optional regex to extract a description; empty => first line
|
|
44
|
+
# Delivery / alerts
|
|
45
|
+
archive_recipient: str # empty => don't email the report
|
|
46
|
+
notify_recipient: str # empty => defaults to imap_user
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require(name: str, value: str) -> str:
|
|
50
|
+
if not value:
|
|
51
|
+
raise SystemExit(
|
|
52
|
+
f"Configuration error: {name} is missing. "
|
|
53
|
+
f"Copy .env.example to .env and fill in the values."
|
|
54
|
+
)
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_config() -> Config:
|
|
59
|
+
load_dotenv()
|
|
60
|
+
|
|
61
|
+
# Resolve provider preset (if any) for host/port/security defaults.
|
|
62
|
+
provider_key = os.getenv("MAIL_PROVIDER", "").strip()
|
|
63
|
+
preset = get_provider(provider_key) if provider_key else None
|
|
64
|
+
if provider_key and preset is None:
|
|
65
|
+
raise SystemExit(
|
|
66
|
+
f"Unknown MAIL_PROVIDER '{provider_key}'. See providers.json for valid keys, "
|
|
67
|
+
f"or set IMAP_HOST/SMTP_HOST explicitly instead."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _host_default(env_name: str, preset_value: str | None) -> str:
|
|
71
|
+
return os.getenv(env_name, "").strip() or (preset_value or "")
|
|
72
|
+
|
|
73
|
+
imap_host = _host_default("IMAP_HOST", preset.imap_host if preset else None)
|
|
74
|
+
smtp_host = _host_default("SMTP_HOST", preset.smtp_host if preset else None)
|
|
75
|
+
|
|
76
|
+
imap_port = int(os.getenv("IMAP_PORT", str(preset.imap_port if preset else 993)))
|
|
77
|
+
smtp_port = int(os.getenv("SMTP_PORT", str(preset.smtp_port if preset else 465)))
|
|
78
|
+
smtp_security = (
|
|
79
|
+
os.getenv("SMTP_SECURITY", "").strip().lower()
|
|
80
|
+
or (preset.smtp_security if preset else "ssl")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
recipient_name = os.getenv("RECIPIENT_NAME", "Me")
|
|
84
|
+
report_title = os.getenv("REPORT_TITLE", "Expense report for {name}").replace(
|
|
85
|
+
"{name}", recipient_name
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return Config(
|
|
89
|
+
imap_host=_require("IMAP_HOST (or MAIL_PROVIDER)", imap_host),
|
|
90
|
+
imap_port=imap_port,
|
|
91
|
+
imap_user=_require("IMAP_USER", os.getenv("IMAP_USER", "").strip()),
|
|
92
|
+
imap_password=_require("IMAP_PASSWORD", os.getenv("IMAP_PASSWORD", "").strip()),
|
|
93
|
+
smtp_host=smtp_host or imap_host,
|
|
94
|
+
smtp_port=smtp_port,
|
|
95
|
+
smtp_security=smtp_security,
|
|
96
|
+
receipt_sender=_require("RECEIPT_SENDER", os.getenv("RECEIPT_SENDER", "").strip()),
|
|
97
|
+
inbox_folder=os.getenv("INBOX_FOLDER", "INBOX"),
|
|
98
|
+
processed_folder=os.getenv("PROCESSED_FOLDER", "Processed/Receipts"),
|
|
99
|
+
output_dir=Path(os.getenv("OUTPUT_DIR", "./output")).expanduser(),
|
|
100
|
+
recipient_name=recipient_name,
|
|
101
|
+
report_title=report_title,
|
|
102
|
+
purpose=os.getenv("PURPOSE", "").strip(),
|
|
103
|
+
currency=os.getenv("CURRENCY", "kr"),
|
|
104
|
+
description_match=os.getenv("DESCRIPTION_MATCH", "").strip(),
|
|
105
|
+
archive_recipient=os.getenv("ARCHIVE_RECIPIENT", "").strip(),
|
|
106
|
+
notify_recipient=os.getenv("NOTIFY_RECIPIENT", "").strip(),
|
|
107
|
+
)
|