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

7.1 KiB

id, title, status, phase, created, depends_on
id title status phase created depends_on
WP-0009 Abgabe und Nachbetrachtung done 9-of-12 2026-05-08 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.


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: " 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.",
    }
)
  1. Flash-Meldung: "Kickoff-Aufgabe erstellt für "

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:

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