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
View File
+23
View File
@@ -0,0 +1,23 @@
from django.contrib import admin
from .models import Category, Supplier, PaymentMethod, Expense, Income
admin.site.register(Category)
admin.site.register(Supplier)
admin.site.register(PaymentMethod)
@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
list_display = ("amount", "project", "category", "date", "created_by")
list_filter = ("project", "category", "date")
search_fields = (
"name",
"project__name",
)
@admin.register(Income)
class IncomeAdmin(admin.ModelAdmin):
list_display = ("amount", "project", "category", "date", "created_by")
list_filter = ("project", "category", "date")
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FinanceConfig(AppConfig):
name = 'finance'
+38
View File
@@ -0,0 +1,38 @@
from django import forms
from .models import Income, Expense
from core.forms.base import BaseForm
from django.utils import timezone
class IncomeForm(BaseForm):
class Meta:
model = Income
fields = ["name", "amount", "date", "category", "payment_method", "note"]
widgets = {
"date": forms.DateTimeInput(attrs={"type": "datetime-local"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.pk:
self.fields["date"].initial = timezone.now()
self.apply_style()
class ExpenseForm(BaseForm):
class Meta:
model = Expense
fields = ["name", "amount", "date", "category", "supplier", "payment_method", "receipt", "note"]
widgets = {
"date": forms.DateTimeInput(attrs={"type": "datetime-local"}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not self.instance.pk:
self.fields["date"].initial = timezone.now()
self.apply_style()
+77
View File
@@ -0,0 +1,77 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
import finance.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
],
options={
'verbose_name': 'Finanzkategorie',
'verbose_name_plural': 'Finanzkategorien',
},
),
migrations.CreateModel(
name='Income',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, null=True, verbose_name='Name')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
('date', models.DateTimeField(verbose_name='Datum')),
('note', models.TextField(blank=True, verbose_name='Notiz')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
],
),
migrations.CreateModel(
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
options={
'verbose_name': 'Zahlungsmethode',
'verbose_name_plural': 'Zahlungsmethoden',
},
),
migrations.CreateModel(
name='Supplier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=150)),
],
options={
'verbose_name': 'Lieferant',
'verbose_name_plural': 'Lieferanten',
},
),
migrations.CreateModel(
name='Expense',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Betrag (€)')),
('date', models.DateTimeField(verbose_name='Datum')),
('receipt', models.FileField(blank=True, null=True, upload_to=finance.models.receipt_upload_path, verbose_name='Beleg')),
('note', models.TextField(blank=True, verbose_name='Notiz')),
('invoiced', models.BooleanField(default=False, verbose_name='Abgerechnet')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('category', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
],
),
]
+54
View File
@@ -0,0 +1,54 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('finance', '0001_initial'),
('projects', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='expense',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='expenses', to='projects.project', verbose_name='Projekt'),
),
migrations.AddField(
model_name='income',
name='category',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie'),
),
migrations.AddField(
model_name='income',
name='created_by',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von'),
),
migrations.AddField(
model_name='income',
name='project',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='incomes', to='projects.project', verbose_name='Projekt'),
),
migrations.AddField(
model_name='income',
name='payment_method',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.paymentmethod', verbose_name='Zahlungsmethode'),
),
migrations.AddField(
model_name='expense',
name='payment_method',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.paymentmethod', verbose_name='Zahlungsmethode'),
),
migrations.AddField(
model_name='expense',
name='supplier',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.supplier', verbose_name='Lieferant'),
),
]
View File
+65
View File
@@ -0,0 +1,65 @@
from django.db import models
from django.contrib.auth import get_user_model
from projects.models import Project
import os
User = get_user_model()
def receipt_upload_path(instance, filename):
return f"projects/{instance.project.id}/receipts/{filename}"
class Category(models.Model):
name = models.CharField(max_length=100)
class Meta:
verbose_name = "Finanzkategorie"
verbose_name_plural = "Finanzkategorien"
def __str__(self):
return self.name
class Supplier(models.Model):
name = models.CharField(max_length=150)
class Meta:
verbose_name = "Lieferant"
verbose_name_plural = "Lieferanten"
def __str__(self):
return self.name
class PaymentMethod(models.Model):
name = models.CharField(max_length=50)
class Meta:
verbose_name = "Zahlungsmethode"
verbose_name_plural = "Zahlungsmethoden"
def __str__(self):
return self.name
class Expense(models.Model):
name = models.CharField(max_length=255, verbose_name="Name")
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="expenses", verbose_name="Projekt")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="Kategorie")
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Betrag (€)")
date = models.DateTimeField(verbose_name="Datum")
receipt = models.FileField(upload_to=receipt_upload_path, null=True, blank=True, verbose_name="Beleg")
note = models.TextField(blank=True, verbose_name="Notiz")
invoiced = models.BooleanField(default=False,verbose_name="Abgerechnet")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
def __str__(self):
return f"{self.name} - {self.amount} €"
class Income(models.Model):
name = models.CharField(max_length=255, verbose_name="Name", null=True)
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="incomes", verbose_name="Projekt")
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, verbose_name="Kategorie")
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Betrag (€)")
date = models.DateTimeField(verbose_name="Datum")
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name="Erstellt von")
payment_method = models.ForeignKey(PaymentMethod, on_delete=models.SET_NULL, null=True, verbose_name="Zahlungsmethode")
note = models.TextField(blank=True, verbose_name="Notiz")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
def __str__(self):
return f"+{self.amount} € - {self.project.name}"
+26
View File
@@ -0,0 +1,26 @@
from masterdata.registry import register_setting
from .models import (
Category,
Supplier,
PaymentMethod,
)
register_setting(
group="Finanzen",
name="Kategorien",
model=Category,
)
register_setting(
group="Finanzen",
name="Lieferanten",
model=Supplier,
)
register_setting(
group="Finanzen",
name="Zahlungsarten",
model=PaymentMethod,
)
+163
View File
@@ -0,0 +1,163 @@
import django_tables2 as tables
from django.utils.html import format_html
class PeriodAnalyticsTable(tables.Table):
period = tables.Column(verbose_name="Zeitraum")
income = tables.Column(verbose_name="Einnahmen")
expense = tables.Column(verbose_name="Ausgaben")
profit = tables.Column(verbose_name="Gewinn")
actions = tables.TemplateColumn(
template_name="finance/tables/period_actions.html",
verbose_name="Aktionen",
orderable=False,
)
class Meta:
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
template_name = "django_tables2/table.html"
fields = (
"period",
"income",
"expense",
"profit",
"actions",
)
def __init__(self, *args, period_type="day", **kwargs):
self.period_type = period_type
super().__init__(*args, **kwargs)
def render_period(self, value):
if self.period_type == "year":
return value.strftime("%Y")
if self.period_type == "month":
return value.strftime("%m / %Y") # z.B. 06.2026
if self.period_type == "week":
week = value.isocalendar().week
return f"KW {week:02d} / {value.year}"
return value.strftime("%d.%m.%Y")
def render_income(self, value):
return f"{value:.2f} €"
def render_expense(self, value):
return f"{value:.2f} €"
def render_profit(self, value):
return f"{value:.2f} €"
class FinanceTransactionTable(tables.Table):
type = tables.Column(
verbose_name="Typ"
)
name = tables.Column(
verbose_name="Name"
)
amount = tables.Column(
verbose_name="Betrag"
)
date = tables.DateColumn(
verbose_name="Datum",
format="d.m.Y"
)
category = tables.Column(
verbose_name="Kategorie"
)
created_by = tables.Column(
verbose_name="Erstellt von"
)
payment_method = tables.Column(
verbose_name="Zahlungsmethode"
)
supplier = tables.Column(
verbose_name="Lieferant"
)
receipt = tables.TemplateColumn(
template_name="finance/tables/receipt.html",
verbose_name="Beleg",
orderable=False,
)
note = tables.Column(
verbose_name="Notiz"
)
class Meta:
attrs = {
"class": "w-full text-sm min-w-[700px]"
}
template_name = "django_tables2/table.html"
fields = (
"type",
"name",
"amount",
"date",
"category",
"created_by",
"payment_method",
"supplier",
"receipt",
"note",
)
def render_type(self, value):
color = "text-green-600" if value == "Einnahme" else "text-red-600"
return format_html(
'<span class="font-bold {}">{}</span>',
color,
value,
)
def render_amount(self, value):
if value is None:
return "/"
return f"{value:.2f} €"
def render_name(self, value):
return value or "/"
def render_category(self, value):
return value or "/"
def render_created_by(self, value):
return value or "/"
def render_payment_method(self, value):
return value or "/"
def render_supplier(self, value):
return value or "/"
def render_note(self, value):
return value or "/"
+32
View File
@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Finanzen
{% endblock %}
{% block header_title %}
Finanzen tracken
{% endblock %}
{% block header_description %}
Einnahme oder Ausgabe hinzufügen
{% endblock %}
{% block content %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-save">
Speichern
</button>
</form>
</div>
{% endblock %}
+206
View File
@@ -0,0 +1,206 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Finanzen
{% endblock %}
{% block header_title %}
Finanzen
{% endblock %}
{% block header_description %}
Einnahmen & Ausgaben
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="mb-8">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
<div>
<h2 class="text-xl font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Einnahmen und Ausgaben tracken</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'finance:add_income' project.id %}" class="btn btn-action-start">+ Einnahme</a>
<a href="{% url 'finance:add_expense' project.id %}" class="btn btn-action-stop">+ Ausgabe</a>
</div>
</div>
</div>
</div>
<div class="space-y-2 mb-8">
<!-- ===================== -->
<!-- FILTER CARD -->
<!-- ===================== -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">Filter</h2>
<p class="text-slate-400 text-sm mt-1">Zeitraum für aktuelle Auswertung</p>
</div>
<form method="get" class="flex flex-wrap gap-4 items-end">
<div>
<label class="text-sm text-slate-400">Start</label>
<input type="date" name="start"
value="{{ request.GET.start|default_if_none:'' }}"
class="border rounded px-3 py-2">
</div>
<div>
<label class="text-sm text-slate-400">Ende</label>
<input type="date" name="end"
value="{{ request.GET.end|default_if_none:'' }}"
class="border rounded px-3 py-2">
</div>
<button class="btn btn-save">
Anwenden
</button>
</form>
</div>
<!-- ===================== -->
<!-- KPI FLOATING CARDS -->
<!-- ===================== -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<p class="text-slate-400 text-sm mt-1">Einnahmen</p>
<p class="text-3xl font-bold text-green-600 mt-1">
{{ total_income }} €
</p>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<p class="text-slate-400 text-sm mt-1">Ausgaben</p>
<p class="text-3xl font-bold text-red-600 mt-1">
{{ total_expense }} €
</p>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<p class="text-slate-400 text-sm mt-1">Gewinn</p>
<p class="text-3xl font-bold text-blue-600 mt-1">
{{ total_profit }} €
</p>
</div>
</div>
<!-- ===================== -->
<!-- LIST CARDS -->
<!-- ===================== -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
<!-- INCOME -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="p-4">
<h3 class="font-semibold text-green-600">Einnahmen</h3>
</div>
<div class="divide-y">
{% for i in incomes %}
<div class="p-4 flex justify-between">
<div>
<p class="font-medium text-green-600">+{{ i.amount }} € {{ i.name }}</p>
<p class="text-slate-400 text-sm mt-1">{{ i.date|date:"d.m.Y" }} · {{ i.category }}</p>
</div>
</div>
{% empty %}
<p class="p-4 text-slate-400">Keine Einnahmen</p>
{% endfor %}
</div>
</div>
<!-- EXPENSE -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="p-4">
<h3 class="font-semibold text-red-600">Ausgaben</h3>
</div>
<div class="divide-y">
{% for e in expenses %}
<div class="p-4 flex justify-between">
<div>
<p class="font-medium text-red-600">-{{ e.amount }} € {{ e.name }}</p>
<p class="text-slate-400 text-sm mt-1">{{ e.date|date:"d.m.Y" }} · {{ e.category }}</p>
</div>
</div>
{% empty %}
<p class="p-4 text-slate-400">Keine Ausgaben</p>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="mb-6">
<h2 class="text-xl font-bold">
Zeitraum-Analyse
</h2>
<p class="text-slate-400 text-sm mt-1">
Aggregierte Auswertung nach Zeitraum
</p>
</div>
<div class="flex gap-2 mb-6">
<a href="?period=year"
class="px-3 py-1 rounded border {% if period == 'year' %}bg-slate-900 text-white{% endif %}">
Jahr
</a>
<a href="?period=month"
class="px-3 py-1 rounded border {% if period == 'month' %}bg-slate-900 text-white{% endif %}">
Monat
</a>
<a href="?period=week"
class="px-3 py-1 rounded border {% if period == 'week' %}bg-slate-900 text-white{% endif %}">
Woche
</a>
<a href="?period=day"
class="px-3 py-1 rounded border {% if period == 'day' %}bg-slate-900 text-white{% endif %}">
Tag
</a>
</div>
<div class="overflow-x-auto rounded-xl">
{% render_table table %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,8 @@
<div class="flex gap-2">
<a href="{% url 'finance:view' project.id %}?start={{ record.period_start|date:'Y-m-d' }}&end={{ record.period_end|date:'Y-m-d' }}"
class="btn btn-edit">
Details
</a>
</div>
@@ -0,0 +1,9 @@
{% if record.receipt %}
<a href="{{ record.receipt.url }}"
target="_blank"
class="btn btn-action">
Download
</a>
{% else %}
—
{% endif %}
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}{{ project.name }} - Finanzen{% endblock %}
{% block header_title %}
Finanzen Details
{% endblock %}
{% block header_description %}
Zeitraum: {{ start_date|date:"d.m.Y" }} → {{ end_date|date:"d.m.Y" }}
{% endblock %}
{% block content %}
{% load django_tables2 %}
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="overflow-x-auto rounded-xl">{% render_table table %}</div>
</div>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from .views import finance_overview, finance_view, add_income, add_expense
app_name = "finance"
urlpatterns = [
path("<int:project_id>/finance/", finance_overview, name="overview"),
path("<int:project_id>/finance/view/", finance_view, name="view"),
path("<int:project_id>/finance/add-income/", add_income, name="add_income"),
path("<int:project_id>/finance/add-expense/", add_expense, name="add_expense"),
]
+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",
})