Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
.env
|
||||||
|
db.sqlite3
|
||||||
|
media/
|
||||||
|
staticfiles/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
django: python manage.py runserver
|
||||||
|
tailwind: python manage.py tailwind start
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import Role, UserProfile
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Role)
|
||||||
|
class RoleAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(UserProfile)
|
||||||
|
class UserProfileAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = [
|
||||||
|
"user",
|
||||||
|
"role",
|
||||||
|
]
|
||||||
|
|
||||||
|
list_filter = [
|
||||||
|
"role",
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"user__username",
|
||||||
|
"user__first_name",
|
||||||
|
"user__last_name",
|
||||||
|
"user__email",
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class AccountsConfig(AppConfig):
|
||||||
|
name = 'accounts'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
|
||||||
|
def permission_required(permission):
|
||||||
|
|
||||||
|
def decorator(view_func):
|
||||||
|
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(request, *args, **kwargs):
|
||||||
|
|
||||||
|
# Superuser haben alles
|
||||||
|
if request.user.is_superuser:
|
||||||
|
return view_func(
|
||||||
|
request,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
profile = getattr(
|
||||||
|
request.user,
|
||||||
|
"profile",
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if profile and profile.has_permission(permission):
|
||||||
|
|
||||||
|
return view_func(
|
||||||
|
request,
|
||||||
|
*args,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"Keine Berechtigung für diese Aktion!"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
return redirect("core:dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
return decorator
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from .models import Role
|
||||||
|
from core.forms.base import BaseForm
|
||||||
|
|
||||||
|
class UserAddForm(BaseForm):
|
||||||
|
|
||||||
|
password = forms.CharField(
|
||||||
|
label="Passwort",
|
||||||
|
widget=forms.PasswordInput
|
||||||
|
)
|
||||||
|
|
||||||
|
role = forms.ModelChoiceField(
|
||||||
|
queryset=Role.objects.order_by("name"),
|
||||||
|
required=False,
|
||||||
|
label="Rolle"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
"username",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
]
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"username": "Benutzername",
|
||||||
|
"first_name": "Vorname",
|
||||||
|
"last_name": "Nachname",
|
||||||
|
"email": "E-Mail",
|
||||||
|
}
|
||||||
|
|
||||||
|
help_texts = {
|
||||||
|
"username": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.apply_style()
|
||||||
|
|
||||||
|
|
||||||
|
class RoleForm(BaseForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
labels = {
|
||||||
|
"name": "Rollenname",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.apply_style()
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 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 = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Role',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=100, unique=True, verbose_name='Rolle')),
|
||||||
|
('projects_view', models.BooleanField(default=False, help_text='Benutzer darf Projekte einsehen.', verbose_name='Projekte anzeigen')),
|
||||||
|
('projects_archived_view', models.BooleanField(default=False, help_text='Benutzer darf archivierte Projekte einsehen.', verbose_name='Archivierte Projekte anzeigen')),
|
||||||
|
('projects_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Details des Projekts einsehen.', verbose_name='Projektdetails anzeigen')),
|
||||||
|
('projects_add', models.BooleanField(default=False, help_text='Benutzer darf neue Projekte anlegen.', verbose_name='Projekt erstellen')),
|
||||||
|
('projects_contact_add', models.BooleanField(default=False, help_text='Benutzer darf Kontakte mit dem Projekt verknüpfen.', verbose_name='Projektkontakt verknüpfen')),
|
||||||
|
('projects_contact_delete', models.BooleanField(default=False, help_text='Benutzer darf Kontakte aus dem Projekt entfernen.', verbose_name='Projektkontakt entfernen')),
|
||||||
|
('projects_location_add', models.BooleanField(default=False, help_text='Benutzer darf Standorte mit dem Projekt verknüpfen.', verbose_name='Projektstandort verknüpfen')),
|
||||||
|
('projects_location_delete', models.BooleanField(default=False, help_text='Benutzer darf Standorte aus dem Projekt entfernen.', verbose_name='Projektstandort entfernen')),
|
||||||
|
('projects_edit', models.BooleanField(default=False, help_text='Benutzer darf Projekte ändern.', verbose_name='Projekte bearbeiten')),
|
||||||
|
('projects_user_add', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter im Projekt hinzufügen.', verbose_name='Projektmitarbeiter erstellen')),
|
||||||
|
('projects_user_delete', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter aus dem Projekt entfernen.', verbose_name='Projektmitarbeiter entfernen')),
|
||||||
|
('projects_finance_view', models.BooleanField(default=False, help_text='Benutzer darf Projektfinanzen einsehen.', verbose_name='Projektfinanzen anzeigen')),
|
||||||
|
('projects_finance_income_add', models.BooleanField(default=False, help_text='Benutzer darf Einnahmen hinzufügen.', verbose_name='Projekteinnahmen erfassen')),
|
||||||
|
('projects_finance_expense_add', models.BooleanField(default=False, help_text='Benutzer darf Ausgaben hinzufügen.', verbose_name='Projektausgaben erfassen')),
|
||||||
|
('projects_time_tracking_view', models.BooleanField(default=False, help_text='Benutzer darf Projektzeiten sehen.', verbose_name='Projektzeiten anzeigen')),
|
||||||
|
('projects_time_tracking_edit', models.BooleanField(default=False, help_text='Benutzer darf Projektzeiten ändern.', verbose_name='Projektzeiten bearbeiten')),
|
||||||
|
('projects_time_tracking_delete', models.BooleanField(default=False, help_text='Benutzer darf Projektzeiten löschen.', verbose_name='Projektzeiten löschen')),
|
||||||
|
('projects_documents_view', models.BooleanField(default=False, help_text='Benutzer darf Dokumente sehen.', verbose_name='Projektdokumente anzeigen')),
|
||||||
|
('projects_documents_add', models.BooleanField(default=False, help_text='Benutzer darf Dokumente hochladen.', verbose_name='Projektdokumente hinzufügen')),
|
||||||
|
('projects_documents_delete', models.BooleanField(default=False, help_text='Benutzer darf Dokumente löschen.', verbose_name='Projektdokumente löschen')),
|
||||||
|
('projects_inventory_inventory_view', models.BooleanField(default=False, help_text='Benutzer darf Projektinventar sehen.', verbose_name='Projektinventar anzeigen')),
|
||||||
|
('projects_inventory_inventory_add', models.BooleanField(default=False, help_text='Benutzer darf Inventar hinzufügen.', verbose_name='Projektinventar hinzufügen')),
|
||||||
|
('projects_inventory_inventory_edit', models.BooleanField(default=False, help_text='Benutzer darf Inventar ändern.', verbose_name='Projektinventar bearbeiten')),
|
||||||
|
('projects_inventory_inventory_delete', models.BooleanField(default=False, help_text='Benutzer darf Inventar löschen.', verbose_name='Projektinventar löschen')),
|
||||||
|
('projects_inventory_shopping_add', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen erstellen.', verbose_name='Einkauf hinzufügen')),
|
||||||
|
('projects_inventory_shopping_edit', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen ändern.', verbose_name='Einkauf bearbeiten')),
|
||||||
|
('projects_inventory_shopping_delete', models.BooleanField(default=False, help_text='Benutzer darf Einkaufspositionen löschen.', verbose_name='Einkauf löschen')),
|
||||||
|
('inventory_view', models.BooleanField(default=False, help_text='Benutzer darf Inventar einsehen.', verbose_name='Inventar anzeigen')),
|
||||||
|
('inventory_add', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge erstellen.', verbose_name='Inventar hinzufügen')),
|
||||||
|
('inventory_edit', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge ändern.', verbose_name='Inventar bearbeiten')),
|
||||||
|
('inventory_delete', models.BooleanField(default=False, help_text='Benutzer darf Inventareinträge löschen.', verbose_name='Inventar löschen')),
|
||||||
|
('invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen verwalten.', verbose_name='Rechnungsbereich anzeigen')),
|
||||||
|
('invoices_customers_view', models.BooleanField(default=False, help_text='Benutzer darf Kunden sehen.', verbose_name='Kunden anzeigen')),
|
||||||
|
('invoices_customers_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Details des Kunden sehen.', verbose_name='Kundendetails anzeigen')),
|
||||||
|
('invoices_customers_add', models.BooleanField(default=False, help_text='Benutzer darf Kunden anlegen.', verbose_name='Kunden erstellen')),
|
||||||
|
('invoices_customers_edit', models.BooleanField(default=False, help_text='Benutzer darf Kunden ändern.', verbose_name='Kunden bearbeiten')),
|
||||||
|
('invoices_customers_archive', models.BooleanField(default=False, help_text='Benutzer darf Kunden archivieren.', verbose_name='Kunden archivieren')),
|
||||||
|
('invoices_customers_activate', models.BooleanField(default=False, help_text='Benutzer darf Kunden wieder aktivieren.', verbose_name='Kunden reaktivieren')),
|
||||||
|
('invoices_items_view', models.BooleanField(default=False, help_text='Benutzer darf Artikel sehen.', verbose_name='Artikel anzeigen')),
|
||||||
|
('invoices_items_add', models.BooleanField(default=False, help_text='Benutzer darf Artikel erstellen.', verbose_name='Artikel erstellen')),
|
||||||
|
('invoices_items_edit', models.BooleanField(default=False, help_text='Benutzer darf Artikel ändern.', verbose_name='Artikel bearbeiten')),
|
||||||
|
('invoices_items_delete', models.BooleanField(default=False, help_text='Benutzer darf Artikel löschen.', verbose_name='Artikel löschen')),
|
||||||
|
('invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen sehen.', verbose_name='Rechnungen anzeigen')),
|
||||||
|
('invoices_invoices_project_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen aus Projekten erstellen.', verbose_name='Projekt-Rechnung erstellen')),
|
||||||
|
('invoices_invoices_customer_add', models.BooleanField(default=False, help_text='Benutzer darf Rechnungen für Kunden erstellen.', verbose_name='Kunden-Rechnung erstellen')),
|
||||||
|
('invoices_invoices_invoices_view', models.BooleanField(default=False, help_text='Benutzer darf Rechnungsdetails sehen.', verbose_name='Rechnungsdetails anzeigen')),
|
||||||
|
('invoices_invoices_invoices_item_add', models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Rechnungsposition hinzufügen')),
|
||||||
|
('invoices_invoices_invoices_item_delete', models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Rechnungsposition löschen')),
|
||||||
|
('invoices_invoices_invoices_preview', models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Rechnungsvorschau anzeigen')),
|
||||||
|
('users_view', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter sehen.', verbose_name='Benutzer anzeigen')),
|
||||||
|
('users_add', models.BooleanField(default=False, help_text='Benutzer darf neue Mitarbeiter anlegen.', verbose_name='Benutzer erstellen')),
|
||||||
|
('users_edit', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter bearbeiten.', verbose_name='Benutzer bearbeiten')),
|
||||||
|
('users_archive', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter deaktivieren.', verbose_name='Benutzer archivieren')),
|
||||||
|
('users_activate', models.BooleanField(default=False, help_text='Benutzer darf Mitarbeiter aktivieren.', verbose_name='Benutzer aktivieren')),
|
||||||
|
('users_groups_add', models.BooleanField(default=False, help_text='Benutzer darf neue Rollen erstellen.', verbose_name='Rollen erstellen')),
|
||||||
|
('users_groups_edit', models.BooleanField(default=False, help_text='Benutzer darf Rollen und Berechtigungen ändern.', verbose_name='Rollen bearbeiten')),
|
||||||
|
('users_groups_delete', models.BooleanField(default=False, help_text='Benutzer darf Rollen entfernen.', verbose_name='Rollen löschen')),
|
||||||
|
('settings_view', models.BooleanField(default=False, help_text='Benutzer darf Einstellungskategorien sehen.', verbose_name='Einstellungsübersicht anzeigen')),
|
||||||
|
('settings_detail_view', models.BooleanField(default=False, help_text='Benutzer darf Systemeinstellungen sehen.', verbose_name='Einstellungen anzeigen')),
|
||||||
|
('settings_add', models.BooleanField(default=False, help_text='Benutzer darf neue Einstellungen hinzufügen.', verbose_name='Einstellungen erstellen')),
|
||||||
|
('settings_delete', models.BooleanField(default=False, help_text='Benutzer darf Einstellungen entfernen.', verbose_name='Einstellungen löschen')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Rolle',
|
||||||
|
'verbose_name_plural': 'Rollen',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserProfile',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.role')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-23 23:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_invoices_invoices_download',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungen herunterladen.', verbose_name='Rechnung herunterladen'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-24 08:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_role_invoices_invoices_invoices_download'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_invoices_invoices_cancel',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungen stornieren.', verbose_name='Rechnung stornieren'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-24 09:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0003_role_invoices_invoices_invoices_cancel'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_invoices_invoices_cancel_download',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Stornorechnungen herunterladen.', verbose_name='Stornorechnung Download'),
|
||||||
|
),
|
||||||
|
]
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-24 13:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0004_role_invoices_invoices_invoices_cancel_download'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_invoices_invoices_cancel_download',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_invoices_invoices_paid',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Rechnungsstatus ändern.', verbose_name='Rechnung auf bezahlt setzen'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-25 16:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0005_remove_role_invoices_invoices_invoices_cancel_download_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_add',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Angebote für Kunden erstellen.', verbose_name='Angebot erstellen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_download',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Angebote herunterladen.', verbose_name='Angebot herunterladen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_item_add',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Positionen hinzufügen.', verbose_name='Angebotsposition hinzufügen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_item_delete',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Positionen löschen.', verbose_name='Angebotsposition löschen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_preview',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Vorschauen öffnen.', verbose_name='Angebotsvorschau anzeigen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_status',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Angebotsstatus ändern.', verbose_name='Angebotsstatus bearbeiten'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_offers_view',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Angebotsdetails sehen.', verbose_name='Angebotsdetails anzeigen'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='role',
|
||||||
|
name='invoices_offers_view',
|
||||||
|
field=models.BooleanField(default=False, help_text='Benutzer darf Angebote sehen.', verbose_name='Angebote anzeigen'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
class Role(models.Model):
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=100,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Rolle"
|
||||||
|
)
|
||||||
|
|
||||||
|
projects_view = models.BooleanField(default=False, verbose_name="Projekte anzeigen", help_text="Benutzer darf Projekte einsehen.")
|
||||||
|
projects_archived_view = models.BooleanField(default=False, verbose_name="Archivierte Projekte anzeigen", help_text="Benutzer darf archivierte Projekte einsehen.")
|
||||||
|
projects_detail_view = models.BooleanField(default=False, verbose_name="Projektdetails anzeigen", help_text="Benutzer darf Details des Projekts einsehen.")
|
||||||
|
projects_add = models.BooleanField(default=False, verbose_name="Projekt erstellen", help_text="Benutzer darf neue Projekte anlegen.")
|
||||||
|
projects_contact_add = models.BooleanField(default=False, verbose_name="Projektkontakt verknüpfen", help_text="Benutzer darf Kontakte mit dem Projekt verknüpfen.")
|
||||||
|
projects_contact_delete = models.BooleanField(default=False, verbose_name="Projektkontakt entfernen", help_text="Benutzer darf Kontakte aus dem Projekt entfernen.")
|
||||||
|
projects_location_add = models.BooleanField(default=False, verbose_name="Projektstandort verknüpfen", help_text="Benutzer darf Standorte mit dem Projekt verknüpfen.")
|
||||||
|
projects_location_delete = models.BooleanField(default=False, verbose_name="Projektstandort entfernen", help_text="Benutzer darf Standorte aus dem Projekt entfernen.")
|
||||||
|
projects_edit = models.BooleanField(default=False, verbose_name="Projekte bearbeiten", help_text="Benutzer darf Projekte ändern.")
|
||||||
|
|
||||||
|
projects_user_add = models.BooleanField(default=False, verbose_name="Projektmitarbeiter erstellen", help_text="Benutzer darf Mitarbeiter im Projekt hinzufügen.")
|
||||||
|
projects_user_delete = models.BooleanField(default=False, verbose_name="Projektmitarbeiter entfernen", help_text="Benutzer darf Mitarbeiter aus dem Projekt entfernen.")
|
||||||
|
|
||||||
|
projects_finance_view = models.BooleanField(default=False, verbose_name="Projektfinanzen anzeigen", help_text="Benutzer darf Projektfinanzen einsehen.")
|
||||||
|
projects_finance_income_add = models.BooleanField(default=False, verbose_name="Projekteinnahmen erfassen", help_text="Benutzer darf Einnahmen hinzufügen.")
|
||||||
|
projects_finance_expense_add = models.BooleanField(default=False, verbose_name="Projektausgaben erfassen", help_text="Benutzer darf Ausgaben hinzufügen.")
|
||||||
|
|
||||||
|
projects_time_tracking_view = models.BooleanField(default=False, verbose_name="Projektzeiten anzeigen", help_text="Benutzer darf Projektzeiten sehen.")
|
||||||
|
projects_time_tracking_edit = models.BooleanField(default=False, verbose_name="Projektzeiten bearbeiten", help_text="Benutzer darf Projektzeiten ändern.")
|
||||||
|
projects_time_tracking_delete = models.BooleanField(default=False, verbose_name="Projektzeiten löschen", help_text="Benutzer darf Projektzeiten löschen.")
|
||||||
|
|
||||||
|
projects_documents_view = models.BooleanField(default=False, verbose_name="Projektdokumente anzeigen", help_text="Benutzer darf Dokumente sehen.")
|
||||||
|
projects_documents_add = models.BooleanField(default=False, verbose_name="Projektdokumente hinzufügen", help_text="Benutzer darf Dokumente hochladen.")
|
||||||
|
projects_documents_delete = models.BooleanField(default=False, verbose_name="Projektdokumente löschen", help_text="Benutzer darf Dokumente löschen.")
|
||||||
|
|
||||||
|
projects_inventory_inventory_view = models.BooleanField(default=False, verbose_name="Projektinventar anzeigen", help_text="Benutzer darf Projektinventar sehen.")
|
||||||
|
projects_inventory_inventory_add = models.BooleanField(default=False, verbose_name="Projektinventar hinzufügen", help_text="Benutzer darf Inventar hinzufügen.")
|
||||||
|
projects_inventory_inventory_edit = models.BooleanField(default=False, verbose_name="Projektinventar bearbeiten", help_text="Benutzer darf Inventar ändern.")
|
||||||
|
projects_inventory_inventory_delete = models.BooleanField(default=False, verbose_name="Projektinventar löschen", help_text="Benutzer darf Inventar löschen.")
|
||||||
|
|
||||||
|
projects_inventory_shopping_add = models.BooleanField(default=False, verbose_name="Einkauf hinzufügen", help_text="Benutzer darf Einkaufspositionen erstellen.")
|
||||||
|
projects_inventory_shopping_edit = models.BooleanField(default=False, verbose_name="Einkauf bearbeiten", help_text="Benutzer darf Einkaufspositionen ändern.")
|
||||||
|
projects_inventory_shopping_delete = models.BooleanField(default=False, verbose_name="Einkauf löschen", help_text="Benutzer darf Einkaufspositionen löschen.")
|
||||||
|
#projects_inventory_shopping_buy = models.BooleanField(default=False, verbose_name="Einkäufe durchführen", help_text="Benutzer darf Einkäufe als erledigt markieren.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Inventar
|
||||||
|
# =====================
|
||||||
|
inventory_view = models.BooleanField(default=False, verbose_name="Inventar anzeigen", help_text="Benutzer darf Inventar einsehen.")
|
||||||
|
inventory_add = models.BooleanField(default=False, verbose_name="Inventar hinzufügen", help_text="Benutzer darf Inventareinträge erstellen.")
|
||||||
|
inventory_edit = models.BooleanField(default=False, verbose_name="Inventar bearbeiten", help_text="Benutzer darf Inventareinträge ändern.")
|
||||||
|
inventory_delete = models.BooleanField(default=False, verbose_name="Inventar löschen", help_text="Benutzer darf Inventareinträge löschen.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Rechnungen
|
||||||
|
# =====================
|
||||||
|
invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsbereich anzeigen", help_text="Benutzer darf Rechnungen verwalten.")
|
||||||
|
|
||||||
|
invoices_customers_view = models.BooleanField(default=False, verbose_name="Kunden anzeigen", help_text="Benutzer darf Kunden sehen.")
|
||||||
|
invoices_customers_detail_view = models.BooleanField(default=False, verbose_name="Kundendetails anzeigen", help_text="Benutzer darf Details des Kunden sehen.")
|
||||||
|
invoices_customers_add = models.BooleanField(default=False, verbose_name="Kunden erstellen", help_text="Benutzer darf Kunden anlegen.")
|
||||||
|
invoices_customers_edit = models.BooleanField(default=False, verbose_name="Kunden bearbeiten", help_text="Benutzer darf Kunden ändern.")
|
||||||
|
invoices_customers_archive = models.BooleanField(default=False, verbose_name="Kunden archivieren", help_text="Benutzer darf Kunden archivieren.")
|
||||||
|
invoices_customers_activate = models.BooleanField(default=False, verbose_name="Kunden reaktivieren", help_text="Benutzer darf Kunden wieder aktivieren.")
|
||||||
|
|
||||||
|
invoices_items_view = models.BooleanField(default=False, verbose_name="Artikel anzeigen", help_text="Benutzer darf Artikel sehen.")
|
||||||
|
invoices_items_add = models.BooleanField(default=False, verbose_name="Artikel erstellen", help_text="Benutzer darf Artikel erstellen.")
|
||||||
|
invoices_items_edit = models.BooleanField(default=False, verbose_name="Artikel bearbeiten", help_text="Benutzer darf Artikel ändern.")
|
||||||
|
invoices_items_delete = models.BooleanField(default=False, verbose_name="Artikel löschen", help_text="Benutzer darf Artikel löschen.")
|
||||||
|
|
||||||
|
invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungen anzeigen", help_text="Benutzer darf Rechnungen sehen.")
|
||||||
|
invoices_invoices_project_add = models.BooleanField(default=False, verbose_name="Projekt-Rechnung erstellen", help_text="Benutzer darf Rechnungen aus Projekten erstellen.")
|
||||||
|
invoices_invoices_customer_add = models.BooleanField(default=False, verbose_name="Kunden-Rechnung erstellen", help_text="Benutzer darf Rechnungen für Kunden erstellen.")
|
||||||
|
invoices_invoices_invoices_view = models.BooleanField(default=False, verbose_name="Rechnungsdetails anzeigen", help_text="Benutzer darf Rechnungsdetails sehen.")
|
||||||
|
invoices_invoices_invoices_item_add = models.BooleanField(default=False, verbose_name="Rechnungsposition hinzufügen", help_text="Benutzer darf Positionen hinzufügen.")
|
||||||
|
invoices_invoices_invoices_item_delete = models.BooleanField(default=False, verbose_name="Rechnungsposition löschen", help_text="Benutzer darf Positionen löschen.")
|
||||||
|
invoices_invoices_invoices_preview = models.BooleanField(default=False, verbose_name="Rechnungsvorschau anzeigen", help_text="Benutzer darf Vorschauen öffnen.")
|
||||||
|
invoices_invoices_invoices_download = models.BooleanField(default=False, verbose_name="Rechnung herunterladen", help_text="Benutzer darf Rechnungen herunterladen.")
|
||||||
|
invoices_invoices_invoices_cancel = models.BooleanField(default=False, verbose_name="Rechnung stornieren", help_text="Benutzer darf Rechnungen stornieren.")
|
||||||
|
invoices_invoices_invoices_paid = models.BooleanField(default=False, verbose_name="Rechnung auf bezahlt setzen", help_text="Benutzer darf Rechnungsstatus ändern.")
|
||||||
|
|
||||||
|
|
||||||
|
invoices_offers_view = models.BooleanField(default=False, verbose_name="Angebote anzeigen", help_text="Benutzer darf Angebote sehen.")
|
||||||
|
invoices_offers_add = models.BooleanField(default=False, verbose_name="Angebot erstellen", help_text="Benutzer darf Angebote für Kunden erstellen.")
|
||||||
|
invoices_offers_offers_view = models.BooleanField(default=False, verbose_name="Angebotsdetails anzeigen", help_text="Benutzer darf Angebotsdetails sehen.")
|
||||||
|
invoices_offers_offers_item_add = models.BooleanField(default=False, verbose_name="Angebotsposition hinzufügen", help_text="Benutzer darf Positionen hinzufügen.")
|
||||||
|
invoices_offers_offers_item_delete = models.BooleanField(default=False, verbose_name="Angebotsposition löschen", help_text="Benutzer darf Positionen löschen.")
|
||||||
|
invoices_offers_offers_preview = models.BooleanField(default=False, verbose_name="Angebotsvorschau anzeigen", help_text="Benutzer darf Vorschauen öffnen.")
|
||||||
|
invoices_offers_offers_download = models.BooleanField(default=False, verbose_name="Angebot herunterladen", help_text="Benutzer darf Angebote herunterladen.")
|
||||||
|
invoices_offers_offers_status = models.BooleanField(default=False, verbose_name="Angebotsstatus bearbeiten", help_text="Benutzer darf Angebotsstatus ändern.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Benutzerverwaltung
|
||||||
|
# =====================
|
||||||
|
users_view = models.BooleanField(default=False, verbose_name="Benutzer anzeigen", help_text="Benutzer darf Mitarbeiter sehen.")
|
||||||
|
users_add = models.BooleanField(default=False, verbose_name="Benutzer erstellen", help_text="Benutzer darf neue Mitarbeiter anlegen.")
|
||||||
|
users_edit = models.BooleanField(default=False, verbose_name="Benutzer bearbeiten", help_text="Benutzer darf Mitarbeiter bearbeiten.")
|
||||||
|
users_archive = models.BooleanField(default=False, verbose_name="Benutzer archivieren", help_text="Benutzer darf Mitarbeiter deaktivieren.")
|
||||||
|
users_activate = models.BooleanField(default=False, verbose_name="Benutzer aktivieren", help_text="Benutzer darf Mitarbeiter aktivieren.")
|
||||||
|
|
||||||
|
users_groups_add = models.BooleanField(default=False, verbose_name="Rollen erstellen", help_text="Benutzer darf neue Rollen erstellen.")
|
||||||
|
users_groups_edit = models.BooleanField(default=False, verbose_name="Rollen bearbeiten", help_text="Benutzer darf Rollen und Berechtigungen ändern.")
|
||||||
|
users_groups_delete = models.BooleanField(default=False, verbose_name="Rollen löschen", help_text="Benutzer darf Rollen entfernen.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Einstellungen
|
||||||
|
# =====================
|
||||||
|
settings_view = models.BooleanField(default=False, verbose_name="Einstellungsübersicht anzeigen", help_text="Benutzer darf Einstellungskategorien sehen.")
|
||||||
|
settings_detail_view = models.BooleanField(default=False, verbose_name="Einstellungen anzeigen", help_text="Benutzer darf Systemeinstellungen sehen.")
|
||||||
|
settings_add = models.BooleanField(default=False, verbose_name="Einstellungen erstellen", help_text="Benutzer darf neue Einstellungen hinzufügen.")
|
||||||
|
#settings_edit = models.BooleanField(default=False, verbose_name="Einstellungen bearbeiten", help_text="Benutzer darf Einstellungen ändern.")
|
||||||
|
settings_delete = models.BooleanField(default=False, verbose_name="Einstellungen löschen", help_text="Benutzer darf Einstellungen entfernen.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Zeiterfassung
|
||||||
|
# =====================
|
||||||
|
#time_tracking_own_view = models.BooleanField(default=False, verbose_name="Eigene Zeiten anzeigen", help_text="Benutzer darf eigene Arbeitszeiten sehen.")
|
||||||
|
#time_tracking_own_add = models.BooleanField(default=False, verbose_name="Eigene Zeiten erfassen", help_text="Benutzer darf eigene Arbeitszeiten hinzufügen.")
|
||||||
|
#time_tracking_own_edit = models.BooleanField(default=False, verbose_name="Eigene Zeiten bearbeiten", help_text="Benutzer darf eigene Arbeitszeiten ändern.")
|
||||||
|
#time_tracking_own_delete = models.BooleanField(default=False, verbose_name="Eigene Zeiten löschen", help_text="Benutzer darf eigene Arbeitszeiten löschen.")
|
||||||
|
|
||||||
|
#time_tracking_all_view = models.BooleanField(default=False, verbose_name="Alle Zeiten anzeigen", help_text="Benutzer darf Arbeitszeiten aller Benutzer sehen.")
|
||||||
|
#time_tracking_all_add = models.BooleanField(default=False, verbose_name="Alle Zeiten erfassen", help_text="Benutzer darf Arbeitszeiten für andere Benutzer hinzufügen.")
|
||||||
|
#time_tracking_all_edit = models.BooleanField(default=False, verbose_name="Alle Zeiten bearbeiten", help_text="Benutzer darf Arbeitszeiten anderer Benutzer ändern.")
|
||||||
|
#time_tracking_all_delete = models.BooleanField(default=False, verbose_name="Alle Zeiten löschen", help_text="Benutzer darf Arbeitszeiten anderer Benutzer löschen.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Finanzen
|
||||||
|
# =====================
|
||||||
|
#finance_view = models.BooleanField(default=False, verbose_name="Finanzen anzeigen", help_text="Benutzer darf Finanzdaten sehen.")
|
||||||
|
#finance_export = models.BooleanField(default=False, verbose_name="Finanzen exportieren", help_text="Benutzer darf Finanzdaten exportieren.")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# Investments
|
||||||
|
# =====================
|
||||||
|
#invests_view = models.BooleanField(default=False, verbose_name="Investitionen anzeigen", help_text="Benutzer darf Investitionen sehen.")
|
||||||
|
#invests_add = models.BooleanField(default=False, verbose_name="Investitionen erstellen", help_text="Benutzer darf neue Investitionen anlegen.")
|
||||||
|
#invests_edit = models.BooleanField(default=False, verbose_name="Investitionen bearbeiten", help_text="Benutzer darf Investitionen ändern.")
|
||||||
|
#invests_delete = models.BooleanField(default=False, verbose_name="Investitionen löschen", help_text="Benutzer darf Investitionen entfernen.")
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Rolle"
|
||||||
|
verbose_name_plural = "Rollen"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfile(models.Model):
|
||||||
|
|
||||||
|
user = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="profile"
|
||||||
|
)
|
||||||
|
|
||||||
|
role = models.ForeignKey(
|
||||||
|
Role,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(self, permission):
|
||||||
|
|
||||||
|
if self.user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not self.role:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return getattr(
|
||||||
|
self.role,
|
||||||
|
permission.replace(".", "_"),
|
||||||
|
False
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.user.username
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from .models import Role
|
||||||
|
|
||||||
|
class RoleTable(tables.Table):
|
||||||
|
toggle_prefix = "role"
|
||||||
|
detail_template = "accounts/tables/user/role_edit_form.html"
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name="Rolle"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Role
|
||||||
|
template_name = "django_tables2/table_toggle.html"
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm min-w-[700px]"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArchivedUserTable(tables.Table):
|
||||||
|
toggle_prefix = "user"
|
||||||
|
detail_template = "accounts/tables/user/archive_edit_form.html"
|
||||||
|
username = tables.Column(
|
||||||
|
verbose_name="Benutzername"
|
||||||
|
)
|
||||||
|
first_name = tables.Column(
|
||||||
|
verbose_name="Vorname"
|
||||||
|
)
|
||||||
|
last_name = tables.Column(
|
||||||
|
verbose_name="Nachname"
|
||||||
|
)
|
||||||
|
email = tables.Column(
|
||||||
|
verbose_name="E-Mail"
|
||||||
|
)
|
||||||
|
role = tables.TemplateColumn(
|
||||||
|
template_name="accounts/tables/user/role.html",
|
||||||
|
verbose_name="Rolle",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
superuser = tables.TemplateColumn(
|
||||||
|
template_name="accounts/tables/user/superuser.html",
|
||||||
|
verbose_name="Superuser",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
template_name = "django_tables2/table_toggle.html"
|
||||||
|
fields = (
|
||||||
|
"username",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"role",
|
||||||
|
"superuser",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm min-w-[700px]"
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserTable(tables.Table):
|
||||||
|
toggle_prefix = "user"
|
||||||
|
detail_template = "accounts/tables/user/edit_form.html"
|
||||||
|
username = tables.Column(
|
||||||
|
verbose_name="Benutzername"
|
||||||
|
)
|
||||||
|
first_name = tables.Column(
|
||||||
|
verbose_name="Vorname"
|
||||||
|
)
|
||||||
|
last_name = tables.Column(
|
||||||
|
verbose_name="Nachname"
|
||||||
|
)
|
||||||
|
email = tables.Column(
|
||||||
|
verbose_name="E-Mail"
|
||||||
|
)
|
||||||
|
role = tables.TemplateColumn(
|
||||||
|
template_name="accounts/tables/user/role.html",
|
||||||
|
verbose_name="Rolle",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
superuser = tables.TemplateColumn(
|
||||||
|
template_name="accounts/tables/user/superuser.html",
|
||||||
|
verbose_name="Superuser",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
template_name = "django_tables2/table_toggle.html"
|
||||||
|
fields = (
|
||||||
|
"username",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"email",
|
||||||
|
"role",
|
||||||
|
"superuser",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm min-w-[700px]"
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Mitarbeiter
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Mitarbeiter hinzufügen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Neuen Mitarbeiter anlegen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
{{ form.as_p }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn mt-4 btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Login
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
<div class="flex justify-center">
|
||||||
|
{% if company_settings.logo %}
|
||||||
|
<img src="{{ company_settings.logo.url }}" alt="" class="h-32">
|
||||||
|
|
||||||
|
{% elif company_settings.companyname %}
|
||||||
|
{{ company_settings.companyname }}
|
||||||
|
{% else %}
|
||||||
|
Projektverwaltung
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center min-h-[80vh]">
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors w-full max-w-md">
|
||||||
|
|
||||||
|
<h1 class="text-3xl font-bold text-center mb-6">
|
||||||
|
Anmelden
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<form method="post" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||||
|
{{ field.label }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="{{ field.field.widget.input_type }}"
|
||||||
|
name="{{ field.html_name }}"
|
||||||
|
id="{{ field.id_for_label }}"
|
||||||
|
{% if field.value %}value="{{ field.value }}"{% endif %}
|
||||||
|
class="w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
|
||||||
|
placeholder="{{ field.label }}"
|
||||||
|
>
|
||||||
|
{% if field.errors %}
|
||||||
|
<p class="text-sm text-red-500">{{ field.errors|join:", " }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn w-full btn-action"
|
||||||
|
>
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Mitarbeiter
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Mitarbeiter
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Mitarbeiter, Rollen und Berechtigungen verwalten
|
||||||
|
{% 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">Mitarbeiter oder Rollen hinzufügen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<a href="{% url 'accounts:user_add' %}" class="btn btn-add">+ Mitarbeiter</a>
|
||||||
|
<a href="{% url 'accounts:role_add' %}" class="btn btn-add">+ Rolle</a>
|
||||||
|
</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 mb-8">
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
Benutzer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
|
Aktive Benutzer mit Rollen und Berechtigungen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|
||||||
|
{% render_table table %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
Archivierte Benutzer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
|
Deaktivierte Benutzer
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|
||||||
|
{% render_table archived_table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mt-8">
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
Rollen
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-slate-400 text-sm mt-1">
|
||||||
|
Rollen und Berechtigungen verwalten
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|
||||||
|
{% render_table role_table %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Mitarbeiter
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Rolle hinzufügen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Neue Rolle anlegen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-8">
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
{{ form.as_p }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn mt-4 btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<form method="post" action="{% url 'accounts:user_edit' item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Benutzername
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value="{{ item.username }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Vorname
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first_name"
|
||||||
|
value="{{ item.first_name }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Nachname
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last_name"
|
||||||
|
value="{{ item.last_name }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
E-Mail
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value="{{ item.email }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Rolle
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="role"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
<option value="">
|
||||||
|
---
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{% for role in table.roles %}
|
||||||
|
<option
|
||||||
|
value="{{ role.id }}"
|
||||||
|
{% if item.profile.role_id == role.id %}selected{% endif %}>
|
||||||
|
{{ role.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Letzter Login
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
||||||
|
class="border rounded px-3 py-2 w-full bg-slate-100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-5 flex gap-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
formaction="{% url 'accounts:user_reactivate' item.id %}"
|
||||||
|
class="btn btn-action">
|
||||||
|
Aktivieren
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<form method="post" action="{% url 'accounts:user_edit' item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Benutzername
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
value="{{ item.username }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Vorname
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="first_name"
|
||||||
|
value="{{ item.first_name }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Nachname
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="last_name"
|
||||||
|
value="{{ item.last_name }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
E-Mail
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value="{{ item.email }}"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Rolle
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
name="role"
|
||||||
|
class="border rounded px-3 py-2 w-full">
|
||||||
|
|
||||||
|
<option value="">
|
||||||
|
---
|
||||||
|
</option>
|
||||||
|
|
||||||
|
{% for role in table.roles %}
|
||||||
|
<option
|
||||||
|
value="{{ role.id }}"
|
||||||
|
{% if item.profile.role_id == role.id %}selected{% endif %}>
|
||||||
|
{{ role.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm mb-1">
|
||||||
|
Letzter Login
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value="{{ item.last_login|date:'d.m.Y H:i' }}"
|
||||||
|
class="border rounded px-3 py-2 w-full bg-slate-100">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-5 flex gap-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
formaction="{% url 'accounts:user_archive' item.id %}"
|
||||||
|
class="btn btn-action">
|
||||||
|
Archivieren
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{{ record.profile.role.name|default:"-" }}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<form method="post" action="{% url 'accounts:role_edit' item.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<h3 class="font-bold mb-4">
|
||||||
|
Berechtigungen
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 xl-grid-cols-3 gap-4">
|
||||||
|
|
||||||
|
{% for permission in item.permissions %}
|
||||||
|
|
||||||
|
<label class="
|
||||||
|
bg-slate-100 dark:bg-slate-700
|
||||||
|
rounded-xl
|
||||||
|
p-3
|
||||||
|
flex
|
||||||
|
items-center
|
||||||
|
gap-3
|
||||||
|
">
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="{{ permission.name }}"
|
||||||
|
{% if permission.value %}
|
||||||
|
checked
|
||||||
|
{% endif %}
|
||||||
|
class="w-4 h-4">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ permission.verbose_name }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-5 flex gap-2">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
formaction="{% url 'accounts:role_delete' item.id %}"
|
||||||
|
class="btn btn-delete">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{% if record.is_superuser %}
|
||||||
|
Ja
|
||||||
|
{% else %}
|
||||||
|
Nein
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "accounts"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("login/", views.CustomLoginView.as_view(), name="login"),
|
||||||
|
path("logout/", views.CustomLogoutView.as_view(), name="logout"),
|
||||||
|
path("", views.overview, name="overview"),
|
||||||
|
|
||||||
|
path("add/",views.user_add,name="user_add"),
|
||||||
|
path("<int:user_id>/edit/",views.user_edit,name="user_edit"),
|
||||||
|
path("<int:user_id>/archive/",views.user_archive,name="user_archive"),
|
||||||
|
path("<int:user_id>/reactivate/",views.user_reactivate,name="user_reactivate"),
|
||||||
|
|
||||||
|
path("role/add/",views.role_add,name="role_add"),
|
||||||
|
path("role/<int:role_id>/edit/",views.role_edit,name="role_edit"),
|
||||||
|
path("role/<int:role_id>/delete/",views.role_delete,name="role_delete"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from accounts.decorators import permission_required
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from .forms import UserAddForm, RoleForm
|
||||||
|
from .models import Role, UserProfile
|
||||||
|
from .tables import UserTable, ArchivedUserTable, RoleTable
|
||||||
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CustomLoginView(LoginView):
|
||||||
|
template_name = "accounts/login.html"
|
||||||
|
redirect_authenticated_user = True
|
||||||
|
|
||||||
|
class CustomLogoutView(LogoutView):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_view")
|
||||||
|
def overview(request):
|
||||||
|
|
||||||
|
users = (
|
||||||
|
User.objects
|
||||||
|
.filter(is_active=True)
|
||||||
|
.select_related(
|
||||||
|
"profile",
|
||||||
|
"profile__role"
|
||||||
|
)
|
||||||
|
.order_by("username")
|
||||||
|
)
|
||||||
|
|
||||||
|
archived_users = (
|
||||||
|
User.objects
|
||||||
|
.filter(is_active=False)
|
||||||
|
.select_related(
|
||||||
|
"profile",
|
||||||
|
"profile__role"
|
||||||
|
)
|
||||||
|
.order_by("username")
|
||||||
|
)
|
||||||
|
|
||||||
|
roles = list(
|
||||||
|
Role.objects.order_by("name")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================
|
||||||
|
# ROLE PERMISSIONS FÜR CHECKBOXEN
|
||||||
|
# ==================================================
|
||||||
|
|
||||||
|
permission_fields = []
|
||||||
|
|
||||||
|
for field in Role._meta.fields:
|
||||||
|
|
||||||
|
if field.get_internal_type() == "BooleanField":
|
||||||
|
|
||||||
|
permission_fields.append({
|
||||||
|
"name": field.name,
|
||||||
|
"verbose_name": field.verbose_name or field.name
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
for role in roles:
|
||||||
|
|
||||||
|
role.permissions = []
|
||||||
|
|
||||||
|
for permission in permission_fields:
|
||||||
|
|
||||||
|
role.permissions.append({
|
||||||
|
|
||||||
|
"name": permission["name"],
|
||||||
|
|
||||||
|
"verbose_name": permission["verbose_name"],
|
||||||
|
|
||||||
|
"value": getattr(
|
||||||
|
role,
|
||||||
|
permission["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================
|
||||||
|
# USER ROLLEN-PERMISSIONS
|
||||||
|
# ==================================================
|
||||||
|
|
||||||
|
for user in list(users) + list(archived_users):
|
||||||
|
|
||||||
|
role = getattr(
|
||||||
|
getattr(user, "profile", None),
|
||||||
|
"role",
|
||||||
|
None
|
||||||
|
)
|
||||||
|
|
||||||
|
user.role_permissions = []
|
||||||
|
|
||||||
|
if role:
|
||||||
|
|
||||||
|
for permission in permission_fields:
|
||||||
|
|
||||||
|
if getattr(
|
||||||
|
role,
|
||||||
|
permission["name"]
|
||||||
|
):
|
||||||
|
user.role_permissions.append(
|
||||||
|
permission["verbose_name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ==================================================
|
||||||
|
# TABLES
|
||||||
|
# ==================================================
|
||||||
|
table = UserTable(users)
|
||||||
|
table.roles = roles
|
||||||
|
RequestConfig(
|
||||||
|
request,
|
||||||
|
paginate=False
|
||||||
|
).configure(table)
|
||||||
|
|
||||||
|
archived_table = ArchivedUserTable(archived_users)
|
||||||
|
archived_table.roles = roles
|
||||||
|
RequestConfig(
|
||||||
|
request,
|
||||||
|
paginate=False
|
||||||
|
).configure(archived_table)
|
||||||
|
|
||||||
|
|
||||||
|
role_table = RoleTable(roles)
|
||||||
|
role_table.roles = roles
|
||||||
|
RequestConfig(
|
||||||
|
request,
|
||||||
|
paginate=False
|
||||||
|
).configure(role_table)
|
||||||
|
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"accounts/overview.html",
|
||||||
|
{
|
||||||
|
"table": table,
|
||||||
|
"archived_table": archived_table,
|
||||||
|
"role_table": role_table,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_edit")
|
||||||
|
def user_edit(request, user_id):
|
||||||
|
|
||||||
|
user = get_object_or_404(
|
||||||
|
User.objects.select_related(
|
||||||
|
"profile"
|
||||||
|
),
|
||||||
|
id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
if request.method != "POST":
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# USER
|
||||||
|
# -------------------------
|
||||||
|
user.username = request.POST.get(
|
||||||
|
"username",
|
||||||
|
user.username
|
||||||
|
)
|
||||||
|
|
||||||
|
user.first_name = request.POST.get(
|
||||||
|
"first_name",
|
||||||
|
user.first_name
|
||||||
|
)
|
||||||
|
|
||||||
|
user.last_name = request.POST.get(
|
||||||
|
"last_name",
|
||||||
|
user.last_name
|
||||||
|
)
|
||||||
|
|
||||||
|
user.email = request.POST.get(
|
||||||
|
"email",
|
||||||
|
user.email
|
||||||
|
)
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# -------------------------
|
||||||
|
# PROFILE / ROLE
|
||||||
|
# -------------------------
|
||||||
|
profile, _ = UserProfile.objects.get_or_create(
|
||||||
|
user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
role_id = request.POST.get("role")
|
||||||
|
|
||||||
|
if role_id:
|
||||||
|
profile.role_id = role_id
|
||||||
|
else:
|
||||||
|
profile.role = None
|
||||||
|
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"accounts:overview"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_archive")
|
||||||
|
def user_archive(request, user_id):
|
||||||
|
|
||||||
|
user = get_object_or_404(
|
||||||
|
User,
|
||||||
|
id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_activate")
|
||||||
|
def user_reactivate(request, user_id):
|
||||||
|
|
||||||
|
user = get_object_or_404(
|
||||||
|
User,
|
||||||
|
id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if user.is_superuser:
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
user.is_active = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return redirect("accounts:overview")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_add")
|
||||||
|
def user_add(request):
|
||||||
|
|
||||||
|
form = UserAddForm(
|
||||||
|
request.POST or None
|
||||||
|
)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
user = form.save(commit=False)
|
||||||
|
|
||||||
|
password = form.cleaned_data["password"]
|
||||||
|
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
UserProfile.objects.create(
|
||||||
|
user=user,
|
||||||
|
role=form.cleaned_data["role"]
|
||||||
|
)
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"accounts:overview"
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"accounts/add.html",
|
||||||
|
{
|
||||||
|
"form": form
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_role_permissions():
|
||||||
|
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
for field in Role._meta.fields:
|
||||||
|
|
||||||
|
if field.get_internal_type() == "BooleanField":
|
||||||
|
|
||||||
|
fields.append({
|
||||||
|
"name": field.name,
|
||||||
|
"label": field.verbose_name or field.name
|
||||||
|
})
|
||||||
|
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_groups_edit")
|
||||||
|
def role_edit(request, role_id):
|
||||||
|
|
||||||
|
role = get_object_or_404(
|
||||||
|
Role,
|
||||||
|
id=role_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
for field in Role._meta.fields:
|
||||||
|
|
||||||
|
if (
|
||||||
|
field.get_internal_type()
|
||||||
|
== "BooleanField"
|
||||||
|
):
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
role,
|
||||||
|
field.name,
|
||||||
|
field.name in request.POST
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
role.name = request.POST.get(
|
||||||
|
"name",
|
||||||
|
role.name
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
role.save()
|
||||||
|
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"accounts:overview"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_groups_delete")
|
||||||
|
def role_delete(request, role_id):
|
||||||
|
|
||||||
|
role = get_object_or_404(
|
||||||
|
Role,
|
||||||
|
id=role_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
role.delete()
|
||||||
|
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"accounts:overview"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("users_groups_add")
|
||||||
|
def role_add(request):
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
form = RoleForm(
|
||||||
|
request.POST
|
||||||
|
)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
|
||||||
|
form.save()
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
"accounts:overview"
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
form = RoleForm()
|
||||||
|
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"accounts/role_add.html",
|
||||||
|
{
|
||||||
|
"form": form
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path("/opt/projects")
|
||||||
|
|
||||||
|
EXCLUDE_DIRS = {
|
||||||
|
"venv",
|
||||||
|
"__pycache__",
|
||||||
|
".git",
|
||||||
|
"node_modules",
|
||||||
|
"staticfiles",
|
||||||
|
"media",
|
||||||
|
}
|
||||||
|
|
||||||
|
CLASS_RE = re.compile(r'class="([^"]*)"')
|
||||||
|
|
||||||
|
|
||||||
|
for path in ROOT.rglob("*"):
|
||||||
|
|
||||||
|
if not path.is_file():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if any(part in EXCLUDE_DIRS for part in path.parts):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if path.suffix not in {".html", ".htm"}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
|
||||||
|
def replace(match):
|
||||||
|
classes = match.group(1).split()
|
||||||
|
|
||||||
|
has_btn = "btn" in classes
|
||||||
|
has_btn_variant = any(
|
||||||
|
c.startswith("btn-")
|
||||||
|
for c in classes
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_btn_variant and not has_btn:
|
||||||
|
classes.insert(0, "btn")
|
||||||
|
return f'class="{" ".join(classes)}"'
|
||||||
|
|
||||||
|
return match.group(0)
|
||||||
|
|
||||||
|
|
||||||
|
new_text = CLASS_RE.sub(replace, text)
|
||||||
|
|
||||||
|
|
||||||
|
if new_text != text:
|
||||||
|
path.write_text(
|
||||||
|
new_text,
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✔ geändert: {path}")
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for config project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-a*^y8tl19j6+i9029ye&n1nhyaze(sd&!drt1++kfq_lok)5o@'
|
||||||
|
DEBUG = True
|
||||||
|
ALLOWED_HOSTS = ["91.99.235.6", "localhost"]
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'accounts',
|
||||||
|
'core',
|
||||||
|
'projects',
|
||||||
|
'finance',
|
||||||
|
'inventory',
|
||||||
|
'documents',
|
||||||
|
'invoices',
|
||||||
|
'masterdata.apps.MasterdataConfig',
|
||||||
|
'time_tracking',
|
||||||
|
'tailwind',
|
||||||
|
'theme',
|
||||||
|
"django_tables2",
|
||||||
|
"django_filters",
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
"core.context_processors.company_settings",
|
||||||
|
"core.context_processors.project_switcher",
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'config.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TAILWIND
|
||||||
|
TAILWIND_APP_NAME = 'theme'
|
||||||
|
|
||||||
|
|
||||||
|
# ACCOUNTS
|
||||||
|
LOGIN_URL = "accounts:login"
|
||||||
|
LOGIN_REDIRECT_URL = "core:dashboard"
|
||||||
|
LOGOUT_REDIRECT_URL = "accounts:login"
|
||||||
|
#AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
|
# MEDIEN
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
MEDIA_ROOT = BASE_DIR / "media"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if DEBUG:
|
||||||
|
INSTALLED_APPS += ["django_browser_reload"]
|
||||||
|
MIDDLEWARE += [
|
||||||
|
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||||
|
]
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", include("core.urls")),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("accounts/", include("accounts.urls")),
|
||||||
|
path("projects/", include("projects.urls")),
|
||||||
|
path("projects/", include("finance.urls")),
|
||||||
|
path("projects/", include("documents.urls")),
|
||||||
|
path("projects/", include("time_tracking.urls")),
|
||||||
|
path("projects/", include("inventory.urls")),
|
||||||
|
path("inventory/", include("inventory.urls_global")),
|
||||||
|
path("invoices/", include("invoices.urls")),
|
||||||
|
path("settings/", include("masterdata.urls")),
|
||||||
|
]
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += [path("__reload__/", include("django_browser_reload.urls")),]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for config project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from .models import CompanySetting
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(CompanySetting)
|
||||||
|
class CompanySettingAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"city",
|
||||||
|
"updated_at",
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class CoreConfig(AppConfig):
|
||||||
|
name = 'core'
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
from projects.models import ProjectMembership
|
||||||
|
from .models import CompanySetting
|
||||||
|
|
||||||
|
def project_switcher(request):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return {"user_projects": []}
|
||||||
|
|
||||||
|
memberships = ProjectMembership.objects.select_related("project").filter(
|
||||||
|
user=request.user,
|
||||||
|
project__active=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"user_projects": [m.project for m in memberships]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def company_settings(request):
|
||||||
|
return {
|
||||||
|
"company_settings": CompanySetting.objects.first()
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class BaseForm(forms.ModelForm):
|
||||||
|
|
||||||
|
default_class = (
|
||||||
|
"w-full px-4 py-3 rounded-lg border border-slate-300 dark:border-slate-600 "
|
||||||
|
"bg-slate-50 dark:bg-slate-700 text-slate-900 dark:text-white "
|
||||||
|
"placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 "
|
||||||
|
"focus:border-transparent transition"
|
||||||
|
)
|
||||||
|
|
||||||
|
def apply_style(self):
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
existing = field.widget.attrs.get("class", "")
|
||||||
|
field.widget.attrs["class"] = f"{self.default_class} {existing}".strip()
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompanySetting',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('companyname', models.CharField(max_length=255, verbose_name='Firmenname')),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Geschäftsführer')),
|
||||||
|
('logo', models.ImageField(blank=True, null=True, upload_to='company/', verbose_name='Logo')),
|
||||||
|
('slogan', models.CharField(blank=True, max_length=255, verbose_name='Slogan')),
|
||||||
|
('color', models.CharField(default='#0f172a', max_length=20, verbose_name='Farbe')),
|
||||||
|
('street', models.CharField(max_length=255, verbose_name='Straße')),
|
||||||
|
('zip', models.CharField(max_length=20, verbose_name='PLZ')),
|
||||||
|
('city', models.CharField(max_length=255, verbose_name='Ort')),
|
||||||
|
('country', models.CharField(default='Deutschland', max_length=100, verbose_name='Land')),
|
||||||
|
('phone', models.CharField(blank=True, max_length=100, verbose_name='Telefon')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=100, verbose_name='E-Mail')),
|
||||||
|
('website', models.URLField(blank=True, max_length=100, verbose_name='Webseite')),
|
||||||
|
('tax_number', models.CharField(blank=True, max_length=100, verbose_name='Steuernummer')),
|
||||||
|
('vat_number', models.CharField(blank=True, max_length=100, verbose_name='USt-IdNr.')),
|
||||||
|
('is_small_business', models.BooleanField(default=False, verbose_name='Kleinunternehmer (§19 UStG)')),
|
||||||
|
('small_business_text', models.TextField(blank=True, verbose_name='Kleinunternehmer Hinweis')),
|
||||||
|
('invoice_header_text', models.TextField(blank=True, verbose_name='Rechnungszusatztext')),
|
||||||
|
('bank_name', models.CharField(blank=True, max_length=255, verbose_name='Bank')),
|
||||||
|
('iban', models.CharField(blank=True, max_length=50, verbose_name='IBAN')),
|
||||||
|
('bic', models.CharField(blank=True, max_length=50, verbose_name='BIC')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Firmeneinstellung',
|
||||||
|
'verbose_name_plural': 'Firmeneinstellungen',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-24 09:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companysetting',
|
||||||
|
name='invoice_number',
|
||||||
|
field=models.CharField(blank=True, max_length=50, verbose_name='BIC'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DocumentNumberSettings',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('invoice_prefix', models.CharField(default='RE', max_length=20)),
|
||||||
|
('invoice_next_number', models.PositiveIntegerField(default=1)),
|
||||||
|
('cancel_prefix', models.CharField(default='ST', max_length=20)),
|
||||||
|
('cancel_next_number', models.PositiveIntegerField(default=1)),
|
||||||
|
('offer_prefix', models.CharField(default='ANG', max_length=20)),
|
||||||
|
('offer_next_number', models.PositiveIntegerField(default=1)),
|
||||||
|
('company', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='number_settings', to='core.companysetting')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-24 09:53
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0002_companysetting_invoice_number_documentnumbersettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='DocumentNumberSettings',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-25 14:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0003_delete_documentnumbersettings'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companysetting',
|
||||||
|
name='offer_header_text',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Angebotszusatztext'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-26 06:13
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_companysetting_offer_header_text'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='companysetting',
|
||||||
|
name='invoice_number',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-27 08:53
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0005_remove_companysetting_invoice_number'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='companysetting',
|
||||||
|
name='reminder_header_text',
|
||||||
|
field=models.TextField(blank=True, verbose_name='Mahnungszusatztext'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class CompanySetting(models.Model):
|
||||||
|
companyname = models.CharField(max_length=255,verbose_name="Firmenname")
|
||||||
|
name = models.CharField(max_length=255,verbose_name="Geschäftsführer")
|
||||||
|
logo = models.ImageField(upload_to="company/",blank=True,null=True,verbose_name="Logo")
|
||||||
|
slogan = models.CharField(max_length=255,blank=True,verbose_name="Slogan")
|
||||||
|
color = models.CharField(max_length=20,default="#0f172a",verbose_name="Farbe")
|
||||||
|
street = models.CharField(max_length=255,verbose_name="Straße")
|
||||||
|
zip = models.CharField(max_length=20,verbose_name="PLZ")
|
||||||
|
city = models.CharField(max_length=255,verbose_name="Ort")
|
||||||
|
country = models.CharField(max_length=100,default="Deutschland",verbose_name="Land")
|
||||||
|
phone = models.CharField(max_length=100,blank=True,verbose_name="Telefon" )
|
||||||
|
email = models.EmailField(max_length=100,blank=True,verbose_name="E-Mail")
|
||||||
|
website = models.URLField(max_length=100,blank=True,verbose_name="Webseite")
|
||||||
|
|
||||||
|
tax_number = models.CharField(max_length=100,blank=True,verbose_name="Steuernummer")
|
||||||
|
vat_number = models.CharField(max_length=100,blank=True,verbose_name="USt-IdNr.")
|
||||||
|
|
||||||
|
is_small_business = models.BooleanField(default=False,verbose_name="Kleinunternehmer (§19 UStG)")
|
||||||
|
small_business_text = models.TextField(blank=True,verbose_name="Kleinunternehmer Hinweis")
|
||||||
|
invoice_header_text = models.TextField(blank=True,verbose_name="Rechnungszusatztext")
|
||||||
|
offer_header_text = models.TextField(blank=True,verbose_name="Angebotszusatztext")
|
||||||
|
reminder_header_text = models.TextField(blank=True,verbose_name="Mahnungszusatztext")
|
||||||
|
|
||||||
|
bank_name = models.CharField(max_length=255,blank=True,verbose_name="Bank")
|
||||||
|
iban = models.CharField(max_length=50,blank=True,verbose_name="IBAN")
|
||||||
|
bic = models.CharField(max_length=50,blank=True,verbose_name="BIC")
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Firmeneinstellung"
|
||||||
|
verbose_name_plural = "Firmeneinstellungen"
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from masterdata.registry import register_setting
|
||||||
|
from .models import CompanySetting
|
||||||
|
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
group="System",
|
||||||
|
name="Firmeneinstellungen",
|
||||||
|
model=CompanySetting,
|
||||||
|
setting_type="single",
|
||||||
|
)
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
{% load static tailwind_tags %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>
|
||||||
|
{% block title %}
|
||||||
|
Projektverwaltung
|
||||||
|
{% endblock %}
|
||||||
|
</title>
|
||||||
|
<script>
|
||||||
|
if (localStorage.getItem("theme") === "dark") {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% tailwind_css %}
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--company-color: {{ company_settings.color }};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="min-h-screen bg-slate-100 dark:bg-slate-900 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex min-h-screen">
|
||||||
|
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div id="overlay"
|
||||||
|
class="fixed inset-0 bg-black/40 hidden z-30 lg:hidden"
|
||||||
|
onclick="toggleSidebar()"></div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside id="sidebar" class="fixed top-0 z-40 w-72 h-screen bg-slate-800 text-white transform -translate-x-full lg:translate-x-0 transition-transform duration-300 flex flex-col border-r border-white/20">
|
||||||
|
|
||||||
|
<div class="p-6 border-b border-white/20 " style="background-color: {{ company_settings.color }};">
|
||||||
|
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
{{ company_settings.companyname|default:"Projektverwaltung" }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex-1 p-4 space-y-2">
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'core:dashboard' %}">
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
|
||||||
|
<!-- Projekt wechseln -->
|
||||||
|
<div class="rounded-lg overflow-hidden">
|
||||||
|
|
||||||
|
<button
|
||||||
|
onclick="toggleProjects()"
|
||||||
|
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
|
||||||
|
<span>{{ current_project.name|default:"Projekt wählen" }}</span>
|
||||||
|
|
||||||
|
<svg id="projectArrow"
|
||||||
|
class="w-4 h-4 transition-transform"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="projectMenu" class="hidden border-t border-slate-700">
|
||||||
|
|
||||||
|
{% for p in user_projects %}
|
||||||
|
<a href="{% url 'projects:overview' p.id %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700">
|
||||||
|
{{ p.name }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rechnungswesen -->
|
||||||
|
<div class="rounded-lg overflow-hidden">
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="toggleInvoicesMenu()"
|
||||||
|
class="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-700 transition">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
Rechnungswesen
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg id="invoiceArrow"
|
||||||
|
class="w-4 h-4 transition-transform duration-200"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="invoiceMenu" class="hidden border-t border-slate-700">
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Übersicht
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:invoices_offers_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Angebote
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:invoices_invoices_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Rechnungen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:invoices_reminders_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Mahnungen
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:customers_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Kunden
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'invoices:items_overview' %}"
|
||||||
|
class="block px-8 py-3 hover:bg-slate-700 transition">
|
||||||
|
Posten
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'core:dashboard' %}">
|
||||||
|
Finanzübersicht
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'core:dashboard' %}">
|
||||||
|
Arbeitszeiten
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'inventory_global:overview' %}">
|
||||||
|
Inventar
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'core:dashboard' %}">
|
||||||
|
Investitionen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'accounts:overview' %}">
|
||||||
|
Mitarbeiter
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full rounded-lg flex items-center justify-between px-4 py-3 hover:bg-slate-700">
|
||||||
|
<a href="{% url 'masterdata:overview' %}">
|
||||||
|
Einstellungen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="p-4 border-t border-white/20">
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<div class="w-full rounded-xl flex items-center justify-between px-4 py-3 hover:bg-slate-700 mb-4 border-white/20 border">
|
||||||
|
<a href="{% url 'admin:index' %}">Admin</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex gap-2 w-full">
|
||||||
|
<button id="theme-toggle" class="rounded-xl px-4 py-3 hover:bg-slate-700 border-white/20 border">🌙</button>
|
||||||
|
<form method="post" action="{% url 'accounts:logout' %}" class="w-full">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="w-full rounded-xl px-4 py-3 border-white/20 border" style="background-color: {{ company_settings.color }};">Abmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Hauptbereich -->
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<div class="flex-1 flex flex-col lg:ml-72">
|
||||||
|
{% else %}
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
{% endif %}
|
||||||
|
<header class="lg:hidden bg-white shadow h-16 flex items-center px-4 dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<button onclick="toggleSidebar()" class="text-3xl">☰</button>
|
||||||
|
<span class="ml-4 font-bold">{{ company_settings.companyname|default:"Projektverwaltung" }}</span>
|
||||||
|
</header>
|
||||||
|
<main class="flex-1 p-6 overflow-x-auto w-screen lg:w-auto">
|
||||||
|
<div class="p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<h1 class="text-3xl font-bold">
|
||||||
|
{% block header_title %}
|
||||||
|
{% endblock %}
|
||||||
|
</h1>
|
||||||
|
<p class="text-slate-400 mt-1">
|
||||||
|
{% block header_description %}
|
||||||
|
{% endblock %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="fixed top-5 right-5 z-50 space-y-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div
|
||||||
|
class="rounded-xl shadow-lg px-5 py-4 flex items-center gap-4
|
||||||
|
{% if message.tags == 'success' %}
|
||||||
|
bg-green-100 border border-green-300 text-green-800
|
||||||
|
{% elif message.tags == 'error' %}
|
||||||
|
bg-red-100 border border-red-300 text-red-800
|
||||||
|
{% elif message.tags == 'warning' %}
|
||||||
|
bg-yellow-100 border border-yellow-300 text-yellow-800
|
||||||
|
{% else %}
|
||||||
|
bg-blue-100 border border-blue-300 text-blue-800
|
||||||
|
{% endif %}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
setTimeout(() => {
|
||||||
|
document
|
||||||
|
.querySelectorAll(".fixed.top-5.right-5 > div")
|
||||||
|
.forEach(el => {
|
||||||
|
|
||||||
|
el.style.opacity = "0";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
el.remove();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}, 3000);
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleProjects() {
|
||||||
|
|
||||||
|
const menu = document.getElementById("projectMenu");
|
||||||
|
const arrow = document.getElementById("projectArrow");
|
||||||
|
|
||||||
|
menu.classList.toggle("hidden");
|
||||||
|
arrow.classList.toggle("rotate-180");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleInvoicesMenu() {
|
||||||
|
|
||||||
|
const menu = document.getElementById("invoiceMenu");
|
||||||
|
const arrow = document.getElementById("invoiceArrow");
|
||||||
|
|
||||||
|
menu.classList.toggle("hidden");
|
||||||
|
arrow.classList.toggle("rotate-180");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
|
||||||
|
const sidebar = document.getElementById("sidebar");
|
||||||
|
const overlay = document.getElementById("overlay");
|
||||||
|
|
||||||
|
sidebar.classList.toggle("-translate-x-full");
|
||||||
|
overlay.classList.toggle("hidden");
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const btn = document.getElementById("theme-toggle");
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
// Gespeichertes Theme anwenden
|
||||||
|
const saved = localStorage.getItem("theme");
|
||||||
|
if (saved === "dark") {
|
||||||
|
html.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
html.classList.remove("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
html.classList.toggle("dark");
|
||||||
|
localStorage.setItem("theme", html.classList.contains("dark") ? "dark" : "light");
|
||||||
|
btn.textContent = html.classList.contains("dark") ? "☀️" : "🌙";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Icon initial setzen
|
||||||
|
btn.textContent = html.classList.contains("dark") ? "☀️" : "🌙";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Dashboard{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Dashboard
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Willkommen {{ user.username }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-8">
|
||||||
|
<a href="{% url 'invoices:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Rechnungswesen</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!--
|
||||||
|
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Finanzübersicht</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Arbeitszeiten</h2>
|
||||||
|
</div>
|
||||||
|
</a> -->
|
||||||
|
<a href="{% url 'inventory_global:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Inventar</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<!--
|
||||||
|
<a href="" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Investitionen</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
-->
|
||||||
|
<a href="{% url 'accounts:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Mitarbeiter</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'masterdata:overview' %}" class="border border-white/20 p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
|
||||||
|
<div class="flex items-center gap-3 justify-center">
|
||||||
|
<h2 class="text-xl font-bold">Einstellungen</h2>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<a href="{% url 'projects:add' %}" class="btn btn-add">+ Projekt</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if memberships %}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{% for m in memberships %}
|
||||||
|
<a href="{% url 'projects:overview' m.project.id %}"
|
||||||
|
class="p-6 rounded-xl shadow hover:shadow-lg transition block text-white"
|
||||||
|
style="background: linear-gradient(135deg, {{ m.project.color }}, {{ m.project.color }}ff);">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
{{ m.project.name }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm mt-2">
|
||||||
|
Rolle: {{ m.get_role_display }}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||||
|
Du bist noch keinem aktiven Projekt zugewiesen.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if archived_count %}
|
||||||
|
<a href="{% url 'projects:archived' %}" class="text-sm text-slate-400 hover:text-slate-500">
|
||||||
|
{{ archived_count }} archivierte
|
||||||
|
{% if archived_count == 1 %}
|
||||||
|
Projekt anzeigen
|
||||||
|
{% else %}
|
||||||
|
Projekte anzeigen
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{% load django_tables2 %}
|
||||||
|
|
||||||
|
<table {% render_attrs table.attrs %}>
|
||||||
|
|
||||||
|
<thead class="bg-slate-100 dark:bg-slate-700 text-left">
|
||||||
|
<tr>
|
||||||
|
{% for column in table.columns %}
|
||||||
|
<th class="p-3">
|
||||||
|
{{ column.header }}
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for row in table.paginated_rows %}
|
||||||
|
|
||||||
|
<tr class="border-t">
|
||||||
|
|
||||||
|
{% for column, cell in row.items %}
|
||||||
|
|
||||||
|
<td class="p-3">
|
||||||
|
{{ cell }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% empty %}
|
||||||
|
|
||||||
|
<tr class="border-t">
|
||||||
|
<td colspan="{{ table.columns|length }}"
|
||||||
|
class="p-6 text-center text-slate-400">
|
||||||
|
|
||||||
|
Keine Daten vorhanden
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
{% load django_tables2 %}
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
|
||||||
|
<table {% render_attrs table.attrs %}>
|
||||||
|
|
||||||
|
<thead class="bg-slate-100 dark:bg-slate-700 text-left">
|
||||||
|
<tr>
|
||||||
|
<th class="p-3"></th>
|
||||||
|
|
||||||
|
{% for column in table.columns %}
|
||||||
|
<th class="p-3">
|
||||||
|
{{ column.header }}
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
{% for row in table.paginated_rows %}
|
||||||
|
|
||||||
|
<tr class="border-t">
|
||||||
|
|
||||||
|
<td class="p-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick="toggleItem('{{ table.toggle_prefix }}-{{ row.record.id }}')"
|
||||||
|
class="font-bold">
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
{% for column, cell in row.items %}
|
||||||
|
|
||||||
|
<td class="p-3">
|
||||||
|
{{ cell }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
{% if table.detail_template %}
|
||||||
|
|
||||||
|
<tr id="{{ table.toggle_prefix }}-{{ row.record.id }}"
|
||||||
|
class="hidden border-t">
|
||||||
|
|
||||||
|
<td colspan="{{ table.columns|length|add:'1' }}"
|
||||||
|
class="p-6">
|
||||||
|
|
||||||
|
{% include table.detail_template with item=row.record categories=table.categories locations=table.locations users=table.users %}
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
{% empty %}
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td colspan="{{ table.columns|length|add:1 }}"
|
||||||
|
class="p-6 text-center text-slate-400">
|
||||||
|
Keine Daten vorhanden
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function toggleItem(id) {
|
||||||
|
|
||||||
|
const element = document.getElementById(id);
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
console.log("Nicht gefunden:", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.classList.toggle("hidden");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from .views import dashboard
|
||||||
|
|
||||||
|
app_name = "core"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("", dashboard, name="dashboard"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import render
|
||||||
|
from projects.models import ProjectMembership
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def dashboard(request):
|
||||||
|
memberships = ProjectMembership.objects.select_related("project").filter(user=request.user,project__active=True)
|
||||||
|
archived_count = ProjectMembership.objects.filter(user=request.user,project__active=False).count()
|
||||||
|
return render(request,"core/dashboard.html",{"memberships": memberships,"archived_count": archived_count,})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import DocumentCategory, Document
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(DocumentCategory)
|
||||||
|
class DocumentCategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
@admin.register(Document)
|
||||||
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"title",
|
||||||
|
"project",
|
||||||
|
"category",
|
||||||
|
"uploaded_by",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
list_filter = (
|
||||||
|
"project",
|
||||||
|
"category",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
search_fields = (
|
||||||
|
"title",
|
||||||
|
)
|
||||||
|
|
||||||
|
readonly_fields = (
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
autocomplete_fields = (
|
||||||
|
"project",
|
||||||
|
"category",
|
||||||
|
"uploaded_by",
|
||||||
|
)
|
||||||
|
|
||||||
|
ordering = (
|
||||||
|
"-created_at",
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentsConfig(AppConfig):
|
||||||
|
name = 'documents'
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
from .models import Document
|
||||||
|
from core.forms.base import BaseForm
|
||||||
|
|
||||||
|
class DocumentForm(BaseForm):
|
||||||
|
class Meta:
|
||||||
|
model = Document
|
||||||
|
fields = ["title", "category", "file"]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.apply_style()
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||||
|
|
||||||
|
import documents.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Document',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('title', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
|
('file', models.FileField(upload_to=documents.models.document_upload_path, verbose_name='Datei')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DocumentCategory',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=120, unique=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Dokumentenkategorie',
|
||||||
|
'verbose_name_plural': 'Dokumentenkategorien',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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 = [
|
||||||
|
('documents', '0001_initial'),
|
||||||
|
('projects', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='projects.project'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='uploaded_by',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='documents.documentcategory', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from django.db import models
|
||||||
|
from projects.models import Project
|
||||||
|
import os
|
||||||
|
|
||||||
|
def document_upload_path(instance, filename):
|
||||||
|
return f"projects/{instance.project.id}/documents/{filename}"
|
||||||
|
|
||||||
|
class DocumentCategory(models.Model):
|
||||||
|
name = models.CharField(max_length=120, unique=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Dokumentenkategorie"
|
||||||
|
verbose_name_plural = "Dokumentenkategorien"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Document(models.Model):
|
||||||
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="documents")
|
||||||
|
category = models.ForeignKey(DocumentCategory, on_delete=models.CASCADE, verbose_name="Kategorie")
|
||||||
|
title = models.CharField(max_length=255, verbose_name="Name")
|
||||||
|
file = models.FileField(upload_to=document_upload_path, verbose_name="Datei")
|
||||||
|
uploaded_by = models.ForeignKey("auth.User", on_delete=models.SET_NULL, null=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extension(self):
|
||||||
|
return os.path.splitext(self.file.name)[1].lower()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.title
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from masterdata.registry import register_setting
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
DocumentCategory,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
group="Dokumente",
|
||||||
|
name="Kategorien",
|
||||||
|
model=DocumentCategory,
|
||||||
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ project.name }} - Dokumente
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Dokumente hochladen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Dokumente und Belege hochladen
|
||||||
|
{% 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">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
{{ form.as_p }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn mt-4 btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ project.name }} - Dokumente
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Dokumente
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Dateien & Bilder
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<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">Dokumente hochladen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<a href="{% url 'documents:add' project.id %}" class="btn btn-add">+ Dokument</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Kategorien -->
|
||||||
|
{% for cat, docs in grouped.items %}
|
||||||
|
|
||||||
|
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors mb-2" >
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h2 class="text-xl font-bold">
|
||||||
|
{{ cat.name }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<span class="text-sm text-slate-400">
|
||||||
|
{{ docs|length }} Dokumente
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if docs %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
|
||||||
|
{% for d in docs %}
|
||||||
|
|
||||||
|
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl shadow overflow-hidden transition">
|
||||||
|
|
||||||
|
<!-- Vorschau klickbar -->
|
||||||
|
<a href="{{ d.file.url }}" target="_blank">
|
||||||
|
|
||||||
|
{% if d.extension == ".jpg" or d.extension == ".jpeg" or d.extension == ".png" or d.extension == ".webp" %}
|
||||||
|
|
||||||
|
<div class="h-64 bg-slate-100 flex items-center justify-center overflow-hidden">
|
||||||
|
<img src="{{ d.file.url }}" class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
<div class="h-64 flex flex-col items-center justify-center bg-slate-100">
|
||||||
|
<div class="text-5xl mb-2">📄</div>
|
||||||
|
<div class="text-slate-400 text-sm">Keine Vorschau verfügbar</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Infos -->
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="font-semibold truncate">{{ d.title }}</h3>
|
||||||
|
<div class="mt-2 text-sm text-slate-400">Hochgeladen von {{ d.uploaded_by }}</div>
|
||||||
|
<div class="text-sm text-slate-400">{{ d.created_at|date:"d.m.Y H:i" }}</div>
|
||||||
|
<div class="mt-4 flex">
|
||||||
|
<form method="post" action="{% url 'documents:delete' project.id d.id %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-delete">Löschen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||||
|
Keine Dokumente vorhanden.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% empty %}
|
||||||
|
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
|
||||||
|
Noch keine Kategorien vorhanden.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "documents"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("<int:project_id>/documents/", views.document_overview, name="overview"),
|
||||||
|
path("<int:project_id>/documents/delete/<int:doc_id>/", views.document_delete, name="delete"),
|
||||||
|
path("<int:project_id>/documents/add/", views.document_add, name="add"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from accounts.decorators import permission_required
|
||||||
|
from projects.models import ProjectMembership
|
||||||
|
from .models import Document, DocumentCategory
|
||||||
|
from .forms import DocumentForm
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("projects_documents_view")
|
||||||
|
def document_overview(request, project_id):
|
||||||
|
|
||||||
|
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||||
|
project = membership.project
|
||||||
|
|
||||||
|
documents = Document.objects.filter(project=project).order_by("-created_at")
|
||||||
|
categories = DocumentCategory.objects.all()
|
||||||
|
|
||||||
|
form = DocumentForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = DocumentForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
doc = form.save(commit=False)
|
||||||
|
doc.project = project
|
||||||
|
doc.uploaded_by = request.user
|
||||||
|
doc.save()
|
||||||
|
return redirect("documents:overview", project.id)
|
||||||
|
|
||||||
|
# gruppiert nach Kategorie
|
||||||
|
grouped = {}
|
||||||
|
|
||||||
|
for cat in categories:
|
||||||
|
docs = list(documents.filter(category=cat))
|
||||||
|
|
||||||
|
if docs:
|
||||||
|
grouped[cat] = docs
|
||||||
|
|
||||||
|
uncategorized = documents.filter(category__isnull=True)
|
||||||
|
|
||||||
|
return render(request, "documents/overview.html", {
|
||||||
|
"project": project,
|
||||||
|
"form": form,
|
||||||
|
"grouped": grouped,
|
||||||
|
"uncategorized": uncategorized,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("projects_documents_delete")
|
||||||
|
def document_delete(request, project_id, doc_id):
|
||||||
|
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||||
|
doc = get_object_or_404(Document, id=doc_id, project=membership.project)
|
||||||
|
doc.file.delete()
|
||||||
|
doc.delete()
|
||||||
|
return redirect("documents:overview", project_id)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@permission_required("projects_documents_add")
|
||||||
|
def document_add(request, project_id):
|
||||||
|
membership = get_object_or_404(ProjectMembership, user=request.user, project_id=project_id)
|
||||||
|
project = membership.project
|
||||||
|
|
||||||
|
documents = Document.objects.filter(project=project).order_by("-created_at")
|
||||||
|
categories = DocumentCategory.objects.all()
|
||||||
|
|
||||||
|
form = DocumentForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = DocumentForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
doc = form.save(commit=False)
|
||||||
|
doc.project = project
|
||||||
|
doc.uploaded_by = request.user
|
||||||
|
doc.save()
|
||||||
|
return redirect("documents:overview", project.id)
|
||||||
|
else:
|
||||||
|
form = DocumentForm()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"documents/add.html",
|
||||||
|
{
|
||||||
|
"project": project,
|
||||||
|
"form": form,
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FinanceConfig(AppConfig):
|
||||||
|
name = 'finance'
|
||||||
@@ -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()
|
||||||
@@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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}"
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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 "/"
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -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"),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
})
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
InventoryCategory,
|
||||||
|
InventoryLocation,
|
||||||
|
InventoryItem,
|
||||||
|
ProjectInventoryItem,
|
||||||
|
ShoppingItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
@admin.register(InventoryCategory)
|
||||||
|
class InventoryCategoryAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InventoryLocation)
|
||||||
|
class InventoryLocationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(InventoryItem)
|
||||||
|
class InventoryItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"quantity",
|
||||||
|
"category",
|
||||||
|
"location",
|
||||||
|
"responsible",
|
||||||
|
"article_number",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"category",
|
||||||
|
"location",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
"article_number",
|
||||||
|
"description",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ProjectInventoryItem)
|
||||||
|
class ProjectInventoryItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"project",
|
||||||
|
"item",
|
||||||
|
"quantity",
|
||||||
|
"status",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"status",
|
||||||
|
"project",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"project__name",
|
||||||
|
"item__name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(ShoppingItem)
|
||||||
|
class ShoppingItemAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"project",
|
||||||
|
"quantity",
|
||||||
|
"price",
|
||||||
|
"category",
|
||||||
|
"supplier",
|
||||||
|
"created_at",
|
||||||
|
)
|
||||||
|
list_filter = (
|
||||||
|
"project",
|
||||||
|
"category",
|
||||||
|
"supplier",
|
||||||
|
)
|
||||||
|
search_fields = (
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"project__name",
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryConfig(AppConfig):
|
||||||
|
name = 'inventory'
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .models import InventoryItem, ShoppingItem
|
||||||
|
from core.forms.base import BaseForm
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItemForm(BaseForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"quantity",
|
||||||
|
"location",
|
||||||
|
"article_number",
|
||||||
|
"responsible",
|
||||||
|
"category",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.apply_style()
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingItemForm(BaseForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ShoppingItem
|
||||||
|
|
||||||
|
fields = [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"quantity",
|
||||||
|
"supplier",
|
||||||
|
"price",
|
||||||
|
"category",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.apply_style()
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# 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 = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InventoryCategory',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Inventarkategorie',
|
||||||
|
'verbose_name_plural': 'Inventarkategorien',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InventoryLocation',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, unique=True, verbose_name='Name')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Inventarstandort',
|
||||||
|
'verbose_name_plural': 'Inventarstandorte',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShoppingItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
|
||||||
|
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Preis')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Einkaufslistenartikel',
|
||||||
|
'verbose_name_plural': 'Einkaufslistenartikel',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='InventoryItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='Beschreibung')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=0, verbose_name='Anzahl')),
|
||||||
|
('article_number', models.CharField(blank=True, max_length=255, verbose_name='Artikelnummer')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorycategory', verbose_name='Kategorie')),
|
||||||
|
('responsible', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Verantwortlicher')),
|
||||||
|
('location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='inventory.inventorylocation', verbose_name='Standort')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Inventarartikel',
|
||||||
|
'verbose_name_plural': 'Inventarartikel',
|
||||||
|
'ordering': ['name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProjectInventoryItem',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('quantity', models.PositiveIntegerField(default=1, verbose_name='Anzahl')),
|
||||||
|
('status', models.CharField(choices=[('open', 'Offen'), ('packed', 'Eingepackt'), ('on_site', 'Vor Ort')], default='open', max_length=20, verbose_name='Status')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='inventory.inventoryitem', verbose_name='Artikel')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Projektartikel',
|
||||||
|
'verbose_name_plural': 'Projektartikel',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Generated by Django 6.0.6 on 2026-06-23 22:12
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('finance', '0002_initial'),
|
||||||
|
('inventory', '0001_initial'),
|
||||||
|
('projects', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='projectinventoryitem',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventory_items', to='projects.project', verbose_name='Projekt'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppingitem',
|
||||||
|
name='category',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.category', verbose_name='Kategorie'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppingitem',
|
||||||
|
name='project',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='shopping_items', to='projects.project', verbose_name='Projekt'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='shoppingitem',
|
||||||
|
name='supplier',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='finance.supplier', verbose_name='Lieferant'),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='projectinventoryitem',
|
||||||
|
unique_together={('project', 'item')},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from projects.models import Project
|
||||||
|
from finance.models import Category, Supplier
|
||||||
|
from django.db.models import Sum
|
||||||
|
|
||||||
|
class InventoryCategory(models.Model):
|
||||||
|
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Inventarkategorie"
|
||||||
|
verbose_name_plural = "Inventarkategorien"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryLocation(models.Model):
|
||||||
|
name = models.CharField(max_length=255, unique=True, verbose_name="Name")
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Inventarstandort"
|
||||||
|
verbose_name_plural = "Inventarstandorte"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryItem(models.Model):
|
||||||
|
name = models.CharField(max_length=255, verbose_name="Name")
|
||||||
|
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||||
|
quantity = models.PositiveIntegerField(default=0, verbose_name="Anzahl")
|
||||||
|
location = models.ForeignKey(InventoryLocation, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Standort")
|
||||||
|
article_number = models.CharField(max_length=255, blank=True, verbose_name="Artikelnummer")
|
||||||
|
responsible = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Verantwortlicher")
|
||||||
|
category = models.ForeignKey(InventoryCategory, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Inventarartikel"
|
||||||
|
verbose_name_plural = "Inventarartikel"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectInventoryItem(models.Model):
|
||||||
|
class Status(models.TextChoices):
|
||||||
|
OPEN = "open", "Offen"
|
||||||
|
PACKED = "packed", "Eingepackt"
|
||||||
|
ON_SITE = "on_site", "Vor Ort"
|
||||||
|
|
||||||
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="inventory_items", verbose_name="Projekt")
|
||||||
|
item = models.ForeignKey(InventoryItem, on_delete=models.CASCADE, verbose_name="Artikel")
|
||||||
|
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
|
||||||
|
status = models.CharField(max_length=20, choices=Status.choices, default=Status.OPEN, verbose_name="Status")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Projektartikel"
|
||||||
|
verbose_name_plural = "Projektartikel"
|
||||||
|
unique_together = ("project", "item")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.project} - {self.item}"
|
||||||
|
|
||||||
|
|
||||||
|
class ShoppingItem(models.Model):
|
||||||
|
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="shopping_items", verbose_name="Projekt")
|
||||||
|
name = models.CharField(max_length=255, verbose_name="Name")
|
||||||
|
description = models.TextField(blank=True, verbose_name="Beschreibung")
|
||||||
|
quantity = models.PositiveIntegerField(default=1, verbose_name="Anzahl")
|
||||||
|
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Lieferant")
|
||||||
|
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Preis")
|
||||||
|
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="Kategorie")
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Einkaufslistenartikel"
|
||||||
|
verbose_name_plural = "Einkaufslistenartikel"
|
||||||
|
ordering = ["name"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from masterdata.registry import register_setting
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
InventoryCategory,
|
||||||
|
InventoryLocation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
group="Inventar",
|
||||||
|
name="Kategorien",
|
||||||
|
model=InventoryCategory,
|
||||||
|
)
|
||||||
|
|
||||||
|
register_setting(
|
||||||
|
group="Inventar",
|
||||||
|
name="Standorte",
|
||||||
|
model=InventoryLocation,
|
||||||
|
)
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
from .models import ProjectInventoryItem, ShoppingItem, InventoryItem
|
||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
class InventoryGlobalTable(tables.Table):
|
||||||
|
toggle_prefix = "inv"
|
||||||
|
detail_template = "inventory/tables/inventory_global/edit_form.html"
|
||||||
|
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name="Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
category = tables.Column(
|
||||||
|
verbose_name="Kategorie"
|
||||||
|
)
|
||||||
|
|
||||||
|
available = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/inventory_global/available.html",
|
||||||
|
verbose_name="Anzahl",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
location = tables.Column(
|
||||||
|
verbose_name="Standort"
|
||||||
|
)
|
||||||
|
|
||||||
|
article_number = tables.Column(
|
||||||
|
verbose_name="Artikelnummer"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
template_name = "django_tables2/table_toggle.html"
|
||||||
|
fields = (
|
||||||
|
"expand",
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"available",
|
||||||
|
"location",
|
||||||
|
"article_number",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm min-w-[700px]"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class InventoryAddTable(tables.Table):
|
||||||
|
name = tables.Column(
|
||||||
|
accessor="name",
|
||||||
|
verbose_name="Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
category = tables.Column(
|
||||||
|
accessor="category",
|
||||||
|
verbose_name="Kategorie"
|
||||||
|
)
|
||||||
|
|
||||||
|
available = tables.Column(
|
||||||
|
accessor="available_quantity",
|
||||||
|
verbose_name="Verfügbar"
|
||||||
|
)
|
||||||
|
|
||||||
|
article_number = tables.Column(
|
||||||
|
accessor="article_number",
|
||||||
|
verbose_name="Artikelnummer"
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/inventory_add/quantity.html",
|
||||||
|
verbose_name="Anzahl hinzufügen",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = InventoryItem
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"available",
|
||||||
|
"article_number",
|
||||||
|
"quantity",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm min-w-[700px]"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShoppingItemTable(tables.Table):
|
||||||
|
|
||||||
|
name = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/name.html",
|
||||||
|
verbose_name="Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
category = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/category.html",
|
||||||
|
verbose_name="Kategorie"
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/quantity.html",
|
||||||
|
verbose_name="Anzahl"
|
||||||
|
)
|
||||||
|
|
||||||
|
supplier = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/supplier.html",
|
||||||
|
verbose_name="Lieferant"
|
||||||
|
)
|
||||||
|
|
||||||
|
price = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/price.html",
|
||||||
|
verbose_name="Preis"
|
||||||
|
)
|
||||||
|
|
||||||
|
description = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/description.html",
|
||||||
|
verbose_name="Beschreibung"
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/shopping/actions.html",
|
||||||
|
verbose_name="Aktionen",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ShoppingItem
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"quantity",
|
||||||
|
"supplier",
|
||||||
|
"price",
|
||||||
|
"description",
|
||||||
|
"actions",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProjectInventoryItemTable(tables.Table):
|
||||||
|
|
||||||
|
name = tables.Column(
|
||||||
|
accessor="item.name",
|
||||||
|
verbose_name="Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
category = tables.Column(
|
||||||
|
accessor="item.category",
|
||||||
|
verbose_name="Kategorie"
|
||||||
|
)
|
||||||
|
|
||||||
|
quantity = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/quantity.html",
|
||||||
|
verbose_name="Anzahl"
|
||||||
|
)
|
||||||
|
|
||||||
|
location = tables.Column(
|
||||||
|
accessor="item.location",
|
||||||
|
verbose_name="Standort"
|
||||||
|
)
|
||||||
|
|
||||||
|
article_number = tables.Column(
|
||||||
|
accessor="item.article_number",
|
||||||
|
verbose_name="Artikelnummer"
|
||||||
|
)
|
||||||
|
|
||||||
|
responsible = tables.Column(
|
||||||
|
accessor="item.responsible",
|
||||||
|
verbose_name="Verantwortlicher"
|
||||||
|
)
|
||||||
|
|
||||||
|
status = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/status.html",
|
||||||
|
verbose_name="Status"
|
||||||
|
)
|
||||||
|
|
||||||
|
actions = tables.TemplateColumn(
|
||||||
|
template_name="inventory/tables/actions.html",
|
||||||
|
verbose_name="Aktionen",
|
||||||
|
orderable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = ProjectInventoryItem
|
||||||
|
fields = (
|
||||||
|
"name",
|
||||||
|
"category",
|
||||||
|
"quantity",
|
||||||
|
"location",
|
||||||
|
"article_number",
|
||||||
|
"responsible",
|
||||||
|
"status",
|
||||||
|
"actions",
|
||||||
|
)
|
||||||
|
attrs = {
|
||||||
|
"class": "w-full text-sm"
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ project.name }} - Inventar
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Packartikel hinzufügen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Artikel zur Packliste hinzufügen
|
||||||
|
{% 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">
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn mt-4 btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Inventar
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Inventarartikel hinzufügen
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Neuen Artikel zum globalen Inventar 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" class="space-y-4">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 gap-4">
|
||||||
|
{{ form.as_p }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn mt-4 btn-save">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Inventar
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_title %}
|
||||||
|
Inventar
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header_description %}
|
||||||
|
Globale Inventarverwaltung
|
||||||
|
{% 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">Inventarartikel hinzufügen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<a href="{% url 'inventory_global:add' %}" class="btn btn-add">+ Artikel</a>
|
||||||
|
</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">Inventarliste</h2>
|
||||||
|
<p class="text-slate-400 text-sm mt-1">Gelagerte und verfügbare Artikel</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto rounded-xl">
|
||||||
|
{% render_table table %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user