Multiple importer and bulksort in the GUI
This commit is contained in:
parent
60cbf9da2e
commit
dfea791bb6
11 changed files with 133 additions and 53 deletions
2
.flake8
Normal file
2
.flake8
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
1
Pipfile
1
Pipfile
|
@ -5,6 +5,7 @@ verify_ssl = true
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
flake8 = "*"
|
flake8 = "*"
|
||||||
|
autopep8 = "*"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
sqlalchemy = "*"
|
sqlalchemy = "*"
|
||||||
|
|
9
Pipfile.lock
generated
9
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "25b9941c75b9e81082b3302534b779bedf7a24979213c97e7da0524201b2f333"
|
"sha256": "731c3810c49b4b77cd9aac8450a97650031dc994b7367106567d351f96011a24"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -64,6 +64,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
|
"autopep8": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0f592a0447acea0c2b0a9602be1e4e3d86db52badd2e3c84f0193bfd89fd3a43"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.5"
|
||||||
|
},
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
"sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
|
||||||
|
|
10
README.md
Normal file
10
README.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# schmeckels
|
||||||
|
|
||||||
|
## ToDo
|
||||||
|
|
||||||
|
- Profile via config files im home
|
||||||
|
- Graphen
|
||||||
|
- Linechart Verlauf über Monat (Gesamtetrag)
|
||||||
|
- Piechart verschiedene Kategorien
|
||||||
|
- Automatcher für Regeln
|
||||||
|
- setup.py
|
90
importer.py
90
importer.py
|
@ -1,47 +1,79 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
import mt940
|
import mt940
|
||||||
|
import csv
|
||||||
import click
|
import click
|
||||||
|
from datetime import datetime
|
||||||
from sqlalchemy import create_engine, desc
|
from sqlalchemy import create_engine, desc
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from models import Transaction
|
from models import Transaction
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def convert_amount(t):
|
@click.command(name="import")
|
||||||
return int(t.amount * 100)
|
@click.option("--filetype", "-t", type=click.Choice(["dkb", "sparkasse-mt940", "bunq-csv"], case_sensitive=False))
|
||||||
|
@click.argument("filename", type=click.Path(exists=True))
|
||||||
@click.command(name='import')
|
def command(filename, filetype):
|
||||||
@click.argument('filename', type=click.Path(exists=True))
|
|
||||||
def command(filename):
|
|
||||||
click.echo(click.format_filename(filename))
|
|
||||||
engine = create_engine("sqlite:///app.db")
|
engine = create_engine("sqlite:///app.db")
|
||||||
Base = declarative_base()
|
|
||||||
|
|
||||||
Session = sessionmaker(bind=engine)
|
Session = sessionmaker(bind=engine)
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
latest = session.query(Transaction).order_by(desc(Transaction.date)).first()
|
latest = session.query(Transaction).order_by(desc(Transaction.date)).first()
|
||||||
|
new = []
|
||||||
|
if filetype == "sparkasse-mt940":
|
||||||
|
transactions = mt940.parse(click.format_filename(filename))
|
||||||
|
for t in transactions:
|
||||||
|
data = t.data
|
||||||
|
|
||||||
transactions = mt940.parse("2020-01.TXT")
|
date = data["date"]
|
||||||
count = 0
|
amount = int(data["amount"].amount * 100)
|
||||||
for t in transactions:
|
iban = data["applicant_iban"]
|
||||||
data = t.data
|
name = data["applicant_name"]
|
||||||
|
description = data["purpose"]
|
||||||
|
|
||||||
date = data['date']
|
# if latest and data['date'] < latest.date:
|
||||||
amount = convert_amount(data['amount'])
|
# print("Found transaction older than then oldest transction in the DB. Aborting")
|
||||||
iban = data['applicant_iban']
|
# sys.exit(1)
|
||||||
name = data['applicant_name']
|
|
||||||
description = data['purpose']
|
|
||||||
|
|
||||||
if latest and data['date'] < latest.date:
|
new.append(Transaction(date=date, name=name, iban=iban, amount=amount, description=description))
|
||||||
print("Found transaction older than then oldest transction in the DB. Aborting")
|
print(".", end="", flush=True)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
t = Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
elif filetype == "bunq-csv":
|
||||||
session.add(t)
|
with open(click.format_filename(filename)) as fh:
|
||||||
session.commit()
|
fh.readline() # Get rid of first line
|
||||||
count += 1
|
csv_reader = csv.reader(fh, delimiter=";")
|
||||||
print(".", end="", flush=True)
|
count = 0
|
||||||
print()
|
for line in csv_reader:
|
||||||
print(f"Imported {count} transactions")
|
date = datetime.strptime(line[0], "%Y-%m-%d")
|
||||||
|
amount = int(float(line[2].replace(",", "")) * 100)
|
||||||
|
iban = line[4]
|
||||||
|
name = line[5]
|
||||||
|
description = line[6]
|
||||||
|
|
||||||
|
# if latest and date < latest.date:
|
||||||
|
# print("Found transaction older than then oldest transction in the DB. Aborting")
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
new.append(Transaction(date=date, name=name, iban=iban, amount=amount, description=description))
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
|
||||||
|
elif filetype == "dkb":
|
||||||
|
with open(click.format_filename(filename), 'r', encoding='utf8') as fh:
|
||||||
|
fh.readline() # Get rid of first line
|
||||||
|
csv_reader = csv.reader(fh, delimiter=";")
|
||||||
|
for line in csv_reader:
|
||||||
|
date = datetime.strptime(line[0], "%Y-%m-%d")
|
||||||
|
amount = line[5]
|
||||||
|
iban = line[4]
|
||||||
|
name = line[3]
|
||||||
|
description = line[4]
|
||||||
|
|
||||||
|
# if latest and date < latest.date:
|
||||||
|
# print("Found transaction older than then oldest transction in the DB. Aborting")
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
new.append(Transaction(date=date, name=name, iban=iban, amount=amount, description=description))
|
||||||
|
print(".", end="", flush=True)
|
||||||
|
|
||||||
|
session.bulk_save_objects(new)
|
||||||
|
session.commit()
|
||||||
|
print(f"Imported {len(new)} transactions")
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Transaction(Base):
|
||||||
return self.amount > 0
|
return self.amount > 0
|
||||||
|
|
||||||
def pretty_amount(self):
|
def pretty_amount(self):
|
||||||
return "{0:.2f}".format(self.amount/100)
|
return "{0:.2f}".format(self.amount / 100)
|
||||||
|
|
||||||
def get_date(self, format):
|
def get_date(self, format):
|
||||||
if format == "iso":
|
if format == "iso":
|
||||||
|
@ -31,7 +31,6 @@ class Transaction(Base):
|
||||||
return "UNKNOWN FORMAT"
|
return "UNKNOWN FORMAT"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Category(Base):
|
class Category(Base):
|
||||||
__tablename__ = "Category"
|
__tablename__ = "Category"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
|
52
serve.py
52
serve.py
|
@ -1,11 +1,13 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
from bottle import route, run, template, redirect
|
from bottle import route, run, template, redirect, request, post
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from models import *
|
from models import Category, Transaction
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
engine = create_engine("sqlite:///app.db")
|
engine = create_engine("sqlite:///app.db")
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
@ -13,41 +15,59 @@ Session = sessionmaker(bind=engine)
|
||||||
session = Session()
|
session = Session()
|
||||||
|
|
||||||
|
|
||||||
@route('/')
|
@route("/")
|
||||||
def index():
|
def index():
|
||||||
return template('index')
|
return template("index")
|
||||||
|
|
||||||
@route('/categories')
|
|
||||||
|
@route("/categories")
|
||||||
def categories():
|
def categories():
|
||||||
categories = session.query(Category).all()
|
categories = session.query(Category).all()
|
||||||
categories = [c for c in categories if c.is_child()]
|
categories = [c for c in categories if c.is_child()]
|
||||||
return template('categories', categories=categories)
|
return template("categories", categories=categories)
|
||||||
|
|
||||||
@route('/category/<name>')
|
|
||||||
|
@route("/category/<name>")
|
||||||
def category(name):
|
def category(name):
|
||||||
c = session.query(Category).filter(Category.name==name).first()
|
c = session.query(Category).filter(Category.name == name).first()
|
||||||
if c:
|
if c:
|
||||||
transactions = session.query(Transaction).filter(Transaction.category_id==c.id).all()
|
transactions = session.query(Transaction).filter(Transaction.category_id == c.id).all()
|
||||||
else:
|
else:
|
||||||
transactions = []
|
transactions = []
|
||||||
|
|
||||||
return template('category', category=c,transactions=transactions)
|
return template("category", category=c, transactions=transactions)
|
||||||
|
|
||||||
@route('/category/<name>/delete')
|
|
||||||
|
@route("/category/<name>/delete")
|
||||||
def category(name):
|
def category(name):
|
||||||
c = session.query(Category).filter(Category.name==name).first()
|
c = session.query(Category).filter(Category.name == name).first()
|
||||||
if c:
|
if c:
|
||||||
session.delete(c)
|
session.delete(c)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
return redirect("/categories")
|
return redirect("/categories")
|
||||||
|
|
||||||
@route('/transactions')
|
|
||||||
|
@route("/transactions")
|
||||||
def transactions():
|
def transactions():
|
||||||
transactions = session.query(Transaction).all()
|
transactions = session.query(Transaction).all()
|
||||||
return template('transactions', transactions=transactions)
|
categories = session.query(Category).all()
|
||||||
|
childs = [c for c in categories if c.is_child()]
|
||||||
|
return template("transactions", transactions=transactions, categories=childs)
|
||||||
|
|
||||||
|
|
||||||
@click.command(name='serve')
|
@post("/bulksort")
|
||||||
|
def buksort():
|
||||||
|
cid = request.forms.get("category")
|
||||||
|
transaction_ids = request.forms.getall("transaction")
|
||||||
|
for id in transaction_ids:
|
||||||
|
t = session.query(Transaction).get(id)
|
||||||
|
t.category__id = cid
|
||||||
|
session.add(t)
|
||||||
|
print(f"Sorted {len(transaction_ids)} transactions into category {cid}")
|
||||||
|
session.commit()
|
||||||
|
return redirect("/transactions")
|
||||||
|
|
||||||
|
@click.command(name="serve")
|
||||||
def command():
|
def command():
|
||||||
run(host='localhost', port=8080, reloader=True,debug=True)
|
run(host="localhost", port=8080, reloader=True, debug=True)
|
||||||
|
|
4
sort.py
4
sort.py
|
@ -11,7 +11,7 @@ from prompt_toolkit.completion import FuzzyWordCompleter
|
||||||
from prompt_toolkit.shortcuts import prompt
|
from prompt_toolkit.shortcuts import prompt
|
||||||
|
|
||||||
|
|
||||||
@click.command(name='sort')
|
@click.command(name="sort")
|
||||||
def command():
|
def command():
|
||||||
engine = create_engine("sqlite:///app.db")
|
engine = create_engine("sqlite:///app.db")
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
@ -45,5 +45,3 @@ def command():
|
||||||
add_category(select)
|
add_category(select)
|
||||||
|
|
||||||
print("-" * 20)
|
print("-" * 20)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
% rebase("base.tpl")
|
% rebase("base.tpl")
|
||||||
|
|
||||||
|
<form action="/bulksort" method="POST">
|
||||||
|
<select name="category">
|
||||||
|
% for c in categories:
|
||||||
|
<option value="{{ c.id }}">{{ c.full_name() }}</option>
|
||||||
|
% end
|
||||||
|
</select>
|
||||||
|
<input type="submit">
|
||||||
<table class="table-auto">
|
<table class="table-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class="px-4 py-2"></th>
|
||||||
<th class="px-4 py-2">Date</th>
|
<th class="px-4 py-2">Date</th>
|
||||||
<th class="px-4 py-2">Sender/Empfänger</th>
|
<th class="px-4 py-2">Sender/Empfänger</th>
|
||||||
<th class="px-4 py-2">Verwendungszweck</th>
|
<th class="px-4 py-2">Verwendungszweck</th>
|
||||||
|
@ -13,6 +21,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
% for t in transactions:
|
% for t in transactions:
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="border px-4 py-2"><input type="checkbox" name="transaction" value="{{ t.id }}"></td>
|
||||||
<td class="border px-4 py-2">{{ t.get_date("de") }}</td>
|
<td class="border px-4 py-2">{{ t.get_date("de") }}</td>
|
||||||
<td class="border px-4 py-2">{{ t.name }}</td>
|
<td class="border px-4 py-2">{{ t.name }}</td>
|
||||||
<td class="border px-4 py-2">{{ t.description }}</td>
|
<td class="border px-4 py-2">{{ t.description }}</td>
|
||||||
|
@ -26,3 +35,5 @@
|
||||||
% end
|
% end
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
Loading…
Add table
Reference in a new issue