This commit is contained in:
Felix Baumann
2026-06-28 10:20:36 +00:00
parent 64b32d0b44
commit 148efd919d
86 changed files with 2475 additions and 760 deletions
+4 -3
View File
@@ -1,5 +1,6 @@
# Generated by Django 6.0.6 on 2026-06-27 17:19
# Generated by Django 6.0.6 on 2026-06-28 10:19
import django.core.validators
import django.db.models.deletion
import finance.models
from django.conf import settings
@@ -31,7 +32,7 @@ class Migration(migrations.Migration):
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 (€)')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], 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')),
@@ -64,7 +65,7 @@ class Migration(migrations.Migration):
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 (€)')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0, message='Der Betrag darf nicht negativ sein.')], 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')),
+7 -1
View File
@@ -1,4 +1,4 @@
# Generated by Django 6.0.6 on 2026-06-27 17:19
# Generated by Django 6.0.6 on 2026-06-28 10:19
import django.db.models.deletion
from django.conf import settings
@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
dependencies = [
('finance', '0001_initial'),
('invoices', '0001_initial'),
('projects', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
@@ -31,6 +32,11 @@ class Migration(migrations.Migration):
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='invoice',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='income', to='invoices.invoice'),
),
migrations.AddField(
model_name='income',
name='project',
+13 -2
View File
@@ -2,6 +2,9 @@ from django.db import models
from django.contrib.auth import get_user_model
from projects.models import Project
import os
from django.core.validators import MinValueValidator
User = get_user_model()
@@ -42,7 +45,7 @@ class Expense(models.Model):
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 (€)")
amount = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Betrag (€)",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
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")
@@ -54,8 +57,16 @@ class Expense(models.Model):
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")
invoice = models.OneToOneField(
"invoices.Invoice",
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name="income"
)
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 (€)")
amount = models.DecimalField(max_digits=10,decimal_places=2,verbose_name="Betrag (€)",validators=[MinValueValidator(0,message="Der Betrag darf nicht negativ sein.")])
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")
+79 -29
View File
@@ -20,7 +20,7 @@ Einnahmen & Ausgaben
<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>
<h2 class="text-lg font-bold">Aktionen</h2>
<p class="text-slate-400 text-sm mt-1">Einnahmen und Ausgaben tracken</p>
</div>
@@ -40,7 +40,7 @@ Einnahmen & Ausgaben
<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>
<h2 class="text-lg font-bold">Filter</h2>
<p class="text-slate-400 text-sm mt-1">Zeitraum für aktuelle Auswertung</p>
</div>
@@ -72,32 +72,82 @@ Einnahmen & Ausgaben
<!-- ===================== -->
<!-- KPI FLOATING CARDS -->
<!-- ===================== -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<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>
<!-- Einnahmen -->
<div class="p-6 rounded-xl border border-green-200 dark:border-green-900 bg-white dark:bg-slate-800 transition">
<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="flex items-center justify-between">
<div>
<p class="text-sm text-slate-500 dark:text-slate-400">
Einnahmen
</p>
<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>
<p class="text-3xl font-bold text-green-600 mt-2">
{{ total_income }} €
</p>
</div>
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-green-100 dark:bg-green-900/40">
<span class="text-2xl text-green-600">
↑
</span>
</div>
</div>
</div>
<!-- Ausgaben -->
<div class="p-6 rounded-xl border border-red-200 dark:border-red-900 bg-white dark:bg-slate-800 transition">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-500 dark:text-slate-400">
Ausgaben
</p>
<p class="text-3xl font-bold text-red-600 mt-2">
{{ total_expense }} €
</p>
</div>
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-red-100 dark:bg-red-900/40">
<span class="text-2xl text-red-600">
↓
</span>
</div>
</div>
</div>
<!-- Gewinn -->
<div class="p-6 rounded-xl border border-blue-200 dark:border-blue-900 bg-white dark:bg-slate-800 transition">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-slate-500 dark:text-slate-400">
Gewinn
</p>
<p class="text-3xl font-bold text-blue-600 mt-2">
{{ total_profit }} €
</p>
</div>
<div class="flex items-center justify-center w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/40">
<span class="text-2xl text-blue-600">
€
</span>
</div>
</div>
</div>
</div>
<!-- ===================== -->
<!-- LIST CARDS -->
<!-- ===================== -->
@@ -106,21 +156,21 @@ Einnahmen & Ausgaben
<!-- 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">
<div class="pb-4">
<h3 class="font-semibold text-green-600">Einnahmen</h3>
</div>
<div class="divide-y">
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto hide-scrollbar">
{% for i in incomes %}
<div class="p-4 flex justify-between">
<div class="p-4 flex justify-between bg-slate-100 dark:bg-slate-700 rounded-xl">
<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>
<div class="bg-white dark:bg-slate-800 border border-slate-500 rounded-xl p-6 text-slate-500">Keine Einnahmen</div>
{% endfor %}
</div>
@@ -131,21 +181,21 @@ Einnahmen & Ausgaben
<!-- 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">
<div class="pb-4">
<h3 class="font-semibold text-red-600">Ausgaben</h3>
</div>
<div class="divide-y">
<div class="flex flex-col gap-2 max-h-80 overflow-y-auto hide-scrollbar">
{% for e in expenses %}
<div class="p-4 flex justify-between">
<div class="p-4 flex justify-between bg-slate-100 dark:bg-slate-700 rounded-xl">
<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>
<div class="bg-white dark:bg-slate-800 border border-slate-500 rounded-xl p-6 text-slate-500">Keine Ausgaben</div>
{% endfor %}
</div>
@@ -160,7 +210,7 @@ Einnahmen & Ausgaben
<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">
<h2 class="text-lg font-bold">
Zeitraum-Analyse
</h2>
+44 -10
View File
@@ -11,6 +11,8 @@ from django.http import HttpResponse
from calendar import monthrange
from .forms import IncomeForm, ExpenseForm
from .tables import PeriodAnalyticsTable, FinanceTransactionTable
import json
from inventory.models import ShoppingItem
# =========================================================
# OVERVIEW
@@ -102,23 +104,35 @@ def finance_overview(request, project_id):
}
period_data = []
period_keys = set()
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)
period_keys.add(i["period_key"])
for e in expense_stats:
period_keys.add(e["period_key"])
income_map = {
str(i["period_key"]): i["total"] or 0
for i in income_stats
}
expense_map = {
str(e["period_key"]): e["total"] or 0
for e in expense_stats
}
for period_start in sorted(period_keys, reverse=True):
income_total = income_map.get(str(period_start), 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]
last_day = monthrange(
period_start.year,
period_start.month
)[1]
period_end = period_start.replace(day=last_day)
period_data.append({
@@ -319,10 +333,30 @@ def add_expense(request, project_id):
expense.project = project
expense.created_by = request.user
expense.save()
return redirect("finance:overview", project_id=project.id)
shopping_ids = request.session.pop(
"shopping_item_ids",
[]
)
if shopping_ids:
ShoppingItem.objects.filter(
id__in=shopping_ids,
project=project
).delete()
return redirect(
"finance:overview",
project_id=project.id
)
else:
form = ExpenseForm()
initial = request.session.pop(
"expense_initial",
{}
)
form = ExpenseForm(initial=initial)
return render(request, "finance/form.html", {
"form": form,