diff --git a/backend/entrypoint-web.sh b/backend/entrypoint-web.sh index 78ac22e..cd7a427 100755 --- a/backend/entrypoint-web.sh +++ b/backend/entrypoint-web.sh @@ -7,5 +7,6 @@ until nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do done python manage.py migrate --noinput +python manage.py bootstrap_initial_users python manage.py collectstatic --noinput exec gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120 --access-logfile - --error-logfile - diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 535862c..d8deee6 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -388,6 +388,8 @@ class OffboardingRequestForm(forms.ModelForm): def __init__(self, *args, **kwargs): prefill_profile = kwargs.pop('prefill_profile', None) super().__init__(*args, **kwargs) + self.fields['full_name'].label = 'Vorname und Nachname' + self.fields['work_email'].help_text = '' if prefill_profile: self.fields['full_name'].initial = prefill_profile.full_name self.fields['work_email'].initial = prefill_profile.work_email diff --git a/backend/workflows/management/commands/bootstrap_initial_users.py b/backend/workflows/management/commands/bootstrap_initial_users.py new file mode 100644 index 0000000..bb6b872 --- /dev/null +++ b/backend/workflows/management/commands/bootstrap_initial_users.py @@ -0,0 +1,48 @@ +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand + + +DEFAULT_USERS = [ + { + 'username': 'admin_test', + 'email': 'admin_test@tub.co', + 'password': 'admin12345', + 'first_name': 'Admin', + 'last_name': 'Test', + 'is_staff': True, + 'is_superuser': True, + }, + { + 'username': 'user_test', + 'email': 'user_test@tub.co', + 'password': 'user12345', + 'first_name': 'Normal', + 'last_name': 'User', + 'is_staff': False, + 'is_superuser': False, + }, +] + + +class Command(BaseCommand): + help = 'Create initial test/admin users when the database is empty.' + + def handle(self, *args, **options): + user_model = get_user_model() + if user_model.objects.exists(): + self.stdout.write('bootstrap skipped: users already exist') + return + + for item in DEFAULT_USERS: + user = user_model.objects.create_user( + username=item['username'], + email=item['email'], + password=item['password'], + first_name=item['first_name'], + last_name=item['last_name'], + is_staff=item['is_staff'], + is_superuser=item['is_superuser'], + ) + self.stdout.write(f'created {user.username}') + + self.stdout.write(self.style.SUCCESS('initial users created')) diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 5c95ea2..4ca8193 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -16,6 +16,16 @@ from .models import EmployeeProfile, NotificationRule, NotificationTemplate, Off from .emailing import send_system_email from .services import upload_to_nextcloud from .services import get_email_test_redirect, is_email_test_mode +from .forms import ( + ACCESS_CHOICES, + DEVICE_CHOICES, + HARDWARE_EXTRA_CHOICES, + OnboardingRequestForm, + RESOURCE_CHOICES, + SOFTWARE_CHOICES, + SOFTWARE_EXTRA_CHOICES, + WORKSPACE_GROUP_CHOICES, +) DEFAULT_NOTIFICATION_TEMPLATES = { @@ -179,6 +189,76 @@ def _split_multiline(text: str) -> list[str]: return [line.strip() for line in (text or '').split('\n') if line.strip()] +def _chunk_choice_labels(choices: list[tuple[str, str]], chunk_size: int = 3) -> list[list[str]]: + labels = [label for _, label in choices] + return _chunk_list(labels, chunk_size=chunk_size) + + +MANUAL_ONBOARDING_FIELD_SECTIONS = [ + ( + 'Stammdaten', + [ + 'gender', + 'first_name', + 'last_name', + 'job_title', + 'department', + 'work_email', + 'order_business_cards', + 'business_card_name', + 'business_card_title', + 'business_card_email', + 'business_card_phone', + ], + ), + ( + 'Vertrag', + [ + 'contract_start', + 'employment_type', + 'employment_end_date', + 'handover_date', + ], + ), + ( + 'IT-Setup', + [ + 'group_mailboxes_required_choice', + 'group_mailboxes', + 'additional_hardware_needed_choice', + 'additional_hardware_other', + 'additional_software_needed_choice', + 'additional_software', + 'additional_access_needed_choice', + 'additional_access_text', + 'successor_required_choice', + 'successor_name', + 'inherit_phone_number_choice', + 'phone_number_choice', + ], + ), + ( + 'Abschluss', + [ + 'additional_notes', + 'signature_image', + 'agreement_confirm', + ], + ), +] + + +def _manual_onboarding_field_sections() -> list[dict]: + fields = OnboardingRequestForm.base_fields + sections = [] + for title, field_names in MANUAL_ONBOARDING_FIELD_SECTIONS: + labels = [str(fields[name].label or name) for name in field_names if name in fields] + if not labels: + continue + sections.append({'title': title, 'rows': _chunk_list(labels, chunk_size=2)}) + return sections + + def _resolve_workflow_emails() -> tuple[str, str, str, str, str]: config = WorkflowConfig.objects.order_by('id').first() it_email = (config.it_onboarding_email if config and config.it_onboarding_email else settings.IT_ONBOARDING_NOTIFICATION_EMAIL) @@ -448,10 +528,12 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: successor_name = (request_obj.successor_name or '').strip() additional_notes = (request_obj.additional_notes or '').strip() phone_number = (request_obj.phone_number or '').strip() + display_name = f"{gender} {first_name} {last_name}".strip() if gender and gender != '-' else f"{first_name} {last_name}".strip() context = { 'VORNAME': first_name, 'NACHNAME': last_name, + 'DISPLAY_NAME': display_name or request_obj.full_name, 'ANREDE': gender, 'BERUFSBEZEICHNUNG': request_obj.job_title or 'N/A', 'ABTEILUNG': request_obj.department or 'N/A', @@ -539,6 +621,7 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: .order_by('-created_at') .first() ) + has_onboarding_data = latest_onboarding is not None onboarding_hardware = _split_multiline(latest_onboarding.needed_devices) if latest_onboarding else [] selected_set = {item.lower() for item in onboarding_hardware} hardware_catalog = [ @@ -568,8 +651,17 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: 'NOTES': request_obj.notes or '-', 'REQUESTED_BY': requester_email, 'REQUESTED_BY_NAME': requester_name, + 'HAS_ONBOARDING_DATA': has_onboarding_data, 'ONBOARDING_HARDWARE': onboarding_hardware, 'HARDWARE_CHECKLIST': checklist, + 'MANUAL_FIELD_SECTIONS': _manual_onboarding_field_sections(), + 'MANUAL_DEVICES': _chunk_choice_labels(DEVICE_CHOICES), + 'MANUAL_SOFTWARE': _chunk_choice_labels(SOFTWARE_CHOICES), + 'MANUAL_ACCESSES': _chunk_choice_labels(ACCESS_CHOICES), + 'MANUAL_WORKSPACE_GROUPS': _chunk_choice_labels(WORKSPACE_GROUP_CHOICES), + 'MANUAL_RESOURCES': _chunk_choice_labels(RESOURCE_CHOICES), + 'MANUAL_EXTRA_HARDWARE': _chunk_choice_labels(HARDWARE_EXTRA_CHOICES), + 'MANUAL_EXTRA_SOFTWARE': _chunk_choice_labels(SOFTWARE_EXTRA_CHOICES), } html = _render_html(template_path, context) diff --git a/backend/workflows/templates/workflows/offboarding_form.html b/backend/workflows/templates/workflows/offboarding_form.html index c06cdce..27ec68f 100644 --- a/backend/workflows/templates/workflows/offboarding_form.html +++ b/backend/workflows/templates/workflows/offboarding_form.html @@ -62,5 +62,59 @@ + + diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index ec720ac..7e62962 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -108,7 +108,8 @@
  • User opens /offboarding/new/ and can search existing profile first.
  • Form saves request with requester name/email from logged-in user.
  • Task process_offboarding_request runs in worker.
  • -
  • PDF is generated (hardware section can be derived from latest onboarding request).
  • +
  • PDF is generated. If a previous onboarding request exists, the hardware section can be derived from that onboarding data.
  • +
  • If no saved onboarding request exists for that person, the offboarding PDF automatically falls back to a manual onboarding reference sheet with grouped onboarding fields and printable checkbox lists.
  • Notification emails are sent (default + rules).
  • PDF upload to Nextcloud runs if enabled.
  • @@ -137,6 +138,7 @@
  • Output folder: /backend/media/pdfs/.
  • Signature images are embedded for compatibility with xhtml2pdf rendering.
  • Conditional sections are hidden if no data is provided.
  • +
  • Offboarding fallback behavior: if no onboarding record is available, the PDF renders a manual onboarding review layout grouped into Stammdaten, Vertrag, IT-Setup, and Abschluss, plus checkbox grids for devices, software, accesses, workspace groups, resources, and additional IT items.
  • 8) Integrations

    @@ -179,6 +181,14 @@
  • Use Docker Compose for web + worker + db + redis services.
  • After template/form/task changes, restart web and worker containers.
  • Run python manage.py check before release.
  • +
  • On a fresh database, the web container boot sequence now runs python manage.py bootstrap_initial_users right after migrations.
  • + + +

    Initial Bootstrap Users

    +

    11) Security & Reliability Hardening (Current)

    @@ -222,6 +232,12 @@
  • Check SMTP settings and worker logs.
  • +

    Login fails after fresh stack start

    + +

    Nextcloud upload missing

    - Last updated for current system behavior as of March 10, 2026. + Last updated for current system behavior as of March 19, 2026.