Compare commits
10 commits
763427017b
...
86118d8fe4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
86118d8fe4 | ||
![]() |
7d857362a6 | ||
![]() |
55bc5f0360 | ||
![]() |
9d13443999 | ||
![]() |
7c8d60e3e5 | ||
![]() |
3c5fc31778 | ||
![]() |
50d4f066e3 | ||
![]() |
b890d13e6e | ||
![]() |
d9a1f637f8 | ||
![]() |
590590c8e5 |
11 changed files with 616 additions and 532 deletions
42
README.md
42
README.md
|
@ -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
958
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.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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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, 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()
|
||||||
|
|
|
@ -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
25
schmeckels/restart.py
Normal 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()
|
|
@ -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)
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue