snapshot: preserve bilingual PDF text phase
This commit is contained in:
@@ -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
|
||||
|
||||
307
backend/media/templates/offboarding_template.html
Normal file
307
backend/media/templates/offboarding_template.html
Normal file
@@ -0,0 +1,307 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ T.lang or 'de' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ T.offboarding_title }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
color: #0f172a;
|
||||
font-size: 10.2px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #fff3f3;
|
||||
border: 1px solid #f0dddd;
|
||||
border-left: 4px solid #a32b2b;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
color: #7a1e1e;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 2px 0 0 0;
|
||||
color: #6b7280;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 9px;
|
||||
font-size: 11px;
|
||||
color: #7a1e1e;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #efd8d8;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #f0e1e1;
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #fff8f8;
|
||||
color: #651818;
|
||||
font-weight: bold;
|
||||
width: 31%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.opt-card {
|
||||
border: 1px solid #f0e1e1;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.opt-title {
|
||||
background: #7a1e1e;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 5px 6px;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.opt-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.opt-grid td {
|
||||
border: 1px solid #f0e1e1;
|
||||
padding: 3px 5px;
|
||||
width: 33.33%;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.sigbox {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 7px;
|
||||
margin-top: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sigline {
|
||||
margin-top: 9px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #334155;
|
||||
min-width: 165px;
|
||||
height: 12px;
|
||||
vertical-align: bottom;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: #64748b;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.cb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
min-width: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
color: #b8b8b8;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.manual-note {
|
||||
margin: 6px 0 8px;
|
||||
color: #5b6472;
|
||||
font-size: 9.4px;
|
||||
}
|
||||
|
||||
.manual-title {
|
||||
margin: 9px 0 5px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.manual-grid td {
|
||||
width: 50%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<h1 class="title">{{ T.offboarding_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="section">{{ T.employee_info }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ T.name }}</th>
|
||||
<td class="mono">{{ FULL_NAME }}</td>
|
||||
<th>{{ T.email }}</th>
|
||||
<td>{{ EMAIL }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.department }}</th>
|
||||
<td>{{ DEPARTMENT }}</td>
|
||||
<th>{{ T.job_title }}</th>
|
||||
<td>{{ JOB_TITLE }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.last_working_day }}</th>
|
||||
<td colspan="3">{{ LAST_WORKING_DAY }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="section">{{ T.offboarding_requester }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ T.requested_by_name }}</th>
|
||||
<td>{{ REQUESTED_BY_NAME }}</td>
|
||||
<th>{{ T.requested_by_email }}</th>
|
||||
<td>{{ REQUESTED_BY }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if HAS_ONBOARDING_DATA %}
|
||||
<div class="section">{{ T.it_hardware_status }}</div>
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.hardware_check }}</div>
|
||||
<table class="opt-grid">
|
||||
{% if HARDWARE_CHECKLIST %}
|
||||
{% for item in HARDWARE_CHECKLIST %}
|
||||
{% if loop.index0 % 3 == 0 %}<tr>{% endif %}
|
||||
<td><span class="cb">□</span> {{ item.label }}</td>
|
||||
{% if loop.index0 % 3 == 2 or loop.last %}</tr>{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr><td colspan="3">{{ T.no_onboarding_hardware }}</td></tr>
|
||||
{% endif %}
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="section">{{ T.manual_return_overview }}</div>
|
||||
<p class="manual-note">{{ T.manual_return_note }}</p>
|
||||
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.returned_devices }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in MANUAL_DEVICES %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.returned_software }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in MANUAL_SOFTWARE %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.removed_workspace_groups }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in MANUAL_WORKSPACE_GROUPS %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.removed_accesses }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in MANUAL_ACCESSES %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.returned_extra_it }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in MANUAL_EXTRA_HARDWARE %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for row in MANUAL_EXTRA_SOFTWARE %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 3 %}
|
||||
{% for _ in range(3 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section">{{ T.it_signatures }}</div>
|
||||
<div class="sigbox">
|
||||
<div class="sigline">{{ T.it_checked_by }} <span class="line"></span></div>
|
||||
<div class="sigline">{{ T.it_signature }} <span class="line"></span></div>
|
||||
<div class="sigline">{{ T.return_complete }} <span class="cb">□</span> {{ T.yes }}     <span class="cb">□</span> {{ T.no }}</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>
|
||||
</body>
|
||||
</html>
|
||||
72
backend/media/templates/onboarding_intro_session_pdf.html
Normal file
72
backend/media/templates/onboarding_intro_session_pdf.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ T.lang or 'de' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ T.live_intro_title }}</title>
|
||||
<style>
|
||||
body { font-family: Helvetica, Arial, sans-serif; color: #0f172a; font-size: 10.2px; line-height: 1.4; margin: 0; }
|
||||
.hero { background: #eef4ff; border: 1px solid #cbd9f6; border-left: 4px solid #3056a3; padding: 8px 10px; margin-bottom: 8px; }
|
||||
.title { margin: 0; font-size: 17px; color: #233f7a; font-weight: bold; }
|
||||
.sub { margin: 2px 0 0 0; color: #475569; font-size: 9.4px; }
|
||||
.section { margin-top: 9px; font-size: 11px; color: #233f7a; font-weight: bold; border-bottom: 1px solid #cbd9f6; padding-bottom: 2px; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 6px 0; }
|
||||
th, td { border: 1px solid #d5e2f9; padding: 4px 6px; vertical-align: top; overflow-wrap: anywhere; word-break: break-word; }
|
||||
th { background: #eaf1ff; color: #1f376b; font-weight: bold; text-align: left; width: 26%; }
|
||||
.opt-card { border: 1px solid #d5e2f9; margin-top: 6px; margin-bottom: 6px; }
|
||||
.opt-title { background: #3056a3; color: #fff; font-weight: bold; padding: 5px 6px; font-size: 9.5px; }
|
||||
.opt-grid { width: 100%; border-collapse: collapse; margin: 0; }
|
||||
.opt-grid td { border: 1px solid #d5e2f9; padding: 5px 6px; font-size: 9.4px; width: 50%; }
|
||||
.selected-mark { font-weight: bold; margin-right: 8px; color: #111827; }
|
||||
.note-box { border: 1px solid #d5e2f9; background: #fcfdff; padding: 8px 10px; margin-top: 8px; min-height: 44px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<h1 class="title">{{ T.live_intro_title }}</h1>
|
||||
<p class="sub">{{ T.live_intro_sub }}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">{{ T.base_data }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ T.name }}</th>
|
||||
<td>{{ DISPLAY_NAME }}</td>
|
||||
<th>{{ T.department }}</th>
|
||||
<td>{{ ABTEILUNG }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.job_title }}</th>
|
||||
<td>{{ BERUFSBEZEICHNUNG }}</td>
|
||||
<th>{{ T.employment_start }}</th>
|
||||
<td>{{ VERTRAGSBEGINN }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.work_email }}</th>
|
||||
<td>{{ EMAIL }}</td>
|
||||
<th>{{ T.introduced_by }}</th>
|
||||
<td>{{ REQUESTED_BY_NAME }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% for section in INTRO_SECTIONS %}
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ section.title }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in section.rows %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="selected-mark">✓</span>{{ cell.label }}</td>{% endfor %}
|
||||
{% if row|length < 2 %}<td></td>{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="section">{{ T.notes }}</div>
|
||||
<div class="note-box">{{ SESSION_NOTES }}</div>
|
||||
|
||||
<div class="section">{{ T.employee_signature_block }}</div>
|
||||
<div class="note-box" style="height: 70px; margin-top: 10px;"></div>
|
||||
</body>
|
||||
</html>
|
||||
193
backend/media/templates/onboarding_intro_template.html
Normal file
193
backend/media/templates/onboarding_intro_template.html
Normal file
@@ -0,0 +1,193 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ T.lang or 'de' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ T.intro_title }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
color: #0f172a;
|
||||
font-size: 10.2px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #eef4ff;
|
||||
border: 1px solid #cbd9f6;
|
||||
border-left: 4px solid #3056a3;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
color: #233f7a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 2px 0 0 0;
|
||||
color: #475569;
|
||||
font-size: 9.4px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 9px;
|
||||
font-size: 11px;
|
||||
color: #233f7a;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #cbd9f6;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #d5e2f9;
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #eaf1ff;
|
||||
color: #1f376b;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.note {
|
||||
margin: 6px 0 8px;
|
||||
color: #536174;
|
||||
font-size: 9.3px;
|
||||
}
|
||||
|
||||
.opt-card {
|
||||
border: 1px solid #d5e2f9;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.opt-title {
|
||||
background: #3056a3;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 5px 6px;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.opt-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.opt-grid td {
|
||||
border: 1px solid #d5e2f9;
|
||||
padding: 5px 6px;
|
||||
font-size: 9.4px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.cb {
|
||||
display: inline-block;
|
||||
min-width: 12px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
color: #b8b8b8;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
}
|
||||
|
||||
.sigbox {
|
||||
border: 1px solid #d5e2f9;
|
||||
padding: 10px;
|
||||
margin-top: 8px;
|
||||
background: #fcfdff;
|
||||
}
|
||||
|
||||
.sigline {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #334155;
|
||||
min-width: 190px;
|
||||
height: 12px;
|
||||
vertical-align: bottom;
|
||||
margin: 0 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<h1 class="title">{{ T.intro_title }}</h1>
|
||||
<p class="sub">{{ T.intro_sub }}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">{{ T.base_data }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ T.name }}</th>
|
||||
<td class="mono">{{ DISPLAY_NAME }}</td>
|
||||
<th>{{ T.department }}</th>
|
||||
<td>{{ ABTEILUNG }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.job_title }}</th>
|
||||
<td>{{ BERUFSBEZEICHNUNG }}</td>
|
||||
<th>{{ T.start_date }}</th>
|
||||
<td>{{ VERTRAGSBEGINN }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.work_email }}</th>
|
||||
<td>{{ EMAIL }}</td>
|
||||
<th>{{ T.introduced_by }}</th>
|
||||
<td>{{ REQUESTED_BY_NAME }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="note">{{ T.intro_note }}</p>
|
||||
|
||||
{% for section in INTRO_SECTIONS %}
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ section.title }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in section.rows %}
|
||||
<tr>
|
||||
{% for cell in row %}<td><span class="cb">□</span> {{ cell }}</td>{% endfor %}
|
||||
{% if row|length < 2 %}
|
||||
{% for _ in range(2 - row|length) %}<td></td>{% endfor %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="section">{{ T.confirmation }}</div>
|
||||
<div class="sigbox">
|
||||
<div class="sigline">{{ T.intro_completed_at }} <span class="line"></span></div>
|
||||
<div class="sigline">{{ T.employee_signature }} <span class="line"></span></div>
|
||||
<div class="sigline">{{ T.trainer_signature }} <span class="line"></span></div>
|
||||
<div class="sigline">{{ T.open_questions }} <span class="line" style="min-width: 260px;"></span></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
395
backend/media/templates/onboarding_template.html
Normal file
395
backend/media/templates/onboarding_template.html
Normal file
@@ -0,0 +1,395 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ PDF_LANG or 'de' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ T.onboarding_title }}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
color: #0f172a;
|
||||
font-size: 10.2px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #eef4ff;
|
||||
border: 1px solid #cbd9f6;
|
||||
border-left: 4px solid #3056a3;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
color: #233f7a;
|
||||
font-weight: bold;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 2px 0 0 0;
|
||||
color: #475569;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-top: 9px;
|
||||
font-size: 11px;
|
||||
color: #233f7a;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #cbd9f6;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #d5e2f9;
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #eaf1ff;
|
||||
color: #1f376b;
|
||||
font-weight: bold;
|
||||
width: 31%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-weight: bold;
|
||||
color: #111827;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.opt-card {
|
||||
border: 1px solid #d5e2f9;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.opt-title {
|
||||
background: #3056a3;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 5px 6px;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.opt-grid {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.opt-grid td {
|
||||
border: 1px solid #d5e2f9;
|
||||
padding: 3px 5px;
|
||||
width: 33.33%;
|
||||
font-size: 9.5px;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: #94a3b8;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.signature {
|
||||
width: 150px;
|
||||
height: 70px;
|
||||
max-width: 150px;
|
||||
max-height: 70px;
|
||||
border: 1px solid #c3d3f3;
|
||||
padding: 1px;
|
||||
object-fit: contain;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.small {
|
||||
color: #64748b;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.muted-cell {
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="hero">
|
||||
<h1 class="title">{{ T.onboarding_title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="section">{{ T.onboarding_staff_data }}</div>
|
||||
<table>
|
||||
<tr>
|
||||
<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 HAS_DEVICES %}
|
||||
<div class="opt-card">
|
||||
<div class="opt-title">{{ T.devices }}</div>
|
||||
<table class="opt-grid">
|
||||
{% for row in ARBEITSGERÄTE_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_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>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{{ T.requested_by_name }}</th>
|
||||
<td>{{ REQUESTED_BY_NAME }}</td>
|
||||
<th>{{ T.requested_by_email }}</th>
|
||||
<td>{{ REQUESTED_BY_EMAIL }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ T.signature }}</th>
|
||||
{% if UNTERSCHRIFT %}
|
||||
<td colspan="3"><img src="{{ UNTERSCHRIFT }}" alt="{{ T.signature_alt }}" class="signature"></td>
|
||||
{% else %}
|
||||
<td colspan="3" class="muted-cell">{{ UNTERSCHRIFT_HINWEIS }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="small">{{ T.onboarding_note }}</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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:
|
||||
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:
|
||||
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:
|
||||
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 = [
|
||||
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,
|
||||
|
||||
@@ -136,6 +136,8 @@ docker compose exec -T web django-admin compilemessages</code></pre>
|
||||
<li>PDF generation is HTML-to-PDF using <code>xhtml2pdf</code>.</li>
|
||||
<li>Letterhead overlay is applied from <code>templates.pdf</code>.</li>
|
||||
<li>Main logic lives in <code>backend/workflows/tasks.py</code>.</li>
|
||||
<li>Fixed PDF labels/headings are rendered from task-level DE/EN text dictionaries, not hard-coded directly in request processing logic.</li>
|
||||
<li>PDF language follows the normalized request <code>preferred_language</code>, with German fallback.</li>
|
||||
<li>Key templates:
|
||||
<ul>
|
||||
<li><code>onboarding_template.html</code></li>
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
<li>Additional onboarding intro template: <code>/backend/media/templates/onboarding_intro_template.html</code>.</li>
|
||||
<li>Letterhead: <code>/backend/media/templates/templates.pdf</code>.</li>
|
||||
<li>Output folder: <code>/backend/media/pdfs/</code>.</li>
|
||||
<li>Fixed PDF labels and notes are rendered through task-level DE/EN text maps and follow the request language with German fallback.</li>
|
||||
<li>Signature images are embedded for compatibility with xhtml2pdf rendering.</li>
|
||||
<li>Conditional sections are hidden if no data is provided.</li>
|
||||
<li>Offboarding fallback behavior: if no onboarding record is available, the PDF renders a manual onboarding review layout grouped into <code>Stammdaten</code>, <code>Vertrag</code>, <code>IT-Setup</code>, and <code>Abschluss</code>, plus checkbox grids for devices, software, accesses, workspace groups, resources, and additional IT items.</li>
|
||||
@@ -169,8 +170,9 @@
|
||||
<li><strong>Email phase added:</strong> admin-managed notification template subject/body content, notification rule custom subject/body content, and welcome email subject/body content now support explicit DE/EN values.</li>
|
||||
<li><strong>Runtime selection:</strong> onboarding and offboarding requests store the UI language at submission time, and notification delivery uses that language with German fallback.</li>
|
||||
<li><strong>Stability hardening:</strong> request models normalize <code>preferred_language</code> on save, and the database schema also enforces a default of <code>de</code>. This prevents null-language inserts outside the main form/view path.</li>
|
||||
<li><strong>PDF phase added:</strong> fixed PDF headings, labels, notes, and confirmation text now render in German or English based on the request language, with German as the fallback.</li>
|
||||
<li><strong>Editing path:</strong> these DE/EN values are maintained directly in the frontend builder pages, not only in Django admin.</li>
|
||||
<li><strong>Not fully bilingual yet:</strong> several generated PDF/business text blocks still remain primarily single-language.</li>
|
||||
<li><strong>Not fully bilingual yet:</strong> the main remaining gaps are long-form handbook/wiki copy and a few secondary admin/help texts.</li>
|
||||
<li><strong>Implementation:</strong> Django i18n with locale middleware, translation catalogs, and a DE/EN language switch in the main UI.</li>
|
||||
</ul>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user