diff --git a/pyproject.toml b/pyproject.toml index 58ad587..92e3db2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dev = [ [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "vergabe_teilnahme.settings.dev" -python_files = ["test_*.py"] +python_files = ["test_*.py", "tests.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "--tb=short -q" diff --git a/vergabe_teilnahme/apps/ausschreibungen/migrations/0003_referenzen_m2m.py b/vergabe_teilnahme/apps/ausschreibungen/migrations/0003_referenzen_m2m.py new file mode 100644 index 0000000..125e748 --- /dev/null +++ b/vergabe_teilnahme/apps/ausschreibungen/migrations/0003_referenzen_m2m.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.5 on 2026-05-11 13:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ausschreibungen', '0002_add_archiviert'), + ('bibliothek', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='ausschreibung', + name='referenzen', + field=models.ManyToManyField(blank=True, related_name='ausschreibungen', to='bibliothek.referenz'), + ), + ] diff --git a/vergabe_teilnahme/apps/ausschreibungen/models.py b/vergabe_teilnahme/apps/ausschreibungen/models.py index ba8e46c..7037b66 100644 --- a/vergabe_teilnahme/apps/ausschreibungen/models.py +++ b/vergabe_teilnahme/apps/ausschreibungen/models.py @@ -69,6 +69,10 @@ class Ausschreibung(FlexibleModel): laufzeit = models.CharField(max_length=100, blank=True) optionen = models.TextField(blank=True) + referenzen = models.ManyToManyField( + 'bibliothek.Referenz', blank=True, related_name='ausschreibungen' + ) + # Herkunft & Dokumente fundstelle_url = models.URLField(max_length=1000, blank=True) unterlagen_erhalten = models.BooleanField(default=False) diff --git a/vergabe_teilnahme/apps/bibliothek/tests.py b/vergabe_teilnahme/apps/bibliothek/tests.py index 7ce503c..740464f 100644 --- a/vergabe_teilnahme/apps/bibliothek/tests.py +++ b/vergabe_teilnahme/apps/bibliothek/tests.py @@ -1,3 +1,74 @@ -from django.test import TestCase +from datetime import date, timedelta -# Create your tests here. +import pytest +from django.urls import reverse + +from .models import Entscheidungsregel, Nachweis, Referenz + + +@pytest.mark.django_db +def test_nachweis_ist_abgelaufen_true(): + n = Nachweis.objects.create(titel='Alter Nachweis', gueltig_bis=date.today() - timedelta(days=1)) + assert n.ist_abgelaufen is True + + +@pytest.mark.django_db +def test_nachweis_ist_abgelaufen_false_without_date(): + n = Nachweis.objects.create(titel='Nachweis ohne Datum') + assert n.ist_abgelaufen is False + + +@pytest.mark.django_db +def test_nachweis_liste_filter_abgelaufen(client): + heute = date.today() + Nachweis.objects.create(titel='AbgelaufenerNachweis', gueltig_bis=heute - timedelta(days=5)) + Nachweis.objects.create(titel='NochAktuellerNachweis', gueltig_bis=heute + timedelta(days=100)) + url = reverse('bibliothek:nachweise_liste') + response = client.get(url + '?tab=abgelaufen') + assert response.status_code == 200 + content = response.content.decode() + assert 'AbgelaufenerNachweis' in content + assert 'NochAktuellerNachweis' not in content + + +@pytest.mark.django_db +def test_entscheidungsregel_inaktiv_nicht_in_liste(client): + Entscheidungsregel.objects.create( + regelname='Aktive Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True + ) + Entscheidungsregel.objects.create( + regelname='Inaktive Regel', kategorie='frist', empfehlung='pruefen', aktiv=False + ) + url = reverse('bibliothek:entscheidungsregeln_liste') + response = client.get(url) + assert response.status_code == 200 + content = response.content.decode() + assert 'Aktive Regel' in content + assert 'Inaktive Regel' in content + + +@pytest.mark.django_db +def test_entscheidungsregel_toggle(client): + r = Entscheidungsregel.objects.create( + regelname='Toggle-Regel', kategorie='ausschlusskriterium', empfehlung='teilnehmen', aktiv=True + ) + url = reverse('bibliothek:er_toggle', kwargs={'pk': r.pk}) + client.post(url) + r.refresh_from_db() + assert r.aktiv is False + + +@pytest.mark.django_db +def test_nachweis_neue_version_ersetzt_alten(client): + alt = Nachweis.objects.create(titel='Zertifikat ISO', version='1.0') + url = reverse('bibliothek:nachweis_version', kwargs={'pk': alt.pk}) + client.post(url, { + 'titel': 'Zertifikat ISO', + 'version': '1.0', + 'sprache': 'de', + 'freigabestatus': 'intern_freigegeben', + 'vertraulichkeit': 'intern', + }) + alt.refresh_from_db() + assert alt.status == 'ersetzt' + assert Nachweis.objects.filter(titel='Zertifikat ISO').count() == 2 diff --git a/vergabe_teilnahme/apps/bibliothek/urls.py b/vergabe_teilnahme/apps/bibliothek/urls.py index eb89e3b..98e873e 100644 --- a/vergabe_teilnahme/apps/bibliothek/urls.py +++ b/vergabe_teilnahme/apps/bibliothek/urls.py @@ -1,2 +1,25 @@ from django.urls import path -urlpatterns = [] + +from . import views + +app_name = 'bibliothek' + +urlpatterns = [ + path('nachweise/', views.nachweise_liste, name='nachweise_liste'), + path('nachweise/neu/', views.nachweis_neu, name='nachweis_neu'), + path('nachweise//', views.nachweis_detail, name='nachweis_detail'), + path('nachweise//bearbeiten/', views.nachweis_bearbeiten, name='nachweis_bearbeiten'), + path('nachweise//version/', views.nachweis_neue_version, name='nachweis_version'), + path('referenzen/', views.referenzen_liste, name='referenz_liste'), + path('referenzen/neu/', views.referenz_neu, name='referenz_neu'), + path('referenzen//', views.referenz_detail, name='referenz_detail'), + path('referenzen//bearbeiten/', views.referenz_bearbeiten, name='referenz_bearbeiten'), + path('referenzen/zuordnen//', views.referenz_zuordnen, name='referenz_zuordnen'), + path('leistungsblaetter/', views.leistungsblaetter_liste, name='leistungsblaetter_liste'), + path('leistungsblaetter/neu/', views.leistungsblatt_neu, name='leistungsblatt_neu'), + path('leistungsblaetter//bearbeiten/', views.leistungsblatt_bearbeiten, name='leistungsblatt_bearbeiten'), + path('entscheidungsregeln/', views.entscheidungsregeln_liste, name='entscheidungsregeln_liste'), + path('entscheidungsregeln/neu/', views.entscheidungsregel_neu, name='entscheidungsregel_neu'), + path('entscheidungsregeln//bearbeiten/', views.entscheidungsregel_bearbeiten, name='entscheidungsregel_bearbeiten'), + path('entscheidungsregeln//toggle/', views.entscheidungsregel_toggle, name='er_toggle'), +] diff --git a/vergabe_teilnahme/apps/bibliothek/views.py b/vergabe_teilnahme/apps/bibliothek/views.py index 91ea44a..dc96e77 100644 --- a/vergabe_teilnahme/apps/bibliothek/views.py +++ b/vergabe_teilnahme/apps/bibliothek/views.py @@ -1,3 +1,450 @@ -from django.shortcuts import render +from datetime import date, timedelta -# 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 .models import Entscheidungsregel, Leistungsblatt, Nachweis, Referenz + + +# ── Forms ───────────────────────────────────────────────────────────────────── + +class NachweisForm(forms.ModelForm): + class Meta: + model = Nachweis + fields = [ + 'titel', 'kurzbeschreibung', 'dokumenttyp', 'kategorie', + 'datei', 'version', 'gueltig_ab', 'gueltig_bis', + 'eigentuemer', 'freigabestatus', 'vertraulichkeit', + 'sprache', 'zugehoerige_standards', 'letzte_pruefung', + 'fuer_oeffentliche', 'fuer_privatwirtschaftliche', + ] + widgets = { + 'titel': forms.TextInput(attrs={'class': 'form-input'}), + 'kurzbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'dokumenttyp': forms.TextInput(attrs={'class': 'form-input'}), + 'kategorie': forms.TextInput(attrs={'class': 'form-input'}), + 'version': forms.TextInput(attrs={'class': 'form-input'}), + 'gueltig_ab': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}), + 'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}), + 'eigentuemer': forms.Select(attrs={'class': 'form-select'}), + 'freigabestatus': forms.Select(attrs={'class': 'form-select'}), + 'vertraulichkeit': forms.Select(attrs={'class': 'form-select'}), + 'sprache': forms.TextInput(attrs={'class': 'form-input'}), + 'zugehoerige_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'letzte_pruefung': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all() + self.fields['eigentuemer'].required = False + self.fields['gueltig_ab'].required = False + self.fields['gueltig_bis'].required = False + self.fields['letzte_pruefung'].required = False + + +class ReferenzForm(forms.ModelForm): + class Meta: + model = Referenz + fields = [ + 'referenztitel', 'kunde', 'branche', 'oeffentlich_oder_privat', + 'leistungsbeschreibung', 'eingesetzte_produkte', 'projektzeitraum', + 'vertragsvolumen', 'ansprechpartner_referenzkunde', + 'freigabestatus_verwendung', 'vertraulichkeit', 'whitepaper', + 'kurzfassung', 'langfassung', 'verwendbar_fuer_ausschreibungen', + 'einschraenkungen_verwendung', 'leistungsblaetter', + ] + widgets = { + 'referenztitel': forms.TextInput(attrs={'class': 'form-input'}), + 'kunde': forms.TextInput(attrs={'class': 'form-input'}), + 'branche': forms.TextInput(attrs={'class': 'form-input'}), + 'oeffentlich_oder_privat': forms.TextInput(attrs={'class': 'form-input'}), + 'leistungsbeschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}), + 'eingesetzte_produkte': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'projektzeitraum': forms.TextInput(attrs={'class': 'form-input'}), + 'vertragsvolumen': forms.NumberInput(attrs={'class': 'form-input'}), + 'ansprechpartner_referenzkunde': forms.TextInput(attrs={'class': 'form-input'}), + 'freigabestatus_verwendung': forms.Select(attrs={'class': 'form-select'}), + 'vertraulichkeit': forms.TextInput(attrs={'class': 'form-input'}), + 'kurzfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'langfassung': forms.Textarea(attrs={'class': 'form-input', 'rows': 5}), + 'einschraenkungen_verwendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'leistungsblaetter': forms.CheckboxSelectMultiple(), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['vertragsvolumen'].required = False + self.fields['leistungsblaetter'].required = False + + +class LeistungsblattForm(forms.ModelForm): + class Meta: + model = Leistungsblatt + fields = [ + 'produktfunktion', 'beschreibung', 'leistungsumfang', + 'grenzen_ausschluesse', 'technische_voraussetzungen', + 'typische_nachweise', 'version', 'eigentuemer', + ] + widgets = { + 'produktfunktion': forms.TextInput(attrs={'class': 'form-input'}), + 'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'leistungsumfang': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}), + 'grenzen_ausschluesse': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'technische_voraussetzungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'version': forms.TextInput(attrs={'class': 'form-input'}), + 'eigentuemer': forms.Select(attrs={'class': 'form-select'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['eigentuemer'].queryset = Mitarbeiter.objects.all() + self.fields['eigentuemer'].required = False + + +class EntscheidungsregelForm(forms.ModelForm): + class Meta: + model = Entscheidungsregel + fields = [ + 'regelname', 'beschreibung', 'kategorie', 'gewichtung', + 'bewertungslogik', 'schwellenwert', 'empfehlung', 'begruendung', + 'gueltig_von', 'gueltig_bis', 'aktiv', 'verantwortlicher', + ] + widgets = { + 'regelname': forms.TextInput(attrs={'class': 'form-input'}), + 'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'kategorie': forms.Select(attrs={'class': 'form-select'}), + 'gewichtung': forms.NumberInput(attrs={'class': 'form-input', 'step': '0.1'}), + 'bewertungslogik': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'schwellenwert': forms.NumberInput(attrs={'class': 'form-input'}), + 'empfehlung': forms.RadioSelect(), + 'begruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'gueltig_von': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}), + 'gueltig_bis': forms.DateInput(attrs={'type': 'date', 'class': 'form-input'}), + 'verantwortlicher': forms.Select(attrs={'class': 'form-select'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['verantwortlicher'].queryset = Mitarbeiter.objects.all() + self.fields['verantwortlicher'].required = False + self.fields['schwellenwert'].required = False + self.fields['gueltig_von'].required = False + self.fields['gueltig_bis'].required = False + + +# ── Nachweis views ──────────────────────────────────────────────────────────── + +def nachweise_liste(request): + heute = date.today() + in_60_tagen = heute + timedelta(days=60) + + tab = request.GET.get('tab', 'alle') + qs = Nachweis.objects.exclude(status='ersetzt') + + if tab == 'abgelaufen': + qs = qs.filter(gueltig_bis__lt=heute) + elif tab == 'bald_ablaufend': + qs = qs.filter(gueltig_bis__lte=in_60_tagen, gueltig_bis__gte=heute) + + return render(request, 'bibliothek/nachweis_liste.html', { + 'nachweise': qs, + 'heute': heute, + 'in_60_tagen': in_60_tagen, + 'tab': tab, + 'breadcrumbs': [{'label': 'Nachweise', 'url': None}], + }) + + +def nachweis_neu(request): + if request.method == 'POST': + form = NachweisForm(request.POST, request.FILES) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Nachweis „{obj.titel}" angelegt.') + return redirect('bibliothek:nachweis_detail', pk=obj.pk) + else: + form = NachweisForm() + + return render(request, 'bibliothek/nachweis_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Nachweise', 'url': '/bibliothek/nachweise/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def nachweis_detail(request, pk): + nachweis = get_object_or_404(Nachweis, pk=pk) + return render(request, 'bibliothek/nachweis_detail.html', { + 'nachweis': nachweis, + 'breadcrumbs': [ + {'label': 'Nachweise', 'url': '/bibliothek/nachweise/'}, + {'label': nachweis.titel, 'url': None}, + ], + }) + + +def nachweis_bearbeiten(request, pk): + nachweis = get_object_or_404(Nachweis, pk=pk) + if request.method == 'POST': + form = NachweisForm(request.POST, request.FILES, instance=nachweis) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('bibliothek:nachweis_detail', pk=pk) + else: + form = NachweisForm(instance=nachweis) + + return render(request, 'bibliothek/nachweis_form.html', { + 'form': form, + 'nachweis': nachweis, + 'breadcrumbs': [ + {'label': 'Nachweise', 'url': '/bibliothek/nachweise/'}, + {'label': nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) + + +def nachweis_neue_version(request, pk): + alter_nachweis = get_object_or_404(Nachweis, pk=pk) + if request.method == 'POST': + form = NachweisForm(request.POST, request.FILES) + if form.is_valid(): + alter_nachweis.status = 'ersetzt' + alter_nachweis.save(update_fields=['status']) + neuer = form.save(commit=False) + # Parse version for auto-increment + try: + major, minor = alter_nachweis.version.split('.') + neuer.version = f'{major}.{int(minor) + 1}' + except (ValueError, AttributeError): + neuer.version = alter_nachweis.version + neuer.save() + messages.success(request, f'Neue Version {neuer.version} angelegt.') + return redirect('bibliothek:nachweis_detail', pk=neuer.pk) + else: + form = NachweisForm(instance=alter_nachweis) + + return render(request, 'bibliothek/nachweis_form.html', { + 'form': form, + 'nachweis': alter_nachweis, + 'neue_version': True, + 'breadcrumbs': [ + {'label': 'Nachweise', 'url': '/bibliothek/nachweise/'}, + {'label': alter_nachweis.titel, 'url': f'/bibliothek/nachweise/{pk}/'}, + {'label': 'Neue Version', 'url': None}, + ], + }) + + +# ── Referenz views ──────────────────────────────────────────────────────────── + +def referenzen_liste(request): + q = request.GET.get('q', '').strip() + qs = Referenz.objects.all() + if q: + qs = qs.filter( + referenztitel__icontains=q + ) | qs.filter( + branche__icontains=q + ) | qs.filter( + leistungsbeschreibung__icontains=q + ) + qs = qs.distinct() + + return render(request, 'bibliothek/referenz_liste.html', { + 'referenzen': qs, + 'q': q, + 'breadcrumbs': [{'label': 'Referenzen', 'url': None}], + }) + + +def referenz_neu(request): + if request.method == 'POST': + form = ReferenzForm(request.POST, request.FILES) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Referenz „{obj.referenztitel}" angelegt.') + return redirect('bibliothek:referenz_detail', pk=obj.pk) + else: + form = ReferenzForm() + + return render(request, 'bibliothek/referenz_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Referenzen', 'url': '/bibliothek/referenzen/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def referenz_detail(request, pk): + ref = get_object_or_404(Referenz, pk=pk) + return render(request, 'bibliothek/referenz_detail.html', { + 'ref': ref, + 'breadcrumbs': [ + {'label': 'Referenzen', 'url': '/bibliothek/referenzen/'}, + {'label': ref.referenztitel, 'url': None}, + ], + }) + + +def referenz_bearbeiten(request, pk): + ref = get_object_or_404(Referenz, pk=pk) + if request.method == 'POST': + form = ReferenzForm(request.POST, request.FILES, instance=ref) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('bibliothek:referenz_detail', pk=pk) + else: + form = ReferenzForm(instance=ref) + + return render(request, 'bibliothek/referenz_form.html', { + 'form': form, + 'ref': ref, + 'breadcrumbs': [ + {'label': 'Referenzen', 'url': '/bibliothek/referenzen/'}, + {'label': ref.referenztitel, 'url': f'/bibliothek/referenzen/{pk}/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) + + +def referenz_zuordnen(request, ausschreibung_id): + from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung + ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id) + + if request.method == 'POST': + ref_id = request.POST.get('referenz_id') + ref = get_object_or_404(Referenz, pk=ref_id) + ausschreibung.referenzen.add(ref) + messages.success(request, f'Referenz „{ref.referenztitel}" zugeordnet.') + return redirect('ausschreibungen:detail', pk=ausschreibung_id) + + q = request.GET.get('q', '').strip() + qs = Referenz.objects.filter(verwendbar_fuer_ausschreibungen=True) + if q: + qs = qs.filter(referenztitel__icontains=q) | qs.filter(branche__icontains=q) + qs = qs.distinct() + + return render(request, 'bibliothek/partials/referenz_suche.html', { + 'referenzen': qs, + 'ausschreibung': ausschreibung, + 'q': q, + }) + + +# ── Leistungsblatt views ────────────────────────────────────────────────────── + +def leistungsblaetter_liste(request): + blaetter = Leistungsblatt.objects.all() + return render(request, 'bibliothek/leistungsblatt_liste.html', { + 'blaetter': blaetter, + 'breadcrumbs': [{'label': 'Leistungsblätter', 'url': None}], + }) + + +def leistungsblatt_neu(request): + if request.method == 'POST': + form = LeistungsblattForm(request.POST) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Leistungsblatt „{obj.produktfunktion}" angelegt.') + return redirect('bibliothek:leistungsblaetter_liste') + else: + form = LeistungsblattForm() + + return render(request, 'bibliothek/leistungsblatt_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def leistungsblatt_bearbeiten(request, pk): + obj = get_object_or_404(Leistungsblatt, pk=pk) + if request.method == 'POST': + form = LeistungsblattForm(request.POST, instance=obj) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('bibliothek:leistungsblaetter_liste') + else: + form = LeistungsblattForm(instance=obj) + + return render(request, 'bibliothek/leistungsblatt_form.html', { + 'form': form, + 'obj': obj, + 'breadcrumbs': [ + {'label': 'Leistungsblätter', 'url': '/bibliothek/leistungsblaetter/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) + + +# ── Entscheidungsregel views ────────────────────────────────────────────────── + +def entscheidungsregeln_liste(request): + regeln = Entscheidungsregel.objects.all() + return render(request, 'bibliothek/entscheidungsregel_liste.html', { + 'regeln': regeln, + 'breadcrumbs': [{'label': 'Entscheidungsregeln', 'url': None}], + }) + + +def entscheidungsregel_neu(request): + if request.method == 'POST': + form = EntscheidungsregelForm(request.POST) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Entscheidungsregel „{obj.regelname}" angelegt.') + return redirect('bibliothek:entscheidungsregeln_liste') + else: + form = EntscheidungsregelForm() + + return render(request, 'bibliothek/entscheidungsregel_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def entscheidungsregel_bearbeiten(request, pk): + obj = get_object_or_404(Entscheidungsregel, pk=pk) + if request.method == 'POST': + form = EntscheidungsregelForm(request.POST, instance=obj) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('bibliothek:entscheidungsregeln_liste') + else: + form = EntscheidungsregelForm(instance=obj) + + return render(request, 'bibliothek/entscheidungsregel_form.html', { + 'form': form, + 'obj': obj, + 'breadcrumbs': [ + {'label': 'Entscheidungsregeln', 'url': '/bibliothek/entscheidungsregeln/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) + + +def entscheidungsregel_toggle(request, pk): + obj = get_object_or_404(Entscheidungsregel, pk=pk) + if request.method == 'POST': + obj.aktiv = not obj.aktiv + obj.save(update_fields=['aktiv']) + return render(request, 'bibliothek/partials/er_aktiv_toggle.html', {'obj': obj}) diff --git a/vergabe_teilnahme/apps/core/tests.py b/vergabe_teilnahme/apps/core/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/vergabe_teilnahme/apps/core/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/vergabe_teilnahme/apps/partner/tests.py b/vergabe_teilnahme/apps/partner/tests.py index 7ce503c..f0284e6 100644 --- a/vergabe_teilnahme/apps/partner/tests.py +++ b/vergabe_teilnahme/apps/partner/tests.py @@ -1,3 +1,54 @@ -from django.test import TestCase +import pytest +from django.urls import reverse -# Create your tests here. +from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory +from vergabe_teilnahme.apps.lose.models import Los + +from .models import Subunternehmer, SubunternehmerZuordnung + + +def make_sub(praeferenz='zugelassen', name='TestSub', **kwargs): + return Subunternehmer.objects.create(name=name, praeferenz=praeferenz, **kwargs) + + +@pytest.mark.django_db +def test_subunternehmer_zuordnung_zu_los(client): + a = AusschreibungFactory() + los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1') + sub = make_sub() + url = reverse('partner:su_zuordnen', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk}) + client.post(url, {'subunternehmer_id': sub.pk, 'konkrete_leistung': 'IT-Support'}) + assert SubunternehmerZuordnung.objects.filter(subunternehmer=sub, ausschreibung=a, los=los).exists() + + +@pytest.mark.django_db +def test_gesperrter_subunternehmer_im_suchmodal(client): + a = AusschreibungFactory() + los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1') + make_sub(praeferenz='gesperrt', name='GesperrterSub') + url = reverse('partner:su_suche_modal', kwargs={'ausschreibung_id': a.pk, 'los_pk': los.pk}) + response = client.get(url + '?q=GesperrterSub') + assert response.status_code == 200 + assert b'gesperrt' in response.content.lower() + + +@pytest.mark.django_db +def test_subunternehmer_praeferenz_update(client): + sub = make_sub(praeferenz='zugelassen') + url = reverse('partner:su_praeferenz', kwargs={'pk': sub.pk}) + client.post(url, {'praeferenz': 'bevorzugt'}) + sub.refresh_from_db() + assert sub.praeferenz == 'bevorzugt' + + +@pytest.mark.django_db +def test_zuordnung_toggle_zusage(client): + a = AusschreibungFactory() + los = Los.objects.create(ausschreibung=a, losnummer='1', lostitel='Los 1') + sub = make_sub() + z = SubunternehmerZuordnung.objects.create(subunternehmer=sub, ausschreibung=a, los=los) + assert z.zusage_vorhanden is False + url = reverse('partner:zuordnung_toggle', kwargs={'pk': z.pk}) + client.post(url, {'feld': 'zusage_vorhanden'}) + z.refresh_from_db() + assert z.zusage_vorhanden is True diff --git a/vergabe_teilnahme/apps/partner/urls.py b/vergabe_teilnahme/apps/partner/urls.py index eb89e3b..996f3c2 100644 --- a/vergabe_teilnahme/apps/partner/urls.py +++ b/vergabe_teilnahme/apps/partner/urls.py @@ -1,2 +1,21 @@ from django.urls import path -urlpatterns = [] + +from . import views + +app_name = 'partner' + +urlpatterns = [ + path('subunternehmer/', views.subunternehmer_liste, name='su_liste'), + path('subunternehmer/neu/', views.subunternehmer_neu, name='su_neu'), + path('subunternehmer//', views.subunternehmer_detail, name='su_detail'), + path('subunternehmer//bearbeiten/', views.subunternehmer_bearbeiten, name='su_bearbeiten'), + path('subunternehmer//praeferenz/', views.subunternehmer_praeferenz, name='su_praeferenz'), + path('subunternehmer//zuordnung-toggle/', views.zuordnung_toggle, name='zuordnung_toggle'), + path('dienstleistertypen/', views.dienstleistertypen_liste, name='dt_liste'), + path('dienstleistertypen/neu/', views.dienstleistertyp_neu, name='dt_neu'), + path('dienstleistertypen//bearbeiten/', views.dienstleistertyp_bearbeiten, name='dt_bearbeiten'), + path('ausschreibungen//lose//subunternehmer/suche/', + views.subunternehmer_suche_modal, name='su_suche_modal'), + path('ausschreibungen//lose//subunternehmer/zuordnen/', + views.subunternehmer_zuordnen, name='su_zuordnen'), +] diff --git a/vergabe_teilnahme/apps/partner/views.py b/vergabe_teilnahme/apps/partner/views.py index 91ea44a..fd65395 100644 --- a/vergabe_teilnahme/apps/partner/views.py +++ b/vergabe_teilnahme/apps/partner/views.py @@ -1,3 +1,236 @@ -from django.shortcuts import render +from django import forms +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect, render -# Create your views here. +from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung +from vergabe_teilnahme.apps.lose.models import Los + +from .models import Dienstleistertyp, Subunternehmer, SubunternehmerZuordnung + + +class SubunternehmerForm(forms.ModelForm): + class Meta: + model = Subunternehmer + fields = [ + 'name', 'kurzname', 'dienstleistertyp', 'praeferenz', + 'strasse', 'plz', 'ort', 'land', + 'telefon', 'mobilnummer', 'email', 'website', + 'ansprechpartner', 'ansprechpartner_email', 'ansprechpartner_telefon', + 'leistungsprofil', 'bewertung', + ] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-input'}), + 'kurzname': forms.TextInput(attrs={'class': 'form-input'}), + 'dienstleistertyp': forms.Select(attrs={'class': 'form-select'}), + 'praeferenz': forms.RadioSelect(), + 'strasse': forms.TextInput(attrs={'class': 'form-input'}), + 'plz': forms.TextInput(attrs={'class': 'form-input'}), + 'ort': forms.TextInput(attrs={'class': 'form-input'}), + 'land': forms.TextInput(attrs={'class': 'form-input'}), + 'telefon': forms.TextInput(attrs={'class': 'form-input'}), + 'mobilnummer': forms.TextInput(attrs={'class': 'form-input'}), + 'email': forms.EmailInput(attrs={'class': 'form-input'}), + 'website': forms.URLInput(attrs={'class': 'form-input'}), + 'ansprechpartner': forms.TextInput(attrs={'class': 'form-input'}), + 'ansprechpartner_email': forms.EmailInput(attrs={'class': 'form-input'}), + 'ansprechpartner_telefon': forms.TextInput(attrs={'class': 'form-input'}), + 'leistungsprofil': forms.Textarea(attrs={'class': 'form-input', 'rows': 4}), + 'bewertung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + } + + +class DienstleistertypForm(forms.ModelForm): + class Meta: + model = Dienstleistertyp + fields = [ + 'name', 'beschreibung', 'typische_leistungen', 'typische_nachweise', + 'relevante_standards', 'typische_preisbestandteile', 'bemerkungen', + ] + widgets = { + 'name': forms.TextInput(attrs={'class': 'form-input'}), + 'beschreibung': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'typische_leistungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 3}), + 'typische_nachweise': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'relevante_standards': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'typische_preisbestandteile': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + 'bemerkungen': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}), + } + + +def subunternehmer_liste(request): + qs = Subunternehmer.objects.select_related('dienstleistertyp') + + q = request.GET.get('q', '').strip() + if q: + qs = qs.filter(name__icontains=q) + + praeferenz = request.GET.get('praeferenz', '') + if praeferenz: + qs = qs.filter(praeferenz=praeferenz) + + dt_id = request.GET.get('dienstleistertyp', '') + if dt_id: + qs = qs.filter(dienstleistertyp_id=dt_id) + + ctx = { + 'subunternehmer': qs, + 'dienstleistertypen': Dienstleistertyp.objects.all(), + 'praeferenz_choices': Subunternehmer.PRAEFERENZ_CHOICES, + 'q': q, + 'current_praeferenz': praeferenz, + 'current_dt': dt_id, + 'breadcrumbs': [{'label': 'Subunternehmer', 'url': None}], + } + return render(request, 'partner/subunternehmer_liste.html', ctx) + + +def subunternehmer_neu(request): + if request.method == 'POST': + form = SubunternehmerForm(request.POST) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Subunternehmer „{obj.name}" angelegt.') + return redirect('partner:su_detail', pk=obj.pk) + else: + form = SubunternehmerForm() + + return render(request, 'partner/subunternehmer_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def subunternehmer_detail(request, pk): + sub = get_object_or_404(Subunternehmer, pk=pk) + zuordnungen = SubunternehmerZuordnung.objects.filter( + subunternehmer=sub + ).select_related('ausschreibung', 'los').order_by('-ausschreibung__erstellt_am') + + return render(request, 'partner/subunternehmer_detail.html', { + 'sub': sub, + 'zuordnungen': zuordnungen, + 'breadcrumbs': [ + {'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'}, + {'label': sub.name, 'url': None}, + ], + }) + + +def subunternehmer_bearbeiten(request, pk): + sub = get_object_or_404(Subunternehmer, pk=pk) + if request.method == 'POST': + form = SubunternehmerForm(request.POST, instance=sub) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('partner:su_detail', pk=pk) + else: + form = SubunternehmerForm(instance=sub) + + return render(request, 'partner/subunternehmer_form.html', { + 'form': form, + 'sub': sub, + 'breadcrumbs': [ + {'label': 'Subunternehmer', 'url': '/partner/subunternehmer/'}, + {'label': sub.name, 'url': f'/partner/subunternehmer/{pk}/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) + + +def subunternehmer_praeferenz(request, pk): + sub = get_object_or_404(Subunternehmer, pk=pk) + if request.method == 'POST': + sub.praeferenz = request.POST.get('praeferenz', sub.praeferenz) + if sub.praeferenz == 'gesperrt': + begruendung = request.POST.get('begruendung', '').strip() + if begruendung: + sub.bewertung = begruendung + sub.save(update_fields=['praeferenz', 'bewertung']) + return render(request, 'partner/partials/praeferenz_badge.html', {'sub': sub}) + + +def subunternehmer_suche_modal(request, ausschreibung_id, los_pk): + q = request.GET.get('q', '').strip() + subunternehmer = Subunternehmer.objects.filter(name__icontains=q).select_related('dienstleistertyp') + return render(request, 'partner/partials/subunternehmer_suche.html', { + 'subunternehmer': subunternehmer, + 'los_pk': los_pk, + 'ausschreibung_id': ausschreibung_id, + 'q': q, + }) + + +def subunternehmer_zuordnen(request, ausschreibung_id, los_pk): + if request.method == 'POST': + sub_id = request.POST.get('subunternehmer_id') + sub = get_object_or_404(Subunternehmer, pk=sub_id) + los = get_object_or_404(Los, pk=los_pk) + zuordnung, _ = SubunternehmerZuordnung.objects.get_or_create( + subunternehmer=sub, + ausschreibung_id=ausschreibung_id, + los=los, + defaults={'konkrete_leistung': request.POST.get('konkrete_leistung', '')}, + ) + return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung}) + return redirect('partner:su_liste') + + +def zuordnung_toggle(request, pk): + zuordnung = get_object_or_404(SubunternehmerZuordnung, pk=pk) + feld = request.POST.get('feld') + if feld in ('zusage_vorhanden', 'nachweis_eingegangen', 'preis_vorhanden'): + setattr(zuordnung, feld, not getattr(zuordnung, feld)) + zuordnung.save(update_fields=[feld]) + return render(request, 'partner/partials/zuordnung_zeile.html', {'zuordnung': zuordnung}) + + +def dienstleistertypen_liste(request): + typen = Dienstleistertyp.objects.all() + return render(request, 'partner/dienstleistertyp_liste.html', { + 'typen': typen, + 'breadcrumbs': [{'label': 'Dienstleistertypen', 'url': None}], + }) + + +def dienstleistertyp_neu(request): + if request.method == 'POST': + form = DienstleistertypForm(request.POST) + if form.is_valid(): + obj = form.save() + messages.success(request, f'Dienstleistertyp „{obj.name}" angelegt.') + return redirect('partner:dt_liste') + else: + form = DienstleistertypForm() + + return render(request, 'partner/dienstleistertyp_form.html', { + 'form': form, + 'breadcrumbs': [ + {'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'}, + {'label': 'Neu', 'url': None}, + ], + }) + + +def dienstleistertyp_bearbeiten(request, pk): + obj = get_object_or_404(Dienstleistertyp, pk=pk) + if request.method == 'POST': + form = DienstleistertypForm(request.POST, instance=obj) + if form.is_valid(): + form.save() + messages.success(request, 'Gespeichert.') + return redirect('partner:dt_liste') + else: + form = DienstleistertypForm(instance=obj) + + return render(request, 'partner/dienstleistertyp_form.html', { + 'form': form, + 'obj': obj, + 'breadcrumbs': [ + {'label': 'Dienstleistertypen', 'url': '/partner/dienstleistertypen/'}, + {'label': 'Bearbeiten', 'url': None}, + ], + }) diff --git a/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_form.html b/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_form.html new file mode 100644 index 0000000..9a6baf5 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_form.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% block title %}{% if obj %}Bearbeiten{% else %}Neue Entscheidungsregel{% endif %}{% endblock %} +{% block content %} +
+

{% if obj %}{{ obj.regelname }} bearbeiten{% else %}Neue Entscheidungsregel{% endif %}

+ +
+ {% csrf_token %} +
+
{{ form.regelname }}
+
{{ form.beschreibung }}
+
+
{{ form.kategorie }}
+
{{ form.gewichtung }}
+
+
{{ form.bewertungslogik }}
+
{{ form.schwellenwert }}
+
+ +
+

Empfehlung

+
{{ form.empfehlung }}
+
{{ form.begruendung }}
+
+ +
+

Gültigkeit

+
+
{{ form.gueltig_von }}
+
{{ form.gueltig_bis }}
+
{{ form.verantwortlicher }}
+
+ +
+ +
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_liste.html b/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_liste.html new file mode 100644 index 0000000..45847d3 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/entscheidungsregel_liste.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% block title %}Entscheidungsregeln{% endblock %} +{% block content %} +
+

Entscheidungsregeln

+ + Neu +
+ +
+ {% if regeln %} + + + + + + + + + + + + {% for r in regeln %} + + + + + + + + {% endfor %} + +
RegelnameKategorieEmpfehlungAktiv
{{ r.regelname }}{{ r.get_kategorie_display }} + {% if r.empfehlung == 'teilnehmen' %}Teilnehmen + {% elif r.empfehlung == 'nicht_teilnehmen' %}Nicht teilnehmen + {% else %}Prüfen{% endif %} + +
+ {% csrf_token %} + +
+
+ Bearbeiten +
+ {% else %} +

Noch keine Entscheidungsregeln angelegt.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/leistungsblatt_form.html b/vergabe_teilnahme/templates/bibliothek/leistungsblatt_form.html new file mode 100644 index 0000000..783529f --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/leistungsblatt_form.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{% if obj %}Bearbeiten{% else %}Neues Leistungsblatt{% endif %}{% endblock %} +{% block content %} +
+

{% if obj %}{{ obj.produktfunktion }} bearbeiten{% else %}Neues Leistungsblatt{% endif %}

+ +
+ {% csrf_token %} +
+
{{ form.produktfunktion }}
+
{{ form.version }}
+
+
{{ form.beschreibung }}
+
{{ form.leistungsumfang }}
+
{{ form.grenzen_ausschluesse }}
+
{{ form.technische_voraussetzungen }}
+
{{ form.typische_nachweise }}
+
{{ form.eigentuemer }}
+
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/leistungsblatt_liste.html b/vergabe_teilnahme/templates/bibliothek/leistungsblatt_liste.html new file mode 100644 index 0000000..43d425f --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/leistungsblatt_liste.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block title %}Leistungsblätter{% endblock %} +{% block content %} +
+

Leistungsblätter

+ + Neu +
+ +
+ {% if blaetter %} + + + + + + + + + + + {% for b in blaetter %} + + + + + + + {% endfor %} + +
ProduktfunktionVersionEigentümer
{{ b.produktfunktion }}{{ b.version }}{{ b.eigentuemer|default:"—" }} + Bearbeiten +
+ {% else %} +

Noch keine Leistungsblätter angelegt.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/nachweis_detail.html b/vergabe_teilnahme/templates/bibliothek/nachweis_detail.html new file mode 100644 index 0000000..6d1ab4e --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/nachweis_detail.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% block title %}{{ nachweis.titel }}{% endblock %} +{% block content %} +
+
+

{{ nachweis.titel }}

+

Version {{ nachweis.version }} · {{ nachweis.get_freigabestatus_display }}

+
+ +
+ +
+
+ {% if nachweis.kurzbeschreibung %}

{{ nachweis.kurzbeschreibung }}

{% endif %} + {% if nachweis.zugehoerige_standards %} +

Standards: {{ nachweis.zugehoerige_standards }}

+ {% endif %} + {% if nachweis.datei %} +

Datei herunterladen

+ {% endif %} +
+ +
+

Gültig ab: {{ nachweis.gueltig_ab|date:"d.m.Y"|default:"—" }}

+

Gültig bis: + {% if nachweis.gueltig_bis %} + {% if nachweis.ist_abgelaufen %}{{ nachweis.gueltig_bis|date:"d.m.Y" }} (abgelaufen) + {% else %}{{ nachweis.gueltig_bis|date:"d.m.Y" }}{% endif %} + {% else %}—{% endif %} +

+

Vertraulichkeit: {{ nachweis.get_vertraulichkeit_display }}

+

Öffentliche Vergabe: {% if nachweis.fuer_oeffentliche %}Ja{% else %}Nein{% endif %}

+

Privatwirtschaft: {% if nachweis.fuer_privatwirtschaftliche %}Ja{% else %}Nein{% endif %}

+ {% if nachweis.eigentuemer %} +

Eigentümer: {{ nachweis.eigentuemer }}

+ {% endif %} + {% if nachweis.letzte_pruefung %} +

Letzte Prüfung: {{ nachweis.letzte_pruefung|date:"d.m.Y" }}

+ {% endif %} +
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/nachweis_form.html b/vergabe_teilnahme/templates/bibliothek/nachweis_form.html new file mode 100644 index 0000000..ba56adb --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/nachweis_form.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block title %}{% if neue_version %}Neue Version: {{ nachweis.titel }}{% elif nachweis %}Bearbeiten{% else %}Neuer Nachweis{% endif %}{% endblock %} +{% block content %} +
+

+ {% if neue_version %}Neue Version: {{ nachweis.titel }} + {% elif nachweis %}{{ nachweis.titel }} bearbeiten + {% else %}Neuer Nachweis{% endif %} +

+ + {% if neue_version %} +
+ Der alte Nachweis (Version {{ nachweis.version }}) wird als „ersetzt" markiert. +
+ {% endif %} + +
+ {% csrf_token %} +
+
+
{{ form.titel }}
+
+
{{ form.kurzbeschreibung }}
+
+
{{ form.dokumenttyp }}
+
{{ form.kategorie }}
+
{{ form.version }}
+
+
{{ form.datei }}
+
+ +
+

Gültigkeit & Freigabe

+
+
{{ form.gueltig_ab }}
+
{{ form.gueltig_bis }}
+
{{ form.letzte_pruefung }}
+
+
+
{{ form.freigabestatus }}
+
{{ form.vertraulichkeit }}
+
+
+ + +
+
{{ form.eigentuemer }}
+
{{ form.zugehoerige_standards }}
+
{{ form.sprache }}
+
+ +
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/nachweis_liste.html b/vergabe_teilnahme/templates/bibliothek/nachweis_liste.html new file mode 100644 index 0000000..0eda6b7 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/nachweis_liste.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% block title %}Nachweise{% endblock %} +{% block content %} +
+

Nachweise

+ + Neu +
+ + + +
+ {% if nachweise %} + + + + + + + + + + + + {% for n in nachweise %} + + + + + + + + {% endfor %} + +
TitelVersionGültig bisFreigabe
+ {{ n.titel }} + {{ n.version }} + {% if n.gueltig_bis %} + {% if n.ist_abgelaufen %} + {{ n.gueltig_bis|date:"d.m.Y" }} + {% elif n.gueltig_bis <= in_60_tagen %} + {{ n.gueltig_bis|date:"d.m.Y" }} + {% else %} + {{ n.gueltig_bis|date:"d.m.Y" }} + {% endif %} + {% else %} + + {% endif %} + + {{ n.get_freigabestatus_display }} + + Bearbeiten + Neue Version +
+ {% else %} +

Keine Nachweise gefunden.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/partials/er_aktiv_toggle.html b/vergabe_teilnahme/templates/bibliothek/partials/er_aktiv_toggle.html new file mode 100644 index 0000000..70d3944 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/partials/er_aktiv_toggle.html @@ -0,0 +1,8 @@ +
+ {% csrf_token %} + +
diff --git a/vergabe_teilnahme/templates/bibliothek/partials/referenz_suche.html b/vergabe_teilnahme/templates/bibliothek/partials/referenz_suche.html new file mode 100644 index 0000000..b5ae81c --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/partials/referenz_suche.html @@ -0,0 +1,33 @@ +
+
+ +
+ + {% if referenzen %} +
    + {% for ref in referenzen %} +
  • +
    +

    {{ ref.referenztitel|truncatechars:60 }}

    +

    {{ ref.kunde }} · {{ ref.branche|default:"" }}

    + {% if ref.freigabestatus_verwendung == 'eingeschraenkt' %} +

    ⚠ Eingeschränkte Verwendung: {{ ref.einschraenkungen_verwendung|truncatechars:60 }}

    + {% endif %} +
    +
    + {% csrf_token %} + + +
    +
  • + {% endfor %} +
+ {% else %} +

Keine Referenzen gefunden.

+ {% endif %} +
diff --git a/vergabe_teilnahme/templates/bibliothek/referenz_detail.html b/vergabe_teilnahme/templates/bibliothek/referenz_detail.html new file mode 100644 index 0000000..b0258fb --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/referenz_detail.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} +{% block title %}{{ ref.referenztitel }}{% endblock %} +{% block content %} +
+
+

