v1.0
This commit is contained in:
@@ -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')),
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user