--- id: WP-0005 title: Lose und Anforderungen status: done phase: 5-of-12 created: "2026-05-08" depends_on: WP-0004 --- # WP-0005 — Lose und Anforderungen Implementiert alle Views, Forms und Templates für Lose (UC-LA-01) und Anforderungen (UC-LA-02 bis UC-LA-05) inklusive Nachweis-Verknüpfung und Ausschlusskriterium-Eskalation. **URL-Präfix:** `/ausschreibungen//lose/` und `.../anforderungen/` --- ```task id: WP-0005-T01 title: Lose-Liste und Lose anlegen (UC-LA-01) status: done `lose/views.py` — lose_liste und los_neu: lose_liste: Zeigt alle Lose einer Ausschreibung geordnet nach Losnummer. Template: `lose/liste.html` — Tabelle mit Losnummer, Lostitel, Zuständiger, Teilnahme (Badge), Status. "+ Los hinzufügen"-Button öffnet Inline-Formular via HTMX. `LosForm(ModelForm)`: Felder losnummer, lostitel, beschreibung, abgrenzung, zustaendiger, teilnahme. Alle Inputs mit `form-input`, Textarea mit 3 rows. los_neu (POST): Erstellt Los, gibt bei HTMX-Request nur neuen Tabellen-Row zurück. los_bearbeiten (GET/POST): Edit in eigenem Template oder Inline. los_loeschen (POST): Löscht Los nach Bestätigung. URLs in `lose/urls.py`: ```python path('', views.lose_liste, name='liste'), path('neu/', views.los_neu, name='neu'), path('/', views.los_detail, name='detail'), path('/bearbeiten/', views.los_bearbeiten, name='bearbeiten'), ``` ``` ```task id: WP-0005-T02 title: Los-Detail-Seite mit eingebetteten Anforderungen status: done `lose/views.py` — los_detail: ```python def los_detail(request, ausschreibung_id, los_pk): los = get_object_or_404(Los, pk=los_pk, ausschreibung_id=ausschreibung_id) ctx = { 'los': los, 'ausschreibung': los.ausschreibung, 'anforderungen': los.anforderungen.all().order_by('verbindlichkeit', 'titel'), 'subunternehmer': SubunternehmerZuordnung.objects.filter(los=los), ... } return render(request, 'lose/detail.html', ctx) ``` `lose/detail.html`: - Los-Stammdaten (render_field Tags) - Abschnitt "Anforderungen" — Tabelle mit Link zur Anforderungsdetailseite - Abschnitt "Subunternehmer" — Liste zugeordneter Subunternehmer - CustomAttribute-Panel (HTMX lazy, wie in WP-0012) - Abschnitt "Teilnahme": Toggle Ja/Nein/Offen (HTMX POST) ``` ```task id: WP-0005-T03 title: Anforderungsliste nach Los gruppiert (UC-LA-02) status: done `lose/views.py` — anforderungen_liste: Lädt alle Anforderungen der Ausschreibung, gruppiert nach Los. Anforderungen ohne Los-Zuordnung erscheinen in "Allgemein". Template `lose/anforderungen_liste.html`: - Filter-Leiste: Verbindlichkeit (Muss/Soll/Kann), Erfüllungsstatus, Los, Zuständiger - Alle Filteränderungen via HTMX ohne Reload - Pro Gruppe: aufklappbarer Akkordeon-Abschnitt (Alpine.js x-show) - Jede Zeile: Titel, Verbindlichkeit-Badge, Erfüllungsstatus-Badge, Zuständiger - Rote Hervorhebung bei Ausschlusskriterium + nicht_erfuellbar - "+ Anforderung" Button oben rechts ``` ```task id: WP-0005-T04 title: Anforderung anlegen und Detailseite (UC-LA-02, UC-LA-03) status: done `AnforderungForm(ModelForm)`: alle Felder aus Modell. Besonderer Widget für verbindlichkeit: Radio-Buttons statt Dropdown. anforderung_neu: Liest ausschreibung_id aus URL. Los-Dropdown zeigt nur Lose der aktuellen Ausschreibung. anforderung_detail: Zeigt alle Felder (via render_field), Kommentarverlauf als Timeline (für v1: einfache Textarea + gespeicherte Kommentare als JSONField auf Anforderung). Verknüpfte Dokumente, Nachweise und Aufgaben als Listen. Erfüllungsstatus inline ändern (UC-LA-03): HTMX-Endpunkt `anforderung_status`: ```python def anforderung_status(request, ausschreibung_id, pk): a = get_object_or_404(Anforderung, pk=pk, ausschreibung_id=ausschreibung_id) if request.method == 'POST': a.erfuellungsstatus = request.POST['erfuellungsstatus'] a.save(update_fields=['erfuellungsstatus']) return render(request, 'lose/partials/erfuellungsstatus_widget.html', {'anforderung': a}) ``` ``` ```task id: WP-0005-T05 title: Nachweis-Verknüpfung mit Bibliothek (UC-LA-04) status: done `lose/views.py` — nachweis_suche_modal und nachweis_zuordnen: Auf der Anforderungsdetailseite: Button "Nachweis zuordnen". ```html
``` nachweis_suche_modal (GET): Gibt Such-Modal zurück mit Textfeld + Ergebnisliste (HTMX-Suche im Bibliothek-Bestand). Jeder Treffer zeigt: Titel, Ablaufdatum, Freigabestatus. Ablaufende/abgelaufene Nachweise in Orange/Rot. nachweis_zuordnen (POST): Fügt Nachweis via M2M hinzu. nachweis_entfernen (DELETE/POST): Entfernt M2M-Verknüpfung. Zeige zugeordnete Nachweise auf Anforderungsdetail als Liste mit Ablaufstatus-Badge. ``` ```task id: WP-0005-T06 title: Ausschlusskriterium-Eskalation auf Phase-2-Seite (UC-LA-05) status: done Ergänze `ausschreibungen/views.py` — ausschreibung_entscheidung: Vor dem Laden der Seite: Prüfe ob es Anforderungen mit `ausschlusskriterium=True AND erfuellungsstatus='nicht_erfuellbar'` gibt. Falls ja: Zeige oben auf der Entscheidungsseite einen roten Alert-Banner: ```html {% if ausschlusskriterien_nicht_erfuellbar %}

