Files
vergabe-teilnahme/workplans/WP-0002-fachmodelle.md
2026-05-08 14:26:48 +02:00

19 KiB
Raw Blame History

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.