Skip to content

Commit 8abe587

Browse files
authored
Merge pull request #35 from Pitastic/add_importers
Add importers
2 parents 85684c4 + 18fab66 commit 8abe587

22 files changed

Lines changed: 655 additions & 79 deletions

Models.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
'date_tx': int, # (UTC)
4040
'text_tx': str,
4141
'betrag': float,
42-
'iban': str,
42+
'gegenkonto': str,
4343
4444
----------- optional -----------
4545
46-
'date_wert': int , # (UTC)
46+
'valuta': int , # (UTC)
4747
'art': str,
4848
'currency': str,
4949
'parsed': dict( str: str )

README.md

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,67 @@ Analyse und Darstellung von Kontoumsätzen bei mehreren Banken.
99

1010
### Parsing
1111

12-
Importieren von Kontoumsätzen aus
13-
14-
- Umsatzübersicht im Online Banking
15-
- CSV Export
16-
- PDF Export
17-
- Kontoauszüge
18-
- PDFs aus dem Online Banking Archiv
19-
- PDFs eingescannter Papierauszüge
20-
- Online Quellen
21-
- HTTP (Daten von APIs - keine Banken-APIs leider :man_shrugging: )
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.
2213

2314
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:
2415

2516
### Analyse
2617

27-
- Automatisches Extrahieren und bewerten einer Transaktion durch Muster *(RegEx parst Kerninformationen)*
28-
- Automatisches Kategorisieren anhand hinterlegter Regeln *(RegEx + Kerninformationen)*
29-
- Manuelles Kategorisieren
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)*
3024

3125
Hinterlegte Regeln können die extrahierten Informationen, weitere Umsatzinformationen und weitere RegExes berücksichtigen und ermöglichen so komplexe Bewertungen einfach zu erstellen.
3226

33-
Eine Klassifizierung *(Tagging)* wird dabei nach Haupt- und Unterkategorie vorgenommen. Sie erfolgt bei einem Durchlauf optional für alle unkategorisierten Umsätze, auf alle oder auf einen Teil anhand einer festgelegten Priorität (der Kategorie).
27+
Ein Tagging findet anschließend auf angereicherten Informationen regelbasiert statt und kann außerdem auch manuell erfolgen.
28+
29+
Auf dieser Grundlage werden Umsätze Kategorisiert wobei auch das händisch editiert werden kann.
3430

3531
### Darstellung
3632

37-
- Umsatzübersicht
38-
- Statistiken
39-
- Verteilungen
33+
- Kontohistorie
34+
- Transaktionsansicht
35+
- Statistiken/Verteilungen/Verläufe
4036

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

39+
## Misc
40+
41+
### Unterstützte Banken
42+
43+
| Bank | CSV | PDF |
44+
|------------------------------|-------------------------------------|
45+
| Comdirect | 🟢 Umsatzübersicht | 🟢 Finanzreport |
46+
| Commerzbank | 🟢 Umsatzübersicht | 🟢 Kontoauszug |
47+
| Sparkasse Hannover |*planned* |*planned* |
48+
| Volksbank Mittelhessen eG | 🟢 Umsatzübersicht |*planned* |
49+
50+
### Workflow (CSV / PDF Imports)
51+
52+
Umsätze können sich beim Import überschneiden oder mehrfach hochgeladen werden: Transaktionen werden in der Regel nicht doppelt importiert.
53+
54+
Die Umsatzinformationen eines Kontoauszugs als PDF und der Export der Ansicht im Online Banking als CSV hat schon bei der Erstellung einen unterschiedlichen Informationsgehalt. Hinzu kommt, dass das Einlesen einer PDF nicht so verlässlich bei Zeilenumbrüchen und Leerzeichen funktioniert, weshalb Worte getrennt oder zusammengeschoben werden können. Ein und die selbe Transaktion kann daher unterschiedlich beschrieben worden sein, was einen doppelten Import (einer je Format) leider möglich macht.
55+
56+
Daher sollte man beachten:
57+
58+
- Regeln nicht auf zwingend vorhandene Leerzeichen auszulegen
59+
- 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)
60+
61+
### Tagging- und Kategorisierungsregeln
62+
63+
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.
64+
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.
66+
67+
4368
## Contribution
4469

