# -*- 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',
F.req + F.lenOf(SingleExpense.description))
category = F.QuerySelectField('Kategorie',
get_label='name',
get_pk=lambda c: c.id)
def __init__(self, obj = None):
super().__init__(obj = obj)
self.category.query = Category.of(current_user).order_by(Category.name)
#
# 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 = "%s" % (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('//')
@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('/', endpoint = 'show_date_str', build_only = True)
@mod.route('/edit/', 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()
if request.method == 'GET' and 'date' in request.args:
form.date.data = parse_date(request.args['date'])
if form.is_submitted() and not form.description.data.strip():
form.description.raw_data = form.description.data = form.category.data.name
if form.validate_on_submit():
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 }