Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8e67a28
Return Message Styles angefangen
Nov 17, 2025
4e8e3eb
Erster Error Handler
Pitastic Nov 17, 2025
0a3aa76
resultText Handling in Index (untested)
Nov 21, 2025
d3f93a5
Upload Settings
Nov 21, 2025
c86d1a0
More Text; Todos für Screensize Designs
Nov 24, 2025
88232a0
Comments, Class-Rename (s-m-l); first Layout breackpoints
Nov 25, 2025
a399f94
Fix test with new code
Pitastic Dec 5, 2025
cde5a57
Disable autofill on hidden inputs
Pitastic Dec 9, 2025
373da79
Hide Columns and reformat Button in Transaction table on M/S Screens
Pitastic Dec 9, 2025
50ac777
Small Layout mit grid; reorder all colums
Pitastic Dec 9, 2025
27ace3b
Notes und kleine fixes
Pitastic Dec 9, 2025
6fa54b1
Random Tag-Color eingebaut; muss noch in PopUps und Inputs
Pitastic Dec 10, 2025
60f2ef2
TagChip as Points on smal Screens
Pitastic Dec 11, 2025
22d5385
Details PopUp Stacking
Pitastic Dec 11, 2025
76a5efe
Table und Tag Chiü Feinschliff
Pitastic Dec 14, 2025
5ae2e0f
VB Hessen Importer angefangen
Pitastic Dec 14, 2025
61ceab7
Important notes
Pitastic Dec 14, 2025
7671c61
Add VB Import
Pitastic Dec 15, 2025
75e2010
JS float mit Decimals
Pitastic Dec 15, 2025
77c91ac
pylint setting
Pitastic Dec 15, 2025
be413ad
Readme angepasst
Pitastic Dec 17, 2025
3d9e04a
Emoji
Pitastic Dec 17, 2025
90907bf
Language hint
Pitastic Dec 17, 2025
42564e2
Add Gegenkonto filter
Pitastic Dec 23, 2025
c03448f
Change "gegenkonto" to "peer"
Pitastic Dec 24, 2025
5954484
repair date filter
Pitastic Dec 27, 2025
c845d65
repair gem-color-classes (jinja & js)
Pitastic Dec 27, 2025
1f6fc4d
Typo in "peer" + Add Filter Tooltip
Pitastic Dec 27, 2025
2cdfe11
Add day for endDate Filter
Pitastic Dec 27, 2025
48e1506
fix pylint
Pitastic Dec 27, 2025
0b9aefb
Add Screenshots
Pitastic Dec 27, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
#
# -- Lint and parse Output
#
pylint_output=$(PYTHONPATH=. pylint . --recursive=y --disable=W0511,R0903 --score=y)
pylint_output=$(PYTHONPATH=. pylint . --recursive=y --disable=W0511,R0903,R0801 --score=y)
exitcode=$?
score=$(sed -n '$s/[^0-9]*\([0-9.]*\).*/\1/p' <<< "$pylint_output")
#
Expand Down
2 changes: 1 addition & 1 deletion Models.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
'date_tx': int, # (UTC)
'text_tx': str,
'betrag': float,
'gegenkonto': str,
'peer': str,

----------- optional -----------

Expand Down
118 changes: 70 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,71 @@
![pytest](https://img.shields.io/badge/pytest-passed%20(53/53)-darkgreen)
![pylint](https://img.shields.io/badge/pylint-9.75-yellow)

*This repo is german but you are welcome to add your language to the frontend.*

Analyse und Darstellung von Kontoumsätzen bei mehreren Banken.

## Get Started

### Setup

```
python3.12 -m venv .venv
source .venv/bin/activate
.venv/bin/python3.12 -m ensurepip --upgrade # (optional)
pip install -r requirements.txt
.venv/bin/python3.12 app/server.py
```

### Start

- Importiere Kontoumsätze über CSV Listen oder PDF Kontoauszüge deiner Bank ([unterstützte Banken](#unterstützte-banken))
- Erstelle eine Gruppe mehrerer Konten, um alle diese Umsätze in einer Übersicht zu sehen
- Wende vorgefertigte oder eigene Regeln für das automatische Taggen und Kategorisieren deiner Umsätze an
- Suche und Filtere deine Umsätze nach einer Vielzahl möglicher Kriterien
- Lerne mehr über deinen Cashflow durch die Übersicht der statistischen Auswertungen. Hier kannst du alle oder nur gefilterte Umsätze berücksichtigen.

## Features

### Parsing
Die Funktionen des PynanceParsers setzen stark auf Reproduzierbarkeit. Das bedeutet, dass du beliebig oft gleiche Daten löschen und reimportieren kannst und halbautomatisch wieder die gleichen Ergebnisse (einmalige Transaktionen, Tagging, Kategorien, Statistiken) erhälts. Ein manuelles Editieren ist zwar möglich, aber die Ausnahme.

👉 **Modernes und responsives Design**

*(Übersichtlich auf vielen Geräten)*

👉 **Keine doppelten Imports**

*(Datum, Text und Betrag bilden eine einmalige Kombination)*

👉 **Automatisches Extrahieren von Zusatzinformationen**

Importiere Kontoumsätzen aus Dateien im Format unterstützter Banken (Exports von Umsatzübersichten als CSV, Kontoauszüge als PDF). Für Auswertung der Ausgaben von Zeit zu Zeit.
*(RegEx parst Kerninformationen)*

Modulare Importer können nach und nach für verschiedene Banken oder spezielle Formate entwickelt werden. Füge einen Importer für deine Bank hinzu :wink:
👉 **Automatisches und/oder manuelles Taggen**

### Analyse
*(Regelbasiert: RegEx + Zusatzinformationen)*

- Keine doppelten Imports *(Datum, Text und Betrag bilden eine einmalige Kombination)*
- Automatisches Extrahieren von Zusatzinformationen einer Transaktion durch Muster *(RegEx parst Kerninformationen)*
- Automatisches und/oder manuelles Taggen von Umsätzen *(Regelbasiert: RegEx + Zusatzinformationen)*
- Automatisches und/oder manuelles Kategorisieren von Umsätzen *(Regelbasiert: RegEx + Tags und weitere Indikatoren)*
- Übersicht über alle Transaktionen *(Vielseitige Filtermöglichkeiten)*
- Statistische Auswertung auf dem angereicherten Datensatz vieler Transaktionen *(interaktive Grafiken)*
👉 **Automatisches und/oder manuelles Kategorisieren**

Hinterlegte Regeln können die extrahierten Informationen, weitere Umsatzinformationen und weitere RegExes berücksichtigen und ermöglichen so komplexe Bewertungen einfach zu erstellen.
*(Regelbasiert: RegEx + Tags + Zusatzinformationen)*

Ein Tagging findet anschließend auf angereicherten Informationen regelbasiert statt und kann außerdem auch manuell erfolgen.
👉 **Übersicht über alle Transaktionen**

*(vielseitige Filtermöglichkeiten in einem Konto oder einer Kontogruppe)*

👉 **Statistische Auswertung auf dem angereicherten Datensatz**

*(Kontextabhängige Statistken)*

Auf dieser Grundlage werden Umsätze Kategorisiert wobei auch das händisch editiert werden kann.

### Darstellung

- Kontenverwaltung
- Kontohistorie
- Transaktionsansicht
- Transaktionsdetails
- Statistiken/Verteilungen/Verläufe

Listen und Diagramme zeigen dir, wo eigentlich das Geld geblieben ist :thinking:

## Misc
![screenshots](https://github.com/user-attachments/assets/f6201658-eeb0-422c-b1a8-df9cb85cf842)

### Unterstützte Banken

Expand All @@ -45,7 +76,11 @@ Listen und Diagramme zeigen dir, wo eigentlich das Geld geblieben ist :thinking:
| Comdirect | 🟢 Umsatzübersicht | 🟢 Finanzreport |
| Commerzbank | 🟢 Umsatzübersicht | 🟢 Kontoauszug |
| Sparkasse Hannover | ⚫ *planned* | ⚫ *planned* |
| Volksbank Mittelhessen eG | 🟢 Umsatzübersicht | ⚫ *planned* |
| Volksbank Mittelhessen eG | 🟢 Umsatzübersicht | 🟢 Kontoauszug |

Ist deine Bank noch nicht dabei? Den modularen Import kannst du mit [überschaubaren Aufwand](#entwickeln-von-neuen-readern) für deine Bank erweitern.

## Hinweise

### Workflow (CSV / PDF Imports)

Expand All @@ -58,28 +93,32 @@ Daher sollte man beachten:
- Regeln nicht auf zwingend vorhandene Leerzeichen auszulegen
- Beim Wechsel eines Formats (PDF / CSV) keine Überschneidungen zu haben (PDF zuerst, dann fehlende Transaktionen selektieren und via CSV exportieren - alternativ bei einem Format bleiben)

### Tagging- und Kategorisierungsregeln
### Default Tagging- und Kategorisierungsregeln

In diesem Repository werden nur Basis-Regeln mitgeliefert, da speziellere und genauere Regeln sehr individuell auf einzelne Personen zugeschnitten sind. So schreibt zum Beispiel eine Versicherung die Versichertennummer mit in die Abbuchungen, was einen sehr guten Tagging-Indikator darstellt, jedoch nur für einen speziellen Nutzer dieses Programms. Das schreiben eigener Regeln ist daher unumgänglich, um bessere Ergebnisse zu erzielen.

Für diesen Zweck gibt es aber die Möglichkeit im Frontend Regeln auszuprobieren, ohne dass Umsätze geändert werden. Neue Regeln können ebenfalls über die Oberfläche temporär hochgeladen werden (bis zum Neustart des Servers) oder dauerhaft im Ordner `settings/rule` abgelegt werden. Die Dateien hier werden in alphabetisch sortierter Reihenfolge geladen (angefangen bei `00-*`), wobei spätere Regeln ggf. bestehende Regeln überschreiben können. Im Rwepository werden nur die Default-Regeln angepasst. Auf diese Weise können eigene Regeln gepflegt werden, ohne dass sie bei Updates verloren gehen.
## Anpassungen / Contribution

**You're Welcome !** :tada:

## Contribution
Erstelle einen Reader für verschiedene Formate deiner Bank oder ergänze die `parser` und `rules`.

You're Welcome !
### Entwickeln neuer `parser` / `rules`

Erstelle einen Reader für verschiedene Formate deiner Bank oder ergänze die `parser` und `rules`.
Für diesen Zweck gibt es die Möglichkeit im Frontend Regeln auszuprobieren, ohne dass Umsätze geändert werden. Neue Regeln können ebenfalls über die Oberfläche temporär hochgeladen werden (bis zum Neustart des Servers) oder dauerhaft im Ordner `settings/rule` abgelegt werden. Die Dateien hier werden in alphabetisch sortierter Reihenfolge geladen (angefangen bei `00-*`), wobei spätere Regeln ggf. bestehende Regeln überschreiben können. Im Repository werden nur die Default-Regeln angepasst. Auf diese Weise können eigene Regeln gepflegt werden, ohne dass sie bei Updates verloren gehen.

## Setup
**Wenn du neue Regeln für dieses Repository beitragen möchtest, gehst du wie folgt vor:**

```
python3.12 -m venv .venv
source .venv/bin/activate
.venv/bin/python3.12 -m ensurepip --upgrade # (optional)
pip install -r requirements.txt
.venv/bin/python3.12 app/server.py
```
- Erstelle einen Fork des Repositories
- Erstelle Testdaten, auf die die neuen Regeln treffen können
- (am einfachsten ist eine JSON Datei wie `tests/commerzbank.json`)
- Erstelle einen Test wie in (`test_unit_handler_Tags.py`: `test_parsing_regex()`)
- Tests helfen beim entwickeln, können aber auch durch die Maintainer während des Pull Request erstellt werden
- Stelle einen Pull Request

### Entwickeln von neuen Readern

Deine Bank fehlt noch in der Support Tabelle? Stelle einen Pull Request mit einem neuen Reader. [So kannst du ihn erstellen.](Reader.md).

### Testumgebung

Expand All @@ -89,20 +128,3 @@ pip install -r requirements.txt
pip install -r tests/requirements.txt
pytest
```

## Entwickeln von neuen Readern

- Erstelle einen neuen Test unter `tests/`
- (kopiere am besten `tests/test_unit_reader_Comdirect.py`)
- Erstelle ein neues Skript unter `reader/`
- (kopiere am besten `reader/Generic.py`)
- Passe die Logik im Test so an, dass dieser ausgeführt wird, wenn eine Testdatei vorhanden ist.
- Entwickle deinen Reader und teste ihn dabei immer wieder mit `pytest -svx tests/test_unit_reader_*.py`
- Pushe **keine** Testdaten (Kontoumsätze) ins Repo!

## Entwickeln neuer `parser` / `rules`

- Erstelle Testdaten, auf die die neuen Regeln treffen können
- (am einfachsten ist eine JSON Datei wie `tests/commerzbank.json`)
- Erstelle einen Test wie in (`test_unit_handler_Tags.py`: `test_parsing_regex()`)
- Tests helfen beim entwickeln, können aber auch durch mich beim Pull Request erstellt werden
54 changes: 54 additions & 0 deletions Reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
## Entwickeln von neuen Readern

### Erstelle notwendige Dateien

Neben dem Modul benätigst du noch einen Test. Ziel des Tests ist es, dass lokal eine Beispiel Datei importiert wird, wenn sie vorhanden ist und im Ergebnis geprüft wird, ob die eingelesenen Einträge sinnvoll sind. Eine Prüfung nach Transaktionsinhalten findet nicht statt.

**Lade keine privaten Kontoauszüge in das Repository hoch !**

Kopiere dazu am besten `tests/test_unit_reader_Comdirect.py` und...

- Passe den Dateinamen der Beispieldatei an
- Entscheide, ggf. eine Testfunktion zu überspringen mit `@pytest.mark.skip(reason="Currently not implemented yet")`

Kopiere als nächstes `reader/Generic.py` und

- Passe den Dateinamen der Beispieldatei an
- Entwickle deinen Reader (siehe unten) und teste ihn zwischendurch/am Ende mit `pytest -svx tests/test_unit_reader_*.py`

Der Test führt die `from_csv` bzw. `from_pdf` Funktion aus und überprüft, ob das Format richtig ist und die Werte der Transaktionen über entsprechende Keys mit sinnvollen Werten verfügen.

### Logik des Readers erstellen

Ziel der Funktion `from_csv` bzw. `from_pdf` ist es, eine Liste von Transaktionen in Form eines Dictionaries einzulesen, wobei die Keys der Vorgabe des [allgemeinen Models für Transaktionen](Models.md) folgt.

Für das Einlesen einer PDF ist dabei mehr Aufwand notwendig. Hierbei kommt das Modul `camelot` zum Einsatz.

Für die Entwicklung der richtigen Einstellungen wird neben dem Python Modul auch das CLI Programm empfohlen. Installation und Konfigurationen findest du in der [Dokumentation](https://camelot-py.readthedocs.io/en/master/).

#### Step by Step

- Lege eine Beispieldatei nach `/tmp/bank.pdf`
- Nutze den `grid` Modus im CLI, um angezeigt zu bekommen, was `camelot` mit den jeweiligen Einstellungen für Tabelleninhalte finden würde.
- Übertrage die erfolgreichen Flags und Einstellungen in den Python Aufruf im Reader-Modul
- Lasse dir dort erst alle eingelesenen Zeileninhalte ausgeben (während eines `pytests`) und entscheide, wie du die Inhalte ggf. umformen, zusammenfassen oder trennen musst, um das [erwartete Muster](Models.md) zu erhalten.

Ein guter Start ist folgender Aufruf:

```
camelot -plot grid /tmp/bank.pdf
```

Nützliche Flags sind `-p` für einzelne Seiten (schneller bzw. können so auch Edgecases geprüft werden), `-C` um die Koordinaten von Spalten festzulegen (meistens erforderlich) und `-T` um den Bereich festzulegen, wo nach einer Tabelle geprüft werden soll (meistens erforderlich). Ein fortgeschrittenerer Aufruf könnte daher so aussehen:

```
camelot -p 1,6 stream -C "75,112,440,526" -T "60,629,573,51" -plot grid /tmp/bank.pdf
```

Am Schluss kann noch der Wert eingestellt werden, ab dem Buchstaben vertikal oder horizontal zu einem Wort oder einer Zeile zusammengefasst oder schon getrennt werden. Mit dem Argument `layout_kwargs` in der Python-Methode können Argumente an den darunterliegenden `PDFMiner` durchgereicht werden, was genau diese Einstellungen ermöglicht.

Die Dokumentation dieser möglichen Werte findet man in der [Dokumentation des PDFMiner](https://pdfminersix.readthedocs.io/en/latest/reference/composable.html). Der [Commerbank.py Reader](reader/Commerzbank.py) nutzt diese wegen der bestimmten Schriftart und Zeichenabständen im PDF.

## Stelle einen Pull Request

Beschreibe, welche Features du hinzugefügt oder verbessert hast. Pytest und Pylint werden hier geprüft. Das Testen des neuen Imports kann nur bei dir erfolgen, da die Maintainer in der Regel über keine Testdateien dafür verfügen. Teste daher sorgfältig.
59 changes: 43 additions & 16 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ def __init__(self, parent):
def timectime(s):
return datetime.fromtimestamp(s).strftime('%d.%m.%Y')

@current_app.template_filter('hash')
def to_hash(string):
hash_value = 0

if len(string) == 0:
return hash_value

for char in string:
char_code = ord(char)
hash_value = ((hash_value << 5) - hash_value) + char_code
hash_value = hash_value & 0xFFFFFFFF # Ensure 32-bit integer

return hash_value

@current_app.context_processor
def version_string():
return {
Expand All @@ -41,7 +55,9 @@ def iban(iban) -> str:
Startseite in einem Konto.

Args (uri):
iban, str: IBAN zu der die Einträge angezeigt werden sollen.
iban, str: IBAN zu der die Einträge angezeigt werden sollen.
text, str (query): Volltextsuche im Betreff mit RegEx Support
peer, str (query): Volltextsuche im Gegenkonto mit RegEx Support
startDate, str (query): Startdatum (Y-m-d) für die Anzeige der Einträge
endDate, str (query): Enddatum (Y-m-d) für die Anzeige der Einträge
category, str (query): Kategorie-Filter
Expand Down Expand Up @@ -237,7 +253,7 @@ def addGroup(groupname):
assert ibans is not None, 'No IBANs provided'
r = parent.db_handler.add_iban_group(groupname, ibans)
if not r.get('inserted'):
return {'error': 'No Group added', 'reason': r.get('error')}, 400
return {'error': f'Keine Gruppe angelegt: {r.get("error")}'}, 400

return r, 201

Expand Down Expand Up @@ -280,7 +296,7 @@ def saveMeta(rule_type):
r = parent.db_handler.set_metadata(entry, overwrite=True)

if not r.get('inserted'):
return {'error': 'No data inserted', 'reason': r.get('error')}, 400
return {'error': f'No data inserted: {r.get("error")}'}, 400

return r, 201

Expand Down Expand Up @@ -323,7 +339,7 @@ def uploadIban(iban):
"""
input_file = request.files.get('file-input')
if not input_file:
return {'error': 'No file provided'}, 400
return {'error': 'Es wurde keine Datei übermittelt.'}, 400

# Store Upload file to tmp
path = '/tmp/transactions.tmp'
Expand All @@ -343,15 +359,26 @@ def uploadIban(iban):
path = f'{path}.pdf'

# Read Input and Parse the contents
parsed_data = parent.read_input(
path, bank=request.form.get('bank', 'Generic'),
data_format=content_format
)
try:
parsed_data = parent.read_input(
path, bank=request.form.get('bank', 'Generic'),
data_format=content_format
)

# Verarbeitete Kontiumsätze in die DB speichern
# und vom Objekt und Dateisystem löschen
insert_result = parent.db_handler.insert(parsed_data, iban)
inserted = insert_result.get('inserted')

except (KeyError, ValueError, NotImplementedError) as ex:
return {
"error": (
"Die hochgeladene Datei konnte nicht verarbeitet werden, "
"da das Format unvollständig ist oder nicht erwartet wurde: "
+ ex.__class__.__name__ + " " + str(ex)
)
}, 406

# Verarbeitete Kontiumsätze in die DB speichern
# und vom Objekt und Dateisystem löschen
insert_result = parent.db_handler.insert(parsed_data, iban)
inserted = insert_result.get('inserted')
os.remove(path)

return_code = 201 if inserted else 200
Expand All @@ -373,9 +400,10 @@ def uploadRules(metadata):
Returns:
json: Informationen zur Datei und Ergebnis der Untersuchung.
"""
input_file = request.files.get('file-input')
print(request.files)
input_file = request.files.get('settings-input')
if not input_file:
return {'error': 'No file provided'}, 400
return {'error': 'Es wurde keine Datei übermittelt.'}, 400

# Store Upload file to tmp
path = f'/tmp/{metadata}.tmp'
Expand All @@ -396,8 +424,7 @@ def deleteDatabase(iban):
Returns:
json: Informationen zum Ergebnis des Löschauftrags.
"""
deleted_entries = parent.db_handler.truncate(iban)
return {'deleted': deleted_entries}, 200
return parent.db_handler.truncate(iban), 200

@current_app.route('/api/tag/<iban>', methods=['PUT'])
def tag(iban) -> dict:
Expand Down
Loading