--- id: WP-0010 title: Subunternehmer, Partner und Bibliothek status: done phase: 10-of-12 created: "2026-05-08" depends_on: WP-0009 --- # WP-0010 — Subunternehmer, Partner und Bibliothek Subunternehmer-Katalog, Dienstleistertypen, Nachweis-/Referenz-/Leistungsblatt-/ Entscheidungsregel-Verwaltung. Referenz: UC-SU-01 bis UC-SU-04, UC-BIB-01 bis UC-BIB-05. --- ```task id: WP-0010-T01 title: Subunternehmer-Katalog: Liste, Suche, Anlegen (UC-SU-01, UC-SU-03) status: done `partner/views.py` — subunternehmer_liste, subunternehmer_neu: Liste: Filter nach Dienstleistertyp, Präferenz (bevorzugt/zugelassen/gesperrt), Freitext-Suche. `Subunternehmer.objects.filter(name__icontains=q)` für Freitext. Präferenz 'gesperrt': Rot-Badge, wird in Suchergebnissen mit Warnsymbol angezeigt. `SubunternehmerForm(ModelForm)`: alle Felder, Präferenz als Radio-Buttons. subunternehmer_detail: Stammdaten + verknüpfte Ausschreibungen (über SubunternehmerZuordnung): ```python zuordnungen = SubunternehmerZuordnung.objects.filter( subunternehmer=obj ).select_related('ausschreibung', 'los').order_by('-ausschreibung__erstellt_am') ``` Zeigt: Ausschreibung, Los, Leistung, Zusage/Nachweis/Preis-Status. CustomAttribute-Panel. ``` ```task id: WP-0010-T02 title: Subunternehmer einer Ausschreibung/Los zuordnen (UC-SU-02) status: done `partner/views.py` — subunternehmer_zuordnen: HTMX-Modal auf Los-Detail-Seite: ```python def subunternehmer_suche_modal(request, ausschreibung_id, los_pk): q = request.GET.get('q', '') subunternehmer = Subunternehmer.objects.filter(name__icontains=q) return render(request, 'partner/partials/subunternehmer_suche.html', {'subunternehmer': subunternehmer, 'los_pk': los_pk, 'ausschreibung_id': ausschreibung_id}) def subunternehmer_zuordnen(request, ausschreibung_id, los_pk): if request.method == 'POST': sub_id = request.POST['subunternehmer_id'] sub = get_object_or_404(Subunternehmer, pk=sub_id) if sub.praeferenz == 'gesperrt': # Warnung anzeigen aber nicht blockieren pass los = get_object_or_404(Los, pk=los_pk) zuordnung, created = 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}) ``` Auf der Los-Detail-Seite zeigt jede Zuordnung: Name, Dienstleistertyp, Leistung, drei Checkboxen (Zusage, Nachweis, Preis) — HTMX-togglebar. ``` ```task id: WP-0010-T03 title: Dienstleistertyp-Katalog und Subunternehmer als gesperrt markieren (UC-SU-04) status: done `partner/views.py` — dienstleistertypen_liste, dienstleistertyp_neu/_bearbeiten: Einfache CRUD-Views für Dienstleistertypen (Katalog-Daten). `subunternehmer_praeferenz (POST)`: ```python def subunternehmer_praeferenz(request, pk): sub = get_object_or_404(Subunternehmer, pk=pk) if request.method == 'POST': sub.praeferenz = request.POST['praeferenz'] if sub.praeferenz == 'gesperrt': sub.bewertung = request.POST.get('begruendung', sub.bewertung) sub.save(update_fields=['praeferenz', 'bewertung']) return render(request, 'partner/partials/praeferenz_badge.html', {'sub': sub}) ``` Bei Präferenz 'gesperrt': Roter Warnhinweis wenn dieser Subunternehmer bei Zuordnung gewählt wird (im Suchmodal). `partner/urls.py`: ```python app_name = 'partner' urlpatterns = [ path('subunternehmer/', views.subunternehmer_liste, name='su_liste'), path('subunternehmer/neu/', views.subunternehmer_neu, name='su_neu'), path('subunternehmer//', views.subunternehmer_detail, name='su_detail'), path('subunternehmer//bearbeiten/', views.subunternehmer_bearbeiten, name='su_bearbeiten'), path('subunternehmer//praeferenz/', views.subunternehmer_praeferenz, name='su_praeferenz'), path('dienstleistertypen/', views.dienstleistertypen_liste, name='dt_liste'), path('dienstleistertypen/neu/', views.dienstleistertyp_neu, name='dt_neu'), ] ``` ``` ```task id: WP-0010-T04 title: Bibliothek: Nachweis-Katalog mit Ablaufwarnung (UC-BIB-01, UC-BIB-02) status: done `bibliothek/views.py` — nachweise_liste, nachweis_neu/_bearbeiten: Liste mit Ablauffilter: ```python from datetime import date, timedelta heute = date.today() in_60_tagen = heute + timedelta(days=60) ``` Tabs: "Alle", "Bald ablaufend" (`gueltig_bis__lte=in_60_tagen`), "Abgelaufen" (`gueltig_bis__lt=heute`). Abgelaufene Nachweise: Roter Badge. Bald ablaufende: Oranger Badge. `NachweisForm(ModelForm)`: `datei` als FileInput. `gueltig_ab`/`gueltig_bis` als DateInput type="date". Checkboxen für fuer_oeffentliche/fuer_privatwirtschaftliche. nachweis_neue_version: Analog zu Dokument-Versionierung (WP-0007-T03). Alten Nachweis auf status='ersetzt', neuen anlegen mit höherer Version. `bibliothek/urls.py` (Auszug): ```python path('nachweise/', views.nachweise_liste, name='nachweise_liste'), path('nachweise/neu/', views.nachweis_neu, name='nachweis_neu'), path('nachweise//', views.nachweis_detail, name='nachweis_detail'), path('nachweise//version/', views.nachweis_neue_version, name='nachweis_version'), ``` ``` ```task id: WP-0010-T05 title: Bibliothek: Referenz anlegen und zuordnen (UC-BIB-03, UC-BIB-04) status: done `bibliothek/views.py` — referenzen_liste, referenz_neu/_bearbeiten: `ReferenzForm(ModelForm)`: `whitepaper` als FileInput. `projektzeitraum` als CharField (freies Datum-Format: "2024-2025"). `leistungsblaetter` als CheckboxSelectMultiple. referenz_detail: Zeigt alle Felder, Whitepaper-Download-Link, verknüpfte Leistungsblätter. `referenz_zuordnen (POST)`: Wird von Ausschreibungs-Abgabe-Seite aufgerufen. Erstellt eine M2M-Verknüpfung zwischen Referenz und Ausschreibung. (Ergänze `referenzen` M2M-Feld auf Ausschreibung-Modell + Migration) HTMX-Suchmodal: Suche nach Branche, Leistungsbeschreibung, Titel. Zeigt Freigabestatus und Nutzungseinschränkungen als Warnung. ``` ```task id: WP-0010-T06 title: Bibliothek: Leistungsblatt und Entscheidungsregel (UC-BIB-05) status: done `bibliothek/views.py` — leistungsblaetter_liste, leistungsblatt_neu/_bearbeiten: Einfache CRUD-Views. `LeistungsblattForm(ModelForm)` mit allen Textfeldern. `bibliothek/views.py` — entscheidungsregeln_liste, entscheidungsregel_neu/_bearbeiten: `EntscheidungsregelForm(ModelForm)`: `kategorie` als Select mit Choices: [(ausschlusskriterium, 'Ausschlusskriterium'), (frist, 'Fristlage'), (referenz, 'Referenzanforderung'), (wirtschaftlichkeit, 'Wirtschaftlichkeit'), (ressourcen, 'Ressourcenverfügbarkeit'), (sonstiges, 'Sonstiges')] `empfehlung` als Radio-Buttons (teilnehmen/nicht_teilnehmen/pruefen). `aktiv`-Toggle in der Liste (HTMX POST). Auf der Entscheidungsseite (Phase 2) werden nur `aktiv=True` Regeln angezeigt. ``` ```task id: WP-0010-T07 title: Bibliothek URL-Verkabelung und Tests status: done `bibliothek/urls.py` vollständig: ```python app_name = 'bibliothek' urlpatterns = [ path('nachweise/', ...), path('referenzen/', ...), path('leistungsblaetter/', ...), path('entscheidungsregeln/', ...), # Detail/Neu/Bearbeiten für jede Entität ] ``` Global in Haupt-URLs: `path('bibliothek/', include('vergabe_teilnahme.apps.bibliothek.urls'))` `path('partner/', include('vergabe_teilnahme.apps.partner.urls'))` Tests: - Test: Nachweis mit abgelaufenem gueltig_bis → `ist_abgelaufen` property True - Test: Nachweis-Liste mit Filter "Abgelaufen" → nur abgelaufene sichtbar - Test: Subunternehmer-Zuordnung zu Los → SubunternehmerZuordnung-Objekt in DB - Test: Gesperrter Subunternehmer → Warnung im Modal sichtbar (Template enthält 'gesperrt') - Test: Entscheidungsregel mit aktiv=False → erscheint nicht in Phase-2-Auswertung ```