Lot of fixing. Graphs on index page. Web importer
This commit is contained in:
parent
7a236a10e5
commit
e88acbeb13
12 changed files with 263 additions and 47 deletions
|
@ -8,8 +8,6 @@
|
||||||
- Übersicht auf Startseite
|
- Übersicht auf Startseite
|
||||||
- Monatsübersicht dieser/letzter Monat
|
- Monatsübersicht dieser/letzter Monat
|
||||||
- Anzahl ungetaggte Transaktionen
|
- Anzahl ungetaggte Transaktionen
|
||||||
- Importer über Webseite
|
|
||||||
- setup.py
|
|
||||||
- Doku
|
- Doku
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ def command(profile, dry_run, verbose):
|
||||||
|
|
||||||
|
|
||||||
def apply_rules(t, rules):
|
def apply_rules(t, rules):
|
||||||
|
tags = []
|
||||||
for r in rules:
|
for r in rules:
|
||||||
iban = r.get("iban")
|
iban = r.get("iban")
|
||||||
name_regex = r.get("name")
|
name_regex = r.get("name")
|
||||||
|
@ -63,4 +64,6 @@ def apply_rules(t, rules):
|
||||||
if not re.search(desc_regex, t.description):
|
if not re.search(desc_regex, t.description):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return r["tags"].split(",")
|
tags.extend(r["tags"].split(","))
|
||||||
|
|
||||||
|
return tags
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
import mt940
|
import mt940
|
||||||
import csv
|
import csv
|
||||||
from schmeckels.models import Transaction
|
from schmeckels import models
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
SUPPORTED_BANKS = (
|
||||||
|
("dkb", "DKB"),
|
||||||
|
("sparkasse-mt940", "Sparkasse MT940"),
|
||||||
|
("bunq-csv", "Bunq CSV"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Bank(object):
|
class Bank(object):
|
||||||
"""
|
"""
|
||||||
|
@ -38,7 +44,7 @@ class Sparkasse_MT940(Bank):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_transactions(self):
|
def get_transactions(self):
|
||||||
transactions = mt940.parse(click.format_filename(filename))
|
transactions = mt940.parse(self.filename)
|
||||||
for t in transactions:
|
for t in transactions:
|
||||||
data = t.data
|
data = t.data
|
||||||
|
|
||||||
|
@ -47,7 +53,7 @@ class Sparkasse_MT940(Bank):
|
||||||
iban = data["applicant_iban"]
|
iban = data["applicant_iban"]
|
||||||
name = data["applicant_name"]
|
name = data["applicant_name"]
|
||||||
description = data["purpose"]
|
description = data["purpose"]
|
||||||
yield Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
yield models.Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
||||||
|
|
||||||
|
|
||||||
class Bunq(Bank):
|
class Bunq(Bank):
|
||||||
|
@ -66,7 +72,7 @@ class Bunq(Bank):
|
||||||
name = line[5]
|
name = line[5]
|
||||||
description = line[6]
|
description = line[6]
|
||||||
|
|
||||||
yield Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
yield models.Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
||||||
|
|
||||||
|
|
||||||
class DKB(Bank):
|
class DKB(Bank):
|
||||||
|
@ -75,7 +81,7 @@ class DKB(Bank):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_transactions(self):
|
def get_transactions(self):
|
||||||
with open(filename, encoding="ISO-8859-1") as fh:
|
with open(self.filename, encoding="ISO-8859-1") as fh:
|
||||||
csv_reader = csv.reader(fh, delimiter=";")
|
csv_reader = csv.reader(fh, delimiter=";")
|
||||||
next(csv_reader, None)
|
next(csv_reader, None)
|
||||||
for line in csv_reader:
|
for line in csv_reader:
|
||||||
|
@ -84,4 +90,4 @@ class DKB(Bank):
|
||||||
iban = line[5]
|
iban = line[5]
|
||||||
name = line[3]
|
name = line[3]
|
||||||
description = line[4]
|
description = line[4]
|
||||||
yield Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
yield models.Transaction(date=date, name=name, iban=iban, amount=amount, description=description)
|
||||||
|
|
|
@ -5,7 +5,7 @@ import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
from schmeckels import importer, serve, autosort, validate, info, sort, models #,report
|
from schmeckels import importer, serve, autosort, validate, info, sort, models # ,report
|
||||||
|
|
||||||
from schmeckels.helper import build_database_filename, create_dirs, build_rules_filename
|
from schmeckels.helper import build_database_filename, create_dirs, build_rules_filename
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
@ -15,6 +15,7 @@ from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def cli():
|
def cli():
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -8,6 +8,7 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
import schmeckels.models
|
import schmeckels.models
|
||||||
import locale
|
import locale
|
||||||
|
from schmeckels import banks, models
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader
|
from yaml import CLoader as Loader
|
||||||
|
@ -45,7 +46,7 @@ def check_single_profile():
|
||||||
if len(files) == 1:
|
if len(files) == 1:
|
||||||
return files[0].split(".")[0]
|
return files[0].split(".")[0]
|
||||||
else:
|
else:
|
||||||
print("--profile is required when you have more than one database.")
|
print("--profile is required when you have more than one profile.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,7 +89,35 @@ def get_rules(profile_name):
|
||||||
def create_tag(name, profile, session):
|
def create_tag(name, profile, session):
|
||||||
t = session.query(models.Tag).filter(models.Tag.name == name).first()
|
t = session.query(models.Tag).filter(models.Tag.name == name).first()
|
||||||
if not t:
|
if not t:
|
||||||
|
print(name)
|
||||||
t = models.Tag(name=name)
|
t = models.Tag(name=name)
|
||||||
session.add(t)
|
session.add(t)
|
||||||
session.commit()
|
session.commit()
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def import_transactions(session, filetype, filename):
|
||||||
|
if filetype not in [b[0] for b in banks.SUPPORTED_BANKS]:
|
||||||
|
raise ValueError("Unsupported filetype")
|
||||||
|
|
||||||
|
new = []
|
||||||
|
|
||||||
|
if filetype == "sparkasse-mt940":
|
||||||
|
bank = banks.Sparkasse_MT940(filename)
|
||||||
|
elif filetype == "bunq-csv":
|
||||||
|
bank = banks.Bunq(filename)
|
||||||
|
elif filetype == "dkb":
|
||||||
|
bank = banks.DKB(filename)
|
||||||
|
|
||||||
|
for t in bank.get_transactions():
|
||||||
|
if (
|
||||||
|
not session.query(models.Transaction)
|
||||||
|
.filter_by(iban=t.iban)
|
||||||
|
.filter_by(amount=t.amount)
|
||||||
|
.filter_by(date=t.date)
|
||||||
|
.filter_by(description=t.description)
|
||||||
|
.first()
|
||||||
|
):
|
||||||
|
new.append(t)
|
||||||
|
|
||||||
|
return new
|
||||||
|
|
|
@ -3,38 +3,25 @@ import click
|
||||||
from sqlalchemy import create_engine, desc
|
from sqlalchemy import create_engine, desc
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from schmeckels.models import Transaction
|
from schmeckels.models import Transaction
|
||||||
from schmeckels.helper import get_session
|
from schmeckels.helper import get_session, import_transactions
|
||||||
import sys
|
import sys
|
||||||
from schmeckels import banks
|
from schmeckels import banks
|
||||||
|
import colorful as cf
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="import")
|
@click.command(name="import")
|
||||||
@click.option("--filetype", "-t", type=click.Choice(["dkb", "sparkasse-mt940", "bunq-csv"], case_sensitive=False))
|
@click.option("--filetype", "-t", type=click.Choice([b[0] for b in banks.SUPPORTED_BANKS], case_sensitive=False))
|
||||||
@click.option("--profile", "-p")
|
@click.option("--profile", "-p")
|
||||||
@click.option("--force", "-f", default=False, is_flag=True)
|
@click.option("--dry-run", default=False, is_flag=True)
|
||||||
@click.argument("filename", type=click.Path(exists=True))
|
@click.argument("filename", type=click.Path(exists=True))
|
||||||
def command(filename, profile, force, filetype):
|
def command(filetype, profile, dry_run, filename):
|
||||||
session = get_session(profile)
|
session = get_session(profile)
|
||||||
|
new = import_transactions(session, filetype, filename)
|
||||||
|
|
||||||
latest = session.query(Transaction).order_by(desc(Transaction.date)).first()
|
if dry_run:
|
||||||
new = []
|
print(cf.yellow(f"Would have imported {len(new)} new transactions"))
|
||||||
|
else:
|
||||||
if filetype == "sparkasse-mt940":
|
if len(new) > 0:
|
||||||
bank = banks.Sparkasse_MT940(filename)
|
session.bulk_save_objects(new)
|
||||||
elif filetype == "bunq-csv":
|
session.commit()
|
||||||
bank = banks.Bunq(filename)
|
print(cf.green(f"Imported {len(new)} new transactions"))
|
||||||
elif filetype == "dkb":
|
|
||||||
bank = banks.DKB(filename)
|
|
||||||
|
|
||||||
for t in bank.get_transactions():
|
|
||||||
if not force and latest and t.date < latest.date:
|
|
||||||
print("Found a transaction older than then newest transction in the DB.")
|
|
||||||
print("If you are sure that you want to import this file, use --force")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
new.append(t)
|
|
||||||
print(".", end="", flush=True)
|
|
||||||
|
|
||||||
session.bulk_save_objects(new)
|
|
||||||
session.commit()
|
|
||||||
print(f"Imported {len(new)} transactions")
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Table, Boolean
|
||||||
from sqlalchemy.orm import relationship, backref
|
from sqlalchemy.orm import relationship, backref
|
||||||
from schmeckels.helper import format_amount
|
from schmeckels import helper
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
@ -32,7 +32,7 @@ class Transaction(Base):
|
||||||
return self.amount > 0
|
return self.amount > 0
|
||||||
|
|
||||||
def pretty_amount(self):
|
def pretty_amount(self):
|
||||||
return format_amount(self.amount)
|
return helper.format_amount(self.amount)
|
||||||
|
|
||||||
def get_date(self, format):
|
def get_date(self, format):
|
||||||
if format == "iso":
|
if format == "iso":
|
||||||
|
@ -47,12 +47,16 @@ class Tag(Base):
|
||||||
__tablename__ = "Tag"
|
__tablename__ = "Tag"
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
name = Column(String, unique=True)
|
name = Column(String, unique=True)
|
||||||
|
reporting = Column(Boolean, default=True)
|
||||||
description = Column(String)
|
description = Column(String)
|
||||||
transactions = relationship("Transaction", secondary=association_table, back_populates="tags")
|
transactions = relationship("Transaction", secondary=association_table, back_populates="tags")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Tag {self.name}>"
|
return f"<Tag {self.name}>"
|
||||||
|
|
||||||
|
def toggle_reporting(self):
|
||||||
|
self.reporting = not self.reporting
|
||||||
|
|
||||||
def color(self):
|
def color(self):
|
||||||
hash = md5(self.name.encode()).hexdigest()[-6:]
|
hash = md5(self.name.encode()).hexdigest()[-6:]
|
||||||
return f"#{hash}"
|
return f"#{hash}"
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
from flask import Flask, render_template, request, redirect
|
from flask import Flask, render_template, request, redirect, flash
|
||||||
from schmeckels.helper import get_session, format_amount
|
from schmeckels.helper import get_session, format_amount
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta, date
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, and_
|
||||||
import click
|
import click
|
||||||
|
import calendar
|
||||||
from schmeckels.models import Transaction, Tag
|
from schmeckels.models import Transaction, Tag
|
||||||
|
from schmeckels.helper import import_transactions
|
||||||
|
from schmeckels import banks
|
||||||
|
|
||||||
session = None
|
session = None
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -12,7 +15,37 @@ app = Flask(__name__)
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
tag_sum_in = {}
|
||||||
|
tag_sum_out = {}
|
||||||
|
|
||||||
|
# Get a list of all tags
|
||||||
|
tags = session.query(Tag).all()
|
||||||
|
|
||||||
|
# Get start and end of last month
|
||||||
|
now = datetime.now()
|
||||||
|
last_month = now - timedelta(days=now.day)
|
||||||
|
num_days = calendar.monthrange(last_month.year, 2)[1]
|
||||||
|
start_date = date(last_month.year, 2, 1)
|
||||||
|
end_date = date(last_month.year, 2, num_days)
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
tag_sum = (
|
||||||
|
session.query(func.sum(Transaction.amount))
|
||||||
|
.filter(and_(Transaction.date >= start_date, Transaction.date <= end_date))
|
||||||
|
.filter(Transaction.tags.any(id=tag.id))
|
||||||
|
.first()[0]
|
||||||
|
)
|
||||||
|
if tag_sum:
|
||||||
|
if tag_sum >= 0:
|
||||||
|
tag_sum_in[tag] = tag_sum
|
||||||
|
else:
|
||||||
|
tag_sum_out[tag] = tag_sum
|
||||||
|
|
||||||
|
tag_sum_in = {
|
||||||
|
k: format_amount(v) for k, v in sorted(tag_sum_in.items(), key=lambda item: float(item[1]), reverse=True)
|
||||||
|
}
|
||||||
|
tag_sum_out = {k: format_amount(v) for k, v in sorted(tag_sum_out.items(), key=lambda item: float(item[1]))}
|
||||||
|
return render_template("index.html", tag_sum_in=tag_sum_in, tag_sum_out=tag_sum_out, date=start_date)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/tags")
|
@app.route("/tags")
|
||||||
|
@ -39,6 +72,17 @@ def delete_tag(name):
|
||||||
return redirect("/tags")
|
return redirect("/tags")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/tag/<name>/toggle")
|
||||||
|
def toggle_tag(name):
|
||||||
|
x = session.query(Tag).filter(Tag.name == name).first()
|
||||||
|
if x:
|
||||||
|
x.toggle_reporting()
|
||||||
|
session.add(x)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return redirect("/tags")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/transactions")
|
@app.route("/transactions")
|
||||||
def transactions():
|
def transactions():
|
||||||
transactions = session.query(Transaction)
|
transactions = session.query(Transaction)
|
||||||
|
@ -67,9 +111,32 @@ def buksort():
|
||||||
return redirect("/transactions")
|
return redirect("/transactions")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/upload", methods=["GET", "POST"])
|
||||||
|
def upload():
|
||||||
|
global session
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
file = request.files.get("file")
|
||||||
|
if not file or file.filename == "":
|
||||||
|
flash("No file choosen")
|
||||||
|
return redirect(request.url)
|
||||||
|
|
||||||
|
path = f"/tmp/{file.filename}"
|
||||||
|
file.save(path)
|
||||||
|
new = import_transactions(session, request.form["bank"], path)
|
||||||
|
if len(new) > 0:
|
||||||
|
session.bulk_save_objects(new)
|
||||||
|
session.commit()
|
||||||
|
flash(f"Imported {len(new)} new transactions")
|
||||||
|
return render_template("upload.html", banks=banks.SUPPORTED_BANKS, transactions=new)
|
||||||
|
|
||||||
|
return render_template("upload.html", banks=banks.SUPPORTED_BANKS)
|
||||||
|
|
||||||
|
|
||||||
@click.command(name="serve")
|
@click.command(name="serve")
|
||||||
@click.option("--profile", "-p")
|
@click.option("--profile", "-p")
|
||||||
def command(profile):
|
def command(profile):
|
||||||
global session
|
global session
|
||||||
session = get_session(profile)
|
session = get_session(profile)
|
||||||
|
app.secret_key = "WeDon'tHaveSessionsSoThisMustNotBeVerySecure!"
|
||||||
app.run(host="0.0.0.0", port=8080)
|
app.run(host="0.0.0.0", port=8080)
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<title>Schmeckels</title>
|
<title>Schmeckels</title>
|
||||||
<link href="https://unpkg.com/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
|
<link href="https://unpkg.com/tailwindcss/dist/tailwind.min.css" rel="stylesheet">
|
||||||
<link href="https://unpkg.com/@tailwindcss/custom-forms/dist/custom-forms.min.css" rel="stylesheet">
|
|
||||||
<link href="/static/style.css" rel="stylesheet">
|
<link href="/static/style.css" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
@ -26,13 +25,27 @@
|
||||||
<li class="mx-4">
|
<li class="mx-4">
|
||||||
<a class="text-blue-500 hover:text-blue-800" href="/tags">Tags</a>
|
<a class="text-blue-500 hover:text-blue-800" href="/tags">Tags</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="mx-4">
|
||||||
|
<a class="text-blue-500 hover:text-blue-800" href="/upload">Upload</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="pt-2 container mx-auto">
|
<div class="pt-2 container mx-auto">
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="bg-blue-100 border-t border-b border-blue-500 text-blue-700 px-2 py-2 mb-5" role="alert">
|
||||||
|
<p class="font-bold">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
{% block main %} {% endblock %}
|
{% block main %} {% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js'></script>
|
||||||
|
{% block script %} {% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,7 +1,84 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<h1 class="text-2xl font-bold text-indigo-500">Start</h1>
|
<h1 class="text-2xl font-bold text-indigo-500">Overview for {{ date.strftime("%B %Y") }} </h1>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-bold text-green-500">Incoming</h2>
|
||||||
|
<div style="display:flex; flex-direction: row;">
|
||||||
|
<div>
|
||||||
|
{% if tag_sum_in %}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th>Sum</th>
|
||||||
|
</tr>
|
||||||
|
{% for tag,sum in tag_sum_in.items() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ tag.name }}</td>
|
||||||
|
<td class="text-right">{{ sum }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
No tagged transactions for this month.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="chart_in" width="600" height="400"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h2 class="text-xl font-bold text-red-500">Outgoing</h2>
|
||||||
|
<div style="display:flex; flex-direction: row;">
|
||||||
|
<div>
|
||||||
|
{% if tag_sum_out %}
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Tag</th>
|
||||||
|
<th>Sum</th>
|
||||||
|
</tr>
|
||||||
|
{% for tag,sum in tag_sum_out.items() %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ tag.name }}</td>
|
||||||
|
<td class="text-right">{{ sum }} €</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
No tagged transactions for this month.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<canvas id="chart_out" width="600" height="400"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
var pieData_in = [
|
||||||
|
{% for tag, sum in tag_sum_in.items() %}
|
||||||
|
{
|
||||||
|
value: "{{sum}}".replace(",", ""),
|
||||||
|
label: "{{ tag.name }}",
|
||||||
|
color : "{{ tag.color() }}"
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
new Chart(document.getElementById("chart_in").getContext("2d")).Pie(pieData_in);
|
||||||
|
|
||||||
|
var pieData_out = [
|
||||||
|
{% for tag, sum in tag_sum_out.items() %}
|
||||||
|
{
|
||||||
|
value: "{{sum}}".replace(",", ""),
|
||||||
|
label: "{{ tag.name }}",
|
||||||
|
color : "{{ tag.color() }}"
|
||||||
|
},
|
||||||
|
{% endfor %}
|
||||||
|
];
|
||||||
|
new Chart(document.getElementById("chart_out").getContext("2d")).Pie(pieData_out);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -7,13 +7,20 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% for tag in tags %}
|
{% for tag in tags %}
|
||||||
<li class="p-2">
|
<li class="p-2">
|
||||||
<span style="background-color: {{ tag.color() }};" class="text-white rounded p-1" >{{ tag.name }}</span>
|
<span style="background-color: {{ tag.color() }};" class="text-white rounded p-1">{{ tag.name }}</span>
|
||||||
<a class="float-right" href="/tag/{{ tag.name }}">
|
<a class="float-right" href="/tag/{{ tag.name }}">
|
||||||
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 y-1 px-2 rounded"> Transactions </button>
|
<button class="bg-blue-500 hover:bg-blue-700 text-white font-bold mx-2 y-1 px-2 rounded"> Transactions </button>
|
||||||
</a>
|
</a>
|
||||||
<a class="float-right" href="/tag/{{ tag.name }}/delete">
|
<a class="float-right" href="/tag/{{ tag.name }}/delete">
|
||||||
<button class="bg-red-500 hover:bg-red-700 text-white font-bold y-1 px-2 rounded"> Delete </button>
|
<button class="bg-red-500 hover:bg-red-700 text-white font-bold y-1 px-2 rounded"> Delete </button>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="float-right" href="/tag/{{ tag.name }}/toggle">
|
||||||
|
{% if tag.reporting %}
|
||||||
|
<button class="bg-red-500 hover:bg-red-700 text-white font-bold y-1 px-2 rounded"> Remove from report </button>
|
||||||
|
{% else %}
|
||||||
|
<button class="bg-green-500 hover:bg-green-700 text-white font-bold y-1 px-2 rounded"> Add to reporting </button>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
24
schmeckels/templates/upload.html
Normal file
24
schmeckels/templates/upload.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<form method="POST" enctype="multipart/form-data" class="max-w-xs">
|
||||||
|
<p class="bg-blue-100 p-2 mb-4">
|
||||||
|
<label class="block" for="bank">Choose your filetype</label>
|
||||||
|
<select id="bank" name="bank">
|
||||||
|
{% for bank in banks %}
|
||||||
|
<option value="{{bank[0]}}"> {{ bank[1] }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="bg-blue-100 p-2 mb-4">
|
||||||
|
<label class="block" for="file">Choose your file</label>
|
||||||
|
<input name="file" type="file"></input>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mt-4 text-center">
|
||||||
|
<button class="bg-green-400 p-1" type="submit">Upload</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue