Files
vergabe-teilnahme/vergabe_teilnahme/apps/partner/views.py
tegwick 278cc1014c feat(partner,bibliothek): Subunternehmer-Katalog, Dienstleistertypen und Bibliothek (WP-0010)
Implementiert Subunternehmer-Katalog mit Suche/Filter, Zuordnung zu Losen via HTMX-Modal,
Dienstleistertyp-CRUD und Präferenz-Badges. Bibliothek: Nachweis-Katalog mit Ablaufwarnung
und Versionierung, Referenz-Katalog mit Ausschreibungszuordnung, Leistungsblatt-CRUD,
Entscheidungsregel-CRUD mit Aktiv-Toggle. Migration für referenzen M2M auf Ausschreibung.
56 Tests grün. Tests-Discovery auf tests.py-Dateien ausgedehnt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:27:53 +02:00

237 lines
9.1 KiB
Python

from django import forms
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
from vergabe_teilnahme.apps.lose.models import Los
from .models import Dienstleistertyp, Subunternehmer, SubunternehmerZuordnung
class SubunternehmerForm(forms.ModelForm):
class Meta:
model = Subunternehmer
fields = [
'name', 'kurzname', 'dienstleistertyp', 'praeferenz',
'strasse', 'plz', 'ort', 'land',
'telefon', 'mobilnummer', 'email', 'website',
'ansprechpartner', 'ansprechpartner_email', 'ansprechpartner_telefon',
'leistungsprofil', 'bewertung',
]
widgets = {
'name': forms.TextInput(attrs={'class': 'form-input'}),
'kurzname': forms.TextInput(attrs={'class': 'form-input'}),
'dienstleistertyp': forms.Select(attrs={'class': 'form-select'}),
'praeferenz': forms.RadioSelect(),
'strasse': forms.TextInput(attrs={'class': 'form-input'}),
'plz': forms.TextInput(attrs={'class': 'form-input'}),
'ort': forms.TextInput(attrs={'class': 'form-input'}),
'land': forms.TextInput(attrs={'class': 'form-input'}),
'telefon': forms.TextInput(attrs={'class': 'form-input'}),
'mobilnummer': forms.TextInput(attrs={'class': 'form-input'}),
'email': forms.EmailInput(attrs={'class': 'form-input'}),
'website': forms.URLInput(attrs={'class': 'form-input'}),
'ansprechpartner': forms.TextInput(attrs={'class': 'form-input'}),
'ansprechpartner_email': forms.EmailInput(attrs={'class': 'form-input'}),
'ansprechpartner_telefon': forms.TextInput(attrs={'class': 'form-input'}),
'leistungsprofil': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
'bewertung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
}
class DienstleistertypForm(forms.ModelForm):
class Meta:
model = Dienstleistertyp
fields = [
'name', 'beschreibung', 'typische_leistungen', 'typische_nachweise',
'relevante_standards', 'typische_preisbestandteile', 'bemerkungen',
]
widgets = {
'name': forms.TextInput(attrs={'class': 'form-input'}),
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'typische_leistungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'relevante_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'typische_preisbestandteile': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'bemerkungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
}
def subunternehmer_liste(request):
qs = Subunternehmer.objects.select_related('dienstleistertyp')
q = request.GET.get('q', '').strip()
if q:
qs = qs.filter(name__icontains=q)
praeferenz = request.GET.get('praeferenz', '')
if praeferenz:
qs = qs.filter(praeferenz=praeferenz)
dt_id = request.GET.get('dienstleistertyp', '')
if dt_id:
qs = qs.filter(dienstleistertyp_id=dt_id)
ctx = {
'subunternehmer': qs,
'dienstleistertypen': Dienstleistertyp.objects.all(),
'praeferenz_choices': Subunternehmer.PRAEFERENZ_CHOICES,
'q': q,
'current_praeferenz': praeferenz,
'current_dt': dt_id,
'breadcrumbs': [{'label': 'Subunternehmer', 'url': None}],
}
return render(request, 'partner/subunternehmer_liste.html', ctx)
def subunternehmer_neu(request):
if request.method == 'POST':
form = SubunternehmerForm(request.POST)
if form.is_valid():
obj = form.save()
messages.success(request, f'Subunternehmer „{obj.name}" angelegt.')
return redirect('partner:su_detail', pk=obj.pk)
else:
form = SubunternehmerForm()
return render(request, 'partner/subunternehmer_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
{'label': 'Neu', 'url': None},
],
})
def subunternehmer_detail(request, pk):
sub = get_object_or_404(Subunternehmer, pk=pk)
zuordnungen = SubunternehmerZuordnung.objects.filter(
subunternehmer=sub
).select_related('ausschreibung', 'los').order_by('-ausschreibung__erstellt_am')
return render(request, 'partner/subunternehmer_detail.html', {
'sub': sub,
'zuordnungen': zuordnungen,
'breadcrumbs': [
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
{'label': sub.name, 'url': None},
],
})
def subunternehmer_bearbeiten(request, pk):
sub = get_object_or_404(Subunternehmer, pk=pk)
if request.method == 'POST':
form = SubunternehmerForm(request.POST, instance=sub)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('partner:su_detail', pk=pk)
else:
form = SubunternehmerForm(instance=sub)
return render(request, 'partner/subunternehmer_form.html', {
'form': form,
'sub': sub,
'breadcrumbs': [
{'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'},
{'label': sub.name, 'url': f'/partner/subunternehmer/{pk}/'},
{'label': 'Bearbeiten', 'url': None},
],
})
def subunternehmer_praeferenz(request, pk):
sub = get_object_or_404(Subunternehmer, pk=pk)
if request.method == 'POST':
sub.praeferenz = request.POST.get('praeferenz', sub.praeferenz)
if sub.praeferenz == 'gesperrt':
begruendung = request.POST.get('begruendung', '').strip()
if begruendung:
sub.bewertung = begruendung
sub.save(update_fields=['praeferenz', 'bewertung'])
return render(request, 'partner/partials/praeferenz_badge.html', {'sub': sub})
def subunternehmer_suche_modal(request, ausschreibung_id, los_pk):
q = request.GET.get('q', '').strip()
subunternehmer = Subunternehmer.objects.filter(name__icontains=q).select_related('dienstleistertyp')
return render(request, 'partner/partials/subunternehmer_suche.html', {
'subunternehmer': subunternehmer,
'los_pk': los_pk,
'ausschreibung_id': ausschreibung_id,
'q': q,
})
def subunternehmer_zuordnen(request, ausschreibung_id, los_pk):
if request.method == 'POST':
sub_id = request.POST.get('subunternehmer_id')
sub = get_object_or_404(Subunternehmer, pk=sub_id)
los = get_object_or_404(Los, pk=los_pk)
zuordnung, _ = SubunternehmerZuordnung.objects.get_or_create(
subunternehmer=sub,
ausschreibung_id=ausschreibung_id,
los=los,
defaults={'konkrete_leistung': request.POST.get('konkrete_leistung', '')},
)
return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung})
return redirect('partner:su_liste')
def zuordnung_toggle(request, pk):
zuordnung = get_object_or_404(SubunternehmerZuordnung, pk=pk)
feld = request.POST.get('feld')
if feld in ('zusage_vorhanden', 'nachweis_eingegangen', 'preis_vorhanden'):
setattr(zuordnung, feld, not getattr(zuordnung, feld))
zuordnung.save(update_fields=[feld])
return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung})
def dienstleistertypen_liste(request):
typen = Dienstleistertyp.objects.all()
return render(request, 'partner/dienstleistertyp_liste.html', {
'typen': typen,
'breadcrumbs': [{'label': 'Dienstleistertypen', 'url': None}],
})
def dienstleistertyp_neu(request):
if request.method == 'POST':
form = DienstleistertypForm(request.POST)
if form.is_valid():
obj = form.save()
messages.success(request, f'Dienstleistertyp „{obj.name}" angelegt.')
return redirect('partner:dt_liste')
else:
form = DienstleistertypForm()
return render(request, 'partner/dienstleistertyp_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
{'label': 'Neu', 'url': None},
],
})
def dienstleistertyp_bearbeiten(request, pk):
obj = get_object_or_404(Dienstleistertyp, pk=pk)
if request.method == 'POST':
form = DienstleistertypForm(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('partner:dt_liste')
else:
form = DienstleistertypForm(instance=obj)
return render(request, 'partner/dienstleistertyp_form.html', {
'form': form,
'obj': obj,
'breadcrumbs': [
{'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'},
{'label': 'Bearbeiten', 'url': None},
],
})