Compare commits
No commits in common. "86118d8fe4d2f6409845968254d9f32a16306fac" and "763427017bde571435ff52e3fd1d66160f747cca" have entirely different histories.
86118d8fe4
...
763427017b
11 changed files with 510 additions and 594 deletions
42
README.md
42
README.md
|
@ -1,41 +1,15 @@
|
||||||
# schmeckels
|
# schmeckels
|
||||||
|
|
||||||
## Installation
|
## ToDo
|
||||||
You need to have Python >= 3.8 and Poetry installed. Then you can run `poetry install` to create a virtuelenv and download all dependencies.
|
|
||||||
|
|
||||||
## Usage
|
- Graphen
|
||||||
It's currently not packages so you have to use `poetry run schmeckels <cmd>`.
|
- Linechart Verlauf über Monate (Gesamtetrag)
|
||||||
Run it without a command to get a small help page listing all commands.
|
- Piechart verschiedene Tags
|
||||||
|
- Übersicht auf Startseite
|
||||||
|
- Monatsübersicht dieser/letzter Monat
|
||||||
|
- Anzahl ungetaggte Transaktionen
|
||||||
|
- Doku
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
### init
|
|
||||||
Run this once to create empty database and rules files
|
|
||||||
|
|
||||||
### info
|
|
||||||
This will print some informations about your sorted/unsorted transactions and also show the location of your config
|
|
||||||
files.
|
|
||||||
|
|
||||||
### import
|
|
||||||
Run this to import CSV's from your bank
|
|
||||||
|
|
||||||
### validate
|
|
||||||
Check the syntax of your rules. Good thing to do before running e.g. autosort
|
|
||||||
|
|
||||||
### autosort
|
|
||||||
Run your defined rules against all unsorted transactions. Run with "-v --dry-run" after writing new rules is a good
|
|
||||||
practice.
|
|
||||||
|
|
||||||
### serve
|
|
||||||
Start the web interface.
|
|
||||||
|
|
||||||
### sort
|
|
||||||
Manually sort all unsorted transactions.
|
|
||||||
|
|
||||||
### stats
|
|
||||||
Render a really simple EUR (Einnahmen Überschuss Rechnung).
|
|
||||||
|
|
||||||
restart
|
|
||||||
|
|
||||||
## Example rules
|
## Example rules
|
||||||
The rules file must be valid YAML and contain a list of all rules. A rule must contain:
|
The rules file must be valid YAML and contain a list of all rules. A rule must contain:
|
||||||
|
|
912
poetry.lock
generated
912
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,17 +6,17 @@ authors = ["Felix Breidenstein <mail@felixbreidenstein.de>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.8"
|
python = "^3.6.1"
|
||||||
SQLAlchemy = "*"
|
SQLAlchemy = "^1.3.20"
|
||||||
Flask = "*"
|
Flask = "^1.1.2"
|
||||||
mt-940 = "*"
|
mt-940 = "^4.23.0"
|
||||||
xdg = "*"
|
xdg = "^5.0.0"
|
||||||
PyYAML = "*"
|
PyYAML = "^5.3.1"
|
||||||
colorful = "*"
|
colorful = "^0.5.4"
|
||||||
schwifty = "*"
|
schwifty = "^2020.9.0"
|
||||||
prompt-toolkit = "*"
|
prompt-toolkit = "^3.0.8"
|
||||||
pytest = "*"
|
pytest = "^6.1.2"
|
||||||
python-dateutil = "*"
|
python-dateutil = "^2.8.1"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
@ -38,6 +38,3 @@ known_third_party = []
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
ignore = ["E501"]
|
|
||||||
|
|
|
@ -4,6 +4,9 @@ import sys
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import colorful as cf
|
import colorful as cf
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from schmeckels.helper import create_tag, get_rules, get_session
|
from schmeckels.helper import create_tag, get_rules, get_session
|
||||||
from schmeckels.models import Tag, Transaction
|
from schmeckels.models import Tag, Transaction
|
||||||
|
|
|
@ -54,8 +54,8 @@ class Sparkasse_MT940(Bank):
|
||||||
data = t.data
|
data = t.data
|
||||||
date = data["date"]
|
date = data["date"]
|
||||||
amount = int(data["amount"].amount * 100)
|
amount = int(data["amount"].amount * 100)
|
||||||
iban = data.get("applicant_iban", "")
|
iban = data.get("applicant_iban","")
|
||||||
name = data.get("applicant_name", "")
|
name = data.get("applicant_name","")
|
||||||
description = data["purpose"]
|
description = data["purpose"]
|
||||||
|
|
||||||
yield models.Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
yield models.Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
||||||
|
|
|
@ -8,7 +8,7 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from schmeckels import autosort, importer, info, models, restart, serve, sort, stats, validate
|
from schmeckels import autosort, importer, info, models, serve, sort, validate, stats
|
||||||
from schmeckels.helper import build_database_filename, build_rules_filename, get_data_dir
|
from schmeckels.helper import build_database_filename, build_rules_filename, get_data_dir
|
||||||
|
|
||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
|
@ -53,5 +53,4 @@ def main():
|
||||||
cli.add_command(validate.command)
|
cli.add_command(validate.command)
|
||||||
cli.add_command(info.command)
|
cli.add_command(info.command)
|
||||||
cli.add_command(stats.command)
|
cli.add_command(stats.command)
|
||||||
cli.add_command(restart.command)
|
|
||||||
cli()
|
cli()
|
||||||
|
|
|
@ -4,7 +4,7 @@ import sys
|
||||||
import click
|
import click
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from schmeckels.helper import build_database_filename, build_rules_filename, get_data_dir, get_rules, get_session
|
from schmeckels.helper import build_database_filename, build_rules_filename, get_data_dir, get_session, get_rules
|
||||||
from schmeckels.models import Tag, Transaction
|
from schmeckels.models import Tag, Transaction
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
#! /usr/bin/env python3
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import click
|
|
||||||
import colorful as cf
|
|
||||||
|
|
||||||
from schmeckels.helper import create_tag, get_rules, get_session
|
|
||||||
from schmeckels.models import Tag, Transaction
|
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="restart")
|
|
||||||
def command():
|
|
||||||
input("This will DELETE all tags and remove all sorting! You sure?")
|
|
||||||
|
|
||||||
session = get_session()
|
|
||||||
allTransactions = session.query(Transaction).all()
|
|
||||||
|
|
||||||
for transaction in allTransactions:
|
|
||||||
transaction.tags = []
|
|
||||||
|
|
||||||
session.bulk_save_objects(allTransactions)
|
|
||||||
|
|
||||||
session.query(Tag).delete()
|
|
||||||
session.commit()
|
|
|
@ -20,7 +20,7 @@ def command():
|
||||||
print("Found {} unsorted transcations".format(len(unsorted)))
|
print("Found {} unsorted transcations".format(len(unsorted)))
|
||||||
|
|
||||||
for t in unsorted:
|
for t in unsorted:
|
||||||
print("-" * 20)
|
print("-"*20)
|
||||||
print(" Name: {}".format(t.name))
|
print(" Name: {}".format(t.name))
|
||||||
print(" IBAN: {}".format(t.iban))
|
print(" IBAN: {}".format(t.iban))
|
||||||
print(" Datum: {}".format(t.date))
|
print(" Datum: {}".format(t.date))
|
||||||
|
@ -36,19 +36,17 @@ def command():
|
||||||
print("Skipping")
|
print("Skipping")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for text in select.split(","):
|
tag = tag_lookup.get(select, None)
|
||||||
tag = tag_lookup.get(text, None)
|
if not tag:
|
||||||
if not tag:
|
print(f"Creating new category '{select}'")
|
||||||
print(f"Creating new category '{text}'")
|
tag = create_tag(select, session)
|
||||||
tag = create_tag(text, session)
|
|
||||||
|
|
||||||
tags = session.query(Tag).all()
|
tags = session.query(Tag).all()
|
||||||
tag_names = FuzzyWordCompleter([x.name for x in tags])
|
tag_names = FuzzyWordCompleter([x.name for x in tags])
|
||||||
tag_lookup = {tag.name: tag for tag in tags}
|
tag_lookup = {tag.name: tag for tag in tags}
|
||||||
|
|
||||||
t.tags.append(tag)
|
|
||||||
session.add(t)
|
|
||||||
|
|
||||||
|
t.tags.append(tag)
|
||||||
|
session.add(t)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
print("-" * 20)
|
print("-" * 20)
|
||||||
|
|
|
@ -1,33 +1,28 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
from datetime import date, datetime
|
from sqlalchemy import create_engine
|
||||||
from math import floor
|
|
||||||
|
|
||||||
import click
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from sqlalchemy import and_, create_engine, func
|
|
||||||
|
|
||||||
from schmeckels.helper import format_amount, get_session
|
|
||||||
from schmeckels.models import Tag, Transaction
|
from schmeckels.models import Tag, Transaction
|
||||||
|
from schmeckels.helper import format_amount, get_session
|
||||||
|
import click
|
||||||
|
from math import floor
|
||||||
|
from datetime import datetime, date
|
||||||
|
from sqlalchemy import func, and_
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="stats")
|
@click.command(name="stats")
|
||||||
def command():
|
def command():
|
||||||
outgoing = {}
|
outgoing = {}
|
||||||
incoming = {}
|
incoming = {}
|
||||||
withdrawal = {}
|
|
||||||
session = get_session()
|
session = get_session()
|
||||||
|
|
||||||
tags = session.query(Tag).filter_by(reporting=True).all()
|
tags = session.query(Tag).filter_by(reporting=True).all()
|
||||||
|
|
||||||
# Get start and end of timerange
|
# Get start and end of timerange
|
||||||
year = 2023
|
year = 2021
|
||||||
start_date = date(year=year, month=1, day=1)
|
start_date = date(year=year,month=1,day=1)
|
||||||
end_date = date(year=year, month=12, day=31)
|
end_date = date(year=year,month=12,day=31)
|
||||||
|
|
||||||
for tag in tags:
|
for tag in tags:
|
||||||
if tag.name.find(":") < 0 and tag.name != "Privatentnahme":
|
|
||||||
continue
|
|
||||||
|
|
||||||
tag_sum = (
|
tag_sum = (
|
||||||
session.query(func.sum(Transaction.amount))
|
session.query(func.sum(Transaction.amount))
|
||||||
.filter(and_(Transaction.date >= start_date, Transaction.date <= end_date))
|
.filter(and_(Transaction.date >= start_date, Transaction.date <= end_date))
|
||||||
|
@ -35,48 +30,30 @@ def command():
|
||||||
.first()[0]
|
.first()[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
# No transactions with this tag in the given timeframe
|
|
||||||
if not tag_sum:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if tag_sum < 0:
|
if tag_sum < 0:
|
||||||
if tag.name == "Privatentnahme":
|
outgoing[tag] = {"sum": tag_sum}
|
||||||
withdrawal[tag] = {"sum": tag_sum}
|
|
||||||
else:
|
|
||||||
outgoing[tag] = {"sum": tag_sum}
|
|
||||||
else:
|
else:
|
||||||
incoming[tag] = {"sum": tag_sum}
|
incoming[tag] = {"sum": tag_sum}
|
||||||
|
|
||||||
sum_outgoing = sum(x["sum"] for x in outgoing.values())
|
sum_outgoing = sum(x["sum"] for x in outgoing.values())
|
||||||
sum_incoming = sum(x["sum"] for x in incoming.values())
|
sum_incoming = sum(x["sum"] for x in incoming.values())
|
||||||
sum_withdrawal = sum(x["sum"] for x in withdrawal.values())
|
|
||||||
|
|
||||||
outgoing = {k: format_amount(v) for k, v in sorted(outgoing.items(), key=lambda item: float(item[1]["sum"]))}
|
outgoing = {k: format_amount(v) for k, v in sorted(outgoing.items(), key=lambda item: float(item[1]["sum"]))}
|
||||||
incoming = {k: format_amount(v) for k, v in sorted(incoming.items(), key=lambda item: float(item[1]["sum"]))}
|
incoming = {k: format_amount(v) for k, v in sorted(incoming.items(), key=lambda item: float(item[1]["sum"]))}
|
||||||
withdrawawl = {k: format_amount(v) for k, v in sorted(withdrawal.items(), key=lambda item: float(item[1]["sum"]))}
|
|
||||||
|
|
||||||
print(f"============== Report for {year} =============\n")
|
print(f"============== Report for {year} =============\n")
|
||||||
|
|
||||||
# Ausgaben
|
|
||||||
for name, data in outgoing.items():
|
for name, data in outgoing.items():
|
||||||
print(" {:<30} {:>10} €".format(name.name, data["sum"]))
|
print(" {:<30} {:>10} €".format(name.name, data['sum']))
|
||||||
print("-" * 44)
|
print("-"*44)
|
||||||
print(" {:<30} {:>10} €".format("AUSGABEN", format_amount(sum_outgoing)))
|
print(" {:<30} {:>10} €".format("AUSGABEN", format_amount(sum_outgoing)))
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
|
|
||||||
# Einnahmen
|
|
||||||
for name, data in incoming.items():
|
for name, data in incoming.items():
|
||||||
print(" {:<30} {:>10} €".format(name.name, data["sum"]))
|
print(" {:<30} {:>10} €".format(name.name, data['sum']))
|
||||||
print("-" * 44)
|
print("-"*44)
|
||||||
print(" {:<30} {:>10} €".format("EINNAHMEN", format_amount(sum_incoming)))
|
print(" {:<30} {:>10} €".format("EINNAHMEN", format_amount(sum_incoming)))
|
||||||
print("\n")
|
|
||||||
|
|
||||||
# Entnahmen
|
|
||||||
for name, data in withdrawal.items():
|
|
||||||
print(" {:<30} {:>10} €".format(name.name, data["sum"]))
|
|
||||||
print("-" * 44)
|
|
||||||
print(" {:<30} {:>10} €".format("ENTNAHME", format_amount(sum_withdrawal)))
|
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
print("=" * 44)
|
print("="*44)
|
||||||
print(" {:<30} {:>10} €".format("GEWINN", format_amount(sum_incoming + sum_outgoing)))
|
print(" {:<30} {:>10} €".format("GEWINN", format_amount(sum_incoming+sum_outgoing)))
|
||||||
|
|
|
@ -2,18 +2,15 @@
|
||||||
|
|
||||||
pkgs.mkShell {
|
pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
black
|
||||||
python3
|
python3
|
||||||
poetry
|
|
||||||
python3Packages.flake8
|
python3Packages.flake8
|
||||||
python3Packages.isort
|
python3Packages.isort
|
||||||
python3Packages.pip
|
python3Packages.pip
|
||||||
|
python3Packages.poetry
|
||||||
python3Packages.setuptools
|
python3Packages.setuptools
|
||||||
# python3Packages.pygobject3
|
# python3Packages.pygobject3
|
||||||
# python3Packages.cairocffi
|
# python3Packages.cairocffi
|
||||||
|
|
||||||
black
|
|
||||||
isort
|
|
||||||
ruff
|
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue