snapshot: preserve dynamic pdf parity and quality pass
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ T.lang or 'de' }}">
|
<html lang="{{ PDF_LANG or T.lang or 'de' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -29,12 +29,6 @@
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub {
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
color: #6b7280;
|
|
||||||
font-size: 9.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -54,6 +48,8 @@
|
|||||||
border: 1px solid #f0e1e1;
|
border: 1px solid #f0e1e1;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@@ -141,17 +137,6 @@
|
|||||||
font-size: 9.4px;
|
font-size: 9.4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manual-title {
|
|
||||||
margin: 9px 0 5px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-grid td {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -159,25 +144,26 @@
|
|||||||
<h1 class="title">{{ T.offboarding_title }}</h1>
|
<h1 class="title">{{ T.offboarding_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">{{ T.employee_info }}</div>
|
{% for section in PDF_SECTIONS %}
|
||||||
<table>
|
{% if section.has_content %}
|
||||||
<tr>
|
<div class="section">{{ section.title }}</div>
|
||||||
<th>{{ T.name }}</th>
|
|
||||||
<td class="mono">{{ FULL_NAME }}</td>
|
{% if section.scalar_rows %}
|
||||||
<th>{{ T.email }}</th>
|
<table>
|
||||||
<td>{{ EMAIL }}</td>
|
{% for row in section.scalar_rows %}
|
||||||
</tr>
|
<tr>
|
||||||
<tr>
|
<th>{{ row[0].label }}</th>
|
||||||
<th>{{ T.department }}</th>
|
<td{% if not row[1] %} colspan="3"{% endif %}{% if row[0].name in ['full_name', 'work_email'] %} class="mono"{% endif %}>{{ row[0].display_value }}</td>
|
||||||
<td>{{ DEPARTMENT }}</td>
|
{% if row[1] %}
|
||||||
<th>{{ T.job_title }}</th>
|
<th>{{ row[1].label }}</th>
|
||||||
<td>{{ JOB_TITLE }}</td>
|
<td>{{ row[1].display_value }}</td>
|
||||||
</tr>
|
{% endif %}
|
||||||
<tr>
|
</tr>
|
||||||
<th>{{ T.last_working_day }}</th>
|
{% endfor %}
|
||||||
<td colspan="3">{{ LAST_WORKING_DAY }}</td>
|
</table>
|
||||||
</tr>
|
{% endif %}
|
||||||
</table>
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
<div class="section">{{ T.offboarding_requester }}</div>
|
<div class="section">{{ T.offboarding_requester }}</div>
|
||||||
<table>
|
<table>
|
||||||
@@ -295,14 +281,6 @@
|
|||||||
<div class="sigline">{{ T.return_complete }} <span class="cb">□</span> {{ T.yes }}     <span class="cb">□</span> {{ T.no }}</div>
|
<div class="sigline">{{ T.return_complete }} <span class="cb">□</span> {{ T.yes }}     <span class="cb">□</span> {{ T.no }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">{{ T.notes }}</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.notes }}</th>
|
|
||||||
<td>{{ NOTES }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p class="small">{{ T.offboarding_note }}</p>
|
<p class="small">{{ T.offboarding_note }}</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -29,12 +29,6 @@
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub {
|
|
||||||
margin: 2px 0 0 0;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 9.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -101,11 +95,6 @@
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature {
|
.signature {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
@@ -133,245 +122,41 @@
|
|||||||
<h1 class="title">{{ T.onboarding_title }}</h1>
|
<h1 class="title">{{ T.onboarding_title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">{{ T.onboarding_staff_data }}</div>
|
{% for section in PDF_SECTIONS %}
|
||||||
<table>
|
{% if section.has_content %}
|
||||||
<tr>
|
<div class="section">{{ section.title }}</div>
|
||||||
<th>{{ T.name }}</th>
|
|
||||||
<td class="mono" colspan="3">{{ DISPLAY_NAME }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.department }}</th>
|
|
||||||
<td>{{ ABTEILUNG }}</td>
|
|
||||||
<th>{{ T.job_title }}</th>
|
|
||||||
<td>{{ BERUFSBEZEICHNUNG }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.work_email }}</th>
|
|
||||||
<td>{{ EMAIL }}</td>
|
|
||||||
<th>{{ T.employment_type }}</th>
|
|
||||||
<td>{{ BESCHAEFTIGUNG }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.contract_start }}</th>
|
|
||||||
<td>{{ VERTRAGSBEGINN }}</td>
|
|
||||||
<th>{{ T.contract_end }}</th>
|
|
||||||
<td>{{ VERTRAGSENDE }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.handover_date }}</th>
|
|
||||||
<td colspan="3">{{ UEBERGABEDATUM }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="section">{{ T.equipment_access }}</div>
|
{% if section.scalar_rows %}
|
||||||
|
<table>
|
||||||
|
{% for row in section.scalar_rows %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ row[0].label }}</th>
|
||||||
|
<td{% if not row[1] %} colspan="3"{% endif %}{% if row[0].name in ['full_name', 'work_email'] %} class="mono"{% endif %}>{{ row[0].display_value }}</td>
|
||||||
|
{% if row[1] %}
|
||||||
|
<th>{{ row[1].label }}</th>
|
||||||
|
<td>{{ row[1].display_value }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if HAS_DEVICES %}
|
{% for field in section.list_fields %}
|
||||||
<div class="opt-card">
|
<div class="opt-card">
|
||||||
<div class="opt-title">{{ T.devices }}</div>
|
<div class="opt-title">{{ field.label }}</div>
|
||||||
<table class="opt-grid">
|
<table class="opt-grid">
|
||||||
{% for row in ARBEITSGERÄTE_LIST %}
|
{% for row in field.display_value|batch(3, '') %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
{% for cell in row %}
|
||||||
{% if row|length < 3 %}
|
<td>{% if cell %}• {{ cell }}{% endif %}</td>
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
</tr>
|
||||||
</tr>
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
{% endif %}
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_GROUPS %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.workspace_groups }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in ZUGAENGE_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_SOFTWARE %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.software }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in SOFTWARE_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_ACCESSES %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.accesses }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in ACCOUNT_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_RESOURCES %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.resources }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in STANDARD_RESSOURCEN %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if GROUP_MAILBOXES_REQUIRED and HAS_GROUP_MAILBOXES %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.group_mailboxes_required }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in GROUP_MAILBOXES_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if ADDITIONAL_HARDWARE_NEEDED and HAS_ADDITIONAL_HARDWARE %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.additional_hardware_needed }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in ADDITIONAL_HARDWARE_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if ADDITIONAL_SOFTWARE_NEEDED and HAS_ADDITIONAL_SOFTWARE %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.additional_software_needed }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in ADDITIONAL_SOFTWARE_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if ADDITIONAL_ACCESS_NEEDED and HAS_ADDITIONAL_ACCESS %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.additional_access_needed }}</div>
|
|
||||||
<table class="opt-grid">
|
|
||||||
{% for row in ADDITIONAL_ACCESS_LIST %}
|
|
||||||
<tr>
|
|
||||||
{% for cell in row %}<td>• {{ cell }}</td>{% endfor %}
|
|
||||||
{% if row|length < 3 %}
|
|
||||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if (VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN) or HAS_ADDITIONAL_HARDWARE_OTHER or HAS_SUCCESSOR_INFO or HAS_ADDITIONAL_NOTES %}
|
|
||||||
<div class="section">{{ T.additional_details }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if VISITENKARTE_BESTELLT and HAS_VISITENKARTE_DATEN %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.business_cards }}</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.name }}</th>
|
|
||||||
<td>{{ VISITENKARTE_NAME }}</td>
|
|
||||||
<th>{{ T.job_title }}</th>
|
|
||||||
<td>{{ VISITENKARTE_TITEL }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.email }}</th>
|
|
||||||
<td>{{ VISITENKARTE_EMAIL }}</td>
|
|
||||||
<th>{{ T.phone }}</th>
|
|
||||||
<td>{{ VISITENKARTE_TELEFON }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_ADDITIONAL_HARDWARE_OTHER %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.additional_hardware_other }}</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>{{ ADDITIONAL_HARDWARE_OTHER }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_SUCCESSOR_INFO %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.successor_phone }}</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.successor_of }}</th>
|
|
||||||
<td>{{ SUCCESSOR_NAME }}</td>
|
|
||||||
<th>{{ T.inherit_phone_number }}</th>
|
|
||||||
<td>{{ INHERIT_PHONE_NUMBER }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{{ T.direct_extension }}</th>
|
|
||||||
<td colspan="3">{{ PHONE_NUMBER }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if HAS_ADDITIONAL_NOTES %}
|
|
||||||
<div class="opt-card">
|
|
||||||
<div class="opt-title">{{ T.notes }}</div>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td>{{ ADDITIONAL_NOTES }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="section">{{ T.confirmation }}</div>
|
<div class="section">{{ T.confirmation }}</div>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ class FormFieldConfig(models.Model):
|
|||||||
('vertrag', _('Vertrag')),
|
('vertrag', _('Vertrag')),
|
||||||
('itsetup', _('IT-Setup')),
|
('itsetup', _('IT-Setup')),
|
||||||
('abschluss', _('Abschluss')),
|
('abschluss', _('Abschluss')),
|
||||||
|
('mitarbeitende', _('Mitarbeitende')),
|
||||||
|
('austritt', _('Austritt')),
|
||||||
]
|
]
|
||||||
FORM_CHOICES = [
|
FORM_CHOICES = [
|
||||||
('onboarding', _('Onboarding')),
|
('onboarding', _('Onboarding')),
|
||||||
@@ -519,12 +521,15 @@ class FormFieldConfig(models.Model):
|
|||||||
class FormSectionConfig(models.Model):
|
class FormSectionConfig(models.Model):
|
||||||
FORM_CHOICES = [
|
FORM_CHOICES = [
|
||||||
('onboarding', _('Onboarding')),
|
('onboarding', _('Onboarding')),
|
||||||
|
('offboarding', _('Offboarding')),
|
||||||
]
|
]
|
||||||
SECTION_CHOICES = [
|
SECTION_CHOICES = [
|
||||||
('stammdaten', _('Stammdaten')),
|
('stammdaten', _('Stammdaten')),
|
||||||
('vertrag', _('Vertrag')),
|
('vertrag', _('Vertrag')),
|
||||||
('itsetup', _('IT-Setup')),
|
('itsetup', _('IT-Setup')),
|
||||||
('abschluss', _('Abschluss')),
|
('abschluss', _('Abschluss')),
|
||||||
|
('mitarbeitende', _('Mitarbeitende')),
|
||||||
|
('austritt', _('Austritt')),
|
||||||
]
|
]
|
||||||
|
|
||||||
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
form_type = models.CharField(max_length=20, choices=FORM_CHOICES)
|
||||||
|
|||||||
290
backend/workflows/pdf_sections.py
Normal file
290
backend/workflows/pdf_sections.py
Normal file
@@ -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
|
||||||
@@ -29,6 +29,7 @@ from .forms import (
|
|||||||
WORKSPACE_GROUP_CHOICES,
|
WORKSPACE_GROUP_CHOICES,
|
||||||
)
|
)
|
||||||
from .notifications import notify_user_by_email
|
from .notifications import notify_user_by_email
|
||||||
|
from .pdf_sections import build_pdf_sections
|
||||||
|
|
||||||
# These templates are the product-level defaults for fresh deployments.
|
# These templates are the product-level defaults for fresh deployments.
|
||||||
# Runtime branding and company config can override the company-facing identity
|
# Runtime branding and company config can override the company-facing identity
|
||||||
@@ -965,6 +966,7 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
|
|||||||
context = {
|
context = {
|
||||||
'T': t,
|
'T': t,
|
||||||
'PDF_LANG': lang,
|
'PDF_LANG': lang,
|
||||||
|
'PDF_SECTIONS': build_pdf_sections('onboarding', request_obj, lang),
|
||||||
'VORNAME': first_name,
|
'VORNAME': first_name,
|
||||||
'NACHNAME': last_name,
|
'NACHNAME': last_name,
|
||||||
'DISPLAY_NAME': display_name or request_obj.full_name,
|
'DISPLAY_NAME': display_name or request_obj.full_name,
|
||||||
@@ -1209,6 +1211,8 @@ def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path:
|
|||||||
|
|
||||||
context = {
|
context = {
|
||||||
'T': t,
|
'T': t,
|
||||||
|
'PDF_LANG': lang,
|
||||||
|
'PDF_SECTIONS': build_pdf_sections('offboarding', request_obj, lang),
|
||||||
'FULL_NAME': request_obj.full_name,
|
'FULL_NAME': request_obj.full_name,
|
||||||
'EMAIL': request_obj.work_email,
|
'EMAIL': request_obj.work_email,
|
||||||
'DEPARTMENT': request_obj.department or t['not_available_short'],
|
'DEPARTMENT': request_obj.department or t['not_available_short'],
|
||||||
|
|||||||
75
backend/workflows/tests/test_pdf_generation.py
Normal file
75
backend/workflows/tests/test_pdf_generation.py
Normal file
@@ -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)
|
||||||
96
backend/workflows/tests/test_pdf_sections.py
Normal file
96
backend/workflows/tests/test_pdf_sections.py
Normal file
@@ -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'])
|
||||||
Reference in New Issue
Block a user