19 KiB
id, title, status, phase, created, depends_on
| id | title | status | phase | created | depends_on |
|---|---|---|---|---|---|
| WP-0002 | Fachmodelle — alle Django-Models, Migrationen, Admin | done | 2-of-12 | 2026-05-08 | WP-0001 |
WP-0002 — Fachmodelle
Implementiert alle Django-Modelle gemäß wiki/ArchitectureBlueprint.md Abschnitt 5.
Alle Modelle erben von FlexibleModel. Am Ende: alle Migrationen, Admin-Registrierung
und ein Management-Command für Seed-Daten.
Arbeitsverzeichnis: /home/worsch/vergabe-teilnahme/
Apps-Pfad: vergabe_teilnahme/apps/
id: WP-0002-T01
title: Accounts-App: Mitarbeiter-Modell (AbstractUser)
status: done
Datei: `vergabe_teilnahme/apps/accounts/models.py`
```python
from django.contrib.auth.models import AbstractUser
from django.db import models
class Mitarbeiter(AbstractUser):
ROLLE_CHOICES = [
('bid_manager', 'Bid Manager'),
('fachexperte', 'Fachverantwortlicher'),
('vertrieb', 'Vertrieb / Account Management'),
('pricing', 'Pricing / Controlling'),
('recht', 'Recht / Compliance'),
('geschaeftsfuehrung', 'Geschäftsführung'),
('projektleitung','Projektleitung Umsetzung'),
('admin', 'Administrator'),
]
rolle = CharField(max_length=30, choices=ROLLE_CHOICES, blank=True)
mobilnummer = CharField(max_length=50, blank=True)
organisationseinheit = CharField(max_length=200, blank=True)
def __str__(self):
return self.get_full_name() or self.username
Registriere in accounts/admin.py mit UserAdmin.
Setze AUTH_USER_MODEL = 'accounts.Mitarbeiter' in settings/base.py (bereits in T02 von WP-0001 vorbereitet — prüfen).
```task
id: WP-0002-T02
title: Core-App: FlexibleModel-Mixin und Basis-Infrastruktur
status: done
Datei: `vergabe_teilnahme/apps/core/models.py`
Implementiere:
1. `FlexibleModel(Model)` — abstract base:
- Hat `GenericRelation` auf `CustomAttribute`
- Methode `get_hidden_fields()` — liest `EntityFieldConfig` für `self._meta.model_name`
- Methode `get_visible_field_names()` — gibt Liste nicht-ausgeblendeter Felder zurück
- `class Meta: abstract = True`
2. `EntityFieldConfig(Model)` — globale Feldkonfiguration pro Entitätstyp:
- `entity_type` CharField(100) — model_name slug (z. B. 'subunternehmer')
- `field_name` CharField(100)
- `is_hidden` BooleanField(default=False)
- `display_label` CharField(200, blank=True) — Umbenennung
- `sort_order` PositiveSmallIntegerField(default=0)
- `Meta: unique_together = ('entity_type', 'field_name')`
3. `CustomAttribute(Model)` — generisches Key-Value-Attribut:
- `content_type` FK(ContentType, CASCADE)
- `object_id` PositiveIntegerField()
- `content_object` GenericForeignKey()
- `key` CharField(100) — slug
- `label` CharField(200) — Anzeigename
- `value` TextField(blank=True)
- `data_type` CharField(20, choices=[text/number/date/boolean/url/email], default='text')
- `sort_order` PositiveSmallIntegerField(default=0)
- `created_at` DateTimeField(auto_now_add=True)
- `Meta: ordering = ['sort_order', 'created_at']`
- `Index` auf `(content_type, object_id)`
4. `Freigabe(Model)` — generische Freigabe:
- `content_type` FK(ContentType, CASCADE)
- `object_id` PositiveIntegerField()
- `content_object` GenericForeignKey()
- `freigabe_typ` CharField(30, choices=[teilnahme/ablehnung/recht/preis/abgabe/standarddokument/referenz])
- `freigebende_person` FK(settings.AUTH_USER_MODEL, PROTECT)
- `status` CharField(20, choices=[erteilt/abgelehnt/ausstehend], default='erteilt')
- `kommentar` TextField(blank=True)
- `timestamp` DateTimeField(auto_now_add=True)
- `Meta: ordering = ['-timestamp']`
Alle vier in `core/admin.py` registrieren.
id: WP-0002-T03
title: Core-App: services.py mit Utility-Funktionen
status: done
Datei: `vergabe_teilnahme/apps/core/services.py`
Implementiere folgende Funktionen (ohne Django-Import-Fehler zur Importzeit):
```python
from datetime import date
from decimal import Decimal
def get_deadline_warnings(ausschreibung):
"""Gibt Liste von Warnungen für nahende Fristen zurück."""
warnings = []
heute = date.today()
if ausschreibung.bieterfragen_bis:
delta = (ausschreibung.bieterfragen_bis - heute).days
if delta <= 3:
warnings.append({'typ': 'bieterfragen', 'tage': delta, 'farbe': 'red' if delta <= 1 else 'amber'})
if ausschreibung.abgabe_bis:
delta = (ausschreibung.abgabe_bis.date() - heute).days
if delta <= 14:
warnings.append({'typ': 'abgabe', 'tage': delta, 'farbe': 'red' if delta <= 3 else 'amber'})
return warnings
def gewichteter_durchschnitt(preispunkte, feld='einzelpreis'):
"""Berechnet gewichteten Durchschnitt für Preispunkte. Gibt None zurück wenn keine verwertbaren Punkte."""
relevante = [p for p in preispunkte
if getattr(p, feld) is not None and p.vergleichsgewicht > 0]
if not relevante:
return None
summe_gewichte = sum(p.vergleichsgewicht for p in relevante)
if summe_gewichte == 0:
return None
summe = sum(getattr(p, feld) * p.vergleichsgewicht for p in relevante)
werte = [getattr(p, feld) for p in relevante]
return {
'wert': summe / summe_gewichte,
'summe_gewichte': summe_gewichte,
'anzahl': len(relevante),
'minimum': min(werte),
'maximum': max(werte),
'ungewichtet': sum(werte) / len(werte),
}
Schreibe Tests in vergabe_teilnahme/apps/core/tests/test_services.py:
- Test gewichteter_durchschnitt mit Beispiel aus Blueprint (100×1,0 + 80×0,2 + 110×1,2 = 103,33)
- Test mit leerem Input → None
- Test mit allen Gewichten 0 → None
```task
id: WP-0002-T04
title: Ausschreibungen-App: Ausschreibung-Modell
status: done
Datei: `vergabe_teilnahme/apps/ausschreibungen/models.py`
Implementiere `Ausschreibung(FlexibleModel)` gemäß Blueprint Abschnitt 5.1.
Import von FlexibleModel: `from vergabe_teilnahme.apps.core.models import FlexibleModel`
Felder exakt wie im Blueprint definiert (STATUS_CHOICES 1-13, TEILNAHME_CHOICES,
alle Datums-/Fristen-Felder, erstellt_am/geaendert_am auto).
Methoden:
- `__str__` → `self.titel`
- `property ist_aktiv` → status in range(1, 10)
- `property naechste_frist` → das nächste nicht-vergangene Datum aus (bieterfragen_bis, abgabe_bis.date())
- `property phase_nummer` → min(8, max(1, self.status)) — Phasennummer für Navigator
`Meta: ordering = ['-erstellt_am']`
Admin-Registrierung mit list_display=['titel', 'ausschreiber', 'status', 'abgabe_bis'].
id: WP-0002-T05
title: Lose-App: Los- und Anforderung-Modell
status: done
Datei: `vergabe_teilnahme/apps/lose/models.py`
`Los(FlexibleModel)`:
- `ausschreibung` FK(Ausschreibung, CASCADE, related_name='lose')
- `losnummer` CharField(50)
- `lostitel` CharField(300)
- `beschreibung` TextField(blank=True)
- `abgrenzung` TextField(blank=True)
- `zustaendiger` FK('accounts.Mitarbeiter', null=True, blank=True, SET_NULL)
- `teilnahme` BooleanField(null=True) # None = offen
- `status` CharField(50, blank=True)
- `Meta: ordering = ['losnummer']`
- `__str__` → `f"Los {self.losnummer}: {self.lostitel}"`
`Anforderung(FlexibleModel)`:
- `ausschreibung` FK(Ausschreibung, CASCADE)
- `los` FK(Los, null=True, blank=True, SET_NULL, related_name='anforderungen')
- Alle Felder aus Blueprint 5.3 (titel, beschreibung, quelle_im_dokument, kategorie,
verbindlichkeit MUSS_CHOICES, ausschlusskriterium, bewertungskriterium,
zustaendiger FK, erfuellungsstatus CHOICES, nachweis_erforderlich)
- `dokumente` M2M('dokumente.Dokument', blank=True)
- `nachweise` M2M('bibliothek.Nachweis', blank=True)
- `__str__` → `self.titel`
Admin für beide Modelle.
id: WP-0002-T06
title: Aufgaben-App: Aufgabe- und Bieterfrage-Modell
status: done
Datei: `vergabe_teilnahme/apps/aufgaben/models.py`
`Aufgabe(FlexibleModel)` mit allen Feldern aus Blueprint 5.4:
TYP_CHOICES (fachlich/rechtlich/kaufmaennisch/technisch/subunternehmer/dokument/preis)
STATUS_CHOICES (offen/in_bearbeitung/wartend_intern/wartend_sub/wartend_ausschreiber/erledigt/verworfen/ueberfaellig)
FKs auf Ausschreibung (CASCADE), Los (null/blank/SET_NULL), Anforderung, Bieterfrage, Dokument.
prioritaet PositiveSmallIntegerField(default=2, choices=[(1,'Hoch'),(2,'Mittel'),(3,'Niedrig')])
frist DateField(null=True, blank=True)
verantwortlicher FK(Mitarbeiter, null=True, SET_NULL)
ergebnis TextField(blank=True)
Meta: ordering = ['prioritaet', 'frist']
property `ist_ueberfaellig`: frist < date.today() and status not in ['erledigt', 'verworfen']
`Bieterfrage(FlexibleModel)` mit allen Feldern aus Blueprint 5.5:
STATUS_CHOICES (entwurf/abgestimmt/eingereicht/beantwortet/eingearbeitet)
FKs auf Ausschreibung (CASCADE), Anforderung (null/blank), Dokument (null/blank)
prioritaet, einreichungsdatum, antwort, auswirkung_angebot, eingearbeitet
Admin für beide Modelle.
id: WP-0002-T07
title: Dokumente-App: Dokument-Modell mit Datei-Upload
status: done
Datei: `vergabe_teilnahme/apps/dokumente/models.py`
`Dokument(FlexibleModel)` mit allen Feldern aus Blueprint 5.6:
- `datei` FileField(upload_to='dokumente/%Y/%m/')
- `dateiname` CharField(300) — wird beim Upload automatisch aus datei.name befüllt
- Alle Status-/Kategorie-Choices
- `ausschreibung` FK(null=True, blank=True)
- `los` FK(Los, null=True, blank=True)
- `verantwortlicher`, `pruefer` FK(Mitarbeiter)
- `finale_abgabeversion` BooleanField(default=False)
- `upload_datum` DateTimeField(auto_now_add=True)
- `GenericRelation(Freigabe)` für direkte Freigaben-Abfrage
Signal `pre_save`: falls `finale_abgabeversion` von False auf True gesetzt wird,
setze `status='final_abgegeben'`.
Datei-Validierung als Modell-Methode `clean()`:
Erlaubte MIME-Typen: pdf, docx, xlsx, zip, png, jpg (prüfe gegen Dateiendung, keine MIME-Detection in v1)
Maximalgröße: settings.MAX_UPLOAD_SIZE (Default: 50 MB)
Admin-Registrierung.
id: WP-0002-T08
title: Preise-App: Preispunkt-Modell
status: done
Datei: `vergabe_teilnahme/apps/preise/models.py`
`Preispunkt(FlexibleModel)` gemäß Blueprint 5.7:
- `ausschreibung` FK(Ausschreibung, CASCADE)
- `los` FK(Los, null=True, blank=True, SET_NULL)
- `leistungstyp` CharField(200) — freier Text, z. B. "Schulung", "Lizenz", "Betrieb"
- `konkrete_leistung` CharField(400)
- `mengeneinheit`, `waehrung` CharFields mit Defaults
- `menge`, `einzelpreis`, `gesamtpreis` DecimalFields(14, 2 bzw. 4, null=True, blank=True)
- `preisstand` DateField(null=True, blank=True)
- `wiederkehrend` BooleanField(default=False)
- `laufzeitbezug` CharField(100, blank=True)
- `subunternehmeranteil` BooleanField(default=False)
- `subunternehmer` FK('partner.Subunternehmer', null=True, blank=True, SET_NULL)
- `vergleichsgewicht` DecimalField(3, 1, default=Decimal('1.0'))
- `gewichtungsbegruendung` TextField(blank=True)
- `kommentar` TextField(blank=True)
- `ausschreibung_gewonnen` BooleanField(null=True)
Modell-Validierung `clean()`:
- `vergleichsgewicht` muss zwischen Decimal('0.0') und Decimal('2.0') liegen,
sonst ValidationError("Vergleichsgewicht muss zwischen 0,0 und 2,0 liegen.")
`Meta: ordering = ['leistungstyp', 'konkrete_leistung']`
Admin mit list_display=['konkrete_leistung', 'leistungstyp', 'einzelpreis', 'vergleichsgewicht'].
id: WP-0002-T09
title: Partner-App: Subunternehmer und Dienstleistertyp
status: done
Datei: `vergabe_teilnahme/apps/partner/models.py`
`Dienstleistertyp(FlexibleModel)`:
- name CharField(200), beschreibung TextField(blank=True)
- typische_leistungen TextField(blank=True), typische_nachweise TextField(blank=True)
- relevante_standards TextField(blank=True), typische_preisbestandteile TextField(blank=True)
- bemerkungen TextField(blank=True)
- `__str__` → name
`Subunternehmer(FlexibleModel)`:
- Alle Felder aus Blueprint 5.9
- PRAEFERENZ_CHOICES: bevorzugt/zugelassen/gesperrt (default='zugelassen')
- `dienstleistertyp` FK(Dienstleistertyp, null=True, blank=True, SET_NULL)
- `bisherige_ausschreibungen` M2M(Ausschreibung, blank=True, through='SubunternehmerZuordnung')
`SubunternehmerZuordnung(Model)` — Durchgangstabelle (kein FlexibleModel):
- `subunternehmer` FK(Subunternehmer, CASCADE)
- `ausschreibung` FK(Ausschreibung, CASCADE)
- `los` FK(Los, null=True, blank=True, SET_NULL)
- `konkrete_leistung` CharField(300, blank=True)
- `zusage_vorhanden` BooleanField(default=False)
- `nachweis_eingegangen` BooleanField(default=False)
- `preis_vorhanden` BooleanField(default=False)
- `kommentar` TextField(blank=True)
Admin für alle drei Modelle.
id: WP-0002-T10
title: Bibliothek-App: Nachweis, Referenz, Leistungsblatt, Entscheidungsregel
status: done
Datei: `vergabe_teilnahme/apps/bibliothek/models.py`
`Nachweis(FlexibleModel)` — Compliance-Nachweise:
Alle Felder aus Blueprint 6.10 (titel, kurzbeschreibung, dokumenttyp, kategorie,
datei FileField, version, gueltig_ab/bis DateFields, eigentuemer FK(Mitarbeiter),
freigabestatus CharField, letzte_pruefung DateField, zugehoerige_standards TextField,
vertraulichkeit CharField, sprache CharField default='de',
fuer_oeffentliche BooleanField, fuer_privatwirtschaftliche BooleanField)
property `ist_abgelaufen`: gueltig_bis < date.today() if gueltig_bis else False
`Referenz(FlexibleModel)` — Projekt-Referenzen:
Alle Felder aus Blueprint 6.11 (referenztitel, kunde, branche, oeffentlich_oder_privat,
leistungsbeschreibung, eingesetzte_produkte, projektzeitraum CharField,
vertragsvolumen DecimalField(null=True), ansprechpartner_referenzkunde,
freigabestatus_verwendung, vertraulichkeit, whitepaper FileField(null=True),
kurzfassung TextField, langfassung TextField,
verwendbar_fuer_ausschreibungen BooleanField, einschraenkungen_verwendung TextField)
leistungsblaetter M2M('Leistungsblatt', blank=True)
`Leistungsblatt(FlexibleModel)`:
produktfunktion, beschreibung, leistungsumfang, grenzen_ausschluesse,
technische_voraussetzungen, typische_nachweise TextField(blank=True),
version CharField, eigentuemer FK(Mitarbeiter, null=True)
`Entscheidungsregel(FlexibleModel)`:
regelname, beschreibung, kategorie, gewichtung DecimalField(5,2, default=1.0),
bewertungslogik TextField, schwellenwert DecimalField(null=True),
empfehlung CharField(choices=[teilnehmen/nicht_teilnehmen/pruefen]),
begruendung TextField, gueltig_von/bis DateFields(null=True), aktiv BooleanField(default=True),
verantwortlicher FK(Mitarbeiter, null=True)
Admin für alle vier Modelle.
id: WP-0002-T11
title: Marktbegleiter-App: Marktbegleiter und Ausschreibungspassage
status: done
Datei: `vergabe_teilnahme/apps/marktbegleiter/models.py`
`Marktbegleiter(FlexibleModel)` — alle Felder aus Blueprint 6.15:
name CharField(300), kurzbeschreibung TextField,
produkt_leistungsportfolio TextField(blank=True),
relevante_branchen TextField(blank=True),
bekannte_staerken TextField(blank=True), bekannte_schwaechen TextField(blank=True),
typische_formulierungen TextField(blank=True),
typische_leistungsmerkmale TextField(blank=True),
bekannte_zertifizierungen TextField(blank=True),
bekannte_referenzen TextField(blank=True),
quellen_links TextField(blank=True), letzte_aktualisierung DateField(null=True),
aktualisierungsstatus CharField(50, blank=True),
interne_notizen TextField(blank=True),
vertraulichkeit CharField(20, choices=[intern/streng_vertraulich], default='intern')
`Ausschreibungspassage(FlexibleModel)` — alle Felder aus Blueprint 6.16:
ausschreibung FK(Ausschreibung, CASCADE, related_name='passagen')
dokument FK(Dokument, null=True, blank=True, SET_NULL)
fundstelle CharField(200, blank=True)
passage TextField()
kategorie CharField(100, blank=True)
marktbegleiter FK(Marktbegleiter, CASCADE, related_name='passagen')
begruendung_zuordnung TextField()
verlaesslichkeitsscore PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)])
auswirkung_entscheidung TextField(blank=True)
auswirkung_preisstrategie TextField(blank=True)
auswirkung_loesungskonzept TextField(blank=True)
erfasst_von FK(Mitarbeiter, null=True, SET_NULL)
erfassungsdatum DateField(auto_now_add=True)
Admin für beide Modelle.
id: WP-0002-T12
title: Nachbetrachtung- und Feedback-Modelle
status: done
Datei: `vergabe_teilnahme/apps/nachbetrachtung/models.py`
`Nachbetrachtung(FlexibleModel)`:
- `ausschreibung` OneToOneField(Ausschreibung, CASCADE, related_name='nachbetrachtung')
- `ergebnis` CharField(20, choices=[gewonnen/verloren/aufgehoben/offen/zurueckgezogen])
- `zuschlagsdatum` DateField(null=True, blank=True)
- `abgegebene_unterlagen` TextField(blank=True)
- `abgegebene_preise` TextField(blank=True)
- `verlustgruende` JSONField(default=list)
# Format: [{"grund": "...", "kategorie": "preis|referenz|...", "verlaesslichkeit": 1-5}]
- `ausschlaggebende_zuschlagsmerkmale` TextField(blank=True)
- `lessons_learned` TextField(blank=True)
- `empfehlungen` TextField(blank=True)
- `wiederverwendbare_erkenntnisse_markiert` BooleanField(default=False)
- `projektverantwortlicher` FK(Mitarbeiter, null=True, blank=True, SET_NULL)
Datei: `vergabe_teilnahme/apps/feedback/models.py`
`Feedbackeintrag(FlexibleModel)` — alle Felder aus Blueprint 6.18:
titel, beschreibung TextField, seite_kontext CharField(500),
ausschreibung FK(Ausschreibung, null=True, blank=True, SET_NULL),
kategorie CharField(choices=[fehler/verbesserung/hinweis]),
dringlichkeit CharField(choices=[niedrig/mittel/hoch/kritisch], default='mittel'),
prioritaet PositiveSmallIntegerField(default=2),
status CharField(choices=[neu/in_bearbeitung/umgesetzt/abgelehnt], default='neu'),
erfasst_von FK(Mitarbeiter, null=True, SET_NULL),
datum DateTimeField(auto_now_add=True),
bewertung TextField(blank=True), entscheidung TextField(blank=True),
umsetzungshinweis TextField(blank=True)
Admin für beide Modelle.
id: WP-0002-T13
title: Alle Migrationen generieren und ausführen
status: done
Führe aus (in dieser Reihenfolge, da Apps voneinander abhängen):
```bash
uv run manage.py makemigrations accounts
uv run manage.py makemigrations core
uv run manage.py makemigrations ausschreibungen
uv run manage.py makemigrations lose
uv run manage.py makemigrations aufgaben
uv run manage.py makemigrations dokumente
uv run manage.py makemigrations preise
uv run manage.py makemigrations partner
uv run manage.py makemigrations bibliothek
uv run manage.py makemigrations marktbegleiter
uv run manage.py makemigrations nachbetrachtung
uv run manage.py makemigrations feedback
uv run manage.py migrate
Behebe etwaige Zirkulär-Import-Fehler durch String-Referenzen (z. B.
FK('ausschreibungen.Ausschreibung', ...) statt direktem Import).
Prüfe: uv run manage.py check → keine Fehler.
Prüfe: uv run manage.py showmigrations → alle Migrationen als [X] markiert.
```task
id: WP-0002-T14
title: Management-Command für Entwicklungs-Seed-Daten
status: done
Erstelle `vergabe_teilnahme/apps/core/management/commands/seed_dev.py`
Der Command erstellt:
1. 3 Mitarbeiter (BM Max Muster, FV Anna Fach, GF Georg Chef)
2. 2 Dienstleistertypen (IT-Dienstleister, Rechtsberatung)
3. 1 Subunternehmer mit Zuordnung zu Dienstleistertyp
4. 2 Nachweise (ISO-27001 gültig, DSGVO-Muster gültig)
5. 1 Marktbegleiter
6. 3 Entscheidungsregeln (Muss-Anforderung nicht erfüllbar, Frist zu kurz, fehlende Referenz)
7. 1 Ausschreibung (Status 4, mit 2 Losen, 3 Anforderungen, 2 Aufgaben, 2 Preispunkten)
Nutze `get_or_create` für alle Objekte damit der Command idempotent ist.
Ausgabe am Ende: Zusammenfassung der erstellten Objekte.
Prüfe: `uv run manage.py seed_dev` läuft fehlerfrei durch.
Prüfe anschließend: `uv run manage.py shell -c "from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung; print(Ausschreibung.objects.count())"` → mindestens 1.