Prototype implementation

This commit is contained in:
2026-05-08 14:26:48 +02:00
parent 315143a6fc
commit 14b0bc6d01
160 changed files with 5731 additions and 42 deletions

View File

View File

View File

@@ -0,0 +1,12 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import Mitarbeiter
@admin.register(Mitarbeiter)
class MitarbeiterAdmin(UserAdmin):
fieldsets = UserAdmin.fieldsets + (
('Firmen-Details', {'fields': ('rolle', 'mobilnummer', 'organisationseinheit')}),
)
list_display = ['username', 'get_full_name', 'rolle', 'organisationseinheit', 'is_active']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'vergabe_teilnahme.apps.accounts'

View File

@@ -0,0 +1,46 @@
# Generated by Django 6.0.5 on 2026-05-08 08:32
import django.contrib.auth.models
import django.contrib.auth.validators
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Mitarbeiter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('rolle', models.CharField(choices=[('ersteller', 'Ersteller'), ('kalkulator', 'Kalkulator'), ('jurist', 'Jurist'), ('freigeber', 'Freigeber'), ('beobachter', 'Beobachter'), ('admin', 'Admin'), ('geschaeftsfuehrung', 'Geschäftsführung'), ('projektleiter', 'Projektleiter')], default='ersteller', max_length=30)),
('mobilnummer', models.CharField(blank=True, max_length=30)),
('organisationseinheit', models.CharField(blank=True, max_length=100)),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Mitarbeiter',
'verbose_name_plural': 'Mitarbeiter',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,28 @@
# Generated by Django 6.0.5 on 2026-05-08 10:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='mitarbeiter',
name='mobilnummer',
field=models.CharField(blank=True, max_length=50),
),
migrations.AlterField(
model_name='mitarbeiter',
name='organisationseinheit',
field=models.CharField(blank=True, max_length=200),
),
migrations.AlterField(
model_name='mitarbeiter',
name='rolle',
field=models.CharField(blank=True, 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')], max_length=30),
),
]

View File

@@ -0,0 +1,25 @@
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 = models.CharField(max_length=30, choices=ROLLE_CHOICES, blank=True)
mobilnummer = models.CharField(max_length=50, blank=True)
organisationseinheit = models.CharField(max_length=200, blank=True)
def __str__(self):
return self.get_full_name() or self.username
class Meta:
verbose_name = 'Mitarbeiter'
verbose_name_plural = 'Mitarbeiter'

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,15 @@
from django.contrib import admin
from .models import Aufgabe, Bieterfrage
@admin.register(Aufgabe)
class AufgabeAdmin(admin.ModelAdmin):
list_display = ['titel', 'typ', 'status', 'prioritaet', 'frist', 'verantwortlicher']
list_filter = ['typ', 'status', 'prioritaet']
@admin.register(Bieterfrage)
class BieterfragAdmin(admin.ModelAdmin):
list_display = ['fragentext', 'status', 'prioritaet', 'einreichungsdatum', 'eingearbeitet']
list_filter = ['status', 'prioritaet', 'eingearbeitet']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AufgabenConfig(AppConfig):
name = 'vergabe_teilnahme.apps.aufgaben'

View File

@@ -0,0 +1,51 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Aufgabe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titel', models.CharField(max_length=400)),
('beschreibung', models.TextField(blank=True)),
('typ', models.CharField(choices=[('fachlich', 'Fachlich'), ('rechtlich', 'Rechtlich'), ('kaufmaennisch', 'Kaufmännisch'), ('technisch', 'Technisch'), ('subunternehmer', 'Subunternehmer'), ('dokument', 'Dokument'), ('preis', 'Preis')], default='fachlich', max_length=20)),
('status', models.CharField(choices=[('offen', 'Offen'), ('in_bearbeitung', 'In Bearbeitung'), ('wartend_intern', 'Wartend (intern)'), ('wartend_sub', 'Wartend (Subunternehmer)'), ('wartend_ausschreiber', 'Wartend (Ausschreiber)'), ('erledigt', 'Erledigt'), ('verworfen', 'Verworfen'), ('ueberfaellig', 'Überfällig')], default='offen', max_length=25)),
('prioritaet', models.PositiveSmallIntegerField(choices=[(1, 'Hoch'), (2, 'Mittel'), (3, 'Niedrig')], default=2)),
('frist', models.DateField(blank=True, null=True)),
('ergebnis', models.TextField(blank=True)),
],
options={
'verbose_name': 'Aufgabe',
'verbose_name_plural': 'Aufgaben',
'ordering': ['prioritaet', 'frist'],
},
),
migrations.CreateModel(
name='Bieterfrage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fragentext', models.TextField()),
('begruendung', models.TextField(blank=True)),
('status', models.CharField(choices=[('entwurf', 'Entwurf'), ('abgestimmt', 'Abgestimmt'), ('eingereicht', 'Eingereicht'), ('beantwortet', 'Beantwortet'), ('eingearbeitet', 'Eingearbeitet')], default='entwurf', max_length=20)),
('prioritaet', models.PositiveSmallIntegerField(choices=[(1, 'Hoch'), (2, 'Mittel'), (3, 'Niedrig')], default=2)),
('einreichungsdatum', models.DateField(blank=True, null=True)),
('antwort', models.TextField(blank=True)),
('auswirkung_angebot', models.TextField(blank=True)),
('eingearbeitet', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Bieterfrage',
'verbose_name_plural': 'Bieterfragen',
'ordering': ['prioritaet', 'status'],
},
),
]

View File

@@ -0,0 +1,71 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('aufgaben', '0001_initial'),
('ausschreibungen', '0001_initial'),
('dokumente', '0002_initial'),
('lose', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='aufgabe',
name='anforderung',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aufgaben', to='lose.anforderung'),
),
migrations.AddField(
model_name='aufgabe',
name='ausschreibung',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aufgaben', to='ausschreibungen.ausschreibung'),
),
migrations.AddField(
model_name='aufgabe',
name='dokument',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aufgaben', to='dokumente.dokument'),
),
migrations.AddField(
model_name='aufgabe',
name='los',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aufgaben', to='lose.los'),
),
migrations.AddField(
model_name='aufgabe',
name='verantwortlicher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='bieterfrage',
name='anforderung',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bieterfragen', to='lose.anforderung'),
),
migrations.AddField(
model_name='bieterfrage',
name='ausschreibung',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bieterfragen', to='ausschreibungen.ausschreibung'),
),
migrations.AddField(
model_name='bieterfrage',
name='dokument',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bieterfragen', to='dokumente.dokument'),
),
migrations.AddField(
model_name='bieterfrage',
name='verfasser',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='aufgabe',
name='bieterfrage',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='aufgaben', to='aufgaben.bieterfrage'),
),
]

View File

@@ -0,0 +1,113 @@
from datetime import date
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Aufgabe(FlexibleModel):
TYP_CHOICES = [
('fachlich', 'Fachlich'),
('rechtlich', 'Rechtlich'),
('kaufmaennisch', 'Kaufmännisch'),
('technisch', 'Technisch'),
('subunternehmer', 'Subunternehmer'),
('dokument', 'Dokument'),
('preis', 'Preis'),
]
STATUS_CHOICES = [
('offen', 'Offen'),
('in_bearbeitung', 'In Bearbeitung'),
('wartend_intern', 'Wartend (intern)'),
('wartend_sub', 'Wartend (Subunternehmer)'),
('wartend_ausschreiber', 'Wartend (Ausschreiber)'),
('erledigt', 'Erledigt'),
('verworfen', 'Verworfen'),
('ueberfaellig', 'Überfällig'),
]
PRIORITAET_CHOICES = [(1, 'Hoch'), (2, 'Mittel'), (3, 'Niedrig')]
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='aufgaben'
)
los = models.ForeignKey(
'lose.Los', on_delete=models.SET_NULL, null=True, blank=True, related_name='aufgaben'
)
anforderung = models.ForeignKey(
'lose.Anforderung', on_delete=models.SET_NULL,
null=True, blank=True, related_name='aufgaben'
)
bieterfrage = models.ForeignKey(
'Bieterfrage', on_delete=models.SET_NULL,
null=True, blank=True, related_name='aufgaben'
)
dokument = models.ForeignKey(
'dokumente.Dokument', on_delete=models.SET_NULL,
null=True, blank=True, related_name='aufgaben'
)
titel = models.CharField(max_length=400)
beschreibung = models.TextField(blank=True)
typ = models.CharField(max_length=20, choices=TYP_CHOICES, default='fachlich')
status = models.CharField(max_length=25, choices=STATUS_CHOICES, default='offen')
prioritaet = models.PositiveSmallIntegerField(choices=PRIORITAET_CHOICES, default=2)
frist = models.DateField(null=True, blank=True)
verantwortlicher = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
ergebnis = models.TextField(blank=True)
class Meta:
ordering = ['prioritaet', 'frist']
verbose_name = 'Aufgabe'
verbose_name_plural = 'Aufgaben'
def __str__(self):
return self.titel
@property
def ist_ueberfaellig(self):
if not self.frist:
return False
return self.frist < date.today() and self.status not in ['erledigt', 'verworfen']
class Bieterfrage(FlexibleModel):
STATUS_CHOICES = [
('entwurf', 'Entwurf'),
('abgestimmt', 'Abgestimmt'),
('eingereicht', 'Eingereicht'),
('beantwortet', 'Beantwortet'),
('eingearbeitet', 'Eingearbeitet'),
]
PRIORITAET_CHOICES = [(1, 'Hoch'), (2, 'Mittel'), (3, 'Niedrig')]
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='bieterfragen'
)
anforderung = models.ForeignKey(
'lose.Anforderung', on_delete=models.SET_NULL,
null=True, blank=True, related_name='bieterfragen'
)
dokument = models.ForeignKey(
'dokumente.Dokument', on_delete=models.SET_NULL,
null=True, blank=True, related_name='bieterfragen'
)
fragentext = models.TextField()
begruendung = models.TextField(blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='entwurf')
prioritaet = models.PositiveSmallIntegerField(choices=PRIORITAET_CHOICES, default=2)
einreichungsdatum = models.DateField(null=True, blank=True)
antwort = models.TextField(blank=True)
auswirkung_angebot = models.TextField(blank=True)
eingearbeitet = models.BooleanField(default=False)
verfasser = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
class Meta:
ordering = ['prioritaet', 'status']
verbose_name = 'Bieterfrage'
verbose_name_plural = 'Bieterfragen'
def __str__(self):
return self.fragentext[:80]

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,2 @@
from django.urls import path
urlpatterns = []

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,11 @@
from django.contrib import admin
from .models import Ausschreibung
@admin.register(Ausschreibung)
class AusschreibungAdmin(admin.ModelAdmin):
list_display = ['titel', 'ausschreiber', 'status', 'abgabe_bis', 'teilnahmeentscheidung']
list_filter = ['status', 'teilnahmeentscheidung', 'vergabeart']
search_fields = ['titel', 'ausschreiber', 'vergabenummer']
date_hierarchy = 'abgabe_bis'

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class AusschreibungenConfig(AppConfig):
name = 'vergabe_teilnahme.apps.ausschreibungen'

View File

@@ -0,0 +1,53 @@
# Generated by Django 6.0.5 on 2026-05-08 10:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Ausschreibung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titel', models.CharField(max_length=400)),
('ausschreiber', models.CharField(max_length=300)),
('vergabeplattform', models.CharField(blank=True, max_length=200)),
('vergabenummer', models.CharField(blank=True, max_length=100)),
('vergabeart', models.CharField(blank=True, choices=[('oeffentlich', 'Öffentliche Ausschreibung'), ('beschraenkt', 'Beschränkte Ausschreibung'), ('freihanding', 'Freihändige Vergabe'), ('wettbewerb', 'Wettbewerb'), ('rahmenvertrag', 'Rahmenvertrag'), ('sonstige', 'Sonstige')], max_length=30)),
('status', models.PositiveSmallIntegerField(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')], default=1)),
('teilnahmeentscheidung', models.CharField(choices=[('offen', 'Offen'), ('teilnahme', 'Teilnahme'), ('ablehnung', 'Ablehnung')], default='offen', max_length=20)),
('entscheidungsbegruendung', models.TextField(blank=True)),
('veroeffentlichungsdatum', models.DateField(blank=True, null=True)),
('bieterfragen_bis', models.DateField(blank=True, null=True)),
('abgabe_bis', models.DateTimeField(blank=True, null=True)),
('bindefrist', models.DateField(blank=True, null=True)),
('leistungsbeschreibung', models.TextField(blank=True)),
('branche', models.CharField(blank=True, max_length=200)),
('schlagwoerter', models.CharField(blank=True, max_length=500)),
('geschaetztes_volumen', models.DecimalField(blank=True, decimal_places=2, max_digits=14, null=True)),
('laufzeit', models.CharField(blank=True, max_length=100)),
('optionen', models.TextField(blank=True)),
('fundstelle_url', models.URLField(blank=True, max_length=1000)),
('unterlagen_erhalten', models.BooleanField(default=False)),
('unterlagen_erhalten_am', models.DateField(blank=True, null=True)),
('erstellt_am', models.DateTimeField(auto_now_add=True)),
('geaendert_am', models.DateTimeField(auto_now=True)),
('bid_manager', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='verwaltete_ausschreibungen', to=settings.AUTH_USER_MODEL)),
('team', models.ManyToManyField(blank=True, related_name='team_ausschreibungen', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Ausschreibung',
'verbose_name_plural': 'Ausschreibungen',
'ordering': ['-erstellt_am'],
},
),
]

View File

@@ -0,0 +1,107 @@
from datetime import date
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'),
]
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)
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)
# 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)
# 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)
# 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 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))

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,9 @@
from django.urls import path
from . import views
app_name = 'ausschreibungen'
urlpatterns = [
path('', views.dashboard, name='dashboard'),
]

View File

@@ -0,0 +1,7 @@
from django.shortcuts import render
def dashboard(request):
return render(request, 'ausschreibungen/dashboard.html', {
'breadcrumbs': [{'label': 'Übersicht', 'url': None}],
})

View File

@@ -0,0 +1,27 @@
from django.contrib import admin
from .models import Entscheidungsregel, Leistungsblatt, Nachweis, Referenz
@admin.register(Nachweis)
class NachweisAdmin(admin.ModelAdmin):
list_display = ['titel', 'kategorie', 'gueltig_bis', 'freigabestatus', 'ist_abgelaufen']
list_filter = ['kategorie', 'freigabestatus', 'fuer_oeffentliche']
@admin.register(Referenz)
class ReferenzAdmin(admin.ModelAdmin):
list_display = ['referenztitel', 'kunde', 'branche', 'freigabestatus_verwendung']
list_filter = ['freigabestatus_verwendung', 'verwendbar_fuer_ausschreibungen']
search_fields = ['referenztitel', 'kunde']
@admin.register(Leistungsblatt)
class LeistungsblattAdmin(admin.ModelAdmin):
list_display = ['produktfunktion', 'version', 'eigentuemer']
@admin.register(Entscheidungsregel)
class EntscheidungsregelAdmin(admin.ModelAdmin):
list_display = ['regelname', 'kategorie', 'empfehlung', 'aktiv']
list_filter = ['kategorie', 'empfehlung', 'aktiv']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class BibliothekConfig(AppConfig):
name = 'vergabe_teilnahme.apps.bibliothek'

View File

@@ -0,0 +1,115 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Entscheidungsregel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('regelname', models.CharField(max_length=300)),
('beschreibung', models.TextField(blank=True)),
('kategorie', models.CharField(choices=[('ausschlusskriterium', 'Ausschlusskriterium'), ('frist', 'Fristlage'), ('referenz', 'Referenzanforderung'), ('wirtschaftlichkeit', 'Wirtschaftlichkeit'), ('ressourcen', 'Ressourcenverfügbarkeit'), ('sonstiges', 'Sonstiges')], max_length=30)),
('gewichtung', models.DecimalField(decimal_places=2, default=1.0, max_digits=5)),
('bewertungslogik', models.TextField(blank=True)),
('schwellenwert', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
('empfehlung', models.CharField(choices=[('teilnehmen', 'Teilnehmen'), ('nicht_teilnehmen', 'Nicht teilnehmen'), ('pruefen', 'Prüfen')], max_length=20)),
('begruendung', models.TextField(blank=True)),
('gueltig_von', models.DateField(blank=True, null=True)),
('gueltig_bis', models.DateField(blank=True, null=True)),
('aktiv', models.BooleanField(default=True)),
('verantwortlicher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Entscheidungsregel',
'verbose_name_plural': 'Entscheidungsregeln',
'ordering': ['regelname'],
},
),
migrations.CreateModel(
name='Leistungsblatt',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('produktfunktion', models.CharField(max_length=300)),
('beschreibung', models.TextField(blank=True)),
('leistungsumfang', models.TextField(blank=True)),
('grenzen_ausschluesse', models.TextField(blank=True)),
('technische_voraussetzungen', models.TextField(blank=True)),
('typische_nachweise', models.TextField(blank=True)),
('version', models.CharField(default='1.0', max_length=20)),
('eigentuemer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Leistungsblatt',
'verbose_name_plural': 'Leistungsblätter',
'ordering': ['produktfunktion'],
},
),
migrations.CreateModel(
name='Nachweis',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titel', models.CharField(max_length=300)),
('kurzbeschreibung', models.TextField(blank=True)),
('dokumenttyp', models.CharField(blank=True, max_length=100)),
('kategorie', models.CharField(blank=True, max_length=100)),
('datei', models.FileField(blank=True, null=True, upload_to='nachweise/%Y/')),
('version', models.CharField(default='1.0', max_length=20)),
('status', models.CharField(default='aktiv', max_length=20)),
('gueltig_ab', models.DateField(blank=True, null=True)),
('gueltig_bis', models.DateField(blank=True, null=True)),
('freigabestatus', models.CharField(choices=[('intern_freigegeben', 'Intern freigegeben'), ('extern_freigegeben', 'Extern freigegeben'), ('eingeschraenkt', 'Eingeschränkt'), ('nicht_freigegeben', 'Nicht freigegeben')], default='intern_freigegeben', max_length=30)),
('letzte_pruefung', models.DateField(blank=True, null=True)),
('zugehoerige_standards', models.TextField(blank=True)),
('vertraulichkeit', models.CharField(choices=[('intern', 'Intern'), ('streng_vertraulich', 'Streng vertraulich'), ('oeffentlich', 'Öffentlich')], default='intern', max_length=20)),
('sprache', models.CharField(default='de', max_length=5)),
('fuer_oeffentliche', models.BooleanField(default=True)),
('fuer_privatwirtschaftliche', models.BooleanField(default=True)),
('eigentuemer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='eigene_nachweise', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nachweis',
'verbose_name_plural': 'Nachweise',
'ordering': ['titel'],
},
),
migrations.CreateModel(
name='Referenz',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('referenztitel', models.CharField(max_length=400)),
('kunde', models.CharField(max_length=300)),
('branche', models.CharField(blank=True, max_length=200)),
('oeffentlich_oder_privat', models.CharField(blank=True, max_length=20)),
('leistungsbeschreibung', models.TextField(blank=True)),
('eingesetzte_produkte', models.TextField(blank=True)),
('projektzeitraum', models.CharField(blank=True, max_length=100)),
('vertragsvolumen', models.DecimalField(blank=True, decimal_places=2, max_digits=14, null=True)),
('ansprechpartner_referenzkunde', models.CharField(blank=True, max_length=200)),
('freigabestatus_verwendung', models.CharField(choices=[('freigegeben', 'Freigegeben'), ('eingeschraenkt', 'Eingeschränkt'), ('nicht_freigegeben', 'Nicht freigegeben')], default='freigegeben', max_length=30)),
('vertraulichkeit', models.CharField(default='intern', max_length=20)),
('whitepaper', models.FileField(blank=True, null=True, upload_to='referenzen/')),
('kurzfassung', models.TextField(blank=True)),
('langfassung', models.TextField(blank=True)),
('verwendbar_fuer_ausschreibungen', models.BooleanField(default=True)),
('einschraenkungen_verwendung', models.TextField(blank=True)),
('leistungsblaetter', models.ManyToManyField(blank=True, to='bibliothek.leistungsblatt')),
],
options={
'verbose_name': 'Referenz',
'verbose_name_plural': 'Referenzen',
'ordering': ['referenztitel'],
},
),
]