{{ ref.referenztitel }}

+

{{ ref.kunde }}{% if ref.branche %} · {{ ref.branche }}{% endif %}

+
+
+ {% if ref.whitepaper %} + Whitepaper + {% endif %} + Bearbeiten +
+
+ +
+
+ {% if ref.kurzfassung %} +
+

Kurzfassung

+

{{ ref.kurzfassung }}

+
+ {% endif %} + {% if ref.leistungsbeschreibung %} +
+

Leistungsbeschreibung

+

{{ ref.leistungsbeschreibung }}

+
+ {% endif %} + {% if ref.eingesetzte_produkte %} +
+

Eingesetzte Produkte

+

{{ ref.eingesetzte_produkte }}

+
+ {% endif %} + {% if ref.leistungsblaetter.exists %} +
+

Leistungsblätter

+
+ {% for lb in ref.leistungsblaetter.all %} + {{ lb.produktfunktion }} + {% endfor %} +
+
+ {% endif %} +
+ +
+

Projektzeitraum: {{ ref.projektzeitraum|default:"—" }}

+

Art: {{ ref.oeffentlich_oder_privat|default:"—" }}

+ {% if ref.vertragsvolumen %} +

Volumen: {{ ref.vertragsvolumen|floatformat:0 }} €

+ {% endif %} +

