From b9441f2503ffd38984447fb11be5b76156474ef1 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Fri, 27 Mar 2026 12:41:32 +0100 Subject: [PATCH] snapshot: preserve dynamic pdf parity and quality pass --- .../media/templates/offboarding_template.html | 68 ++-- .../media/templates/onboarding_template.html | 279 ++--------------- backend/workflows/models.py | 5 + backend/workflows/pdf_sections.py | 290 ++++++++++++++++++ backend/workflows/tasks.py | 4 + .../workflows/tests/test_pdf_generation.py | 75 +++++ backend/workflows/tests/test_pdf_sections.py | 96 ++++++ 7 files changed, 525 insertions(+), 292 deletions(-) create mode 100644 backend/workflows/pdf_sections.py create mode 100644 backend/workflows/tests/test_pdf_generation.py create mode 100644 backend/workflows/tests/test_pdf_sections.py diff --git a/backend/media/templates/offboarding_template.html b/backend/media/templates/offboarding_template.html index 6b3ee37..39a1d1e 100644 --- a/backend/media/templates/offboarding_template.html +++ b/backend/media/templates/offboarding_template.html @@ -1,5 +1,5 @@ - + @@ -29,12 +29,6 @@ letter-spacing: 0.2px; } - .sub { - margin: 2px 0 0 0; - color: #6b7280; - font-size: 9.5px; - } - .section { margin-top: 9px; font-size: 11px; @@ -54,6 +48,8 @@ border: 1px solid #f0e1e1; padding: 4px 6px; vertical-align: top; + overflow-wrap: anywhere; + word-break: break-word; } th { @@ -141,17 +137,6 @@ font-size: 9.4px; } - .manual-title { - margin: 9px 0 5px; - font-size: 10px; - font-weight: bold; - color: #111827; - } - - .manual-grid td { - width: 50%; - } - @@ -159,25 +144,26 @@

{{ T.offboarding_title }}

-
{{ T.employee_info }}
- - - - - - - - - - - - - - - - - -
{{ T.name }}{{ FULL_NAME }}{{ T.email }}{{ EMAIL }}
{{ T.department }}{{ DEPARTMENT }}{{ T.job_title }}{{ JOB_TITLE }}
{{ T.last_working_day }}{{ LAST_WORKING_DAY }}
+ {% for section in PDF_SECTIONS %} + {% if section.has_content %} +
{{ section.title }}
+ + {% if section.scalar_rows %} + + {% for row in section.scalar_rows %} + + + {{ row[0].display_value }} + {% if row[1] %} + + + {% endif %} + + {% endfor %} +
{{ row[0].label }}{{ row[1].label }}{{ row[1].display_value }}
+ {% endif %} + {% endif %} + {% endfor %}
{{ T.offboarding_requester }}
@@ -295,14 +281,6 @@
{{ T.return_complete }}  {{ T.yes }}      {{ T.no }}
-
{{ T.notes }}
-
- - - - -
{{ T.notes }}{{ NOTES }}
-

{{ T.offboarding_note }}

diff --git a/backend/media/templates/onboarding_template.html b/backend/media/templates/onboarding_template.html index 5807195..8b9055b 100644 --- a/backend/media/templates/onboarding_template.html +++ b/backend/media/templates/onboarding_template.html @@ -29,12 +29,6 @@ letter-spacing: 0.2px; } - .sub { - margin: 2px 0 0 0; - color: #475569; - font-size: 9.5px; - } - .section { margin-top: 9px; font-size: 11px; @@ -101,11 +95,6 @@ word-break: break-word; } - .empty { - color: #94a3b8; - font-style: italic; - } - .signature { width: 150px; height: 70px; @@ -133,245 +122,41 @@

{{ T.onboarding_title }}

-
{{ T.onboarding_staff_data }}
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ T.name }}{{ DISPLAY_NAME }}
{{ T.department }}{{ ABTEILUNG }}{{ T.job_title }}{{ BERUFSBEZEICHNUNG }}
{{ T.work_email }}{{ EMAIL }}{{ T.employment_type }}{{ BESCHAEFTIGUNG }}
{{ T.contract_start }}{{ VERTRAGSBEGINN }}{{ T.contract_end }}{{ VERTRAGSENDE }}
{{ T.handover_date }}{{ UEBERGABEDATUM }}
+ {% for section in PDF_SECTIONS %} + {% if section.has_content %} +
{{ section.title }}
-
{{ T.equipment_access }}
+ {% if section.scalar_rows %} + + {% for row in section.scalar_rows %} + + + {{ row[0].display_value }} + {% if row[1] %} + + + {% endif %} + + {% endfor %} +
{{ row[0].label }}{{ row[1].label }}{{ row[1].display_value }}
+ {% endif %} - {% if HAS_DEVICES %} -
-
{{ T.devices }}
- - {% for row in ARBEITSGERÄTE_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - + {% for field in section.list_fields %} +
+
{{ field.label }}
+
• {{ cell }}
+ {% for row in field.display_value|batch(3, '') %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} +
{% if cell %}• {{ cell }}{% endif %}
+
{% endfor %} - - - {% endif %} - - {% if HAS_GROUPS %} -
-
{{ T.workspace_groups }}
- - {% for row in ZUGAENGE_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if HAS_SOFTWARE %} -
-
{{ T.software }}
- - {% for row in SOFTWARE_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if HAS_ACCESSES %} -
-
{{ T.accesses }}
- - {% for row in ACCOUNT_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if HAS_RESOURCES %} -
-
{{ T.resources }}
- - {% for row in STANDARD_RESSOURCEN %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if GROUP_MAILBOXES_REQUIRED and HAS_GROUP_MAILBOXES %} -
-
{{ T.group_mailboxes_required }}
- - {% for row in GROUP_MAILBOXES_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if ADDITIONAL_HARDWARE_NEEDED and HAS_ADDITIONAL_HARDWARE %} -
-
{{ T.additional_hardware_needed }}
- - {% for row in ADDITIONAL_HARDWARE_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if ADDITIONAL_SOFTWARE_NEEDED and HAS_ADDITIONAL_SOFTWARE %} -
-
{{ T.additional_software_needed }}
- - {% for row in ADDITIONAL_SOFTWARE_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if ADDITIONAL_ACCESS_NEEDED and HAS_ADDITIONAL_ACCESS %} -
-
{{ T.additional_access_needed }}
- - {% for row in ADDITIONAL_ACCESS_LIST %} - - {% for cell in row %}{% endfor %} - {% if row|length < 3 %} - {% for _ in range(3 - row|length) %}{% endfor %} - {% endif %} - - {% endfor %} -
• {{ cell }}
-
- {% endif %} - - {% if (VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN) or HAS_ADDITIONAL_HARDWARE_OTHER or HAS_SUCCESSOR_INFO or HAS_ADDITIONAL_NOTES %} -
{{ T.additional_details }}
- {% endif %} - - {% if VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN %} -
-
{{ T.business_cards }}
- - - - - - - - - - - - - -
{{ T.name }}{{ VISITENKARTE_NAME }}{{ T.job_title }}{{ VISITENKARTE_TITEL }}
{{ T.email }}{{ VISITENKARTE_EMAIL }}{{ T.phone }}{{ VISITENKARTE_TELEFON }}
-
- {% endif %} - - {% if HAS_ADDITIONAL_HARDWARE_OTHER %} -
-
{{ T.additional_hardware_other }}
- - - - -
{{ ADDITIONAL_HARDWARE_OTHER }}
-
- {% endif %} - - {% if HAS_SUCCESSOR_INFO %} -
-
{{ T.successor_phone }}
- - - - - - - - - - - -
{{ T.successor_of }}{{ SUCCESSOR_NAME }}{{ T.inherit_phone_number }}{{ INHERIT_PHONE_NUMBER }}
{{ T.direct_extension }}{{ PHONE_NUMBER }}
-
- {% endif %} - - {% if HAS_ADDITIONAL_NOTES %} -
-
{{ T.notes }}
- - - - -
{{ ADDITIONAL_NOTES }}
-
- {% endif %} + {% endif %} + {% endfor %}
{{ T.confirmation }}
diff --git a/backend/workflows/models.py b/backend/workflows/models.py index 96c18a9..f524efa 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -477,6 +477,8 @@ class FormFieldConfig(models.Model): ('vertrag', _('Vertrag')), ('itsetup', _('IT-Setup')), ('abschluss', _('Abschluss')), + ('mitarbeitende', _('Mitarbeitende')), + ('austritt', _('Austritt')), ] FORM_CHOICES = [ ('onboarding', _('Onboarding')), @@ -519,12 +521,15 @@ class FormFieldConfig(models.Model): class FormSectionConfig(models.Model): FORM_CHOICES = [ ('onboarding', _('Onboarding')), + ('offboarding', _('Offboarding')), ] SECTION_CHOICES = [ ('stammdaten', _('Stammdaten')), ('vertrag', _('Vertrag')), ('itsetup', _('IT-Setup')), ('abschluss', _('Abschluss')), + ('mitarbeitende', _('Mitarbeitende')), + ('austritt', _('Austritt')), ] form_type = models.CharField(max_length=20, choices=FORM_CHOICES) diff --git a/backend/workflows/pdf_sections.py b/backend/workflows/pdf_sections.py new file mode 100644 index 0000000..948fc84 --- /dev/null +++ b/backend/workflows/pdf_sections.py @@ -0,0 +1,290 @@ +from __future__ import annotations + +from collections import OrderedDict +from datetime import date, datetime + +from django.utils import formats +from django.utils.translation import override + +from .form_builder import ( + LOCKED_FIELD_RULES, + LOCKED_SECTION_RULES, + ensure_form_field_configs, + ensure_form_section_configs, + get_default_page_map, + get_section_labels, + get_section_order, +) +from .forms import OffboardingRequestForm, OnboardingRequestForm + +PDF_SECTION_TITLES = { + "onboarding": { + "stammdaten": "Stammdaten", + "vertrag": "Vertrag", + "itsetup": "IT-Setup", + "abschluss": "Abschluss", + }, + "offboarding": { + "mitarbeitende": "Mitarbeitende", + "austritt": "Austritt", + "abschluss": "Abschluss", + }, +} + +PDF_FIELD_LABELS = { + "onboarding": { + "full_name": {"de": "Name", "en": "Name"}, + }, + "offboarding": { + "full_name": {"de": "Name", "en": "Name"}, + }, +} + +PDF_EXCLUDED_FIELDS = { + "onboarding": { + "first_name", + "last_name", + "onboarded_by_email", + "agreement_confirm", + "signature_url", + "signature_image", + }, + "offboarding": set(), +} + +PDF_BOOLEAN_CONTROL_FIELDS = { + "onboarding": { + "order_business_cards", + "group_mailboxes_required_choice", + "additional_hardware_needed_choice", + "additional_software_needed_choice", + "additional_access_needed_choice", + "successor_required_choice", + "inherit_phone_number_choice", + }, + "offboarding": set(), +} + + +def _normalized_lang(language_code: str | None) -> str: + return (language_code or "de").split("-")[0].lower() or "de" + + +def _yes_no_text(language_code: str | None) -> tuple[str, str]: + lang = _normalized_lang(language_code) + if lang == "en": + return "Yes", "No" + return "Ja", "Nein" + + +def _not_available_text(language_code: str | None) -> str: + return "Not provided" if _normalized_lang(language_code) == "en" else "Keine Angabe" + + +def _split_name(full_name: str) -> tuple[str, str]: + parts = (full_name or "").split() + if not parts: + return "", "" + return parts[0], " ".join(parts[1:]) + + +def _split_multiline(value: str) -> list[str]: + return [line.strip() for line in (value or "").splitlines() if line.strip()] + + +def _format_date(value, language_code: str | None) -> str: + if not value: + return "" + if isinstance(value, datetime): + with override(_normalized_lang(language_code)): + return formats.date_format(value, "DATETIME_FORMAT", use_l10n=True) + if isinstance(value, date): + with override(_normalized_lang(language_code)): + return formats.date_format(value, "DATE_FORMAT", use_l10n=True) + return str(value) + + +def _coerce_text(value) -> str: + if value is None: + return "" + if isinstance(value, (date, datetime)): + return str(value) + return str(value).strip() + + +def _field_value_from_request(form_type: str, request_obj, field_name: str, language_code: str | None): + yes_text, no_text = _yes_no_text(language_code) + first_name, last_name = _split_name(getattr(request_obj, "full_name", "")) + + onboarding_map = { + "first_name": first_name, + "last_name": last_name, + "full_name": getattr(request_obj, "full_name", ""), + "gender": getattr(request_obj, "get_gender_display", lambda: "")() or getattr(request_obj, "gender", ""), + "job_title": getattr(request_obj, "job_title", ""), + "department": getattr(request_obj, "department", ""), + "work_email": getattr(request_obj, "work_email", ""), + "order_business_cards": yes_text if getattr(request_obj, "order_business_cards", False) else no_text, + "business_card_name": getattr(request_obj, "business_card_name", ""), + "business_card_title": getattr(request_obj, "business_card_title", ""), + "business_card_email": getattr(request_obj, "business_card_email", ""), + "business_card_phone": getattr(request_obj, "business_card_phone", ""), + "contract_start": _format_date(getattr(request_obj, "contract_start", None), language_code), + "employment_type": getattr(request_obj, "get_employment_type_display", lambda: "")() or getattr(request_obj, "employment_type", ""), + "employment_end_date": _format_date(getattr(request_obj, "employment_end_date", None), language_code), + "handover_date": _format_date(getattr(request_obj, "handover_date", None), language_code), + "group_mailboxes_required_choice": yes_text if getattr(request_obj, "group_mailboxes_required", False) else no_text, + "group_mailboxes": _split_multiline(getattr(request_obj, "group_mailboxes", "")), + "needed_devices_multi": _split_multiline(getattr(request_obj, "needed_devices", "")), + "additional_hardware_needed_choice": yes_text if getattr(request_obj, "additional_hardware_needed", False) else no_text, + "additional_hardware_multi": _split_multiline(getattr(request_obj, "additional_hardware", "")), + "additional_hardware_other": getattr(request_obj, "additional_hardware_other", ""), + "needed_software_multi": _split_multiline(getattr(request_obj, "needed_software", "")), + "additional_software_needed_choice": yes_text if getattr(request_obj, "additional_software_needed", False) else no_text, + "additional_software_multi": _split_multiline(getattr(request_obj, "additional_software", "")), + "additional_software": getattr(request_obj, "additional_software", ""), + "needed_accesses_multi": _split_multiline(getattr(request_obj, "needed_accesses", "")), + "additional_access_needed_choice": yes_text if getattr(request_obj, "additional_access_needed", False) else no_text, + "additional_access_text": _split_multiline(getattr(request_obj, "additional_access_text", "")), + "needed_workspace_groups_multi": _split_multiline(getattr(request_obj, "needed_workspace_groups", "")), + "needed_resources_multi": _split_multiline(getattr(request_obj, "needed_resources", "")), + "successor_required_choice": yes_text if getattr(request_obj, "successor_required", False) else no_text, + "successor_name": getattr(request_obj, "successor_name", ""), + "inherit_phone_number_choice": yes_text if getattr(request_obj, "inherit_phone_number", False) else no_text, + "phone_number_choice": getattr(request_obj, "phone_number", ""), + "additional_notes": getattr(request_obj, "additional_notes", ""), + "signature_url": getattr(request_obj, "signature_url", ""), + "signature_image": getattr(getattr(request_obj, "signature_image", None), "name", "") or "", + "onboarded_by_email": getattr(request_obj, "onboarded_by_email", ""), + "agreement_confirm": yes_text if _coerce_text(getattr(request_obj, "agreement", "")) else no_text, + } + offboarding_map = { + "full_name": getattr(request_obj, "full_name", ""), + "work_email": getattr(request_obj, "work_email", ""), + "department": getattr(request_obj, "department", ""), + "job_title": getattr(request_obj, "job_title", ""), + "last_working_day": _format_date(getattr(request_obj, "last_working_day", None), language_code), + "notes": getattr(request_obj, "notes", ""), + } + value_map = onboarding_map if form_type == "onboarding" else offboarding_map + return value_map.get(field_name, "") + + +def _field_kind(value) -> str: + if isinstance(value, list): + return "list" + return "text" + + +def _is_empty_value(value) -> bool: + if isinstance(value, list): + return len(value) == 0 + return _coerce_text(value) == "" + + +def _field_meta(form_type: str, field_name: str, language_code: str | None) -> tuple[str, str]: + custom_label = PDF_FIELD_LABELS.get(form_type, {}).get(field_name, {}) + if custom_label: + return custom_label.get(_normalized_lang(language_code), field_name), "" + form_class = OnboardingRequestForm if form_type == "onboarding" else OffboardingRequestForm + base_field = form_class.base_fields.get(field_name) + if not base_field: + return field_name, "" + with override(_normalized_lang(language_code)): + label = str(base_field.label or field_name) + help_text = str(base_field.help_text or "").strip() + return label, help_text + + +def build_pdf_sections(form_type: str, request_obj, language_code: str | None = None) -> list[dict]: + language_code = _normalized_lang(language_code or getattr(request_obj, "preferred_language", None)) + default_page_map = get_default_page_map(form_type) + section_order = get_section_order(form_type) + section_labels = get_section_labels(form_type) + field_names = list(default_page_map.keys()) + configs = ensure_form_field_configs(form_type, field_names) + section_configs = ensure_form_section_configs(form_type) + locked_fields = LOCKED_FIELD_RULES.get(form_type, set()) + locked_sections = LOCKED_SECTION_RULES.get(form_type, set()) + + sections: OrderedDict[str, dict] = OrderedDict() + for key in section_order: + section_cfg = section_configs.get(key) + is_visible = True + if key not in locked_sections and section_cfg is not None: + is_visible = bool(section_cfg.is_visible) + if not is_visible: + continue + sections[key] = { + "key": key, + "title": PDF_SECTION_TITLES.get(form_type, {}).get(key, section_labels.get(key, key)), + "fields": [], + } + + ordered_configs = sorted( + configs.values(), + key=lambda cfg: (cfg.sort_order, cfg.field_name), + ) + for cfg in ordered_configs: + field_name = cfg.field_name + section_key = cfg.page_key or default_page_map.get(field_name, "") + if section_key not in sections: + continue + if field_name in PDF_EXCLUDED_FIELDS.get(form_type, set()): + continue + if field_name not in locked_fields and not cfg.is_visible: + continue + + base_label, base_help_text = _field_meta(form_type, field_name, language_code) + label = cfg.translated_label_override(language_code) or base_label + help_text = cfg.translated_help_text_override(language_code) or base_help_text + raw_value = _field_value_from_request(form_type, request_obj, field_name, language_code) + sections[section_key]["fields"].append( + { + "name": field_name, + "label": label, + "help_text": help_text, + "kind": _field_kind(raw_value), + "value": raw_value, + "is_empty": _is_empty_value(raw_value), + "is_locked": field_name in locked_fields, + } + ) + + not_available = _not_available_text(language_code) + result = [] + for section in sections.values(): + visible_fields = [] + for field in section["fields"]: + if ( + field["name"] in PDF_BOOLEAN_CONTROL_FIELDS.get(form_type, set()) + and _coerce_text(field["value"]).lower() in {"nein", "no"} + ): + continue + display_value = field["value"] if field["kind"] == "list" else (_coerce_text(field["value"]) or not_available) + visible_fields.append( + { + **field, + "display_value": display_value, + } + ) + render_fields = [field for field in visible_fields if not field["is_empty"]] + scalar_fields = [field for field in render_fields if field["kind"] != "list"] + list_fields = [field for field in render_fields if field["kind"] == "list"] + scalar_rows = [scalar_fields[index:index + 2] for index in range(0, len(scalar_fields), 2)] + for row in scalar_rows: + if len(row) < 2: + row.append(None) + result.append( + { + "key": section["key"], + "title": section["title"], + "fields": visible_fields, + "render_fields": render_fields, + "scalar_fields": scalar_fields, + "list_fields": list_fields, + "scalar_rows": scalar_rows, + "has_content": bool(render_fields), + } + ) + return result diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 1b9aa58..4ece6b1 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -29,6 +29,7 @@ from .forms import ( WORKSPACE_GROUP_CHOICES, ) from .notifications import notify_user_by_email +from .pdf_sections import build_pdf_sections # These templates are the product-level defaults for fresh deployments. # Runtime branding and company config can override the company-facing identity @@ -965,6 +966,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: context = { 'T': t, 'PDF_LANG': lang, + 'PDF_SECTIONS': build_pdf_sections('onboarding', request_obj, lang), 'VORNAME': first_name, 'NACHNAME': last_name, 'DISPLAY_NAME': display_name or request_obj.full_name, @@ -1209,6 +1211,8 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: context = { 'T': t, + 'PDF_LANG': lang, + 'PDF_SECTIONS': build_pdf_sections('offboarding', request_obj, lang), 'FULL_NAME': request_obj.full_name, 'EMAIL': request_obj.work_email, 'DEPARTMENT': request_obj.department or t['not_available_short'], diff --git a/backend/workflows/tests/test_pdf_generation.py b/backend/workflows/tests/test_pdf_generation.py new file mode 100644 index 0000000..83d0ef3 --- /dev/null +++ b/backend/workflows/tests/test_pdf_generation.py @@ -0,0 +1,75 @@ +from datetime import date +from pathlib import Path + +from django.test import TestCase, override_settings +from pypdf import PdfReader + +from workflows.models import FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest +from workflows.tasks import _generate_offboarding_pdf, _generate_onboarding_pdf + + +@override_settings(PDF_OUTPUT_DIR=Path('/tmp/onoff_test_pdfs')) +class PDFGenerationTests(TestCase): + def _extract_pdf_text(self, pdf_path: Path) -> str: + reader = PdfReader(str(pdf_path)) + return "\n".join((page.extract_text() or "") for page in reader.pages) + + def test_onboarding_pdf_respects_hidden_section_and_field(self): + FormSectionConfig.objects.update_or_create( + form_type='onboarding', + section_key='itsetup', + defaults={'is_visible': False}, + ) + FormFieldConfig.objects.update_or_create( + form_type='onboarding', + field_name='job_title', + defaults={'is_visible': False}, + ) + request_obj = OnboardingRequest.objects.create( + full_name='Max Mustermann', + gender='herr', + job_title='Consultant', + department='IT-Service', + work_email='max.mustermann@workdock.de', + contract_start=date(2026, 11, 1), + employment_type='unbefristet', + needed_devices='Laptop\nMonitor', + onboarded_by_email='requester@workdock.de', + onboarded_by_name='Mia Beispiel', + agreement='accepted', + ) + + pdf_path = _generate_onboarding_pdf(request_obj) + text = self._extract_pdf_text(pdf_path) + + self.assertIn('Max Mustermann', text) + self.assertIn('IT-Service', text) + self.assertIn('Stammdaten', text) + self.assertNotIn('Consultant', text) + self.assertNotIn('Laptop', text) + self.assertNotIn('IT-Setup', text) + self.assertNotIn('1. Stammdaten', text) + self.assertNotIn('Vorname', text) + self.assertNotIn('Nachname', text) + self.assertNotIn('onboarded_by_email', text) + + def test_offboarding_pdf_uses_dynamic_sections(self): + request_obj = OffboardingRequest.objects.create( + full_name='Lara Beispiel', + work_email='lara.beispiel@workdock.de', + department='IT-Service', + job_title='Engineer', + last_working_day=date(2026, 12, 31), + notes='Bitte Accounts sperren.', + requested_by_email='admin@workdock.de', + requested_by_name='Nina Admin', + ) + + pdf_path = _generate_offboarding_pdf(request_obj) + text = self._extract_pdf_text(pdf_path) + + self.assertIn('Lara Beispiel', text) + self.assertIn('Engineer', text) + self.assertIn('31. Dezember 2026', text) + self.assertIn('Bitte Accounts sperren.', text) + self.assertNotIn('1. Mitarbeitende', text) diff --git a/backend/workflows/tests/test_pdf_sections.py b/backend/workflows/tests/test_pdf_sections.py new file mode 100644 index 0000000..9d4321e --- /dev/null +++ b/backend/workflows/tests/test_pdf_sections.py @@ -0,0 +1,96 @@ +from django.test import TestCase + +from workflows.models import FormFieldConfig, FormSectionConfig, OffboardingRequest, OnboardingRequest +from workflows.pdf_sections import build_pdf_sections + + +class PDFSectionBuilderTests(TestCase): + def test_onboarding_builder_respects_hidden_section_and_hidden_field(self): + FormSectionConfig.objects.update_or_create( + form_type='onboarding', + section_key='itsetup', + defaults={'is_visible': False}, + ) + FormFieldConfig.objects.update_or_create( + form_type='onboarding', + field_name='job_title', + defaults={'is_visible': False}, + ) + request_obj = OnboardingRequest.objects.create( + full_name='Max Mustermann', + gender='herr', + job_title='Consultant', + department='IT-Service', + work_email='max.mustermann@workdock.de', + contract_start='2026-11-01', + employment_type='unbefristet', + agreement='accepted', + ) + + sections = build_pdf_sections('onboarding', request_obj, 'de') + + self.assertEqual([section['key'] for section in sections], ['stammdaten', 'vertrag', 'abschluss']) + stammdaten = next(section for section in sections if section['key'] == 'stammdaten') + self.assertNotIn('job_title', [field['name'] for field in stammdaten['fields']]) + + def test_onboarding_builder_uses_field_order_and_overrides(self): + FormFieldConfig.objects.update_or_create( + form_type='onboarding', + field_name='department', + defaults={ + 'sort_order': 1, + 'label_override': 'Team', + 'help_text_override': 'Interne Organisationseinheit', + }, + ) + FormFieldConfig.objects.update_or_create( + form_type='onboarding', + field_name='gender', + defaults={'sort_order': 5}, + ) + request_obj = OnboardingRequest.objects.create( + full_name='Max Mustermann', + gender='herr', + job_title='Consultant', + department='IT-Service', + work_email='max.mustermann@workdock.de', + contract_start='2026-11-01', + employment_type='unbefristet', + agreement='accepted', + ) + + sections = build_pdf_sections('onboarding', request_obj, 'de') + stammdaten = next(section for section in sections if section['key'] == 'stammdaten') + visible_names = [field['name'] for field in stammdaten['fields']] + department_field = next(field for field in stammdaten['fields'] if field['name'] == 'department') + + self.assertLess(visible_names.index('department'), visible_names.index('gender')) + self.assertEqual(department_field['label'], 'Team') + self.assertEqual(department_field['help_text'], 'Interne Organisationseinheit') + self.assertEqual(department_field['display_value'], 'IT-Service') + + def test_offboarding_builder_has_section_parity_and_formats_values(self): + FormSectionConfig.objects.update_or_create( + form_type='offboarding', + section_key='abschluss', + defaults={'is_visible': False}, + ) + request_obj = OffboardingRequest.objects.create( + full_name='Lara Beispiel', + work_email='lara.beispiel@workdock.de', + department='IT-Service', + job_title='Engineer', + last_working_day='2026-12-31', + notes='Bitte Accounts sperren.', + requested_by_email='admin@workdock.de', + ) + + sections = build_pdf_sections('offboarding', request_obj, 'de') + + self.assertEqual([section['key'] for section in sections], ['mitarbeitende', 'austritt']) + mitarbeitende = next(section for section in sections if section['key'] == 'mitarbeitende') + austritt = next(section for section in sections if section['key'] == 'austritt') + self.assertIn('full_name', [field['name'] for field in mitarbeitende['fields']]) + self.assertIn('last_working_day', [field['name'] for field in austritt['fields']]) + date_field = next(field for field in austritt['fields'] if field['name'] == 'last_working_day') + self.assertTrue(date_field['display_value'])