Initial commit

This commit is contained in:
Felix Baumann
2026-06-27 17:07:34 +00:00
commit fb47c53d52
256 changed files with 21270 additions and 0 deletions
+331
View File
@@ -0,0 +1,331 @@
from django.contrib.auth.decorators import login_required
from accounts.decorators import permission_required
from django.shortcuts import render, get_object_or_404, redirect
from django.db.models.functions import TruncMonth, TruncYear, TruncWeek, TruncDay
from django.db.models import Sum
from datetime import date, datetime, timedelta
from projects.models import ProjectMembership
from .models import Income, Expense
import csv
from django.http import HttpResponse
from calendar import monthrange
from .forms import IncomeForm, ExpenseForm
from .tables import PeriodAnalyticsTable, FinanceTransactionTable
# =========================================================
# OVERVIEW
# =========================================================
@login_required
@permission_required("projects_finance_view")
def finance_overview(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
# -------------------------
# DATE FILTER (for KPI ONLY)
# -------------------------
start = request.GET.get("start")
end = request.GET.get("end")
if not start or not end:
start_date = end_date = date.today()
else:
start_date = datetime.strptime(start, "%Y-%m-%d").date()
end_date = datetime.strptime(end, "%Y-%m-%d").date()
# -------------------------
# KPI DATA (FILTERED)
# -------------------------
filtered_incomes = Income.objects.filter(
project=project,
date__date__range=[start_date, end_date]
)
filtered_expenses = Expense.objects.filter(
project=project,
date__date__range=[start_date, end_date]
)
total_income = sum(i.amount for i in filtered_incomes)
total_expense = sum(e.amount for e in filtered_expenses)
total_profit = total_income - total_expense
incomes = Income.objects.filter(
project=project,
date__date__range=[start_date, end_date]
).order_by("-date")
expenses = Expense.objects.filter(
project=project,
date__date__range=[start_date, end_date]
).order_by("-date")
# -------------------------
# PERIOD VIEW
# -------------------------
period = request.GET.get("period", "year")
if period == "year":
trunc = TruncYear("date")
elif period == "week":
trunc = TruncWeek("date")
elif period == "day":
trunc = TruncDay("date")
else:
trunc = TruncMonth("date")
income_stats = (
Income.objects.filter(project=project)
.annotate(period_key=trunc)
.values("period_key")
.annotate(total=Sum("amount"))
.order_by("-period_key")
)
expense_stats = (
Expense.objects.filter(project=project)
.annotate(period_key=trunc)
.values("period_key")
.annotate(total=Sum("amount"))
.order_by("-period_key")
)
expense_map = {
str(e["period_key"]): e["total"]
for e in expense_stats
}
period_data = []
for i in income_stats:
period_start = i["period_key"]
income_total = i["total"] or 0
expense_total = expense_map.get(str(period_start), 0)
if period == "year":
period_end = period_start.replace(month=12, day=31)
elif period == "week":
period_end = period_start + timedelta(days=6)
elif period == "day":
period_end = period_start
else:
last_day = monthrange(period_start.year, period_start.month)[1]
period_end = period_start.replace(day=last_day)
period_data.append({
"period": period_start,
"period_start": period_start,
"period_end": period_end,
"income": income_total,
"expense": expense_total,
"profit": income_total - expense_total,
})
# -------------------------
# RETURN
# -------------------------
table = PeriodAnalyticsTable(
period_data,
period_type=period,
request=request,
)
return render(request, "finance/overview.html", {
"project": project,
# KPI (filtered)
"total_income": total_income,
"total_expense": total_expense,
"total_profit": total_profit,
# LISTS (all data)
"incomes": incomes,
"expenses": expenses,
# PERIOD
"period_data": period_data,
"period": period,
# UI
"start_date": start_date,
"end_date": end_date,
"table": table,
})
@login_required
@permission_required("projects_finance_view")
def finance_view(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
start = request.GET.get("start")
end = request.GET.get("end")
incomes = Income.objects.filter(project=project)
expenses = Expense.objects.filter(project=project)
if start:
try:
start_date = datetime.strptime(start, "%Y-%m-%d").date()
incomes = incomes.filter(date__date__gte=start_date)
expenses = expenses.filter(date__date__gte=start_date)
except ValueError:
pass
if end:
try:
end_date = datetime.strptime(end, "%Y-%m-%d").date()
incomes = incomes.filter(date__date__lte=end_date)
expenses = expenses.filter(date__date__lte=end_date)
except ValueError:
pass
# -------------------------
# MERGED LIST (LIKE EXPORT ORDER)
# -------------------------
transactions = []
for i in incomes:
transactions.append({
"type": "Einnahme",
"date": i.date,
"amount": i.amount,
"category": i.category,
"name": i.name,
"created_by": i.created_by,
"note": i.note,
})
for e in expenses:
transactions.append({
"type": "Ausgabe",
"date": e.date,
"amount": e.amount,
"category": e.category,
"name": e.name,
"created_by": e.created_by,
"note": e.note,
"supplier": e.supplier,
"payment_method": e.payment_method,
"receipt": e.receipt,
})
transactions.sort(
key=lambda x: x["date"],
reverse=True,
)
table = FinanceTransactionTable(
transactions,
request=request,
)
return render(
request,
"finance/view.html",
{
"project": project,
"table": table,
"start_date": start_date,
"end_date": end_date,
},
)
# =========================
# INCOME CREATE
# =========================
@login_required
@permission_required("projects_finance_income_add")
def add_income(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method == "POST":
form = IncomeForm(request.POST)
if form.is_valid():
income = form.save(commit=False)
income.project = project
income.created_by = request.user
income.save()
return redirect("finance:overview", project_id=project.id)
else:
form = IncomeForm()
return render(request, "finance/form.html", {
"form": form,
"project": project,
"type": "income",
})
# =========================
# EXPENSE CREATE
# =========================
@login_required
@permission_required("projects_finance_expense_add")
def add_expense(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method == "POST":
form = ExpenseForm(request.POST, request.FILES)
if form.is_valid():
expense = form.save(commit=False)
expense.project = project
expense.created_by = request.user
expense.save()
return redirect("finance:overview", project_id=project.id)
else:
form = ExpenseForm()
return render(request, "finance/form.html", {
"form": form,
"project": project,
"type": "expense",
})