from django import forms from pathlib import Path from datetime import timedelta from django.contrib.auth import get_user_model, password_validation from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, PasswordResetForm, SetPasswordForm from django.utils import timezone from django.utils.translation import get_language, gettext as _, gettext_lazy from .branding import get_company_email_domain from .form_builder import apply_form_field_config from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, WorkflowConfig from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')] EMPLOYMENT_CHOICES = [('', 'Wählen Sie eine'), ('befristet', 'befristet'), ('unbefristet', 'unbefristet')] GENDER_CHOICES = [('', 'Wählen Sie eine'), ('herr', 'Herr'), ('frau', 'Frau'), ('divers', 'Divers')] DEPARTMENT_CHOICES = [ ('Buchhaltung/Personalverwaltung', 'Buchhaltung/Personalverwaltung'), ('IT-Service', 'IT-Service'), ('Azubi', 'Azubi'), ('Marketing/Kommunikation', 'Marketing/Kommunikation'), ('Messe', 'Messe'), ('Kongresse', 'Kongresse'), ('TNB/Studiengänge', 'TNB/Studiengänge'), ('TUB-Academy', 'TUB-Academy'), ('Projekte TUB', 'Projekte TUB'), ('TU Berlin Summer + Winter School', 'TU Berlin Summer + Winter School'), ] DEVICE_CHOICES = [ ('Laptop', 'Laptop'), ('Docking-Station', 'Docking-Station'), ('Tastatur und Maus', 'Tastatur und Maus'), ('Kopfhörer', 'Kopfhörer'), ('Tragetasche', 'Tragetasche'), ('Monitor', 'Monitor'), ('Schlüssel', 'Schlüssel'), ('Tischtelefon', 'Tischtelefon'), ] SOFTWARE_CHOICES = [ ('eM Client', 'eM Client'), ('KeepassXC', 'KeepassXC'), ('Nextcloud', 'Nextcloud'), ('7-Zip', '7-Zip'), ('PDF Reader', 'PDF Reader'), ('PDF-Editor (Flexi PDF)', 'PDF-Editor (Flexi PDF)'), ('Firefox', 'Firefox'), ('Chrome', 'Chrome'), ('Backup Client', 'Backup Client'), ('MS Office', 'MS Office'), ('Zoom', 'Zoom'), ('Cisco VPN Client', 'Cisco VPN Client'), ] ACCESS_CHOICES = [ ('TU Konto', 'TU Konto'), ('HR Works', 'HR Works'), ('Datev', 'Datev'), ('Odoo', 'Odoo'), ] WORKSPACE_GROUP_CHOICES = [ ('Group-Academy', 'Group-Academy'), ('Group-BuSu', 'Group-BuSu'), ('Group-EIT-Urban-Mobility', 'Group-EIT-Urban-Mobility'), ('Group-EL', 'Group-EL'), ('Group-EM', 'Group-EM'), ('Group-IT-Services', 'Group-IT-Services'), ('Group-Kongresse-Events', 'Group-Kongresse-Events'), ('Group-MaCo', 'Group-MaCo'), ('Group-Messe', 'Group-Messe'), ('Group-Messe-Kongresse', 'Group-Messe-Kongresse'), ('Group-MSE', 'Group-MSE'), ('Group-SuMo', 'Group-SuMo'), ('Group-TNB', 'Group-TNB'), ('Group-TUBS', 'Group-TUBS'), ('Group-Wima', 'Group-Wima'), ('Group-Leitungsrunde', 'Group-Leitungsrunde'), ('Campus-Euref', 'Campus-Euref'), ('Group-Lohnbuchhaltung', 'Group-Lohnbuchhaltung'), ('Group-SU-WU', 'Group-SU-WU'), ('NWM', 'NWM'), ] RESOURCE_CHOICES = [('Drucker HBS 5./6. OG', 'Drucker HBS 5./6. OG'), ('Drucker Euref', 'Drucker Euref')] PHONE_CHOICES = [ ('030 4472021 (0-9)', '030 4472021 (0-9)'), ('030 4472022 (0-9)', '030 4472022 (0-9)'), ('030 4472023 (0-9)', '030 4472023 (0-9)'), ('030 4472024 (0-9)', '030 4472024 (0-9)'), ('030 4472025 (0-9)', '030 4472025 (0-9)'), ('030 4472026 (0-9)', '030 4472026 (0-9)'), ('030 4472027 (0-9)', '030 4472027 (0-9)'), ('030 4472028 (0-9)', '030 4472028 (0-9)'), ] HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')] SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')] class AppAuthenticationForm(AuthenticationForm): username = forms.CharField(label=gettext_lazy('Benutzername')) password = forms.CharField(label=gettext_lazy('Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'})) class AppPasswordResetForm(PasswordResetForm): email = forms.EmailField(label=gettext_lazy('E-Mail-Adresse')) class AppSetPasswordForm(SetPasswordForm): new_password1 = forms.CharField( label=gettext_lazy('Neues Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), help_text=password_validation.password_validators_help_text_html(), ) new_password2 = forms.CharField( label=gettext_lazy('Neues Passwort bestätigen'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), ) class AppPasswordChangeForm(PasswordChangeForm): old_password = forms.CharField( label=gettext_lazy('Aktuelles Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}), ) new_password1 = forms.CharField( label=gettext_lazy('Neues Passwort'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), help_text=password_validation.password_validators_help_text_html(), ) new_password2 = forms.CharField( label=gettext_lazy('Neues Passwort bestätigen'), strip=False, widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}), ) class UserManagementCreateForm(forms.Form): first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False) last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False) username = forms.CharField(label=_('Benutzername'), max_length=150) email = forms.EmailField(label=_('E-Mail-Adresse')) role_key = forms.ChoiceField(label=_('Rolle')) def __init__(self, *args, include_product_owner: bool = False, **kwargs): super().__init__(*args, **kwargs) self.include_product_owner = include_product_owner role_order = [ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF] if include_product_owner: role_order = [ROLE_PLATFORM_OWNER] + role_order self.fields['role_key'].choices = [(role_key, str(ROLE_LABELS[role_key])) for role_key in role_order] def clean_username(self): username = (self.cleaned_data.get('username') or '').strip() user_model = get_user_model() if user_model.objects.filter(username=username).exists(): raise forms.ValidationError(_('Dieser Benutzername ist bereits vergeben.')) return username def clean_email(self): return (self.cleaned_data.get('email') or '').strip().lower() def clean_role_key(self): role_key = (self.cleaned_data.get('role_key') or '').strip() if role_key not in ROLE_GROUP_NAMES: raise forms.ValidationError(_('Ungültige Rolle.')) if role_key == ROLE_PLATFORM_OWNER and not self.include_product_owner: raise forms.ValidationError(_('Nur Platform Owner dürfen diese Rolle vergeben.')) return role_key def save(self): user_model = get_user_model() user = user_model.objects.create_user( username=self.cleaned_data['username'], email=self.cleaned_data['email'], password=None, first_name=self.cleaned_data.get('first_name', ''), last_name=self.cleaned_data.get('last_name', ''), is_active=True, ) assign_user_role(user, self.cleaned_data['role_key']) return user class PortalBrandingForm(forms.ModelForm): class Meta: model = PortalBranding fields = [ 'portal_title', 'company_name', 'company_domain', 'support_email', 'sender_display_name', 'login_subtitle', 'footer_text', 'footer_text_en', 'legal_notice', 'legal_notice_en', 'default_language', 'logo_image', 'pdf_letterhead', 'favicon_image', 'primary_color', 'secondary_color', ] labels = { 'portal_title': gettext_lazy('Portal-Titel'), 'company_name': gettext_lazy('Firmenname'), 'company_domain': gettext_lazy('Firmen-Domain'), 'support_email': gettext_lazy('Support-E-Mail'), 'sender_display_name': gettext_lazy('Absender-Anzeigename'), 'login_subtitle': gettext_lazy('Login-Untertitel'), 'footer_text': gettext_lazy('Footer-Text DE'), 'footer_text_en': gettext_lazy('Footer-Text EN'), 'legal_notice': gettext_lazy('Rechtlicher Hinweis DE'), 'legal_notice_en': gettext_lazy('Rechtlicher Hinweis EN'), 'default_language': gettext_lazy('Standardsprache'), 'logo_image': gettext_lazy('Logo'), 'pdf_letterhead': gettext_lazy('PDF-Briefkopf'), 'favicon_image': gettext_lazy('Favicon'), 'primary_color': gettext_lazy('Primärfarbe'), 'secondary_color': gettext_lazy('Sekundärfarbe'), } widgets = { 'primary_color': forms.TextInput(attrs={'type': 'color'}), 'secondary_color': forms.TextInput(attrs={'type': 'color'}), 'logo_image': forms.ClearableFileInput(attrs={'accept': '.svg,.png,.jpg,.jpeg,.webp'}), 'pdf_letterhead': forms.ClearableFileInput(attrs={'accept': '.pdf'}), 'favicon_image': forms.ClearableFileInput(attrs={'accept': '.ico,.png,.svg,.webp'}), 'legal_notice': forms.Textarea(attrs={'rows': 3}), 'legal_notice_en': forms.Textarea(attrs={'rows': 3}), } def clean_logo_image(self): logo = self.cleaned_data.get('logo_image') if not logo: return logo if getattr(logo, 'size', 0) > 5 * 1024 * 1024: raise forms.ValidationError(_('Das Logo darf maximal 5 MB groß sein.')) return logo def clean_pdf_letterhead(self): letterhead = self.cleaned_data.get('pdf_letterhead') if not letterhead: return letterhead if getattr(letterhead, 'size', 0) > 10 * 1024 * 1024: raise forms.ValidationError(_('Der PDF-Briefkopf darf maximal 10 MB groß sein.')) return letterhead def clean_favicon_image(self): favicon = self.cleaned_data.get('favicon_image') if not favicon: return favicon if getattr(favicon, 'size', 0) > 2 * 1024 * 1024: raise forms.ValidationError(_('Das Favicon darf maximal 2 MB groß sein.')) return favicon class PortalCompanyConfigForm(forms.ModelForm): class Meta: model = PortalCompanyConfig fields = [ 'legal_company_name', 'street_address', 'postal_code', 'city', 'country', 'website_url', 'imprint_url', 'privacy_url', 'hr_contact_email', 'it_contact_email', 'operations_contact_email', 'phone_number', 'vat_id', 'registration_number', ] labels = { 'legal_company_name': gettext_lazy('Rechtlicher Firmenname'), 'street_address': gettext_lazy('Straße und Hausnummer'), 'postal_code': gettext_lazy('Postleitzahl'), 'city': gettext_lazy('Stadt'), 'country': gettext_lazy('Land'), 'website_url': gettext_lazy('Website'), 'imprint_url': gettext_lazy('Impressum-URL'), 'privacy_url': gettext_lazy('Datenschutz-URL'), 'hr_contact_email': gettext_lazy('HR-Kontakt'), 'it_contact_email': gettext_lazy('IT-Kontakt'), 'operations_contact_email': gettext_lazy('Operations-Kontakt'), 'phone_number': gettext_lazy('Zentrale Telefonnummer'), 'vat_id': gettext_lazy('USt-IdNr.'), 'registration_number': gettext_lazy('Register- oder Handelsnummer'), } class PortalTrialConfigForm(forms.ModelForm): class Meta: model = PortalTrialConfig fields = [ 'is_trial_mode', 'trial_started_at', 'trial_expires_at', 'restrict_production_integrations', 'auto_cleanup_enabled', 'trial_banner_text', 'trial_banner_text_en', ] labels = { 'is_trial_mode': gettext_lazy('Trial-Modus aktiv'), 'trial_started_at': gettext_lazy('Trial-Beginn'), 'trial_expires_at': gettext_lazy('Trial-Ende'), 'restrict_production_integrations': gettext_lazy('Produktive Integrationen begrenzen'), 'auto_cleanup_enabled': gettext_lazy('Cleanup nach Ablauf zulassen'), 'trial_banner_text': gettext_lazy('Banner-Text DE'), 'trial_banner_text_en': gettext_lazy('Banner-Text EN'), } widgets = { 'trial_started_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'trial_expires_at': forms.DateTimeInput(attrs={'type': 'datetime-local'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field_name in ('trial_started_at', 'trial_expires_at'): field = self.fields[field_name] if self.instance and getattr(self.instance, field_name): field.initial = timezone.localtime(getattr(self.instance, field_name)).strftime('%Y-%m-%dT%H:%M') field.input_formats = ['%Y-%m-%dT%H:%M'] def clean(self): cleaned = super().clean() started = cleaned.get('trial_started_at') expires = cleaned.get('trial_expires_at') if cleaned.get('is_trial_mode') and not expires: self.add_error('trial_expires_at', _('Bitte ein Trial-Ende festlegen.')) if started and expires and expires <= started: self.add_error('trial_expires_at', _('Das Trial-Ende muss nach dem Trial-Beginn liegen.')) return cleaned class OnboardingRequestForm(forms.ModelForm): first_name = forms.CharField(label='Vorname', required=False) last_name = forms.CharField(label='Nachname', required=False) full_name = forms.CharField(required=False, widget=forms.HiddenInput()) gender = forms.ChoiceField(label='Anrede', choices=GENDER_CHOICES, required=True) department = forms.ChoiceField(label='Abteilung', choices=DEPARTMENT_CHOICES, required=True) work_email = forms.EmailField( label='Gewünschte dienstliche E-Mail-Adresse', help_text='Bitte nutzen Sie das Format name@tub.co.', ) contract_start = forms.DateField(label='Vertragsbeginn', widget=forms.DateInput(attrs={'type': 'date'})) employment_type = forms.ChoiceField(label='Beschäftigungsverhältnis', choices=EMPLOYMENT_CHOICES, required=True) employment_end_date = forms.DateField( label='Enddatum (nur bei befristet)', required=False, widget=forms.DateInput(attrs={'type': 'date'}), ) handover_date = forms.DateField(label='Gewünschtes Übergabedatum der Geräte', required=False, widget=forms.DateInput(attrs={'type': 'date'})) group_mailboxes_required_choice = forms.ChoiceField(label='Gruppenpostfächer erforderlich?', choices=YES_NO_CHOICES, required=False) additional_software_needed_choice = forms.ChoiceField(label='Wird zusätzliche Software benötigt?', choices=YES_NO_CHOICES, required=False) additional_hardware_needed_choice = forms.ChoiceField(label='Darüber hinaus wird weitere Hardware benötigt?', choices=YES_NO_CHOICES, required=False) additional_access_needed_choice = forms.ChoiceField(label='Darüber hinaus werden weitere Zugänge benötigt?', choices=YES_NO_CHOICES, required=False) successor_required_choice = forms.ChoiceField(label='Neue Mitarbeitende ist Nachfolge von?', choices=YES_NO_CHOICES, required=False) inherit_phone_number_choice = forms.ChoiceField(label='Telefonnummer von Vorgängerperson übernehmen?', choices=YES_NO_CHOICES, required=False) order_business_cards = forms.BooleanField(label='Visitenkarten bestellen?', required=False) needed_devices_multi = forms.MultipleChoiceField( label='Benötigte Geräte und Gegenstände', choices=DEVICE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) needed_software_multi = forms.MultipleChoiceField( label='Benötigte Software', choices=SOFTWARE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) needed_accesses_multi = forms.MultipleChoiceField( label='Benötigte Zugänge', choices=ACCESS_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) needed_workspace_groups_multi = forms.MultipleChoiceField( label='Benötigte Gruppen im Workspace', choices=WORKSPACE_GROUP_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) needed_resources_multi = forms.MultipleChoiceField( label='Benötigte Ressourcen', choices=RESOURCE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) phone_number_choice = forms.CharField( label='Telefon-Direktwahl', required=False, widget=forms.TextInput(attrs={'placeholder': 'z. B. 030 44720212'}), ) additional_hardware_multi = forms.MultipleChoiceField( label='Zusätzliche Hardware', choices=HARDWARE_EXTRA_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) additional_software_multi = forms.MultipleChoiceField( label='Zusätzlich gewünschte Software', choices=SOFTWARE_EXTRA_CHOICES, required=False, widget=forms.CheckboxSelectMultiple, ) agreement_confirm = forms.BooleanField( label='Hiermit bestätige ich die Richtigkeit und Vollständigkeit meiner Angaben.', required=True, ) class Meta: model = OnboardingRequest fields = [ 'full_name', 'gender', 'job_title', 'department', 'work_email', 'contract_start', 'employment_type', 'employment_end_date', 'handover_date', 'order_business_cards', 'business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone', 'group_mailboxes', 'additional_software', 'additional_hardware_other', 'additional_access_text', 'needed_resources', 'successor_name', 'additional_notes', 'signature_image', ] widgets = { 'group_mailboxes': forms.Textarea(attrs={'rows': 2}), 'additional_software': forms.Textarea(attrs={'rows': 2}), 'additional_hardware_other': forms.Textarea(attrs={'rows': 2}), 'additional_access_text': forms.Textarea(attrs={'rows': 2}), 'successor_name': forms.TextInput(), 'additional_notes': forms.Textarea(attrs={'rows': 4}), } @staticmethod def _choices_from_options(category: str, fallback: list[tuple[str, str]]) -> list[tuple[str, str]]: options = FormOption.objects.filter(category=category, is_active=True).order_by('sort_order', 'label') if not options.exists(): return fallback language_code = get_language() return [(o.value or o.label, o.translated_label(language_code)) for o in options] def __init__(self, *args, **kwargs): self.requester_email = (kwargs.pop('requester_email', '') or '').strip().lower() super().__init__(*args, **kwargs) self.email_domain = get_company_email_domain() config = WorkflowConfig.objects.order_by('id').first() self.handover_lead_days = max(0, int(getattr(config, 'device_handover_lead_days', 5) or 5)) minimum_handover_date = timezone.localdate() + timedelta(days=self.handover_lead_days) self.fields['handover_date'].widget.attrs['min'] = minimum_handover_date.isoformat() self.fields['full_name'].label = 'Name' self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain} full_name_initial = (self.initial.get('full_name') or '').strip() if full_name_initial and not self.initial.get('first_name') and not self.initial.get('last_name'): name_parts = full_name_initial.split() if name_parts: self.fields['first_name'].initial = name_parts[0] self.fields['last_name'].initial = ' '.join(name_parts[1:]) if len(name_parts) > 1 else '' self.fields['department'].choices = self._choices_from_options('department', DEPARTMENT_CHOICES) self.fields['needed_devices_multi'].choices = self._choices_from_options('device', DEVICE_CHOICES) self.fields['needed_software_multi'].choices = self._choices_from_options('software', SOFTWARE_CHOICES) self.fields['needed_accesses_multi'].choices = self._choices_from_options('access', ACCESS_CHOICES) self.fields['needed_workspace_groups_multi'].choices = self._choices_from_options('workspace_group', WORKSPACE_GROUP_CHOICES) self.fields['needed_resources_multi'].choices = self._choices_from_options('resource', RESOURCE_CHOICES) self.fields['signature_image'].required = False apply_form_field_config('onboarding', self) def clean_work_email(self): value = (self.cleaned_data.get('work_email') or '').strip().lower() if not value: return value expected_suffix = f'@{self.email_domain}' if self.email_domain and not value.endswith(expected_suffix): raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain}) return value def clean_signature_image(self): image = self.cleaned_data.get('signature_image') if not image: return image max_size = 4 * 1024 * 1024 # 4 MB if image.size > max_size: raise forms.ValidationError('Die Signatur-Datei ist zu groß (max. 4 MB).') content_type = (getattr(image, 'content_type', '') or '').lower().strip() extension = Path(getattr(image, 'name', '')).suffix.lower() allowed_content_types = { 'image/png', 'image/x-png', 'image/jpeg', 'image/jpg', 'image/pjpeg', } allowed_extensions = {'.png', '.jpg', '.jpeg'} if content_type and not content_type.startswith('image/'): raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.') if content_type and content_type not in allowed_content_types and extension not in allowed_extensions: raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.') if not content_type and extension not in allowed_extensions: raise forms.ValidationError('Bitte eine PNG- oder JPG-Datei hochladen.') try: header = image.read(16) image.seek(0) except Exception: raise forms.ValidationError('Die Signatur-Datei konnte nicht gelesen werden.') is_png = header.startswith(b'\x89PNG\r\n\x1a\n') is_jpeg = header.startswith(b'\xff\xd8\xff') if not (is_png or is_jpeg): raise forms.ValidationError('Die Signatur-Datei ist kein gültiges PNG/JPG-Bild.') return image def clean(self): cleaned = super().clean() has = self.fields.__contains__ first_name = (cleaned.get('first_name') or '').strip() last_name = (cleaned.get('last_name') or '').strip() full_name = (cleaned.get('full_name') or '').strip() if first_name and last_name: cleaned['full_name'] = f'{first_name} {last_name}'.strip() elif full_name: parts = full_name.split() cleaned['first_name'] = parts[0] if parts else '' cleaned['last_name'] = ' '.join(parts[1:]) if len(parts) > 1 else '' else: if not first_name: self.add_error('first_name', 'Bitte Vornamen eingeben.') if not last_name: self.add_error('last_name', 'Bitte Nachnamen eingeben.') if has('employment_end_date') and cleaned.get('employment_type') == 'befristet' and not cleaned.get('employment_end_date'): self.add_error('employment_end_date', 'Bei befristeter Beschäftigung ist ein Enddatum erforderlich.') if cleaned.get('order_business_cards'): for f in ['business_card_name', 'business_card_title', 'business_card_email', 'business_card_phone']: if has(f) and not cleaned.get(f): self.add_error(f, 'Dieses Feld ist für die Visitenkartenbestellung erforderlich.') if has('group_mailboxes') and cleaned.get('group_mailboxes_required_choice') == 'ja' and not cleaned.get('group_mailboxes'): self.add_error('group_mailboxes', 'Bitte Gruppenpostfächer eintragen.') if has('additional_hardware_multi') and cleaned.get('additional_hardware_needed_choice') == 'ja' and not cleaned.get('additional_hardware_multi'): self.add_error('additional_hardware_multi', 'Bitte mindestens eine Hardware-Option wählen.') if has('additional_software_multi') and cleaned.get('additional_software_needed_choice') == 'ja' and not cleaned.get('additional_software_multi'): self.add_error('additional_software_multi', 'Bitte mindestens eine Software-Option wählen.') if has('additional_access_text') and cleaned.get('additional_access_needed_choice') == 'ja' and not cleaned.get('additional_access_text'): self.add_error('additional_access_text', 'Bitte zusätzliche Zugänge eintragen.') if has('successor_name') and cleaned.get('successor_required_choice') == 'ja' and not cleaned.get('successor_name'): self.add_error('successor_name', 'Bitte Name der Vorgängerperson eintragen.') handover_date = cleaned.get('handover_date') if has('handover_date') and handover_date: minimum_date = timezone.localdate() + timedelta(days=getattr(self, 'handover_lead_days', 5)) if handover_date < minimum_date: self.add_error( 'handover_date', _('Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen (frühestens %(date)s).') % { 'days': getattr(self, 'handover_lead_days', 5), 'date': minimum_date.strftime('%Y-%m-%d'), }, ) return cleaned def save(self, commit=True): instance = super().save(commit=False) if not (instance.preferred_language or '').strip(): instance.preferred_language = (get_language() or 'de').split('-')[0] instance.group_mailboxes_required = self.cleaned_data.get('group_mailboxes_required_choice') == 'ja' instance.additional_software_needed = self.cleaned_data.get('additional_software_needed_choice') == 'ja' instance.additional_hardware_needed = self.cleaned_data.get('additional_hardware_needed_choice') == 'ja' instance.additional_access_needed = self.cleaned_data.get('additional_access_needed_choice') == 'ja' instance.successor_required = self.cleaned_data.get('successor_required_choice') == 'ja' instance.inherit_phone_number = self.cleaned_data.get('inherit_phone_number_choice') == 'ja' instance.needed_devices = '\n'.join(self.cleaned_data.get('needed_devices_multi', [])) instance.needed_software = '\n'.join(self.cleaned_data.get('needed_software_multi', [])) instance.needed_accesses = '\n'.join(self.cleaned_data.get('needed_accesses_multi', [])) instance.needed_workspace_groups = '\n'.join(self.cleaned_data.get('needed_workspace_groups_multi', [])) instance.needed_resources = '\n'.join(self.cleaned_data.get('needed_resources_multi', [])) instance.additional_hardware = '\n'.join(self.cleaned_data.get('additional_hardware_multi', [])) selected_extra_software = self.cleaned_data.get('additional_software_multi', []) free_software = self.cleaned_data.get('additional_software', '').strip() all_extra_software = list(selected_extra_software) if free_software: all_extra_software.append(free_software) instance.additional_software = '\n'.join([s for s in all_extra_software if s]) if not instance.successor_required: instance.successor_name = '' instance.inherit_phone_number = False if instance.inherit_phone_number: instance.phone_number = '' else: instance.phone_number = self.cleaned_data.get('phone_number_choice', '') instance.agreement = 'accepted' if self.cleaned_data.get('agreement_confirm') else '' instance.onboarded_by_email = self.requester_email if commit: instance.save() return instance class OffboardingRequestForm(forms.ModelForm): search_query = forms.CharField( label='Mitarbeitende suchen (Name oder E-Mail)', required=False, help_text='Optional: Suche zur automatischen Vorbefüllung aus bereits onboardeten Personen.', ) class Meta: model = OffboardingRequest fields = [ 'full_name', 'work_email', 'department', 'job_title', 'last_working_day', 'notes', ] widgets = { 'last_working_day': forms.DateInput(attrs={'type': 'date'}), 'notes': forms.Textarea(attrs={'rows': 3}), } def __init__(self, *args, **kwargs): prefill_profile = kwargs.pop('prefill_profile', None) super().__init__(*args, **kwargs) self.email_domain = get_company_email_domain() self.fields['full_name'].label = 'Vorname und Nachname' self.fields['work_email'].help_text = _('Bitte nutzen Sie das Format name@%(domain)s.') % {'domain': self.email_domain} if prefill_profile: self.fields['full_name'].initial = prefill_profile.full_name self.fields['work_email'].initial = prefill_profile.work_email self.fields['department'].initial = prefill_profile.department self.fields['job_title'].initial = prefill_profile.job_title apply_form_field_config('offboarding', self) def clean_work_email(self): value = (self.cleaned_data.get('work_email') or '').strip().lower() if not value: return value expected_suffix = f'@{self.email_domain}' if self.email_domain and not value.endswith(expected_suffix): raise forms.ValidationError(_('Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse.') % {'domain': self.email_domain}) return value