mirror of
https://github.com/Bostame/onboarding_system.git
synced 2025-12-05 23:45:28 +01:00
Final Onboarding processes
This commit is contained in:
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"livePreview.defaultPreviewPath": "/templates/onboarding_template.html"
|
||||
}
|
||||
16
email_text/dritich_email.txt
Normal file
16
email_text/dritich_email.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Subject: HR Works Access Request for New Employee
|
||||
|
||||
Hello Dritich,
|
||||
|
||||
A new employee requires access to the HR system (HR Works) for their onboarding process.
|
||||
|
||||
Please proceed with creating the HR Works account for the following employee:
|
||||
|
||||
Employee Name: {{VORNAME}} {{NACHNAME}}
|
||||
Department: {{ABTEILUNG}}
|
||||
Start Date: {{VERTRAGSBEGINN}}
|
||||
|
||||
If you require any additional information, please let us know.
|
||||
|
||||
Best regards,
|
||||
The Onboarding Team
|
||||
16
email_text/minuth_email.txt
Normal file
16
email_text/minuth_email.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Subject: New Key Request for Onboarding Employee
|
||||
|
||||
Hello Minuth,
|
||||
|
||||
We have a new employee who requires a key (Schlüssel) as part of their onboarding process.
|
||||
|
||||
Please proceed with creating and providing the necessary key for the following employee:
|
||||
|
||||
Employee Name: {{VORNAME}} {{NACHNAME}}
|
||||
Department: {{ABTEILUNG}}
|
||||
Start Date: {{VERTRAGSBEGINN}}
|
||||
|
||||
If you have any questions or need further details, feel free to reach out.
|
||||
|
||||
Best regards,
|
||||
The Onboarding Team
|
||||
19
env/.env
vendored
Normal file
19
env/.env
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Email Server Configuration
|
||||
IMAP_SERVER=mxe839.netcup.net
|
||||
SMTP_SERVER=mxe839.netcup.net
|
||||
EMAIL_PORT=465 # Port for SSL/TLS (Port 465)
|
||||
EMAIL_ACCOUNT=onboarding@bostame.de
|
||||
PASSWORD=
|
||||
MAILBOX=INBOX
|
||||
|
||||
# Recipients for notification emails
|
||||
MINUTH_EMAIL=minuth@bostame.de
|
||||
DRITICH_EMAIL=dittrich@bostame.de
|
||||
|
||||
# Additional directories or paths
|
||||
TEMPLATES_DIR=templates
|
||||
ATTACHMENTS_DIR=attachments
|
||||
ONBOARDED_DIR=onboarded_person
|
||||
TEMP_PDF_DIR=temp_pdf
|
||||
EMAIL_TEXT_DIR=email_text
|
||||
|
||||
276
main.py
Normal file
276
main.py
Normal file
@@ -0,0 +1,276 @@
|
||||
import imaplib
|
||||
import email
|
||||
import os
|
||||
import pandas as pd
|
||||
import pdfkit
|
||||
from jinja2 import Template
|
||||
from PyPDF2 import PdfWriter, PdfReader, PageObject
|
||||
from datetime import datetime
|
||||
from email.header import decode_header
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import uuid
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import re
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv(dotenv_path=Path(__file__).parent / 'env' / '.env')
|
||||
|
||||
# Retrieve secret credentials and directories from environment variables
|
||||
IMAP_SERVER = os.getenv('IMAP_SERVER')
|
||||
SMTP_SERVER = os.getenv('SMTP_SERVER')
|
||||
EMAIL_PORT = int(os.getenv('EMAIL_PORT'))
|
||||
EMAIL_ACCOUNT = os.getenv('EMAIL_ACCOUNT')
|
||||
PASSWORD = os.getenv('PASSWORD')
|
||||
MAILBOX = os.getenv('MAILBOX')
|
||||
|
||||
# Recipients from .env file
|
||||
MINUTH_EMAIL = os.getenv('MINUTH_EMAIL')
|
||||
DRITICH_EMAIL = os.getenv('DRITICH_EMAIL')
|
||||
|
||||
# Retrieve directory paths from environment variables
|
||||
TEMPLATES_DIR = Path(os.getenv('TEMPLATES_DIR'))
|
||||
ATTACHMENTS_DIR = Path(os.getenv('ATTACHMENTS_DIR'))
|
||||
ONBOARDED_DIR = Path(os.getenv('ONBOARDED_DIR'))
|
||||
TEMP_PDF_DIR = Path(os.getenv('TEMP_PDF_DIR'))
|
||||
EMAIL_TEXT_DIR = Path(os.getenv('EMAIL_TEXT_DIR'))
|
||||
|
||||
# Ensure directories exist
|
||||
for directory in [ATTACHMENTS_DIR, TEMPLATES_DIR, ONBOARDED_DIR, TEMP_PDF_DIR, EMAIL_TEXT_DIR]:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Configure path to wkhtmltopdf
|
||||
path_to_wkhtmltopdf = Path(r'C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe')
|
||||
config = pdfkit.configuration(wkhtmltopdf=str(path_to_wkhtmltopdf))
|
||||
options = {
|
||||
'enable-local-file-access': None,
|
||||
'page-size': 'A4',
|
||||
'margin-top': '50mm',
|
||||
'margin-bottom': '20mm',
|
||||
'margin-left': '20mm',
|
||||
'margin-right': '20mm'
|
||||
}
|
||||
|
||||
# Function to send an email with dynamic content
|
||||
def send_email_notification(to_email, subject, template_file, context):
|
||||
try:
|
||||
# Load the email content from the text file and render the template
|
||||
with open(template_file, 'r', encoding='utf-8') as file:
|
||||
template_content = file.read()
|
||||
|
||||
# Render the email content with the context (employee data)
|
||||
template = Template(template_content)
|
||||
rendered_content = template.render(context)
|
||||
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = EMAIL_ACCOUNT
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = subject
|
||||
|
||||
# Attach the rendered email body
|
||||
msg.attach(MIMEText(rendered_content, 'plain'))
|
||||
|
||||
# Send the email via SMTP over SSL
|
||||
print(f"Attempting to connect to SMTP server {SMTP_SERVER} on port {EMAIL_PORT} using SSL...")
|
||||
with smtplib.SMTP_SSL(SMTP_SERVER, EMAIL_PORT) as server:
|
||||
print(f"Connected to SMTP server {SMTP_SERVER}, attempting to login...")
|
||||
server.login(EMAIL_ACCOUNT, PASSWORD)
|
||||
print(f"Logged in successfully, sending email to {to_email}...")
|
||||
server.send_message(msg)
|
||||
print(f"Email sent to {to_email}")
|
||||
except Exception as e:
|
||||
print(f"Failed to send email to {to_email}: {e}")
|
||||
finally:
|
||||
# Introduce a small delay between emails to avoid potential throttling issues
|
||||
time.sleep(5)
|
||||
|
||||
# Helper function to split a list into chunks of a specified size and format it in title case
|
||||
def chunk_list(data_list, chunk_size=4):
|
||||
title_case_list = [item.title() for item in data_list]
|
||||
for i in range(0, len(title_case_list), chunk_size):
|
||||
yield title_case_list[i:i + chunk_size]
|
||||
|
||||
# Function to generate PDF from HTML
|
||||
def generate_pdf_from_html(html_content, output_pdf):
|
||||
try:
|
||||
pdfkit.from_string(html_content, output_pdf, configuration=config, options=options)
|
||||
print(f"Generated PDF: {output_pdf}")
|
||||
except Exception as e:
|
||||
print(f"Error generating PDF {output_pdf}: {e}")
|
||||
|
||||
# Overlay generated PDF content onto the letterhead
|
||||
def overlay_content_on_letterhead(content_pdf, letterhead_pdf, output_pdf):
|
||||
try:
|
||||
letterhead_reader = PdfReader(letterhead_pdf)
|
||||
content_reader = PdfReader(content_pdf)
|
||||
|
||||
writer = PdfWriter()
|
||||
letterhead_page = letterhead_reader.pages[0]
|
||||
|
||||
for page_num in range(len(content_reader.pages)):
|
||||
content_page = content_reader.pages[page_num]
|
||||
overlay_page = PageObject.create_blank_page(width=letterhead_page.mediabox.width, height=letterhead_page.mediabox.height)
|
||||
overlay_page.merge_page(letterhead_page)
|
||||
overlay_page.merge_page(content_page)
|
||||
writer.add_page(overlay_page)
|
||||
|
||||
with open(output_pdf, 'wb') as output_file:
|
||||
writer.write(output_file)
|
||||
print(f"Final PDF saved to: {output_pdf}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error overlaying content on letterhead: {e}")
|
||||
|
||||
# Function to process CSV and generate PDF with email feature
|
||||
def process_csv_and_generate_pdf(csv_file):
|
||||
try:
|
||||
print(f"Processing CSV file: {csv_file}")
|
||||
|
||||
# Load CSV data
|
||||
data = pd.read_csv(csv_file)
|
||||
print(f"CSV Data loaded successfully for {csv_file}")
|
||||
|
||||
# Load the HTML template from the templates directory
|
||||
with open(TEMPLATES_DIR / 'onboarding_template.html', 'r', encoding='utf-8') as file:
|
||||
html_template = file.read()
|
||||
print("HTML template loaded successfully")
|
||||
|
||||
for index, row in data.iterrows():
|
||||
try:
|
||||
print(f"Processing row {index}")
|
||||
|
||||
# Split "Vorname und Nachname" into first and last name
|
||||
name_parts = row['Vorname und Nachname'].split()
|
||||
vorname = name_parts[0]
|
||||
nachname = " ".join(name_parts[1:])
|
||||
|
||||
# Chunk the multiline fields into formatted lists
|
||||
arbeitsegeraete_list = list(chunk_list(row.get('Der Mitarbeiter Benötigt Folgende Arbeitsgeräte', '').split('\n')))
|
||||
zugaenge_list = list(chunk_list(row.get('Die Folgenden Zugänge und Rollen Sollen Zu Workspace Eingerichtet Werden', '').split('\n')))
|
||||
software_list = list(chunk_list(row.get('Darüber Hinaus Benötigt Er Folgende Software', '').split('\n')))
|
||||
account_list = list(chunk_list(row.get('Zugänge, Die Standardmäßig Eingerichtet Werden Sollen, Bitte Benennen', '').split('\n')))
|
||||
standard_zugaenge_list = row.get('Zugänge, Die Standardmäßig Eingerichtet Werden Sollen, Bitte Benennen', '').split('\n')
|
||||
printer_list = list(chunk_list(row.get('Ressourcen, Die Standardmäßig Eingerichtet Werden Sollen, Bitte Benennen', '').split('\n')))
|
||||
telephone = row.get('TUBS-Telefon-Direktwahl-Nr. 030 447202 (10-89)', None)
|
||||
|
||||
|
||||
|
||||
# Prepare the context with employee data
|
||||
context = {
|
||||
'VORNAME': vorname,
|
||||
'NACHNAME': nachname,
|
||||
'BERUFSBEZEICHNUNG': row.get('Berufsbezeichnung', 'N/A'),
|
||||
'ABTEILUNG': row.get('Abteilung', 'N/A'),
|
||||
'EMAIL': row.get('Gewünschte Dienstliche E-Mail-Adresse', 'N/A'),
|
||||
'VERTRAGSBEGINN': row.get('Vertragsbeginn', 'N/A'),
|
||||
'UEBERGABEDATUM': row.get('Gewünschtes Übergabedatum der Geräte', 'N/A'),
|
||||
'GRUPPENPOSTFAECHER_ERFORDERLICH': row.get('Gruppenpostfächer Erforderlich?', 'N/A'),
|
||||
'ZUGAENGE_LIST': zugaenge_list,
|
||||
'ARBEITSGERÄTE_LIST': arbeitsegeraete_list,
|
||||
'SOFTWARE_LIST': software_list,
|
||||
'ACCOUNT_LIST': account_list, # Pass Standard-Zugänge data
|
||||
'STANDARD_ZUGAENGE': row.get('Zugänge, Die Standardmäßig Eingerichtet Werden Sollen, Bitte Benennen', ''),
|
||||
'SOFTWAREWUNSCH': row.get('Haben Sie Einen Zusätzlichen Softwarewunsch?', ''),
|
||||
'STANDARD_RESSOURCEN': printer_list,
|
||||
'TELEFONNUMMER': telephone,
|
||||
'BEMERKUNGEN': row.get('Haben Wir Irgendetwas Übersehen? Schreiben Sie Uns Hier.', 'nan'),
|
||||
'VEREINBARUNG': row.get('Vereinbarung', ''),
|
||||
'UNTERSCHRIFT': row.get('Unterschrift', ''),
|
||||
}
|
||||
|
||||
# Fill the template with data
|
||||
html_content = Template(html_template).render(context)
|
||||
|
||||
# Generate content PDF and save to temp_pdf directory
|
||||
content_pdf_path = TEMP_PDF_DIR / f'temp_content_{vorname}_{nachname}.pdf'
|
||||
print(f"Generating content PDF at: {content_pdf_path}")
|
||||
generate_pdf_from_html(html_content, str(content_pdf_path))
|
||||
|
||||
# Define output PDF filename and save to onboarded_person directory
|
||||
output_pdf_path = ONBOARDED_DIR / f'onboarding_letter_{vorname}_{nachname}.pdf'.replace(" ", "_")
|
||||
|
||||
# Overlay content on letterhead and save final PDF
|
||||
letterhead_pdf = TEMPLATES_DIR / 'templates.pdf'
|
||||
print(f"Overlaying content on letterhead: {letterhead_pdf}")
|
||||
overlay_content_on_letterhead(str(content_pdf_path), letterhead_pdf, output_pdf_path)
|
||||
|
||||
print(f"Final PDF generated and saved at: {output_pdf_path}")
|
||||
|
||||
# Email notifications based on conditions
|
||||
# Check for "Schlüssel" in ARBEITSGERÄTE_LIST and send email
|
||||
if any("Schlüssel".lower() in s.lower() for sublist in arbeitsegeraete_list for s in sublist if isinstance(s, str)):
|
||||
print(f"'Schlüssel' found for {vorname} {nachname}")
|
||||
send_email_notification(MINUTH_EMAIL, 'Schlüssel Required', EMAIL_TEXT_DIR / 'minuth_email.txt', context)
|
||||
|
||||
# Check for "HR Works" in STANDARD_ZUGAENGE_LIST and send email
|
||||
if any("HR Works".lower() in s.lower() for s in standard_zugaenge_list if isinstance(s, str)):
|
||||
print(f"'HR Works' found for {vorname} {nachname}")
|
||||
send_email_notification(DRITICH_EMAIL, 'HR Works Access Required', EMAIL_TEXT_DIR / 'dritich_email.txt', context)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing row {index}: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing CSV {csv_file}: {e}")
|
||||
|
||||
|
||||
# Function to check email for CSV attachments
|
||||
def check_email_for_csv():
|
||||
try:
|
||||
# Connect to the email server
|
||||
mail = imaplib.IMAP4_SSL(IMAP_SERVER)
|
||||
mail.login(EMAIL_ACCOUNT, PASSWORD)
|
||||
mail.select(MAILBOX)
|
||||
|
||||
# Search for all unread emails
|
||||
status, messages = mail.search(None, '(UNSEEN)')
|
||||
email_ids = messages[0].split()
|
||||
|
||||
for e_id in email_ids:
|
||||
res, msg = mail.fetch(e_id, "(RFC822)")
|
||||
for response_part in msg:
|
||||
if isinstance(response_part, tuple):
|
||||
msg = email.message_from_bytes(response_part[1])
|
||||
subject, encoding = decode_header(msg["Subject"])[0]
|
||||
|
||||
if isinstance(subject, bytes):
|
||||
subject = subject.decode(encoding or "utf-8")
|
||||
|
||||
print(f"Processing email: {subject}")
|
||||
|
||||
# Loop through email parts to find attachments
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_disposition = str(part.get("Content-Disposition"))
|
||||
if "attachment" in content_disposition:
|
||||
filename = part.get_filename()
|
||||
|
||||
if filename.endswith(".csv"):
|
||||
# Save the CSV attachment with a unique name
|
||||
unique_filename = f"{filename.split('.')[0]}_{uuid.uuid4().hex}.csv"
|
||||
filepath = ATTACHMENTS_DIR / unique_filename
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(part.get_payload(decode=True))
|
||||
print(f"CSV file saved as {filepath}")
|
||||
|
||||
# Process CSV and generate PDF
|
||||
process_csv_and_generate_pdf(filepath)
|
||||
|
||||
mail.logout()
|
||||
except Exception as e:
|
||||
print(f"Failed to check email: {e}")
|
||||
|
||||
# Countdown timer for 30 seconds before checking again
|
||||
def countdown_timer(seconds):
|
||||
for i in range(seconds, 0, -1):
|
||||
print(f"Waiting {i} seconds...", end="\r")
|
||||
time.sleep(1)
|
||||
print("Checking emails now...")
|
||||
|
||||
# Continuously check for new emails every 30 seconds
|
||||
while True:
|
||||
check_email_for_csv()
|
||||
countdown_timer(30)
|
||||
204
templates/onboarding_template.html
Normal file
204
templates/onboarding_template.html
Normal file
@@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Onboarding Letter</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
font-size: 14px; /* Reduced font size to fit more content on the page */
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: #231093;
|
||||
font-weight: bold;
|
||||
font-size: 16px; /* Reduced heading sizes */
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.csv-input {
|
||||
color: #000078;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.signature {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
img.signature {
|
||||
width: 150px; /* Reduced image size */
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.table-layout {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.table-layout td, .table-layout th {
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px; /* Reduced padding in table cells */
|
||||
text-align: left;
|
||||
font-size: 12px; /* Reduced font size in tables */
|
||||
}
|
||||
|
||||
.table-layout th {
|
||||
background-color: #f0f8ff;
|
||||
color: #231093;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding: 30px 0; /* Reduced padding to conserve space */
|
||||
}
|
||||
|
||||
.intro-message {
|
||||
font-size: 15px; /* Reduced size of the introductory message */
|
||||
font-style: italic;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.footer-message {
|
||||
margin-top: 40px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-content">
|
||||
<p class="intro-message">"Willkommen im Team! Wir sind begeistert, Sie auf dieser spannenden Reise an unserer Seite zu haben."</p>
|
||||
|
||||
<!-- Use the full name in the salutation -->
|
||||
<p>Sehr geehrte/r <span class="csv-input">{{VORNAME}} {{NACHNAME}}</span>,</p>
|
||||
|
||||
<p>Wir freuen uns riesig, Sie als unsere/n neue/n <span class="csv-input">{{BERUFSBEZEICHNUNG}}</span> in der Abteilung <span class="csv-input">{{ABTEILUNG}}</span> begrüßen zu dürfen. Ihr Arbeitsvertrag beginnt am <span class="csv-input">{{VERTRAGSBEGINN}}</span>. Der Termin für die Übergabe der benötigten Arbeitsmittel ist für den <span class="csv-input">{{UEBERGABEDATUM}}</span> vorgesehen. Ihre dienstliche E-Mail-Adresse lautet: <span class="csv-input">{{EMAIL}}</span></p>
|
||||
|
||||
<p>Mit Ihnen gemeinsam werden wir großartige Dinge erreichen!</p>
|
||||
|
||||
<h3>Ihre Arbeitsgeräte</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Arbeitsgerät</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in ARBEITSGERÄTE_LIST %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Zugänge und Rollen</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zugang</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in ZUGAENGE_LIST %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Benötigte Software</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Software</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in SOFTWARE_LIST %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Liste der Konten</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Konten</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in ACCOUNT_LIST %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Drucker im Büro</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Drucker</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in STANDARD_RESSOURCEN %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>TUBS-Telefon</h3>
|
||||
<table class="table-layout">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Direktwahl-Nr.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in TELEFONNUMMER %}
|
||||
<tr>
|
||||
{% for cell in row %}
|
||||
<td>{{ cell }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<h3>Unterschrift:</h3>
|
||||
<img src="{{UNTERSCHRIFT}}" alt="Unterschrift" class="signature">
|
||||
|
||||
<p class="footer-message">Nochmals herzlich willkommen! Zögern Sie nicht, uns bei Fragen jederzeit zu kontaktieren. Wir freuen uns darauf, gemeinsam mit Ihnen durchzustarten!</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
BIN
templates/templates.pdf
Normal file
BIN
templates/templates.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user