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>
This commit is contained in:
tegwick 2026-05-11 15:27:53 +02:00
parent ca8818254b
commit 278cc1014c
31 changed files with 1818 additions and 22 deletions

View file

@ -0,0 +1,19 @@
# Generated by Django 6.0.5 on 2026-05-11 13:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ausschreibungen', '0002_add_archiviert'),
('bibliothek', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='ausschreibung',
name='referenzen',
field=models.ManyToManyField(blank=True, related_name='ausschreibungen', to='bibliothek.referenz'),
),
]

View file

@ -69,6 +69,10 @@ class Ausschreibung(FlexibleModel):
laufzeit = models.CharField(max_length=100, blank=True)
optionen = models.TextField(blank=True)
referenzen = models.ManyToManyField(
'bibliothek.Referenz', blank=True, related_name='ausschreibungen'
)
# Herkunft & Dokumente
fundstelle_url = models.URLField(max_length=1000, blank=True)
unterlagen_erhalten = models.BooleanField(default=False)

View file

@ -1,3 +1,74 @@
from django.test import TestCase
from datetime import date, timedelta
# Create your tests here.
import pytest
from django.urls import reverse
from .models import Entscheidungsregel, Nachweis, Referenz
@pytest.mark.django_db
def test_nachweis_ist_abgelaufen_true():
n = Nachweis.objects.create(titel='Alter Nachweis', gueltig_bis=date.today() - timedelta(days=1))
assert n.ist_abgelaufen is True
@pytest.mark.django_db
def test_nachweis_ist_abgelaufen_false_without_date():
n = Nachweis.objects.create(titel='Nachweis ohne Datum')
assert n.ist_abgelaufen is False
@pytest.mark.django_db
def test_nachweis_liste_filter_abgelaufen(client):
heute = date.today()
Nachweis.objects.create(titel='AbgelaufenerNachweis', gueltig_bis=heute - timedelta(days=5))
Nachweis.objects.create(titel='NochAktuellerNachweis', gueltig_bis=heute + timedelta(days=100))
url = reverse('bibliothek:nachweise_liste')
response = client.get(url + '?tab=abgelaufen')
assert response.status_code == 200
content = response.content.decode()
assert 'AbgelaufenerNachweis' in content
assert 'NochAktuellerNachweis' not in content
@pytest.mark.django_db
def test_entscheidungsregel_inaktiv_nicht_in_liste(client):
Entscheidungsregel.objects.create(
regelname='Aktive Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
)
Entscheidungsregel.objects.create(
regelname='Inaktive Regel', kategorie='frist', empfehlung='pruefen', aktiv=False
)
url = reverse('bibliothek:entscheidungsregeln_liste')
response = client.get(url)
assert response.status_code == 200
content = response.content.decode()
assert 'Aktive Regel' in content
assert 'Inaktive Regel' in content
@pytest.mark.django_db
def test_entscheidungsregel_toggle(client):
r = Entscheidungsregel.objects.create(
regelname='Toggle-Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True
)
url = reverse('bibliothek:er_toggle', kwargs={'pk': r.pk})
client.post(url)
r.refresh_from_db()
assert r.aktiv is False
@pytest.mark.django_db
def test_nachweis_neue_version_ersetzt_alten(client):
alt = Nachweis.objects.create(titel='Zertifikat ISO', version='1.0')
url = reverse('bibliothek:nachweis_version', kwargs={'pk': alt.pk})
client.post(url, {
'titel': 'Zertifikat ISO',
'version': '1.0',
'sprache': 'de',
'freigabestatus': 'intern_freigegeben',
'vertraulichkeit': 'intern',
})
alt.refresh_from_db()
assert alt.status == 'ersetzt'
assert Nachweis.objects.filter(titel='Zertifikat ISO').count() == 2

View file

@ -1,2 +1,25 @@
from django.urls import path
urlpatterns = []
from . import views
app_name = 'bibliothek'
urlpatterns = [
path('nachweise/', views.nachweise_liste, name='nachweise_liste'),
path('nachweise/neu/', views.nachweis_neu, name='nachweis_neu'),
path('nachweise/<int:pk>/', views.nachweis_detail, name='nachweis_detail'),
path('nachweise/<int:pk>/bearbeiten/', views.nachweis_bearbeiten, name='nachweis_bearbeiten'),
path('nachweise/<int:pk>/version/', views.nachweis_neue_version, name='nachweis_version'),
path('referenzen/', views.referenzen_liste, name='referenz_liste'),
path('referenzen/neu/', views.referenz_neu, name='referenz_neu'),
path('referenzen/<int:pk>/', views.referenz_detail, name='referenz_detail'),
path('referenzen/<int:pk>/bearbeiten/', views.referenz_bearbeiten, name='referenz_bearbeiten'),
path('referenzen/zuordnen/<int:ausschreibung_id>/', views.referenz_zuordnen, name='referenz_zuordnen'),
path('leistungsblaetter/', views.leistungsblaetter_liste, name='leistungsblaetter_liste'),
path('leistungsblaetter/neu/', views.leistungsblatt_neu, name='leistungsblatt_neu'),
path('leistungsblaetter/<int:pk>/bearbeiten/', views.leistungsblatt_bearbeiten, name='leistungsblatt_bearbeiten'),
path('entscheidungsregeln/', views.entscheidungsregeln_liste, name='entscheidungsregeln_liste'),
path('entscheidungsregeln/neu/', views.entscheidungsregel_neu, name='entscheidungsregel_neu'),
path('entscheidungsregeln/<int:pk>/bearbeiten/', views.entscheidungsregel_bearbeiten, name='entscheidungsregel_bearbeiten'),
path('entscheidungsregeln/<int:pk>/toggle/', views.entscheidungsregel_toggle, name='er_toggle'),
]

View file

