diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md
index 670a241..44a1d20 100644
--- a/WORK-RECORDS.md
+++ b/WORK-RECORDS.md
@@ -9,7 +9,7 @@
| Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- |
| workplan | VERGABE-WP-0018 | blocked | — | workplans/VERGABE-WP-0018-customer-factory-delivery.md |
-| workplan | VERGABE-WP-0019 | active | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
+| workplan | VERGABE-WP-0019 | blocked | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
| workplan | VT-WP-0001 | finished | — | workplans/VT-WP-0001-projektgeruest.md |
| workplan | VT-WP-0002 | finished | — | workplans/VT-WP-0002-fachmodelle.md |
| workplan | VT-WP-0003 | finished | — | workplans/VT-WP-0003-basis-ui.md |
@@ -32,10 +32,10 @@
| task | VERGABE-WP-0018-T03 | wait | — | workplans/VERGABE-WP-0018-customer-factory-delivery.md |
| task | VERGABE-WP-0019-T01 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
| task | VERGABE-WP-0019-T02 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
-| task | VERGABE-WP-0019-T03 | progress | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
+| task | VERGABE-WP-0019-T03 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
| task | VERGABE-WP-0019-T04 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
| task | VERGABE-WP-0019-T05 | done | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
-| task | VERGABE-WP-0019-T06 | progress | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
+| task | VERGABE-WP-0019-T06 | wait | — | workplans/VERGABE-WP-0019-invited-company-pilot.md |
| task | VT-WP-0001-T03 | done | — | workplans/VT-WP-0001-projektgeruest.md |
| task | VT-WP-0001-T05 | done | — | workplans/VT-WP-0001-projektgeruest.md |
| task | VT-WP-0001-T07 | done | — | workplans/VT-WP-0001-projektgeruest.md |
@@ -45,6 +45,18 @@
| task | VT-WP-0002-T13 | done | — | workplans/VT-WP-0002-fachmodelle.md |
| task | VT-WP-0003-T01 | done | — | workplans/VT-WP-0003-basis-ui.md |
| task | VT-WP-0003-T10 | done | — | workplans/VT-WP-0003-basis-ui.md |
+| task | VT-WP-0004-T01 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T02 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T03 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T04 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T05 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T06 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T07 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T08 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T09 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T10 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T11 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
+| task | VT-WP-0004-T12 | done | — | workplans/VT-WP-0004-dashboard-ausschreibungen.md |
| task | VT-WP-0005-T07 | done | — | workplans/VT-WP-0005-lose-anforderungen.md |
| task | VT-WP-0012-T06 | done | — | workplans/VT-WP-0012-querschnitt.md |
| task | VT-WP-0015-T03 | done | — | workplans/VT-WP-0015-aufgaben-verknuepfungen-frist-issuefacade.md |
diff --git a/vergabe_teilnahme/apps/ausschreibungen/forms.py b/vergabe_teilnahme/apps/ausschreibungen/forms.py
index b9a95da..a661ba3 100644
--- a/vergabe_teilnahme/apps/ausschreibungen/forms.py
+++ b/vergabe_teilnahme/apps/ausschreibungen/forms.py
@@ -38,6 +38,13 @@ class AusschreibungForm(forms.ModelForm):
'entscheidungsbegruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}),
}
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, historisch=False, **kwargs):
super().__init__(*args, **kwargs)
self.fields['teilnahmeentscheidung'].required = False
+ if historisch:
+ from vergabe_teilnahme.apps.nachbetrachtung.models import Nachbetrachtung
+
+ self.fields['ergebnis'] = forms.ChoiceField(
+ choices=Nachbetrachtung.ERGEBNIS_CHOICES, initial='offen',
+ widget=forms.Select(attrs={'class': 'form-input'}),
+ )
diff --git a/vergabe_teilnahme/apps/ausschreibungen/tests.py b/vergabe_teilnahme/apps/ausschreibungen/tests.py
index c3a7ad2..e0e6d36 100644
--- a/vergabe_teilnahme/apps/ausschreibungen/tests.py
+++ b/vergabe_teilnahme/apps/ausschreibungen/tests.py
@@ -98,3 +98,93 @@ def test_status_htmx_returns_partial(admitted_client):
response = admitted_client.post(url, {"status": "3"}, HTTP_HX_REQUEST="true")
assert response.status_code == 200
assert b"status-widget" in response.content
+
+
+@pytest.mark.django_db
+def test_dashboard_expiry_boundary(admitted_client):
+ from datetime import date, timedelta
+
+ from vergabe_teilnahme.apps.bibliothek.models import Nachweis
+
+ for title, days in [('Expired', -1), ('Boundary', 60), ('Later', 61)]:
+ Nachweis.objects.create(titel=title, gueltig_bis=date.today() + timedelta(days=days))
+ Nachweis.objects.create(titel='Undated')
+ response = admitted_client.get(reverse('ausschreibungen:dashboard'))
+ assert response.status_code == 200
+ assert [n.titel for n in response.context['ablaufende_nachweise']] == ['Expired', 'Boundary']
+ assert 'Boundary' in response.content.decode()
+
+
+@pytest.mark.django_db
+def test_list_search_combines_filters_and_htmx(admitted_client):
+ matching = AusschreibungFactory(titel='Network upgrade', ausschreiber='City', status=4)
+ AusschreibungFactory(titel='Other', ausschreiber='City', status=3)
+ AusschreibungFactory(titel='Archived', ausschreiber='City', status=4, archiviert=True)
+ for query in ['Network', 'City']:
+ response = admitted_client.get(reverse('ausschreibungen:liste'),
+ {'q': query, 'status': '4'}, HTTP_HX_REQUEST='true')
+ assert list(response.context['ausschreibungen']) == [matching]
+ assert 'ausschreibungen/liste_partial.html' in [t.name for t in response.templates]
+
+
+@pytest.mark.django_db
+def test_historical_result_validation_and_save(admitted_client):
+ url = reverse('ausschreibungen:neu')
+ response = admitted_client.get(url, {'historisch': '1'})
+ assert 'name="ergebnis"' in response.content.decode()
+ data = {'titel': 'Historical award', 'ausschreiber': 'City',
+ 'historisch_erfassen': '1', 'teilnahmeentscheidung': 'teilnahme',
+ 'ergebnis': 'invalid'}
+ response = admitted_client.post(url, data)
+ assert response.status_code == 200
+ assert response.context['form'].errors['ergebnis']
+ assert not Ausschreibung.objects.filter(titel=data['titel']).exists()
+ data['ergebnis'] = 'gewonnen'
+ response = admitted_client.post(url, data)
+ assert response.status_code == 302
+ tender = Ausschreibung.objects.get(titel=data['titel'])
+ assert tender.nachbetrachtung.ergebnis == 'gewonnen'
+ assert tender.status == 10
+ assert tender.teilnahmeentscheidung == 'teilnahme'
+
+
+@pytest.mark.django_db
+def test_global_search_tasks_partners_and_issuer(admitted_client):
+ from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
+ from vergabe_teilnahme.apps.partner.models import Subunternehmer
+
+ tender = AusschreibungFactory(titel='Tender', ausschreiber='UniqueIssuer')
+ task = Aufgabe.objects.create(titel='UniqueTask', ausschreibung=tender)
+ partner = Subunternehmer.objects.create(name='UniquePartner')
+ cases = [
+ ('UniqueIssuer', reverse('ausschreibungen:detail', kwargs={'pk': tender.pk})),
+ ('UniqueTask', reverse('ausschreibungen:aufgaben:detail',
+ kwargs={'ausschreibung_id': tender.pk, 'pk': task.pk})),
+ ('UniquePartner', reverse('partner:su_detail', kwargs={'pk': partner.pk})),
+ ]
+ for query, target in cases:
+ response = admitted_client.get('/suche/', {'q': query})
+ assert response.status_code == 200
+ assert f'href="{target}"' in response.content.decode()
+
+
+
+@pytest.mark.django_db
+def test_inline_status_with_rendered_csrf_header(mitarbeiter):
+ import json
+ import re
+
+ from django.test import Client
+
+ client = Client(enforce_csrf_checks=True)
+ client.force_login(mitarbeiter)
+ tender = AusschreibungFactory(status=1)
+ response = client.get(reverse('ausschreibungen:detail', kwargs={'pk': tender.pk}))
+ headers = json.loads(re.search(r"hx-headers='([^']+)'", response.content.decode())[1])
+ url = reverse('ausschreibungen:status', kwargs={'pk': tender.pk})
+ assert client.post(url, {'status': '4'}).status_code == 403
+ response = client.post(url, {'status': '4'}, HTTP_HX_REQUEST='true',
+ HTTP_X_CSRFTOKEN=headers['X-CSRFToken'])
+ assert response.status_code == 200
+ tender.refresh_from_db()
+ assert tender.status == 4
diff --git a/vergabe_teilnahme/apps/ausschreibungen/views.py b/vergabe_teilnahme/apps/ausschreibungen/views.py
index f6e74e8..90816a7 100644
--- a/vergabe_teilnahme/apps/ausschreibungen/views.py
+++ b/vergabe_teilnahme/apps/ausschreibungen/views.py
@@ -1,5 +1,7 @@
from datetime import date, timedelta
+from django.db import transaction
+from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
@@ -15,6 +17,7 @@ def _is_htmx(request):
def dashboard(request):
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
+ from vergabe_teilnahme.apps.bibliothek.models import Nachweis
heute = date.today()
in_14_tagen = heute + timedelta(days=14)
@@ -41,6 +44,10 @@ def dashboard(request):
status__range=(1, 9),
).order_by('-geaendert_am')[:10],
+ 'ablaufende_nachweise': Nachweis.objects.filter(
+ gueltig_bis__lte=heute + timedelta(days=60),
+ ).order_by('gueltig_bis', 'pk'),
+ 'today': heute,
'breadcrumbs': [{'label': 'Übersicht', 'url': None}],
}
return render(request, 'ausschreibungen/dashboard.html', ctx)
@@ -49,6 +56,10 @@ def dashboard(request):
def ausschreibung_liste(request):
qs = Ausschreibung.objects.all()
+ q = request.GET.get('q', '').strip()
+ if q:
+ qs = qs.filter(Q(titel__icontains=q) | Q(ausschreiber__icontains=q))
+
status_filter = request.GET.get('status')
if status_filter:
qs = qs.filter(status=status_filter)
@@ -60,7 +71,7 @@ def ausschreibung_liste(request):
if bid_manager_filter:
qs = qs.filter(bid_manager=bid_manager_filter)
- from django.db.models import Count, Q
+ from django.db.models import Count
qs = qs.select_related('bid_manager').annotate(
aufgaben_total=Count('aufgaben', distinct=True),
aufgaben_erledigt=Count(
@@ -75,6 +86,7 @@ def ausschreibung_liste(request):
'status_choices': Ausschreibung.STATUS_CHOICES,
'mitarbeiter': Mitarbeiter.objects.all(),
'archiviert': archiviert,
+ 'q': q,
'current_status': status_filter or '',
'current_bid_manager': bid_manager_filter or '',
'breadcrumbs': [{'label': 'Ausschreibungen', 'url': None}],
@@ -88,14 +100,28 @@ def ausschreibung_liste(request):
def ausschreibung_neu(request):
from .forms import AusschreibungForm
- historisch = request.GET.get('historisch') == '1'
+ historisch = (request.GET.get('historisch') == '1'
+ or request.POST.get('historisch_erfassen') == '1')
if request.method == 'POST':
- form = AusschreibungForm(request.POST)
+ form = AusschreibungForm(request.POST, historisch=historisch)
if form.is_valid():
- a = form.save()
+ with transaction.atomic():
+ a = form.save()
+ if historisch:
+ from vergabe_teilnahme.apps.nachbetrachtung.models import Nachbetrachtung
+
+ ergebnis = form.cleaned_data["ergebnis"]
+ Nachbetrachtung.objects.create(ausschreibung=a, ergebnis=ergebnis)
+ result_status = {
+ "gewonnen": 10, "verloren": 11,
+ "aufgehoben": 12, "zurueckgezogen": 13,
+ }
+ if ergebnis in result_status:
+ a.status = result_status[ergebnis]
+ a.save(update_fields=["status"])
return redirect('ausschreibungen:detail', pk=a.pk)
else:
- form = AusschreibungForm()
+ form = AusschreibungForm(historisch=historisch)
return render(request, 'ausschreibungen/form.html', {
'form': form,
diff --git a/vergabe_teilnahme/apps/core/views.py b/vergabe_teilnahme/apps/core/views.py
index b880bba..b4ae25b 100644
--- a/vergabe_teilnahme/apps/core/views.py
+++ b/vergabe_teilnahme/apps/core/views.py
@@ -200,23 +200,35 @@ def suche(request):
q = request.GET.get('q', '').strip()
if not q or len(q) < 2:
return HttpResponse('')
+ from django.db.models import Q
+
+ from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
from vergabe_teilnahme.apps.bibliothek.models import Nachweis, Referenz
from vergabe_teilnahme.apps.dokumente.models import Dokument
from vergabe_teilnahme.apps.marktbegleiter.models import Marktbegleiter
+ from vergabe_teilnahme.apps.partner.models import Subunternehmer
- ausschreibungen = Ausschreibung.objects.filter(titel__icontains=q)[:5]
+ ausschreibungen = Ausschreibung.objects.filter(
+ Q(titel__icontains=q) | Q(ausschreiber__icontains=q)
+ )[:5]
+ aufgaben = Aufgabe.objects.filter(titel__icontains=q)[:5]
+ subunternehmer = Subunternehmer.objects.filter(name__icontains=q)[:5]
dokumente = Dokument.objects.filter(dateiname__icontains=q).select_related('ausschreibung')[:5]
nachweise = Nachweis.objects.filter(titel__icontains=q)[:5]
referenzen = Referenz.objects.filter(referenztitel__icontains=q)[:5]
marktbegleiter = Marktbegleiter.objects.filter(name__icontains=q)[:5]
- has_results = any([ausschreibungen, dokumente, nachweise, referenzen, marktbegleiter])
+ has_results = any([
+ ausschreibungen, aufgaben, subunternehmer, dokumente, nachweise, referenzen, marktbegleiter,
+ ])
if not has_results:
return render(request, 'partials/search_results.html', {'q': q, 'empty': True})
return render(request, 'partials/search_results.html', {
'q': q,
+ 'aufgaben': aufgaben,
+ 'subunternehmer': subunternehmer,
'ausschreibungen': ausschreibungen,
'dokumente': dokumente,
'nachweise': nachweise,
diff --git a/vergabe_teilnahme/templates/ausschreibungen/dashboard.html b/vergabe_teilnahme/templates/ausschreibungen/dashboard.html
index aaa2edc..06f63a6 100644
--- a/vergabe_teilnahme/templates/ausschreibungen/dashboard.html
+++ b/vergabe_teilnahme/templates/ausschreibungen/dashboard.html
@@ -98,5 +98,19 @@
{% endif %}
+
+
+
Ablaufende Nachweise (60 Tage)
+ {{ ablaufende_nachweise|length }}
+
+ {% for n in ablaufende_nachweise %}
+
+
{{ n.titel }}
+
{{ n.gueltig_bis|date:"d.m.Y" }}
+
+ {% empty %}
+
Keine ablaufenden Nachweise.
+ {% endfor %}
+
{% endblock %}
diff --git a/vergabe_teilnahme/templates/ausschreibungen/form.html b/vergabe_teilnahme/templates/ausschreibungen/form.html
index 5c407e8..b5b67cb 100644
--- a/vergabe_teilnahme/templates/ausschreibungen/form.html
+++ b/vergabe_teilnahme/templates/ausschreibungen/form.html
@@ -119,6 +119,11 @@
{{ form.teilnahmeentscheidung }}
+
+
+ {{ form.ergebnis }}
+ {{ form.ergebnis.errors }}
+
{{ form.entscheidungsbegruendung }}
diff --git a/vergabe_teilnahme/templates/ausschreibungen/liste.html b/vergabe_teilnahme/templates/ausschreibungen/liste.html
index 7661faa..32ef704 100644
--- a/vergabe_teilnahme/templates/ausschreibungen/liste.html
+++ b/vergabe_teilnahme/templates/ausschreibungen/liste.html
@@ -13,8 +13,12 @@
hx-get="{% url 'ausschreibungen:liste' %}"
hx-target="#ausschreibungen-table"
hx-push-url="true"
- hx-trigger="change from:select, change from:input[type=checkbox]"
+ hx-trigger="change from:select, change from:input[type=checkbox], input changed delay:300ms from:input[type=search]"
class="flex flex-wrap gap-3 items-end">
+
+
+
+