generated from coulomb/repo-seed
Workplan Preise has been implemented
This commit is contained in:
59
vergabe_teilnahme/apps/preise/forms.py
Normal file
59
vergabe_teilnahme/apps/preise/forms.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from decimal import Decimal
|
||||
|
||||
from django import forms
|
||||
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
from vergabe_teilnahme.apps.partner.models import Subunternehmer
|
||||
|
||||
from .models import Preispunkt
|
||||
|
||||
NUMBER_ATTRS = {'class': 'form-input', 'step': '0.01'}
|
||||
WEIGHT_ATTRS = {'class': 'form-input', 'step': '0.1', 'min': '0.0', 'max': '2.0', 'type': 'number'}
|
||||
|
||||
|
||||
class PreispunktForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Preispunkt
|
||||
fields = [
|
||||
'leistungstyp', 'konkrete_leistung', 'mengeneinheit', 'menge',
|
||||
'einzelpreis', 'gesamtpreis', 'waehrung', 'preisstand',
|
||||
'wiederkehrend', 'laufzeitbezug',
|
||||
'subunternehmeranteil', 'subunternehmer',
|
||||
'vergleichsgewicht', 'gewichtungsbegruendung', 'kommentar', 'los',
|
||||
]
|
||||
widgets = {
|
||||
'leistungstyp': forms.TextInput(attrs={'class': 'form-input', 'list': 'leistungstypen-list'}),
|
||||
'konkrete_leistung': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'mengeneinheit': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'menge': forms.NumberInput(attrs={**NUMBER_ATTRS, 'id': 'id_menge'}),
|
||||
'einzelpreis': forms.NumberInput(attrs={**NUMBER_ATTRS, 'id': 'id_einzelpreis'}),
|
||||
'gesamtpreis': forms.NumberInput(attrs={**NUMBER_ATTRS, 'id': 'id_gesamtpreis'}),
|
||||
'waehrung': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'preisstand': forms.DateInput(attrs={'class': 'form-input', 'type': 'date'}),
|
||||
'laufzeitbezug': forms.TextInput(attrs={'class': 'form-input'}),
|
||||
'vergleichsgewicht': forms.NumberInput(attrs=WEIGHT_ATTRS),
|
||||
'gewichtungsbegruendung': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'kommentar': forms.Textarea(attrs={'class': 'form-input', 'rows': 2}),
|
||||
'subunternehmer': forms.Select(attrs={'class': 'form-select'}),
|
||||
'los': forms.Select(attrs={'class': 'form-select'}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, ausschreibung=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['vergleichsgewicht'].initial = Decimal('1.0')
|
||||
for field in ['menge', 'gesamtpreis', 'preisstand', 'laufzeitbezug',
|
||||
'subunternehmeranteil', 'subunternehmer',
|
||||
'gewichtungsbegruendung', 'kommentar', 'los']:
|
||||
self.fields[field].required = False
|
||||
if ausschreibung is not None:
|
||||
self.fields['los'].queryset = Los.objects.filter(ausschreibung=ausschreibung)
|
||||
else:
|
||||
self.fields['los'].queryset = Los.objects.none()
|
||||
self.fields['subunternehmer'].queryset = Subunternehmer.objects.all()
|
||||
|
||||
def clean_vergleichsgewicht(self):
|
||||
wert = self.cleaned_data.get('vergleichsgewicht')
|
||||
if wert is not None:
|
||||
if wert < Decimal('0.0') or wert > Decimal('2.0'):
|
||||
raise forms.ValidationError('Vergleichsgewicht muss zwischen 0,0 und 2,0 liegen.')
|
||||
return wert
|
||||
@@ -1,3 +1,80 @@
|
||||
from django.test import TestCase
|
||||
from decimal import Decimal
|
||||
|
||||
# Create your tests here.
|
||||
import factory
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.tests import AusschreibungFactory
|
||||
from vergabe_teilnahme.apps.core.services import gewichteter_durchschnitt
|
||||
|
||||
from .models import Preispunkt
|
||||
|
||||
|
||||
class PreispunktFactory(factory.django.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Preispunkt
|
||||
|
||||
ausschreibung = factory.SubFactory(AusschreibungFactory)
|
||||
leistungstyp = 'IT-Dienstleistung'
|
||||
konkrete_leistung = factory.Sequence(lambda n: f'Leistung {n}')
|
||||
mengeneinheit = 'Stück'
|
||||
einzelpreis = Decimal('100.00')
|
||||
vergleichsgewicht = Decimal('1.0')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_vergleichsgewicht_null_gespeichert(client):
|
||||
a = AusschreibungFactory()
|
||||
url = reverse('ausschreibungen:preise:neu', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.post(url, {
|
||||
'leistungstyp': 'Test',
|
||||
'konkrete_leistung': 'Leistung mit Gewicht 0',
|
||||
'mengeneinheit': 'Stück',
|
||||
'waehrung': 'EUR',
|
||||
'vergleichsgewicht': '0.0',
|
||||
})
|
||||
assert response.status_code == 302
|
||||
pp = Preispunkt.objects.get(ausschreibung=a)
|
||||
assert pp.vergleichsgewicht == Decimal('0.0')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_vergleichsgewicht_zu_gross_validation_error(client):
|
||||
a = AusschreibungFactory()
|
||||
url = reverse('ausschreibungen:preise:neu', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.post(url, {
|
||||
'leistungstyp': 'Test',
|
||||
'konkrete_leistung': 'Leistung',
|
||||
'mengeneinheit': 'Stück',
|
||||
'waehrung': 'EUR',
|
||||
'vergleichsgewicht': '2.5',
|
||||
})
|
||||
assert response.status_code == 200
|
||||
assert not Preispunkt.objects.filter(ausschreibung=a).exists()
|
||||
|
||||
|
||||
def test_gewichteter_durchschnitt_berechnung():
|
||||
class FakePP:
|
||||
def __init__(self, einzelpreis, gewicht):
|
||||
self.einzelpreis = Decimal(str(einzelpreis))
|
||||
self.vergleichsgewicht = Decimal(str(gewicht))
|
||||
|
||||
punkte = [
|
||||
FakePP(100, 1.0),
|
||||
FakePP(110, 2.0),
|
||||
FakePP(90, 0.0), # ausgeschlossen
|
||||
]
|
||||
ergebnis = gewichteter_durchschnitt(punkte)
|
||||
assert ergebnis is not None
|
||||
expected = (Decimal('100') * 1 + Decimal('110') * 2) / (1 + 2)
|
||||
assert round(ergebnis['wert'], 2) == round(expected, 2)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auswertung_view_200(client):
|
||||
a = AusschreibungFactory()
|
||||
PreispunktFactory(ausschreibung=a, einzelpreis=Decimal('150.00'))
|
||||
url = reverse('ausschreibungen:preise:auswertung', kwargs={'ausschreibung_id': a.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 200
|
||||
assert 'ergebnis' in response.context
|
||||
|
||||
@@ -1,2 +1,13 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'preise'
|
||||
urlpatterns = [
|
||||
path('', views.preispunkte_liste, name='liste'),
|
||||
path('neu/', views.preispunkt_neu, name='neu'),
|
||||
path('<int:pk>/', views.preispunkt_detail, name='detail'),
|
||||
path('<int:pk>/bearbeiten/', views.preispunkt_bearbeiten, name='bearbeiten'),
|
||||
path('<int:pk>/loeschen/', views.preispunkt_loeschen, name='loeschen'),
|
||||
path('auswertung/', views.leistungstyp_auswertung, name='auswertung'),
|
||||
]
|
||||
|
||||
@@ -1,3 +1,192 @@
|
||||
from django.shortcuts import render
|
||||
from decimal import Decimal
|
||||
|
||||
# Create your views here.
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
|
||||
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
|
||||
from vergabe_teilnahme.apps.core.models import Freigabe
|
||||
from vergabe_teilnahme.apps.core.services import gewichteter_durchschnitt
|
||||
|
||||
from .forms import PreispunktForm
|
||||
from .models import Preispunkt
|
||||
|
||||
|
||||
def preispunkte_liste(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
qs = Preispunkt.objects.filter(ausschreibung=ausschreibung).select_related('los', 'subunternehmer')
|
||||
|
||||
leistungstyp_filter = request.GET.get('leistungstyp', '').strip()
|
||||
los_filter = request.GET.get('los', '').strip()
|
||||
sub_filter = request.GET.get('subunternehmer', '')
|
||||
|
||||
if leistungstyp_filter:
|
||||
qs = qs.filter(leistungstyp__icontains=leistungstyp_filter)
|
||||
if los_filter:
|
||||
qs = qs.filter(los_id=los_filter)
|
||||
if sub_filter == 'ja':
|
||||
qs = qs.filter(subunternehmeranteil=True)
|
||||
elif sub_filter == 'nein':
|
||||
qs = qs.filter(subunternehmeranteil=False)
|
||||
|
||||
summe_gesamt = sum(p.gesamtpreis for p in qs if p.gesamtpreis is not None)
|
||||
|
||||
from vergabe_teilnahme.apps.lose.models import Los
|
||||
lose = Los.objects.filter(ausschreibung=ausschreibung)
|
||||
leistungstypen = Preispunkt.objects.filter(ausschreibung=ausschreibung).values_list(
|
||||
'leistungstyp', flat=True
|
||||
).distinct()
|
||||
|
||||
ctx = {
|
||||
'ausschreibung': ausschreibung,
|
||||
'preispunkte': qs,
|
||||
'summe_gesamt': summe_gesamt,
|
||||
'lose': lose,
|
||||
'leistungstypen': leistungstypen,
|
||||
'filter': {
|
||||
'leistungstyp': leistungstyp_filter,
|
||||
'los': los_filter,
|
||||
'subunternehmer': sub_filter,
|
||||
},
|
||||
}
|
||||
return render(request, 'preise/liste.html', ctx)
|
||||
|
||||
|
||||
def preispunkt_neu(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
form = PreispunktForm(request.POST or None, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
pp = form.save(commit=False)
|
||||
pp.ausschreibung = ausschreibung
|
||||
pp.save()
|
||||
return redirect('ausschreibungen:preise:liste', ausschreibung_id=ausschreibung_id)
|
||||
leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct()
|
||||
return render(request, 'preise/form.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'form': form,
|
||||
'leistungstypen': leistungstypen,
|
||||
'titel': 'Neuer Preispunkt',
|
||||
})
|
||||
|
||||
|
||||
def preispunkt_detail(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
pp = get_object_or_404(Preispunkt, pk=pk, ausschreibung=ausschreibung)
|
||||
ct = ContentType.objects.get_for_model(Preispunkt)
|
||||
freigaben = Freigabe.objects.filter(content_type=ct, object_id=pp.pk)
|
||||
return render(request, 'preise/detail.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'preispunkt': pp,
|
||||
'freigaben': freigaben,
|
||||
})
|
||||
|
||||
|
||||
def preispunkt_bearbeiten(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
pp = get_object_or_404(Preispunkt, pk=pk, ausschreibung=ausschreibung)
|
||||
form = PreispunktForm(request.POST or None, instance=pp, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
return redirect('ausschreibungen:preise:detail', ausschreibung_id=ausschreibung_id, pk=pk)
|
||||
leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct()
|
||||
return render(request, 'preise/form.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'form': form,
|
||||
'preispunkt': pp,
|
||||
'leistungstypen': leistungstypen,
|
||||
'titel': 'Preispunkt bearbeiten',
|
||||
})
|
||||
|
||||
|
||||
def preispunkt_loeschen(request, ausschreibung_id, pk):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
pp = get_object_or_404(Preispunkt, pk=pk, ausschreibung=ausschreibung)
|
||||
if request.method == 'POST':
|
||||
pp.delete()
|
||||
return redirect('ausschreibungen:preise:liste', ausschreibung_id=ausschreibung_id)
|
||||
return render(request, 'preise/loeschen_bestaetigen.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'preispunkt': pp,
|
||||
})
|
||||
|
||||
|
||||
def leistungstyp_auswertung(request, ausschreibung_id):
|
||||
ausschreibung = get_object_or_404(Ausschreibung, pk=ausschreibung_id)
|
||||
|
||||
leistungstyp = request.GET.get('leistungstyp', '').strip()
|
||||
filter_gewonnen = request.GET.get('gewonnen')
|
||||
|
||||
qs = Preispunkt.objects.filter(einzelpreis__isnull=False)
|
||||
if leistungstyp:
|
||||
qs = qs.filter(leistungstyp__icontains=leistungstyp)
|
||||
if filter_gewonnen == 'ja':
|
||||
qs = qs.filter(ausschreibung_gewonnen=True)
|
||||
elif filter_gewonnen == 'nein':
|
||||
qs = qs.filter(ausschreibung_gewonnen=False)
|
||||
|
||||
ergebnis = gewichteter_durchschnitt(list(qs))
|
||||
ungewichtet = None
|
||||
if qs.exists():
|
||||
werte = [p.einzelpreis for p in qs if p.einzelpreis is not None]
|
||||
ungewichtet = sum(werte) / len(werte) if werte else None
|
||||
|
||||
alle_leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct()
|
||||
|
||||
return render(request, 'preise/auswertung.html', {
|
||||
'ausschreibung': ausschreibung,
|
||||
'leistungstyp': leistungstyp,
|
||||
'filter_gewonnen': filter_gewonnen,
|
||||
'ergebnis': ergebnis,
|
||||
'ungewichtet': ungewichtet,
|
||||
'preispunkte': qs.order_by('-ausschreibung__erstellt_am'),
|
||||
'alle_leistungstypen': alle_leistungstypen,
|
||||
})
|
||||
|
||||
|
||||
def globaler_preisvergleich(request):
|
||||
leistungstyp = request.GET.get('leistungstyp', '').strip()
|
||||
filter_gewonnen = request.GET.get('gewonnen', '')
|
||||
von = request.GET.get('von', '')
|
||||
bis = request.GET.get('bis', '')
|
||||
|
||||
qs = Preispunkt.objects.filter(einzelpreis__isnull=False).select_related('ausschreibung')
|
||||
|
||||
if leistungstyp:
|
||||
qs = qs.filter(leistungstyp__icontains=leistungstyp)
|
||||
if filter_gewonnen == 'ja':
|
||||
qs = qs.filter(ausschreibung_gewonnen=True)
|
||||
elif filter_gewonnen == 'nein':
|
||||
qs = qs.filter(ausschreibung_gewonnen=False)
|
||||
if von:
|
||||
qs = qs.filter(preisstand__gte=von)
|
||||
if bis:
|
||||
qs = qs.filter(preisstand__lte=bis)
|
||||
|
||||
ergebnis = gewichteter_durchschnitt(list(qs))
|
||||
ungewichtet = None
|
||||
if qs.exists():
|
||||
werte = [p.einzelpreis for p in qs if p.einzelpreis is not None]
|
||||
ungewichtet = sum(werte) / len(werte) if werte else None
|
||||
|
||||
gewonnen_punkte = list(qs.filter(ausschreibung_gewonnen=True))
|
||||
verloren_punkte = list(qs.filter(ausschreibung_gewonnen=False))
|
||||
ergebnis_gewonnen = gewichteter_durchschnitt(gewonnen_punkte) if gewonnen_punkte else None
|
||||
ergebnis_verloren = gewichteter_durchschnitt(verloren_punkte) if verloren_punkte else None
|
||||
|
||||
alle_leistungstypen = Preispunkt.objects.values_list('leistungstyp', flat=True).distinct()
|
||||
|
||||
return render(request, 'preise/globaler_vergleich.html', {
|
||||
'leistungstyp': leistungstyp,
|
||||
'filter_gewonnen': filter_gewonnen,
|
||||
'von': von,
|
||||
'bis': bis,
|
||||
'ergebnis': ergebnis,
|
||||
'ungewichtet': ungewichtet,
|
||||
'ergebnis_gewonnen': ergebnis_gewonnen,
|
||||
'ergebnis_verloren': ergebnis_verloren,
|
||||
'preispunkte': qs.order_by('-ausschreibung__erstellt_am'),
|
||||
'alle_leistungstypen': alle_leistungstypen,
|
||||
})
|
||||
|
||||
|
||||
def preisfreigabe(request, ausschreibung_id):
|
||||
return redirect('ausschreibungen:preise:liste', ausschreibung_id=ausschreibung_id)
|
||||
|
||||
88
vergabe_teilnahme/templates/preise/auswertung.html
Normal file
88
vergabe_teilnahme/templates/preise/auswertung.html
Normal file
@@ -0,0 +1,88 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Preisauswertung — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Preisauswertung</h1>
|
||||
<a href="{% url 'ausschreibungen:preise:liste' ausschreibung.pk %}" class="btn-ghost text-xs">← Preise</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="card mb-4 flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="form-label">Leistungstyp</label>
|
||||
<input type="text" name="leistungstyp" value="{{ leistungstyp }}" list="lt-list" class="form-input">
|
||||
<datalist id="lt-list">{% for lt in alle_leistungstypen %}<option value="{{ lt }}">{% endfor %}</datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Gewonnen</label>
|
||||
<select name="gewonnen" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="ja" {% if filter_gewonnen == 'ja' %}selected{% endif %}>Ja</option>
|
||||
<option value="nein" {% if filter_gewonnen == 'nein' %}selected{% endif %}>Nein</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary text-xs">Auswerten</button>
|
||||
</form>
|
||||
|
||||
{% if ergebnis %}
|
||||
<div class="grid grid-cols-3 gap-3 mb-6">
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Gewichteter Durchschnitt</p>
|
||||
<p class="text-2xl font-bold text-blue-700">{{ ergebnis.wert|floatformat:2 }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Ungewichteter Durchschnitt</p>
|
||||
<p class="text-2xl font-bold text-slate-700">{% if ungewichtet %}{{ ungewichtet|floatformat:2 }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Messpunkte / Summe Gewichte</p>
|
||||
<p class="text-2xl font-bold text-slate-700">{{ ergebnis.anzahl }} / {{ ergebnis.summe_gewichte }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Minimum</p>
|
||||
<p class="text-xl font-semibold text-green-700">{{ ergebnis.minimum|floatformat:2 }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Maximum</p>
|
||||
<p class="text-xl font-semibold text-red-700">{{ ergebnis.maximum|floatformat:2 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% elif leistungstyp %}
|
||||
<div class="card text-center py-6 text-sm text-slate-500 mb-6">Keine Daten für "{{ leistungstyp }}".</div>
|
||||
{% else %}
|
||||
<div class="card text-center py-6 text-sm text-slate-500 mb-6">Leistungstyp eingeben, um Auswertung zu starten.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if preispunkte %}
|
||||
<div class="card overflow-hidden">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Einzelmesspunkte</p>
|
||||
<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">Ausschreibung</th>
|
||||
<th class="pb-2 pr-3">Leistung</th>
|
||||
<th class="pb-2 pr-3 text-right">Einzelpreis</th>
|
||||
<th class="pb-2 pr-3 text-center">Gewicht</th>
|
||||
<th class="pb-2 text-center">Gewonnen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for pp in preispunkte %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-2 pr-3 text-xs text-slate-600">{{ pp.ausschreibung.titel|truncatechars:30 }}</td>
|
||||
<td class="py-2 pr-3 text-xs">{{ pp.konkrete_leistung }}</td>
|
||||
<td class="py-2 pr-3 text-right text-xs">{{ pp.einzelpreis|floatformat:2 }} {{ pp.waehrung }}</td>
|
||||
<td class="py-2 pr-3 text-center text-xs">{{ pp.vergleichsgewicht }}</td>
|
||||
<td class="py-2 text-center text-xs">
|
||||
{% if pp.ausschreibung_gewonnen == True %}<span class="text-green-600">✓</span>
|
||||
{% elif pp.ausschreibung_gewonnen == False %}<span class="text-red-500">✗</span>
|
||||
{% else %}<span class="text-slate-400">?</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
71
vergabe_teilnahme/templates/preise/detail.html
Normal file
71
vergabe_teilnahme/templates/preise/detail.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% load vergabe_tags %}
|
||||
{% block title %}{{ preispunkt.konkrete_leistung }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">{{ preispunkt.konkrete_leistung }}</h1>
|
||||
<a href="{% url 'ausschreibungen:preise:liste' ausschreibung.pk %}" class="btn-ghost text-xs">← Übersicht</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="col-span-2 space-y-4">
|
||||
<div class="card space-y-3">
|
||||
<p class="text-sm text-slate-500 font-medium">{{ preispunkt.leistungstyp }}</p>
|
||||
<div class="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<p class="text-xs text-slate-400">Einzelpreis</p>
|
||||
<p class="font-semibold">{% if preispunkt.einzelpreis %}{{ preispunkt.einzelpreis|floatformat:2 }} {{ preispunkt.waehrung }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400">Gesamtpreis</p>
|
||||
<p class="font-semibold">{% if preispunkt.gesamtpreis %}{{ preispunkt.gesamtpreis|floatformat:2 }} {{ preispunkt.waehrung }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-slate-400">Menge</p>
|
||||
<p>{% if preispunkt.menge %}{{ preispunkt.menge|floatformat:2 }} {{ preispunkt.mengeneinheit }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if preispunkt.kommentar %}
|
||||
<p class="text-sm text-slate-600 whitespace-pre-wrap">{{ preispunkt.kommentar }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if freigaben %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Freigaben</p>
|
||||
<ul class="space-y-2">
|
||||
{% for fg in freigaben %}
|
||||
<li class="text-sm">
|
||||
{% status_badge fg.status fg.get_status_display %}
|
||||
<span class="ml-2 text-slate-500">{{ fg.freigebender }}</span>
|
||||
{% if fg.kommentar %}<span class="ml-2 text-slate-400 text-xs">— {{ fg.kommentar }}</span>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="card space-y-2">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Details</p>
|
||||
<p class="text-xs text-slate-600">Gewicht: <span class="font-medium">{{ preispunkt.vergleichsgewicht }}</span></p>
|
||||
{% if preispunkt.preisstand %}<p class="text-xs text-slate-600">Preisstand: {{ preispunkt.preisstand|date:"d.m.Y" }}</p>{% endif %}
|
||||
{% if preispunkt.los %}<p class="text-xs text-slate-600">Los: {{ preispunkt.los }}</p>{% endif %}
|
||||
{% if preispunkt.subunternehmer %}<p class="text-xs text-slate-600">Subunternehmer: {{ preispunkt.subunternehmer }}</p>{% endif %}
|
||||
{% if preispunkt.laufzeitbezug %}<p class="text-xs text-slate-600">Laufzeit: {{ preispunkt.laufzeitbezug }}</p>{% endif %}
|
||||
{% if preispunkt.gewichtungsbegruendung %}<p class="text-xs text-slate-600">Begründung: {{ preispunkt.gewichtungsbegruendung }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="card space-y-2">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Aktionen</p>
|
||||
<a href="{% url 'ausschreibungen:preise:bearbeiten' ausschreibung.pk preispunkt.pk %}" class="btn-secondary text-xs w-full block text-center">Bearbeiten</a>
|
||||
<form method="post" action="{% url 'ausschreibungen:preise:loeschen' ausschreibung.pk preispunkt.pk %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-ghost text-xs w-full text-red-600 mt-1" onclick="return confirm('Preispunkt löschen?')">Löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
124
vergabe_teilnahme/templates/preise/form.html
Normal file
124
vergabe_teilnahme/templates/preise/form.html
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ titel }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">{{ titel }}</h1>
|
||||
<a href="{% url 'ausschreibungen:preise:liste' ausschreibung.pk %}" class="btn-ghost text-xs">← Zurück</a>
|
||||
</div>
|
||||
|
||||
<form method="post" x-data="{ subunternehmer: {{ form.subunternehmeranteil.value|yesno:'true,false' }} }" class="space-y-4">
|
||||
{% csrf_token %}
|
||||
|
||||
<datalist id="leistungstypen-list">
|
||||
{% for lt in leistungstypen %}<option value="{{ lt }}">{% endfor %}
|
||||
</datalist>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Leistung</p>
|
||||
<div>
|
||||
<label class="form-label">Leistungstyp *</label>
|
||||
{{ form.leistungstyp }}
|
||||
{% if form.leistungstyp.errors %}<p class="text-xs text-red-600 mt-1">{{ form.leistungstyp.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Konkrete Leistung *</label>
|
||||
{{ form.konkrete_leistung }}
|
||||
{% if form.konkrete_leistung.errors %}<p class="text-xs text-red-600 mt-1">{{ form.konkrete_leistung.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="form-label">Menge</label>
|
||||
{{ form.menge }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Einheit</label>
|
||||
{{ form.mengeneinheit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="form-label">Wiederkehrend</label>
|
||||
{{ form.wiederkehrend }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Laufzeitbezug</label>
|
||||
{{ form.laufzeitbezug }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Preis</p>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="form-label">Einzelpreis</label>
|
||||
<input name="einzelpreis" id="id_einzelpreis" class="form-input"
|
||||
type="number" step="0.01"
|
||||
value="{{ form.einzelpreis.value|default:'' }}"
|
||||
x-model="einzelpreis"
|
||||
@input="gesamtpreis = (einzelpreis && menge) ? (parseFloat(einzelpreis) * parseFloat(menge)).toFixed(2) : gesamtpreis">
|
||||
{% if form.einzelpreis.errors %}<p class="text-xs text-red-600 mt-1">{{ form.einzelpreis.errors.0 }}</p>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Gesamtpreis</label>
|
||||
<input name="gesamtpreis" id="id_gesamtpreis" class="form-input"
|
||||
type="number" step="0.01"
|
||||
value="{{ form.gesamtpreis.value|default:'' }}"
|
||||
x-model="gesamtpreis">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Währung</label>
|
||||
{{ form.waehrung }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Preisstand</label>
|
||||
{{ form.preisstand }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Vergleichsgewicht</p>
|
||||
<div>
|
||||
<label class="form-label">Gewicht</label>
|
||||
{{ form.vergleichsgewicht }}
|
||||
{% if form.vergleichsgewicht.errors %}<p class="text-xs text-red-600 mt-1">{{ form.vergleichsgewicht.errors.0 }}</p>{% endif %}
|
||||
<p class="text-xs text-slate-400 mt-1">0,0 = nicht gewertet · 1,0 = Standard · 2,0 = doppelt gewichtet</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Begründung</label>
|
||||
{{ form.gewichtungsbegruendung }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card space-y-4">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide">Zuordnung</p>
|
||||
<div>
|
||||
<label class="form-label">Los</label>
|
||||
{{ form.los }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label flex items-center gap-2">
|
||||
<span x-ref="subLabel" @click="subunternehmer = !subunternehmer">Subunternehmeranteil</span>
|
||||
{{ form.subunternehmeranteil }}
|
||||
</label>
|
||||
</div>
|
||||
<div x-show="subunternehmer">
|
||||
<label class="form-label">Subunternehmer</label>
|
||||
{{ form.subunternehmer }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<label class="form-label">Kommentar</label>
|
||||
{{ form.kommentar }}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="btn-primary">Speichern</button>
|
||||
<a href="{% url 'ausschreibungen:preise:liste' ausschreibung.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
121
vergabe_teilnahme/templates/preise/globaler_vergleich.html
Normal file
121
vergabe_teilnahme/templates/preise/globaler_vergleich.html
Normal file
@@ -0,0 +1,121 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Globaler Preisvergleich{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Globaler Preisvergleich</h1>
|
||||
</div>
|
||||
|
||||
<form method="get"
|
||||
hx-get="{% url 'preisvergleich' %}"
|
||||
hx-target="#ergebnis-bereich"
|
||||
hx-swap="innerHTML"
|
||||
class="card mb-4 flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="form-label">Leistungstyp</label>
|
||||
<input type="text" name="leistungstyp" value="{{ leistungstyp }}" list="lt-list" class="form-input">
|
||||
<datalist id="lt-list">{% for lt in alle_leistungstypen %}<option value="{{ lt }}">{% endfor %}</datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Gewonnen</label>
|
||||
<select name="gewonnen" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="ja" {% if filter_gewonnen == 'ja' %}selected{% endif %}>Ja</option>
|
||||
<option value="nein" {% if filter_gewonnen == 'nein' %}selected{% endif %}>Nein</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Von</label>
|
||||
<input type="date" name="von" value="{{ von }}" class="form-input">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Bis</label>
|
||||
<input type="date" name="bis" value="{{ bis }}" class="form-input">
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary text-xs">Auswerten</button>
|
||||
</form>
|
||||
|
||||
<div id="ergebnis-bereich">
|
||||
{% if ergebnis %}
|
||||
<div class="grid grid-cols-3 gap-3 mb-6">
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Gewichteter Durchschnitt</p>
|
||||
<p class="text-2xl font-bold text-blue-700">{{ ergebnis.wert|floatformat:2 }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Ungewichteter Durchschnitt</p>
|
||||
<p class="text-2xl font-bold text-slate-700">{% if ungewichtet %}{{ ungewichtet|floatformat:2 }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Messpunkte</p>
|
||||
<p class="text-2xl font-bold text-slate-700">{{ ergebnis.anzahl }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Minimum</p>
|
||||
<p class="text-xl font-semibold text-green-700">{{ ergebnis.minimum|floatformat:2 }}</p>
|
||||
</div>
|
||||
<div class="card text-center">
|
||||
<p class="text-xs text-slate-500 mb-1">Maximum</p>
|
||||
<p class="text-xl font-semibold text-red-700">{{ ergebnis.maximum|floatformat:2 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if ergebnis_gewonnen or ergebnis_verloren %}
|
||||
<div class="grid grid-cols-2 gap-3 mb-6">
|
||||
{% if ergebnis_gewonnen %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-green-700 uppercase tracking-wide mb-2">Ø bei Gewinn</p>
|
||||
<p class="text-xl font-bold text-green-700">{{ ergebnis_gewonnen.wert|floatformat:2 }}</p>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ ergebnis_gewonnen.anzahl }} Messpunkte</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if ergebnis_verloren %}
|
||||
<div class="card">
|
||||
<p class="text-xs font-medium text-red-600 uppercase tracking-wide mb-2">Ø bei Verlust</p>
|
||||
<p class="text-xl font-bold text-red-600">{{ ergebnis_verloren.wert|floatformat:2 }}</p>
|
||||
<p class="text-xs text-slate-500 mt-1">{{ ergebnis_verloren.anzahl }} Messpunkte</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% elif leistungstyp %}
|
||||
<div class="card text-center py-6 text-sm text-slate-500 mb-6">Keine Daten für "{{ leistungstyp }}".</div>
|
||||
{% else %}
|
||||
<div class="card text-center py-6 text-sm text-slate-500 mb-6">Leistungstyp eingeben, um Vergleich zu starten.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if preispunkte %}
|
||||
<div class="card overflow-hidden">
|
||||
<p class="text-xs font-medium text-slate-500 uppercase tracking-wide mb-3">Einzelmesspunkte ({{ preispunkte.count }})</p>
|
||||
<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">Ausschreibung</th>
|
||||
<th class="pb-2 pr-3">Leistung</th>
|
||||
<th class="pb-2 pr-3 text-right">Einzelpreis</th>
|
||||
<th class="pb-2 pr-3 text-center">Gewicht</th>
|
||||
<th class="pb-2 text-center">Gew.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for pp in preispunkte %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-2 pr-3 text-xs text-slate-600">{{ pp.ausschreibung.titel|truncatechars:30 }}</td>
|
||||
<td class="py-2 pr-3 text-xs">{{ pp.konkrete_leistung }}</td>
|
||||
<td class="py-2 pr-3 text-right text-xs">{{ pp.einzelpreis|floatformat:2 }} {{ pp.waehrung }}</td>
|
||||
<td class="py-2 pr-3 text-center text-xs">{{ pp.vergleichsgewicht }}</td>
|
||||
<td class="py-2 text-center text-xs">
|
||||
{% if pp.ausschreibung_gewonnen == True %}<span class="text-green-600">✓</span>
|
||||
{% elif pp.ausschreibung_gewonnen == False %}<span class="text-red-500">✗</span>
|
||||
{% else %}<span class="text-slate-400">?</span>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
109
vergabe_teilnahme/templates/preise/liste.html
Normal file
109
vergabe_teilnahme/templates/preise/liste.html
Normal file
@@ -0,0 +1,109 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Preise — {{ ausschreibung.titel }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="page-title">Preise</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'ausschreibungen:preise:auswertung' ausschreibung.pk %}" class="btn-secondary text-xs">Auswertung</a>
|
||||
<a href="{% url 'ausschreibungen:preise:neu' ausschreibung.pk %}" class="btn-primary text-xs">+ Preispunkt</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="card mb-4 flex flex-wrap gap-3 items-end">
|
||||
<div>
|
||||
<label class="form-label">Leistungstyp</label>
|
||||
<input type="text" name="leistungstyp" value="{{ filter.leistungstyp }}" list="lt-list" class="form-input">
|
||||
<datalist id="lt-list">{% for lt in leistungstypen %}<option value="{{ lt }}">{% endfor %}</datalist>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Los</label>
|
||||
<select name="los" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
{% for los in lose %}<option value="{{ los.pk }}" {% if filter.los == los.pk|stringformat:"s" %}selected{% endif %}>{{ los.bezeichnung }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Subunternehmer</label>
|
||||
<select name="subunternehmer" class="form-select">
|
||||
<option value="">Alle</option>
|
||||
<option value="ja" {% if filter.subunternehmer == 'ja' %}selected{% endif %}>Ja</option>
|
||||
<option value="nein" {% if filter.subunternehmer == 'nein' %}selected{% endif %}>Nein</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-secondary text-xs">Filtern</button>
|
||||
{% if filter.leistungstyp or filter.los or filter.subunternehmer %}
|
||||
<a href="{% url 'ausschreibungen:preise:liste' ausschreibung.pk %}" class="btn-ghost text-xs">Zurücksetzen</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if preispunkte %}
|
||||
<div class="card overflow-hidden">
|
||||
<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">Leistungstyp</th>
|
||||
<th class="pb-2 pr-3">Konkrete Leistung</th>
|
||||
<th class="pb-2 pr-3 text-right">Menge/Einh.</th>
|
||||
<th class="pb-2 pr-3 text-right">Einzelpreis</th>
|
||||
<th class="pb-2 pr-3 text-right">Gesamtpreis</th>
|
||||
<th class="pb-2 pr-3 text-center">Gewicht</th>
|
||||
<th class="pb-2">Los</th>
|
||||
<th class="pb-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for pp in preispunkte %}
|
||||
<tr class="hover:bg-slate-50">
|
||||
<td class="py-2 pr-3 text-xs text-slate-600">{{ pp.leistungstyp }}</td>
|
||||
<td class="py-2 pr-3">
|
||||
<a href="{% url 'ausschreibungen:preise:detail' ausschreibung.pk pp.pk %}" class="text-blue-600 hover:underline">{{ pp.konkrete_leistung }}</a>
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right text-xs text-slate-600">
|
||||
{% if pp.menge %}{{ pp.menge|floatformat:2 }} {{ pp.mengeneinheit }}{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right text-xs">
|
||||
{% if pp.einzelpreis %}
|
||||
<span class="{% if pp.vergleichsgewicht == 0 %}line-through text-slate-400{% elif pp.vergleichsgewicht > 1 %}font-semibold{% else %}text-slate-700{% endif %}">
|
||||
{{ pp.einzelpreis|floatformat:2 }} {{ pp.waehrung }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-right text-xs">
|
||||
{% if pp.gesamtpreis %}
|
||||
<span class="{% if pp.vergleichsgewicht == 0 %}line-through text-slate-400{% elif pp.vergleichsgewicht > 1 %}font-semibold{% else %}text-slate-700{% endif %}">
|
||||
{{ pp.gesamtpreis|floatformat:2 }} {{ pp.waehrung }}
|
||||
</span>
|
||||
{% else %}—{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-3 text-center text-xs">
|
||||
<span class="{% if pp.vergleichsgewicht == 0 %}text-slate-400{% elif pp.vergleichsgewicht > 1 %}font-semibold text-blue-700{% else %}text-slate-600{% endif %}">
|
||||
{{ pp.vergleichsgewicht }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 text-xs text-slate-500">{{ pp.los.bezeichnung|default:"—" }}</td>
|
||||
<td class="py-2 text-right">
|
||||
<a href="{% url 'ausschreibungen:preise:bearbeiten' ausschreibung.pk pp.pk %}" class="text-xs text-slate-400 hover:text-blue-600">Bearb.</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="border-t border-slate-200">
|
||||
<td colspan="4" class="pt-2 text-xs text-slate-500 font-medium">Summe Gesamtpreise (ungewichtet)</td>
|
||||
<td class="pt-2 text-right text-xs font-semibold text-slate-700">
|
||||
{% if summe_gesamt %}{{ summe_gesamt|floatformat:2 }} EUR{% else %}—{% endif %}
|
||||
</td>
|
||||
<td colspan="3"></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card text-center py-10 text-sm text-slate-500">
|
||||
Noch keine Preispunkte erfasst.
|
||||
<a href="{% url 'ausschreibungen:preise:neu' ausschreibung.pk %}" class="text-blue-600 hover:underline ml-1">+ Ersten Preispunkt anlegen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
16
vergabe_teilnahme/templates/preise/loeschen_bestaetigen.html
Normal file
16
vergabe_teilnahme/templates/preise/loeschen_bestaetigen.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Preispunkt löschen{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto">
|
||||
<div class="card text-center space-y-4">
|
||||
<p class="text-slate-700">Preispunkt <strong>{{ preispunkt.konkrete_leistung }}</strong> wirklich löschen?</p>
|
||||
<div class="flex gap-3 justify-center">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn-primary bg-red-600 hover:bg-red-700">Löschen</button>
|
||||
</form>
|
||||
<a href="{% url 'ausschreibungen:preise:detail' ausschreibung.pk preispunkt.pk %}" class="btn-ghost">Abbrechen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -6,6 +6,7 @@ from django.shortcuts import redirect
|
||||
from django.urls import include, path
|
||||
|
||||
from vergabe_teilnahme.apps.core import views as core_views
|
||||
from vergabe_teilnahme.apps.preise import views as preise_views
|
||||
|
||||
|
||||
def health(request):
|
||||
@@ -28,6 +29,7 @@ urlpatterns = [
|
||||
path('aufgaben/', include('vergabe_teilnahme.apps.aufgaben.global_urls')),
|
||||
path('dokumente/', include('vergabe_teilnahme.apps.dokumente.urls')),
|
||||
path('preise/', include('vergabe_teilnahme.apps.preise.urls')),
|
||||
path('preise/vergleich/', preise_views.globaler_preisvergleich, name='preisvergleich'),
|
||||
path('partner/', include('vergabe_teilnahme.apps.partner.urls')),
|
||||
path('bibliothek/', include('vergabe_teilnahme.apps.bibliothek.urls')),
|
||||
path('marktbegleiter/', include('vergabe_teilnahme.apps.marktbegleiter.urls')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: WP-0008
|
||||
title: Preise und Marktpreisauswertung
|
||||
status: todo
|
||||
status: done
|
||||
phase: 8-of-12
|
||||
created: "2026-05-08"
|
||||
depends_on: WP-0007
|
||||
@@ -17,7 +17,7 @@ und Leistungstyp-Auswertung. Referenz: UC-PR-01 bis UC-PR-04, FR-25 bis FR-36.
|
||||
```task
|
||||
id: WP-0008-T01
|
||||
title: Preispunkt anlegen mit Vergleichsgewicht-Validierung (UC-PR-01, UC-PR-02)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`preise/views.py` — preispunkt_neu:
|
||||
|
||||
@@ -43,7 +43,7 @@ Template `preise/form.html`:
|
||||
```task
|
||||
id: WP-0008-T02
|
||||
title: Preispunkt-Liste pro Ausschreibung
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`preise/views.py` — preispunkte_liste:
|
||||
|
||||
@@ -66,7 +66,7 @@ Gesamtpreis-Auto-Berechnung:
|
||||
```task
|
||||
id: WP-0008-T03
|
||||
title: Leistungstyp-Auswertung mit gewichtetem Durchschnitt (UC-PR-03)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`preise/views.py` — leistungstyp_auswertung:
|
||||
|
||||
@@ -108,7 +108,7 @@ Darunter: Tabelle aller Einzelmesspunkte mit Ausschreibungstitel, Datum, Gewicht
|
||||
```task
|
||||
id: WP-0008-T04
|
||||
title: Globaler Preisvergleich (cross-Ausschreibung, UC-PR-03)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
URL: `/preise/vergleich/` (globaler Endpunkt, kein ausschreibung_id Präfix)
|
||||
|
||||
@@ -130,7 +130,7 @@ in Haupt-URLs einbinden.
|
||||
```task
|
||||
id: WP-0008-T05
|
||||
title: Preisfreigabe und URL-Verkabelung (UC-PR-04)
|
||||
status: todo
|
||||
status: done
|
||||
|
||||
`preise/views.py` — preisfreigabe:
|
||||
Button "Preisfreigabe erteilen" auf der Preisliste öffnet Freigabe-Modal (aus WP-0012).
|
||||
|
||||
Reference in New Issue
Block a user