--- 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`: `` - `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 ``` ``` ```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('/', views.preispunkt_detail, name='detail'), path('/bearbeiten/', views.preispunkt_bearbeiten, name='bearbeiten'), path('/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 ```