diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 319c3df..4fec879 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -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 @@ -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=$? @@ -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 @@ -60,11 +114,8 @@ jobs: exit $exitcode fi exit 0 - PyLint: - runs-on: ubuntu-latest - steps: - name: Checkout repository uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 656328a..6128e7a 100644 --- a/.gitignore +++ b/.gitignore @@ -160,4 +160,4 @@ cython_debug/ # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python # Credentials: -config.conf \ No newline at end of file +settings/*/0[^0]*.json diff --git a/Models.md b/Models.md index e0947c1..6400b5e 100644 --- a/Models.md +++ b/Models.md @@ -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 @@ -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`) diff --git a/README.md b/README.md index 710339d..cdc0aec 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/app/routes.py b/app/routes.py index e501754..c5c942e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -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('/', methods=['GET']) @@ -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 = [] @@ -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) @@ -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 @@ -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'), diff --git a/app/static/js/functions.js b/app/static/js/functions.js index a2ae3d3..25ccdd3 100644 --- a/app/static/js/functions.js +++ b/app/static/js/functions.js @@ -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 = '&'; diff --git a/app/static/js/iban.js b/app/static/js/iban.js index b8ac3db..db0c424 100644 --- a/app/static/js/iban.js +++ b/app/static/js/iban.js @@ -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 = [ @@ -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 * @@ -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 diff --git a/app/templates/iban.html b/app/templates/iban.html index 3ed06ea..5eebf4a 100644 --- a/app/templates/iban.html +++ b/app/templates/iban.html @@ -218,6 +218,10 @@

Transaktions Details

Art: + + Konto: + + Gegenkonto: diff --git a/app/templates/macros.html b/app/templates/macros.html index a8c7db3..f6e9fdb 100644 --- a/app/templates/macros.html +++ b/app/templates/macros.html @@ -19,7 +19,7 @@ {{ transaction.date_tx | ctime }} ({{ transaction.valuta | ctime }}) - {{ transaction.text_tx[:90] }}{% if transaction.text_tx|length > 60 %} ...{%endif%} + {{ transaction.text_tx[:90] }}{% if transaction.text_tx|length > 90 %} ...{%endif%} {% if transaction.category %} @@ -123,6 +123,7 @@

Transaktionen filtern

+ diff --git a/app/templates/tx.html b/app/templates/tx.html index 7cc37c2..8068123 100644 --- a/app/templates/tx.html +++ b/app/templates/tx.html @@ -35,10 +35,6 @@

- - Art: - {{tx.art}} - Datum (Wertstellnug) {{ tx.valuta | ctime }} @@ -47,9 +43,21 @@

