From 622b396986d22fad30ae39320af04928e2c316bf Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Sat, 28 Mar 2026 09:18:53 +0100 Subject: [PATCH] snapshot: modularize workflow model layer by responsibility --- backend/workflows/model_account.py | 152 ++++ backend/workflows/model_forms.py | 202 +++++ backend/workflows/model_notifications.py | 89 +++ backend/workflows/model_ops.py | 49 ++ backend/workflows/model_portal.py | 129 +++ backend/workflows/model_requests.py | 172 ++++ backend/workflows/model_shared.py | 14 + backend/workflows/model_system.py | 62 ++ backend/workflows/models.py | 952 +---------------------- 9 files changed, 876 insertions(+), 945 deletions(-) create mode 100644 backend/workflows/model_account.py create mode 100644 backend/workflows/model_forms.py create mode 100644 backend/workflows/model_notifications.py create mode 100644 backend/workflows/model_ops.py create mode 100644 backend/workflows/model_portal.py create mode 100644 backend/workflows/model_requests.py create mode 100644 backend/workflows/model_shared.py create mode 100644 backend/workflows/model_system.py diff --git a/backend/workflows/model_account.py b/backend/workflows/model_account.py new file mode 100644 index 0000000..7abd427 --- /dev/null +++ b/backend/workflows/model_account.py @@ -0,0 +1,152 @@ +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 import timezone +from django.utils.translation import gettext_lazy as _ + + +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']) diff --git a/backend/workflows/model_forms.py b/backend/workflows/model_forms.py new file mode 100644 index 0000000..9114278 --- /dev/null +++ b/backend/workflows/model_forms.py @@ -0,0 +1,202 @@ +from django.db import models +from django.utils.translation import get_language, gettext_lazy as _ + + +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')), + ('mitarbeitende', _('Mitarbeitende')), + ('austritt', _('Austritt')), + ] + 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=80, blank=True, default='') + 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')), ('offboarding', _('Offboarding'))] + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + section_key = models.CharField(max_length=80) + sort_order = models.PositiveIntegerField(default=0) + is_visible = models.BooleanField(default=True) + + class Meta: + ordering = ['form_type', 'sort_order', '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 FormConditionalRuleConfig(models.Model): + FORM_CHOICES = [('onboarding', _('Onboarding'))] + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + target_key = models.CharField(max_length=80) + clauses = models.JSONField(default=list, blank=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ['form_type', 'target_key'] + unique_together = ('form_type', 'target_key') + verbose_name = 'Formular-Bedingungsregel' + verbose_name_plural = 'Formular-Bedingungsregeln' + + def __str__(self) -> str: + return f'{self.form_type}: {self.target_key}' + + +class FormCustomSectionConfig(models.Model): + FORM_CHOICES = [('onboarding', _('Onboarding'))] + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + section_key = models.SlugField(max_length=80) + sort_order = models.PositiveIntegerField(default=0) + title = models.CharField(max_length=255) + title_en = models.CharField(max_length=255, blank=True) + is_active = models.BooleanField(default=True) + + class Meta: + ordering = ['form_type', 'sort_order', 'section_key'] + unique_together = ('form_type', 'section_key') + verbose_name = 'Benutzerdefinierter Formularabschnitt' + verbose_name_plural = 'Benutzerdefinierte Formularabschnitte' + + def __str__(self) -> str: + return f'{self.form_type}: {self.title}' + + def translated_title(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.title_en.strip(): + return self.title_en.strip() + return self.title.strip() + + +class FormCustomFieldConfig(models.Model): + FIELD_TYPE_TEXT = 'text' + FIELD_TYPE_TEXTAREA = 'textarea' + FIELD_TYPE_SELECT = 'select' + FIELD_TYPE_CHECKBOX = 'checkbox' + FIELD_TYPE_CHOICES = [ + (FIELD_TYPE_TEXT, _('Text')), + (FIELD_TYPE_TEXTAREA, _('Mehrzeilig')), + (FIELD_TYPE_SELECT, _('Auswahl')), + (FIELD_TYPE_CHECKBOX, _('Checkbox')), + ] + FORM_CHOICES = [('onboarding', _('Onboarding')), ('offboarding', _('Offboarding'))] + form_type = models.CharField(max_length=20, choices=FORM_CHOICES) + field_key = models.SlugField(max_length=80) + section_key = models.CharField(max_length=80) + sort_order = models.PositiveIntegerField(default=0) + field_type = models.CharField(max_length=20, choices=FIELD_TYPE_CHOICES, default=FIELD_TYPE_TEXT) + is_active = models.BooleanField(default=True) + is_required = models.BooleanField(default=False) + label = models.CharField(max_length=255) + label_en = models.CharField(max_length=255, blank=True) + help_text = models.TextField(blank=True) + help_text_en = models.TextField(blank=True) + select_options = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: wert|Label') + select_options_en = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: value|Label') + + class Meta: + ordering = ['form_type', 'section_key', 'sort_order', 'field_key'] + unique_together = ('form_type', 'field_key') + verbose_name = 'Benutzerdefiniertes Formularfeld' + verbose_name_plural = 'Benutzerdefinierte Formularfelder' + + def __str__(self) -> str: + return f'{self.form_type}: {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() + + def translated_help_text(self, language_code: str | None = None) -> str: + lang = (language_code or get_language() or 'de').split('-')[0] + if lang == 'en' and self.help_text_en.strip(): + return self.help_text_en.strip() + return self.help_text.strip() + + def translated_select_options(self, language_code: str | None = None) -> list[tuple[str, str]]: + lang = (language_code or get_language() or 'de').split('-')[0] + raw = self.select_options_en if lang == 'en' and self.select_options_en.strip() else self.select_options + options = [] + for line in (raw or '').splitlines(): + line = line.strip() + if not line: + continue + if '|' in line: + value, label = [part.strip() for part in line.split('|', 1)] + else: + value = label = line + if value: + options.append((value, label or value)) + return options diff --git a/backend/workflows/model_notifications.py b/backend/workflows/model_notifications.py new file mode 100644 index 0000000..549a43f --- /dev/null +++ b/backend/workflows/model_notifications.py @@ -0,0 +1,89 @@ +from django.db import models +from django.utils.translation import get_language, gettext_lazy as _ + + +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() diff --git a/backend/workflows/model_ops.py b/backend/workflows/model_ops.py new file mode 100644 index 0000000..d797be8 --- /dev/null +++ b/backend/workflows/model_ops.py @@ -0,0 +1,49 @@ +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +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}' diff --git a/backend/workflows/model_portal.py b/backend/workflows/model_portal.py new file mode 100644 index 0000000..18db518 --- /dev/null +++ b/backend/workflows/model_portal.py @@ -0,0 +1,129 @@ +from django.core.validators import FileExtensionValidator +from django.db import models +from django.utils.translation import get_language, gettext_lazy as _ + + +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) diff --git a/backend/workflows/model_requests.py b/backend/workflows/model_requests.py new file mode 100644 index 0000000..283a4a2 --- /dev/null +++ b/backend/workflows/model_requests.py @@ -0,0 +1,172 @@ +from django.db import models +from django.utils.translation import get_language, gettext_lazy as _ + +from .model_account import EmployeeProfile +from .model_shared import REQUEST_STATUS_CHOICES, normalized_language_code + + +class OnboardingRequest(models.Model): + STATUS_CHOICES = REQUEST_STATUS_CHOICES + + 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.') + custom_field_values = models.JSONField(default=dict, blank=True) + 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 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 OffboardingRequest(models.Model): + STATUS_CHOICES = REQUEST_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) + custom_field_values = models.JSONField(default=dict, 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) diff --git a/backend/workflows/model_shared.py b/backend/workflows/model_shared.py new file mode 100644 index 0000000..4bcf1b4 --- /dev/null +++ b/backend/workflows/model_shared.py @@ -0,0 +1,14 @@ +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' + + +REQUEST_STATUS_CHOICES = [ + ('submitted', _('Eingereicht')), + ('processing', _('In Bearbeitung')), + ('completed', _('Abgeschlossen')), + ('failed', _('Fehlgeschlagen')), +] diff --git a/backend/workflows/model_system.py b/backend/workflows/model_system.py new file mode 100644 index 0000000..a0e6c9e --- /dev/null +++ b/backend/workflows/model_system.py @@ -0,0 +1,62 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +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})' diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 8caf2a6..6577f21 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,945 +1,7 @@ -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.', - ) - custom_field_values = models.JSONField(default=dict, blank=True) - - 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')), - ('mitarbeitende', _('Mitarbeitende')), - ('austritt', _('Austritt')), - ] - 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=80, blank=True, default='') - 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')), - ('offboarding', _('Offboarding')), - ] - form_type = models.CharField(max_length=20, choices=FORM_CHOICES) - section_key = models.CharField(max_length=80) - sort_order = models.PositiveIntegerField(default=0) - is_visible = models.BooleanField(default=True) - - class Meta: - ordering = ['form_type', 'sort_order', '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 FormConditionalRuleConfig(models.Model): - FORM_CHOICES = [ - ('onboarding', _('Onboarding')), - ] - - form_type = models.CharField(max_length=20, choices=FORM_CHOICES) - target_key = models.CharField(max_length=80) - clauses = models.JSONField(default=list, blank=True) - is_active = models.BooleanField(default=True) - - class Meta: - ordering = ['form_type', 'target_key'] - unique_together = ('form_type', 'target_key') - verbose_name = 'Formular-Bedingungsregel' - verbose_name_plural = 'Formular-Bedingungsregeln' - - def __str__(self) -> str: - return f'{self.form_type}: {self.target_key}' - - -class FormCustomSectionConfig(models.Model): - FORM_CHOICES = [ - ('onboarding', _('Onboarding')), - ] - - form_type = models.CharField(max_length=20, choices=FORM_CHOICES) - section_key = models.SlugField(max_length=80) - sort_order = models.PositiveIntegerField(default=0) - title = models.CharField(max_length=255) - title_en = models.CharField(max_length=255, blank=True) - is_active = models.BooleanField(default=True) - - class Meta: - ordering = ['form_type', 'sort_order', 'section_key'] - unique_together = ('form_type', 'section_key') - verbose_name = 'Benutzerdefinierter Formularabschnitt' - verbose_name_plural = 'Benutzerdefinierte Formularabschnitte' - - def __str__(self) -> str: - return f'{self.form_type}: {self.title}' - - def translated_title(self, language_code: str | None = None) -> str: - lang = (language_code or get_language() or 'de').split('-')[0] - if lang == 'en' and self.title_en.strip(): - return self.title_en.strip() - return self.title.strip() - - -class FormCustomFieldConfig(models.Model): - FIELD_TYPE_TEXT = 'text' - FIELD_TYPE_TEXTAREA = 'textarea' - FIELD_TYPE_SELECT = 'select' - FIELD_TYPE_CHECKBOX = 'checkbox' - FIELD_TYPE_CHOICES = [ - (FIELD_TYPE_TEXT, _('Text')), - (FIELD_TYPE_TEXTAREA, _('Mehrzeilig')), - (FIELD_TYPE_SELECT, _('Auswahl')), - (FIELD_TYPE_CHECKBOX, _('Checkbox')), - ] - FORM_CHOICES = [ - ('onboarding', _('Onboarding')), - ('offboarding', _('Offboarding')), - ] - form_type = models.CharField(max_length=20, choices=FORM_CHOICES) - field_key = models.SlugField(max_length=80) - section_key = models.CharField(max_length=80) - sort_order = models.PositiveIntegerField(default=0) - field_type = models.CharField(max_length=20, choices=FIELD_TYPE_CHOICES, default=FIELD_TYPE_TEXT) - is_active = models.BooleanField(default=True) - is_required = models.BooleanField(default=False) - label = models.CharField(max_length=255) - label_en = models.CharField(max_length=255, blank=True) - help_text = models.TextField(blank=True) - help_text_en = models.TextField(blank=True) - select_options = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: wert|Label') - select_options_en = models.TextField(blank=True, help_text='Eine Option pro Zeile. Optional: value|Label') - - class Meta: - ordering = ['form_type', 'section_key', 'sort_order', 'field_key'] - unique_together = ('form_type', 'field_key') - verbose_name = 'Benutzerdefiniertes Formularfeld' - verbose_name_plural = 'Benutzerdefinierte Formularfelder' - - def __str__(self) -> str: - return f'{self.form_type}: {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() - - def translated_help_text(self, language_code: str | None = None) -> str: - lang = (language_code or get_language() or 'de').split('-')[0] - if lang == 'en' and self.help_text_en.strip(): - return self.help_text_en.strip() - return self.help_text.strip() - - def translated_select_options(self, language_code: str | None = None) -> list[tuple[str, str]]: - lang = (language_code or get_language() or 'de').split('-')[0] - raw = self.select_options_en if lang == 'en' and self.select_options_en.strip() else self.select_options - options = [] - for line in (raw or '').splitlines(): - line = line.strip() - if not line: - continue - if '|' in line: - value, label = [part.strip() for part in line.split('|', 1)] - else: - value = label = line - if value: - options.append((value, label or value)) - return options - - -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) - custom_field_values = models.JSONField(default=dict, 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) +from .model_account import * +from .model_portal import * +from .model_ops import * +from .model_forms import * +from .model_notifications import * +from .model_requests import * +from .model_system import *