generated from coulomb/repo-seed
161 lines
5.7 KiB
Markdown
161 lines
5.7 KiB
Markdown
---
|
|
id: WP-0008
|
|
title: Preise und Marktpreisauswertung
|
|
status: done
|
|
phase: 8-of-12
|
|
created: "2026-05-08"
|
|
depends_on: 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.
|
|
|
|
---
|
|
|
|
```task
|
|
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)
|
|
```
|
|
|
|
```task
|
|
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.
|
|
```
|
|
|
|
```task
|
|
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
|
|
```
|