generated from coulomb/repo-seed
Implementiert Subunternehmer-Katalog mit Suche/Filter, Zuordnung zu Losen via HTMX-Modal, Dienstleistertyp-CRUD und Präferenz-Badges. Bibliothek: Nachweis-Katalog mit Ablaufwarnung und Versionierung, Referenz-Katalog mit Ausschreibungszuordnung, Leistungsblatt-CRUD, Entscheidungsregel-CRUD mit Aktiv-Toggle. Migration für referenzen M2M auf Ausschreibung. 56 Tests grün. Tests-Discovery auf tests.py-Dateien ausgedehnt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
451 lines
18 KiB
Python
451 lines
18 KiB
Python
from datetime import date, timedelta
|
|
|
|
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})
|