⚠ Nicht erfüllbare Ausschlusskriterien

    {% for a in ausschlusskriterien_nicht_erfuellbar %}
  • {{ a.titel }} (Los: {{ a.los|default:"Allgemein" }})
  • {% endfor %}

Empfehlung: Nichtteilnahme

{% endif %} ``` Prüfe: Seed-Daten mit einer nicht erfüllbaren Muss-Anforderung + Ausschlusskriterium anlegen, dann Phase-2-Seite öffnen → Banner erscheint. ``` ```task id: WP-0005-T07 title: Aufgabe aus Anforderung ableiten (UC-AU-02) status: done Auf der Anforderungsdetailseite: Button "Aufgabe erstellen". ```python def anforderung_aufgabe_erstellen(request, ausschreibung_id, pk): anforderung = get_object_or_404(Anforderung, pk=pk) if request.method == 'POST': from vergabe_teilnahme.apps.aufgaben.models import Aufgabe Aufgabe.objects.create( ausschreibung_id=ausschreibung_id, los=anforderung.los, anforderung=anforderung, titel=f"Klärung: {anforderung.titel[:200]}", typ='fachlich', verantwortlicher=anforderung.zustaendiger, ) return redirect('ausschreibungen:lose:anforderung_detail', ausschreibung_id=ausschreibung_id, pk=pk) return render(request, 'lose/aufgabe_erstellen_confirm.html', {'anforderung': anforderung}) ``` Nach Erstellen: Anforderungsdetail zeigt die neue Aufgabe im Abschnitt "Verbundene Aufgaben". ``` ```task id: WP-0005-T08 title: Tests für Lose und Anforderungen status: done `lose/tests/test_views.py`: - Test: Lose-Liste gibt 200 zurück - Test: Los anlegen mit POST → Redirect, Los existiert in DB - Test: Anforderung anlegen mit Muss-Verbindlichkeit - Test: Erfüllungsstatus via HTMX-POST auf 'nicht_erfuellbar' setzen - Test: Ausschlusskriterium + nicht_erfuellbar → Entscheidungsseite zeigt Alert-Banner - Test: Nachweis-Verknüpfung über M2M Nutze AusschreibungFactory und erstelle Factories für Los und Anforderung. ```