4570
You're Welcome !
4671

47-
Erstelle einen Reader für verschiedene Formate deiner Bank.
48-
49-
Dieses Repo ist test-driven. Vor dem Merge ist ein Unit- und ggf. Integrationtest erforderlich, der aber auch vom Kernprojekt erstellt werden kann.
72+
Erstelle einen Reader für verschiedene Formate deiner Bank oder ergänze die `parser` und `rules`.
5073

5174
## Setup
5275

@@ -65,4 +88,21 @@ pip install -r requirements.txt
6588
```
6689
pip install -r tests/requirements.txt
6790
pytest
68-
```
91+
```
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

app/routes.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ def uploadIban(iban):
297297
298298
Args (multipart/form-data):
299299
file-input (binary): Dateiupload aus Formular-Submit
300+
bank (str, optional): Bankkennung (Default: Generic)
300301
Returns:
301302
json: Informationen zur Datei und Ergebnis der Untersuchung.
302303
"""
@@ -309,16 +310,22 @@ def uploadIban(iban):
309310
content_type, size = parent.mv_fileupload(input_file, path)
310311

311312
# Daten einlesen und in Object speichern (Bank und Format default bzw. wird geraten)
312-
content_formats = {
313+
content_format = {
313314
'application/json': 'json',
314315
'text/csv': 'csv',
315316
'application/pdf': 'pdf',
316317
'text/plain': 'text',
317-
}
318+
}.get(content_type)
319+
320+
# Special handling for PDFs (extension needed)
321+
if content_format == 'pdf':
322+
os.rename(path, f'{path}.pdf')
323+
path = f'{path}.pdf'
318324

319325
# Read Input and Parse the contents
320326
parsed_data = parent.read_input(
321-
path, data_format=content_formats.get(content_type)
327+
path, bank=request.form.get('bank', 'Generic'),
328+
data_format=content_format
322329
)
323330

324331
# Verarbeitete Kontiumsätze in die DB speichern
@@ -454,8 +461,8 @@ def tag_and_cat(iban) -> dict:
454461
category=custom_rule.get('category'),
455462
tags=custom_rule.get('tags'),
456463
filters=custom_rule.get('filters'),
457-
parsed_keys=custom_rule.get('parsed_keys'),
458-
parsed_vals=custom_rule.get('parsed_vals'),
464+
parsed_keys=list(custom_rule.get('parsed', {}).keys()),
465+
parsed_vals=list(custom_rule.get('parsed', {}).values()),
459466
multi=custom_rule.get('multi', 'AND'),
460467
prio=custom_rule.get('prio', 1),
461468
prio_set=custom_rule.get('prio_set'),

app/static/js/functions.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ function getFilteredList() {
4747
arg_concat = '&';
4848
}
4949

50+
const text_search = document.getElementById('filter-text').value;
51+
if (text_search) {
52+
query_args = query_args + arg_concat + 'text=' + text_search;
53+
arg_concat = '&';
54+
}
55+
5056
const category = document.getElementById('filter-cat').value;
5157
if (category) {
5258
query_args = query_args + arg_concat + 'category=' + category;

app/static/js/index.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function saveSetting() {
125125
* Sends a file to the server for upload.
126126
* The file is selected via the file input element 'settings-input'.
127127
*/
128-
function uploadFile() {
128+
function importSettings() {
129129
const settings_type = document.getElementById('settings-type').value;
130130

131131
const fileInput = document.getElementById('settings-input');
@@ -158,21 +158,23 @@ function uploadFile() {
158158
return;
159159
}
160160

161+
const bank_id = document.getElementById('bank-type').value
162+
161163
const fileInput = document.getElementById('file-input');
162164
if (fileInput.files.length === 0) {
163165
alert('Please select a file to upload.');
164166
return;
165167
}
166168

167-
const params = { file: 'file-input' }; // The value of 'file' corresponds to the input element's ID
169+
const params = { file: 'file-input', 'bank': bank_id}; // The value of 'file' corresponds to the input element's ID
168170
apiSubmit('upload/' + iban, params, function (responseText, error) {
169171
if (error) {
170172
alert('File upload failed: ' + '(' + error + ')' + responseText);
171173

172174
} else {
173-
alert('File uploaded successfully!' + responseText);
174-
window.location.href = '/' + iban;
175-
175+
if (confirm('File uploaded successfully!' + responseText + '\nKonto aufrufen?')) {
176+
window.location.href = '/' + iban;
177+
}
176178
}
177179
}, true);
178180
}

app/templates/iban.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ <h1>Kontoübersicht</h1>
2323
<li>
2424
<input type="text" id="filter-range-start" placeholder="Start Date: d.m.Y" value="{{filters.startDate|ctime if filters.startDate}}">
2525
<input type="text" id="filter-range-end" placeholder="End Date: d.m.Y" value="{{filters.endDate|ctime if filters.endDate}}">
26+
<input type="text" id="filter-text" placeholder="RegEx Textfilter" value="{{filters.text}}">
2627
<input type="text" id="filter-cat" list="cat-list" placeholder="Kategorie" value="{{filters.category}}">
2728
<input type="text" id="filter-tag" list="tag-list" placeholder="Tag (, getrennt)" value="{{filters.tags}}">
2829
<select id="filter-tag-mode">

app/templates/index.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,17 @@
4646
<h2>Neue Transactionen</h2>
4747
<p>Importiere Kontoumsätze in folgendem Konto:</p>
4848
<p>
49-
<input type="text" name="iban-input" id="iban-input" placeholder="DE XXXXXXXXXXXXX" list="iban-list">
49+
<input type="text" name="iban-input" id="iban-input" placeholder="DE XXXXXXXXXXXXX" list="iban-list"
50+
oninput="this.value = this.value.replace(/\s+/g, '')">
5051
<datalist id="iban-list">
5152
{% for iban in ibans %}
5253
<option value="{{iban}}"></option>
5354
{% endfor %}
5455
</datalist>
56+
<select id="bank-type">
57+
<option>Comdirect</option>
58+
<option>Commerzbank</option>
59+
</select>
5560
</p>
5661
<p>
5762
<div id="file-drop-area">

app/templates/tx.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ <h1>Transaktionsdetails</h1>
3232
</tr>
3333
<tr>
3434
<td>Datum (Wertstellnug)</td>
35-
<td>{{tx.date_wert}}</td>
35+
<td>{{tx.valuta}}</td>
3636
</tr>
3737
<tr>
3838
<td>Datum (Transaktion)</td>

app/ui.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from handler.Tags import Tagger
2020

2121
from reader.Generic import Reader as Generic
22+
from reader.Comdirect import Reader as Comdirect
2223
from reader.Commerzbank import Reader as Commerzbank
2324

2425

@@ -48,6 +49,7 @@ def __init__(self):
4849
self.readers = {
4950
'Generic': Generic,
5051
'Commerzbank': Commerzbank,
52+
'Comdirect': Comdirect,
5153
}
5254

5355
# Tagger
@@ -156,6 +158,17 @@ def filter_to_condition(self, get_args: dict) -> list:
156158
except (ValueError, TypeError) as e:
157159
logging.warning(f"Invalid betrag format '{e}' will be ignored")
158160

161+
# Filter for Text Search
162+
text_search = get_args.get('text')
163+
if text_search is not None:
164+
condition.append({
165+
'key': 'text_tx',
166+
'value': text_search,
167+
'compare': 'regex'
168+
})
169+
170+
frontend_filters['text'] = text_search
171+
159172
return condition, frontend_filters
160173

161174
def check_requested_iban(self, iban):

handler/BaseDb.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ def select(self, collection:str, condition: dict|list[dict]=None, multi: str='AN
9191
if isinstance(r.get('date_tx'), int):
9292
r['date_tx'] = datetime.fromtimestamp(r['date_tx']).strftime('%d.%m.%Y')
9393

94-
if isinstance(r.get('date_wert'), int):
95-
r['date_wert'] = datetime.fromtimestamp(r['date_wert']).strftime('%d.%m.%Y')
94+
if isinstance(r.get('valuta'), int):
95+
r['valuta'] = datetime.fromtimestamp(r['valuta']).strftime('%d.%m.%Y')
9696

9797
return result_list
9898

0 commit comments

Comments
 (0)