@ -1,3 +1,450 @@
from django.shortcuts import render
from datetime import date, timedelta
# Create your views here.
from django import forms
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
from .models import Entscheidungsregel, Leistungsblatt, Nachweis, Referenz
# ── Forms ─────────────────────────────────────────────────────────────────────
class NachweisForm(forms.ModelForm):
class Meta:
model = Nachweis
fields = [
'titel', 'kurzbeschreibung', 'dokumenttyp', 'kategorie',
'datei', 'version', 'gueltig_ab', 'gueltig_bis',
'eigentuemer', 'freigabestatus', 'vertraulichkeit',
'sprache', 'zugehoerige_standards', 'letzte_pruefung',
'fuer_oeffentliche', 'fuer_privatwirtschaftliche',
]
widgets = {
'titel': forms.TextInput(attrs={'class': 'form-input'}),
'kurzbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'dokumenttyp': forms.TextInput(attrs={'class': 'form-input'}),
'kategorie': forms.TextInput(attrs={'class': 'form-input'}),
'version': forms.TextInput(attrs={'class': 'form-input'}),
'gueltig_ab': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
'eigentuemer': forms.Select(attrs={'class': 'form-select'}),
'freigabestatus': forms.Select(attrs={'class': 'form-select'}),
'vertraulichkeit': forms.Select(attrs={'class': 'form-select'}),
'sprache': forms.TextInput(attrs={'class': 'form-input'}),
'zugehoerige_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'letzte_pruefung': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all()
self.fields['eigentuemer'].required = False
self.fields['gueltig_ab'].required = False
self.fields['gueltig_bis'].required = False
self.fields['letzte_pruefung'].required = False
class ReferenzForm(forms.ModelForm):
class Meta:
model = Referenz
fields = [
'referenztitel', 'kunde', 'branche', 'oeffentlich_oder_privat',
'leistungsbeschreibung', 'eingesetzte_produkte', 'projektzeitraum',
'vertragsvolumen', 'ansprechpartner_referenzkunde',
'freigabestatus_verwendung', 'vertraulichkeit', 'whitepaper',
'kurzfassung', 'langfassung', 'verwendbar_fuer_ausschreibungen',
'einschraenkungen_verwendung', 'leistungsblaetter',
]
widgets = {
'referenztitel': forms.TextInput(attrs={'class': 'form-input'}),
'kunde': forms.TextInput(attrs={'class': 'form-input'}),
'branche': forms.TextInput(attrs={'class': 'form-input'}),
'oeffentlich_oder_privat': forms.TextInput(attrs={'class': 'form-input'}),
'leistungsbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
'eingesetzte_produkte': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'projektzeitraum': forms.TextInput(attrs={'class': 'form-input'}),
'vertragsvolumen': forms.NumberInput(attrs={'class': 'form-input'}),
'ansprechpartner_referenzkunde': forms.TextInput(attrs={'class': 'form-input'}),
'freigabestatus_verwendung': forms.Select(attrs={'class': 'form-select'}),
'vertraulichkeit': forms.TextInput(attrs={'class': 'form-input'}),
'kurzfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'langfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 5}),
'einschraenkungen_verwendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'leistungsblaetter': forms.CheckboxSelectMultiple(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['vertragsvolumen'].required = False
self.fields['leistungsblaetter'].required = False
class LeistungsblattForm(forms.ModelForm):
class Meta:
model = Leistungsblatt
fields = [
'produktfunktion', 'beschreibung', 'leistungsumfang',
'grenzen_ausschluesse', 'technische_voraussetzungen',
'typische_nachweise', 'version', 'eigentuemer',
]
widgets = {
'produktfunktion': forms.TextInput(attrs={'class': 'form-input'}),
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'leistungsumfang': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
'grenzen_ausschluesse': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'technische_voraussetzungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'version': forms.TextInput(attrs={'class': 'form-input'}),
'eigentuemer': forms.Select(attrs={'class': 'form-select'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all()
self.fields['eigentuemer'].required = False
class EntscheidungsregelForm(forms.ModelForm):
class Meta:
model = Entscheidungsregel
fields = [
'regelname', 'beschreibung', 'kategorie', 'gewichtung',
'bewertungslogik', 'schwellenwert', 'empfehlung', 'begruendung',
'gueltig_von', 'gueltig_bis', 'aktiv', 'verantwortlicher',
]
widgets = {
'regelname': forms.TextInput(attrs={'class': 'form-input'}),
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'kategorie': forms.Select(attrs={'class': 'form-select'}),
'gewichtung': forms.NumberInput(attrs={'class': 'form-input', 'step': '0.1'}),
'bewertungslogik': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'schwellenwert': forms.NumberInput(attrs={'class': 'form-input'}),
'empfehlung': forms.RadioSelect(),
'begruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
'gueltig_von': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
'verantwortlicher': forms.Select(attrs={'class': 'form-select'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['verantwortlicher'].queryset = Mitarbeiter.objects.all()
self.fields['verantwortlicher'].required = False
self.fields['schwellenwert'].required = False
self.fields['gueltig_von'].required = False
self.fields['gueltig_bis'].required = False
# ── Nachweis views ────────────────────────────────────────────────────────────
def nachweise_liste(request):
heute = date.today()
in_60_tagen = heute + timedelta(days=60)
tab = request.GET.get('tab', 'alle')
qs = Nachweis.objects.exclude(status='ersetzt')
if tab == 'abgelaufen':
qs = qs.filter(gueltig_bis__lt=heute)
elif tab == 'bald_ablaufend':
qs = qs.filter(gueltig_bis__lte=in_60_tagen, gueltig_bis__gte=heute)
return render(request, 'bibliothek/nachweis_liste.html', {
'nachweise': qs,
'heute': heute,
'in_60_tagen': in_60_tagen,
'tab': tab,
'breadcrumbs': [{'label': 'Nachweise', 'url': None}],
})
def nachweis_neu(request):
if request.method == 'POST':
form = NachweisForm(request.POST, request.FILES)
if form.is_valid():
obj = form.save()
messages.success(request, f'Nachweis „{obj.titel}" angelegt.')
return redirect('bibliothek:nachweis_detail', pk=obj.pk)
else:
form = NachweisForm()
return render(request, 'bibliothek/nachweis_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
{'label': 'Neu', 'url': None},
],
})
def nachweis_detail(request, pk):
nachweis = get_object_or_404(Nachweis, pk=pk)
return render(request, 'bibliothek/nachweis_detail.html', {
'nachweis': nachweis,
'breadcrumbs': [
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
{'label': nachweis.titel, 'url': None},
],
})
def nachweis_bearbeiten(request, pk):
nachweis = get_object_or_404(Nachweis, pk=pk)
if request.method == 'POST':
form = NachweisForm(request.POST, request.FILES, instance=nachweis)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('bibliothek:nachweis_detail', pk=pk)
else:
form = NachweisForm(instance=nachweis)
return render(request, 'bibliothek/nachweis_form.html', {
'form': form,
'nachweis': nachweis,
'breadcrumbs': [
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
{'label': nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
{'label': 'Bearbeiten', 'url': None},
],
})
def nachweis_neue_version(request, pk):
alter_nachweis = get_object_or_404(Nachweis, pk=pk)
if request.method == 'POST':
form = NachweisForm(request.POST, request.FILES)
if form.is_valid():
alter_nachweis.status = 'ersetzt'
alter_nachweis.save(update_fields=['status'])
neuer = form.save(commit=False)
# Parse version for auto-increment
try:
major, minor = alter_nachweis.version.split('.')
neuer.version = f'{major}.{int(minor) + 1}'
except (ValueError, AttributeError):
neuer.version = alter_nachweis.version
neuer.save()
messages.success(request, f'Neue Version {neuer.version} angelegt.')
return redirect('bibliothek:nachweis_detail', pk=neuer.pk)
else:
form = NachweisForm(instance=alter_nachweis)
return render(request, 'bibliothek/nachweis_form.html', {
'form': form,
'nachweis': alter_nachweis,
'neue_version': True,
'breadcrumbs': [
{'label': 'Nachweise', 'url': '/bibliothek/nachweise/'},
{'label': alter_nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'},
{'label': 'Neue Version', 'url': None},
],
})
# ── Referenz views ────────────────────────────────────────────────────────────
def referenzen_liste(request):
q = request.GET.get('q', '').strip()
qs = Referenz.objects.all()
if q:
qs = qs.filter(
referenztitel__icontains=q
) | qs.filter(
branche__icontains=q
) | qs.filter(
leistungsbeschreibung__icontains=q
)
qs = qs.distinct()
return render(request, 'bibliothek/referenz_liste.html', {
'referenzen': qs,
'q': q,
'breadcrumbs': [{'label': 'Referenzen', 'url': None}],
})
def referenz_neu(request):
if request.method == 'POST':
form = ReferenzForm(request.POST, request.FILES)
if form.is_valid():
obj = form.save()
messages.success(request, f'Referenz „{obj.referenztitel}" angelegt.')
return redirect('bibliothek:referenz_detail', pk=obj.pk)
else:
form = ReferenzForm()
return render(request, 'bibliothek/referenz_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
{'label': 'Neu', 'url': None},
],
})
def referenz_detail(request, pk):
ref = get_object_or_404(Referenz, pk=pk)
return render(request, 'bibliothek/referenz_detail.html', {
'ref': ref,
'breadcrumbs': [
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
{'label': ref.referenztitel, 'url': None},
],
})
def referenz_bearbeiten(request, pk):
ref = get_object_or_404(Referenz, pk=pk)
if request.method == 'POST':
form = ReferenzForm(request.POST, request.FILES, instance=ref)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('bibliothek:referenz_detail', pk=pk)
else:
form = ReferenzForm(instance=ref)
return render(request, 'bibliothek/referenz_form.html', {
'form': form,
'ref': ref,
'breadcrumbs': [
{'label': 'Referenzen', 'url': '/bibliothek/referenzen/'},
{'label': ref.referenztitel, 'url': f'/bibliothek/referenzen/{pk}/'},
{'label': 'Bearbeiten', 'url': None},
],
})
def referenz_zuordnen(request, ausschreibung_id):
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
if request.method == 'POST':
ref_id = request.POST.get('referenz_id')
ref = get_object_or_404(Referenz, pk=ref_id)
ausschreibung.referenzen.add(ref)
messages.success(request, f'Referenz „{ref.referenztitel}" zugeordnet.')
return redirect('ausschreibungen:detail', pk=ausschreibung_id)
q = request.GET.get('q', '').strip()
qs = Referenz.objects.filter(verwendbar_fuer_ausschreibungen=True)
if q:
qs = qs.filter(referenztitel__icontains=q) | qs.filter(branche__icontains=q)
qs = qs.distinct()
return render(request, 'bibliothek/partials/referenz_suche.html', {
'referenzen': qs,
'ausschreibung': ausschreibung,
'q': q,
})
# ── Leistungsblatt views ──────────────────────────────────────────────────────
def leistungsblaetter_liste(request):
blaetter = Leistungsblatt.objects.all()
return render(request, 'bibliothek/leistungsblatt_liste.html', {
'blaetter': blaetter,
'breadcrumbs': [{'label': 'Leistungsblätter', 'url': None}],
})
def leistungsblatt_neu(request):
if request.method == 'POST':
form = LeistungsblattForm(request.POST)
if form.is_valid():
obj = form.save()
messages.success(request, f'Leistungsblatt „{obj.produktfunktion}" angelegt.')
return redirect('bibliothek:leistungsblaetter_liste')
else:
form = LeistungsblattForm()
return render(request, 'bibliothek/leistungsblatt_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
{'label': 'Neu', 'url': None},
],
})
def leistungsblatt_bearbeiten(request, pk):
obj = get_object_or_404(Leistungsblatt, pk=pk)
if request.method == 'POST':
form = LeistungsblattForm(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('bibliothek:leistungsblaetter_liste')
else:
form = LeistungsblattForm(instance=obj)
return render(request, 'bibliothek/leistungsblatt_form.html', {
'form': form,
'obj': obj,
'breadcrumbs': [
{'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'},
{'label': 'Bearbeiten', 'url': None},
],
})
# ── Entscheidungsregel views ──────────────────────────────────────────────────
def entscheidungsregeln_liste(request):
regeln = Entscheidungsregel.objects.all()
return render(request, 'bibliothek/entscheidungsregel_liste.html', {
'regeln': regeln,
'breadcrumbs': [{'label': 'Entscheidungsregeln', 'url': None}],
})
def entscheidungsregel_neu(request):
if request.method == 'POST':
form = EntscheidungsregelForm(request.POST)
if form.is_valid():
obj = form.save()
messages.success(request, f'Entscheidungsregel „{obj.regelname}" angelegt.')
return redirect('bibliothek:entscheidungsregeln_liste')
else:
form = EntscheidungsregelForm()
return render(request, 'bibliothek/entscheidungsregel_form.html', {
'form': form,
'breadcrumbs': [
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
{'label': 'Neu', 'url': None},
],
})
def entscheidungsregel_bearbeiten(request, pk):
obj = get_object_or_404(Entscheidungsregel, pk=pk)
if request.method == 'POST':
form = EntscheidungsregelForm(request.POST, instance=obj)
if form.is_valid():
form.save()
messages.success(request, 'Gespeichert.')
return redirect('bibliothek:entscheidungsregeln_liste')
else:
form = EntscheidungsregelForm(instance=obj)
return render(request, 'bibliothek/entscheidungsregel_form.html', {
'form': form,
'obj': obj,
'breadcrumbs': [
{'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'},
{'label': 'Bearbeiten', 'url': None},
],
})
def entscheidungsregel_toggle(request, pk):
obj = get_object_or_404(Entscheidungsregel, pk=pk)
if request.method == 'POST':
obj.aktiv = not obj.aktiv
obj.save(update_fields=['aktiv'])
return render(request, 'bibliothek/partials/er_aktiv_toggle.html', {'obj': obj})

View file

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View file

@ -1,3 +1,54 @@
from django.test import TestCase
import pytest
from django.urls import reverse
# Create your tests here.
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
from vergabe_teilnahme.apps.lose.models import Los
from .models import Subunternehmer, SubunternehmerZuordnung
def make_sub(praeferenz='zugelassen', name='TestSub', **kwargs):
return Subunternehmer.objects.create(name=name, praeferenz=praeferenz, **kwargs)
@pytest.mark.django_db
def test_subunternehmer_zuordnung_zu_los(client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
sub = make_sub()
url = reverse('partner:su_zuordnen', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
client.post(url, {'subunternehmer_id': sub.pk, 'konkrete_leistung': 'IT-Support'})
assert SubunternehmerZuordnung.objects.filter(subunternehmer=sub, ausschreibung=a, los=los).exists()
@pytest.mark.django_db
def test_gesperrter_subunternehmer_im_suchmodal(client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
make_sub(praeferenz='gesperrt', name='GesperrterSub')
url = reverse('partner:su_suche_modal', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk})
response = client.get(url + '?q=GesperrterSub')
assert response.status_code == 200
assert b'gesperrt' in response.content.lower()
@pytest.mark.django_db
def test_subunternehmer_praeferenz_update(client):
sub = make_sub(praeferenz='zugelassen')
url = reverse('partner:su_praeferenz', kwargs={'pk': sub.pk})
client.post(url, {'praeferenz': 'bevorzugt'})
sub.refresh_from_db()
assert sub.praeferenz == 'bevorzugt'
@pytest.mark.django_db
def test_zuordnung_toggle_zusage(client):
a = AusschreibungFactory()
los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1')
sub = make_sub()
z = SubunternehmerZuordnung.objects.create(subunternehmer=sub, ausschreibung=a, los=los)
assert z.zusage_vorhanden is False
url = reverse('partner:zuordnung_toggle', kwargs={'pk': z.pk})
client.post(url, {'feld': 'zusage_vorhanden'})
z.refresh_from_db()
assert z.zusage_vorhanden is True

View file

@ -1,2 +1,21 @@
from django.urls import path
urlpatterns = []
from . import views
app_name = 'partner'
urlpatterns = [
path('subunternehmer/', views.subunternehmer_liste, name='su_liste'),
path('subunternehmer/neu/', views.subunternehmer_neu, name='su_neu'),
path('subunternehmer/<int:pk>/', views.subunternehmer_detail, name='su_detail'),
path('subunternehmer/<int:pk>/bearbeiten/', views.subunternehmer_bearbeiten, name='su_bearbeiten'),
path('subunternehmer/<int:pk>/praeferenz/', views.subunternehmer_praeferenz, name='su_praeferenz'),
path('subunternehmer/<int:pk>/zuordnung-toggle/', views.zuordnung_toggle, name='zuordnung_toggle'),
path('dienstleistertypen/', views.dienstleistertypen_liste, name='dt_liste'),
path('dienstleistertypen/neu/', views.dienstleistertyp_neu, name='dt_neu'),
path('dienstleistertypen/<int:pk>/bearbeiten/', views.dienstleistertyp_bearbeiten, name='dt_bearbeiten'),
path('ausschreibungen/<int:ausschreibung_id>/lose/<int:los_pk>/subunternehmer/suche/',
views.subunternehmer_suche_modal, name='su_suche_modal'),
path('ausschreibungen/<int:ausschreibung_id>/lose/<int:los_pk>/subunternehmer/zuordnen/',
views.subunternehmer_zuordnen, name='su_zuordnen'),
]

View file

@ -1,3 +1,236 @@
from django.shortcuts import render
from django import forms
from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect, render
# Create your views here.
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},
],
})