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:
parent
f5950b6cbc
commit
e4eb5bc368
14 changed files with 343 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
]
|
||||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,15 @@ def ausschreibung_liste(request):
|
|||
if bid_manager_filter:
|
||||
qs = qs.filter(bid_manager=bid_manager_filter)
|
||||
|
||||
qs = qs.select_related('bid_manager').order_by('-geaendert_am')
|
||||
from django.db.models import Count, Q
|
||||
qs = qs.select_related('bid_manager').annotate(
|
||||
aufgaben_total=Count('aufgaben', distinct=True),
|
||||
aufgaben_erledigt=Count(
|
||||
'aufgaben',
|
||||
filter=Q(aufgaben__status__in=['erledigt', 'verworfen']),
|
||||
distinct=True,
|
||||
),
|
||||
).order_by('-geaendert_am')
|
||||
|
||||
ctx = {
|
||||
'ausschreibungen': qs,
|
||||
|
|
@ -100,7 +108,8 @@ def ausschreibung_neu(request):
|
|||
|
||||
|
||||
def ausschreibung_detail(request, pk):
|
||||
from vergabe_teilnahme.apps.core.services import build_phase_nav, get_deadline_warnings
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from vergabe_teilnahme.apps.core.services import aufgaben_score, build_phase_nav, get_deadline_warnings
|
||||
|
||||
a = get_object_or_404(Ausschreibung, pk=pk)
|
||||
ctx = {
|
||||
|
|
@ -108,6 +117,7 @@ def ausschreibung_detail(request, pk):
|
|||
'ausschreibung_id': pk,
|
||||
'phases': build_phase_nav(a),
|
||||
'warnungen': get_deadline_warnings(a),
|
||||
'aufgaben_score': aufgaben_score(Aufgabe.objects.filter(ausschreibung=a)),
|
||||
'breadcrumbs': [
|
||||
{'label': 'Ausschreibungen', 'url': '/ausschreibungen/'},
|
||||
{'label': a.titel, 'url': None},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,15 @@ STATUS_TO_PHASE = {
|
|||
}
|
||||
|
||||
|
||||
def aufgaben_score(qs):
|
||||
total = qs.count()
|
||||
abgeschlossen = qs.filter(status__in=['erledigt', 'verworfen']).count()
|
||||
pct = round(abgeschlossen / total * 100) if total else 0
|
||||
return {'total': total, 'abgeschlossen': abgeschlossen, 'score_pct': pct}
|
||||
|
||||
|
||||
def build_phase_nav(ausschreibung, current_url=''):
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
aktuelle_phase = STATUS_TO_PHASE.get(ausschreibung.status, 1)
|
||||
base = f'/ausschreibungen/{ausschreibung.pk}'
|
||||
phase_urls = {
|
||||
|
|
@ -38,6 +46,11 @@ def build_phase_nav(ausschreibung, current_url=''):
|
|||
7: f'{base}/abgabe/',
|
||||
8: f'{base}/nachbetrachtung/',
|
||||
}
|
||||
aufgaben_qs = Aufgabe.objects.filter(ausschreibung=ausschreibung)
|
||||
phasen_stats = {
|
||||
num: aufgaben_score(aufgaben_qs.filter(phase=num))
|
||||
for num, _ in PHASEN
|
||||
}
|
||||
return [
|
||||
{
|
||||
'nummer': num,
|
||||
|
|
@ -46,6 +59,7 @@ def build_phase_nav(ausschreibung, current_url=''):
|
|||
'aktiv': num == aktuelle_phase,
|
||||
'erledigt': num < aktuelle_phase,
|
||||
'warnung': False,
|
||||
'aufgaben_score': phasen_stats[num],
|
||||
}
|
||||
for num, name in PHASEN
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,8 +24,16 @@ def _ausschreibung_breadcrumbs(ausschreibung, *extra):
|
|||
# ─── Lose ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def lose_liste(request, ausschreibung_id):
|
||||
from django.db.models import Count, Q
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
lose = Los.objects.filter(ausschreibung=ausschreibung).order_by('losnummer')
|
||||
lose = Los.objects.filter(ausschreibung=ausschreibung).annotate(
|
||||
aufgaben_total=Count('aufgaben', distinct=True),
|
||||
aufgaben_erledigt=Count(
|
||||
'aufgaben',
|
||||
filter=Q(aufgaben__status__in=['erledigt', 'verworfen']),
|
||||
distinct=True,
|
||||
),
|
||||
).order_by('losnummer')
|
||||
return render(request, 'lose/liste.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'lose': lose,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue