Files
vergabe-teilnahme/workplans/WP-0008-preise.md

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