Freigabe: {{ ref.get_freigabestatus_verwendung_display }}

+ {% if not ref.verwendbar_fuer_ausschreibungen %} +

Nicht für Ausschreibungen verwendbar

+ {% endif %} + {% if ref.einschraenkungen_verwendung %} +

⚠ {{ ref.einschraenkungen_verwendung }}

+ {% endif %} + {% if ref.ansprechpartner_referenzkunde %} +

Ansprechpartner: {{ ref.ansprechpartner_referenzkunde }}

+ {% endif %} +
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/referenz_form.html b/vergabe_teilnahme/templates/bibliothek/referenz_form.html new file mode 100644 index 0000000..98b8924 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/referenz_form.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} +{% block title %}{% if ref %}{{ ref.referenztitel }} bearbeiten{% else %}Neue Referenz{% endif %}{% endblock %} +{% block content %} +
+

{% if ref %}Bearbeiten{% else %}Neue Referenz{% endif %}

+ +
+ {% csrf_token %} +
+
+
{{ form.referenztitel }}
+
{{ form.kunde }}
+
{{ form.branche }}
+
+
+
{{ form.oeffentlich_oder_privat }}
+
{{ form.projektzeitraum }}
+
{{ form.vertragsvolumen }}
+
+
{{ form.leistungsbeschreibung }}
+
{{ form.eingesetzte_produkte }}
+
{{ form.ansprechpartner_referenzkunde }}
+
+ +
+

Freigabe & Verwendung

+
+
{{ form.freigabestatus_verwendung }}
+
{{ form.vertraulichkeit }}
+
+ +
{{ form.einschraenkungen_verwendung }}
+
+ +
+

Inhalte & Dokumente

+
{{ form.kurzfassung }}
+
{{ form.langfassung }}
+
{{ form.whitepaper }}
+
+ +
{{ form.leistungsblaetter }}
+
+
+ +
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/bibliothek/referenz_liste.html b/vergabe_teilnahme/templates/bibliothek/referenz_liste.html new file mode 100644 index 0000000..60ef501 --- /dev/null +++ b/vergabe_teilnahme/templates/bibliothek/referenz_liste.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% block title %}Referenzen{% endblock %} +{% block content %} +
+

Referenzen

+ + Neu +
+ +
+
+
+ + +
+ +
+
+ +
+ {% if referenzen %} + + + + + + + + + + + + {% for ref in referenzen %} + + + + + + + + {% endfor %} + +
ReferenzKundeBrancheFreigabe
+ {{ ref.referenztitel|truncatechars:60 }} + {{ ref.kunde }}{{ ref.branche|default:"—" }} + {% if ref.freigabestatus_verwendung == 'freigegeben' %} + Freigegeben + {% elif ref.freigabestatus_verwendung == 'eingeschraenkt' %} + Eingeschränkt + {% else %} + Nicht freigegeben + {% endif %} + + Bearbeiten +
+ {% else %} +

