From 26096449aae0e15d61b93b1d5291348be64916c4 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Tue, 24 Mar 2026 13:57:27 +0100 Subject: [PATCH] snapshot: preserve bilingual PDF text phase --- README.md | 7 +- .../media/templates/offboarding_template.html | 307 ++++++++++++++ .../onboarding_intro_session_pdf.html | 72 ++++ .../templates/onboarding_intro_template.html | 193 +++++++++ .../media/templates/onboarding_template.html | 395 ++++++++++++++++++ backend/workflows/tasks.py | 319 +++++++++++--- .../workflows/developer_handbook.html | 2 + .../templates/workflows/project_wiki.html | 4 +- 8 files changed, 1234 insertions(+), 65 deletions(-) create mode 100644 backend/media/templates/offboarding_template.html create mode 100644 backend/media/templates/onboarding_intro_session_pdf.html create mode 100644 backend/media/templates/onboarding_intro_template.html create mode 100644 backend/media/templates/onboarding_template.html diff --git a/README.md b/README.md index a8e1cfe..a61c262 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,12 @@ Notes: - Language stability hardening is in place: - onboarding/offboarding request records normalize `preferred_language` at model-save time - both request tables now have a database default of `de` -- Several generated PDF business text blocks are still not fully bilingual yet. +- Generated PDF fixed text is now bilingual for: + - onboarding PDF + - offboarding PDF + - intro PDF + - live introduction protocol PDF +- Remaining bilingual gap is mostly long-form handbook/wiki copy and a few secondary admin/help texts. - CI now validates that translation catalogs compile successfully on push and pull request. ## Current implemented scope diff --git a/backend/media/templates/offboarding_template.html b/backend/media/templates/offboarding_template.html new file mode 100644 index 0000000..af585a6 --- /dev/null +++ b/backend/media/templates/offboarding_template.html @@ -0,0 +1,307 @@ + + + + + + {{ T.offboarding_title }} + + + +
+

{{ 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 }}
+ +
{{ T.offboarding_requester }}
+ + + + + + + +
{{ T.requested_by_name }}{{ REQUESTED_BY_NAME }}{{ T.requested_by_email }}{{ REQUESTED_BY }}
+ + {% if HAS_ONBOARDING_DATA %} +
{{ T.it_hardware_status }}
+
+
{{ T.hardware_check }}
+ + {% if HARDWARE_CHECKLIST %} + {% for item in HARDWARE_CHECKLIST %} + {% if loop.index0 % 3 == 0 %}{% endif %} + + {% if loop.index0 % 3 == 2 or loop.last %}{% endif %} + {% endfor %} + {% else %} + + {% endif %} +
 {{ item.label }}
{{ T.no_onboarding_hardware }}
+
+ {% else %} +
{{ T.manual_return_overview }}
+

{{ T.manual_return_note }}

+ +
+
{{ T.returned_devices }}
+ + {% for row in MANUAL_DEVICES %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
+
+ +
+
{{ T.returned_software }}
+ + {% for row in MANUAL_SOFTWARE %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
+
+ +
+
{{ T.removed_workspace_groups }}
+ + {% for row in MANUAL_WORKSPACE_GROUPS %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
+
+ +
+
{{ T.removed_accesses }}
+ + {% for row in MANUAL_ACCESSES %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
+
+ +
+
{{ T.returned_extra_it }}
+ + {% for row in MANUAL_EXTRA_HARDWARE %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} + {% for row in MANUAL_EXTRA_SOFTWARE %} + + {% for cell in row %}{% endfor %} + {% if row|length < 3 %} + {% for _ in range(3 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
 {{ cell }}
+
+ {% endif %} + +
{{ T.it_signatures }}
+
+
{{ T.it_checked_by }}
+
{{ T.it_signature }}
+
{{ T.return_complete }}  {{ T.yes }}      {{ T.no }}
+
+ +
{{ T.notes }}
+ + + + + +
{{ T.notes }}{{ NOTES }}
+ +

{{ T.offboarding_note }}

+ + diff --git a/backend/media/templates/onboarding_intro_session_pdf.html b/backend/media/templates/onboarding_intro_session_pdf.html new file mode 100644 index 0000000..30165e0 --- /dev/null +++ b/backend/media/templates/onboarding_intro_session_pdf.html @@ -0,0 +1,72 @@ + + + + + + {{ T.live_intro_title }} + + + +
+

{{ T.live_intro_title }}

+

{{ T.live_intro_sub }}

+
+ +
{{ T.base_data }}
+ + + + + + + + + + + + + + + + + + + +
{{ T.name }}{{ DISPLAY_NAME }}{{ T.department }}{{ ABTEILUNG }}
{{ T.job_title }}{{ BERUFSBEZEICHNUNG }}{{ T.employment_start }}{{ VERTRAGSBEGINN }}
{{ T.work_email }}{{ EMAIL }}{{ T.introduced_by }}{{ REQUESTED_BY_NAME }}
+ + {% for section in INTRO_SECTIONS %} +
+
{{ section.title }}
+ + {% for row in section.rows %} + + {% for cell in row %}{% endfor %} + {% if row|length < 2 %}{% endif %} + + {% endfor %} +
{{ cell.label }}
+
+ {% endfor %} + +
{{ T.notes }}
+
{{ SESSION_NOTES }}
+ +
{{ T.employee_signature_block }}
+
+ + diff --git a/backend/media/templates/onboarding_intro_template.html b/backend/media/templates/onboarding_intro_template.html new file mode 100644 index 0000000..8be677a --- /dev/null +++ b/backend/media/templates/onboarding_intro_template.html @@ -0,0 +1,193 @@ + + + + + + {{ T.intro_title }} + + + +
+

{{ T.intro_title }}

+

{{ T.intro_sub }}

+
+ +
{{ T.base_data }}
+ + + + + + + + + + + + + + + + + + + +
{{ T.name }}{{ DISPLAY_NAME }}{{ T.department }}{{ ABTEILUNG }}
{{ T.job_title }}{{ BERUFSBEZEICHNUNG }}{{ T.start_date }}{{ VERTRAGSBEGINN }}
{{ T.work_email }}{{ EMAIL }}{{ T.introduced_by }}{{ REQUESTED_BY_NAME }}
+ +

{{ T.intro_note }}

+ + {% for section in INTRO_SECTIONS %} +
+
{{ section.title }}
+ + {% for row in section.rows %} + + {% for cell in row %}{% endfor %} + {% if row|length < 2 %} + {% for _ in range(2 - row|length) %}{% endfor %} + {% endif %} + + {% endfor %} +
 {{ cell }}
+
+ {% endfor %} + +
{{ T.confirmation }}
+
+
{{ T.intro_completed_at }}
+
{{ T.employee_signature }}
+
{{ T.trainer_signature }}
+
{{ T.open_questions }}
+
+ + diff --git a/backend/media/templates/onboarding_template.html b/backend/media/templates/onboarding_template.html new file mode 100644 index 0000000..273619e --- /dev/null +++ b/backend/media/templates/onboarding_template.html @@ -0,0 +1,395 @@ + + + + + + {{ T.onboarding_title }} + + + +
+

{{ 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 }}
+ +
{{ T.equipment_access }}
+ + {% 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 %} + + {% endfor %} +
• {{ cell }}
+
+ {% 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 %} + +
{{ T.confirmation }}
+ + + + + + + + + + {% if UNTERSCHRIFT %} + + {% else %} + + {% endif %} + +
{{ T.requested_by_name }}{{ REQUESTED_BY_NAME }}{{ T.requested_by_email }}{{ REQUESTED_BY_EMAIL }}
{{ T.signature }}{{ T.signature_alt }}{{ UNTERSCHRIFT_HINWEIS }}
+ +

{{ T.onboarding_note }}

+ + diff --git a/backend/workflows/tasks.py b/backend/workflows/tasks.py index 8bc0ffe..e8e0a78 100644 --- a/backend/workflows/tasks.py +++ b/backend/workflows/tasks.py @@ -291,6 +291,169 @@ def _chunk_choice_labels(choices: list[tuple[str, str]], chunk_size: int = 3) -> return _chunk_list(labels, chunk_size=chunk_size) +def _normalized_lang(language_code: str | None) -> str: + return (language_code or 'de').split('-')[0].lower() or 'de' + + +def _pdf_texts(language_code: str | None = None) -> dict[str, str]: + lang = _normalized_lang(language_code) + texts = { + 'de': { + 'lang': 'de', + 'not_available': 'Keine Angabe', + 'not_available_short': '-', + 'yes': 'Ja', + 'no': 'Nein', + 'onboarding_title': 'Onboarding-Unterlagen', + 'onboarding_staff_data': 'Personaldaten', + 'name': 'Name', + 'department': 'Abteilung', + 'job_title': 'Berufsbezeichnung', + 'work_email': 'Dienstliche E-Mail', + 'employment_type': 'Beschäftigungsverhältnis', + 'contract_start': 'Vertragsbeginn', + 'contract_end': 'Vertragsende', + 'handover_date': 'Übergabedatum', + 'equipment_access': 'Ausstattung und Zugänge', + 'devices': 'Benötigte Geräte und Gegenstände', + 'workspace_groups': 'Benötigte Gruppen im Workspace', + 'software': 'Benötigte Software', + 'accesses': 'Benötigte Zugänge', + 'resources': 'Benötigte Ressourcen', + 'group_mailboxes_required': 'Gruppenpostfächer erforderlich', + 'additional_hardware_needed': 'Darüber hinaus wird weitere Hardware benötigt', + 'additional_software_needed': 'Wird zusätzliche Software benötigt', + 'additional_access_needed': 'Darüber hinaus werden weitere Zugänge benötigt', + 'additional_details': 'Zusätzliche Angaben', + 'business_cards': 'Visitenkarten', + 'email': 'E-Mail', + 'phone': 'Telefon', + 'additional_hardware_other': 'Weitere Hardware (Freitext)', + 'successor_phone': 'Nachfolge und Telefon', + 'successor_of': 'Nachfolge von', + 'inherit_phone_number': 'Telefon von Vorgänger übernehmen', + 'direct_extension': 'Direktwahl', + 'notes': 'Notizen', + 'confirmation': 'Bestätigung', + 'requested_by_name': 'Angefordert von (Name)', + 'requested_by_email': 'Angefordert von (E-Mail)', + 'signature': 'Unterschrift', + 'signature_alt': 'Unterschrift', + 'onboarding_note': 'Hinweis: Dieses Formular dient als interne Prozessgrundlage für das Onboarding.', + 'offboarding_title': 'Offboarding-Unterlagen', + 'employee_info': 'Mitarbeitenden-Informationen', + 'last_working_day': 'Letzter Arbeitstag', + 'offboarding_requester': 'Offboarding-Anfordernde Person', + 'it_hardware_status': 'IT-Hardware-Status (aus Onboarding)', + 'hardware_check': 'Hardware-Check', + 'no_onboarding_hardware': 'Keine Onboarding-Hardwaredaten gefunden.', + 'manual_return_overview': 'Manuelle Rückgabeübersicht', + 'manual_return_note': 'Es wurden keine gespeicherten Onboarding-Daten zu dieser Person gefunden. Die folgenden Listen dienen als manuelle Rückgabe- und Prüfübersicht.', + 'returned_devices': 'Zurückgegebene Geräte und Artikel', + 'returned_software': 'Zurückgegebene bzw. deaktivierte Software', + 'removed_workspace_groups': 'Entfernte Gruppen im Workspace', + 'removed_accesses': 'Entfernte Zugänge', + 'returned_extra_it': 'Zurückgegebene zusätzliche Hardware / Software', + 'it_signatures': 'IT-Section: Signaturen', + 'it_checked_by': 'IT geprüft am durch:', + 'it_signature': 'IT-Unterschrift:', + 'return_complete': 'Rückgabe vollständig:', + 'offboarding_note': 'Hinweis: Dieses Formular dient als interne Prozessgrundlage für das Offboarding.', + 'intro_title': 'Einweisungs- und Übergabeprotokoll', + 'intro_sub': 'Gesprächsleitfaden für die persönliche Einführung neuer Mitarbeitender.', + 'base_data': 'Basisdaten', + 'start_date': 'Startdatum', + 'introduced_by': 'Einweisung durch', + 'intro_note': 'Dieses Dokument dient als Gesprächsleitfaden für die persönliche Einweisung. Die Felder können während des Termins manuell abgehakt und anschließend unterschrieben werden.', + 'employee_signature': 'Unterschrift Mitarbeitende Person:', + 'trainer_signature': 'Unterschrift Einweisende Person:', + 'intro_completed_at': 'Einweisung durchgeführt am:', + 'open_questions': 'Rückfragen offen / Nacharbeit erforderlich:', + 'live_intro_title': 'Einweisungsprotokoll', + 'live_intro_sub': 'Export des aktuellen Live-Status aus der webbasierten Einweisung.', + 'employment_start': 'Vertragsbeginn', + 'employee_signature_block': 'Unterschrift Mitarbeitende Person', + }, + 'en': { + 'lang': 'en', + 'not_available': 'Not provided', + 'not_available_short': '-', + 'yes': 'Yes', + 'no': 'No', + 'onboarding_title': 'Onboarding Documents', + 'onboarding_staff_data': 'Employee Details', + 'name': 'Name', + 'department': 'Department', + 'job_title': 'Job title', + 'work_email': 'Work email', + 'employment_type': 'Employment type', + 'contract_start': 'Contract start', + 'contract_end': 'Contract end', + 'handover_date': 'Handover date', + 'equipment_access': 'Equipment and access', + 'devices': 'Required devices and items', + 'workspace_groups': 'Required workspace groups', + 'software': 'Required software', + 'accesses': 'Required accesses', + 'resources': 'Required resources', + 'group_mailboxes_required': 'Group mailboxes required', + 'additional_hardware_needed': 'Additional hardware required', + 'additional_software_needed': 'Additional software required', + 'additional_access_needed': 'Additional accesses required', + 'additional_details': 'Additional details', + 'business_cards': 'Business cards', + 'email': 'Email', + 'phone': 'Phone', + 'additional_hardware_other': 'Additional hardware (free text)', + 'successor_phone': 'Successor and phone', + 'successor_of': 'Successor to', + 'inherit_phone_number': 'Take over predecessor phone number', + 'direct_extension': 'Direct extension', + 'notes': 'Notes', + 'confirmation': 'Confirmation', + 'requested_by_name': 'Requested by (name)', + 'requested_by_email': 'Requested by (email)', + 'signature': 'Signature', + 'signature_alt': 'Signature', + 'onboarding_note': 'Note: This form serves as the internal process basis for onboarding.', + 'offboarding_title': 'Offboarding Documents', + 'employee_info': 'Employee information', + 'last_working_day': 'Last working day', + 'offboarding_requester': 'Offboarding requester', + 'it_hardware_status': 'IT hardware status (from onboarding)', + 'hardware_check': 'Hardware check', + 'no_onboarding_hardware': 'No onboarding hardware data found.', + 'manual_return_overview': 'Manual return overview', + 'manual_return_note': 'No stored onboarding data was found for this person. The following lists serve as a manual return and review overview.', + 'returned_devices': 'Returned devices and items', + 'returned_software': 'Returned or disabled software', + 'removed_workspace_groups': 'Removed workspace groups', + 'removed_accesses': 'Removed accesses', + 'returned_extra_it': 'Returned additional hardware / software', + 'it_signatures': 'IT section: signatures', + 'it_checked_by': 'Checked by IT on:', + 'it_signature': 'IT signature:', + 'return_complete': 'Return complete:', + 'offboarding_note': 'Note: This form serves as the internal process basis for offboarding.', + 'intro_title': 'Introduction and Handover Protocol', + 'intro_sub': 'Conversation guide for the personal introduction of new employees.', + 'base_data': 'Basic data', + 'start_date': 'Start date', + 'introduced_by': 'Introduction by', + 'intro_note': 'This document serves as a conversation guide for the personal introduction. The fields can be checked manually during the meeting and signed afterwards.', + 'employee_signature': 'Employee signature:', + 'trainer_signature': 'Trainer signature:', + 'intro_completed_at': 'Introduction completed on:', + 'open_questions': 'Open questions / follow-up required:', + 'live_intro_title': 'Introduction Protocol', + 'live_intro_sub': 'Export of the current live status from the web-based introduction.', + 'employment_start': 'Contract start', + 'employee_signature_block': 'Employee signature', + }, + } + return texts.get(lang, texts['de']) + + MANUAL_ONBOARDING_FIELD_SECTIONS = [ ( 'Stammdaten', @@ -406,7 +569,7 @@ def _build_intro_sections_from_admin(request_obj: OnboardingRequest, language_co def build_intro_sections_for_request(request_obj: OnboardingRequest, language_code: str | None = None) -> list[dict]: - lang = (language_code or get_language() or 'de').split('-')[0] + lang = _normalized_lang(language_code or get_language()) section_titles = { 'de': { 'workplace': 'Geräte und Arbeitsplatz', @@ -432,39 +595,56 @@ def build_intro_sections_for_request(request_obj: OnboardingRequest, language_co workplace_items = [] for item in devices: - workplace_items.append(f'{item} übergeben und Grundfunktionen erklärt') + if lang == 'en': + workplace_items.append(f'{item} handed over and basic functions explained') + else: + workplace_items.append(f'{item} übergeben und Grundfunktionen erklärt') for item in resources: - workplace_items.append(f'{item} gezeigt bzw. Nutzung erklärt') + if lang == 'en': + workplace_items.append(f'{item} shown or usage explained') + else: + workplace_items.append(f'{item} gezeigt bzw. Nutzung erklärt') if request_obj.phone_number: - workplace_items.append(f'Telefonnummer / Direktwahl erklärt: {request_obj.phone_number}') + if lang == 'en': + workplace_items.append(f'Phone number / direct extension explained: {request_obj.phone_number}') + else: + workplace_items.append(f'Telefonnummer / Direktwahl erklärt: {request_obj.phone_number}') if not workplace_items: - workplace_items.append('Arbeitsplatz, Geräte und allgemeine Nutzung besprochen') + workplace_items.append('Workplace, devices, and general usage reviewed' if lang == 'en' else 'Arbeitsplatz, Geräte und allgemeine Nutzung besprochen') - account_items = [f'{item} Zugang erklärt' for item in accesses] - account_items.extend([f'{item} Gruppe / Berechtigung erläutert' for item in groups]) + account_items = [f'{item} access explained' if lang == 'en' else f'{item} Zugang erklärt' for item in accesses] + account_items.extend([f'{item} group / permission explained' if lang == 'en' else f'{item} Gruppe / Berechtigung erläutert' for item in groups]) if request_obj.work_email: - account_items.insert(0, f'Dienstliche E-Mail-Adresse erläutert: {request_obj.work_email}') + account_items.insert(0, f'Work email address explained: {request_obj.work_email}' if lang == 'en' else f'Dienstliche E-Mail-Adresse erläutert: {request_obj.work_email}') if group_mailboxes: - account_items.extend([f'Gruppenpostfach erklärt: {item}' for item in group_mailboxes]) + account_items.extend([f'Group mailbox explained: {item}' if lang == 'en' else f'Gruppenpostfach erklärt: {item}' for item in group_mailboxes]) if not account_items: - account_items.append('Zugänge, Konten und Anmeldelogik besprochen') + account_items.append('Accesses, accounts, and login logic reviewed' if lang == 'en' else 'Zugänge, Konten und Anmeldelogik besprochen') - software_items = [f'{item} Einführung durchgeführt' for item in software] - software_items.extend([f'{item} zusätzlich besprochen' for item in extra_software]) + software_items = [f'{item} introduction completed' if lang == 'en' else f'{item} Einführung durchgeführt' for item in software] + software_items.extend([f'{item} discussed additionally' if lang == 'en' else f'{item} zusätzlich besprochen' for item in extra_software]) if not software_items: - software_items.append('Benötigte Standardsoftware und tägliche Nutzung erklärt') + software_items.append('Required standard software and daily usage explained' if lang == 'en' else 'Benötigte Standardsoftware und tägliche Nutzung erklärt') - process_items = [ - 'Passwortregeln und sicherer Umgang besprochen', - 'Dateiablage, Nextcloud und Freigaben erklärt', - 'Kommunikationswege und Support-Prozess erklärt', - ] + process_items = ( + [ + 'Password rules and secure handling reviewed', + 'File storage, Nextcloud, and sharing explained', + 'Communication channels and support process explained', + ] + if lang == 'en' + else [ + 'Passwortregeln und sicherer Umgang besprochen', + 'Dateiablage, Nextcloud und Freigaben erklärt', + 'Kommunikationswege und Support-Prozess erklärt', + ] + ) if extra_hardware: - process_items.extend([f'{item} als zusätzliche Ausstattung besprochen' for item in extra_hardware]) + process_items.extend([f'{item} discussed as additional equipment' if lang == 'en' else f'{item} als zusätzliche Ausstattung besprochen' for item in extra_hardware]) if request_obj.additional_access_text: - process_items.extend([f'Zusätzlicher Zugang besprochen: {item}' for item in _split_multiline(request_obj.additional_access_text)]) + process_items.extend([f'Additional access discussed: {item}' if lang == 'en' else f'Zusätzlicher Zugang besprochen: {item}' for item in _split_multiline(request_obj.additional_access_text)]) if request_obj.successor_name: - process_items.append(f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}') + process_items.append(f'Handover / successor context reviewed: {request_obj.successor_name}' if lang == 'en' else f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}') custom_intro_items = _build_intro_sections_from_admin(request_obj, lang) intro_sections_raw = [ @@ -701,6 +881,8 @@ def _overlay_with_letterhead(content_pdf: Path, letterhead_pdf: Path, output_pdf def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: + lang = _normalized_lang(request_obj.preferred_language) + t = _pdf_texts(lang) first_name, last_name = _split_name(request_obj.full_name) safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_{request_obj.id}') output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_letter_{safe_name}.pdf' @@ -720,7 +902,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: additional_access_list = _split_multiline(request_obj.additional_access_text or '') signature_src = '' - signature_note = '-' + signature_note = t['not_available_short'] if getattr(request_obj, 'signature_image', None): try: signature_path = Path(request_obj.signature_image.path).resolve() @@ -728,18 +910,18 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: encoded = base64.b64encode(sig_fp.read()).decode('ascii') mime_type = mimetypes.guess_type(signature_path.name)[0] or 'image/png' signature_src = f"data:{mime_type};base64,{encoded}" - signature_note = 'Digitale Signatur als Bilddatei hinterlegt.' + signature_note = 'Digital signature stored as image file.' if lang == 'en' else 'Digitale Signatur als Bilddatei hinterlegt.' except Exception: signature_src = '' - signature_note = request_obj.signature_url or '-' + signature_note = request_obj.signature_url or t['not_available_short'] elif request_obj.signature_url: signature_note = request_obj.signature_url - requester_email = request_obj.onboarded_by_email or '-' - requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-' - gender = (request_obj.get_gender_display() or '-').strip() or '-' - employment_type = (request_obj.employment_type or '-').strip() or '-' - employment_end = request_obj.employment_end_date or '-' + requester_email = request_obj.onboarded_by_email or t['not_available_short'] + requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or t['not_available_short'] + gender = (request_obj.get_gender_display() or t['not_available_short']).strip() or t['not_available_short'] + employment_type = (request_obj.employment_type or t['not_available_short']).strip() or t['not_available_short'] + employment_end = request_obj.employment_end_date or t['not_available_short'] order_business_cards = bool(request_obj.order_business_cards) group_mailboxes = (request_obj.group_mailboxes or '').strip() additional_hardware_other = (request_obj.additional_hardware_other or '').strip() @@ -752,22 +934,24 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: display_name = f"{gender} {first_name} {last_name}".strip() if gender and gender != '-' else f"{first_name} {last_name}".strip() context = { + 'T': t, + 'PDF_LANG': lang, 'VORNAME': first_name, 'NACHNAME': last_name, 'DISPLAY_NAME': display_name or request_obj.full_name, 'ANREDE': gender, - 'BERUFSBEZEICHNUNG': request_obj.job_title or 'N/A', - 'ABTEILUNG': request_obj.department or 'N/A', - 'EMAIL': request_obj.work_email or 'N/A', + 'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available'], + 'ABTEILUNG': request_obj.department or t['not_available'], + 'EMAIL': request_obj.work_email or t['not_available'], 'VERTRAGSBEGINN': request_obj.contract_start, 'BESCHAEFTIGUNG': employment_type, 'VERTRAGSENDE': employment_end, - 'UEBERGABEDATUM': request_obj.handover_date or '-', - 'ARBEITSGERAETE_TEXT': ' | '.join(devices) if devices else 'Keine Angabe', - 'WORKSPACE_GROUPS_TEXT': ' | '.join(groups) if groups else 'Keine Angabe', - 'SOFTWARE_TEXT': ' | '.join(software) if software else 'Keine Angabe', - 'ZUGAENGE_TEXT': ' | '.join(accesses) if accesses else 'Keine Angabe', - 'RESSOURCEN_TEXT': ' | '.join(resources) if resources else 'Keine Angabe', + 'UEBERGABEDATUM': request_obj.handover_date or t['not_available_short'], + 'ARBEITSGERAETE_TEXT': ' | '.join(devices) if devices else t['not_available'], + 'WORKSPACE_GROUPS_TEXT': ' | '.join(groups) if groups else t['not_available'], + 'SOFTWARE_TEXT': ' | '.join(software) if software else t['not_available'], + 'ZUGAENGE_TEXT': ' | '.join(accesses) if accesses else t['not_available'], + 'RESSOURCEN_TEXT': ' | '.join(resources) if resources else t['not_available'], 'VISITENKARTE_BESTELLT': order_business_cards, 'HAS_VISITENKARTE_DATEN': order_business_cards and any( [ @@ -777,19 +961,19 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: (request_obj.business_card_phone or '').strip(), ] ), - 'VISITENKARTE_NAME': request_obj.business_card_name or '-', - 'VISITENKARTE_TITEL': request_obj.business_card_title or '-', - 'VISITENKARTE_EMAIL': request_obj.business_card_email or '-', - 'VISITENKARTE_TELEFON': request_obj.business_card_phone or '-', - 'GROUP_MAILBOXES': group_mailboxes or 'Keine Angabe', - 'ADDITIONAL_HARDWARE_OTHER': additional_hardware_other or 'Keine Angabe', - 'ADDITIONAL_HARDWARE': additional_hardware or 'Keine Angabe', - 'ADDITIONAL_SOFTWARE': additional_software or 'Keine Angabe', - 'ADDITIONAL_ACCESS_TEXT': additional_access_text or 'Keine Angabe', - 'SUCCESSOR_NAME': successor_name or 'Keine Angabe', - 'PHONE_NUMBER': phone_number or '-', - 'INHERIT_PHONE_NUMBER': 'Ja' if request_obj.inherit_phone_number else 'Nein', - 'ADDITIONAL_NOTES': additional_notes or 'Keine Angabe', + 'VISITENKARTE_NAME': request_obj.business_card_name or t['not_available_short'], + 'VISITENKARTE_TITEL': request_obj.business_card_title or t['not_available_short'], + 'VISITENKARTE_EMAIL': request_obj.business_card_email or t['not_available_short'], + 'VISITENKARTE_TELEFON': request_obj.business_card_phone or t['not_available_short'], + 'GROUP_MAILBOXES': group_mailboxes or t['not_available'], + 'ADDITIONAL_HARDWARE_OTHER': additional_hardware_other or t['not_available'], + 'ADDITIONAL_HARDWARE': additional_hardware or t['not_available'], + 'ADDITIONAL_SOFTWARE': additional_software or t['not_available'], + 'ADDITIONAL_ACCESS_TEXT': additional_access_text or t['not_available'], + 'SUCCESSOR_NAME': successor_name or t['not_available'], + 'PHONE_NUMBER': phone_number or t['not_available_short'], + 'INHERIT_PHONE_NUMBER': t['yes'] if request_obj.inherit_phone_number else t['no'], + 'ADDITIONAL_NOTES': additional_notes or t['not_available'], 'GROUP_MAILBOXES_REQUIRED': bool(request_obj.group_mailboxes_required), 'ADDITIONAL_HARDWARE_NEEDED': bool(request_obj.additional_hardware_needed), 'ADDITIONAL_SOFTWARE_NEEDED': bool(request_obj.additional_software_needed), @@ -831,6 +1015,8 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path: def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code: str | None = None) -> Path: + lang = _normalized_lang(language_code or request_obj.preferred_language) + t = _pdf_texts(lang) safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_{request_obj.id}') output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_{safe_name}.pdf' temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_{safe_name}.pdf' @@ -852,11 +1038,12 @@ def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest, language_code requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-' context = { + 'T': t, 'DISPLAY_NAME': display_name, - 'ABTEILUNG': request_obj.department or '-', - 'BERUFSBEZEICHNUNG': request_obj.job_title or '-', + 'ABTEILUNG': request_obj.department or t['not_available_short'], + 'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available_short'], 'VERTRAGSBEGINN': request_obj.contract_start, - 'EMAIL': request_obj.work_email or '-', + 'EMAIL': request_obj.work_email or t['not_available_short'], 'REQUESTED_BY_NAME': requester_name, 'REQUESTED_BY_EMAIL': requester_email, 'INTRO_SECTIONS': intro_sections, @@ -877,6 +1064,8 @@ def _generate_onboarding_intro_session_pdf( language_code: str | None = None, ) -> Path: request_obj = session.onboarding_request + lang = _normalized_lang(language_code or request_obj.preferred_language) + t = _pdf_texts(lang) safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'onboarding_intro_session_{request_obj.id}') version = timezone.now().strftime('%Y%m%d%H%M%S') output_pdf = settings.PDF_OUTPUT_DIR / f'onboarding_intro_session_{safe_name}_{version}.pdf' @@ -911,18 +1100,19 @@ def _generate_onboarding_intro_session_pdf( requester_name = request_obj.onboarded_by_name or _resolve_user_display_name(request_obj.onboarded_by_email) or '-' context = { + 'T': t, 'DISPLAY_NAME': display_name, - 'ABTEILUNG': request_obj.department or '-', - 'BERUFSBEZEICHNUNG': request_obj.job_title or '-', + 'ABTEILUNG': request_obj.department or t['not_available_short'], + 'BERUFSBEZEICHNUNG': request_obj.job_title or t['not_available_short'], 'VERTRAGSBEGINN': request_obj.contract_start, - 'EMAIL': request_obj.work_email or '-', + 'EMAIL': request_obj.work_email or t['not_available_short'], 'REQUESTED_BY_NAME': requester_name, 'REQUESTED_BY_EMAIL': requester_email, 'SESSION_STATUS': session.get_status_display(), 'SESSION_COMPLETED_BY': session.completed_by_name or '-', 'SESSION_COMPLETED_AT': session.completed_at or '-', 'SESSION_UPDATED_AT': session.updated_at, - 'SESSION_NOTES': session.notes or '-', + 'SESSION_NOTES': session.notes or t['not_available_short'], 'INTRO_SECTIONS': exported_sections, } @@ -936,6 +1126,8 @@ def _generate_onboarding_intro_session_pdf( def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: + lang = _normalized_lang(request_obj.preferred_language) + t = _pdf_texts(lang) safe_name = _safe_filename_fragment(request_obj.full_name, fallback=f'offboarding_{request_obj.id}') output_pdf = settings.PDF_OUTPUT_DIR / f'offboarding_letter_{safe_name}.pdf' temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_offboarding_{safe_name}.pdf' @@ -965,16 +1157,17 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path: for item in extra_selected: checklist.append({'label': item, 'selected': True}) - requester_email = request_obj.requested_by_email or '-' - requester_name = request_obj.requested_by_name or _resolve_user_display_name(request_obj.requested_by_email) or '-' + requester_email = request_obj.requested_by_email or t['not_available_short'] + requester_name = request_obj.requested_by_name or _resolve_user_display_name(request_obj.requested_by_email) or t['not_available_short'] context = { + 'T': t, 'FULL_NAME': request_obj.full_name, 'EMAIL': request_obj.work_email, - 'DEPARTMENT': request_obj.department or '-', - 'JOB_TITLE': request_obj.job_title or '-', + 'DEPARTMENT': request_obj.department or t['not_available_short'], + 'JOB_TITLE': request_obj.job_title or t['not_available_short'], 'LAST_WORKING_DAY': request_obj.last_working_day, - 'NOTES': request_obj.notes or '-', + 'NOTES': request_obj.notes or t['not_available_short'], 'REQUESTED_BY': requester_email, 'REQUESTED_BY_NAME': requester_name, 'HAS_ONBOARDING_DATA': has_onboarding_data, diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 1662ec8..b59335d 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -136,6 +136,8 @@ docker compose exec -T web django-admin compilemessages
  • PDF generation is HTML-to-PDF using xhtml2pdf.
  • Letterhead overlay is applied from templates.pdf.
  • Main logic lives in backend/workflows/tasks.py.
  • +
  • Fixed PDF labels/headings are rendered from task-level DE/EN text dictionaries, not hard-coded directly in request processing logic.
  • +
  • PDF language follows the normalized request preferred_language, with German fallback.
  • Key templates: