summaryrefslogtreecommitdiff
path: root/kosten/app/views/expenses.py
diff options
context:
space:
mode:
Diffstat (limited to 'kosten/app/views/expenses.py')
-rw-r--r--kosten/app/views/expenses.py224
1 files changed, 224 insertions, 0 deletions
diff --git a/kosten/app/views/expenses.py b/kosten/app/views/expenses.py
new file mode 100644
index 0000000..90c8ffd
--- /dev/null
+++ b/kosten/app/views/expenses.py
@@ -0,0 +1,224 @@
+# -*- coding: utf-8 -*-
+from . import Blueprint, flash, db, \
+ current_user, login_required, \
+ assert_authorisation, templated, redirect, request, url_for, today
+
+from flask import Markup
+
+from ..model import Category, SingleExpense, CatExpense, MonthExpense
+from .. import forms as F
+
+import datetime
+from sqlalchemy import func
+from functools import partial
+
+assert_authorisation = partial(assert_authorisation, SingleExpense.get)
+mod = Blueprint('expenses', __name__)
+
+#
+# Form
+#
+class ExpenseForm(F.Form):
+ date = F.DateField('Datum', F.req,
+ default=lambda: today())
+
+ expense = F.DecimalField('Betrag', F.req,
+ description='EUR',
+ places=2)
+
+ description = F.StringField('Beschreibung')
+
+ category = F.QuerySelectField('Kategorie',
+ get_label='name',
+ get_pk=lambda c: c.id)
+
+ def __init__(self, obj = None, description_req = True):
+ super().__init__(obj = obj)
+ self.category.query = Category.of(current_user).order_by(Category.name)
+
+ if description_req:
+ self.description.validators.extend(F.req)
+
+#
+# Utilities
+#
+def calc_month_exp(year, month):
+ """Returns the `MonthExpense` for the given month."""
+ ssum = func.sum(SingleExpense.expense)
+ query = SingleExpense.of_month(current_user, month, year)
+
+ result = query.group_by(SingleExpense.category_id).\
+ values(SingleExpense.category_id, ssum)
+
+ exps = [CatExpense(Category.get(c), s, query.filter(SingleExpense.category_id == c)) for c,s in result]
+
+ return MonthExpense(current_user, datetime.date(year, month, 1), exps)
+
+
+def pie_data(exp):
+ """Generates the dictionary needed to show the pie diagram.
+ The resulting dict is category → sum of expenses.
+ """
+ expenses = {}
+ for c in exp.catexps:
+ expenses[c.cat.name] = float(c.sum)
+
+ for c in Category.of(current_user).order_by(Category.name).all():
+ yield (c.name, expenses.get(c.name, 0.0))
+
+
+def calc_month_and_pie(year, month):
+ exp = calc_month_exp(year,month)
+ pie = pie_data(exp)
+ return (exp, dict(pie))
+
+
+def entry_flash(msg, exp):
+ """When changing/adding an entry, a message is shown."""
+ url = url_for('.edit', id = exp.id)
+ link = "<a href=\"%s\">%s</a>" % (url, exp.description)
+ flash(Markup(msg % link))
+
+DATE_FORMAT='%Y%m%d'
+def parse_date(value):
+ try:
+ dt = datetime.datetime.strptime(value, DATE_FORMAT)
+ except ValueError:
+ return today()
+ else:
+ return dt.date()
+
+def gen_date(value):
+ return value.strftime(DATE_FORMAT)
+
+#
+# Template additions
+#
+@mod.app_template_filter()
+def prev_date(exp):
+ if exp.date.month == 1:
+ return exp.date.replace(year = exp.date.year - 1, month = 12)
+ else:
+ return exp.date.replace(month = exp.date.month - 1)
+
+
+@mod.app_template_filter()
+def next_date(exp):
+ if exp.date.month == 12:
+ return exp.date.replace(year = exp.date.year + 1, month = 1)
+ else:
+ return exp.date.replace(month = exp.date.month + 1)
+
+
+@mod.app_template_test('last_date')
+def is_last(exp):
+ return exp.date >= today().replace(day = 1)
+
+#
+# Views
+#
+@mod.route('/')
+@login_required
+@templated
+def show():
+ """Show this and the last month."""
+ d = today()
+
+ first, pfirst = calc_month_and_pie(d.year, d.month)
+ if d.month == 1:
+ second, psecond = calc_month_and_pie(d.year - 1, 12)
+ else:
+ second, psecond = calc_month_and_pie(d.year, d.month - 1)
+
+ return { 'exps' : [first, second], 'pies': [pfirst, psecond] }
+
+
+@mod.route('/<int(fixed_digits=4):year>/<int(fixed_digits=2):month>')
+@login_required
+@templated('.show')
+def show_date(year, month):
+ """Show the expenses of the specified month."""
+ c,p = calc_month_and_pie(year, month)
+ return { 'exps' : [c], 'pies' : [p] }
+
+# shortcut to allow calling the above route, when year/month is a string
+mod.add_url_rule('/<path:p>', endpoint = 'show_date_str', build_only = True)
+
+
+@mod.route('/edit/<int:id>', methods=('GET', 'POST'))
+@login_required
+@assert_authorisation('id')
+@templated
+def edit(id):
+ """Edit a single expense, given by `id`."""
+ exp = SingleExpense.get(id)
+ form = ExpenseForm(exp)
+
+ if form.is_submitted():
+ if 'deleteB' in request.form:
+ db.session.delete(exp)
+
+ elif form.flash_validate(): # change
+ form.populate_obj(exp)
+
+ else:
+ return { 'form': form }
+
+ db.session.commit()
+ entry_flash("Eintrag %s geändert.", exp)
+ return redirect('index')
+
+ return { 'form': form }
+
+
+@mod.route('/add', methods=('GET', 'POST'))
+@login_required
+@templated
+def add():
+ """Add a new expense."""
+ form = ExpenseForm(description_req=False)
+
+ if request.method == 'GET' and 'date' in request.args:
+ form.date.data = parse_date(request.args['date'])
+
+ if form.validate_on_submit():
+ if not form.description.data.strip():
+ form.description.data = form.category.data.name
+
+ exp = SingleExpense()
+
+ form.populate_obj(exp)
+ exp.user = current_user
+
+ db.session.add(exp)
+ db.session.commit()
+
+ entry_flash("Neuer Eintrag %s hinzugefügt.", exp)
+
+ return redirect('.add', date = gen_date(exp.date) if exp.date != today() else None)
+
+ return { 'form': form }
+
+@mod.route('/search', methods=('POST', 'GET'))
+@login_required
+@templated
+def search():
+ try:
+ query = request.form['search'].strip()
+ except KeyError:
+ flash("Ungültige Suchanfrage")
+ return redirect('index')
+
+ if not query:
+ flash("Leere Suche")
+ return redirect('index')
+
+ exps = SingleExpense.of(current_user).filter(SingleExpense.description.ilike(query))\
+ .order_by(SingleExpense.year.desc(), SingleExpense.month, SingleExpense.day, SingleExpense.description)\
+ .all()
+
+ if not exps:
+ flash("Keine Ergebnisse")
+ return redirect('index')
+
+ return { 'exps': exps }