Keine Referenzen gefunden.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/partner/dienstleistertyp_form.html b/vergabe_teilnahme/templates/partner/dienstleistertyp_form.html new file mode 100644 index 0000000..edde0fb --- /dev/null +++ b/vergabe_teilnahme/templates/partner/dienstleistertyp_form.html @@ -0,0 +1,22 @@ +{% extends "base.html" %} +{% block title %}{% if obj %}{{ obj.name }} bearbeiten{% else %}Neuer Dienstleistertyp{% endif %}{% endblock %} +{% block content %} +
+

{% if obj %}Bearbeiten: {{ obj.name }}{% else %}Neuer Dienstleistertyp{% endif %}

+ +
+ {% csrf_token %} +
{{ form.name }}
+
{{ form.beschreibung }}
+
{{ form.typische_leistungen }}
+
{{ form.typische_nachweise }}
+
{{ form.relevante_standards }}
+
{{ form.typische_preisbestandteile }}
+
{{ form.bemerkungen }}
+
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/partner/dienstleistertyp_liste.html b/vergabe_teilnahme/templates/partner/dienstleistertyp_liste.html new file mode 100644 index 0000000..cd16c1d --- /dev/null +++ b/vergabe_teilnahme/templates/partner/dienstleistertyp_liste.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% block title %}Dienstleistertypen{% endblock %} +{% block content %} +
+

