generated from coulomb/repo-seed
feat(nachbetrachtung): Abgabe-Checkliste, Dokumentation und Nachbetrachtung (WP-0009)
Vollständigkeitsprüfung mit Freigaben-Check, Abgabe dokumentieren mit Nachweis-Upload, Nachbetrachtung mit Kickoff-Aufgabe (gewonnen) und Alpine.js-gesteuerter Verlustanalyse (verloren). 5 Tests grün. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = []
|
||||
from . import abgabe_views as views
|
||||
|
||||
app_name = 'abgabe'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.abgabe_checkliste, name='checkliste'),
|
||||
path('dokumentieren/', views.abgabe_dokumentieren, name='dokumentieren'),
|
||||
path('problem/', views.abgabe_problem, name='problem'),
|
||||
]
|
||||
|
||||
120
vergabe_teilnahme/apps/nachbetrachtung/abgabe_views.py
Normal file
120
vergabe_teilnahme/apps/nachbetrachtung/abgabe_views.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
from vergabe_teilnahme.apps.core.models import Freigabe
|
||||
from vergabe_teilnahme.apps.dokumente.models import Dokument
|
||||
|
||||
|
||||
def abgabe_vollstaendigkeit(ausschreibung):
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
ct = ContentType.objects.get_for_model(ausschreibung)
|
||||
freigaben = Freigabe.objects.filter(content_type=ct, object_id=ausschreibung.pk)
|
||||
|
||||
def hat_freigabe(typ):
|
||||
return freigaben.filter(freigabe_typ=typ, status='erteilt').exists()
|
||||
|
||||
return {
|
||||
'dokumente_gesamt': Dokument.objects.filter(ausschreibung=ausschreibung).count(),
|
||||
'dokumente_freigegeben': Dokument.objects.filter(
|
||||
ausschreibung=ausschreibung, status__in=['freigegeben', 'final_abgegeben']).count(),
|
||||
'teilnahme_freigabe': hat_freigabe('teilnahme'),
|
||||
'preis_freigabe': hat_freigabe('preis'),
|
||||
'recht_freigabe': hat_freigabe('recht'),
|
||||
'abgabe_freigabe': hat_freigabe('abgabe'),
|
||||
'entscheidung_getroffen': ausschreibung.teilnahmeentscheidung == 'teilnahme',
|
||||
}
|
||||
|
||||
|
||||
def abgabe_checkliste(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
vollstaendigkeit = abgabe_vollstaendigkeit(ausschreibung)
|
||||
dokumente = Dokument.objects.filter(ausschreibung=ausschreibung).order_by('kategorie', 'bezeichnung')
|
||||
|
||||
punkte = [
|
||||
('entscheidung_getroffen', 'Teilnahmeentscheidung getroffen'),
|
||||
('teilnahme_freigabe', 'Teilnahme-Freigabe erteilt'),
|
||||
('preis_freigabe', 'Preis-Freigabe erteilt'),
|
||||
('recht_freigabe', 'Rechtliche Freigabe erteilt'),
|
||||
('abgabe_freigabe', 'Abgabe-Freigabe erteilt'),
|
||||
]
|
||||
erfuellt = sum(1 for k, _ in punkte if vollstaendigkeit.get(k))
|
||||
gesamt = len(punkte)
|
||||
|
||||
ctx = {
|
||||
'ausschreibung': ausschreibung,
|
||||
'vollstaendigkeit': vollstaendigkeit,
|
||||
'punkte': [(label, vollstaendigkeit.get(key)) for key, label in punkte],
|
||||
'erfuellt': erfuellt,
|
||||
'gesamt': gesamt,
|
||||
'dokumente': dokumente,
|
||||
}
|
||||
return render(request, 'nachbetrachtung/abgabe.html', ctx)
|
||||
|
||||
|
||||
class AbgabeForm(forms.Form):
|
||||
abgabe_zeitpunkt = forms.DateTimeField(
|
||||
widget=forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-input'}),
|
||||
label='Abgabezeitpunkt',
|
||||
)
|
||||
abgabe_plattform = forms.CharField(
|
||||
max_length=200, required=False,
|
||||
widget=forms.TextInput(attrs={'class': 'form-input'}),
|
||||
label='Abgabeplattform',
|
||||
)
|
||||
verantwortlicher = forms.ModelChoiceField(
|
||||
queryset=Mitarbeiter.objects.all(),
|
||||
required=False,
|
||||
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
label='Verantwortlicher',
|
||||
)
|
||||
abgabenachweis = forms.FileField(
|
||||
required=False,
|
||||
widget=forms.FileInput(attrs={'class': 'form-input'}),
|
||||
label='Abgabenachweis (Eingangsbestätigung)',
|
||||
)
|
||||
kommentar = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
label='Kommentar',
|
||||
)
|
||||
|
||||
|
||||
def abgabe_dokumentieren(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
if request.method == 'POST':
|
||||
form = AbgabeForm(request.POST, request.FILES)
|
||||
if form.is_valid():
|
||||
if form.cleaned_data.get('abgabenachweis'):
|
||||
Dokument.objects.create(
|
||||
ausschreibung=ausschreibung,
|
||||
datei=form.cleaned_data['abgabenachweis'],
|
||||
kategorie='abgabenachweis',
|
||||
status='final_abgegeben',
|
||||
finale_abgabeversion=True,
|
||||
)
|
||||
ausschreibung.status = 9
|
||||
ausschreibung.save(update_fields=['status', 'geaendert_am'])
|
||||
messages.success(request, 'Abgabe dokumentiert.')
|
||||
return redirect('ausschreibungen:detail', pk=ausschreibung_id)
|
||||
else:
|
||||
form = AbgabeForm()
|
||||
return render(request, 'nachbetrachtung/abgabe_formular.html', {
|
||||
'form': form,
|
||||
'ausschreibung': ausschreibung,
|
||||
})
|
||||
|
||||
|
||||
def abgabe_problem(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
if request.method == 'POST':
|
||||
kommentar = request.POST.get('kommentar', '')
|
||||
ausschreibung.status = 7
|
||||
ausschreibung.save(update_fields=['status', 'geaendert_am'])
|
||||
messages.warning(request, f'Problem bei Abgabe vermerkt. {kommentar}')
|
||||
return redirect('ausschreibungen:nachbetrachtung:abgabe:checkliste',
|
||||
ausschreibung_id=ausschreibung_id)
|
||||
return render(request, 'nachbetrachtung/abgabe_problem.html', {'ausschreibung': ausschreibung})
|
||||
@@ -1,3 +1,74 @@
|
||||
from django.test import TestCase
|
||||
import pytest
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
# Create your tests here.
|
||||
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||
from vergabe_teilnahme.apps.core.models import Freigabe
|
||||
|
||||
from .abgabe_views import abgabe_vollstaendigkeit
|
||||
from .models import Nachbetrachtung
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_abgabe_vollstaendigkeit_ohne_freigaben():
|
||||
a = AusschreibungFactory(teilnahmeentscheidung='offen')
|
||||
v = abgabe_vollstaendigkeit(a)
|
||||
assert v['teilnahme_freigabe'] is False
|
||||
assert v['preis_freigabe'] is False
|
||||
assert v['recht_freigabe'] is False
|
||||
assert v['abgabe_freigabe'] is False
|
||||
assert v['entscheidung_getroffen'] is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_abgabe_vollstaendigkeit_mit_freigabe():
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
user = Mitarbeiter.objects.create_user(username='pruefer', password='x')
|
||||
a = AusschreibungFactory(teilnahmeentscheidung='teilnahme')
|
||||
ct = ContentType.objects.get_for_model(a)
|
||||
Freigabe.objects.create(
|
||||
content_type=ct, object_id=a.pk,
|
||||
freigabe_typ='preis', status='erteilt',
|
||||
freigebende_person=user,
|
||||
)
|
||||
v = abgabe_vollstaendigkeit(a)
|
||||
assert v['preis_freigabe'] is True
|
||||
assert v['teilnahme_freigabe'] is False
|
||||
assert v['entscheidung_getroffen'] is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ergebnis_gewonnen_erstellt_kickoff_aufgabe(client):
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
a = AusschreibungFactory(status=9)
|
||||
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.post(url, {
|
||||
'ergebnis': 'gewonnen',
|
||||
'verlustgruende': '[]',
|
||||
})
|
||||
assert response.status_code == 302
|
||||
a.refresh_from_db()
|
||||
assert a.status == 10
|
||||
assert Aufgabe.objects.filter(ausschreibung=a, titel='Kickoff vorbereiten').exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_ergebnis_verloren_setzt_status_11(client):
|
||||
a = AusschreibungFactory(status=9)
|
||||
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
|
||||
client.post(url, {'ergebnis': 'verloren', 'verlustgruende': '[]'})
|
||||
a.refresh_from_db()
|
||||
assert a.status == 11
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_verlustgruende_json_gespeichert(client):
|
||||
import json
|
||||
a = AusschreibungFactory(status=9)
|
||||
url = reverse('ausschreibungen:nachbetrachtung:detail', kwargs={'ausschreibung_id': a.pk})
|
||||
gruende = [{'grund': 'Zu teuer', 'kategorie': 'preis', 'verlaesslichkeit': 4}]
|
||||
client.post(url, {'ergebnis': 'verloren', 'verlustgruende': json.dumps(gruende)})
|
||||
nb = Nachbetrachtung.objects.get(ausschreibung=a)
|
||||
assert nb.verlustgruende[0]['grund'] == 'Zu teuer'
|
||||
assert nb.verlustgruende[0]['kategorie'] == 'preis'
|
||||
assert nb.verlustgruende[0]['verlaesslichkeit'] == 4
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'nachbetrachtung'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.nachbetrachtung_detail, name='detail'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,102 @@
|
||||
from django.shortcuts import render
|
||||
import json
|
||||
|
||||
# Create your views here.
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
|
||||
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
|
||||
from .models import Nachbetrachtung
|
||||
|
||||
|
||||
class NachbetrachtungForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Nachbetrachtung
|
||||
fields = [
|
||||
'ergebnis', 'zuschlagsdatum', 'projektverantwortlicher',
|
||||
'abgegebene_unterlagen', 'abgegebene_preise',
|
||||
'ausschlaggebende_zuschlagsmerkmale', 'lessons_learned',
|
||||
'empfehlungen', 'wiederverwendbare_erkenntnisse_markiert',
|
||||
]
|
||||
widgets = {
|
||||
'ergebnis': forms.RadioSelect(attrs={'class': 'mr-2'}),
|
||||
'zuschlagsdatum': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}),
|
||||
'projektverantwortlicher': forms.Select(attrs={'class': 'form-select'}),
|
||||
'abgegebene_unterlagen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'abgegebene_preise': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'ausschlaggebende_zuschlagsmerkmale': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
'lessons_learned': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}),
|
||||
'empfehlungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['projektverantwortlicher'].queryset = Mitarbeiter.objects.all()
|
||||
self.fields['projektverantwortlicher'].required = False
|
||||
self.fields['zuschlagsdatum'].required = False
|
||||
|
||||
|
||||
def nachbetrachtung_detail(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
nb, _ = Nachbetrachtung.objects.get_or_create(ausschreibung=ausschreibung)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = NachbetrachtungForm(request.POST, instance=nb)
|
||||
verlustgruende_raw = request.POST.get('verlustgruende', '[]')
|
||||
try:
|
||||
verlustgruende = json.loads(verlustgruende_raw)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
verlustgruende = []
|
||||
|
||||
if form.is_valid():
|
||||
nb = form.save(commit=False)
|
||||
nb.verlustgruende = verlustgruende
|
||||
nb.save()
|
||||
|
||||
ergebnis = form.cleaned_data['ergebnis']
|
||||
if ergebnis == 'gewonnen':
|
||||
ausschreibung.status = 10
|
||||
ausschreibung.save(update_fields=['status', 'geaendert_am'])
|
||||
aufgabe, created = Aufgabe.objects.get_or_create(
|
||||
ausschreibung=ausschreibung,
|
||||
titel='Kickoff vorbereiten',
|
||||
defaults={
|
||||
'typ': 'fachlich',
|
||||
'prioritaet': 1,
|
||||
'verantwortlicher': form.cleaned_data.get('projektverantwortlicher'),
|
||||
'beschreibung': (
|
||||
f'Kickoff für {ausschreibung.titel}. '
|
||||
'Angebotsumfang und Annahmen übergeben.'
|
||||
),
|
||||
},
|
||||
)
|
||||
if created:
|
||||
pv = form.cleaned_data.get('projektverantwortlicher')
|
||||
pv_name = str(pv) if pv else '—'
|
||||
messages.success(request, f'Kickoff-Aufgabe erstellt für {pv_name}.')
|
||||
elif ergebnis == 'verloren':
|
||||
ausschreibung.status = 11
|
||||
ausschreibung.save(update_fields=['status', 'geaendert_am'])
|
||||
|
||||
messages.success(request, 'Nachbetrachtung gespeichert.')
|
||||
return redirect('ausschreibungen:nachbetrachtung:detail',
|
||||
ausschreibung_id=ausschreibung_id)
|
||||
else:
|
||||
form = NachbetrachtungForm(instance=nb)
|
||||
|
||||
kickoff_aufgabe = None
|
||||
if nb.ergebnis == 'gewonnen':
|
||||
kickoff_aufgabe = Aufgabe.objects.filter(
|
||||
ausschreibung=ausschreibung, titel='Kickoff vorbereiten'
|
||||
).first()
|
||||
|
||||
ctx = {
|
||||
'ausschreibung': ausschreibung,
|
||||
'nachbetrachtung': nb,
|
||||
'form': form,
|
||||
'kickoff_aufgabe': kickoff_aufgabe,
|
||||
'verlustgruende_json': json.dumps(nb.verlustgruende),
|
||||
}
|
||||
return render(request, 'nachbetrachtung/detail.html', ctx)
|
||||
|
||||
81
vergabe_teilnahme/templates/nachbetrachtung/abgabe.html
Normal file
81
vergabe_teilnahme/templates/nachbetrachtung/abgabe.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Abgabe-Checkliste — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Abgabe-Checkliste</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:dokumentieren' ausschreibung.pk %}" class="btn-primary text-xs">Abgabe dokumentieren</a>
|
||||
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost text-xs">← Ausschreibung</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if ausschreibung.abgabe_bis %}
|
||||
<div class="card mb-4 bg-amber-50 border border-amber-200">
|
||||
<p class="text-sm font-medium text-amber-800">Abgabe bis: <strong>{{ ausschreibung.abgabe_bis }}</strong></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<p class="text-sm font-medium text-slate-700">Fortschritt</p>
|
||||
<p class="text-sm font-bold text-slate-700">{{ erfuellt }} / {{ gesamt }}</p>
|
||||
</div>
|
||||
<div class="w-full bg-slate-200 rounded-full h-2">
|
||||
<div class="bg-blue-600 h-2 rounded-full" style="width: {% widthratio erfuellt gesamt 100 %}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Freigaben & Entscheidungen</p>
|
||||
<ul class="space-y-2">
|
||||
{% for label, ok in punkte %}
|
||||
<li class="flex items-center gap-3">
|
||||
{% if ok %}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-green-100 text-green-700 text-xs font-bold">✓</span>
|
||||
<span class="text-sm text-slate-700">{{ label }}</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-red-100 text-red-600 text-xs font-bold">✗</span>
|
||||
<span class="text-sm text-slate-400">{{ label }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Dokumente</p>
|
||||
<p class="text-xs text-slate-500">{{ vollstaendigkeit.dokumente_freigegeben }} / {{ vollstaendigkeit.dokumente_gesamt }} freigegeben</p>
|
||||
</div>
|
||||
{% if dokumente %}
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 text-left text-xs text-slate-500">
|
||||
<th class="pb-2 pr-3">Bezeichnung</th>
|
||||
<th class="pb-2 pr-3">Kategorie</th>
|
||||
<th class="pb-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for dok in dokumente %}
|
||||
<tr>
|
||||
<td class="py-2 pr-3 text-xs">{{ dok.bezeichnung|default:dok.datei.name }}</td>
|
||||
<td class="py-2 pr-3 text-xs text-slate-500">{{ dok.get_kategorie_display }}</td>
|
||||
<td class="py-2 text-xs">
|
||||
{% if dok.status == 'final_abgegeben' or dok.status == 'freigegeben' %}
|
||||
<span class="text-green-700 font-medium">{{ dok.get_status_display }}</span>
|
||||
{% else %}
|
||||
<span class="text-slate-400">{{ dok.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-sm text-slate-400">Noch keine Dokumente hochgeladen.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Abgabe dokumentieren — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Abgabe dokumentieren</h1>
|
||||
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost text-xs">← Checkliste</a>
|
||||
</div>
|
||||
|
||||
<div class="max-w-xl">
|
||||
<form method="post" enctype="multipart/form-data" class="card space-y-4">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="form-label">{{ form.abgabe_zeitpunkt.label }}</label>
|
||||
{{ form.abgabe_zeitpunkt }}
|
||||
{% if form.abgabe_zeitpunkt.errors %}<p class="text-xs text-red-600 mt-1">{{ form.abgabe_zeitpunkt.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.abgabe_plattform.label }}</label>
|
||||
{{ form.abgabe_plattform }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.verantwortlicher.label }}</label>
|
||||
{{ form.verantwortlicher }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.abgabenachweis.label }}</label>
|
||||
{{ form.abgabenachweis }}
|
||||
<p class="text-xs text-slate-400 mt-1">Eingangsbestätigung, Screenshot o.ä.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.kommentar.label }}</label>
|
||||
{{ form.kommentar }}
|
||||
</div>
|
||||
<div class="flex gap-3 pt-2">
|
||||
<button type="submit" class="btn-primary">Abgabe bestätigen</button>
|
||||
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Problem bei Abgabe — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Problem bei Abgabe vermerken</h1>
|
||||
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost text-xs">← Checkliste</a>
|
||||
</div>
|
||||
|
||||
<div class="max-w-md">
|
||||
<form method="post" class="card space-y-4">
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<label class="form-label">Beschreibung des Problems</label>
|
||||
<textarea name="kommentar" rows="4" class="form-input w-full" placeholder="Was ist passiert?"></textarea>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary bg-amber-600 hover:bg-amber-700">Problem vermerken</button>
|
||||
<a href="{% url 'ausschreibungen:nachbetrachtung:abgabe:checkliste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
119
vergabe_teilnahme/templates/nachbetrachtung/detail.html
Normal file
119
vergabe_teilnahme/templates/nachbetrachtung/detail.html
Normal file
@@ -0,0 +1,119 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Nachbetrachtung — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Nachbetrachtung</h1>
|
||||
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost text-xs">← Ausschreibung</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Ergebnis</p>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for value, label in form.ergebnis.field.choices %}
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="radio" name="ergebnis" value="{{ value }}"
|
||||
{% if form.ergebnis.value == value %}checked{% endif %}
|
||||
class="text-blue-600">
|
||||
<span class="text-sm">{{ label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="card">
|
||||
<label class="form-label">{{ form.zuschlagsdatum.label }}</label>
|
||||
{{ form.zuschlagsdatum }}
|
||||
</div>
|
||||
<div class="card">
|
||||
<label class="form-label">{{ form.projektverantwortlicher.label }}</label>
|
||||
{{ form.projektverantwortlicher }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if kickoff_aufgabe %}
|
||||
<div class="card bg-green-50 border border-green-200">
|
||||
<p class="text-xs font-medium text-green-700 uppercase tracking-wide mb-1">Übergabe</p>
|
||||
<p class="text-sm text-green-800">
|
||||
Kickoff-Aufgabe:
|
||||
<a href="{% url 'ausschreibungen:aufgaben:detail' ausschreibung.pk kickoff_aufgabe.pk %}"
|
||||
class="underline">{{ kickoff_aufgabe.titel }}</a>
|
||||
— {{ kickoff_aufgabe.get_status_display }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Abgegebene Unterlagen</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="form-label">{{ form.abgegebene_unterlagen.label }}</label>
|
||||
{{ form.abgegebene_unterlagen }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.abgegebene_preise.label }}</label>
|
||||
{{ form.abgegebene_preise }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" x-data="{ gruende: {{ verlustgruende_json }} }">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Verlustanalyse</p>
|
||||
|
||||
<div>
|
||||
<template x-for="(g, i) in gruende" :key="i">
|
||||
<div class="flex gap-2 mb-2 items-center">
|
||||
<input x-model="g.grund" class="form-input flex-1" placeholder="Verlustgrund">
|
||||
<select x-model="g.kategorie" class="form-input w-36">
|
||||
<option value="preis">Preis</option>
|
||||
<option value="referenz">Referenz</option>
|
||||
<option value="anforderung">Anforderung</option>
|
||||
<option value="subunternehmer">Subunternehmer</option>
|
||||
<option value="sonstiges">Sonstiges</option>
|
||||
</select>
|
||||
<input type="number" x-model.number="g.verlaesslichkeit" min="1" max="5"
|
||||
class="form-input w-20" placeholder="1-5" title="Verlässlichkeit 1-5">
|
||||
<button type="button" @click="gruende.splice(i, 1)" class="btn-ghost text-red-500 text-xs">✕</button>
|
||||
</div>
|
||||
</template>
|
||||
<button type="button"
|
||||
@click="gruende.push({grund:'', kategorie:'sonstiges', verlaesslichkeit:3})"
|
||||
class="btn-secondary text-xs mt-1">+ Verlustgrund</button>
|
||||
</div>
|
||||
<input type="hidden" name="verlustgruende" :value="JSON.stringify(gruende)">
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="form-label">{{ form.ausschlaggebende_zuschlagsmerkmale.label }}</label>
|
||||
{{ form.ausschlaggebende_zuschlagsmerkmale }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Lessons Learned</p>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="form-label">{{ form.lessons_learned.label }}</label>
|
||||
{{ form.lessons_learned }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">{{ form.empfehlungen.label }}</label>
|
||||
{{ form.empfehlungen }}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{{ form.wiederverwendbare_erkenntnisse_markiert }}
|
||||
<label class="text-sm text-slate-700">Wiederverwendbare Erkenntnisse markieren</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'ausschreibungen:detail' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: WP-0009
|
||||
title: Abgabe und Nachbetrachtung
|
||||
status: todo
|
||||
status: done
|
||||
phase: 9-of-12
|
||||
created: "2026-05-08"
|
||||
depends_on: WP-0008
|
||||
@@ -18,7 +18,7 @@ Referenz: UC-AB-01 bis UC-AB-03, UC-NB-01 bis UC-NB-03.
|
||||
```task
|
||||
id: WP-0009-T01
|
||||
title: Abgabe-Checkliste mit Vollständigkeitsstatus (UC-AB-01)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`nachbetrachtung/abgabe_views.py` — abgabe_checkliste:
|
||||
|
||||
@@ -58,7 +58,7 @@ Template `nachbetrachtung/abgabe.html`:
|
||||
```task
|
||||
id: WP-0009-T02
|
||||
title: Abgabe dokumentieren mit Nachweis-Upload (UC-AB-02)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`nachbetrachtung/abgabe_views.py` — abgabe_dokumentieren:
|
||||
|
||||
@@ -99,7 +99,7 @@ def abgabe_dokumentieren(request, ausschreibung_id):
|
||||
```task
|
||||
id: WP-0009-T03
|
||||
title: Nachbetrachtung-View — Ergebnis und Kickoff (UC-NB-01)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`nachbetrachtung/views.py` — nachbetrachtung_detail:
|
||||
|
||||
@@ -134,7 +134,7 @@ Template `nachbetrachtung/detail.html`:
|
||||
```task
|
||||
id: WP-0009-T04
|
||||
title: Verlustanalyse und Lessons Learned (UC-NB-02, UC-NB-03)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
**Verlustgründe** — dynamisches JSONField-Formular:
|
||||
Alpine.js-gesteuertes Array:
|
||||
@@ -170,7 +170,7 @@ Bei Speichern: Aktualisiere `Nachbetrachtung`-Objekt, Redirect zur Nachbetrachtu
|
||||
```task
|
||||
id: WP-0009-T05
|
||||
title: URL-Verkabelung Abgabe/Nachbetrachtung und Tests
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`nachbetrachtung/abgabe_urls.py`:
|
||||
```python
|
||||
|
||||
Reference in New Issue
Block a user