Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c260816
More Supermarkets and Baecker
Jan 30, 2026
200faad
Load more now works with select all
Jan 30, 2026
57e357e
Fix Rule + Add Tagging Integ Tests
Pitastic Feb 2, 2026
fa5906b
Take care when to merge lists in TinyDB update
Feb 2, 2026
4c18b8a
Add MongoDB Tests to GitHub Action
Feb 2, 2026
dc7f21b
remove newlines
Feb 2, 2026
b181e51
Replace MongoDB health options with healthcheck
Pitastic Feb 3, 2026
01c0b15
Refactor MongoDB healthcheck options in workflow
Pitastic Feb 3, 2026
4d7d96d
Fix health command syntax in MongoDB service
Pitastic Feb 3, 2026
6654c2e
modify sed expressions
Pitastic Feb 3, 2026
723c6b8
Implement default testing for custom data (Step 1)
Feb 3, 2026
c684891
Generic Test for external data implemented
Pitastic Feb 3, 2026
e133e49
Weitere Regeln teilw. vorbereitet
Pitastic Feb 4, 2026
dbf7d48
Fix rule with multiple filters
Pitastic Feb 4, 2026
fb2089d
Add Src Konto to Details
Pitastic Feb 4, 2026
4418d9c
Add Kredit-rule, fix custom rule in ui
Pitastic Feb 4, 2026
27d48b1
Fix Tagging in Groups (TinyDB)
Pitastic Feb 4, 2026
b043433
Solve Groups for delete and update in BaseDB
Pitastic Feb 4, 2026
f20238b
Fix Tests in MongoDB
Pitastic Feb 5, 2026
126f2a1
Add Empty Taglist Filter
Pitastic Feb 6, 2026
6421071
UI polishing, more Rules and Parsers (Parser Regexes noch fixen)
Pitastic Feb 7, 2026
3403b79
Parser verbsessert und EREF hinzugefügt
Pitastic Feb 8, 2026
47c9167
Rules: Strom, Wasser, Gas, Telefon
Pitastic Feb 10, 2026
91de43b
letzte allgemeine Tags
Pitastic Feb 10, 2026
d5cb6e5
Letzte Rule
Pitastic Feb 11, 2026
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
69 changes: 60 additions & 9 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,73 @@
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
workflow_dispatch:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

permissions:
contents: read

jobs:
PyTest_TinyDB:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.12
uses: actions/setup-python@v3
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
pip install -r requirements.txt
pip install -r tests/requirements.txt
- name: Test with pytest
run: |
set +e

PyTest:
test_output="$(PYTHONPATH=. pytest -rN)"
exitcode=$?

test_failed=$(sed -n '$s/^=*\s*\([0-9]*\)\sfailed.*/\1/p' <<< "$test_output" |tail -n1)
if [[ -z $test_failed ]]; then test_failed=0 ; fi

test_passed=$(sed -n -E 's|^=*\s([0-9]+\sfailed,\s)?(([0-9]*)\spassed,\s)?.*|\3|p' <<< "$test_output" | tail -n1)
if [[ -z $test_passed ]]; then test_passed=0 ; fi

runs-on: ubuntu-latest
test_skipped=$(tail -n1 <<< "$test_output" | rev |sed -E 's|^(.*deppiks\s([0-9]*))?.*|\2|')
if [[ -z $test_skipped ]]; then echo test_skipped=0 ; fi

all_tests=$(expr ${test_passed} + ${test_failed})

echo "### Results (with TinyDB)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| :checkered_flag: | :arrow_right_hook: | :x: |" >> $GITHUB_STEP_SUMMARY
echo "| ------------- | ------------- | ------------- |" >> $GITHUB_STEP_SUMMARY
echo "| $test_passed | $test_skipped | $test_failed |" >> $GITHUB_STEP_SUMMARY

echo "$test_output"
if [[ $exitcode != 0 ]]; then
exit $exitcode
fi
exit 0
PyTest_MongoDB:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:8.2.4
env:
MONGO_INITDB_ROOT_USERNAME: testuser
MONGO_INITDB_ROOT_PASSWORD: testpassword
ports:
- 27017:27017
options: >-
--health-cmd "echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet"
--health-interval 20s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.12
Expand All @@ -35,6 +85,10 @@ jobs:
run: |
set +e