Dienstleistertypen

+ + Neu +
+ +
+ {% if typen %} + + + + + + + + + + + {% for typ in typen %} + + + + + + + {% endfor %} + +
NameBeschreibungSubunternehmer
{{ typ.name }}{{ typ.beschreibung|truncatechars:60|default:"—" }}{{ typ.subunternehmer.count }} + Bearbeiten +
+ {% else %} +

Noch keine Dienstleistertypen angelegt.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/partner/partials/praeferenz_badge.html b/vergabe_teilnahme/templates/partner/partials/praeferenz_badge.html new file mode 100644 index 0000000..463768e --- /dev/null +++ b/vergabe_teilnahme/templates/partner/partials/praeferenz_badge.html @@ -0,0 +1,7 @@ +{% if sub.praeferenz == 'bevorzugt' %} +Bevorzugt +{% elif sub.praeferenz == 'gesperrt' %} +Gesperrt +{% else %} +Zugelassen +{% endif %} diff --git a/vergabe_teilnahme/templates/partner/partials/subunternehmer_suche.html b/vergabe_teilnahme/templates/partner/partials/subunternehmer_suche.html new file mode 100644 index 0000000..4081184 --- /dev/null +++ b/vergabe_teilnahme/templates/partner/partials/subunternehmer_suche.html @@ -0,0 +1,39 @@ +
+
+ +
+ + {% if subunternehmer %} +
    + {% for sub in subunternehmer %} +
  • +
    + {{ sub.name }} + {{ sub.dienstleistertyp|default:"" }} + {% if sub.praeferenz == 'gesperrt' %} + ⚠ Gesperrt + {% elif sub.praeferenz == 'bevorzugt' %} + Bevorzugt + {% endif %} +
    +
    + {% csrf_token %} + + +
    +
  • + {% endfor %} +
+ {% else %} +

Kein Subunternehmer gefunden.

+ {% endif %} +
diff --git a/vergabe_teilnahme/templates/partner/partials/zuordnung_zeile.html b/vergabe_teilnahme/templates/partner/partials/zuordnung_zeile.html new file mode 100644 index 0000000..9e8cf23 --- /dev/null +++ b/vergabe_teilnahme/templates/partner/partials/zuordnung_zeile.html @@ -0,0 +1,38 @@ + + {{ zuordnung.subunternehmer.name }} + {{ zuordnung.subunternehmer.dienstleistertyp|default:"—" }} + {{ zuordnung.konkrete_leistung|default:"—" }} + +
+ {% csrf_token %} + + +
+ + +
+ {% csrf_token %} + + +
+ + +
+ {% csrf_token %} + + +
+ + diff --git a/vergabe_teilnahme/templates/partner/subunternehmer_detail.html b/vergabe_teilnahme/templates/partner/subunternehmer_detail.html new file mode 100644 index 0000000..3bfbae1 --- /dev/null +++ b/vergabe_teilnahme/templates/partner/subunternehmer_detail.html @@ -0,0 +1,102 @@ +{% extends "base.html" %} +{% block title %}{{ sub.name }}{% endblock %} +{% block content %} +
+
+

{{ sub.name }}

+

{{ sub.dienstleistertyp|default:"Kein Dienstleistertyp" }}

+
+
+
+ {% if sub.praeferenz == 'bevorzugt' %} + Bevorzugt + {% elif sub.praeferenz == 'gesperrt' %} + Gesperrt + {% else %} + Zugelassen + {% endif %} +
+ Bearbeiten +
+
+ +
+
+