View File

@@ -0,0 +1,154 @@
from datetime import date
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Leistungsblatt(FlexibleModel):
produktfunktion = models.CharField(max_length=300)
beschreibung = models.TextField(blank=True)
leistungsumfang = models.TextField(blank=True)
grenzen_ausschluesse = models.TextField(blank=True)
technische_voraussetzungen = models.TextField(blank=True)
typische_nachweise = models.TextField(blank=True)
version = models.CharField(max_length=20, default='1.0')
eigentuemer = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
class Meta:
verbose_name = 'Leistungsblatt'
verbose_name_plural = 'Leistungsblätter'
ordering = ['produktfunktion']
def __str__(self):
return self.produktfunktion
class Nachweis(FlexibleModel):
FREIGABE_CHOICES = [
('intern_freigegeben', 'Intern freigegeben'),
('extern_freigegeben', 'Extern freigegeben'),
('eingeschraenkt', 'Eingeschränkt'),
('nicht_freigegeben', 'Nicht freigegeben'),
]
VERTRAULICHKEIT_CHOICES = [
('intern', 'Intern'),
('streng_vertraulich', 'Streng vertraulich'),
('oeffentlich', 'Öffentlich'),
]
titel = models.CharField(max_length=300)
kurzbeschreibung = models.TextField(blank=True)
dokumenttyp = models.CharField(max_length=100, blank=True)
kategorie = models.CharField(max_length=100, blank=True)
datei = models.FileField(upload_to='nachweise/%Y/', null=True, blank=True)
version = models.CharField(max_length=20, default='1.0')
status = models.CharField(max_length=20, default='aktiv')
gueltig_ab = models.DateField(null=True, blank=True)
gueltig_bis = models.DateField(null=True, blank=True)
eigentuemer = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL,
null=True, blank=True, related_name='eigene_nachweise'
)
freigabestatus = models.CharField(
max_length=30, choices=FREIGABE_CHOICES, default='intern_freigegeben'
)
letzte_pruefung = models.DateField(null=True, blank=True)
zugehoerige_standards = models.TextField(blank=True)
vertraulichkeit = models.CharField(
max_length=20, choices=VERTRAULICHKEIT_CHOICES, default='intern'
)
sprache = models.CharField(max_length=5, default='de')
fuer_oeffentliche = models.BooleanField(default=True)
fuer_privatwirtschaftliche = models.BooleanField(default=True)
class Meta:
verbose_name = 'Nachweis'
verbose_name_plural = 'Nachweise'
ordering = ['titel']
def __str__(self):
return self.titel
@property
def ist_abgelaufen(self):
if not self.gueltig_bis:
return False
return self.gueltig_bis < date.today()
class Referenz(FlexibleModel):
FREIGABE_CHOICES = [
('freigegeben', 'Freigegeben'),
('eingeschraenkt', 'Eingeschränkt'),
('nicht_freigegeben', 'Nicht freigegeben'),
]
referenztitel = models.CharField(max_length=400)
kunde = models.CharField(max_length=300)
branche = models.CharField(max_length=200, blank=True)
oeffentlich_oder_privat = models.CharField(max_length=20, blank=True)
leistungsbeschreibung = models.TextField(blank=True)
eingesetzte_produkte = models.TextField(blank=True)
projektzeitraum = models.CharField(max_length=100, blank=True)
vertragsvolumen = models.DecimalField(max_digits=14, decimal_places=2, null=True, blank=True)
ansprechpartner_referenzkunde = models.CharField(max_length=200, blank=True)
freigabestatus_verwendung = models.CharField(
max_length=30, choices=FREIGABE_CHOICES, default='freigegeben'
)
vertraulichkeit = models.CharField(max_length=20, default='intern')
whitepaper = models.FileField(upload_to='referenzen/', null=True, blank=True)
kurzfassung = models.TextField(blank=True)
langfassung = models.TextField(blank=True)
verwendbar_fuer_ausschreibungen = models.BooleanField(default=True)
einschraenkungen_verwendung = models.TextField(blank=True)
leistungsblaetter = models.ManyToManyField(Leistungsblatt, blank=True)
class Meta:
verbose_name = 'Referenz'
verbose_name_plural = 'Referenzen'
ordering = ['referenztitel']
def __str__(self):
return self.referenztitel
class Entscheidungsregel(FlexibleModel):
KATEGORIE_CHOICES = [
('ausschlusskriterium', 'Ausschlusskriterium'),
('frist', 'Fristlage'),
('referenz', 'Referenzanforderung'),
('wirtschaftlichkeit', 'Wirtschaftlichkeit'),
('ressourcen', 'Ressourcenverfügbarkeit'),
('sonstiges', 'Sonstiges'),
]
EMPFEHLUNG_CHOICES = [
('teilnehmen', 'Teilnehmen'),
('nicht_teilnehmen', 'Nicht teilnehmen'),
('pruefen', 'Prüfen'),
]
regelname = models.CharField(max_length=300)
beschreibung = models.TextField(blank=True)
kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES)
gewichtung = models.DecimalField(max_digits=5, decimal_places=2, default=1.0)
bewertungslogik = models.TextField(blank=True)
schwellenwert = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
empfehlung = models.CharField(max_length=20, choices=EMPFEHLUNG_CHOICES)
begruendung = models.TextField(blank=True)
gueltig_von = models.DateField(null=True, blank=True)
gueltig_bis = models.DateField(null=True, blank=True)
aktiv = models.BooleanField(default=True)
verantwortlicher = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
class Meta:
verbose_name = 'Entscheidungsregel'
verbose_name_plural = 'Entscheidungsregeln'
ordering = ['regelname']
def __str__(self):
return self.regelname

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,2 @@
from django.urls import path
urlpatterns = []

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

View File

@@ -0,0 +1,22 @@
from django.contrib import admin
from .models import CustomAttribute, EntityFieldConfig, Freigabe
@admin.register(EntityFieldConfig)
class EntityFieldConfigAdmin(admin.ModelAdmin):
list_display = ['entity_type', 'field_name', 'is_hidden', 'display_label', 'sort_order']
list_filter = ['entity_type', 'is_hidden']
search_fields = ['entity_type', 'field_name']
@admin.register(CustomAttribute)
class CustomAttributeAdmin(admin.ModelAdmin):
list_display = ['label', 'key', 'data_type', 'value', 'content_type', 'object_id']
list_filter = ['data_type', 'content_type']
@admin.register(Freigabe)
class FreigabeAdmin(admin.ModelAdmin):
list_display = ['freigabe_typ', 'status', 'freigebende_person', 'timestamp']
list_filter = ['freigabe_typ', 'status']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
name = 'vergabe_teilnahme.apps.core'

View File

@@ -0,0 +1,13 @@
def vergabe_context(request):
context = {}
ausschreibung_id = None
if hasattr(request, 'resolver_match') and request.resolver_match:
kwargs = request.resolver_match.kwargs
ausschreibung_id = kwargs.get('ausschreibung_id') or kwargs.get('pk')
if ausschreibung_id:
try:
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
context['current_ausschreibung'] = Ausschreibung.objects.get(pk=ausschreibung_id)
except (Ausschreibung.DoesNotExist, ValueError):
pass
return context

View File