Datum (Transaktion) {{ tx.date_tx | ctime }} + + Art: + {{tx.art}} + + + Konto: + {{tx.iban}} + + + Gegenkonto: + {{tx.gegenkonto}} + Betrag - {{tx.amount}} {{tx.currency}} + {{ "%.2f"|format(tx.amount|round(2)) }} {{tx.currency}} Buchungstext diff --git a/app/ui.py b/app/ui.py index 29c2ada..5520774 100644 --- a/app/ui.py +++ b/app/ui.py @@ -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, @@ -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): diff --git a/handler/BaseDb.py b/handler/BaseDb.py index 45c57bf..9f91f02 100644 --- a/handler/BaseDb.py +++ b/handler/BaseDb.py @@ -160,8 +160,8 @@ def _insert(self, data: dict|list[dict], collection: str): """ raise NotImplementedError() - def update(self, data: dict, collection: str, condition: dict|list[dict], - multi:str, merge:bool=True): + def update(self, data: dict, collection: str, condition: dict|list[dict]=None, + multi:str='AND', merge:bool=True): """ Aktualisiert Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -184,9 +184,29 @@ def update(self, data: dict, collection: str, condition: dict|list[dict], dict: - updated, int: Anzahl der aktualisierten Datensätze """ + if self.check_collection_is_iban(collection): + # Directly update IBAN collection + return self._update(data, collection, condition, multi, merge) + + # Update all IBANs in group + update_result = 0 + for iban in self.get_group_ibans(collection): + update_result += self._update(data, iban, condition, multi, merge).get('updated', 0) + + return {'updated': update_result} + + def _update(self, data: dict, collection: str, condition: dict|list[dict], + multi:str, merge:bool=True): + """ + Private Methode zum Aktualisieren von Datensätzen in der Datenbank, + Siehe 'update' Methode. + Returns: + dict: + - updated, int: Anzahl der aktualisierten Datensätze + """ raise NotImplementedError() - def delete(self, collection: str, condition: dict | list[dict]): + def delete(self, collection: str, condition: dict | list[dict]=None, multi: str='AND'): """ Löscht Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -201,6 +221,26 @@ def delete(self, collection: str, condition: dict | list[dict]): - 'regex' : value wird als RegEx behandelt multi (str) : ['AND' | 'OR'] Wenn 'condition' eine Liste mit conditions ist, werden diese logisch wie hier angegeben verknüpft. Default: 'AND' + Returns: + dict: + - deleted, int: Anzahl der gelöschten Datensätze + """ + if self.check_collection_is_iban(collection): + # Directly update IBAN collection + return self._delete(collection, condition, multi) + + # Update all IBANs in group + update_result = 0 + for iban in self.get_group_ibans(collection): + update_result += self._delete(iban, condition, multi).get('deleted', 0) + + return {'deleted': update_result} + + def _delete(self, collection: str, condition: dict | list[dict], multi: str): + """ + Private Methode zum Löschen von Datensätzen in der Datenbank, + die die angegebene Bedingung erfüllen. Siehe 'delete' Methode. + Returns: dict: - deleted, int: Anzahl der gelöschten Datensätze @@ -324,6 +364,7 @@ def list_ibans(self): """ all_collections = self._get_collections() ibans = [col for col in all_collections if self.check_collection_is_iban(col)] + ibans.sort() return ibans def list_groups(self): @@ -346,6 +387,7 @@ def list_groups(self): for group in meta_results: groups.append(group.get('groupname')) + groups.sort() return groups def _get_collections(self): diff --git a/handler/MongoDb.py b/handler/MongoDb.py index 5974d2c..0f8e079 100644 --- a/handler/MongoDb.py +++ b/handler/MongoDb.py @@ -112,7 +112,7 @@ def _insert(self, data: dict|list[dict], collection: str): except pymongo.errors.BulkWriteError: return {'inserted': 0} - def update(self, data, collection, condition=None, multi='AND', merge=True): + def _update(self, data, collection, condition=None, multi='AND', merge=True): """ Aktualisiert Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -135,33 +135,47 @@ def update(self, data, collection, condition=None, multi='AND', merge=True): - updated, int: Anzahl der aktualisierten Datensätze """ collection = self.connection[collection] + if collection is None: + logging.error(f"Collection {collection} not found for update!") + return {'updated': 0} # Form condition into a query query = self._form_complete_query(condition, multi) - # Handle Tag-Lists - new_tags = data.get('tags') - if new_tags and merge: - # care about the right format - if not isinstance(new_tags, list): - data['tags'] = [new_tags] + # care about the right format + if not isinstance(data.get('tags', []), list): + data['tags'] = [data['tags']] - # Clean $set data from tags - del data['tags'] + # Form condition into a query + if condition is None and merge: + logging.error('Using "merge" without a query is not possible') + return { 'error': 'Using "merge" without a query is not possible', 'updated': 0 } + + if merge: + # Care about lists and merge with special command + unmerged_data = {} + add_to_set = {} + for d in data.keys(): + + if isinstance(data[d], list): + add_to_set[d] = {'$each': data[d]} + continue - # Define Operation - update_op = { - '$set': data, - '$addToSet': {'tags': {'$each': new_tags}} - } + unmerged_data[d] = data[d] + + # Set all list keys to operation (or not if none) + update_op = {'$set': unmerged_data} + if add_to_set: + update_op['$addToSet'] = add_to_set else: + # No special handling update_op = {'$set': data} update_result = collection.update_many(query, update_op) return {'updated': update_result.modified_count} - def delete(self, collection, condition=None, multi='AND'): + def _delete(self, collection, condition=None, multi='AND'): """ Löscht Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -285,6 +299,8 @@ def _form_condition(self, condition): stmt = {'$nin': condition.get('value')} if condition_method == 'all': stmt = {'$all': condition.get('value')} + if condition_method == 'exact': + stmt = {'$all': condition.get('value'), '$size': len(condition.get('value'))} # Nested or Plain Key condition_key = condition.get('key') diff --git a/handler/Tags.py b/handler/Tags.py index 0a2276d..11a78b8 100644 --- a/handler/Tags.py +++ b/handler/Tags.py @@ -35,7 +35,7 @@ def parse(self, input_data): for name, regex in parses.items(): re_match = regex.search(d['text_tx']) if re_match: - d['parsed'][name] = re_match.group(1) + d['parsed'][name] = re_match.group(1).strip() return input_data diff --git a/handler/TinyDb.py b/handler/TinyDb.py index e92be84..965cb52 100644 --- a/handler/TinyDb.py +++ b/handler/TinyDb.py @@ -2,7 +2,6 @@ """Datenbankhandler für die Interaktion mit einer TinyDB Datenbankdatei.""" import os -import copy import operator import logging import re @@ -150,7 +149,7 @@ def _insert(self, data: dict|list[dict], collection: str): result = self.connection.table(collection).insert(data) return {'inserted': (1 if result else 0)} - def update(self, data, collection, condition=None, multi='AND', merge=True): + def _update(self, data, collection, condition=None, multi='AND', merge=True): """ Aktualisiert Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -172,46 +171,61 @@ def update(self, data, collection, condition=None, multi='AND', merge=True): dict: - updated, int: Anzahl der aktualisierten Datensätze """ - # Form condition into a query and run - if condition is None: + # run update + docs_to_update = self.select(collection, condition, multi) + if not docs_to_update: + # No match, no update + return { 'updated': 0 } + + collection = self.connection.table(collection) + + # care about the right format + if data.get('tags') is not None and not isinstance(data.get('tags'), list): + data['tags'] = [data.get('tags')] + + # Result store for updated uuids + update_result = [] + + # Form condition into a query + if condition is None and merge: + logging.error('Using "merge" without a query is not possible') + return { 'error': 'Using "merge" without a query is not possible', 'updated': 0 } + + elif condition is None: query = Query().noop() else: query = self._form_complete_query(condition, multi) - # run update - new_tags = data.get('tags') - if new_tags and merge: - - docs_to_update = self.select(collection, condition, multi) - if not docs_to_update: - # No match, no update - return { 'updated': 0 } - - # Special handling with list to update (select - edit - store) - collection = self.connection.table(collection) - update_data = {} - update_result = [] - - # care about the right format - if not isinstance(new_tags, list): - data['tags'] = [new_tags] - - # Merge DB docs and update data and write it back to DB - for doc in docs_to_update: - existing_tags = doc.get('tags') or [] - update_data = copy.deepcopy(data) - update_data['tags'] = list(set(existing_tags + new_tags)) - update_result += collection.update(update_data, query) + if not merge: + # Update all at once (no merging) + update_result += collection.update(data, query) + return { 'updated': len(update_result) } + + # Update every Entry one-by-one (merge every list item) + for doc in docs_to_update: - else: - # Plain function (overwrite existing attributes) - collection = self.connection.table(collection) - update_result = collection.update(data, query) + + # Look for lists to merge with this entry + for d in data.keys(): + + if isinstance(doc.get(d), list) and merge: + data[d] = list(set(doc.get(d) + data[d])) + continue + + # Update this uuid + update_result += collection.update( + data, + self._form_complete_query({ + 'key': 'uuid', + 'value': doc.get('uuid'), + 'compare': '==' + }) + ) return { 'updated': len(update_result) } - def delete(self, collection, condition=None, multi='AND'): + def _delete(self, collection, condition=None, multi='AND'): """ Löscht Datensätze in der Datenbank, die die angegebene Bedingung erfüllen. @@ -340,6 +354,8 @@ def test_contains(value, search): where_statement = where_statement.test(self._none_of_test, condition_val) if condition_method == 'all': where_statement = where_statement.all(condition_val) + if condition_method == 'exact': + where_statement = where_statement.test(self._same_elements, condition_val) # Standard Query try: @@ -438,6 +454,10 @@ def _none_of_test(self, value, forbidden_values): """Benutzerdefinierter Test: Keines der Elemente ist in einer Liste vorhanden""" return not any(item in forbidden_values for item in value) + def _same_elements(self, value, given_values): + """Benutzerdefinierter Test: Alle Elemente der Listen sind gleich""" + return set(value) == set(given_values) + def _get_collections(self): """ Liste alle tables der Datenbank. diff --git a/settings/parser/00-default.json b/settings/parser/00-default.json index 92545c5..bcda06c 100644 --- a/settings/parser/00-default.json +++ b/settings/parser/00-default.json @@ -2,7 +2,12 @@ { "name": "Mandatsreferenz", "metatype": "parser", - "regex": "Mandatsref\\:\\s?([A-z0-9]*)" + "regex": "(?:MREF\\:\\s|Mandatsref\\:\\s)([A-z0-9]+)\\b(?!\\b:)" + }, + { + "name": "End-to-End-Referenz", + "metatype": "parser", + "regex": "(?:EREF:\\s|End-to-End-Ref\\.\\:\\s)([A-Za-z0-9\\-\\s]+)\\b(?!\\b:)" }, { "name": "Gläubiger-ID", diff --git a/settings/rule/00-default-categories.json b/settings/rule/00-default-categories.json new file mode 100644 index 0000000..86efd0b --- /dev/null +++ b/settings/rule/00-default-categories.json @@ -0,0 +1,50 @@ +[ + { + "metatype": "category", + "name": "Lebensmittel", + "category": "Lebensmittel", + "filter": [{ + "key": "tags", + "value": ["Supermarkt", "Drogerie", "Apotheke", "Bäcker"], + "compare": "in" + }] + }, + { + "metatype": "category", + "name": "Abgaben", + "category": "Abgaben", + "multi": "AND", + "filter": [{ + "key": "tags", + "value": ["Stadt", "Steuer"], + "compare": "in" + },{ + "key": "tags", + "value": ["Betreuung", "Bildung", "Verkehr"], + "compare": "notin" + }], + "parsed": { + "Gläubiger-ID": "DE7000100000077777" + } + }, + { + "metatype": "category", + "name": "Fixkosten", + "category": "Fixkosten", + "filter": [{ + "key": "tags", + "value": ["Strom", "Wasser", "Gas", "Versicherung", "Kommunikation", "Miete", "Kredite"], + "compare": "in" + }] + }, + { + "metatype": "category", + "name": "Fortbewegung", + "category": "Fortbewegung", + "filter": [{ + "key": "tags", + "value": ["Tanken", "Auto", "ÖPNV"], + "compare": "in" + }] + } +] \ No newline at end of file diff --git a/settings/rule/00-default-tags.json b/settings/rule/00-default-tags.json new file mode 100644 index 0000000..0706000 --- /dev/null +++ b/settings/rule/00-default-tags.json @@ -0,0 +1,154 @@ +[ + { + "metatype": "rule", + "name": "City Tax", + "tags": ["Stadt"], + "filter": [{ + "key": "text_tx", + "value": "(ABGABEN\\sLT\\.\\sBESCHEID)", + "compare": "regex" + }] + }, + { + "metatype": "rule", + "name": "Supermarkets", + "tags": ["Lebensmittel"], + "filter": [{ + "key": "text_tx", + "value": "((?