Initial commit

This commit is contained in:
Felix Baumann
2026-06-27 17:07:34 +00:00
commit fb47c53d52
256 changed files with 21270 additions and 0 deletions
View File
+53
View File
@@ -0,0 +1,53 @@
from django.contrib import admin
from .models import Project, ProjectMembership, ProjectContact, ProjectLocation
class ProjectMembershipInline(admin.TabularInline):
model = ProjectMembership
extra = 1
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = (
"name",
"active",
"created_at",
)
search_fields = (
"name",
)
inlines = [
ProjectMembershipInline
]
@admin.register(ProjectMembership)
class ProjectMembershipAdmin(admin.ModelAdmin):
list_display = (
"user",
"project",
"role",
)
list_filter = (
"role",
)
@admin.register(ProjectContact)
class ProjectContactAdmin(admin.ModelAdmin):
list_display = ("project", "contact", "created_at")
search_fields = ("project__name", "contact__first_name", "contact__last_name")
autocomplete_fields = ("project", "contact")
list_filter = ("project",)
@admin.register(ProjectLocation)
class ProjectLocationAdmin(admin.ModelAdmin):
list_display = ("project", "location", "created_at")
search_fields = ("project__name", "location__name")
autocomplete_fields = ("project", "location")
list_filter = ("project",)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProjectsConfig(AppConfig):
name = 'projects'
+102
View File
@@ -0,0 +1,102 @@
from django import forms
from core.forms.base import BaseForm
from invoices.models import CustomerContact, CustomerLocation
from .models import ProjectContact, ProjectLocation, Project, ProjectMembership
from django.contrib.auth import get_user_model
User = get_user_model()
class ProjectAddForm(BaseForm):
class Meta:
model = Project
fields = [
"name",
"customer",
"logo",
"color",
"icon",
"hourly_rate",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
class ProjectForm(BaseForm):
class Meta:
model = Project
fields = [
"name",
"logo",
"color",
"icon",
"hourly_rate",
"active"
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
class ProjectAddContactForm(BaseForm):
class Meta:
model = ProjectContact
fields = ["contact"]
def __init__(self, *args, **kwargs):
project = kwargs.pop("project", None)
super().__init__(*args, **kwargs)
qs = CustomerContact.objects.none()
if project and project.customer:
qs = CustomerContact.objects.filter(
customer=project.customer
).exclude(
project_links__project=project
)
self.fields["contact"].queryset = qs
self.apply_style()
class ProjectAddLocationForm(BaseForm):
class Meta:
model = ProjectLocation
fields = ["location"]
def __init__(self, *args, **kwargs):
project = kwargs.pop("project", None)
super().__init__(*args, **kwargs)
qs = CustomerLocation.objects.none()
if project and project.customer:
qs = CustomerLocation.objects.filter(
customer=project.customer
).exclude(
project_links__project=project
)
self.fields["location"].queryset = qs
self.apply_style()
class ProjectAddWorkerForm(BaseForm):
class Meta:
model = ProjectMembership
fields = [
"user",
"role",
]
def __init__(self, *args, project=None, **kwargs):
super().__init__(*args, **kwargs)
self.apply_style()
if project:
self.fields["user"].queryset = User.objects.exclude(
project_memberships__project=project
)
+71
View File
@@ -0,0 +1,71 @@
# Generated by Django 6.0.6 on 2026-06-23 22:12
import django.db.models.deletion
import projects.models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('invoices', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Project',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Name')),
('logo', models.ImageField(blank=True, null=True, upload_to=projects.models.logo_upload_path, verbose_name='Logo')),
('color', models.CharField(default='#2563eb', max_length=20, verbose_name='Farbe')),
('icon', models.CharField(blank=True, max_length=100, verbose_name='Icon')),
('active', models.BooleanField(default=True, verbose_name='Status')),
('hourly_rate', models.DecimalField(decimal_places=2, default=25, max_digits=10, verbose_name='Stundenlohn')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='projects', to='invoices.customer', verbose_name='Kunde')),
],
),
migrations.CreateModel(
name='ProjectContact',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_links', to='invoices.customercontact', verbose_name='Ansprechpartner')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_contacts', to='projects.project', verbose_name='Projekt')),
],
options={
'unique_together': {('project', 'contact')},
},
),
migrations.CreateModel(
name='ProjectLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_links', to='invoices.customerlocation', verbose_name='Standort')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_locations', to='projects.project', verbose_name='Projekt')),
],
options={
'unique_together': {('project', 'location')},
},
),
migrations.CreateModel(
name='ProjectMembership',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.CharField(choices=[('owner', 'Eigentümer'), ('manager', 'Manager'), ('employee', 'Mitarbeiter')], default='employee', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='projects.project')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_memberships', to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('project', 'user')},
},
),
]
View File
+131
View File
@@ -0,0 +1,131 @@
from django.db import models
from django.contrib.auth import get_user_model
from invoices.models import Customer, CustomerContact, CustomerLocation
import os
User = get_user_model()
def logo_upload_path(instance, filename):
return f"projects/{instance.id}/logo/{filename}"
class Project(models.Model):
name = models.CharField(max_length=255, verbose_name="Name")
logo = models.ImageField(upload_to=logo_upload_path, blank=True, null=True, verbose_name="Logo")
color = models.CharField(max_length=20, default="#2563eb", verbose_name="Farbe")
icon = models.CharField(max_length=100, blank=True, verbose_name="Icon")
active = models.BooleanField(default=True, verbose_name="Status")
customer = models.ForeignKey(
Customer,
on_delete=models.PROTECT,
null=True,
blank=True,
related_name="projects",
verbose_name="Kunde"
)
hourly_rate = models.DecimalField(max_digits=10,decimal_places=2,default=25,verbose_name="Stundenlohn")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
customer_changed = False
if self.pk:
old_customer_id = (
Project.objects
.filter(pk=self.pk)
.values_list("customer_id", flat=True)
.first()
)
customer_changed = old_customer_id != self.customer_id
super().save(*args, **kwargs)
if customer_changed:
self.project_contacts.all().delete()
self.project_locations.all().delete()
def __str__(self):
return self.name
class ProjectMembership(models.Model):
class Role(models.TextChoices):
OWNER = "owner", "Eigentümer"
MANAGER = "manager", "Manager"
EMPLOYEE = "employee", "Mitarbeiter"
project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name="memberships")
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="project_memberships")
role = models.CharField(max_length=20, choices=Role.choices, default=Role.EMPLOYEE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ("project", "user")
def __str__(self):
return f"{self.user} - {self.project} ({self.role})"
class ProjectContact(models.Model):
project = models.ForeignKey(
"projects.Project",
on_delete=models.CASCADE,
related_name="project_contacts",
verbose_name="Projekt"
)
contact = models.ForeignKey(
CustomerContact,
on_delete=models.CASCADE,
related_name="project_links",
verbose_name="Ansprechpartner"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
def clean(self):
super().clean()
if not self.project_id or not self.location_id:
return
if self.location.customer_id != self.project.customer_id:
raise ValidationError({
"location": "Der Ansprechpartner gehört nicht zum Kunden des Projekts."
})
class Meta:
unique_together = ("project", "contact")
class ProjectLocation(models.Model):
project = models.ForeignKey(
"projects.Project",
on_delete=models.CASCADE,
related_name="project_locations",
verbose_name="Projekt"
)
location = models.ForeignKey(
CustomerLocation,
on_delete=models.CASCADE,
related_name="project_links",
verbose_name="Standort"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Erstellt am")
def clean(self):
super().clean()
if not self.project_id or not self.location_id:
return
if self.location.customer_id != self.project.customer_id:
raise ValidationError({
"location": "Der Standort gehört nicht zum Kunden des Projekts."
})
class Meta:
unique_together = ("project", "location")
+32
View File
@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}
Projekt - Erstellen
{% endblock %}
{% block header_title %}
Projekt erstellen
{% endblock %}
{% block header_description %}
Ein neues Projekt erstellen
{% 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 %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}
Projekt - Archiviert
{% endblock %}
{% block header_title %}
Archivierte Projekte
{% endblock %}
{% block header_description %}
Abgeschlossene und deaktivierte Projekte
{% endblock %}
{% block content %}
{% 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 }}cc);">
<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-yellow-100 border border-yellow-300 rounded-xl p-6">
Keine archivierten Projekte vorhanden.
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Informationen
{% endblock %}
{% block header_title %}
Ansprechpartner hinzufügen
{% endblock %}
{% block header_description %}
Neuen Ansprechpartner für das Projekt 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">
<form method="post" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<div class="mt-6 flex gap-2">
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</div>
</form>
</div>
{% endblock %}
+249
View File
@@ -0,0 +1,249 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Informationen
{% endblock %}
{% block header_title %}
Projektinformationen
{% endblock %}
{% block header_description %}
Standorte & Ansprechpartner
{% 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">Standorte und Ansprechpartner hinzufügen</p>
</div>
<div class="flex flex-col sm:flex-row gap-2">
<a href="{% url 'projects:location_add' project.id %}" class="btn btn-add">+ Standort</a>
<a href="{% url 'projects:contact_add' project.id %}" class="btn btn-add">+ Ansprechpartner</a>
<a href="{% url 'projects:worker_add' project.id %}" class="btn btn-add">+ Mitarbeiter</a>
</div>
</div>
</div>
</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">
<h2 class="text-xl font-bold mb-2">Allgemein</h2>
<form method="post" enctype="multipart/form-data" action="{% url 'projects:edit' project.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="update_project">
<div class="grid md:grid-cols-2 gap-4">
{% for field in form %}
<div>
<label class="block text-sm font-medium mb-1">
{{ field.label }}
</label>
{{ field }}
{% if field.errors %}
<div class="text-red-600 text-sm mt-1">
{{ field.errors }}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<button type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</form>
</div>
</div>
<div class="grid grid-cols-1 gap-5 mb-8">
<!-- Mitarbeiter -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-3">
Mitarbeiter
</h2>
{% for worker in workers %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-3 mb-2 flex justify-between">
<div>
<div class="font-medium">{{ worker.user.get_full_name|default:worker.user.username }}</div>
<div class="text-slate-400 text-sm">{{ worker.user.email }}</div>
<div class="text-slate-400 text-sm">Rolle: {{ worker.get_role_display }}</div>
</div>
{% if worker.role != "owner" %}
<form method="post" action="{% url 'projects:worker_delete' project.id worker.id %}">
{% csrf_token %}
<input type="hidden" name="action" value="delete_worker">
<input type="hidden" name="worker_id" value="{{ worker.id }}">
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
{% endif %}
</div>
{% empty %}
<div class="bg-yellow-100 border border-yellow-300 rounded-xl p-6">
Keine Mitarbeiter vorhanden
</div>
{% endfor %}
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<!-- Standorte -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-3">
Standorte
</h2>
{% for location in locations %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-3 mb-2 flex justify-between">
<div>
<div class="font-medium">
{{ location.location.name }}
</div>
<div class="text-slate-400 text-sm">
{{ location.location.street }}
</div>
<div class="text-slate-400 text-sm">
{{ location.location.zip_code }} {{ location.location.city }}
</div>
<div class="text-slate-400 text-sm">
{{ location.location.country }}
</div>
</div>
<form method="post" action="{% url 'projects:location_delete' project.id location.id %}">
{% csrf_token %}
<input
type="hidden"
name="action"
value="delete_location">
<input
type="hidden"
name="location_id"
value="{{ location.id }}">
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
</div>
{% empty %}
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
Keine Standorte vorhanden
</div>
{% endfor %}
</div>
<!-- Ansprechpartner -->
<div class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold mb-3">
Ansprechpartner
</h2>
{% for contact in contacts %}
<div class="bg-slate-100 dark:bg-slate-700 rounded-xl p-3 mb-2 flex justify-between">
<div>
<div class="font-medium">
{{ contact.contact.first_name }} {{ contact.contact.last_name }}
</div>
{% if contact.contact.company %}
<div class="text-slate-400 text-sm">
{{ contact.contact.company }}
</div>
{% endif %}
{% if contact.contact.phone %}
<div class="text-slate-400 text-sm">
{{ contact.contact.phone }}
</div>
{% endif %}
{% if contact.contact.email %}
<div class="text-slate-400 text-sm">
{{ contact.contact.email }}
</div>
{% endif %}
</div>
<form method="post" action="{% url 'projects:contact_delete' project.id contact.id %}">
{% csrf_token %}
<input
type="hidden"
name="action"
value="delete_contact">
<input
type="hidden"
name="contact_id"
value="{{ contact.id }}">
<button
type="submit"
class="btn btn-delete">
Löschen
</button>
</form>
</div>
{% empty %}
<div class="bg-white dark:bg-slate-800 border border-orange-500 rounded-xl p-6 text-orange-500">
Keine Ansprechpartner vorhanden
</div>
{% endfor %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Informationen
{% endblock %}
{% block header_title %}
Standort hinzufügen
{% endblock %}
{% block header_description %}
Neuen Projektstandort 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">
<form method="post" class="space-y-4">
{% csrf_token %}
{{ form.as_p }}
<div class="mt-6 flex gap-2">
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</div>
</form>
</div>
{% endblock %}
+80
View File
@@ -0,0 +1,80 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }}
{% endblock %}
{% block header_title %}
{{ project.name }}
{% endblock %}
{% block header_description %}
Projektübersicht und Module
{% endblock %}
{% block content %}
<!-- MODULE GRID -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<!-- FINANZEN -->
<a href="{% url 'finance:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">💰</div>
<h2 class="text-xl font-bold">Finanzen</h2>
<p class="text-slate-400 text-sm mt-1">Einnahmen & Ausgaben</p>
</a>
<!-- KASSE -->
<a href="" class="relative p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="absolute top-3 right-3 text-xs font-semibold px-2 py-1 rounded-full text-white" style="background-color: {{ company_settings.color }};">
In Arbeit
</div>
<div class="text-2xl mb-2">🏦</div>
<h2 class="text-xl font-bold">Kasse</h2>
<p class="text-slate-400 text-sm mt-1">Buchungen vornehmen</p>
</a>
<!-- ZEITERFASSUNG -->
<a href="{% url 'time_tracking:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">⏱</div>
<h2 class="text-xl font-bold">Zeiterfassung</h2>
<p class="text-slate-400 text-sm mt-1">Arbeitszeiten der Helfer</p>
</a>
<!-- DOKUMENTE -->
<a href="{% url 'documents:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">📁</div>
<h2 class="text-xl font-bold">Dokumente</h2>
<p class="text-slate-400 text-sm mt-1">Dateien & Bilder</p>
</a>
<!-- LISTE -->
<a href="{% url 'inventory:overview' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">🛒</div>
<h2 class="text-xl font-bold">Pack-/Einkaufsliste</h2>
<p class="text-slate-400 text-sm mt-1">Vorhandenen & benötigtes Material</p>
</a>
<!-- Projektinformationen -->
<a href="{% url 'projects:info' project.id %}" class="p-6 rounded-xl shadow bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<div class="text-2xl mb-2">ℹ️</div>
<h2 class="text-xl font-bold">Projektinformationen</h2>
<p class="text-slate-400 text-sm mt-1">Standorte & Ansprechpartner</p>
</a>
</div>
<!-- OPTIONAL INFO BOX -->
<div class="p-6 rounded-xl shadow mb-8 bg-white dark:bg-slate-800 text-slate-900 dark:text-white transition-colors">
<h2 class="text-xl font-bold">
Zusammenfassung
</h2>
<p class="text-slate-400 text-sm mt-1">
folgt
</p>
</div>
{% endblock %}
@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}
{{ project.name }} - Informationen
{% endblock %}
{% block header_title %}
Mitarbeiter hinzufügen
{% endblock %}
{% block header_description %}
Benutzer zum Projekt 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 %}
{{ form.as_p }}
<div class="mt-6 flex gap-2">
<button
type="submit"
class="btn mt-4 btn-save">
Speichern
</button>
</div>
</form>
</div>
{% endblock %}
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+18
View File
@@ -0,0 +1,18 @@
from django.urls import path
from . import views
app_name = "projects"
urlpatterns = [
path("add/", views.project_add, name="add"),
path("archived/", views.projects_archived, name="archived"),
path("<int:project_id>/", views.project_overview, name="overview"),
path("<int:project_id>/info/", views.project_info, name="info"),
path("<int:project_id>/info/edit/",views.project_edit,name="edit"),
path("<int:project_id>/info/contact/add/", views.project_contact_add, name="contact_add"),
path("<int:project_id>/info/contact/<int:contact_id>/delete/", views.project_contact_delete, name="contact_delete"),
path("<int:project_id>/info/location/add/", views.project_location_add, name="location_add"),
path("<int:project_id>/info/location/<int:location_id>/delete/", views.project_location_delete, name="location_delete"),
path("<int:project_id>/info/worker/add/", views.project_worker_add, name="worker_add"),
path("<int:project_id>/info/worker/<int:worker_id>/delete/", views.project_worker_delete, name="worker_delete"),
]
+310
View File
@@ -0,0 +1,310 @@
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 .forms import ProjectForm, ProjectAddLocationForm, ProjectAddContactForm, ProjectAddForm, ProjectAddWorkerForm
from .models import (
Project,
ProjectMembership,
ProjectContact,
ProjectLocation,
)
from invoices.models import CustomerContact, CustomerLocation
@login_required
@permission_required("projects_add")
def project_add(request):
form = ProjectAddForm(request.POST or None,request.FILES or None,)
if request.method == "POST" and form.is_valid():
project = form.save()
ProjectMembership.objects.create(project=project,user=request.user,role=ProjectMembership.Role.OWNER,)
return redirect("projects:overview",project.id)
return render(request,"projects/add.html",{"form": form,})
@login_required
@permission_required("projects_archived_view")
def projects_archived(request):
memberships = ProjectMembership.objects.select_related("project").filter(user=request.user,project__active=False)
return render(request,"projects/archived.html",{"memberships": memberships,})
# ---------------------------
# OVERVIEW
# ---------------------------
@login_required
@permission_required("projects_view")
def project_overview(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
return render(request, "projects/overview.html", {
"project": project,
"role": membership.role
})
@login_required
@permission_required("projects_detail_view")
def project_info(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
form = ProjectForm(
instance=project
)
workers = project.memberships.select_related(
"user"
).all()
return render(
request,
"projects/info.html",
{
"project": project,
"form": form,
"locations": project.project_locations.select_related(
"location"
),
"contacts": project.project_contacts.select_related(
"contact"
),
"workers": workers,
}
)
@login_required
@permission_required("projects_edit")
def project_edit(request, project_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method != "POST":
return redirect(
"projects:info",
project.id
)
form = ProjectForm(
request.POST,
request.FILES,
instance=project
)
if form.is_valid():
form.save()
return redirect(
"projects:info",
project.id
)
# ---------------------------
# ADD CONTACT TO PROJECT
# ---------------------------
@login_required
@permission_required("projects_contact_add")
def project_contact_add(request, project_id):
project = get_object_or_404(Project, pk=project_id)
if request.method == "POST":
form = ProjectAddContactForm(
request.POST,
project=project,
)
if form.is_valid():
contact = form.cleaned_data["contact"]
ProjectContact.objects.get_or_create(
project=project,
contact=contact
)
return redirect("projects:info", project.id)
else:
form = ProjectAddContactForm(project=project)
return render(request, "projects/contact_add.html", {
"form": form,
"project": project,
})
@login_required
@permission_required("projects_contact_delete")
def project_contact_delete(request, project_id, contact_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method == "POST":
ProjectContact.objects.filter(
id=contact_id,
project=project
).delete()
return redirect(
"projects:info",
project.id
)
# ---------------------------
# ADD LOCATION TO PROJECT
# ---------------------------
@login_required
@permission_required("projects_location_add")
def project_location_add(request, project_id):
project = get_object_or_404(Project, pk=project_id)
if request.method == "POST":
form = ProjectAddLocationForm(
request.POST,
project=project,
)
if form.is_valid():
location = form.cleaned_data["location"]
ProjectLocation.objects.get_or_create(
project=project,
location=location
)
return redirect("projects:info", project.id)
else:
form = ProjectAddLocationForm(project=project)
return render(request, "projects/location_add.html", {
"form": form,
"project": project,
})
@login_required
@permission_required("projects_location_delete")
def project_location_delete(request, project_id, location_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
if request.method == "POST":
ProjectLocation.objects.filter(
id=location_id,
project=project
).delete()
return redirect(
"projects:info",
project.id
)
@login_required
@permission_required("projects_user_add")
def project_worker_add(request, project_id):
project = get_object_or_404(Project, pk=project_id)
get_object_or_404(ProjectMembership,project=project,user=request.user)
form = ProjectAddWorkerForm(request.POST or None,project=project)
if request.method == "POST" and form.is_valid():
membership = form.save(commit=False)
membership.project = project
membership.save()
return redirect("projects:info",project.id)
return render(request,"projects/worker_add.html",{"form": form,"project": project,})
@login_required
@permission_required("projects_user_delete")
def project_worker_delete(request, project_id, worker_id):
membership = get_object_or_404(
ProjectMembership,
user=request.user,
project_id=project_id
)
project = membership.project
worker = get_object_or_404(
ProjectMembership,
id=worker_id,
project=project
)
if request.method == "POST":
if worker.role != ProjectMembership.Role.OWNER:
worker.delete()
return redirect(
"projects:info",
project.id
)