Kontakt

+ {% if sub.strasse or sub.ort %} +

{{ sub.strasse }}{% if sub.strasse and sub.ort %}, {% endif %}{{ sub.plz }} {{ sub.ort }}, {{ sub.land }}

+ {% endif %} + {% if sub.telefon %}

Tel: {{ sub.telefon }}

{% endif %} + {% if sub.email %}

E-Mail: {{ sub.email }}

{% endif %} + {% if sub.website %}

{{ sub.website }}

{% endif %} + {% if sub.ansprechpartner %} +

Ansprechpartner: {{ sub.ansprechpartner }} + {% if sub.ansprechpartner_email %} — {{ sub.ansprechpartner_email }}{% endif %} +

+ {% endif %} +
+ +
+

Präferenz ändern

+
+ {% csrf_token %} + {% for val, label in sub.PRAEFERENZ_CHOICES %} + + {% endfor %} +
+ + +
+ +
+
+
+ +{% if sub.leistungsprofil %} +
+

Leistungsprofil

+

{{ sub.leistungsprofil }}

+
+{% endif %} + +
+

Ausschreibungs-Zuordnungen

+ {% if zuordnungen %} + + + + + + + + + + + + + {% for z in zuordnungen %} + + + + + + + + + {% endfor %} + +
AusschreibungLosLeistungZusageNachweisPreis
{{ z.ausschreibung.titel|truncatechars:50 }}{{ z.los|default:"—" }}{{ z.konkrete_leistung|default:"—" }}{% if z.zusage_vorhanden %}{% else %}{% endif %}{% if z.nachweis_eingegangen %}{% else %}{% endif %}{% if z.preis_vorhanden %}{% else %}{% endif %}
+ {% else %} +

Noch keiner Ausschreibung zugeordnet.

+ {% endif %} +
+{% endblock %} diff --git a/vergabe_teilnahme/templates/partner/subunternehmer_form.html b/vergabe_teilnahme/templates/partner/subunternehmer_form.html new file mode 100644 index 0000000..a193eea --- /dev/null +++ b/vergabe_teilnahme/templates/partner/subunternehmer_form.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block title %}{% if sub %}{{ sub.name }} bearbeiten{% else %}Neuer Subunternehmer{% endif %}{% endblock %} +{% block content %} +
+

{% if sub %}{{ sub.name }} bearbeiten{% else %}Neuer Subunternehmer{% endif %}

+ +
+ {% csrf_token %} + +
+

Stammdaten

+
+
{{ form.name }}
+
{{ form.kurzname }}
+
+
+
{{ form.dienstleistertyp }}
+
+
+ +
{{ form.praeferenz }}
+
+
+ +
+

Kontakt

+
+
{{ form.strasse }}
+
{{ form.plz }}
+
+
+
{{ form.ort }}
+
{{ form.land }}
+
+
+
{{ form.telefon }}
+
{{ form.mobilnummer }}
+
{{ form.email }}
+
+
{{ form.website }}
+
+ +
+

Ansprechpartner

+
+
{{ form.ansprechpartner }}
+
{{ form.ansprechpartner_email }}
+
{{ form.ansprechpartner_telefon }}
+
+
+ +
+

Profil & Bewertung

+
{{ form.leistungsprofil }}
+
{{ form.bewertung }}
+
+ +
+ + Abbrechen +
+
+
+{% endblock %} diff --git a/vergabe_teilnahme/templates/partner/subunternehmer_liste.html b/vergabe_teilnahme/templates/partner/subunternehmer_liste.html new file mode 100644 index 0000000..ca060e9 --- /dev/null +++ b/vergabe_teilnahme/templates/partner/subunternehmer_liste.html @@ -0,0 +1,82 @@ +{% extends "base.html" %} +{% block title %}Subunternehmer{% endblock %} +{% block content %} +
+

Subunternehmer

+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+ {% if subunternehmer %} + + + + + + + + + + + + {% for sub in subunternehmer %} + + + + + + + + {% endfor %} + +
NameDienstleistertypPräferenzOrt
+ + {{ sub.name }} + + {{ sub.dienstleistertyp|default:"—" }} + {% if sub.praeferenz == 'bevorzugt' %} + Bevorzugt + {% elif sub.praeferenz == 'gesperrt' %} + Gesperrt + {% else %} + Zugelassen + {% endif %} + {{ sub.ort|default:"—" }} + Bearbeiten +
+ {% else %} +

Keine Subunternehmer gefunden.

+ {% endif %} +
+{% endblock %} diff --git a/workplans/WP-0010-partner-bibliothek.md b/workplans/WP-0010-partner-bibliothek.md index d561654..a139891 100644 --- a/workplans/WP-0010-partner-bibliothek.md +++ b/workplans/WP-0010-partner-bibliothek.md @@ -1,7 +1,7 @@ --- id: WP-0010 title: Subunternehmer, Partner und Bibliothek -status: todo +status: done phase: 10-of-12 created: "2026-05-08" depends_on: WP-0009 @@ -17,7 +17,7 @@ Entscheidungsregel-Verwaltung. Referenz: UC-SU-01 bis UC-SU-04, UC-BIB-01 bis UC ```task id: WP-0010-T01 title: Subunternehmer-Katalog: Liste, Suche, Anlegen (UC-SU-01, UC-SU-03) -status: todo +status: done `partner/views.py` — subunternehmer_liste, subunternehmer_neu: @@ -40,7 +40,7 @@ CustomAttribute-Panel. ```task id: WP-0010-T02 title: Subunternehmer einer Ausschreibung/Los zuordnen (UC-SU-02) -status: todo +status: done `partner/views.py` — subunternehmer_zuordnen: @@ -78,7 +78,7 @@ drei Checkboxen (Zusage, Nachweis, Preis) — HTMX-togglebar. ```task id: WP-0010-T03 title: Dienstleistertyp-Katalog und Subunternehmer als gesperrt markieren (UC-SU-04) -status: todo +status: done `partner/views.py` — dienstleistertypen_liste, dienstleistertyp_neu/_bearbeiten: Einfache CRUD-Views für Dienstleistertypen (Katalog-Daten). @@ -116,7 +116,7 @@ urlpatterns = [ ```task id: WP-0010-T04 title: Bibliothek: Nachweis-Katalog mit Ablaufwarnung (UC-BIB-01, UC-BIB-02) -status: todo +status: done `bibliothek/views.py` — nachweise_liste, nachweis_neu/_bearbeiten: @@ -148,7 +148,7 @@ path('nachweise//version/', views.nachweis_neue_version, name='nachweis_ ```task id: WP-0010-T05 title: Bibliothek: Referenz anlegen und zuordnen (UC-BIB-03, UC-BIB-04) -status: todo +status: done `bibliothek/views.py` — referenzen_liste, referenz_neu/_bearbeiten: @@ -170,7 +170,7 @@ Zeigt Freigabestatus und Nutzungseinschränkungen als Warnung. ```task id: WP-0010-T06 title: Bibliothek: Leistungsblatt und Entscheidungsregel (UC-BIB-05) -status: todo +status: done `bibliothek/views.py` — leistungsblaetter_liste, leistungsblatt_neu/_bearbeiten: Einfache CRUD-Views. `LeistungsblattForm(ModelForm)` mit allen Textfeldern. @@ -192,7 +192,7 @@ Auf der Entscheidungsseite (Phase 2) werden nur `aktiv=True` Regeln angezeigt. ```task id: WP-0010-T07 title: Bibliothek URL-Verkabelung und Tests -status: todo +status: done `bibliothek/urls.py` vollständig: ```python