Files
vergabe-teilnahme/workplans/WP-0009-abgabe-nachbetrachtung.md
tegwick a1cc317b3b feat(nachbetrachtung): Abgabe-Checkliste, Dokumentation und Nachbetrachtung (WP-0009)
Vollständigkeitsprüfung mit Freigaben-Check, Abgabe dokumentieren mit
Nachweis-Upload, Nachbetrachtung mit Kickoff-Aufgabe (gewonnen) und
Alpine.js-gesteuerter Verlustanalyse (verloren). 5 Tests grün.

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

202 lines
7.1 KiB
Markdown

---
id: WP-0009
title: Abgabe und Nachbetrachtung
status: done
phase: 9-of-12
created: "2026-05-08"
depends_on: WP-0008
---
# WP-0009 — Abgabe (Phase 6/7) und Nachbetrachtung (Phase 8)
Abgabe-Checkliste, Vollständigkeitsprüfung, Abgabe-Dokumentation mit Nachweis,
Ergebnis erfassen, Kickoff-Aufgabe erstellen, Verlustanalyse, Lessons Learned.
Referenz: UC-AB-01 bis UC-AB-03, UC-NB-01 bis UC-NB-03.
---
```task
id: WP-0009-T01
title: Abgabe-Checkliste mit Vollständigkeitsstatus (UC-AB-01)
status: done
`nachbetrachtung/abgabe_views.py` — abgabe_checkliste:
Vollständigkeitsprüfung-Service `abgabe_vollstaendigkeit(ausschreibung)`:
```python
def abgabe_vollstaendigkeit(ausschreibung):
from vergabe_teilnahme.apps.dokumente.models import Dokument
from vergabe_teilnahme.apps.core.models import Freigabe
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(ausschreibung)
freigaben = Freigabe.objects.filter(content_type=ct, object_id=ausschreibung.pk)
def hat_freigabe(typ):
return freigaben.filter(freigabe_typ=typ, status='erteilt').exists()
return {
'dokumente_gesamt': Dokument.objects.filter(ausschreibung=ausschreibung).count(),
'dokumente_freigegeben': Dokument.objects.filter(
ausschreibung=ausschreibung, status__in=['freigegeben', 'final_abgegeben']).count(),
'teilnahme_freigabe': hat_freigabe('teilnahme'),
'preis_freigabe': hat_freigabe('preis'),
'recht_freigabe': hat_freigabe('recht'),
'abgabe_freigabe': hat_freigabe('abgabe'),
'entscheidung_getroffen': ausschreibung.teilnahmeentscheidung == 'teilnahme',
}
```
Template `nachbetrachtung/abgabe.html`:
- Fortschrittsbalken oben (Anzahl erfüllter Checkpunkte / Gesamt)
- Checkliste mit Grün-/Rot-Badges für jeden Punkt
- Dokumente-Sektion: Liste aller Dokumente mit Status
- Freigaben-Sektion: Welche Freigaben vorliegen / fehlen
- Frist-Banner: "Abgabe bis: <Datum>" prominent oben
```
```task
id: WP-0009-T02
title: Abgabe dokumentieren mit Nachweis-Upload (UC-AB-02)
status: done
`nachbetrachtung/abgabe_views.py` — abgabe_dokumentieren:
`AbgabeForm(Form)`:
- `abgabe_zeitpunkt` DateTimeInput(type='datetime-local')
- `abgabe_plattform` CharField
- `verantwortlicher` ModelChoiceField(Mitarbeiter)
- `abgabenachweis` FileField (Eingangsbestätigung, Screenshot etc.)
- `kommentar` Textarea
```python
def abgabe_dokumentieren(request, ausschreibung_id):
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
if request.method == 'POST':
form = AbgabeForm(request.POST, request.FILES)
if form.is_valid():
# Abgabenachweis als Dokument speichern
if form.cleaned_data.get('abgabenachweis'):
Dokument.objects.create(
ausschreibung=ausschreibung,
datei=form.cleaned_data['abgabenachweis'],
kategorie='abgabenachweis',
status='final_abgegeben',
finale_abgabeversion=True,
)
# Ausschreibungsstatus auf Abgegeben setzen
ausschreibung.status = 9
ausschreibung.save(update_fields=['status', 'geaendert_am'])
# Alle Dokumente mit finale_abgabeversion=True: gesperrt (kein weiterer Upload)
return redirect('ausschreibungen:detail', pk=ausschreibung_id)
else:
form = AbgabeForm()
return render(request, 'nachbetrachtung/abgabe_formular.html',
{'form': form, 'ausschreibung': ausschreibung})
```
```
```task
id: WP-0009-T03
title: Nachbetrachtung-View — Ergebnis und Kickoff (UC-NB-01)
status: done
`nachbetrachtung/views.py` — nachbetrachtung_detail:
Erstellt oder lädt die OneToOne `Nachbetrachtung` für die Ausschreibung.
`NachbetrachtungForm(ModelForm)`:
Felder: ergebnis (Radio), zuschlagsdatum, projektverantwortlicher (Select Mitarbeiter).
Bei Ergebnis 'gewonnen' (POST):
1. Setze Ausschreibungsstatus auf 10
2. Erstelle automatisch Aufgabe:
```python
Aufgabe.objects.get_or_create(
ausschreibung=ausschreibung,
titel="Kickoff vorbereiten",
defaults={
'typ': 'fachlich',
'prioritaet': 1,
'verantwortlicher': form.cleaned_data.get('projektverantwortlicher'),
'beschreibung': f"Kickoff für {ausschreibung.titel}. Angebotsumfang und Annahmen übergeben.",
}
)
```
3. Flash-Meldung: "Kickoff-Aufgabe erstellt für <Projektverantwortlicher>"
Template `nachbetrachtung/detail.html`:
- Ergebnis-Formular oben
- Bei Ergebnis 'gewonnen': Übergabe-Abschnitt (Projektverantwortlicher, Kickoff-Aufgabe-Link)
- Bei Ergebnis 'verloren': Verlustanalyse-Abschnitt (aus UC-NB-02)
```
```task
id: WP-0009-T04
title: Verlustanalyse und Lessons Learned (UC-NB-02, UC-NB-03)
status: done
**Verlustgründe** — dynamisches JSONField-Formular:
Alpine.js-gesteuertes Array:
```html
<div x-data="{ gruende: {{ nachbetrachtung.verlustgruende|json_script:'gruende'|safe }} }">
<template x-for="(g, i) in gruende" :key="i">
<div class="flex gap-2 mb-2">
<input x-model="g.grund" class="form-input flex-1" placeholder="Verlustgrund">
<select x-model="g.kategorie" class="form-input w-32">
<option value="preis">Preis</option>
<option value="referenz">Referenz</option>
<option value="anforderung">Anforderung</option>
<option value="subunternehmer">Subunternehmer</option>
<option value="sonstiges">Sonstiges</option>
</select>
<input type="number" x-model.number="g.verlaesslichkeit" min="1" max="5"
class="form-input w-20" placeholder="1-5">
<button @click="gruende.splice(i, 1)" class="btn-ghost text-red-500">✕</button>
</div>
</template>
<button @click="gruende.push({grund:'', kategorie:'sonstiges', verlaesslichkeit:3})"
class="btn-secondary">+ Verlustgrund</button>
<input type="hidden" name="verlustgruende" :value="JSON.stringify(gruende)">
</div>
```
Ausschlaggebende Merkmale, Lessons Learned, Empfehlungen: einfache Textareas.
Checkbox "Wiederverwendbare Erkenntnisse markieren".
Bei Speichern: Aktualisiere `Nachbetrachtung`-Objekt, Redirect zur Nachbetrachtungsseite.
```
```task
id: WP-0009-T05
title: URL-Verkabelung Abgabe/Nachbetrachtung und Tests
status: done
`nachbetrachtung/abgabe_urls.py`:
```python
urlpatterns = [
path('', abgabe_views.abgabe_checkliste, name='checkliste'),
path('dokumentieren/', abgabe_views.abgabe_dokumentieren, name='dokumentieren'),
path('problem/', abgabe_views.abgabe_problem, name='problem'),
]
```
`nachbetrachtung/urls.py`:
```python
app_name = 'nachbetrachtung'
urlpatterns = [
path('', views.nachbetrachtung_detail, name='detail'),
]
```
`abgabe_problem (POST)`:
Setzt `ausschreibung.status` auf internen Marker "Problem bei Abgabe" (eigenes Status-Choice ergänzen wenn nötig, oder Kommentarfeld).
Tests:
- Test: abgabe_vollstaendigkeit ohne Freigaben → alle False
- Test: Freigabe erteilen → entsprechendes Feld True
- Test: Ergebnis 'gewonnen' → Kickoff-Aufgabe wird erstellt, Status 10
- Test: Ergebnis 'verloren' → Status 11
- Test: Verlustgründe JSONField gespeichert mit korrekter Struktur
```