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
|
||||
|
||||
## 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:
|
||||
|
|
956
poetry.lock
generated
956
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"
|
||||
|
||||
[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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
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()
|
|
@ -36,10 +36,11 @@ def command():
|
|||
print("Skipping")
|
||||
continue
|
||||
|
||||
tag = tag_lookup.get(select, None)
|
||||
for text in select.split(","):
|
||||
tag = tag_lookup.get(text, None)
|
||||
if not tag:
|
||||
print(f"Creating new category '{select}'")
|
||||
tag = create_tag(select, session)
|
||||
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])
|
||||
|
@ -47,6 +48,7 @@ def command():
|
|||
|
||||
t.tags.append(tag)
|
||||
session.add(t)
|
||||
|
||||
session.commit()
|
||||
|
||||
print("-" * 20)
|
||||
|
|
|
@ -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
|
||||
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,29 +35,47 @@ def command():
|
|||
.first()[0]
|
||||
)
|
||||
|
||||
# No transactions with this tag in the given timeframe
|
||||
if not tag_sum:
|
||||
continue
|
||||
|
||||
if tag_sum < 0:
|
||||
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(" {:<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(" {:<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)
|
||||
|
|
|
@ -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
|
||||
];
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue