Compare commits

...

10 commits

Author SHA1 Message Date
fleaz
86118d8fe4
stats: Bump year because it's hardcoded and I'm lazy 2024-01-05 23:52:34 +01:00
fleaz
7d857362a6
update deps 2024-01-05 23:52:23 +01:00
fleaz
55bc5f0360
Write some docs on how to use 2023-07-31 20:35:31 +02:00
fleaz
9d13443999
Update packages 2023-07-19 22:56:42 +02:00
fleaz
7c8d60e3e5
Added ruff 2023-07-19 22:56:35 +02:00
fleaz
3c5fc31778
Cleanup and formatting 2023-02-24 00:07:01 +01:00
fleaz
50d4f066e3
fixup! Added 'restart' to start over 2023-02-24 00:05:55 +01:00
fleaz
b890d13e6e
stats: Automatic selection of valid tags. Show withdrawals 2023-02-24 00:05:01 +01:00
fleaz
d9a1f637f8
sort: Allow adding of comma seperated tags 2023-02-24 00:03:58 +01:00
fleaz
590590c8e5
Added 'restart' to start over 2023-02-24 00:03:01 +01:00
11 changed files with 616 additions and 532 deletions

View file

@ -1,15 +1,41 @@
# schmeckels # schmeckels
## ToDo ## Installation
You need to have Python >= 3.8 and Poetry installed. Then you can run `poetry install` to create a virtuelenv and download all dependencies.
- Graphen ## Usage
- Linechart Verlauf über Monate (Gesamtetrag) It's currently not packages so you have to use `poetry run schmeckels <cmd>`.
- Piechart verschiedene Tags Run it without a command to get a small help page listing all commands.
- Ü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:

958
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,17 +6,17 @@ authors = ["Felix Breidenstein <mail@felixbreidenstein.de>"]
license = "MIT" license = "MIT"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6.1" python = "^3.8"
SQLAlchemy = "^1.3.20" SQLAlchemy = "*"
Flask = "^1.1.2" Flask = "*"
mt-940 = "^4.23.0" mt-940 = "*"
xdg = "^5.0.0" xdg = "*"
PyYAML = "^5.3.1" PyYAML = "*"
colorful = "^0.5.4" colorful = "*"
schwifty = "^2020.9.0" schwifty = "*"
prompt-toolkit = "^3.0.8" prompt-toolkit = "*"
pytest = "^6.1.2" pytest = "*"
python-dateutil = "^2.8.1" python-dateutil = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
@ -38,3 +38,6 @@ known_third_party = []
[tool.black] [tool.black]
line-length = 120 line-length = 120
[tool.ruff]
ignore = ["E501"]

View file

@ -4,9 +4,6 @@ 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

View file

@ -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)

View file

@ -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, serve, sort, validate, stats from schmeckels import autosort, importer, info, models, restart, serve, sort, stats, validate
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,4 +53,5 @@ 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()

View file

@ -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_session, get_rules from schmeckels.helper import build_database_filename, build_rules_filename, get_data_dir, get_rules, get_session
from schmeckels.models import Tag, Transaction from schmeckels.models import Tag, Transaction

25
schmeckels/restart.py Normal file
View file

@ -0,0 +1,25 @@
#! /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()

View file

@ -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,17 +36,19 @@ def command():
print("Skipping") print("Skipping")
continue continue
tag = tag_lookup.get(select, None) for text in select.split(","):
if not tag: tag = tag_lookup.get(text, None)
print(f"Creating new category '{select}'") if not tag:
tag = create_tag(select, session) print(f"Creating new category '{text}'")
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)

View file

@ -1,28 +1,33 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
from sqlalchemy import create_engine from datetime import date, datetime
from schmeckels.models import Tag, Transaction
from schmeckels.helper import format_amount, get_session
import click
from math import floor from math import floor
from datetime import datetime, date
from sqlalchemy import func, and_ import click
from dateutil.relativedelta import relativedelta 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
@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 = 2021 year = 2023
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))
@ -30,30 +35,48 @@ 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:
outgoing[tag] = {"sum": tag_sum} if tag.name == "Privatentnahme":
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)))

View file

@ -2,15 +2,18 @@
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
]; ];
} }