832 lines
37 KiB
Python
832 lines
37 KiB
Python
from django.conf import settings
|
|
from django.contrib.auth.hashers import check_password, make_password
|
|
from django.core.validators import FileExtensionValidator
|
|
from django.db import models
|
|
from django.utils.translation import get_language
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
def _normalized_language_code(value: str | None) -> str:
|
|
lang = (value or '').strip().split('-')[0].lower()
|
|
return lang or 'de'
|
|
|
|
|
|
class EmployeeProfile(models.Model):
|
|
full_name = models.CharField(max_length=255)
|
|
first_name = models.CharField(max_length=100)
|
|
last_name = models.CharField(max_length=155)
|
|
department = models.CharField(max_length=255, blank=True)
|
|
job_title = models.CharField(max_length=255, blank=True)
|
|
work_email = models.EmailField(unique=True)
|
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.full_name} <{self.work_email}>"
|
|
|
|
|
|
class UserProfile(models.Model):
|
|
NOTIFICATION_ONBOARDING_SUCCESS = 'onboarding_success'
|
|
NOTIFICATION_ONBOARDING_FAILURE = 'onboarding_failure'
|
|
NOTIFICATION_OFFBOARDING_SUCCESS = 'offboarding_success'
|
|
NOTIFICATION_OFFBOARDING_FAILURE = 'offboarding_failure'
|
|
NOTIFICATION_BACKUP_SUCCESS = 'backup_success'
|
|
NOTIFICATION_BACKUP_FAILURE = 'backup_failure'
|
|
NOTIFICATION_WELCOME_EMAIL_SUCCESS = 'welcome_email_success'
|
|
NOTIFICATION_WELCOME_EMAIL_FAILURE = 'welcome_email_failure'
|
|
NOTIFICATION_TRIAL_ALERTS = 'trial_alerts'
|
|
NOTIFICATION_SYSTEM_ALERTS = 'system_alerts'
|
|
NOTIFICATION_PREFERENCE_DEFAULTS = {
|
|
NOTIFICATION_ONBOARDING_SUCCESS: True,
|
|
NOTIFICATION_ONBOARDING_FAILURE: True,
|
|
NOTIFICATION_OFFBOARDING_SUCCESS: True,
|
|
NOTIFICATION_OFFBOARDING_FAILURE: True,
|
|
NOTIFICATION_BACKUP_SUCCESS: True,
|
|
NOTIFICATION_BACKUP_FAILURE: True,
|
|
NOTIFICATION_WELCOME_EMAIL_SUCCESS: True,
|
|
NOTIFICATION_WELCOME_EMAIL_FAILURE: True,
|
|
NOTIFICATION_TRIAL_ALERTS: True,
|
|
NOTIFICATION_SYSTEM_ALERTS: True,
|
|
}
|
|
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='profile')
|
|
avatar_image = models.FileField(
|
|
upload_to='profiles/',
|
|
blank=True,
|
|
null=True,
|
|
validators=[FileExtensionValidator(allowed_extensions=['png', 'jpg', 'jpeg', 'webp', 'svg'])],
|
|
)
|
|
phone_number = models.CharField(max_length=80, blank=True, default='')
|
|
mobile_number = models.CharField(max_length=80, blank=True, default='')
|
|
job_title = models.CharField(max_length=255, blank=True, default='')
|
|
department = models.CharField(max_length=255, blank=True, default='')
|
|
location = models.CharField(max_length=255, blank=True, default='')
|
|
contact_notes = models.CharField(max_length=255, blank=True, default='')
|
|
totp_secret = models.CharField(max_length=64, blank=True, default='')
|
|
totp_enabled = models.BooleanField(default=False)
|
|
totp_confirmed_at = models.DateTimeField(null=True, blank=True)
|
|
totp_recovery_codes = models.JSONField(default=list, blank=True)
|
|
notification_preferences = models.JSONField(default=dict, blank=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'User Profile'
|
|
verbose_name_plural = 'User Profiles'
|
|
|
|
def __str__(self) -> str:
|
|
return getattr(self.user, 'username', '') or str(self.user_id)
|
|
|
|
def disable_totp(self) -> None:
|
|
self.totp_secret = ''
|
|
self.totp_enabled = False
|
|
self.totp_confirmed_at = None
|
|
self.totp_recovery_codes = []
|
|
self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'totp_recovery_codes', 'updated_at'])
|
|
|
|
def enable_totp(self, secret: str, recovery_codes: list[str]) -> None:
|
|
self.totp_secret = secret
|
|
self.totp_enabled = True
|
|
self.totp_confirmed_at = timezone.now()
|
|
self.set_recovery_codes(recovery_codes)
|
|
self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'totp_recovery_codes', 'updated_at'])
|
|
|
|
def set_recovery_codes(self, recovery_codes: list[str]) -> None:
|
|
self.totp_recovery_codes = [make_password(code) for code in recovery_codes]
|
|
|
|
def consume_recovery_code(self, raw_code: str) -> bool:
|
|
remaining_hashes = []
|
|
matched = False
|
|
for hashed_code in self.totp_recovery_codes or []:
|
|
if not matched and check_password(raw_code, hashed_code):
|
|
matched = True
|
|
continue
|
|
remaining_hashes.append(hashed_code)
|
|
if matched:
|
|
self.totp_recovery_codes = remaining_hashes
|
|
self.save(update_fields=['totp_recovery_codes', 'updated_at'])
|
|
return matched
|
|
|
|
def get_notification_preferences(self) -> dict[str, bool]:
|
|
current = self.notification_preferences or {}
|
|
prefs = dict(self.NOTIFICATION_PREFERENCE_DEFAULTS)
|
|
for key in prefs:
|
|
if key in current:
|
|
prefs[key] = bool(current[key])
|
|
return prefs
|
|
|
|
def notification_enabled(self, event_key: str) -> bool:
|
|
return bool(self.get_notification_preferences().get(event_key, True))
|
|
|
|
|
|
class UserNotification(models.Model):
|
|
LEVEL_INFO = 'info'
|
|
LEVEL_SUCCESS = 'success'
|
|
LEVEL_WARNING = 'warning'
|
|
LEVEL_ERROR = 'error'
|
|
LEVEL_CHOICES = [
|
|
(LEVEL_INFO, _('Info')),
|
|
(LEVEL_SUCCESS, _('Erfolg')),
|
|
(LEVEL_WARNING, _('Warnung')),
|
|
(LEVEL_ERROR, _('Fehler')),
|
|
]
|
|
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notifications')
|
|
title = models.CharField(max_length=255)
|
|
body = models.TextField(blank=True, default='')
|
|
level = models.CharField(max_length=20, choices=LEVEL_CHOICES, default=LEVEL_INFO)
|
|
link_url = models.CharField(max_length=500, blank=True, default='')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
read_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at', '-id']
|
|
verbose_name = 'User Notification'
|
|
verbose_name_plural = 'User Notifications'
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.user_id} | {self.level} | {self.title}'
|
|
|
|
@property
|
|
def is_unread(self) -> bool:
|
|
return self.read_at is None
|
|
|
|
def mark_read(self) -> None:
|
|
if self.read_at is None:
|
|
self.read_at = timezone.now()
|
|
self.save(update_fields=['read_at'])
|
|
|
|
|
|
class PortalBranding(models.Model):
|
|
name = models.CharField(max_length=80, default='Default', unique=True)
|
|
portal_title = models.CharField(max_length=255, default='Workdock')
|
|
company_name = models.CharField(max_length=255, default='Workdock')
|
|
company_domain = models.CharField(max_length=120, blank=True, default='workdock.de')
|
|
support_email = models.EmailField(blank=True, default='info@workdock.de')
|
|
sender_display_name = models.CharField(max_length=255, blank=True, default='Workdock')
|
|
login_subtitle = models.CharField(max_length=255, blank=True, default='Bitte melden Sie sich mit Ihrem Benutzerkonto an.')
|
|
footer_text = models.CharField(max_length=255, blank=True, default='Workdock')
|
|
footer_text_en = models.CharField(max_length=255, blank=True, default='Workdock')
|
|
legal_notice = models.TextField(blank=True, default='')
|
|
legal_notice_en = models.TextField(blank=True, default='')
|
|
default_language = models.CharField(
|
|
max_length=10,
|
|
choices=[('de', 'Deutsch'), ('en', 'English')],
|
|
default='de',
|
|
)
|
|
logo_image = models.FileField(
|
|
upload_to='branding/',
|
|
blank=True,
|
|
null=True,
|
|
validators=[FileExtensionValidator(allowed_extensions=['svg', 'png', 'jpg', 'jpeg', 'webp'])],
|
|
)
|
|
pdf_letterhead = models.FileField(
|
|
upload_to='branding/',
|
|
blank=True,
|
|
null=True,
|
|
validators=[FileExtensionValidator(allowed_extensions=['pdf'])],
|
|
)
|
|
favicon_image = models.FileField(
|
|
upload_to='branding/',
|
|
blank=True,
|
|
null=True,
|
|
validators=[FileExtensionValidator(allowed_extensions=['ico', 'png', 'svg', 'webp'])],
|
|
)
|
|
primary_color = models.CharField(max_length=20, blank=True, default='#000078')
|
|
secondary_color = models.CharField(max_length=20, blank=True, default='#c0002b')
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Portal Branding'
|
|
verbose_name_plural = 'Portal Branding'
|
|
|
|
def __str__(self) -> str:
|
|
return self.portal_title or self.company_name or self.name
|
|
|
|
|
|
class PortalCompanyConfig(models.Model):
|
|
name = models.CharField(max_length=80, default='Default', unique=True)
|
|
legal_company_name = models.CharField(max_length=255, blank=True, default='')
|
|
street_address = models.CharField(max_length=255, blank=True, default='')
|
|
postal_code = models.CharField(max_length=50, blank=True, default='')
|
|
city = models.CharField(max_length=120, blank=True, default='')
|
|
country = models.CharField(max_length=120, blank=True, default='Deutschland')
|
|
website_url = models.URLField(blank=True, default='')
|
|
imprint_url = models.URLField(blank=True, default='')
|
|
privacy_url = models.URLField(blank=True, default='')
|
|
hr_contact_email = models.EmailField(blank=True, default='')
|
|
it_contact_email = models.EmailField(blank=True, default='')
|
|
operations_contact_email = models.EmailField(blank=True, default='')
|
|
phone_number = models.CharField(max_length=80, blank=True, default='')
|
|
vat_id = models.CharField(max_length=80, blank=True, default='')
|
|
registration_number = models.CharField(max_length=120, blank=True, default='')
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Portal Company Config'
|
|
verbose_name_plural = 'Portal Company Config'
|
|
|
|
def __str__(self) -> str:
|
|
return self.legal_company_name or self.name
|
|
|
|
|
|
class PortalTrialConfig(models.Model):
|
|
name = models.CharField(max_length=80, default='Default', unique=True)
|
|
is_trial_mode = models.BooleanField(default=False)
|
|
trial_started_at = models.DateTimeField(null=True, blank=True)
|
|
trial_expires_at = models.DateTimeField(null=True, blank=True)
|
|
restrict_production_integrations = models.BooleanField(default=True)
|
|
auto_cleanup_enabled = models.BooleanField(default=True)
|
|
trial_banner_text = models.CharField(max_length=255, blank=True, default='')
|
|
trial_banner_text_en = models.CharField(max_length=255, blank=True, default='')
|
|
last_cleanup_at = models.DateTimeField(null=True, blank=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'Portal Trial Config'
|
|
verbose_name_plural = 'Portal Trial Config'
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class PortalAppConfig(models.Model):
|
|
SECTION_APP = 'app'
|
|
SECTION_PLATFORM = 'platform'
|
|
SECTION_ADMIN = 'admin'
|
|
SECTION_CHOICES = [
|
|
(SECTION_APP, _('Apps')),
|
|
(SECTION_PLATFORM, _('Platform Apps')),
|
|
(SECTION_ADMIN, _('Admin Apps')),
|
|
]
|
|
|
|
key = models.CharField(max_length=80, unique=True)
|
|
section = models.CharField(max_length=20, choices=SECTION_CHOICES, default=SECTION_APP)
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
is_enabled = models.BooleanField(default=True)
|
|
visible_to_super_admin = models.BooleanField(default=True)
|
|
visible_to_admin = models.BooleanField(default=True)
|
|
visible_to_it_staff = models.BooleanField(default=False)
|
|
visible_to_staff = models.BooleanField(default=False)
|
|
title_override = models.CharField(max_length=255, blank=True)
|
|
title_override_en = models.CharField(max_length=255, blank=True)
|
|
description_override = models.TextField(blank=True)
|
|
description_override_en = models.TextField(blank=True)
|
|
action_label_override = models.CharField(max_length=255, blank=True)
|
|
action_label_override_en = models.CharField(max_length=255, blank=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['section', 'sort_order', 'key']
|
|
verbose_name = 'Portal App'
|
|
verbose_name_plural = 'Portal Apps'
|
|
|
|
def __str__(self) -> str:
|
|
return self.key
|
|
|
|
def _translated_value(self, field_name: str, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en':
|
|
english_value = (getattr(self, f'{field_name}_en', '') or '').strip()
|
|
if english_value:
|
|
return english_value
|
|
return (getattr(self, field_name, '') or '').strip()
|
|
|
|
def translated_title_override(self, language_code: str | None = None) -> str:
|
|
return self._translated_value('title_override', language_code)
|
|
|
|
def translated_description_override(self, language_code: str | None = None) -> str:
|
|
return self._translated_value('description_override', language_code)
|
|
|
|
def translated_action_label_override(self, language_code: str | None = None) -> str:
|
|
return self._translated_value('action_label_override', language_code)
|
|
|
|
|
|
class AsyncTaskLog(models.Model):
|
|
STATUS_CHOICES = [
|
|
('started', _('Gestartet')),
|
|
('succeeded', _('Erfolgreich')),
|
|
('failed', _('Fehlgeschlagen')),
|
|
]
|
|
|
|
task_name = models.CharField(max_length=255)
|
|
task_id = models.CharField(max_length=255, blank=True)
|
|
target_type = models.CharField(max_length=80, blank=True)
|
|
target_id = models.PositiveIntegerField(null=True, blank=True)
|
|
target_label = models.CharField(max_length=255, blank=True)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='started')
|
|
error_message = models.TextField(blank=True)
|
|
started_at = models.DateTimeField(auto_now_add=True)
|
|
finished_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['-started_at', '-id']
|
|
verbose_name = 'Async Task Log'
|
|
verbose_name_plural = 'Async Task Logs'
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.task_name} | {self.status} | {self.target_label or self.target_type}'
|
|
|
|
|
|
class AdminAuditLog(models.Model):
|
|
actor = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name='admin_audit_logs',
|
|
)
|
|
actor_display = models.CharField(max_length=255, blank=True)
|
|
action = models.CharField(max_length=120)
|
|
target_type = models.CharField(max_length=80, blank=True)
|
|
target_id = models.PositiveIntegerField(null=True, blank=True)
|
|
target_label = models.CharField(max_length=255, blank=True)
|
|
details = models.JSONField(default=dict, blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at', '-id']
|
|
verbose_name = 'Admin Audit Log'
|
|
verbose_name_plural = 'Admin Audit Logs'
|
|
|
|
def __str__(self) -> str:
|
|
actor = self.actor_display or 'Unbekannt'
|
|
return f'{self.created_at:%Y-%m-%d %H:%M} | {actor} | {self.action}'
|
|
|
|
|
|
class OnboardingRequest(models.Model):
|
|
STATUS_CHOICES = [
|
|
('submitted', _('Eingereicht')),
|
|
('processing', _('In Bearbeitung')),
|
|
('completed', _('Abgeschlossen')),
|
|
('failed', _('Fehlgeschlagen')),
|
|
]
|
|
|
|
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
|
gender = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
choices=[('herr', _('Herr')), ('frau', _('Frau')), ('divers', _('Divers'))],
|
|
verbose_name='Anrede',
|
|
)
|
|
job_title = models.CharField(max_length=255, blank=True, verbose_name='Berufsbezeichnung')
|
|
department = models.CharField(max_length=255, blank=True, verbose_name='Abteilung')
|
|
work_email = models.EmailField(verbose_name='Gewünschte dienstliche E-Mail-Adresse')
|
|
contract_start = models.DateField(verbose_name='Vertragsbeginn')
|
|
employment_type = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
choices=[('befristet', _('befristet')), ('unbefristet', _('unbefristet'))],
|
|
verbose_name='Beschäftigungsverhältnis',
|
|
)
|
|
employment_end_date = models.DateField(null=True, blank=True, verbose_name='Enddatum (nur bei befristet)')
|
|
handover_date = models.DateField(null=True, blank=True, verbose_name='Gewünschtes Übergabedatum der Geräte')
|
|
|
|
order_business_cards = models.BooleanField(default=False, verbose_name='Bestellung Visitenkarten')
|
|
business_card_name = models.CharField(max_length=255, blank=True, verbose_name='Name (Visitenkarte)')
|
|
business_card_title = models.CharField(max_length=255, blank=True, verbose_name='Titel (Visitenkarte)')
|
|
business_card_email = models.EmailField(blank=True, verbose_name='E-Mailadresse (Visitenkarte)')
|
|
business_card_phone = models.CharField(max_length=100, blank=True, verbose_name='Telefonnummer (Visitenkarte)')
|
|
|
|
group_mailboxes_required = models.BooleanField(default=False, verbose_name='Gruppenpostfächer erforderlich?')
|
|
group_mailboxes = models.TextField(blank=True, verbose_name='Gruppenpostfächer')
|
|
|
|
needed_devices = models.TextField(blank=True, verbose_name='Benötigte Geräte und Gegenstände')
|
|
needed_software = models.TextField(blank=True, verbose_name='Benötigte Software')
|
|
needed_accesses = models.TextField(blank=True, verbose_name='Benötigte Zugänge')
|
|
needed_workspace_groups = models.TextField(blank=True, verbose_name='Benötigte Gruppen im Workspace')
|
|
|
|
additional_software_needed = models.BooleanField(default=False, verbose_name='Wird zusätzliche Software benötigt?')
|
|
additional_software = models.TextField(blank=True, verbose_name='Zusätzlich gewünschte Software (ohne Garantie)')
|
|
additional_hardware_needed = models.BooleanField(default=False, verbose_name='Wird zusätzliche Hardware benötigt?')
|
|
additional_hardware = models.TextField(blank=True, verbose_name='Zusätzliche Hardware')
|
|
additional_hardware_other = models.TextField(blank=True, verbose_name='Weitere Hardware (Freitext)')
|
|
additional_access_needed = models.BooleanField(default=False, verbose_name='Werden weitere Zugänge benötigt?')
|
|
additional_access_text = models.TextField(blank=True, verbose_name='Weitere Zugänge (Freitext)')
|
|
needed_resources = models.TextField(blank=True, verbose_name='Benötigte Ressourcen')
|
|
phone_number = models.CharField(max_length=100, blank=True, verbose_name='Telefon-Direktwahl')
|
|
successor_required = models.BooleanField(default=False, verbose_name='Neue Mitarbeitende ist Nachfolge von?')
|
|
successor_name = models.CharField(max_length=255, blank=True, verbose_name='Name der Vorgängerperson')
|
|
inherit_phone_number = models.BooleanField(default=False, verbose_name='Telefonnummer von Vorgängerperson übernehmen')
|
|
|
|
additional_notes = models.TextField(blank=True, verbose_name='Raum für zusätzliche Anmerkungen und Wünsche')
|
|
onboarded_by_email = models.EmailField(blank=True, verbose_name='E-Mail der anfordernden Person')
|
|
onboarded_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person')
|
|
agreement = models.TextField(blank=True, verbose_name='Vereinbarung')
|
|
signature_url = models.URLField(blank=True, verbose_name='Unterschrift')
|
|
signature_image = models.ImageField(upload_to='signatures/', blank=True, null=True, verbose_name='Unterschrift (Bilddatei)')
|
|
|
|
personalized_text = models.TextField(
|
|
blank=True,
|
|
verbose_name='Personalisierter Text für PDF',
|
|
help_text='Optionaler individueller Textblock im Onboarding PDF.',
|
|
)
|
|
|
|
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
|
intro_pdf_path = models.CharField(max_length=500, blank=True)
|
|
processing_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted')
|
|
last_error = models.TextField(blank=True)
|
|
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self) -> str:
|
|
return f"Onboarding #{self.id} - {self.full_name}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.preferred_language = _normalized_language_code(self.preferred_language)
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class FormOption(models.Model):
|
|
CATEGORY_CHOICES = [
|
|
('department', _('Abteilung')),
|
|
('device', _('Geräte')),
|
|
('software', _('Software')),
|
|
('access', _('Zugänge')),
|
|
('workspace_group', _('Workspace-Gruppen')),
|
|
('resource', _('Ressourcen')),
|
|
('phone', _('Telefonnummern')),
|
|
]
|
|
|
|
category = models.CharField(max_length=40, choices=CATEGORY_CHOICES)
|
|
label = models.CharField(max_length=255)
|
|
label_en = models.CharField(max_length=255, blank=True)
|
|
value = models.CharField(max_length=255, blank=True)
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
is_active = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['category', 'sort_order', 'label']
|
|
unique_together = ('category', 'label')
|
|
|
|
def __str__(self) -> str:
|
|
return f"{self.get_category_display()}: {self.label}"
|
|
|
|
def translated_label(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.label_en.strip():
|
|
return self.label_en.strip()
|
|
return self.label.strip()
|
|
|
|
|
|
class FormFieldConfig(models.Model):
|
|
PAGE_CHOICES = [
|
|
('', _('Automatisch')),
|
|
('stammdaten', _('Stammdaten')),
|
|
('vertrag', _('Vertrag')),
|
|
('itsetup', _('IT-Setup')),
|
|
('abschluss', _('Abschluss')),
|
|
]
|
|
FORM_CHOICES = [
|
|
('onboarding', _('Onboarding')),
|
|
('offboarding', _('Offboarding')),
|
|
]
|
|
|
|
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
|
field_name = models.CharField(max_length=80)
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
is_visible = models.BooleanField(default=True)
|
|
is_required = models.BooleanField(null=True, blank=True, default=None)
|
|
page_key = models.CharField(max_length=20, blank=True, default='', choices=PAGE_CHOICES)
|
|
label_override = models.CharField(max_length=255, blank=True)
|
|
label_override_en = models.CharField(max_length=255, blank=True)
|
|
help_text_override = models.TextField(blank=True)
|
|
help_text_override_en = models.TextField(blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['form_type', 'sort_order', 'field_name']
|
|
unique_together = ('form_type', 'field_name')
|
|
verbose_name = 'Formularfeld-Konfiguration'
|
|
verbose_name_plural = 'Formularfeld-Konfigurationen'
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.get_form_type_display()}: {self.field_name}'
|
|
|
|
def translated_label_override(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.label_override_en.strip():
|
|
return self.label_override_en.strip()
|
|
return self.label_override.strip()
|
|
|
|
def translated_help_text_override(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.help_text_override_en.strip():
|
|
return self.help_text_override_en.strip()
|
|
return self.help_text_override.strip()
|
|
|
|
|
|
class FormSectionConfig(models.Model):
|
|
FORM_CHOICES = [
|
|
('onboarding', _('Onboarding')),
|
|
]
|
|
SECTION_CHOICES = [
|
|
('stammdaten', _('Stammdaten')),
|
|
('vertrag', _('Vertrag')),
|
|
('itsetup', _('IT-Setup')),
|
|
('abschluss', _('Abschluss')),
|
|
]
|
|
|
|
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
|
section_key = models.CharField(max_length=20, choices=SECTION_CHOICES)
|
|
is_visible = models.BooleanField(default=True)
|
|
|
|
class Meta:
|
|
ordering = ['form_type', 'section_key']
|
|
unique_together = ('form_type', 'section_key')
|
|
verbose_name = 'Formularabschnitt-Konfiguration'
|
|
verbose_name_plural = 'Formularabschnitt-Konfigurationen'
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.form_type}: {self.section_key}'
|
|
|
|
|
|
class NotificationTemplate(models.Model):
|
|
TEMPLATE_CHOICES = [
|
|
('onboarding_it', _('Onboarding: IT')),
|
|
('onboarding_general_info', _('Onboarding: Allgemeine Info')),
|
|
('onboarding_business_card', _('Onboarding: Visitenkarte')),
|
|
('onboarding_hr_works', _('Onboarding: HR Works')),
|
|
('onboarding_key', _('Onboarding: Schlüssel')),
|
|
('onboarding_reference', _('Onboarding: Referenz Anfordernde Person')),
|
|
('onboarding_welcome', _('Onboarding: Welcome E-Mail')),
|
|
('offboarding_it', _('Offboarding: IT')),
|
|
('offboarding_general_info', _('Offboarding: Allgemeine Info')),
|
|
('offboarding_hr_works_disable', _('Offboarding: HR Works Deaktivierung')),
|
|
('offboarding_reference', _('Offboarding: Referenz Anfordernde Person')),
|
|
]
|
|
|
|
key = models.CharField(max_length=60, choices=TEMPLATE_CHOICES, unique=True)
|
|
subject_template = models.CharField(max_length=255)
|
|
subject_template_en = models.CharField(max_length=255, blank=True)
|
|
body_template = models.TextField()
|
|
body_template_en = models.TextField(blank=True)
|
|
is_active = models.BooleanField(default=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['key']
|
|
|
|
def __str__(self) -> str:
|
|
return self.get_key_display()
|
|
|
|
def translated_subject_template(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.subject_template_en.strip():
|
|
return self.subject_template_en.strip()
|
|
return self.subject_template.strip()
|
|
|
|
def translated_body_template(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.body_template_en.strip():
|
|
return self.body_template_en.strip()
|
|
return self.body_template.strip()
|
|
|
|
|
|
class NotificationRule(models.Model):
|
|
EVENT_CHOICES = [
|
|
('onboarding', _('Onboarding')),
|
|
('offboarding', _('Offboarding')),
|
|
]
|
|
OPERATOR_CHOICES = [
|
|
('always', _('Immer')),
|
|
('contains', _('Enthält')),
|
|
('equals', _('Ist gleich')),
|
|
('is_true', _('Ist aktiv/Ja')),
|
|
('is_false', _('Ist inaktiv/Nein')),
|
|
]
|
|
|
|
name = models.CharField(max_length=120)
|
|
is_active = models.BooleanField(default=True)
|
|
event_type = models.CharField(max_length=20, choices=EVENT_CHOICES)
|
|
field_name = models.CharField(max_length=80, blank=True)
|
|
operator = models.CharField(max_length=20, choices=OPERATOR_CHOICES, default='always')
|
|
expected_value = models.CharField(max_length=255, blank=True)
|
|
recipients = models.TextField(
|
|
help_text='Mehrere E-Mail-Adressen mit Komma, Semikolon oder Zeilenumbruch trennen.'
|
|
)
|
|
template_key = models.CharField(max_length=60, blank=True)
|
|
custom_subject = models.CharField(max_length=255, blank=True)
|
|
custom_subject_en = models.CharField(max_length=255, blank=True)
|
|
custom_body = models.TextField(blank=True)
|
|
custom_body_en = models.TextField(blank=True)
|
|
include_pdf_attachment = models.BooleanField(default=False)
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['event_type', 'sort_order', 'id']
|
|
|
|
def __str__(self) -> str:
|
|
state = 'aktiv' if self.is_active else 'inaktiv'
|
|
return f'{self.get_event_type_display()} | {self.name} ({state})'
|
|
|
|
def translated_custom_subject(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.custom_subject_en.strip():
|
|
return self.custom_subject_en.strip()
|
|
return self.custom_subject.strip()
|
|
|
|
def translated_custom_body(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.custom_body_en.strip():
|
|
return self.custom_body_en.strip()
|
|
return self.custom_body.strip()
|
|
|
|
|
|
class ScheduledWelcomeEmail(models.Model):
|
|
STATUS_CHOICES = [
|
|
('scheduled', _('Geplant')),
|
|
('paused', _('Pausiert')),
|
|
('cancelled', _('Abgebrochen')),
|
|
('sent', _('Gesendet')),
|
|
('failed', _('Fehlgeschlagen')),
|
|
]
|
|
|
|
onboarding_request = models.OneToOneField(OnboardingRequest, on_delete=models.CASCADE)
|
|
recipient_email = models.EmailField()
|
|
send_at = models.DateTimeField()
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='scheduled')
|
|
celery_task_id = models.CharField(max_length=100, blank=True)
|
|
sent_at = models.DateTimeField(null=True, blank=True)
|
|
last_error = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-send_at', '-id']
|
|
|
|
def __str__(self) -> str:
|
|
return f'Welcome #{self.id} | {self.recipient_email} | {self.status}'
|
|
|
|
|
|
class IntroChecklistItem(models.Model):
|
|
SECTION_CHOICES = [
|
|
('workplace', _('Geräte und Arbeitsplatz')),
|
|
('accounts', _('Konten und Berechtigungen')),
|
|
('software', _('Software und Tools')),
|
|
('process', _('Prozesse und Hinweise')),
|
|
]
|
|
OPERATOR_CHOICES = [
|
|
('always', _('Immer anzeigen')),
|
|
('contains', _('Enthält')),
|
|
('equals', _('Ist gleich')),
|
|
('is_true', _('Ist Ja / aktiv')),
|
|
('is_false', _('Ist Nein / inaktiv')),
|
|
]
|
|
|
|
section = models.CharField(max_length=30, choices=SECTION_CHOICES)
|
|
label = models.CharField(max_length=255)
|
|
label_en = models.CharField(max_length=255, blank=True)
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
is_active = models.BooleanField(default=True)
|
|
condition_field = models.CharField(max_length=80, blank=True)
|
|
condition_operator = models.CharField(max_length=20, choices=OPERATOR_CHOICES, default='always')
|
|
condition_value = models.CharField(max_length=255, blank=True)
|
|
|
|
class Meta:
|
|
ordering = ['section', 'sort_order', 'label']
|
|
|
|
def __str__(self) -> str:
|
|
return f'{self.get_section_display()}: {self.label}'
|
|
|
|
def translated_label(self, language_code: str | None = None) -> str:
|
|
lang = (language_code or get_language() or 'de').split('-')[0]
|
|
if lang == 'en' and self.label_en.strip():
|
|
return self.label_en.strip()
|
|
return self.label.strip()
|
|
|
|
|
|
class OnboardingIntroductionSession(models.Model):
|
|
STATUS_CHOICES = [
|
|
('draft', _('Entwurf')),
|
|
('completed', _('Abgeschlossen')),
|
|
]
|
|
|
|
onboarding_request = models.OneToOneField(OnboardingRequest, on_delete=models.CASCADE)
|
|
checklist_state = models.JSONField(default=dict, blank=True)
|
|
notes = models.TextField(blank=True)
|
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
|
|
completed_at = models.DateTimeField(null=True, blank=True)
|
|
completed_by_name = models.CharField(max_length=255, blank=True)
|
|
exported_pdf_path = models.CharField(max_length=500, blank=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self) -> str:
|
|
return f'Einweisung #{self.id} | {self.onboarding_request.full_name} | {self.status}'
|
|
|
|
|
|
class WorkflowConfig(models.Model):
|
|
REMOTE_BACKUP_TARGET_CHOICES = [
|
|
('nextcloud', _('Nextcloud')),
|
|
('s3', _('S3')),
|
|
('nfs', _('NFS')),
|
|
]
|
|
|
|
name = models.CharField(max_length=120, default='Default', unique=True)
|
|
it_onboarding_email = models.EmailField(blank=True)
|
|
general_info_email = models.EmailField(blank=True)
|
|
business_card_email = models.EmailField(blank=True)
|
|
hr_works_email = models.EmailField(blank=True)
|
|
key_notification_email = models.EmailField(blank=True)
|
|
nextcloud_enabled_override = models.BooleanField(
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
verbose_name='Nextcloud Upload aktiviert (Override)',
|
|
help_text='Leer = ENV-Wert nutzen, Ja = erzwingen aktiv, Nein = erzwingen inaktiv',
|
|
)
|
|
email_test_mode_override = models.BooleanField(
|
|
null=True,
|
|
blank=True,
|
|
default=None,
|
|
verbose_name='E-Mail Testmodus aktiv (Override)',
|
|
help_text='Leer = ENV-Wert nutzen, Ja = Testmodus erzwingen, Nein = Produktionsmodus erzwingen',
|
|
)
|
|
nextcloud_base_url_override = models.CharField(max_length=500, blank=True, verbose_name='Nextcloud Base URL (Override)')
|
|
nextcloud_username_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Benutzername (Override)')
|
|
nextcloud_password_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Passwort (Override)')
|
|
nextcloud_directory_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Verzeichnis (Override)')
|
|
sync_interval_seconds = models.PositiveIntegerField(default=60, verbose_name='Sync-Intervall (Sekunden)')
|
|
device_handover_lead_days = models.PositiveIntegerField(default=5, verbose_name='Vorlauf Geräteübergabe (Tage)')
|
|
remote_backup_enabled = models.BooleanField(default=False, verbose_name='Remote Backup aktiviert')
|
|
remote_backup_target_type = models.CharField(
|
|
max_length=20,
|
|
choices=REMOTE_BACKUP_TARGET_CHOICES,
|
|
default='nextcloud',
|
|
verbose_name='Remote Backup Zieltyp',
|
|
)
|
|
remote_backup_nextcloud_directory = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Backup-Verzeichnis')
|
|
remote_backup_s3_bucket = models.CharField(max_length=255, blank=True, verbose_name='S3 Bucket (optional)')
|
|
remote_backup_nfs_path = models.CharField(max_length=255, blank=True, verbose_name='NFS Pfad (optional)')
|
|
welcome_email_delay_days = models.PositiveIntegerField(default=5, verbose_name='Welcome E-Mail Verzögerung (Tage)')
|
|
welcome_sender_email = models.EmailField(blank=True, verbose_name='Welcome E-Mail Absender')
|
|
welcome_include_pdf = models.BooleanField(default=True, verbose_name='Welcome E-Mail mit PDF-Anhang')
|
|
|
|
imap_server = models.CharField(max_length=255, blank=True, verbose_name='IMAP Server')
|
|
mailbox = models.CharField(max_length=120, blank=True, default='INBOX', verbose_name='Mailbox')
|
|
smtp_server = models.CharField(max_length=255, blank=True, verbose_name='SMTP Server')
|
|
smtp_port = models.PositiveIntegerField(default=465, verbose_name='SMTP Port')
|
|
email_account = models.EmailField(blank=True, verbose_name='E-Mail Konto')
|
|
email_password = models.CharField(max_length=255, blank=True, verbose_name='E-Mail Passwort')
|
|
smtp_use_ssl = models.BooleanField(default=True, verbose_name='SMTP SSL nutzen')
|
|
smtp_use_tls = models.BooleanField(default=False, verbose_name='SMTP TLS nutzen')
|
|
|
|
legal_text = models.TextField(
|
|
blank=True,
|
|
default='Eine Ausrüstungsvereinbarung erlaubt es einem Mitarbeitenden, die Ausrüstung des Unternehmens im Außendienst oder zu Hause zu nutzen und mitzunehmen.',
|
|
)
|
|
|
|
def __str__(self) -> str:
|
|
return self.name
|
|
|
|
|
|
class SystemEmailConfig(models.Model):
|
|
name = models.CharField(max_length=120, default='Default SMTP', unique=True)
|
|
is_active = models.BooleanField(default=False)
|
|
host = models.CharField(max_length=255, blank=True)
|
|
port = models.PositiveIntegerField(default=587)
|
|
username = models.CharField(max_length=255, blank=True)
|
|
password = models.CharField(max_length=255, blank=True)
|
|
use_tls = models.BooleanField(default=True)
|
|
use_ssl = models.BooleanField(default=False)
|
|
from_email = models.EmailField(blank=True)
|
|
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = 'System SMTP Konfiguration'
|
|
verbose_name_plural = 'System SMTP Konfigurationen'
|
|
|
|
def __str__(self) -> str:
|
|
state = 'aktiv' if self.is_active else 'inaktiv'
|
|
return f'{self.name} ({state})'
|
|
|
|
|
|
class OffboardingRequest(models.Model):
|
|
STATUS_CHOICES = OnboardingRequest.STATUS_CHOICES
|
|
|
|
employee_profile = models.ForeignKey(EmployeeProfile, null=True, blank=True, on_delete=models.SET_NULL)
|
|
full_name = models.CharField(max_length=255, verbose_name='Vorname und Nachname')
|
|
work_email = models.EmailField(verbose_name='Dienstliche E-Mail-Adresse')
|
|
department = models.CharField(max_length=255, blank=True, verbose_name='Abteilung')
|
|
job_title = models.CharField(max_length=255, blank=True, verbose_name='Berufsbezeichnung')
|
|
last_working_day = models.DateField(verbose_name='Letzter Arbeitstag')
|
|
offboarding_reason = models.TextField(blank=True, verbose_name='Grund')
|
|
notes = models.TextField(blank=True, verbose_name='Notizen')
|
|
signature = models.CharField(max_length=255, blank=True, verbose_name='Unterschrift (Name)')
|
|
requested_by_email = models.EmailField(verbose_name='E-Mail der anfordernden Person')
|
|
requested_by_name = models.CharField(max_length=255, blank=True, verbose_name='Name der anfordernden Person')
|
|
preferred_language = models.CharField(max_length=10, blank=True, default='de', db_default='de')
|
|
generated_pdf_path = models.CharField(max_length=500, blank=True)
|
|
processing_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='submitted')
|
|
last_error = models.TextField(blank=True)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
def __str__(self) -> str:
|
|
return f"Offboarding #{self.id} - {self.full_name}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.preferred_language = _normalized_language_code(self.preferred_language)
|
|
super().save(*args, **kwargs)
|