Implemented autosort command
This commit is contained in:
parent
c687b244a7
commit
48ba732038
5 changed files with 162 additions and 27 deletions
3
Pipfile
3
Pipfile
|
@ -14,6 +14,9 @@ bottle = "*"
|
||||||
mt-940 = "*"
|
mt-940 = "*"
|
||||||
click = "*"
|
click = "*"
|
||||||
xdg = "*"
|
xdg = "*"
|
||||||
|
pyyaml = "*"
|
||||||
|
colored = "*"
|
||||||
|
colorful = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
|
|
54
Pipfile.lock
generated
54
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "45bd6b24d71ce0251c64ad6cad358882053f9cacc52bb418fce22889da6ff702"
|
"sha256": "816c7c2536e6f09c3088905953eb3d20b067dad888a1626eedb2ddef41c6b045"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -32,21 +32,53 @@
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==7.1.1"
|
"version": "==7.1.1"
|
||||||
},
|
},
|
||||||
"mt-940": {
|
"colored": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:95e55584908c7d51b8a08cfb55d72297f06385e40c9baf9258cdaaf26811aafa",
|
"sha256:056fac09d9e39b34296e7618897ed1b8c274f98423770c2980d829fd670955ed"
|
||||||
"sha256:fad1a00df51ede762d7d5e4d019de9ad86357d7556862b259ce7fba400ac2d6e"
|
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.20.0"
|
"version": "==1.4.2"
|
||||||
|
},
|
||||||
|
"colorful": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:86848ad4e2eda60cd2519d8698945d22f6f6551e23e95f3f14dfbb60997807ea",
|
||||||
|
"sha256:8d264b52a39aae4c0ba3e2a46afbaec81b0559a99be0d2cfe2aba4cf94531348"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.5.4"
|
||||||
|
},
|
||||||
|
"mt-940": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7cbd88fd7252d5a2694593633b31f819eb302423058fecb9f9959e74c01c2b86",
|
||||||
|
"sha256:81fbfe647ebf2fc9700d6f61e2ba4c9d6c5fab56ccde2b54144481497c64b65c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==4.21.0"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:859e1b205b6cf6a51fa57fa34202e45365cf58f8338f0ee9f4e84a4165b37d5b",
|
"sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8",
|
||||||
"sha256:ebe6b1b08c888b84c50d7f93dee21a09af39860144ff6130aadbd61ae8d29783"
|
"sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.0.4"
|
"version": "==3.0.5"
|
||||||
|
},
|
||||||
|
"pyyaml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||||
|
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
|
||||||
|
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
|
||||||
|
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
|
||||||
|
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
|
||||||
|
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
|
||||||
|
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
|
||||||
|
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
|
||||||
|
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
|
||||||
|
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
|
||||||
|
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.3.1"
|
||||||
},
|
},
|
||||||
"sqlalchemy": {
|
"sqlalchemy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -57,10 +89,10 @@
|
||||||
},
|
},
|
||||||
"wcwidth": {
|
"wcwidth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
|
"sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
|
||||||
"sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
|
"sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
|
||||||
],
|
],
|
||||||
"version": "==0.1.8"
|
"version": "==0.1.9"
|
||||||
},
|
},
|
||||||
"xdg": {
|
"xdg": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
70
autosort.py
Normal file
70
autosort.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from models import Category, Transaction
|
||||||
|
from helper import get_session, add_category, get_rules
|
||||||
|
import sys
|
||||||
|
import click
|
||||||
|
import colorful as cf
|
||||||
|
|
||||||
|
from prompt_toolkit.completion import FuzzyWordCompleter
|
||||||
|
from prompt_toolkit.shortcuts import prompt
|
||||||
|
|
||||||
|
def print_verbose(verbose, string):
|
||||||
|
if verbose:
|
||||||
|
print(string)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="autosort")
|
||||||
|
@click.option("--profile", "-p")
|
||||||
|
@click.option("--dry-run", default=False, is_flag=True)
|
||||||
|
@click.option("--verbose", "-v", default=False, is_flag=True)
|
||||||
|
def command(profile, dry_run, verbose):
|
||||||
|
session = get_session(profile)
|
||||||
|
rules = get_rules(profile)
|
||||||
|
|
||||||
|
unsorted = session.query(Transaction).filter(Transaction.category_id == None).all()
|
||||||
|
print("Found {} unsorted transcations".format(len(unsorted)))
|
||||||
|
matched = 0
|
||||||
|
|
||||||
|
for t in unsorted:
|
||||||
|
cat_name = apply_rules(t, rules)
|
||||||
|
if cat_name:
|
||||||
|
short_name = (t.name[:25] if len(t.name)>25 else t.name)
|
||||||
|
short_desc = (t.description[:45] if len(t.description)>50 else t.description)
|
||||||
|
print_verbose(verbose,"{:<25} | {:<50}| {:>10}€".format(short_name, short_desc, t.amount/100))
|
||||||
|
print_verbose(verbose, cf.green(f"Matched '{cat_name}'"))
|
||||||
|
cat_id = session.query(Category).filter(Category.name == cat_name).first()
|
||||||
|
if not cat_id:
|
||||||
|
cat_id = add_category(cat_name, profile, session)
|
||||||
|
matched +=1
|
||||||
|
t.category_id = cat_id
|
||||||
|
session.add(t)
|
||||||
|
print_verbose(verbose, "-"*90)
|
||||||
|
|
||||||
|
print(f"Automatically matched {matched} transactions")
|
||||||
|
if not dry_run:
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_rules(t, rules):
|
||||||
|
for r in rules:
|
||||||
|
iban = r.get("iban")
|
||||||
|
name_regex = r.get("name_regex")
|
||||||
|
desc_regex = r.get("desc_regex")
|
||||||
|
|
||||||
|
if iban:
|
||||||
|
if t.iban != iban:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name_regex:
|
||||||
|
if not name_regex.search(t.name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if desc_regex:
|
||||||
|
if not desc_regex.search(t.description):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return r["category"]
|
||||||
|
|
2
cli
2
cli
|
@ -6,6 +6,7 @@ import sys
|
||||||
import sort
|
import sort
|
||||||
import importer
|
import importer
|
||||||
import serve
|
import serve
|
||||||
|
import autosort
|
||||||
|
|
||||||
from helper import build_database_filename, create_dirs
|
from helper import build_database_filename, create_dirs
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
@ -38,4 +39,5 @@ if __name__ == '__main__':
|
||||||
cli.add_command(sort.command)
|
cli.add_command(sort.command)
|
||||||
cli.add_command(importer.command)
|
cli.add_command(importer.command)
|
||||||
cli.add_command(serve.command)
|
cli.add_command(serve.command)
|
||||||
|
cli.add_command(autosort.command)
|
||||||
cli()
|
cli()
|
||||||
|
|
60
helper.py
60
helper.py
|
@ -1,11 +1,19 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import yaml
|
||||||
|
import re
|
||||||
from xdg import XDG_CONFIG_HOME, XDG_DATA_HOME
|
from xdg import XDG_CONFIG_HOME, XDG_DATA_HOME
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from models import Category, Transaction
|
from models import Category, Transaction
|
||||||
|
|
||||||
|
try:
|
||||||
|
from yaml import CLoader as Loader, CDumper as Dumper
|
||||||
|
except ImportError:
|
||||||
|
from yaml import Loader, Dumper
|
||||||
|
|
||||||
|
|
||||||
CONFIG_DIR = os.path.join(XDG_CONFIG_HOME, "schmeckels")
|
CONFIG_DIR = os.path.join(XDG_CONFIG_HOME, "schmeckels")
|
||||||
DATA_DIR = os.path.join(XDG_DATA_HOME, "schmeckels")
|
DATA_DIR = os.path.join(XDG_DATA_HOME, "schmeckels")
|
||||||
|
|
||||||
|
@ -19,20 +27,23 @@ def create_dirs():
|
||||||
|
|
||||||
|
|
||||||
def build_database_filename(profile_name):
|
def build_database_filename(profile_name):
|
||||||
return f"{DATA_DIR}/{profile_name}.db"
|
return f"{DATA_DIR}/databases/{profile_name}.db"
|
||||||
|
|
||||||
|
def build_rules_filename(profile_name):
|
||||||
|
return f"{DATA_DIR}/rules/{profile_name}.yaml"
|
||||||
|
|
||||||
|
def check_single_profile():
|
||||||
|
files = os.listdir(f"{DATA_DIR}/databases")
|
||||||
|
if len(files) == 1:
|
||||||
|
return files.split(".")[0]
|
||||||
|
else:
|
||||||
|
print("--profile is required when you have more than one database.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
def get_session(profile_name):
|
def get_session(profile_name):
|
||||||
if not profile_name:
|
if not profile_name:
|
||||||
count = len(os.listdir(DATA_DIR))
|
profile_name = check_single_profile()
|
||||||
if count == 1:
|
filename = build_database_filename(profile_name)
|
||||||
filename = f"{DATA_DIR}/{os.listdir(DATA_DIR)[0]}"
|
|
||||||
else:
|
|
||||||
print("--profile is required when you have more than one database.")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
filename = build_database_filename(profile_name)
|
|
||||||
|
|
||||||
if os.path.exists(filename) and os.path.isfile(filename):
|
if os.path.exists(filename) and os.path.isfile(filename):
|
||||||
engine = create_engine(f"sqlite:///{filename}")
|
engine = create_engine(f"sqlite:///{filename}")
|
||||||
Session = sessionmaker(bind=engine)
|
Session = sessionmaker(bind=engine)
|
||||||
|
@ -42,8 +53,25 @@ def get_session(profile_name):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def create_category(name, parent, profile):
|
def get_rules(profile_name):
|
||||||
session = get_session(profile)
|
if not profile_name:
|
||||||
|
profile_name = check_single_profile()
|
||||||
|
filename = build_rules_filename(profile_name)
|
||||||
|
if os.path.exists(filename) and os.path.isfile(filename):
|
||||||
|
with open(filename) as fh:
|
||||||
|
data = yaml.load(fh, Loader=Loader)
|
||||||
|
for rule in data:
|
||||||
|
if rule.get("name"):
|
||||||
|
rule["name_regex"] = re.compile(rule["name"])
|
||||||
|
if rule.get("description"):
|
||||||
|
rule["desc_regex"] = re.compile(rule["description"])
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
print(f"No rules for profile '{profile_name}'. Did you run 'init'?")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_category(name, parent, profile, session):
|
||||||
c = session.query(Category).filter(Category.name == name).first()
|
c = session.query(Category).filter(Category.name == name).first()
|
||||||
if not c:
|
if not c:
|
||||||
c = Category(name=name, parent_id=parent)
|
c = Category(name=name, parent_id=parent)
|
||||||
|
@ -52,14 +80,14 @@ def create_category(name, parent, profile):
|
||||||
return c.id
|
return c.id
|
||||||
|
|
||||||
|
|
||||||
def add_category(name, profile):
|
def add_category(name, profile, session):
|
||||||
parts = name.split(":")
|
parts = name.split(":")
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
return create_category(name, None, profile)
|
return create_category(name, None, profile, session)
|
||||||
else:
|
else:
|
||||||
for i in range(len(parts) - 1):
|
for i in range(len(parts) - 1):
|
||||||
parent = parts[i]
|
parent = parts[i]
|
||||||
child = parts[i + 1]
|
child = parts[i + 1]
|
||||||
parent_id = create_category(parent, None, profile)
|
parent_id = create_category(parent, None, profile, session)
|
||||||
id = create_category(child, parent_id, profile)
|
id = create_category(child, parent_id, profile, session)
|
||||||
return id
|
return id
|
||||||
|
|
Loading…
Add table
Reference in a new issue