Established workplans
This commit is contained in:
parent
026f7c0db1
commit
315143a6fc
16 changed files with 5492 additions and 0 deletions
160
workplans/WP-0008-preise.md
Normal file
160
workplans/WP-0008-preise.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
---
|
||||
id: WP-0008
|
||||
title: Preise und Marktpreisauswertung
|
||||
status: todo
|
||||
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: todo
|
||||
|
||||
`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: todo
|
||||
|
||||
`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: todo
|
||||
|
||||
`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: todo
|
||||
|
||||
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: todo
|
||||
|
||||
`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
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue