diff options
Diffstat (limited to 'kosten/app/views/expenses.py')
-rw-r--r-- | kosten/app/views/expenses.py | 224 |
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 } |