generated from coulomb/repo-seed
5.7 KiB
5.7 KiB
id, title, status, phase, created, depends_on
| id | title | status | phase | created | depends_on |
|---|---|---|---|---|---|
| WP-0008 | Preise und Marktpreisauswertung | done | 8-of-12 | 2026-05-08 | WP-0007 |
WP-0008 — Preise und Marktpreisauswertung
Preispunkt-CRUD, Vergleichsgewicht-Validierung, gewichtete Durchschnittsberechnung und Leistungstyp-Auswertung. Referenz: UC-PR-01 bis UC-PR-04, FR-25 bis FR-36.
id: WP-0008-T01
title: Preispunkt anlegen mit Vergleichsgewicht-Validierung (UC-PR-01, UC-PR-02)
status: done
`preise/views.py` — preispunkt_neu:
`PreispunktForm(ModelForm)`:
- Felder: leistungstyp, konkrete_leistung, mengeneinheit, menge, einzelpreis, gesamtpreis,
waehrung (default EUR), preisstand, wiederkehrend, laufzeitbezug,
subunternehmeranteil, subunternehmer, vergleichsgewicht, gewichtungsbegruendung, kommentar, los
- `vergleichsgewicht` als DecimalField-Input mit Schritt 0.1, min=0.0, max=2.0
- Widget für `vergleichsgewicht`: `<input type="number" step="0.1" min="0.0" max="2.0">`
- `clean_vergleichsgewicht()`: prüft Decimal('0.0') ≤ wert ≤ Decimal('2.0'),
sonst ValidationError("Vergleichsgewicht muss zwischen 0,0 und 2,0 liegen.")
- `initial={'vergleichsgewicht': Decimal('1.0')}`
Template `preise/form.html`:
- Abschnitt "Leistung": leistungstyp (mit Datalist für Autovervollständigung bekannter Leistungstypen),
konkrete_leistung, Mengenfelder
- Abschnitt "Preis": einzelpreis, gesamtpreis, waehrung, preisstand
- Abschnitt "Vergleichsgewicht": Numerisches Input + Hilfetextlabel
"0,0 = nicht gewertet | 1,0 = Standard | 2,0 = doppelt gewichtet"
- Subunternehmer-Toggle (Alpine x-show)
id: WP-0008-T02
title: Preispunkt-Liste pro Ausschreibung
status: done
`preise/views.py` — preispunkte_liste:
Zeigt alle Preispunkte der Ausschreibung, grupierbar nach Leistungstyp oder Los.
Filter: Leistungstyp, Los, Subunternehmeranteil ja/nein.
Template `preise/liste.html`:
- Tabelle: Leistungstyp, konkrete Leistung, Menge/Einheit, Einzelpreis, Gesamtpreis, Gewicht, Los
- Gewicht < 1.0: Grauer Text; Gewicht > 1.0: fetter Text; Gewicht = 0.0: durchgestrichen
- Spaltensumme Gesamtpreis (ungewichtet) am Ende der Tabelle
- "+ Preispunkt" Button
Gesamtpreis-Auto-Berechnung:
```html
<input name="einzelpreis" x-model="einzelpreis" @input="gesamtpreis = einzelpreis * menge">
<input name="gesamtpreis" x-model="gesamtpreis">
```task
id: WP-0008-T03
title: Leistungstyp-Auswertung mit gewichtetem Durchschnitt (UC-PR-03)
status: done
`preise/views.py` — leistungstyp_auswertung:
```python
def leistungstyp_auswertung(request, ausschreibung_id):
from vergabe_teilnahme.apps.core.services import gewichteter_durchschnitt
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
leistungstyp = request.GET.get('leistungstyp')
filter_gewonnen = request.GET.get('gewonnen') # 'ja'/'nein'/None
qs = Preispunkt.objects.filter(einzelpreis__isnull=False)
if leistungstyp:
qs = qs.filter(leistungstyp__icontains=leistungstyp)
if filter_gewonnen == 'ja':
qs = qs.filter(ausschreibung_gewonnen=True)
elif filter_gewonnen == 'nein':
qs = qs.filter(ausschreibung_gewonnen=False)
ergebnis = gewichteter_durchschnitt(list(qs))
alle_leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct()
ctx = {
'ausschreibung': ausschreibung,
'leistungstyp': leistungstyp,
'ergebnis': ergebnis,
'preispunkte': qs.order_by('-ausschreibung__erstellt_am'),
'alle_leistungstypen': alle_leistungstypen,
}
return render(request, 'preise/auswertung.html', ctx)
Template preise/auswertung.html:
Zeigt Statistik-Kacheln: Gewichteter Durchschnitt, Ungewichteter Durchschnitt,
Anzahl Messpunkte, Summe Gewichte, Minimum, Maximum.
Darunter: Tabelle aller Einzelmesspunkte mit Ausschreibungstitel, Datum, Gewicht.
```task
id: WP-0008-T04
title: Globaler Preisvergleich (cross-Ausschreibung, UC-PR-03)
status: done
URL: `/preise/vergleich/` (globaler Endpunkt, kein ausschreibung_id Präfix)
`preise/views.py` — globaler_preisvergleich:
Gleiche Logik wie leistungstyp_auswertung, aber über alle Ausschreibungen.
Zusätzliche Filter: Zeitraum (von/bis), Ausschreibungstyp (öffentlich/privat via Ausschreiber-Feld),
nur gewonnene / nur verlorene.
Template `preise/globaler_vergleich.html`:
- Filterleiste oben (HTMX-Update der Ergebnisse)
- Datalist für Leistungstyp-Autocomplete aus allen existierenden Leistungstypen
- Statistik-Kacheln
- Aufschlüsselung: Durchschnitt bei Gewinn vs. Verlust (falls Daten vorhanden)
URL: `path('preise/vergleich/', preise_views.globaler_preisvergleich, name='preisvergleich')`
in Haupt-URLs einbinden.
id: WP-0008-T05
title: Preisfreigabe und URL-Verkabelung (UC-PR-04)
status: done
`preise/views.py` — preisfreigabe:
Button "Preisfreigabe erteilen" auf der Preisliste öffnet Freigabe-Modal (aus WP-0012).
Freigabe-Typ: 'preis'.
Sobald eine Preisfreigabe mit `status='erteilt'` für die Ausschreibung vorliegt,
zeigt die Abgabe-Checkliste (WP-0009) den Preisfreigabe-Punkt als abgehakt.
`preise/urls.py`:
```python
app_name = 'preise'
urlpatterns = [
path('', views.preispunkte_liste, name='liste'),
path('neu/', views.preispunkt_neu, name='neu'),
path('<int:pk>/', views.preispunkt_detail, name='detail'),
path('<int:pk>/bearbeiten/', views.preispunkt_bearbeiten, name='bearbeiten'),
path('<int:pk>/loeschen/', views.preispunkt_loeschen, name='loeschen'),
path('auswertung/', views.leistungstyp_auswertung, name='auswertung'),
]
Tests:
- Test: Vergleichsgewicht 0.0 → gespeichert, nicht in Durchschnitt
- Test: Vergleichsgewicht 2.5 → ValidationError
- Test: gewichteter_durchschnitt mit Blueprint-Beispiel → 103.33 (auf 2 Stellen)
- Test: Auswertungs-View gibt 200 zurück, enthält 'ergebnis' in Context