generated from coulomb/repo-seed
- Fristen-Widget-Format: DateInput/DateTimeInput mit ISO-Format-Attribut, damit Browser date/datetime-local korrekt vorausfüllen (Feedback #4) - Phase 2 Teilnahmeentscheidung: URL in build_phase_nav von /teilnahmeentscheidung/ → /entscheidung/ korrigiert (Feedback #6) - Phase 3 Detaillierte Durchsicht: URL in build_phase_nav von /anforderungen/ → /lose/anforderungen/ korrigiert (Feedback #7) - Phase 7 Abgabe: order_by('bezeichnung') → order_by('beschreibung') in abgabe_views.py (Dokument hat kein Feld 'bezeichnung') (Feedback #8) - Ausschreibungen-Liste: Ausschreiber zuerst, Titel zweite Spalte, neues geschätztes Volumen (Feedback #5) - Feedback-Backlog Leerstand: bereits durch vorherigen URL-Fix abgedeckt (Feedback #1) - Rechtsgrundlage (VgV/UVgO/VOB/A/SektVO/GWB) als neues Formularfeld incl. Migration (Feedback #2) - Bindefrist in Tagen + berechnetes Enddatum als Modell-Property bindefrist_berechnet, Formular und Detailansicht erweitert (Feedback #3) 68/68 Tests grün. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
133 lines
4.7 KiB
Python
133 lines
4.7 KiB
Python
from datetime import date, timedelta
|
|
|
|
from django.db import models
|
|
|
|
from vergabe_teilnahme.apps.core.models import FlexibleModel
|
|
|
|
|
|
class Ausschreibung(FlexibleModel):
|
|
STATUS_CHOICES = [
|
|
(1, 'Recherche & Unterlagen'),
|
|
(2, 'Teilnahmeentscheidung'),
|
|
(3, 'Detaillierte Durchsicht'),
|
|
(4, 'Klärungsphase'),
|
|
(5, 'Preismodell'),
|
|
(6, 'Unterlagen finalisieren'),
|
|
(7, 'Abgabe'),
|
|
(8, 'Zuschlag / Nachbetrachtung'),
|
|
(9, 'Abgegeben'),
|
|
(10, 'Gewonnen'),
|
|
(11, 'Verloren'),
|
|
(12, 'Aufgehoben'),
|
|
(13, 'Zurückgezogen'),
|
|
]
|
|
TEILNAHME_CHOICES = [
|
|
('offen', 'Offen'),
|
|
('teilnahme', 'Teilnahme'),
|
|
('ablehnung', 'Ablehnung'),
|
|
]
|
|
VERGABEART_CHOICES = [
|
|
('oeffentlich', 'Öffentliche Ausschreibung'),
|
|
('beschraenkt', 'Beschränkte Ausschreibung'),
|
|
('freihanding', 'Freihändige Vergabe'),
|
|
('wettbewerb', 'Wettbewerb'),
|
|
('rahmenvertrag', 'Rahmenvertrag'),
|
|
('sonstige', 'Sonstige'),
|
|
]
|
|
RECHTSGRUNDLAGE_CHOICES = [
|
|
('vgv', 'VgV'),
|
|
('uvgo', 'UVgO'),
|
|
('vob_a', 'VOB/A'),
|
|
('sektvo', 'SektVO'),
|
|
('gwb', 'GWB'),
|
|
('sonstige', 'Sonstige'),
|
|
]
|
|
|
|
titel = models.CharField(max_length=400)
|
|
ausschreiber = models.CharField(max_length=300)
|
|
vergabeplattform = models.CharField(max_length=200, blank=True)
|
|
vergabenummer = models.CharField(max_length=100, blank=True)
|
|
vergabeart = models.CharField(max_length=30, choices=VERGABEART_CHOICES, blank=True)
|
|
rechtsgrundlage = models.CharField(max_length=20, choices=RECHTSGRUNDLAGE_CHOICES, blank=True)
|
|
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=1)
|
|
teilnahmeentscheidung = models.CharField(
|
|
max_length=20, choices=TEILNAHME_CHOICES, default='offen'
|
|
)
|
|
entscheidungsbegruendung = models.TextField(blank=True)
|
|
|
|
# Fristen
|
|
veroeffentlichungsdatum = models.DateField(null=True, blank=True)
|
|
bieterfragen_bis = models.DateField(null=True, blank=True)
|
|
abgabe_bis = models.DateTimeField(null=True, blank=True)
|
|
bindefrist = models.DateField(null=True, blank=True)
|
|
bindefrist_tage = models.PositiveSmallIntegerField(null=True, blank=True, help_text='Bindefrist in Tagen ab Abgabe')
|
|
|
|
# Verantwortlichkeiten
|
|
bid_manager = models.ForeignKey(
|
|
'accounts.Mitarbeiter', on_delete=models.SET_NULL,
|
|
null=True, blank=True, related_name='verwaltete_ausschreibungen'
|
|
)
|
|
team = models.ManyToManyField(
|
|
'accounts.Mitarbeiter', blank=True, related_name='team_ausschreibungen'
|
|
)
|
|
|
|
# Beschreibung & Klassifizierung
|
|
leistungsbeschreibung = models.TextField(blank=True)
|
|
branche = models.CharField(max_length=200, blank=True)
|
|
schlagwoerter = models.CharField(max_length=500, blank=True)
|
|
geschaetztes_volumen = models.DecimalField(max_digits=14, decimal_places=2, null=True, blank=True)
|
|
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)
|
|
unterlagen_erhalten_am = models.DateField(null=True, blank=True)
|
|
|
|
archiviert = models.BooleanField(default=False, db_index=True)
|
|
|
|
# Timestamps
|
|
erstellt_am = models.DateTimeField(auto_now_add=True)
|
|
geaendert_am = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-erstellt_am']
|
|
verbose_name = 'Ausschreibung'
|
|
verbose_name_plural = 'Ausschreibungen'
|
|
|
|
def __str__(self):
|
|
return self.titel
|
|
|
|
@property
|
|
def bindefrist_berechnet(self):
|
|
"""Enddatum aus abgabe_bis + bindefrist_tage, falls kein festes bindefrist-Datum."""
|
|
if self.bindefrist:
|
|
return self.bindefrist
|
|
if self.abgabe_bis and self.bindefrist_tage:
|
|
return (self.abgabe_bis + timedelta(days=self.bindefrist_tage)).date()
|
|
return None
|
|
|
|
@property
|
|
def ist_aktiv(self):
|
|
return 1 <= self.status <= 9
|
|
|
|
@property
|
|
def naechste_frist(self):
|
|
heute = date.today()
|
|
kandidaten = []
|
|
if self.bieterfragen_bis and self.bieterfragen_bis >= heute:
|
|
kandidaten.append(self.bieterfragen_bis)
|
|
if self.abgabe_bis:
|
|
abgabe = self.abgabe_bis.date() if hasattr(self.abgabe_bis, 'date') else self.abgabe_bis
|
|
if abgabe >= heute:
|
|
kandidaten.append(abgabe)
|
|
return min(kandidaten) if kandidaten else None
|
|
|
|
@property
|
|
def phase_nummer(self):
|
|
return min(8, max(1, self.status))
|