feat(WP-0014): Aufgaben-Phasenzuordnung und Fertigstellungs-Scores

Aufgabe bekommt ein phase-Feld (1–8). aufgaben_score()-Helper in
core/services.py berechnet abgeschlossen/total/score_pct für jedes
QuerySet. Score-Spalten in Ausschreibungen-Liste, Lose-Liste und
Ausschreibungs-Detail; per-Phase-Scores in der Seitenleisten-Navigation.
Phasenfilter in Aufgaben-Liste. 68 Tests grün.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-05-14 01:47:05 +02:00
parent f5950b6cbc
commit e4eb5bc368
14 changed files with 343 additions and 5 deletions

View file

@ -7,13 +7,14 @@ class AufgabeForm(forms.ModelForm):
class Meta:
model = Aufgabe
fields = [
'titel', 'beschreibung', 'typ', 'prioritaet', 'frist',
'titel', 'beschreibung', 'typ', 'phase', 'prioritaet', 'frist',
'verantwortlicher', 'los', 'anforderung', 'bieterfrage',
]
widgets = {
'titel': forms.TextInput(attrs={'class': 'form-input', 'autofocus': True}),
'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
'typ': forms.Select(attrs={'class': 'form-input'}),
'phase': forms.Select(attrs={'class': 'form-input'}),
'prioritaet': forms.RadioSelect(),
'frist': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
'verantwortlicher': forms.Select(attrs={'class': 'form-input'}),
@ -30,6 +31,7 @@ class AufgabeForm(forms.ModelForm):
self.fields['anforderung'].queryset = Anforderung.objects.filter(ausschreibung=ausschreibung)
self.fields['bieterfrage'].queryset = Bieterfrage.objects.filter(ausschreibung=ausschreibung)
self.fields['beschreibung'].required = False
self.fields['phase'].required = False
self.fields['frist'].required = False
self.fields['verantwortlicher'].required = False
self.fields['los'].required = False

View file

@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-05-13 23:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('aufgaben', '0002_initial'),
]
operations = [
migrations.AddField(
model_name='aufgabe',
name='phase',
field=models.PositiveSmallIntegerField(blank=True, choices=[(1, 'Recherche & Unterlagen'), (2, 'Teilnahmeentscheidung'), (3, 'Detaillierte Durchsicht'), (4, 'Bieterfragen & Klärung'), (5, 'Preismodell'), (6, 'Unterlagen finalisieren'), (7, 'Abgabe'), (8, 'Zuschlag / Nachbetrachtung')], null=True),
),
]

View file

@ -26,6 +26,16 @@ class Aufgabe(FlexibleModel):
('ueberfaellig', 'Überfällig'),
]
PRIORITAET_CHOICES = [(1, 'Hoch'), (2, 'Mittel'), (3, 'Niedrig')]
PHASE_CHOICES = [
(1, 'Recherche & Unterlagen'),
(2, 'Teilnahmeentscheidung'),
(3, 'Detaillierte Durchsicht'),
(4, 'Bieterfragen & Klärung'),
(5, 'Preismodell'),
(6, 'Unterlagen finalisieren'),
(7, 'Abgabe'),
(8, 'Zuschlag / Nachbetrachtung'),
]
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='aufgaben'
@ -55,6 +65,7 @@ class Aufgabe(FlexibleModel):
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
ergebnis = models.TextField(blank=True)
phase = models.PositiveSmallIntegerField(choices=PHASE_CHOICES, null=True, blank=True)
class Meta:
ordering = ['prioritaet', 'frist']

View file

@ -40,6 +40,10 @@ def aufgaben_liste(request, ausschreibung_id=None):
if typ_filter:
qs = qs.filter(typ=typ_filter)
phase_filter = request.GET.get('phase')
if phase_filter:
qs = qs.filter(phase=phase_filter)
verantwortlicher_filter = request.GET.get('verantwortlicher')
if verantwortlicher_filter:
qs = qs.filter(verantwortlicher=verantwortlicher_filter)
@ -67,6 +71,8 @@ def aufgaben_liste(request, ausschreibung_id=None):
'mitarbeiter': Mitarbeiter.objects.all(),
'current_status': status_filter or '',
'current_typ': typ_filter or '',
'phase_choices': Aufgabe.PHASE_CHOICES,
'current_phase': phase_filter or '',
'current_verantwortlicher': verantwortlicher_filter or '',
'breadcrumbs': breadcrumbs,
}