@@ -0,0 +1,239 @@
from datetime import date, timedelta
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Erstellt Entwicklungs-Seed-Daten (idempotent)'
def handle(self, *args, **options):
from vergabe_teilnahme.apps.accounts.models import Mitarbeiter
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
from vergabe_teilnahme.apps.aufgaben.models import Aufgabe
from vergabe_teilnahme.apps.bibliothek.models import Entscheidungsregel, Nachweis
from vergabe_teilnahme.apps.lose.models import Anforderung, Los
from vergabe_teilnahme.apps.marktbegleiter.models import Marktbegleiter
from vergabe_teilnahme.apps.partner.models import Dienstleistertyp, Subunternehmer
from vergabe_teilnahme.apps.preise.models import Preispunkt
self.stdout.write('Erstelle Seed-Daten...')
# 1. Mitarbeiter
bm, _ = Mitarbeiter.objects.get_or_create(
username='max.muster',
defaults={
'first_name': 'Max', 'last_name': 'Muster',
'rolle': 'bid_manager', 'organisationseinheit': 'Vertrieb',
'email': 'max.muster@example.com',
}
)
bm.set_password('testpass123')
bm.save(update_fields=['password'])
fach, _ = Mitarbeiter.objects.get_or_create(
username='anna.fach',
defaults={
'first_name': 'Anna', 'last_name': 'Fach',
'rolle': 'fachexperte', 'organisationseinheit': 'Technik',
'email': 'anna.fach@example.com',
}
)
fach.set_password('testpass123')
fach.save(update_fields=['password'])
gf, _ = Mitarbeiter.objects.get_or_create(
username='georg.chef',
defaults={
'first_name': 'Georg', 'last_name': 'Chef',
'rolle': 'geschaeftsfuehrung', 'organisationseinheit': 'GF',
'email': 'georg.chef@example.com', 'is_staff': True,
}
)
gf.set_password('testpass123')
gf.save(update_fields=['password'])
self.stdout.write(f' ✓ 3 Mitarbeiter')
# 2. Dienstleistertypen
dt_it, _ = Dienstleistertyp.objects.get_or_create(
name='IT-Dienstleister',
defaults={'beschreibung': 'Software- und IT-Infrastruktur-Dienstleister'}
)
dt_recht, _ = Dienstleistertyp.objects.get_or_create(
name='Rechtsberatung',
defaults={'beschreibung': 'Anwaltskanzleien und Rechtsberatungsunternehmen'}
)
self.stdout.write(f' ✓ 2 Dienstleistertypen')
# 3. Subunternehmer
sub, _ = Subunternehmer.objects.get_or_create(
name='TechPartner GmbH',
defaults={
'dienstleistertyp': dt_it,
'praeferenz': 'bevorzugt',
'ort': 'München',
'email': 'kontakt@techpartner.example.com',
'leistungsprofil': 'Cloud-Infrastruktur, DevOps, Security',
}
)
self.stdout.write(f' ✓ 1 Subunternehmer')
# 4. Nachweise
nachweis_iso, _ = Nachweis.objects.get_or_create(
titel='ISO 27001 Zertifikat',
defaults={
'kategorie': 'Zertifizierung',
'version': '2.0',
'gueltig_ab': date.today() - timedelta(days=365),
'gueltig_bis': date.today() + timedelta(days=365),
'eigentuemer': bm,
'freigabestatus': 'intern_freigegeben',
}
)
nachweis_dsgvo, _ = Nachweis.objects.get_or_create(
titel='DSGVO-Datenschutzerklärung',
defaults={
'kategorie': 'Rechtlich',
'version': '1.2',
'gueltig_bis': date.today() + timedelta(days=730),
'eigentuemer': bm,
'freigabestatus': 'intern_freigegeben',
}
)
self.stdout.write(f' ✓ 2 Nachweise')
# 5. Marktbegleiter
mb, _ = Marktbegleiter.objects.get_or_create(
name='Konkurrent AG',
defaults={
'kurzbeschreibung': 'Größter Wettbewerber im IT-Bereich',
'relevante_branchen': 'IT, Behörden, Finanzwesen',
'bekannte_staerken': 'Große Kundenbasis, günstige Preise',
'bekannte_schwaechen': 'Langsamer Support, wenig Flexibilität',
}
)
self.stdout.write(f' ✓ 1 Marktbegleiter')
# 6. Entscheidungsregeln
Entscheidungsregel.objects.get_or_create(
regelname='Muss-Anforderung nicht erfüllbar',
defaults={
'kategorie': 'ausschlusskriterium',
'empfehlung': 'nicht_teilnehmen',
'begruendung': 'Falls eine Muss-Anforderung nicht erfüllt werden kann.',
'aktiv': True,
}
)
Entscheidungsregel.objects.get_or_create(
regelname='Abgabefrist zu kurz',
defaults={
'kategorie': 'frist',
'empfehlung': 'pruefen',
'begruendung': 'Weniger als 2 Wochen bis zur Abgabe.',
'aktiv': True,
}
)
Entscheidungsregel.objects.get_or_create(
regelname='Fehlende Referenz',
defaults={
'kategorie': 'referenz',
'empfehlung': 'nicht_teilnehmen',
'begruendung': 'Keine passende Referenz für die geforderte Branche vorhanden.',
'aktiv': True,
}
)
self.stdout.write(f' ✓ 3 Entscheidungsregeln')
# 7. Ausschreibung mit Losen, Anforderungen, Aufgaben, Preispunkten
ausschreibung, _ = Ausschreibung.objects.get_or_create(
vergabenummer='DEV-2026-001',
defaults={
'titel': 'Digitalisierung Verwaltungsportal Phase 2',
'ausschreiber': 'Beispiel-Behörde GmbH',
'vergabeplattform': 'DTVP',
'status': 4,
'bieterfragen_bis': date.today() + timedelta(days=5),
'abgabe_bis': None,
'bid_manager': bm,
'branche': 'Öffentliche Verwaltung',
'leistungsbeschreibung': 'Entwicklung und Betrieb eines digitalen Verwaltungsportals.',
}
)
los1, _ = Los.objects.get_or_create(
ausschreibung=ausschreibung, losnummer='1',
defaults={'lostitel': 'Softwareentwicklung', 'zustaendiger': fach}
)
los2, _ = Los.objects.get_or_create(
ausschreibung=ausschreibung, losnummer='2',
defaults={'lostitel': 'Hosting & Betrieb', 'zustaendiger': bm}
)
anf1, _ = Anforderung.objects.get_or_create(
ausschreibung=ausschreibung,
titel='ISO 27001 Zertifizierung nachweisen',
defaults={
'verbindlichkeit': 'muss', 'ausschlusskriterium': True,
'kategorie': 'zertifizierung', 'los': los1,
}
)
anf2, _ = Anforderung.objects.get_or_create(
ausschreibung=ausschreibung,
titel='Mindestens 3 Referenzprojekte in der öffentlichen Verwaltung',
defaults={
'verbindlichkeit': 'muss', 'kategorie': 'referenz', 'los': los1,
}
)
anf3, _ = Anforderung.objects.get_or_create(
ausschreibung=ausschreibung,
titel='SLA 99,5% Verfügbarkeit',
defaults={
'verbindlichkeit': 'soll', 'kategorie': 'technisch', 'los': los2,
}
)
Aufgabe.objects.get_or_create(
ausschreibung=ausschreibung,
titel='ISO-Zertifikat aktualisieren und einreichen',
defaults={
'typ': 'dokument', 'prioritaet': 1,
'verantwortlicher': bm, 'anforderung': anf1,
}
)
Aufgabe.objects.get_or_create(
ausschreibung=ausschreibung,
titel='Referenzliste zusammenstellen',
defaults={
'typ': 'fachlich', 'prioritaet': 2,
'verantwortlicher': fach, 'anforderung': anf2,
}
)
from decimal import Decimal
Preispunkt.objects.get_or_create(
ausschreibung=ausschreibung,
konkrete_leistung='Entwicklung Frontend',
defaults={
'leistungstyp': 'Entwicklung',
'menge': Decimal('1'),
'mengeneinheit': 'Pauschal',
'einzelpreis': Decimal('85000.00'),
'los': los1,
}
)
Preispunkt.objects.get_or_create(
ausschreibung=ausschreibung,
konkrete_leistung='Hosting p.a.',
defaults={
'leistungstyp': 'Betrieb',
'menge': Decimal('1'),
'mengeneinheit': 'Jahr',
'einzelpreis': Decimal('24000.00'),
'wiederkehrend': True,
'los': los2,
'vergleichsgewicht': Decimal('0.8'),
}
)
self.stdout.write(f' ✓ 1 Ausschreibung mit 2 Losen, 3 Anforderungen, 2 Aufgaben, 2 Preispunkten')
self.stdout.write(self.style.SUCCESS('\nSeed-Daten erfolgreich erstellt.'))

View File

@@ -0,0 +1,70 @@
# Generated by Django 6.0.5 on 2026-05-08 10:32
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='EntityFieldConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('entity_type', models.CharField(max_length=100)),
('field_name', models.CharField(max_length=100)),
('is_hidden', models.BooleanField(default=False)),
('display_label', models.CharField(blank=True, max_length=200)),
('sort_order', models.PositiveSmallIntegerField(default=0)),
],
options={
'verbose_name': 'Feldkonfiguration',
'verbose_name_plural': 'Feldkonfigurationen',
'unique_together': {('entity_type', 'field_name')},
},
),
migrations.CreateModel(
name='Freigabe',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('freigabe_typ', models.CharField(choices=[('teilnahme', 'Teilnahme-Freigabe'), ('ablehnung', 'Ablehnungs-Freigabe'), ('recht', 'Rechtliche Freigabe'), ('preis', 'Preis-Freigabe'), ('abgabe', 'Abgabe-Freigabe'), ('standarddokument', 'Standarddokument-Freigabe'), ('referenz', 'Referenz-Freigabe')], max_length=30)),
('status', models.CharField(choices=[('erteilt', 'Erteilt'), ('abgelehnt', 'Abgelehnt'), ('ausstehend', 'Ausstehend')], default='erteilt', max_length=20)),
('kommentar', models.TextField(blank=True)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('freigebende_person', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='erteilte_freigaben', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Freigabe',
'verbose_name_plural': 'Freigaben',
'ordering': ['-timestamp'],
},
),
migrations.CreateModel(
name='CustomAttribute',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('key', models.CharField(max_length=100)),
('label', models.CharField(max_length=200)),
('value', models.TextField(blank=True)),
('data_type', models.CharField(choices=[('text', 'Text'), ('number', 'Zahl'), ('date', 'Datum'), ('boolean', 'Ja/Nein'), ('url', 'URL'), ('email', 'E-Mail')], default='text', max_length=20)),
('sort_order', models.PositiveSmallIntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
options={
'ordering': ['sort_order', 'created_at'],
'indexes': [models.Index(fields=['content_type', 'object_id'], name='core_custom_content_b275a1_idx')],
},
),
]

View File

@@ -0,0 +1,106 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
class EntityFieldConfig(models.Model):
entity_type = models.CharField(max_length=100)
field_name = models.CharField(max_length=100)
is_hidden = models.BooleanField(default=False)
display_label = models.CharField(max_length=200, blank=True)
sort_order = models.PositiveSmallIntegerField(default=0)
class Meta:
unique_together = ('entity_type', 'field_name')
verbose_name = 'Feldkonfiguration'
verbose_name_plural = 'Feldkonfigurationen'
def __str__(self):
return f'{self.entity_type}.{self.field_name}'
class CustomAttribute(models.Model):
DATA_TYPE_CHOICES = [
('text', 'Text'),
('number', 'Zahl'),
('date', 'Datum'),
('boolean', 'Ja/Nein'),
('url', 'URL'),
('email', 'E-Mail'),
]
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
key = models.CharField(max_length=100)
label = models.CharField(max_length=200)
value = models.TextField(blank=True)
data_type = models.CharField(max_length=20, choices=DATA_TYPE_CHOICES, default='text')
sort_order = models.PositiveSmallIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['sort_order', 'created_at']
indexes = [models.Index(fields=['content_type', 'object_id'])]
def __str__(self):
return f'{self.label}: {self.value}'
class Freigabe(models.Model):
TYP_CHOICES = [
('teilnahme', 'Teilnahme-Freigabe'),
('ablehnung', 'Ablehnungs-Freigabe'),
('recht', 'Rechtliche Freigabe'),
('preis', 'Preis-Freigabe'),
('abgabe', 'Abgabe-Freigabe'),
('standarddokument', 'Standarddokument-Freigabe'),
('referenz', 'Referenz-Freigabe'),
]
STATUS_CHOICES = [
('erteilt', 'Erteilt'),
('abgelehnt', 'Abgelehnt'),
('ausstehend', 'Ausstehend'),
]
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
freigabe_typ = models.CharField(max_length=30, choices=TYP_CHOICES)
freigebende_person = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='erteilte_freigaben'
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='erteilt')
kommentar = models.TextField(blank=True)
timestamp = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-timestamp']
verbose_name = 'Freigabe'
verbose_name_plural = 'Freigaben'
def __str__(self):
return f'{self.get_freigabe_typ_display()}{self.status}'
class FlexibleModel(models.Model):
custom_attributes = GenericRelation(CustomAttribute)
freigaben = GenericRelation(Freigabe)
def get_hidden_fields(self):
model_name = self._meta.model_name
return set(
EntityFieldConfig.objects.filter(
entity_type=model_name, is_hidden=True
).values_list('field_name', flat=True)
)
def get_visible_field_names(self):
hidden = self.get_hidden_fields()
return [
f.name
for f in self._meta.get_fields()
if hasattr(f, 'column') and not f.name.startswith('_') and f.name not in hidden
]
class Meta:
abstract = True

View File

@@ -0,0 +1,106 @@
from datetime import date
from decimal import Decimal
PHASEN = [
(1, 'Recherche & Unterlagen'),
(2, 'Teilnahmeentscheidung'),
(3, 'Detaillierte Durchsicht'),
(4, 'Bieterfragen & Klärung'),
(5, 'Preismodell'),
(6, 'Unterlagen finalisieren'),
(7, 'Abgabe'),
(8, 'Zuschlag / Nachbetrachtung'),
]
# Maps Ausschreibung.status integer to phase number
STATUS_TO_PHASE = {
1: 1, 2: 1, # Recherche
3: 2, # Teilnahmeentscheidung
4: 3, 5: 3, # Durchsicht
6: 4, 7: 4, # Bieterfragen
8: 5, # Preise
9: 6, # Finalisierung
10: 7, 11: 7, # Abgabe
12: 8, 13: 8, # Nachbetrachtung
}
def build_phase_nav(ausschreibung, current_url=''):
aktuelle_phase = STATUS_TO_PHASE.get(ausschreibung.status, 1)
base = f'/ausschreibungen/{ausschreibung.pk}'
phase_urls = {
1: f'{base}/',
2: f'{base}/teilnahmeentscheidung/',
3: f'{base}/anforderungen/',
4: f'{base}/bieterfragen/',
5: f'{base}/preise/',
6: f'{base}/dokumente/',
7: f'{base}/abgabe/',
8: f'{base}/nachbetrachtung/',
}
return [
{
'nummer': num,
'name': name,
'url': phase_urls[num],
'aktiv': num == aktuelle_phase,
'erledigt': num < aktuelle_phase,
'warnung': False,
}
for num, name in PHASEN
]
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:
abgabe_date = (
ausschreibung.abgabe_bis.date()
if hasattr(ausschreibung.abgabe_bis, 'date')
else ausschreibung.abgabe_bis
)
delta = (abgabe_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.
Punkte mit Gewicht 0,0 werden ausgeschlossen.
Gibt None zurück wenn keine verwertbaren Punkte vorhanden.
"""
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),
}

View File

@@ -0,0 +1,71 @@
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
STATUS_COLORS = {
'offen': 'bg-slate-100 text-slate-700',
'in_bearbeitung': 'bg-blue-100 text-blue-700',
'erledigt': 'bg-green-100 text-green-700',
'freigegeben': 'bg-green-100 text-green-700',
'erteilt': 'bg-green-100 text-green-700',
'gewonnen': 'bg-green-100 text-green-700',
'intern_freigegeben': 'bg-green-100 text-green-700',
'ueberfaellig': 'bg-red-100 text-red-700',
'nicht_erfuellbar': 'bg-red-100 text-red-700',
'verloren': 'bg-red-100 text-red-700',
'abgelehnt': 'bg-red-100 text-red-700',
'ausstehend': 'bg-amber-100 text-amber-700',
'in_pruefung': 'bg-amber-100 text-amber-700',
'wartend_intern': 'bg-amber-100 text-amber-700',
'wartend_sub': 'bg-amber-100 text-amber-700',
'wartend_ausschreiber': 'bg-amber-100 text-amber-700',
'archiviert': 'bg-gray-100 text-gray-500',
'ersetzt': 'bg-gray-100 text-gray-500',
'verworfen': 'bg-gray-100 text-gray-500',
}
@register.inclusion_tag('partials/status_badge.html')
def status_badge(value, display_label=None):
css = STATUS_COLORS.get(str(value), 'bg-slate-100 text-slate-700')
label = display_label or str(value).replace('_', ' ').capitalize()
return {'css': css, 'label': label}
@register.simple_tag
def phase_badge(nummer, zustand='todo'):
css_map = {
'todo': 'phase-todo',
'active': 'phase-active',
'done': 'phase-done',
'warn': 'phase-warn',
}
css = css_map.get(zustand, 'phase-todo')
return mark_safe(f'<span class="{css}">{nummer}</span>')
def _is_field_hidden(entity_type, field_name):
from vergabe_teilnahme.apps.core.models import EntityFieldConfig
return EntityFieldConfig.objects.filter(
entity_type=entity_type, field_name=field_name, is_hidden=True
).exists()
def _get_field_label(entity_type, field_name, default_label):
from vergabe_teilnahme.apps.core.models import EntityFieldConfig
try:
cfg = EntityFieldConfig.objects.get(entity_type=entity_type, field_name=field_name)
return cfg.display_label or default_label
except EntityFieldConfig.DoesNotExist:
return default_label
@register.inclusion_tag('partials/field_row.html')
def render_field(obj, field_name, label=None, force_show=False):
entity_type = obj._meta.model_name
if not force_show and _is_field_hidden(entity_type, field_name):
return {'hidden': True}
value = getattr(obj, field_name, None)
display_label = _get_field_label(entity_type, field_name, label or field_name)
return {'hidden': False, 'label': display_label, 'value': value, 'field_name': field_name}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,41 @@
from decimal import Decimal
import pytest
from vergabe_teilnahme.apps.core.services import gewichteter_durchschnitt
class FakePreispunkt:
def __init__(self, einzelpreis, vergleichsgewicht):
self.einzelpreis = Decimal(str(einzelpreis)) if einzelpreis is not None else None
self.vergleichsgewicht = Decimal(str(vergleichsgewicht))
class TestGewichteterDurchschnitt:
def test_blueprint_beispiel(self):
# 100×1,0 + 80×0,2 + 110×1,2 = 100+16+132=248 / (1,0+0,2+1,2)=2,4 = 103,333...
punkte = [
FakePreispunkt(100, '1.0'),
FakePreispunkt(80, '0.2'),
FakePreispunkt(110, '1.2'),
]
result = gewichteter_durchschnitt(punkte)
assert result is not None
assert abs(result['wert'] - Decimal('103.333')) < Decimal('0.001')
assert result['anzahl'] == 3
assert result['minimum'] == Decimal('80')
assert result['maximum'] == Decimal('110')
def test_leerer_input(self):
assert gewichteter_durchschnitt([]) is None
def test_alle_gewichte_null(self):
punkte = [FakePreispunkt(100, '0.0'), FakePreispunkt(200, '0.0')]
assert gewichteter_durchschnitt(punkte) is None
def test_einzelpreis_none_wird_ausgeschlossen(self):
punkte = [FakePreispunkt(None, '1.0'), FakePreispunkt(100, '1.0')]
result = gewichteter_durchschnitt(punkte)
assert result is not None
assert result['anzahl'] == 1
assert result['wert'] == Decimal('100')

View File

@@ -0,0 +1,14 @@
def make_breadcrumbs(*args):
"""Build a breadcrumbs list from alternating label/url pairs or (label, url) tuples.
Usage:
make_breadcrumbs(('Ausschreibungen', '/ausschreibungen/'), ('Mein Tender', None))
"""
crumbs = []
for item in args:
if isinstance(item, (list, tuple)) and len(item) == 2:
label, url = item
else:
label, url = item, None
crumbs.append({'label': label, 'url': url})
return crumbs

View File

@@ -0,0 +1,29 @@
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.http import require_GET
def custom_404(request, exception=None):
return render(request, 'errors/404.html', status=404)
def custom_500(request):
return render(request, 'errors/500.html', status=500)
@require_GET
def suche(request):
q = request.GET.get('q', '').strip()
if not q or len(q) < 2:
return HttpResponse('')
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
ausschreibungen = Ausschreibung.objects.filter(titel__icontains=q)[:5]
if not ausschreibungen:
return HttpResponse('')
items = ''.join(
f'<a href="/ausschreibungen/{a.pk}/" '
f'class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">'
f'{a.titel}</a>'
for a in ausschreibungen
)
return HttpResponse(items)

View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Dokument
@admin.register(Dokument)
class DokumentAdmin(admin.ModelAdmin):
list_display = ['dateiname', 'kategorie', 'status', 'version', 'upload_datum', 'finale_abgabeversion']
list_filter = ['kategorie', 'status', 'finale_abgabeversion']
search_fields = ['dateiname', 'beschreibung']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class DokumenteConfig(AppConfig):
name = 'vergabe_teilnahme.apps.dokumente'

View File

@@ -0,0 +1,36 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ausschreibungen', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Dokument',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('datei', models.FileField(upload_to='dokumente/%Y/%m/')),
('dateiname', models.CharField(blank=True, max_length=300)),
('kategorie', models.CharField(choices=[('angebotsunterlage', 'Angebotsunterlage'), ('leistungsbeschreibung', 'Leistungsbeschreibung'), ('preisblatt', 'Preisblatt'), ('referenz', 'Referenz'), ('nachweis', 'Nachweis'), ('abgabenachweis', 'Abgabenachweis'), ('kommunikation', 'Kommunikation'), ('intern', 'Intern'), ('sonstiges', 'Sonstiges')], default='sonstiges', max_length=30)),
('status', models.CharField(choices=[('entwurf', 'Entwurf'), ('in_pruefung', 'In Prüfung'), ('freigegeben', 'Freigegeben'), ('final_abgegeben', 'Final abgegeben'), ('ersetzt', 'Ersetzt'), ('archiviert', 'Archiviert')], default='entwurf', max_length=20)),
('version', models.CharField(default='1.0', max_length=20)),
('beschreibung', models.TextField(blank=True)),
('finale_abgabeversion', models.BooleanField(default=False)),
('upload_datum', models.DateTimeField(auto_now_add=True)),
('ausschreibung', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dokumente', to='ausschreibungen.ausschreibung')),
],
options={
'verbose_name': 'Dokument',
'verbose_name_plural': 'Dokumente',
'ordering': ['-upload_datum'],
},
),
]

View File

@@ -0,0 +1,34 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('dokumente', '0001_initial'),
('lose', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='dokument',
name='los',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dokumente', to='lose.los'),
),
migrations.AddField(
model_name='dokument',
name='pruefer',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='zu_pruefende_dokumente', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='dokument',
name='verantwortlicher',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='verantwortete_dokumente', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,97 @@
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.signals import pre_save
from django.dispatch import receiver
from vergabe_teilnahme.apps.core.models import FlexibleModel, Freigabe
ALLOWED_EXTENSIONS = {'.pdf', '.docx', '.xlsx', '.zip', '.png', '.jpg', '.jpeg'}
class Dokument(FlexibleModel):
STATUS_CHOICES = [
('entwurf', 'Entwurf'),
('in_pruefung', 'In Prüfung'),
('freigegeben', 'Freigegeben'),
('final_abgegeben', 'Final abgegeben'),
('ersetzt', 'Ersetzt'),
('archiviert', 'Archiviert'),
]
KATEGORIE_CHOICES = [
('angebotsunterlage', 'Angebotsunterlage'),
('leistungsbeschreibung', 'Leistungsbeschreibung'),
('preisblatt', 'Preisblatt'),
('referenz', 'Referenz'),
('nachweis', 'Nachweis'),
('abgabenachweis', 'Abgabenachweis'),
('kommunikation', 'Kommunikation'),
('intern', 'Intern'),
('sonstiges', 'Sonstiges'),
]
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.SET_NULL,
null=True, blank=True, related_name='dokumente'
)
los = models.ForeignKey(
'lose.Los', on_delete=models.SET_NULL,
null=True, blank=True, related_name='dokumente'
)
datei = models.FileField(upload_to='dokumente/%Y/%m/')
dateiname = models.CharField(max_length=300, blank=True)
kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES, default='sonstiges')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='entwurf')
version = models.CharField(max_length=20, default='1.0')
beschreibung = models.TextField(blank=True)
verantwortlicher = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL,
null=True, blank=True, related_name='verantwortete_dokumente'
)
pruefer = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL,
null=True, blank=True, related_name='zu_pruefende_dokumente'
)
finale_abgabeversion = models.BooleanField(default=False)
upload_datum = models.DateTimeField(auto_now_add=True)
freigaben_rel = GenericRelation(Freigabe)
class Meta:
ordering = ['-upload_datum']
verbose_name = 'Dokument'
verbose_name_plural = 'Dokumente'
def __str__(self):
return self.dateiname or self.datei.name
def clean(self):
if self.datei:
import os
ext = os.path.splitext(self.datei.name)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValidationError(
f'Dateityp "{ext}" nicht erlaubt. Erlaubt: {", ".join(ALLOWED_EXTENSIONS)}'
)
max_size = getattr(settings, 'MAX_UPLOAD_SIZE', 52428800)
if hasattr(self.datei, 'size') and self.datei.size > max_size:
raise ValidationError(
f'Datei zu groß. Maximum: {max_size // 1024 // 1024} MB'
)
def save(self, *args, **kwargs):
if self.datei and not self.dateiname:
import os
self.dateiname = os.path.basename(self.datei.name)
super().save(*args, **kwargs)
@receiver(pre_save, sender=Dokument)
def set_final_status(sender, instance, **kwargs):
if instance.pk:
try:
old = Dokument.objects.get(pk=instance.pk)
if not old.finale_abgabeversion and instance.finale_abgabeversion:
instance.status = 'final_abgegeben'
except Dokument.DoesNotExist:
pass

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,2 @@
from django.urls import path
urlpatterns = []

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import Feedbackeintrag
@admin.register(Feedbackeintrag)
class FeedbackeintragAdmin(admin.ModelAdmin):
list_display = ['titel', 'kategorie', 'dringlichkeit', 'status', 'datum', 'erfasst_von']
list_filter = ['kategorie', 'dringlichkeit', 'status']
search_fields = ['titel', 'beschreibung']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class FeedbackConfig(AppConfig):
name = 'vergabe_teilnahme.apps.feedback'

View File

@@ -0,0 +1,42 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ausschreibungen', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Feedbackeintrag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titel', models.CharField(default='Ohne Titel', max_length=300)),
('beschreibung', models.TextField()),
('seite_kontext', models.CharField(blank=True, max_length=500)),
('kategorie', models.CharField(choices=[('fehler', 'Fehler'), ('verbesserung', 'Verbesserungsvorschlag'), ('hinweis', 'Hinweis')], default='hinweis', max_length=20)),
('dringlichkeit', models.CharField(choices=[('niedrig', 'Niedrig'), ('mittel', 'Mittel'), ('hoch', 'Hoch'), ('kritisch', 'Kritisch')], default='mittel', max_length=10)),
('prioritaet', models.PositiveSmallIntegerField(default=2)),
('status', models.CharField(choices=[('neu', 'Neu'), ('in_bearbeitung', 'In Bearbeitung'), ('umgesetzt', 'Umgesetzt'), ('abgelehnt', 'Abgelehnt')], default='neu', max_length=20)),
('datum', models.DateTimeField(auto_now_add=True)),
('bewertung', models.TextField(blank=True)),
('entscheidung', models.TextField(blank=True)),
('umsetzungshinweis', models.TextField(blank=True)),
('ausschreibung', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feedback', to='ausschreibungen.ausschreibung')),
('erfasst_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Feedbackeintrag',
'verbose_name_plural': 'Feedbackeinträge',
'ordering': ['-datum'],
},
),
]

View File

@@ -0,0 +1,50 @@
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Feedbackeintrag(FlexibleModel):
KATEGORIE_CHOICES = [
('fehler', 'Fehler'),
('verbesserung', 'Verbesserungsvorschlag'),
('hinweis', 'Hinweis'),
]
DRINGLICHKEIT_CHOICES = [
('niedrig', 'Niedrig'),
('mittel', 'Mittel'),
('hoch', 'Hoch'),
('kritisch', 'Kritisch'),
]
STATUS_CHOICES = [
('neu', 'Neu'),
('in_bearbeitung', 'In Bearbeitung'),
('umgesetzt', 'Umgesetzt'),
('abgelehnt', 'Abgelehnt'),
]
titel = models.CharField(max_length=300, default='Ohne Titel')
beschreibung = models.TextField()
seite_kontext = models.CharField(max_length=500, blank=True)
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.SET_NULL,
null=True, blank=True, related_name='feedback'
)
kategorie = models.CharField(max_length=20, choices=KATEGORIE_CHOICES, default='hinweis')
dringlichkeit = models.CharField(max_length=10, choices=DRINGLICHKEIT_CHOICES, default='mittel')
prioritaet = models.PositiveSmallIntegerField(default=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='neu')
erfasst_von = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
datum = models.DateTimeField(auto_now_add=True)
bewertung = models.TextField(blank=True)
entscheidung = models.TextField(blank=True)
umsetzungshinweis = models.TextField(blank=True)
class Meta:
verbose_name = 'Feedbackeintrag'
verbose_name_plural = 'Feedbackeinträge'
ordering = ['-datum']
def __str__(self):
return self.titel

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,10 @@
from django.urls import path
from . import views
app_name = 'feedback'
urlpatterns = [
path('modal/', views.modal, name='modal'),
path('', views.submit, name='submit'),
]

View File

@@ -0,0 +1,46 @@
from django.http import HttpResponse
from django.shortcuts import render
from django.views.decorators.http import require_GET, require_POST
from .models import Feedbackeintrag
@require_GET
def modal(request):
return render(request, 'partials/feedback_modal.html')
@require_POST
def submit(request):
beschreibung = request.POST.get('beschreibung', '').strip()
if not beschreibung:
return render(request, 'partials/feedback_modal.html',
{'error': 'Beschreibung ist erforderlich.'})
entry = Feedbackeintrag(
titel=request.POST.get('titel', 'Ohne Titel') or 'Ohne Titel',
beschreibung=beschreibung,
seite_kontext=request.POST.get('seite_kontext', ''),
kategorie=request.POST.get('kategorie', 'hinweis'),
dringlichkeit=request.POST.get('dringlichkeit', 'mittel'),
)
ausschreibung_pk = request.POST.get('ausschreibung')
if ausschreibung_pk:
try:
from vergabe_teilnahme.apps.ausschreibungen.models import Ausschreibung
entry.ausschreibung = Ausschreibung.objects.get(pk=ausschreibung_pk)
except (Ausschreibung.DoesNotExist, ValueError):
pass
if request.user.is_authenticated:
entry.erfasst_von = request.user
entry.save()
return HttpResponse(
'<div x-data="{open:true}" x-show="open" x-cloak '
'class="fixed inset-0 bg-black/30 z-50 flex items-center justify-center">'
'<div class="bg-white rounded-xl shadow-xl p-8 text-center max-w-sm mx-4">'
'<p class="text-2xl mb-2">✅</p>'
'<p class="font-medium text-slate-900">Danke für dein Feedback!</p>'
'<button @click="open=false" class="btn-secondary mt-4">Schließen</button>'
'</div></div>'
)

View File

View File

@@ -0,0 +1,15 @@
from django.contrib import admin
from .models import Anforderung, Los
@admin.register(Los)
class LosAdmin(admin.ModelAdmin):
list_display = ['losnummer', 'lostitel', 'ausschreibung', 'teilnahme', 'zustaendiger']
list_filter = ['teilnahme']
@admin.register(Anforderung)
class AnforderungAdmin(admin.ModelAdmin):
list_display = ['titel', 'verbindlichkeit', 'kategorie', 'erfuellungsstatus', 'ausschlusskriterium']
list_filter = ['verbindlichkeit', 'kategorie', 'erfuellungsstatus', 'ausschlusskriterium']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class LoseConfig(AppConfig):
name = 'vergabe_teilnahme.apps.lose'

View File

@@ -0,0 +1,63 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ausschreibungen', '0001_initial'),
('bibliothek', '0001_initial'),
('dokumente', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Los',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('losnummer', models.CharField(max_length=50)),
('lostitel', models.CharField(max_length=300)),
('beschreibung', models.TextField(blank=True)),
('abgrenzung', models.TextField(blank=True)),
('teilnahme', models.BooleanField(null=True)),
('status', models.CharField(blank=True, max_length=50)),
('ausschreibung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lose', to='ausschreibungen.ausschreibung')),
('zustaendiger', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Los',
'verbose_name_plural': 'Lose',
'ordering': ['losnummer'],
},
),
migrations.CreateModel(
name='Anforderung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('titel', models.CharField(max_length=400)),
('beschreibung', models.TextField(blank=True)),
('quelle_im_dokument', models.CharField(blank=True, max_length=300)),
('kategorie', models.CharField(blank=True, choices=[('fachlich', 'Fachlich'), ('rechtlich', 'Rechtlich'), ('kaufmaennisch', 'Kaufmännisch'), ('technisch', 'Technisch'), ('zertifizierung', 'Zertifizierung'), ('referenz', 'Referenz'), ('sonstiges', 'Sonstiges')], max_length=30)),
('verbindlichkeit', models.CharField(choices=[('muss', 'Muss-Kriterium'), ('soll', 'Soll-Kriterium'), ('kann', 'Kann-Kriterium'), ('info', 'Information')], default='muss', max_length=10)),
('ausschlusskriterium', models.BooleanField(default=False)),
('bewertungskriterium', models.BooleanField(default=False)),
('erfuellungsstatus', models.CharField(choices=[('offen', 'Offen'), ('erfuellt', 'Erfüllt'), ('teilweise', 'Teilweise erfüllt'), ('nicht_erfuellt', 'Nicht erfüllt'), ('entfaellt', 'Entfällt')], default='offen', max_length=20)),
('nachweis_erforderlich', models.BooleanField(default=False)),
('ausschreibung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='anforderungen', to='ausschreibungen.ausschreibung')),
('dokumente', models.ManyToManyField(blank=True, to='dokumente.dokument')),
('nachweise', models.ManyToManyField(blank=True, to='bibliothek.nachweis')),
('zustaendiger', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('los', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='anforderungen', to='lose.los')),
],
options={
'verbose_name': 'Anforderung',
'verbose_name_plural': 'Anforderungen',
},
),
]

View File

@@ -0,0 +1,81 @@
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Los(FlexibleModel):
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='lose'
)
losnummer = models.CharField(max_length=50)
lostitel = models.CharField(max_length=300)
beschreibung = models.TextField(blank=True)
abgrenzung = models.TextField(blank=True)
zustaendiger = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
teilnahme = models.BooleanField(null=True)
status = models.CharField(max_length=50, blank=True)
class Meta:
ordering = ['losnummer']
verbose_name = 'Los'
verbose_name_plural = 'Lose'
def __str__(self):
return f'Los {self.losnummer}: {self.lostitel}'
class Anforderung(FlexibleModel):
VERBINDLICHKEIT_CHOICES = [
('muss', 'Muss-Kriterium'),
('soll', 'Soll-Kriterium'),
('kann', 'Kann-Kriterium'),
('info', 'Information'),
]
KATEGORIE_CHOICES = [
('fachlich', 'Fachlich'),
('rechtlich', 'Rechtlich'),
('kaufmaennisch', 'Kaufmännisch'),
('technisch', 'Technisch'),
('zertifizierung', 'Zertifizierung'),
('referenz', 'Referenz'),
('sonstiges', 'Sonstiges'),
]
ERFUELLUNG_CHOICES = [
('offen', 'Offen'),
('erfuellt', 'Erfüllt'),
('teilweise', 'Teilweise erfüllt'),
('nicht_erfuellt', 'Nicht erfüllt'),
('entfaellt', 'Entfällt'),
]
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='anforderungen'
)
los = models.ForeignKey(
Los, on_delete=models.SET_NULL, null=True, blank=True, related_name='anforderungen'
)
titel = models.CharField(max_length=400)
beschreibung = models.TextField(blank=True)
quelle_im_dokument = models.CharField(max_length=300, blank=True)
kategorie = models.CharField(max_length=30, choices=KATEGORIE_CHOICES, blank=True)
verbindlichkeit = models.CharField(max_length=10, choices=VERBINDLICHKEIT_CHOICES, default='muss')
ausschlusskriterium = models.BooleanField(default=False)
bewertungskriterium = models.BooleanField(default=False)
zustaendiger = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
erfuellungsstatus = models.CharField(
max_length=20, choices=ERFUELLUNG_CHOICES, default='offen'
)
nachweis_erforderlich = models.BooleanField(default=False)
dokumente = models.ManyToManyField('dokumente.Dokument', blank=True)
nachweise = models.ManyToManyField('bibliothek.Nachweis', blank=True)
class Meta:
verbose_name = 'Anforderung'
verbose_name_plural = 'Anforderungen'
def __str__(self):
return self.titel

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,2 @@
from django.urls import path
urlpatterns = []

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,16 @@
from django.contrib import admin
from .models import Ausschreibungspassage, Marktbegleiter
@admin.register(Marktbegleiter)
class MarktbegleiterAdmin(admin.ModelAdmin):
list_display = ['name', 'vertraulichkeit', 'letzte_aktualisierung']
list_filter = ['vertraulichkeit']
search_fields = ['name']
@admin.register(Ausschreibungspassage)
class AusschreibungspassageAdmin(admin.ModelAdmin):
list_display = ['marktbegleiter', 'ausschreibung', 'verlaesslichkeitsscore', 'kategorie', 'erfassungsdatum']
list_filter = ['kategorie', 'marktbegleiter']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class MarktbegleiterConfig(AppConfig):
name = 'vergabe_teilnahme.apps.marktbegleiter'

View File

@@ -0,0 +1,70 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ausschreibungen', '0001_initial'),
('dokumente', '0002_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Marktbegleiter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=300)),
('kurzbeschreibung', models.TextField(blank=True)),
('produkt_leistungsportfolio', models.TextField(blank=True)),
('relevante_branchen', models.TextField(blank=True)),
('bekannte_staerken', models.TextField(blank=True)),
('bekannte_schwaechen', models.TextField(blank=True)),
('typische_formulierungen', models.TextField(blank=True)),
('typische_leistungsmerkmale', models.TextField(blank=True)),
('bekannte_zertifizierungen', models.TextField(blank=True)),
('bekannte_referenzen', models.TextField(blank=True)),
('quellen_links', models.TextField(blank=True)),
('letzte_aktualisierung', models.DateField(blank=True, null=True)),
('aktualisierungsstatus', models.CharField(blank=True, max_length=50)),
('interne_notizen', models.TextField(blank=True)),
('vertraulichkeit', models.CharField(choices=[('intern', 'Intern'), ('streng_vertraulich', 'Streng vertraulich')], default='intern', max_length=20)),
],
options={
'verbose_name': 'Marktbegleiter',
'verbose_name_plural': 'Marktbegleiter',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Ausschreibungspassage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fundstelle', models.CharField(blank=True, max_length=200)),
('passage', models.TextField()),
('kategorie', models.CharField(blank=True, max_length=100)),
('begruendung_zuordnung', models.TextField(blank=True)),
('verlaesslichkeitsscore', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])),
('auswirkung_entscheidung', models.TextField(blank=True)),
('auswirkung_preisstrategie', models.TextField(blank=True)),
('auswirkung_loesungskonzept', models.TextField(blank=True)),
('erfassungsdatum', models.DateField(auto_now_add=True)),
('ausschreibung', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passagen', to='ausschreibungen.ausschreibung')),
('dokument', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dokumente.dokument')),
('erfasst_von', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
('marktbegleiter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passagen', to='marktbegleiter.marktbegleiter')),
],
options={
'verbose_name': 'Ausschreibungspassage',
'verbose_name_plural': 'Ausschreibungspassagen',
'ordering': ['-verlaesslichkeitsscore'],
},
),
]

View File

@@ -0,0 +1,71 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Marktbegleiter(FlexibleModel):
VERTRAULICHKEIT_CHOICES = [
('intern', 'Intern'),
('streng_vertraulich', 'Streng vertraulich'),
]
name = models.CharField(max_length=300)
kurzbeschreibung = models.TextField(blank=True)
produkt_leistungsportfolio = models.TextField(blank=True)
relevante_branchen = models.TextField(blank=True)
bekannte_staerken = models.TextField(blank=True)
bekannte_schwaechen = models.TextField(blank=True)
typische_formulierungen = models.TextField(blank=True)
typische_leistungsmerkmale = models.TextField(blank=True)
bekannte_zertifizierungen = models.TextField(blank=True)
bekannte_referenzen = models.TextField(blank=True)
quellen_links = models.TextField(blank=True)
letzte_aktualisierung = models.DateField(null=True, blank=True)
aktualisierungsstatus = models.CharField(max_length=50, blank=True)
interne_notizen = models.TextField(blank=True)
vertraulichkeit = models.CharField(
max_length=20, choices=VERTRAULICHKEIT_CHOICES, default='intern'
)
class Meta:
verbose_name = 'Marktbegleiter'
verbose_name_plural = 'Marktbegleiter'
ordering = ['name']
def __str__(self):
return self.name
class Ausschreibungspassage(FlexibleModel):
ausschreibung = models.ForeignKey(
'ausschreibungen.Ausschreibung', on_delete=models.CASCADE, related_name='passagen'
)
dokument = models.ForeignKey(
'dokumente.Dokument', on_delete=models.SET_NULL, null=True, blank=True
)
fundstelle = models.CharField(max_length=200, blank=True)
passage = models.TextField()
kategorie = models.CharField(max_length=100, blank=True)
marktbegleiter = models.ForeignKey(
Marktbegleiter, on_delete=models.CASCADE, related_name='passagen'
)
begruendung_zuordnung = models.TextField(blank=True)
verlaesslichkeitsscore = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10)]
)
auswirkung_entscheidung = models.TextField(blank=True)
auswirkung_preisstrategie = models.TextField(blank=True)
auswirkung_loesungskonzept = models.TextField(blank=True)
erfasst_von = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
erfassungsdatum = models.DateField(auto_now_add=True)
class Meta:
verbose_name = 'Ausschreibungspassage'
verbose_name_plural = 'Ausschreibungspassagen'
ordering = ['-verlaesslichkeitsscore']
def __str__(self):
return f'{self.marktbegleiter}{self.passage[:60]}'

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,2 @@
from django.urls import path
urlpatterns = []

View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -0,0 +1,9 @@
from django.contrib import admin
from .models import Nachbetrachtung
@admin.register(Nachbetrachtung)
class NachbetrachtungAdmin(admin.ModelAdmin):
list_display = ['ausschreibung', 'ergebnis', 'zuschlagsdatum', 'projektverantwortlicher']
list_filter = ['ergebnis', 'wiederverwendbare_erkenntnisse_markiert']

View File

@@ -0,0 +1,5 @@
from django.apps import AppConfig
class NachbetrachtungConfig(AppConfig):
name = 'vergabe_teilnahme.apps.nachbetrachtung'

View File

@@ -0,0 +1,39 @@
# Generated by Django 6.0.5 on 2026-05-08 10:33
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('ausschreibungen', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Nachbetrachtung',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ergebnis', models.CharField(choices=[('gewonnen', 'Gewonnen'), ('verloren', 'Verloren'), ('aufgehoben', 'Aufgehoben'), ('offen', 'Offen'), ('zurueckgezogen', 'Zurückgezogen')], default='offen', max_length=20)),
('zuschlagsdatum', models.DateField(blank=True, null=True)),
('abgegebene_unterlagen', models.TextField(blank=True)),
('abgegebene_preise', models.TextField(blank=True)),
('verlustgruende', models.JSONField(default=list)),
('ausschlaggebende_zuschlagsmerkmale', models.TextField(blank=True)),
('lessons_learned', models.TextField(blank=True)),
('empfehlungen', models.TextField(blank=True)),
('wiederverwendbare_erkenntnisse_markiert', models.BooleanField(default=False)),
('ausschreibung', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='nachbetrachtung', to='ausschreibungen.ausschreibung')),
('projektverantwortlicher', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nachbetrachtung',
'verbose_name_plural': 'Nachbetrachtungen',
},
),
]

View File

@@ -0,0 +1,38 @@
from django.db import models
from vergabe_teilnahme.apps.core.models import FlexibleModel
class Nachbetrachtung(FlexibleModel):
ERGEBNIS_CHOICES = [
('gewonnen', 'Gewonnen'),
('verloren', 'Verloren'),
('aufgehoben', 'Aufgehoben'),
('offen', 'Offen'),
('zurueckgezogen', 'Zurückgezogen'),
]
ausschreibung = models.OneToOneField(
'ausschreibungen.Ausschreibung',
on_delete=models.CASCADE,
related_name='nachbetrachtung',
)
ergebnis = models.CharField(max_length=20, choices=ERGEBNIS_CHOICES, default='offen')
zuschlagsdatum = models.DateField(null=True, blank=True)
abgegebene_unterlagen = models.TextField(blank=True)
abgegebene_preise = models.TextField(blank=True)
verlustgruende = models.JSONField(default=list)
ausschlaggebende_zuschlagsmerkmale = models.TextField(blank=True)
lessons_learned = models.TextField(blank=True)
empfehlungen = models.TextField(blank=True)
wiederverwendbare_erkenntnisse_markiert = models.BooleanField(default=False)
projektverantwortlicher = models.ForeignKey(
'accounts.Mitarbeiter', on_delete=models.SET_NULL, null=True, blank=True
)
class Meta:
verbose_name = 'Nachbetrachtung'
verbose_name_plural = 'Nachbetrachtungen'
def __str__(self):
return f'Nachbetrachtung: {self.ausschreibung}'

Some files were not shown because too many files have changed in this diff Show More