generated from coulomb/repo-seed
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>
202 lines
7.1 KiB
Markdown
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
|
|
```
|