sed -i -E s"|(DATABASE_BACKEND = )('tiny').*$|\1'mongo'|m" tests/config.py
sed -i -E s"|(DATABASE_URI = )('/tmp.*').*$|\1'mongodb://testuser:testpassword@localhost:27017'|" tests/config.py
sed -i -E s"|(DATABASE_NAME = )('testdata.json').*$|\1'testdata'|m" tests/config.py

test_output="$(PYTHONPATH=. pytest -rN)"
exitcode=$?

Expand All @@ -49,7 +103,7 @@ jobs:

all_tests=$(expr ${test_passed} + ${test_failed})

echo "### Results" >> $GITHUB_STEP_SUMMARY
echo "### Results (with MongoDB)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| :checkered_flag: | :arrow_right_hook: | :x: |" >> $GITHUB_STEP_SUMMARY
echo "| ------------- | ------------- | ------------- |" >> $GITHUB_STEP_SUMMARY
Expand All @@ -60,11 +114,8 @@ jobs:
exit $exitcode
fi
exit 0

PyLint:

runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,4 @@ cython_debug/
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python

# Credentials:
config.conf
settings/*/0[^0]*.json
3 changes: 2 additions & 1 deletion Models.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Frei wählbarer Name der Regel.

#### .regex, r-str

Regex String, der auf den Buchungstext angewendet werden soll. Er muss genau eine Matching-Group enthalten. Der Wert dieses Treffers (der Gruppe) wird als Wert mit dem Namen der Regel in der Transaktion als Ergebnis gespeichert.
Regex String, der auf den Buchungstext angewendet werden soll. Es wird immer die erste Matching-Group übernommen. Der Wert dieses Treffers (der Gruppe) wird als Wert mit dem Namen der Regel in der Transaktion als Ergebnis gespeichert.

## Rule Objects

Expand Down Expand Up @@ -157,6 +157,7 @@ Dabei haben die Operatoren folgende Bedeutung:
- `in`: Mindestens ein Wert der Vergleichsliste muss in dem Listenwert aus der Datenbank vorkommen.
- `all`: Alle Werte der Vergleichsliste müssen in dem Listenwert aus der Datenbank vorkommen.
- `notin`: Kein Wert der Vergleichsliste darf in dem Listenwert aus der Datenbank vorkommen.
- `exact` : Alle Werte der Vergleichsliste und keine anderen müssen in dem Listenwert aus der Datenbank vorkommen (unabhängig von der Reihenfolge).
- `regex`: Regex String, der auf den Buchungstext angewendet werden soll. Ein Teil-Treffer des RegExes wird als Treffer gewertet.

#### .tags, list (nur bei metatype: `rule`)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ Daher sollte man beachten:

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.

### Wahl der Datenbankengine

TinyDB sollte nur bei kleinen Instanzen mit einzelnen Benutzern gewählt werden. Ein paralleler Zugriff ist mit PynanceParser zwar möglich, allerdings sinkt die Performance und die Fehleranfälligkeit steigt mit der Anzahl der Requests und der Anzahl der Einträge in der Datenbank. Insbesondere bei I/O-schwacher Hardware (z.B. Raspberry mit SD Karte) kann es schnell zum Crash des Servers kommen.

Für die produktive Nutzung wird MongoDB daher empfohlen!

## Anpassungen / Contribution

**You're Welcome !** :tada:
Expand Down
8 changes: 6 additions & 2 deletions app/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def welcome() -> str:
ibans = parent.db_handler.list_ibans()
groups = parent.db_handler.list_groups()
meta = parent.db_handler.filter_metadata(condition=None)
meta.sort(key=lambda m: (m.get('metatype'), m.get('name')))
return render_template('index.html', ibans=ibans, groups=groups, meta=meta)

@current_app.route('/<iban>', methods=['GET'])
Expand Down Expand Up @@ -159,6 +160,8 @@ def iban(iban) -> str:
if t not in tags:
tags.append(t)

tags.sort()

# All distinct Categories
# (must be filtered on our own because TinyDB doesn't support 'distinct' queries)
cats = []
Expand All @@ -168,6 +171,8 @@ def iban(iban) -> str:
if c and c not in cats:
cats.append(c)

cats.sort()

return render_template('iban.html', transactions=rows[:entries_per_page],
IBAN=iban, tags=tags, categories=cats,
rules=rulenames, filters=frontend_filters)
Expand Down Expand Up @@ -446,7 +451,6 @@ def uploadRules(metadata):
Returns:
json: Informationen zur Datei und Ergebnis der Untersuchung.
"""
print(request.files)
input_file = request.files.get('settings-input')
if not input_file:
return {'error': 'Es wurde keine Datei übermittelt.'}, 400
Expand Down Expand Up @@ -553,7 +557,7 @@ def tag_and_cat(iban) -> dict:
iban,
category=custom_rule.get('category'),
tags=custom_rule.get('tags'),
filters=custom_rule.get('filters'),
filters=custom_rule.get('filter'),
parsed_keys=list(custom_rule.get('parsed', {}).keys()),
parsed_vals=list(custom_rule.get('parsed', {}).values()),
multi=custom_rule.get('multi', 'AND'),
Expand Down
4 changes: 2 additions & 2 deletions app/static/js/functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ function getFilteredList() {
}

const tags = document.getElementById('filter-tag-result').value;
if (tags) {
const tag_mode = document.getElementById('filter-tag-mode').value;
if (tags || tag_mode == 'exact') {
query_args = query_args + arg_concat + 'tags=' + tags;
arg_concat = '&';
const tag_mode = document.getElementById('filter-tag-mode').value;
if (tag_mode) {
query_args = query_args + arg_concat + 'tag_mode=' + tag_mode;
arg_concat = '&';
Expand Down
60 changes: 38 additions & 22 deletions app/static/js/iban.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,15 @@
"use strict";

const selectAllCheckbox = document.getElementById('select-all');
let ROW_CHECKBOXES = null;
let PAGE = 1;

document.addEventListener('DOMContentLoaded', function () {

// enabling/disabling the edit button based on checkbox selection
const selectAllCheckbox = document.getElementById('select-all');
ROW_CHECKBOXES = document.querySelectorAll('.row-checkbox');

selectAllCheckbox.addEventListener('change', function () {
ROW_CHECKBOXES.forEach(checkbox => {
checkbox.checked = selectAllCheckbox.checked;
});
updateEditButtonState();
listTxElements();
});

ROW_CHECKBOXES.forEach(checkbox => {
checkbox.checked = false;
checkbox.addEventListener('change', function () {
if (!this.checked) {
selectAllCheckbox.checked = false;
} else if (Array.from(ROW_CHECKBOXES).every(cb => cb.checked)) {
selectAllCheckbox.checked = true;
}
updateEditButtonState();
listTxElements();
});
});
selectAllCheckbox.addEventListener('change', set_all_checkboxes);
ROW_CHECKBOXES.forEach(checkbox => set_row_checkboxes(checkbox));

// Tag Chip Bullets
const inputTagContainers = [
Expand Down Expand Up @@ -68,6 +49,35 @@ document.addEventListener('DOMContentLoaded', function () {
// -- DOM Functions -----------------------------------------------------------
// ----------------------------------------------------------------------------

/**
* Set all checkboxes to the state of the headerbox
*/
function set_all_checkboxes() {
ROW_CHECKBOXES.forEach(checkbox => {
checkbox.checked = this.checked;
});
updateEditButtonState();
listTxElements();
}

