7.3 KiB
id, title, status, phase, created, depends_on
| id | title | status | phase | created | depends_on |
|---|---|---|---|---|---|
| WP-0005 | Lose und Anforderungen | done | 5-of-12 | 2026-05-08 | 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/<ausschreibung_id>/lose/ und .../anforderungen/
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('<int:los_pk>/', views.los_detail, name='detail'),
path('<int:los_pk>/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
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
<button hx-get="{% url 'lose:nachweis_suche' ausschreibung.pk anforderung.pk %}"
hx-target="#nachweis-modal"
class="btn-secondary">Nachweis zuordnen</button>
<div id="nachweis-modal"></div>
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 %}
<div class="bg-red-50 border border-red-300 rounded-lg p-4 mb-6">
<p class="font-semibold text-red-700">⚠ Nicht erfüllbare Ausschlusskriterien</p>
<ul class="mt-2 text-sm text-red-600 list-disc ml-4">
{% for a in ausschlusskriterien_nicht_erfuellbar %}
<li>{{ a.titel }} (Los: {{ a.los|default:"Allgemein" }})</li>
{% endfor %}
</ul>
<p class="text-sm text-red-500 mt-2">Empfehlung: Nichtteilnahme</p>
</div>
{% 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.