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
## 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
- Linechart Verlauf über Monate (Gesamtetrag)
- Piechart verschiedene Tags
- Übersicht auf Startseite
- Monatsübersicht dieser/letzter Monat
- Anzahl ungetaggte Transaktionen
- Doku
## Usage
It's currently not packages so you have to use `poetry run schmeckels <cmd>`.
Run it without a command to get a small help page listing all commands.
## 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
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"
[tool.poetry.dependencies]
python = "^3.6.1"
SQLAlchemy = "^1.3.20"
Flask = "^1.1.2"
mt-940 = "^4.23.0"
xdg = "^5.0.0"
PyYAML = "^5.3.1"
colorful = "^0.5.4"
schwifty = "^2020.9.0"
prompt-toolkit = "^3.0.8"
pytest = "^6.1.2"
python-dateutil = "^2.8.1"
python = "^3.8"
SQLAlchemy = "*"
Flask = "*"
mt-940 = "*"
xdg = "*"
PyYAML = "*"
colorful = "*"
schwifty = "*"
prompt-toolkit = "*"
pytest = "*"
python-dateutil = "*"
[tool.poetry.dev-dependencies]
@ -38,3 +38,6 @@ known_third_party = []
[tool.black]
line-length = 120
[tool.ruff]
ignore = ["E501"]

View file

@ -4,9 +4,6 @@ import sys
import click
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.models import Tag, Transaction

View file

@ -54,8 +54,8 @@ class Sparkasse_MT940(Bank):
data = t.data
date = data["date"]
amount = int(data["amount"].amount * 100)
iban = data.get("applicant_iban","")
name = data.get("applicant_name","")
iban = data.get("applicant_iban", "")
name = data.get("applicant_name", "")
description = data["purpose"]
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.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
__version__ = "0.0.1"
@ -53,4 +53,5 @@ def main():
cli.add_command(validate.command)
cli.add_command(info.command)
cli.add_command(stats.command)
cli.add_command(restart.command)
cli()

View file

@ -4,7 +4,7 @@ import sys
import click
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

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)))
for t in unsorted:
print("-"*20)
print("-" * 20)
print(" Name: {}".format(t.name))
print(" IBAN: {}".format(t.iban))
print(" Datum: {}".format(t.date))
@ -36,17 +36,19 @@ def command():
print("Skipping")
continue
tag = tag_lookup.get(select, None)
if not tag:
print(f"Creating new category '{select}'")
tag = create_tag(select, session)
for text in select.split(","):
tag = tag_lookup.get(text, None)
if not tag:
print(f"Creating new category '{text}'")
tag = create_tag(text, session)
tags = session.query(Tag).all()
tag_names = FuzzyWordCompleter([x.name for x in tags])
tag_lookup = {tag.name: tag for tag in tags}
tags = session.query(Tag).all()
tag_names = FuzzyWordCompleter([x.name for x 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()
print("-" * 20)

View file

@ -1,28 +1,33 @@
#! /usr/bin/env python3
from sqlalchemy import create_engine
from schmeckels.models import Tag, Transaction
from schmeckels.helper import format_amount, get_session
import click
from datetime import date, datetime
from math import floor
from datetime import datetime, date
from sqlalchemy import func, and_
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
@click.command(name="stats")
def command():
outgoing = {}
incoming = {}
withdrawal = {}
session = get_session()
tags = session.query(Tag).filter_by(reporting=True).all()
# Get start and end of timerange
year = 2021
start_date = date(year=year,month=1,day=1)
end_date = date(year=year,month=12,day=31)
year = 2023
start_date = date(year=year, month=1, day=1)
end_date = date(year=year, month=12, day=31)
for tag in tags:
if tag.name.find(":") < 0 and tag.name != "Privatentnahme":
continue
tag_sum = (
session.query(func.sum(Transaction.amount))
.filter(and_(Transaction.date >= start_date, Transaction.date <= end_date))
@ -30,30 +35,48 @@ def command():
.first()[0]
)
# No transactions with this tag in the given timeframe
if not tag_sum:
continue
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:
incoming[tag] = {"sum": tag_sum}
sum_outgoing = sum(x["sum"] for x in outgoing.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"]))}
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")
# Ausgaben
for name, data in outgoing.items():
print(" {:<30} {:>10}".format(name.name, data['sum']))
print("-"*44)
print(" {:<30} {:>10}".format(name.name, data["sum"]))
print("-" * 44)
print(" {:<30} {:>10}".format("AUSGABEN", format_amount(sum_outgoing)))
print("\n")
# Einnahmen
for name, data in incoming.items():
print(" {:<30} {:>10}".format(name.name, data['sum']))
print("-"*44)
print(" {:<30} {:>10}".format(name.name, data["sum"]))
print("-" * 44)
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("="*44)
print(" {:<30} {:>10}".format("GEWINN", format_amount(sum_incoming+sum_outgoing)))
print("=" * 44)
print(" {:<30} {:>10}".format("GEWINN", format_amount(sum_incoming + sum_outgoing)))

View file

@ -2,15 +2,18 @@
pkgs.mkShell {
buildInputs = with pkgs; [
black
python3
poetry
python3Packages.flake8
python3Packages.isort
python3Packages.pip
python3Packages.poetry
python3Packages.setuptools
# python3Packages.pygobject3
# python3Packages.cairocffi
black
isort
ruff
];
}