diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4dc0bd0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM ubuntu:24.04 + +RUN apt update && apt install python3.12-venv apache2 apache2-utils libapache2-mod-wsgi-py3 python3-pip -y + +COPY . /app +WORKDIR /app + +RUN /usr/bin/python3.12 -m venv .venv +RUN . .venv/bin/activate && \ + .venv/bin/python3.12 -m pip install --upgrade pip && \ + .venv/bin/python3.12 -m pip install -r requirements.txt + +RUN sed -i -E s"|(DATABASE_BACKEND = )('tiny').*$|\1'mongo'|" app/config.py +RUN sed -i -E s"|(DATABASE_URI = )('/tmp').*$|\1'mongodb://mongo:27017'|" app/config.py +RUN sed -i -E s"|(DATABASE_NAME = )('testdata.json').*$|\1'pynanceparser'|" app/config.py + +RUN cp docker/apache2.conf /etc/apache2/sites-available/pynanceparser.conf +RUN a2ensite pynanceparser && a2dissite 000* +RUN ln -sf /dev/stdout /var/log/apache2/access.log && \ + ln -sf /dev/stdout /var/log/apache2/error.log + +EXPOSE 80 + +ENTRYPOINT ["/app/docker/entrypoint.sh"] diff --git a/Models.md b/Models.md index d789f75..e0947c1 100644 --- a/Models.md +++ b/Models.md @@ -38,7 +38,7 @@ 'uuid': str, # (generated) 'date_tx': int, # (UTC) 'text_tx': str, - 'betrag': float, + 'amount': float, 'peer': str, ----------- optional ----------- @@ -75,7 +75,7 @@ Klassifiziert das Objekt als Regel für das Parsing. Frei wählbarer Name der Regel. -#### .regex, r-str (optional) +#### .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. diff --git a/README.md b/README.md index 6919a00..f1690ce 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,24 @@ Analyse und Darstellung von Kontoumsätzen bei mehreren Banken. ## Get Started -### Setup +Am einfachsten mit Docker: + +``` +git clone https://github.com/Pitastic/PynanceParser.git +cd PynanceParser +docker compose build +docker compose up -d +``` + +Bei Updates kann das Docker Image neu gebaut und gestartet werden: + +``` +git pull +docker compose build +docker compose down && docker compose up -d +``` + +### Standalone non-Docker Setup ``` python3.12 -m venv .venv @@ -111,7 +128,7 @@ Für diesen Zweck gibt es die Möglichkeit im Frontend Regeln auszuprobieren, oh - 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`) + - (am einfachsten ist eine JSON Datei wie `tests/input_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 diff --git a/app/config.py b/app/config.py index e4aa375..54a9ae3 100644 --- a/app/config.py +++ b/app/config.py @@ -1,17 +1,17 @@ #!/usr/bin/python3 """App Settings zum Zeitpunkt der Initalisierung von PynanceParser""" - +# Logging (will also log in webserver logs if used via wsgi) LOG_ACCESS_FILE = '/tmp/pynance_access.log' LOG_ERROR_FILE = '/tmp/pynance_error.log' # Options: -DATABASE_BACKEND = 'tiny' -#DATABASE_BACKEND = 'mongo' +DATABASE_BACKEND = 'tiny' # or 'mongo' -#DATABASE_URI = 'mongodb://testuser:testpassword@localhost:27017' # For mongo (URI) -DATABASE_URI = '/tmp' # For tiny (/path/to/) +# For tiny: Path to the Folder (/path/to) +# For mongo: MongoDB URI +DATABASE_URI = '/tmp' # or 'mongodb://testuser:testpassword@localhost:27017' # For tiny: Filename ('testdata.json') # For mongo: Collection name ('testdata') -DATABASE_NAME = 'testdata.json' +DATABASE_NAME = 'testdata.json' # or 'testdata' diff --git a/app/routes.py b/app/routes.py index d28bc47..cf99dfa 100644 --- a/app/routes.py +++ b/app/routes.py @@ -57,15 +57,16 @@ def iban(iban) -> str: Args (uri): 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 + 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 tag, str (query): Tag-Filter, einzelner Eintrag oder kommagetrennte Liste tag_mode, str (query): Vergleichsmodus für Tag-Filter (siehe Models.md) - betrag_min, float (query): Betragsfilter (größer gleich betrag_min) - betrag_max, float (query): Betragsfilter (kleiner gleich betrag_max) + amount_min, float (query): Betragsfilter (größer gleich amount_min) + amount_max, float (query): Betragsfilter (kleiner gleich amount_max) page, int (query): Seite für die Paginierung (default: 1) + descending, bool (query): Sortierreihenfolge nach Datum (default: True) Returns: html: Startseite mit Navigation """ @@ -77,7 +78,8 @@ def iban(iban) -> str: # Table with Transactions current_app.logger.debug(f"Using condition filter: {condition}") - rows = parent.db_handler.select(iban, condition) + sort_order = request.args.get('descending', 'true').lower() == 'true' + rows = parent.db_handler.select(iban, condition, descending=sort_order) # If pagination is requested, do not serve the whole page and all metadata entries_per_page = 50 @@ -168,8 +170,8 @@ def show_stats(iban) -> str: category, str (query): Kategorie-Filter tag, str (query): Tag-Filter, einzelner Eintrag oder kommagetrennte Liste tag_mode, str (query): Vergleichsmodus für Tag-Filter (siehe Models.md) - betrag_min, float (query): Betragsfilter (größer gleich betrag_min) - betrag_max, float (query): Betragsfilter (kleiner gleich betrag_max) + amount_min, float (query): Betragsfilter (größer gleich amount_min) + amount_max, float (query): Betragsfilter (kleiner gleich amount_max) Returns: html: Seite mit Grafiken und Statistiken über die slektierten Einträge (IBAN und optional Query) @@ -186,12 +188,12 @@ def show_stats(iban) -> str: # Calculate TOP categories and tags sums = {'categories': {}, 'tags': {}} for row in rows: - betrag = row.get('betrag', 0.0) + amount = row.get('amount', 0.0) cat = row.get('category', 'unkategorisiert') if cat not in sums['categories']: sums['categories'][cat] = 0.0 - sums['categories'][cat] += betrag + sums['categories'][cat] += amount tags = row.get('tags', []) if not tags: @@ -200,7 +202,7 @@ def show_stats(iban) -> str: if tag not in sums['tags']: sums['tags'][tag] = 0.0 - sums['tags'][tag] += betrag + sums['tags'][tag] += amount # Sort Sums sums['categories'] = dict(sorted(sums['categories'].items(), @@ -419,7 +421,7 @@ def deleteDatabase(iban): """ Leert die Datenbank zu einer IBAN Args (uri): - iban, str: (optional) IBAN zu der die Datenbank geleert werden soll. + iban, str: IBAN zu der die Datenbank geleert werden soll. (Default: Primäre IBAN aus der Config) Returns: json: Informationen zum Ergebnis des Löschauftrags. diff --git a/app/server.py b/app/server.py old mode 100644 new mode 100755 index ca8091b..3483387 --- a/app/server.py +++ b/app/server.py @@ -18,7 +18,7 @@ def create_app(config_path: str) -> Flask: Returns: FlaskApp """ # Logging - loglevel = 'DEBUG' + loglevel = 'INFO' dictConfig({ 'version': 1, 'formatters': {'default': { @@ -53,10 +53,12 @@ def create_app(config_path: str) -> Flask: return app -if __name__ == '__main__': - config = os.path.join( - os.path.dirname(os.path.abspath(__file__)), - 'config.py' - ) - application = create_app(config) - application.run(host='0.0.0.0', port=8110, debug=True) +config = os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'config.py' +) +application = create_app(config) + +if __name__ == "__main__": + # Run the application directly if executed as a standalone script + application.run(host='0.0.0.0', port=8000, debug=True) diff --git a/app/static/css/grid.css b/app/static/css/grid.css index 4a003bf..5c6b3a8 100644 --- a/app/static/css/grid.css +++ b/app/static/css/grid.css @@ -83,7 +83,7 @@ grid-template-rows: 1fr 1fr; gap: 0.25em; grid-template-areas: - "checkbox dates category betrag" + "checkbox dates category amount" "button betreff betreff betreff"; padding: 1em 0; } @@ -107,7 +107,7 @@ .transactions tr td:nth-child(4){grid-area: category;} .transactions tr td:nth-child(6){ padding-right: 0; - grid-area: betrag; + grid-area: amount; } .transactions tr td:nth-child(7){ padding-left: 0; diff --git a/app/static/css/style.css b/app/static/css/style.css index 38d2cd6..f429d14 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -97,7 +97,7 @@ button.info { } /* Tables */ -.transactions .betrag { +.transactions .amount { white-space: nowrap; text-align: right; } @@ -108,7 +108,7 @@ button.info { /* S : Slim button in transaction table */ @media (max-width: 767px) { - .transactions th.betrag { + .transactions th.amount { text-align: center; } .transactions tr { diff --git a/app/static/js/functions.js b/app/static/js/functions.js index 4414289..72265ea 100644 --- a/app/static/js/functions.js +++ b/app/static/js/functions.js @@ -74,17 +74,23 @@ function getFilteredList() { } } - let betrag_min = document.getElementById('filter-betrag-min').value; - if (betrag_min) { - betrag_min = betrag_min.replace(',', '.'); - query_args = query_args + arg_concat + 'betrag_min=' + betrag_min; + let amount_min = document.getElementById('filter-amount-min').value; + if (amount_min) { + amount_min = amount_min.replace(',', '.'); + query_args = query_args + arg_concat + 'amount_min=' + amount_min; arg_concat = '&'; } - let betrag_max = document.getElementById('filter-betrag-max').value; - if (betrag_max) { - betrag_max = betrag_max.replace(',', '.'); - query_args = query_args + arg_concat + 'betrag_max=' + betrag_max; + let amount_max = document.getElementById('filter-amount-max').value; + if (amount_max) { + amount_max = amount_max.replace(',', '.'); + query_args = query_args + arg_concat + 'amount_max=' + amount_max; + arg_concat = '&'; + } + + let sort_order = document.getElementById('filter-descending').value; + if (sort_order) { + query_args = query_args + arg_concat + 'descending=' + sort_order; arg_concat = '&'; } diff --git a/app/static/js/iban.js b/app/static/js/iban.js index 9e0a8e8..89c0f21 100644 --- a/app/static/js/iban.js +++ b/app/static/js/iban.js @@ -115,7 +115,7 @@ function fillTxDetails(result) { td.appendChild(a_link); } - } else if (key == 'betrag') { + } else if (key == 'amount') { // Round td.innerHTML = r[key].toFixed(2); diff --git a/app/templates/iban.html b/app/templates/iban.html index 7599b66..6dc3c89 100644 --- a/app/templates/iban.html +++ b/app/templates/iban.html @@ -42,7 +42,7 @@

Buchungstext Kategorie Tags - Betrag + Betrag   @@ -208,7 +208,7 @@

Transaktions Details

Betrag - + Währung: diff --git a/app/templates/macros.html b/app/templates/macros.html index 8cf1b77..a8c7db3 100644 --- a/app/templates/macros.html +++ b/app/templates/macros.html @@ -36,8 +36,8 @@ {% endfor %} - - {{ "%.2f"|format(transaction.betrag|round(2)) }} + + {{ "%.2f"|format(transaction.amount|round(2)) }} {{ transaction.currency }} @@ -55,11 +55,11 @@
-

Transaktions Details

+

Transaktionen filtern