generated from coulomb/repo-seed
Prototype implementation
This commit is contained in:
0
vergabe_teilnahme/__init__.py
Normal file
0
vergabe_teilnahme/__init__.py
Normal file
0
vergabe_teilnahme/apps/__init__.py
Normal file
0
vergabe_teilnahme/apps/__init__.py
Normal file
0
vergabe_teilnahme/apps/accounts/__init__.py
Normal file
0
vergabe_teilnahme/apps/accounts/__init__.py
Normal file
12
vergabe_teilnahme/apps/accounts/admin.py
Normal file
12
vergabe_teilnahme/apps/accounts/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/accounts/apps.py
Normal file
5
vergabe_teilnahme/apps/accounts/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.accounts'
|
||||
46
vergabe_teilnahme/apps/accounts/migrations/0001_initial.py
Normal file
46
vergabe_teilnahme/apps/accounts/migrations/0001_initial.py
Normal 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()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
25
vergabe_teilnahme/apps/accounts/models.py
Normal file
25
vergabe_teilnahme/apps/accounts/models.py
Normal 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'
|
||||
3
vergabe_teilnahme/apps/accounts/tests.py
Normal file
3
vergabe_teilnahme/apps/accounts/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
vergabe_teilnahme/apps/accounts/views.py
Normal file
3
vergabe_teilnahme/apps/accounts/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/aufgaben/__init__.py
Normal file
0
vergabe_teilnahme/apps/aufgaben/__init__.py
Normal file
15
vergabe_teilnahme/apps/aufgaben/admin.py
Normal file
15
vergabe_teilnahme/apps/aufgaben/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/aufgaben/apps.py
Normal file
5
vergabe_teilnahme/apps/aufgaben/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AufgabenConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.aufgaben'
|
||||
51
vergabe_teilnahme/apps/aufgaben/migrations/0001_initial.py
Normal file
51
vergabe_teilnahme/apps/aufgaben/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
71
vergabe_teilnahme/apps/aufgaben/migrations/0002_initial.py
Normal file
71
vergabe_teilnahme/apps/aufgaben/migrations/0002_initial.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
113
vergabe_teilnahme/apps/aufgaben/models.py
Normal file
113
vergabe_teilnahme/apps/aufgaben/models.py
Normal 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]
|
||||
3
vergabe_teilnahme/apps/aufgaben/tests.py
Normal file
3
vergabe_teilnahme/apps/aufgaben/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
2
vergabe_teilnahme/apps/aufgaben/urls.py
Normal file
2
vergabe_teilnahme/apps/aufgaben/urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
3
vergabe_teilnahme/apps/aufgaben/views.py
Normal file
3
vergabe_teilnahme/apps/aufgaben/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/ausschreibungen/__init__.py
Normal file
0
vergabe_teilnahme/apps/ausschreibungen/__init__.py
Normal file
11
vergabe_teilnahme/apps/ausschreibungen/admin.py
Normal file
11
vergabe_teilnahme/apps/ausschreibungen/admin.py
Normal 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'
|
||||
5
vergabe_teilnahme/apps/ausschreibungen/apps.py
Normal file
5
vergabe_teilnahme/apps/ausschreibungen/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AusschreibungenConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.ausschreibungen'
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
107
vergabe_teilnahme/apps/ausschreibungen/models.py
Normal file
107
vergabe_teilnahme/apps/ausschreibungen/models.py
Normal 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))
|
||||
3
vergabe_teilnahme/apps/ausschreibungen/tests.py
Normal file
3
vergabe_teilnahme/apps/ausschreibungen/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
9
vergabe_teilnahme/apps/ausschreibungen/urls.py
Normal file
9
vergabe_teilnahme/apps/ausschreibungen/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'ausschreibungen'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.dashboard, name='dashboard'),
|
||||
]
|
||||
7
vergabe_teilnahme/apps/ausschreibungen/views.py
Normal file
7
vergabe_teilnahme/apps/ausschreibungen/views.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def dashboard(request):
|
||||
return render(request, 'ausschreibungen/dashboard.html', {
|
||||
'breadcrumbs': [{'label': 'Übersicht', 'url': None}],
|
||||
})
|
||||
0
vergabe_teilnahme/apps/bibliothek/__init__.py
Normal file
0
vergabe_teilnahme/apps/bibliothek/__init__.py
Normal file
27
vergabe_teilnahme/apps/bibliothek/admin.py
Normal file
27
vergabe_teilnahme/apps/bibliothek/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/bibliothek/apps.py
Normal file
5
vergabe_teilnahme/apps/bibliothek/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BibliothekConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.bibliothek'
|
||||
115
vergabe_teilnahme/apps/bibliothek/migrations/0001_initial.py
Normal file
115
vergabe_teilnahme/apps/bibliothek/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
154
vergabe_teilnahme/apps/bibliothek/models.py
Normal file
154
vergabe_teilnahme/apps/bibliothek/models.py
Normal 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
|
||||
3
vergabe_teilnahme/apps/bibliothek/tests.py
Normal file
3
vergabe_teilnahme/apps/bibliothek/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
2
vergabe_teilnahme/apps/bibliothek/urls.py
Normal file
2
vergabe_teilnahme/apps/bibliothek/urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
3
vergabe_teilnahme/apps/bibliothek/views.py
Normal file
3
vergabe_teilnahme/apps/bibliothek/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/core/__init__.py
Normal file
0
vergabe_teilnahme/apps/core/__init__.py
Normal file
22
vergabe_teilnahme/apps/core/admin.py
Normal file
22
vergabe_teilnahme/apps/core/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/core/apps.py
Normal file
5
vergabe_teilnahme/apps/core/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.core'
|
||||
13
vergabe_teilnahme/apps/core/context_processors.py
Normal file
13
vergabe_teilnahme/apps/core/context_processors.py
Normal 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
|
||||
0
vergabe_teilnahme/apps/core/management/__init__.py
Normal file
0
vergabe_teilnahme/apps/core/management/__init__.py
Normal file
239
vergabe_teilnahme/apps/core/management/commands/seed_dev.py
Normal file
239
vergabe_teilnahme/apps/core/management/commands/seed_dev.py
Normal 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.'))
|
||||
70
vergabe_teilnahme/apps/core/migrations/0001_initial.py
Normal file
70
vergabe_teilnahme/apps/core/migrations/0001_initial.py
Normal 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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
vergabe_teilnahme/apps/core/migrations/__init__.py
Normal file
0
vergabe_teilnahme/apps/core/migrations/__init__.py
Normal file
106
vergabe_teilnahme/apps/core/models.py
Normal file
106
vergabe_teilnahme/apps/core/models.py
Normal 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
|
||||
106
vergabe_teilnahme/apps/core/services.py
Normal file
106
vergabe_teilnahme/apps/core/services.py
Normal 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),
|
||||
}
|
||||
71
vergabe_teilnahme/apps/core/templatetags/vergabe_tags.py
Normal file
71
vergabe_teilnahme/apps/core/templatetags/vergabe_tags.py
Normal 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}
|
||||
3
vergabe_teilnahme/apps/core/tests.py
Normal file
3
vergabe_teilnahme/apps/core/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
0
vergabe_teilnahme/apps/core/tests/__init__.py
Normal file
0
vergabe_teilnahme/apps/core/tests/__init__.py
Normal file
41
vergabe_teilnahme/apps/core/tests/test_services.py
Normal file
41
vergabe_teilnahme/apps/core/tests/test_services.py
Normal 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')
|
||||
14
vergabe_teilnahme/apps/core/view_helpers.py
Normal file
14
vergabe_teilnahme/apps/core/view_helpers.py
Normal 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
|
||||
29
vergabe_teilnahme/apps/core/views.py
Normal file
29
vergabe_teilnahme/apps/core/views.py
Normal 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)
|
||||
0
vergabe_teilnahme/apps/dokumente/__init__.py
Normal file
0
vergabe_teilnahme/apps/dokumente/__init__.py
Normal file
10
vergabe_teilnahme/apps/dokumente/admin.py
Normal file
10
vergabe_teilnahme/apps/dokumente/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/dokumente/apps.py
Normal file
5
vergabe_teilnahme/apps/dokumente/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DokumenteConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.dokumente'
|
||||
36
vergabe_teilnahme/apps/dokumente/migrations/0001_initial.py
Normal file
36
vergabe_teilnahme/apps/dokumente/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
34
vergabe_teilnahme/apps/dokumente/migrations/0002_initial.py
Normal file
34
vergabe_teilnahme/apps/dokumente/migrations/0002_initial.py
Normal 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),
|
||||
),
|
||||
]
|
||||
97
vergabe_teilnahme/apps/dokumente/models.py
Normal file
97
vergabe_teilnahme/apps/dokumente/models.py
Normal 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
|
||||
3
vergabe_teilnahme/apps/dokumente/tests.py
Normal file
3
vergabe_teilnahme/apps/dokumente/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
2
vergabe_teilnahme/apps/dokumente/urls.py
Normal file
2
vergabe_teilnahme/apps/dokumente/urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
3
vergabe_teilnahme/apps/dokumente/views.py
Normal file
3
vergabe_teilnahme/apps/dokumente/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/feedback/__init__.py
Normal file
0
vergabe_teilnahme/apps/feedback/__init__.py
Normal file
10
vergabe_teilnahme/apps/feedback/admin.py
Normal file
10
vergabe_teilnahme/apps/feedback/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/feedback/apps.py
Normal file
5
vergabe_teilnahme/apps/feedback/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FeedbackConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.feedback'
|
||||
42
vergabe_teilnahme/apps/feedback/migrations/0001_initial.py
Normal file
42
vergabe_teilnahme/apps/feedback/migrations/0001_initial.py
Normal 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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
50
vergabe_teilnahme/apps/feedback/models.py
Normal file
50
vergabe_teilnahme/apps/feedback/models.py
Normal 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
|
||||
3
vergabe_teilnahme/apps/feedback/tests.py
Normal file
3
vergabe_teilnahme/apps/feedback/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
vergabe_teilnahme/apps/feedback/urls.py
Normal file
10
vergabe_teilnahme/apps/feedback/urls.py
Normal 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'),
|
||||
]
|
||||
46
vergabe_teilnahme/apps/feedback/views.py
Normal file
46
vergabe_teilnahme/apps/feedback/views.py
Normal 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>'
|
||||
)
|
||||
0
vergabe_teilnahme/apps/lose/__init__.py
Normal file
0
vergabe_teilnahme/apps/lose/__init__.py
Normal file
15
vergabe_teilnahme/apps/lose/admin.py
Normal file
15
vergabe_teilnahme/apps/lose/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/lose/apps.py
Normal file
5
vergabe_teilnahme/apps/lose/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class LoseConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.lose'
|
||||
63
vergabe_teilnahme/apps/lose/migrations/0001_initial.py
Normal file
63
vergabe_teilnahme/apps/lose/migrations/0001_initial.py
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
||||
0
vergabe_teilnahme/apps/lose/migrations/__init__.py
Normal file
0
vergabe_teilnahme/apps/lose/migrations/__init__.py
Normal file
81
vergabe_teilnahme/apps/lose/models.py
Normal file
81
vergabe_teilnahme/apps/lose/models.py
Normal 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
|
||||
3
vergabe_teilnahme/apps/lose/tests.py
Normal file
3
vergabe_teilnahme/apps/lose/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
2
vergabe_teilnahme/apps/lose/urls.py
Normal file
2
vergabe_teilnahme/apps/lose/urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
3
vergabe_teilnahme/apps/lose/views.py
Normal file
3
vergabe_teilnahme/apps/lose/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/marktbegleiter/__init__.py
Normal file
0
vergabe_teilnahme/apps/marktbegleiter/__init__.py
Normal file
16
vergabe_teilnahme/apps/marktbegleiter/admin.py
Normal file
16
vergabe_teilnahme/apps/marktbegleiter/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/marktbegleiter/apps.py
Normal file
5
vergabe_teilnahme/apps/marktbegleiter/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MarktbegleiterConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.marktbegleiter'
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
71
vergabe_teilnahme/apps/marktbegleiter/models.py
Normal file
71
vergabe_teilnahme/apps/marktbegleiter/models.py
Normal 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]}'
|
||||
3
vergabe_teilnahme/apps/marktbegleiter/tests.py
Normal file
3
vergabe_teilnahme/apps/marktbegleiter/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
2
vergabe_teilnahme/apps/marktbegleiter/urls.py
Normal file
2
vergabe_teilnahme/apps/marktbegleiter/urls.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from django.urls import path
|
||||
urlpatterns = []
|
||||
3
vergabe_teilnahme/apps/marktbegleiter/views.py
Normal file
3
vergabe_teilnahme/apps/marktbegleiter/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
vergabe_teilnahme/apps/nachbetrachtung/__init__.py
Normal file
0
vergabe_teilnahme/apps/nachbetrachtung/__init__.py
Normal file
9
vergabe_teilnahme/apps/nachbetrachtung/admin.py
Normal file
9
vergabe_teilnahme/apps/nachbetrachtung/admin.py
Normal 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']
|
||||
5
vergabe_teilnahme/apps/nachbetrachtung/apps.py
Normal file
5
vergabe_teilnahme/apps/nachbetrachtung/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class NachbetrachtungConfig(AppConfig):
|
||||
name = 'vergabe_teilnahme.apps.nachbetrachtung'
|
||||
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
38
vergabe_teilnahme/apps/nachbetrachtung/models.py
Normal file
38
vergabe_teilnahme/apps/nachbetrachtung/models.py
Normal 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
Reference in New Issue
Block a user