fix(WP-0013): Feedback-Bugs — alle 8 Einträge aus Backlog behoben

- Fristen-Widget-Format: DateInput/DateTimeInput mit ISO-Format-Attribut, damit
  Browser date/datetime-local korrekt vorausfüllen (Feedback #4)
- Phase 2 Teilnahmeentscheidung: URL in build_phase_nav von
  /teilnahmeentscheidung/ → /entscheidung/ korrigiert (Feedback #6)
- Phase 3 Detaillierte Durchsicht: URL in build_phase_nav von
  /anforderungen/ → /lose/anforderungen/ korrigiert (Feedback #7)
- Phase 7 Abgabe: order_by('bezeichnung') → order_by('beschreibung') in
  abgabe_views.py (Dokument hat kein Feld 'bezeichnung') (Feedback #8)
- Ausschreibungen-Liste: Ausschreiber zuerst, Titel zweite Spalte,
  neues geschätztes Volumen (Feedback #5)
- Feedback-Backlog Leerstand: bereits durch vorherigen URL-Fix abgedeckt
  (Feedback #1)
- Rechtsgrundlage (VgV/UVgO/VOB/A/SektVO/GWB) als neues Formularfeld
  incl. Migration (Feedback #2)
- Bindefrist in Tagen + berechnetes Enddatum als Modell-Property
  bindefrist_berechnet, Formular und Detailansicht erweitert (Feedback #3)

68/68 Tests grün.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 00:31:55 +02:00
parent d6873d7b88
commit 7903f59f85
10 changed files with 240 additions and 14 deletions

View File

@@ -7,10 +7,10 @@ class AusschreibungForm(forms.ModelForm):
class Meta:
model = Ausschreibung
fields = [
'titel', 'ausschreiber', 'vergabeplattform', 'vergabenummer', 'vergabeart',
'titel', 'ausschreiber', 'vergabeplattform', 'vergabenummer', 'vergabeart', 'rechtsgrundlage',
'fundstelle_url', 'bid_manager', 'leistungsbeschreibung',
'branche', 'schlagwoerter', 'geschaetztes_volumen',
'veroeffentlichungsdatum', 'bieterfragen_bis', 'abgabe_bis', 'bindefrist',
'veroeffentlichungsdatum', 'bieterfragen_bis', 'abgabe_bis', 'bindefrist', 'bindefrist_tage',
'unterlagen_erhalten', 'unterlagen_erhalten_am',
'teilnahmeentscheidung', 'entscheidungsbegruendung',
]
@@ -20,17 +20,19 @@ class AusschreibungForm(forms.ModelForm):
'vergabeplattform': forms.TextInput(attrs={'class': 'form-input'}),
'vergabenummer': forms.TextInput(attrs={'class': 'form-input'}),
'vergabeart': forms.Select(attrs={'class': 'form-input'}),
'rechtsgrundlage': forms.Select(attrs={'class': 'form-input'}),
'fundstelle_url': forms.URLInput(attrs={'class': 'form-input'}),
'bid_manager': forms.Select(attrs={'class': 'form-input'}),
'leistungsbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
'branche': forms.TextInput(attrs={'class': 'form-input'}),
'schlagwoerter': forms.TextInput(attrs={'class': 'form-input'}),
'geschaetztes_volumen': forms.NumberInput(attrs={'class': 'form-input'}),
'veroeffentlichungsdatum': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
'bieterfragen_bis': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
'abgabe_bis': forms.DateTimeInput(attrs={'class': 'form-input', 'type': 'datetime-local'}),
'bindefrist': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
'unterlagen_erhalten_am': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
'veroeffentlichungsdatum': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}, format='%Y-%m-%d'),
'bieterfragen_bis': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}, format='%Y-%m-%d'),
'abgabe_bis': forms.DateTimeInput(attrs={'class': 'form-input', 'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M'),
'bindefrist': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}, format='%Y-%m-%d'),
'bindefrist_tage': forms.NumberInput(attrs={'class': 'form-input', 'min': '1', 'placeholder': 'Tage'}),
'unterlagen_erhalten_am': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}, format='%Y-%m-%d'),
'teilnahmeentscheidung': forms.Select(attrs={'class': 'form-input'}),
'entscheidungsbegruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-13 22:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ausschreibungen', '0003_referenzen_m2m'),
]
operations = [
migrations.AddField(
model_name='ausschreibung',
name='rechtsgrundlage',
field=models.CharField(blank=True, choices=[('vgv', 'VgV'), ('uvgo', 'UVgO'), ('vob_a', 'VOB/A'), ('sektvo', 'SektVO'), ('gwb', 'GWB'), ('sonstige', 'Sonstige')], max_length=20),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-13 22:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ausschreibungen', '0004_rechtsgrundlage'),
]
operations = [
migrations.AddField(
model_name='ausschreibung',
name='bindefrist_tage',
field=models.PositiveSmallIntegerField(blank=True, help_text='Bindefrist in Tagen ab Abgabe', null=True),
),
]

View File

@@ -1,4 +1,4 @@
from datetime import date
from datetime import date, timedelta
from django.db import models
@@ -34,12 +34,21 @@ class Ausschreibung(FlexibleModel):
('rahmenvertrag', 'Rahmenvertrag'),
('sonstige', 'Sonstige'),
]
RECHTSGRUNDLAGE_CHOICES = [
('vgv', 'VgV'),
('uvgo', 'UVgO'),
('vob_a', 'VOB/A'),
('sektvo', 'SektVO'),
('gwb', 'GWB'),
('sonstige', 'Sonstige'),
]
titel = models.CharField(max_length=400)
ausschreiber = models.CharField(max_length=300)
vergabeplattform = models.CharField(max_length=200, blank=True)
vergabenummer = models.CharField(max_length=100, blank=True)
vergabeart = models.CharField(max_length=30, choices=VERGABEART_CHOICES, blank=True)
rechtsgrundlage = models.CharField(max_length=20, choices=RECHTSGRUNDLAGE_CHOICES, blank=True)
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=1)
teilnahmeentscheidung = models.CharField(
max_length=20, choices=TEILNAHME_CHOICES, default='offen'
@@ -51,6 +60,7 @@ class Ausschreibung(FlexibleModel):
bieterfragen_bis = models.DateField(null=True, blank=True)
abgabe_bis = models.DateTimeField(null=True, blank=True)
bindefrist = models.DateField(null=True, blank=True)
bindefrist_tage = models.PositiveSmallIntegerField(null=True, blank=True, help_text='Bindefrist in Tagen ab Abgabe')
# Verantwortlichkeiten
bid_manager = models.ForeignKey(
@@ -92,6 +102,15 @@ class Ausschreibung(FlexibleModel):
def __str__(self):
return self.titel
@property
def bindefrist_berechnet(self):
"""Enddatum aus abgabe_bis + bindefrist_tage, falls kein festes bindefrist-Datum."""
if self.bindefrist:
return self.bindefrist
if self.abgabe_bis and self.bindefrist_tage:
return (self.abgabe_bis + timedelta(days=self.bindefrist_tage)).date()
return None
@property
def ist_aktiv(self):
return 1 <= self.status <= 9

View File

@@ -30,8 +30,8 @@ def build_phase_nav(ausschreibung, current_url=''):
base = f'/ausschreibungen/{ausschreibung.pk}'
phase_urls = {
1: f'{base}/',
2: f'{base}/teilnahmeentscheidung/',
3: f'{base}/anforderungen/',
2: f'{base}/entscheidung/',
3: f'{base}/lose/anforderungen/',
4: f'{base}/bieterfragen/',
5: f'{base}/preise/',
6: f'{base}/dokumente/',

View File

@@ -32,7 +32,7 @@ def abgabe_vollstaendigkeit(ausschreibung):
def abgabe_checkliste(request, ausschreibung_id):
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
vollstaendigkeit = abgabe_vollstaendigkeit(ausschreibung)
dokumente = Dokument.objects.filter(ausschreibung=ausschreibung).order_by('kategorie', 'bezeichnung')
dokumente = Dokument.objects.filter(ausschreibung=ausschreibung).order_by('kategorie', 'beschreibung')
punkte = [
('entscheidung_getroffen', 'Teilnahmeentscheidung getroffen'),

View File

@@ -59,6 +59,7 @@
<dl class="space-y-1">
{% render_field ausschreibung "ausschreiber" "Ausschreiber" %}
{% render_field ausschreibung "vergabeart" "Vergabeart" %}
{% render_field ausschreibung "rechtsgrundlage" "Rechtsgrundlage" %}
{% render_field ausschreibung "vergabenummer" "Vergabenummer" %}
{% render_field ausschreibung "vergabeplattform" "Plattform" %}
{% render_field ausschreibung "branche" "Branche" %}
@@ -73,7 +74,19 @@
{% render_field ausschreibung "veroeffentlichungsdatum" "Veröffentlicht" %}
{% render_field ausschreibung "bieterfragen_bis" "Bieterfragen bis" %}
{% render_field ausschreibung "abgabe_bis" "Abgabe bis" %}
{% if ausschreibung.bindefrist_tage %}
<div class="flex justify-between text-sm py-0.5">
<dt class="text-slate-500">Bindefrist</dt>
<dd class="text-slate-800">
{{ ausschreibung.bindefrist_tage }} Tage
{% if ausschreibung.bindefrist_berechnet %}
<span class="text-slate-400">(bis {{ ausschreibung.bindefrist_berechnet|date:"d.m.Y" }})</span>
{% endif %}
</dd>
</div>
{% else %}
{% render_field ausschreibung "bindefrist" "Bindefrist" %}
{% endif %}
</dl>
</div>
</div>

View File

@@ -26,6 +26,10 @@
<label class="form-label">Vergabeart</label>
{{ form.vergabeart }}
</div>
<div>
<label class="form-label">Rechtsgrundlage</label>
{{ form.rechtsgrundlage }}
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
@@ -83,9 +87,14 @@
{{ form.abgabe_bis }}
</div>
<div>
<label class="form-label">Bindefrist</label>
<label class="form-label">Bindefrist (Datum)</label>
{{ form.bindefrist }}
</div>
<div>
<label class="form-label">Bindefrist (Tage ab Abgabe)</label>
{{ form.bindefrist_tage }}
<p class="text-xs text-slate-400 mt-0.5">Alternativ zum Datum — Enddatum wird automatisch berechnet</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="flex items-center gap-2">

View File

@@ -4,8 +4,9 @@
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left px-4 py-2 font-medium text-slate-600">Titel</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Ausschreiber</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Titel</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Volumen (gesch.)</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Status</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Abgabe</th>
<th class="text-left px-4 py-2 font-medium text-slate-600">Bid Manager</th>
@@ -14,12 +15,15 @@
<tbody class="divide-y divide-slate-100">
{% for a in ausschreibungen %}
<tr class="hover:bg-slate-50">
<td class="px-4 py-2 text-slate-600 font-medium">{{ a.ausschreiber|default:"—" }}</td>
<td class="px-4 py-2">
<a href="{% url 'ausschreibungen:detail' a.pk %}" class="text-brand-700 hover:underline font-medium">
{{ a.titel }}
</a>
</td>
<td class="px-4 py-2 text-slate-600">{{ a.ausschreiber }}</td>
<td class="px-4 py-2 text-slate-600 text-right whitespace-nowrap">
{% if a.geschaetztes_volumen %}{{ a.geschaetztes_volumen|floatformat:0 }} €{% else %}—{% endif %}
</td>
<td class="px-4 py-2">
{% status_badge a.status a.get_status_display %}
</td>

View File

@@ -0,0 +1,143 @@
---
id: WP-0013
title: Feedback-Bugs — Fehlerbehebungen und Verbesserungen aus dem Backlog
status: done
phase: 13-of-13
created: "2026-05-14"
depends_on: WP-0012
---
# WP-0013 — Feedback-Bugs
Fehlerbehebungen und Verbesserungen, die während der ersten realen Nutzung
des Systems im Feedback-Backlog erfasst wurden. Priorisiert nach Dringlichkeit:
zuerst die kritischen Fehler (Fristen, Teilnahmeentscheidung), dann mittlere
(Phase 3, Abgabe, Listen-Darstellung), zuletzt kosmetische Hinweise.
---
```task
id: WP-0013-T01
title: Fehler — Fristen verschwinden nach Speichern (Feedback #4, Hoch)
status: done
Nach dem Speichern einer Ausschreibung sind beim erneuten Bearbeiten die Fristen
nicht mehr vorausgefüllt. Ursache vermutlich: DateTimeField-Konvertierung oder
fehlendes `initial`-Argument im Bearbeitungsformular.
Schritte:
1. `apps/ausschreibungen/views.py` — Bearbeitungs-View debuggen: prüfen, ob
die Fristen-Felder korrekt aus der Instanz gelesen und ans Template übergeben
werden.
2. Formular-Template prüfen: `value="{{ form.frist_angebot.value|date:'Y-m-d\\TH:i' }}"` o.ä.
3. Reproduzieren: Ausschreibung anlegen, Fristen setzen, speichern, erneut öffnen.
4. Fix und Regression-Test.
```
```task
id: WP-0013-T02
title: Fehler — Phase Teilnahmeentscheidung liefert Fehler (Feedback #6, Hoch)
status: done
Die Seite für Phase 2 "Teilnahmeentscheidung" gibt einen Fehler aus oder fehlt.
Schritte:
1. URL `/ausschreibungen/<pk>/teilnahmeentscheidung/` aufrufen und Traceback lesen.
2. View und Template für Phase 2 identifizieren.
3. Fehlenden Import, fehlende Migration oder fehlendes Template beheben.
4. Manuell testen: Teilnahmeentscheidung für eine Ausschreibung setzen.
```
```task
id: WP-0013-T03
title: Fehler — Phase "Detaillierte Durchsicht" fehlerhaft (Feedback #7, Mittel)
status: done
Die Seite für Phase 3 "Detaillierte Durchsicht & offene Punkte" ist fehlerhaft
oder fehlt.
Schritte:
1. URL aufrufen, Traceback/Fehlermeldung dokumentieren.
2. View, URLs und Template der Phase prüfen.
3. Ursache beheben (fehlender Import, Query-Fehler, Template-Variable).
4. Manuell testen.
```
```task
id: WP-0013-T04
title: Fehler — Abgabe-Seite defekt (Feedback #8, Mittel)
status: done
Die Seite für Phase 7 "Abgabe" funktioniert nicht korrekt.
Schritte:
1. URL `/ausschreibungen/<pk>/abgabe/` aufrufen, Fehler dokumentieren.
2. `apps/nachbetrachtung/views.py` und `abgabe_views.py` prüfen.
3. Namespace-Fehler in Templates ausschließen (wurden in WP-0012 teilweise bereits
gefixt — prüfen ob noch weitere URL-Referenzen falsch sind).
4. Fix und manuell testen.
```
```task
id: WP-0013-T05
title: Hinweis — Ausschreibungen-Liste: Ausschreiber zuerst, Volumen ergänzen (Feedback #5, Mittel)
status: done
In der Liste "Alle Ausschreibungen" soll der Ausschreiber in die erste Spalte,
der Titel in die zweite. Das geschätzte Auftragsvolumen soll als weitere Spalte
erscheinen.
Schritte:
1. `templates/ausschreibungen/liste.html` — Spaltenreihenfolge anpassen.
2. Sicherstellen, dass `ausschreiber` und `auftragsvolumen` im Queryset und
Template-Kontext vorhanden sind.
3. `auftragsvolumen` ggf. formatiert darstellen (Tausendertrennzeichen, EUR).
```
```task
id: WP-0013-T06
title: Fehler — Feedback-Backlog bei leerem Bestand (Feedback #1, Niedrig)
status: done
Die Feedback-Backlog-Seite liefert einen Fehler, wenn noch kein Eintrag vorhanden
ist. Vermutlich ein Template-Fehler beim Iterieren über ein leeres QuerySet.
Schritte:
1. Alle Einträge temporär deaktivieren oder neuen User testen.
2. `templates/feedback/backlog.html` — `{% if eintraege %}` Guard prüfen
(ist vorhanden, aber ggf. greift `.count` oder ein anderer Ausdruck vorher).
3. Fix, ggf. auch den `{{ eintraege.count }}` im Header absichern.
```
```task
id: WP-0013-T07
title: Hinweis — Rechtsgrundlage in Ausschreibungs-Stammdaten (Feedback #2, Mittel)
status: done
In den Stammdaten der Ausschreibung fehlt das Feld "Rechtsgrundlage" (z.B. VgV,
UVgO, VOB/A, SektVO).
Schritte:
1. `apps/ausschreibungen/models.py` — Feld `rechtsgrundlage` ergänzen
(CharField mit choices: VgV, UVgO, VOB/A, SektVO, GWB, Sonstige).
2. Migration erstellen und anwenden.
3. Formular und Template für Stammdaten erweitern.
4. In der Detailansicht darstellen.
```
```task
id: WP-0013-T08
title: Hinweis — Bindefrist: Tage erfassen, Datum berechnen (Feedback #3, Niedrig)
status: done
Bindefrist soll als Anzahl Tage erfasst werden. Das konkrete Enddatum ergibt
sich dann aus "Abgabe bis" + Bindefrist-Tage und soll ergänzend dargestellt werden.
Schritte:
1. `apps/ausschreibungen/models.py` — Feld `bindefrist_tage` (PositiveIntegerField,
optional) ergänzen neben dem bestehenden Datums-Feld (falls vorhanden).
2. Migration erstellen.
3. Template: berechnetes Datum `{{ ausschreibung.abgabe_bis|add_days:ausschreibung.bindefrist_tage }}`
oder Berechnung im View/Model-Property.
4. Formular anpassen.
```