/**
* Set an eventlistener for every box and change the header when unselected
*
* @param {DOMElement} checkbox
*/
function set_row_checkboxes(checkbox){
checkbox.checked = false;
checkbox.addEventListener('change', function () {
if (!this.checked) {
selectAllCheckbox.checked = false;
} else if (Array.from(ROW_CHECKBOXES).every(cb => cb.checked)) {
selectAllCheckbox.checked = true;
}
updateEditButtonState();
listTxElements();
});
}

/**
* Clears information from a result Box
*
Expand Down Expand Up @@ -384,6 +394,12 @@ function loadMore() {

// Append new Rows
document.querySelector('.transactions tbody').innerHTML += responseText;

// enabling/disabling the edit button based on checkbox selection
const selectAllCheckbox = document.getElementById('select-all');
ROW_CHECKBOXES = document.querySelectorAll('.row-checkbox');
selectAllCheckbox.addEventListener('change', set_all_checkboxes);
ROW_CHECKBOXES.forEach(checkbox => set_row_checkboxes(checkbox));
});

// Call URI
Expand Down
4 changes: 4 additions & 0 deletions app/templates/iban.html
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ <h2>Transaktions Details</h2>
<th>Art:</th>
<td class="art"></td>
</tr>
<tr>
<th>Konto:</th>
<td class="iban"></td>
</tr>
<tr>
<th>Gegenkonto:</th>
<td class="peer"></td>
Expand Down
3 changes: 2 additions & 1 deletion app/templates/macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
{{ transaction.date_tx | ctime }}
<small class="secondary">({{ transaction.valuta | ctime }})</small>
</td>
<td>{{ transaction.text_tx[:90] }}{% if transaction.text_tx|length > 60 %} ...{%endif%}</td>
<td>{{ transaction.text_tx[:90] }}{% if transaction.text_tx|length > 90 %} ...{%endif%}</td>
<td>
{% if transaction.category %}
<a class="tag-chip category {{generate_class(transaction.category)}}" href="?category={{transaction.category}}" data-tooltip="Add Filter">
Expand Down Expand Up @@ -123,6 +123,7 @@ <h2>Transaktionen filtern</h2>
<option value="in" {{"selected" if filters.tag_mode and filters.tag_mode == 'in'}}>eines davon</option>
<option value="all" {{"selected" if filters.tag_mode and filters.tag_mode == 'all'}}>alle davon</option>
<option value="notin" {{"selected" if filters.tag_mode and filters.tag_mode == 'notin'}}>keine davon</option>
<option value="exact" {{"selected" if filters.tag_mode and filters.tag_mode == 'exact'}}>genau diese</option>
</select>
</div>
</label>
Expand Down
18 changes: 13 additions & 5 deletions app/templates/tx.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ <h3>
</tr>
</thead>
<tbody>
<tr>
<td>Art:</td>
<td>{{tx.art}}</td>
</tr>
<tr>
<td>Datum (Wertstellnug)</td>
<td>{{ tx.valuta | ctime }}</td>
Expand All @@ -47,9 +43,21 @@ <h3>
<td>Datum (Transaktion)</td>
<td>{{ tx.date_tx | ctime }}</td>
</tr>
<tr>
<td>Art:</td>
<td>{{tx.art}}</td>
</tr>
<tr>
<td>Konto:</td>
<td>{{tx.iban}}</td>
</tr>
<tr>
<td>Gegenkonto:</td>
<td>{{tx.gegenkonto}}</td>
</tr>
<tr>
<td>Betrag</td>
<td>{{tx.amount}} {{tx.currency}}</td>
<td>{{ "%.2f"|format(tx.amount|round(2)) }} {{tx.currency}}</td>
</tr>
<tr>
<td>Buchungstext</td>
Expand Down
4 changes: 2 additions & 2 deletions app/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ def filter_to_condition(self, get_args: dict) -> list:
# Filter for Tags
tag_filter = get_args.get('tags')
if tag_filter is not None:
tag_filter = [t.strip() for t in tag_filter.split(',')]
tag_filter = [t.strip() for t in tag_filter.split(',') if t]
condition.append({
'key': 'tags',
'value': tag_filter,
Expand Down Expand Up @@ -280,7 +280,7 @@ def remove_tags(self, iban, t_id):
'compare': '=='
}]

updated_entries = self.db_handler.update(new_data, iban, condition)
updated_entries = self.db_handler.update(new_data, iban, condition, merge=False)
return updated_entries

def remove_cat(self, iban, t_id):
Expand Down
Loading