Skip to content

Commit e4bfea2

Browse files
authored
Merge pull request #47 from Pitastic/polishing
Polishing
2 parents 39b3d0d + 0b9aefb commit e4bfea2

27 files changed

Lines changed: 1065 additions & 306 deletions

.github/workflows/python-app.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
#
8686
# -- Lint and parse Output
8787
#
88-
pylint_output=$(PYTHONPATH=. pylint . --recursive=y --disable=W0511,R0903 --score=y)
88+
pylint_output=$(PYTHONPATH=. pylint . --recursive=y --disable=W0511,R0903,R0801 --score=y)
8989
exitcode=$?
9090
score=$(sed -n '$s/[^0-9]*\([0-9.]*\).*/\1/p' <<< "$pylint_output")
9191
#

Models.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
'date_tx': int, # (UTC)
4040
'text_tx': str,
4141
'betrag': float,
42-
'gegenkonto': str,
42+
'peer': str,
4343
4444
----------- optional -----------
4545

README.md

Lines changed: 70 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,71 @@
33
![pytest](https://img.shields.io/badge/pytest-passed%20(53/53)-darkgreen)
44
![pylint](https://img.shields.io/badge/pylint-9.75-yellow)
55

6+
*This repo is german but you are welcome to add your language to the frontend.*
7+
68
Analyse und Darstellung von Kontoumsätzen bei mehreren Banken.
79

10+
## Get Started
11+
12+
### Setup
13+
14+
```
15+
python3.12 -m venv .venv
16+
source .venv/bin/activate
17+
.venv/bin/python3.12 -m ensurepip --upgrade # (optional)
18+
pip install -r requirements.txt
19+
.venv/bin/python3.12 app/server.py
20+
```
21+
22+
### Start
23+
24+
- Importiere Kontoumsätze über CSV Listen oder PDF Kontoauszüge deiner Bank ([unterstützte Banken](#unterstützte-banken))
25+
- Erstelle eine Gruppe mehrerer Konten, um alle diese Umsätze in einer Übersicht zu sehen
26+
- Wende vorgefertigte oder eigene Regeln für das automatische Taggen und Kategorisieren deiner Umsätze an
27+
- Suche und Filtere deine Umsätze nach einer Vielzahl möglicher Kriterien
28+
- Lerne mehr über deinen Cashflow durch die Übersicht der statistischen Auswertungen. Hier kannst du alle oder nur gefilterte Umsätze berücksichtigen.
29+
830
## Features
931

10-
### Parsing
32+
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.
33+
34+
👉 **Modernes und responsives Design**
35+
36+
*(Übersichtlich auf vielen Geräten)*
37+
38+
👉 **Keine doppelten Imports**
39+
40+
*(Datum, Text und Betrag bilden eine einmalige Kombination)*
41+
42+
👉 **Automatisches Extrahieren von Zusatzinformationen**
1143

12-
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.
44+
*(RegEx parst Kerninformationen)*
1345

14-
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:
46+
👉 **Automatisches und/oder manuelles Taggen**
1547

16-
### Analyse
48+
*(Regelbasiert: RegEx + Zusatzinformationen)*
1749

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

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

27-
Ein Tagging findet anschließend auf angereicherten Informationen regelbasiert statt und kann außerdem auch manuell erfolgen.
54+
👉 **Übersicht über alle Transaktionen**
55+
56+
*(vielseitige Filtermöglichkeiten in einem Konto oder einer Kontogruppe)*
57+
58+
👉 **Statistische Auswertung auf dem angereicherten Datensatz**
59+
60+
*(Kontextabhängige Statistken)*
2861

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

3163
### Darstellung
3264

65+
- Kontenverwaltung
3366
- Kontohistorie
34-
- Transaktionsansicht
67+
- Transaktionsdetails
3568
- Statistiken/Verteilungen/Verläufe
3669

37-
Listen und Diagramme zeigen dir, wo eigentlich das Geld geblieben ist :thinking:
38-
39-
## Misc
70+
![screenshots](https://github.com/user-attachments/assets/f6201658-eeb0-422c-b1a8-df9cb85cf842)
4071

4172
### Unterstützte Banken
4273

@@ -45,7 +76,11 @@ Listen und Diagramme zeigen dir, wo eigentlich das Geld geblieben ist :thinking:
4576
| Comdirect | 🟢 Umsatzübersicht | 🟢 Finanzreport |
4677
| Commerzbank | 🟢 Umsatzübersicht | 🟢 Kontoauszug |
4778
| Sparkasse Hannover |*planned* |*planned* |
48-
| Volksbank Mittelhessen eG | 🟢 Umsatzübersicht |*planned* |
79+
| Volksbank Mittelhessen eG | 🟢 Umsatzübersicht | 🟢 Kontoauszug |
80+
81+
Ist deine Bank noch nicht dabei? Den modularen Import kannst du mit [überschaubaren Aufwand](#entwickeln-von-neuen-readern) für deine Bank erweitern.
82+
83+
## Hinweise
4984

5085
### Workflow (CSV / PDF Imports)
5186

@@ -58,28 +93,32 @@ Daher sollte man beachten:
5893
- Regeln nicht auf zwingend vorhandene Leerzeichen auszulegen
5994
- 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)
6095

61-
### Tagging- und Kategorisierungsregeln
96+
### Default Tagging- und Kategorisierungsregeln
6297

6398
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.
6499

65-
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.
100+
## Anpassungen / Contribution
66101

102+
**You're Welcome !** :tada:
67103

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

70-
You're Welcome !
106+
### Entwickeln neuer `parser` / `rules`
71107

72-
Erstelle einen Reader für verschiedene Formate deiner Bank oder ergänze die `parser` und `rules`.
108+
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.
73109

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

76-
```
77-
python3.12 -m venv .venv
78-
source .venv/bin/activate
79-
.venv/bin/python3.12 -m ensurepip --upgrade # (optional)
80-
pip install -r requirements.txt
81-
.venv/bin/python3.12 app/server.py
82-
```
112+
- Erstelle einen Fork des Repositories
113+
- Erstelle Testdaten, auf die die neuen Regeln treffen können
114+
- (am einfachsten ist eine JSON Datei wie `tests/commerzbank.json`)
115+
- Erstelle einen Test wie in (`test_unit_handler_Tags.py`: `test_parsing_regex()`)
116+
- Tests helfen beim entwickeln, können aber auch durch die Maintainer während des Pull Request erstellt werden
117+
- Stelle einen Pull Request
118+
119+
### Entwickeln von neuen Readern
120+
121+
Deine Bank fehlt noch in der Support Tabelle? Stelle einen Pull Request mit einem neuen Reader. [So kannst du ihn erstellen.](Reader.md).
83122

84123
### Testumgebung
85124

@@ -89,20 +128,3 @@ pip install -r requirements.txt
89128
pip install -r tests/requirements.txt
90129
pytest
91130
```
92-
93-
## Entwickeln von neuen Readern
94-
95-
- Erstelle einen neuen Test unter `tests/`
96-
- (kopiere am besten `tests/test_unit_reader_Comdirect.py`)
97-
- Erstelle ein neues Skript unter `reader/`
98-
- (kopiere am besten `reader/Generic.py`)
99-
- Passe die Logik im Test so an, dass dieser ausgeführt wird, wenn eine Testdatei vorhanden ist.
100-
- Entwickle deinen Reader und teste ihn dabei immer wieder mit `pytest -svx tests/test_unit_reader_*.py`
101-
- Pushe **keine** Testdaten (Kontoumsätze) ins Repo!
102-
103-
## Entwickeln neuer `parser` / `rules`
104-
105-
- Erstelle Testdaten, auf die die neuen Regeln treffen können
106-
- (am einfachsten ist eine JSON Datei wie `tests/commerzbank.json`)
107-
- Erstelle einen Test wie in (`test_unit_handler_Tags.py`: `test_parsing_regex()`)
108-
- Tests helfen beim entwickeln, können aber auch durch mich beim Pull Request erstellt werden

Reader.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
## Entwickeln von neuen Readern
2+
3+
### Erstelle notwendige Dateien
4+
5+
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.
6+
7+
**Lade keine privaten Kontoauszüge in das Repository hoch !**
8+
9+
Kopiere dazu am besten `tests/test_unit_reader_Comdirect.py` und...
10+
11+
- Passe den Dateinamen der Beispieldatei an
12+
- Entscheide, ggf. eine Testfunktion zu überspringen mit `@pytest.mark.skip(reason="Currently not implemented yet")`
13+
14+
Kopiere als nächstes `reader/Generic.py` und
15+
16+
- Passe den Dateinamen der Beispieldatei an
17+
- Entwickle deinen Reader (siehe unten) und teste ihn zwischendurch/am Ende mit `pytest -svx tests/test_unit_reader_*.py`
18+
19+
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.
20+
21+
### Logik des Readers erstellen
22+
23+
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.
24+
25+
Für das Einlesen einer PDF ist dabei mehr Aufwand notwendig. Hierbei kommt das Modul `camelot` zum Einsatz.
26+
27+
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/).
28+
29+
#### Step by Step
30+
31+
- Lege eine Beispieldatei nach `/tmp/bank.pdf`
32+
- Nutze den `grid` Modus im CLI, um angezeigt zu bekommen, was `camelot` mit den jeweiligen Einstellungen für Tabelleninhalte finden würde.
33+
- Übertrage die erfolgreichen Flags und Einstellungen in den Python Aufruf im Reader-Modul
34+
- 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.
35+
36+
Ein guter Start ist folgender Aufruf:
37+
38+
```
39+
camelot -plot grid /tmp/bank.pdf
40+
```
41+
42+
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:
43+
44+
```
45+
camelot -p 1,6 stream -C "75,112,440,526" -T "60,629,573,51" -plot grid /tmp/bank.pdf
46+
```
47+
48+
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.
49+
50+
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.
51+
52+
## Stelle einen Pull Request
53+
54+
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.

app/routes.py

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ def __init__(self, parent):
1717
def timectime(s):
1818
return datetime.fromtimestamp(s).strftime('%d.%m.%Y')
1919

20+
@current_app.template_filter('hash')
21+
def to_hash(string):
22+
hash_value = 0
23+
24+
if len(string) == 0:
25+
return hash_value
26+
27+
for char in string:
28+
char_code = ord(char)
29+
hash_value = ((hash_value << 5) - hash_value) + char_code
30+
hash_value = hash_value & 0xFFFFFFFF # Ensure 32-bit integer
31+
32+
return hash_value
33+
2034
@current_app.context_processor
2135
def version_string():
2236
return {
@@ -41,7 +55,9 @@ def iban(iban) -> str:
4155
Startseite in einem Konto.
4256
4357
Args (uri):
44-
iban, str: IBAN zu der die Einträge angezeigt werden sollen.
58+
iban, str: IBAN zu der die Einträge angezeigt werden sollen.
59+
text, str (query): Volltextsuche im Betreff mit RegEx Support
60+
peer, str (query): Volltextsuche im Gegenkonto mit RegEx Support
4561
startDate, str (query): Startdatum (Y-m-d) für die Anzeige der Einträge
4662
endDate, str (query): Enddatum (Y-m-d) für die Anzeige der Einträge
4763
category, str (query): Kategorie-Filter
@@ -237,7 +253,7 @@ def addGroup(groupname):
237253
assert ibans is not None, 'No IBANs provided'
238254
r = parent.db_handler.add_iban_group(groupname, ibans)
239255
if not r.get('inserted'):
240-
return {'error': 'No Group added', 'reason': r.get('error')}, 400
256+
return {'error': f'Keine Gruppe angelegt: {r.get("error")}'}, 400
241257

242258
return r, 201
243259

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

282298
if not r.get('inserted'):
283-
return {'error': 'No data inserted', 'reason': r.get('error')}, 400
299+
return {'error': f'No data inserted: {r.get("error")}'}, 400
284300

285301
return r, 201
286302

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

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

345361
# Read Input and Parse the contents
346-
parsed_data = parent.read_input(
347-
path, bank=request.form.get('bank', 'Generic'),
348-
data_format=content_format
349-
)
362+
try:
363+
parsed_data = parent.read_input(
364+
path, bank=request.form.get('bank', 'Generic'),
365+
data_format=content_format
366+
)
367+
368+
# Verarbeitete Kontiumsätze in die DB speichern
369+
# und vom Objekt und Dateisystem löschen
370+
insert_result = parent.db_handler.insert(parsed_data, iban)
371+
inserted = insert_result.get('inserted')
372+
373+
except (KeyError, ValueError, NotImplementedError) as ex:
374+
return {
375+
"error": (
376+
"Die hochgeladene Datei konnte nicht verarbeitet werden, "
377+
"da das Format unvollständig ist oder nicht erwartet wurde: "
378+
+ ex.__class__.__name__ + " " + str(ex)
379+
)
380+
}, 406
350381

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

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

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

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

0 commit comments

Comments
 (0)