snapshot: preserve dashboard redesign and live protocol workflow state
This commit is contained in:
@@ -12,7 +12,7 @@ from jinja2 import Template
|
||||
from pypdf import PageObject, PdfReader, PdfWriter
|
||||
from xhtml2pdf import pisa
|
||||
|
||||
from .models import EmployeeProfile, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig
|
||||
from .models import EmployeeProfile, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig
|
||||
from .emailing import send_system_email
|
||||
from .services import upload_to_nextcloud
|
||||
from .services import get_email_test_redirect, is_email_test_mode
|
||||
@@ -269,6 +269,111 @@ def _resolve_workflow_emails() -> tuple[str, str, str, str, str]:
|
||||
return it_email, general_info_email, business_card_email, hr_works_email, key_email
|
||||
|
||||
|
||||
def _matches_intro_condition(request_obj: OnboardingRequest, item: IntroChecklistItem) -> bool:
|
||||
operator = (item.condition_operator or 'always').strip()
|
||||
field_name = (item.condition_field or '').strip()
|
||||
expected = (item.condition_value or '').strip()
|
||||
|
||||
if operator == 'always' or not field_name:
|
||||
return True
|
||||
|
||||
raw_value = getattr(request_obj, field_name, '')
|
||||
if raw_value is None:
|
||||
raw_value = ''
|
||||
|
||||
if operator == 'is_true':
|
||||
return bool(raw_value)
|
||||
if operator == 'is_false':
|
||||
return not bool(raw_value)
|
||||
|
||||
text_value = str(raw_value).strip()
|
||||
if operator == 'equals':
|
||||
return text_value.lower() == expected.lower()
|
||||
if operator == 'contains':
|
||||
return expected.lower() in text_value.lower()
|
||||
return True
|
||||
|
||||
|
||||
def _build_intro_sections_from_admin(request_obj: OnboardingRequest) -> dict[str, list[str]]:
|
||||
items = list(IntroChecklistItem.objects.filter(is_active=True).order_by('section', 'sort_order', 'label'))
|
||||
if not items:
|
||||
return {}
|
||||
|
||||
section_map = {key: [] for key, _label in IntroChecklistItem.SECTION_CHOICES}
|
||||
for item in items:
|
||||
if item.section not in section_map:
|
||||
continue
|
||||
if _matches_intro_condition(request_obj, item):
|
||||
section_map[item.section].append(item.label)
|
||||
return {key: values for key, values in section_map.items() if values}
|
||||
|
||||
|
||||
def build_intro_sections_for_request(request_obj: OnboardingRequest) -> list[dict]:
|
||||
devices = _split_multiline(request_obj.needed_devices)
|
||||
software = _split_multiline(request_obj.needed_software)
|
||||
accesses = _split_multiline(request_obj.needed_accesses)
|
||||
groups = _split_multiline(request_obj.needed_workspace_groups)
|
||||
resources = _split_multiline(request_obj.needed_resources)
|
||||
extra_hardware = _split_multiline(request_obj.additional_hardware)
|
||||
extra_software = _split_multiline(request_obj.additional_software)
|
||||
group_mailboxes = _split_multiline(request_obj.group_mailboxes)
|
||||
|
||||
workplace_items = []
|
||||
for item in devices:
|
||||
workplace_items.append(f'{item} übergeben und Grundfunktionen erklärt')
|
||||
for item in resources:
|
||||
workplace_items.append(f'{item} gezeigt bzw. Nutzung erklärt')
|
||||
if request_obj.phone_number:
|
||||
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')
|
||||
|
||||
account_items = [f'{item} Zugang erklärt' for item in accesses]
|
||||
account_items.extend([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}')
|
||||
if group_mailboxes:
|
||||
account_items.extend([f'Gruppenpostfach erklärt: {item}' for item in group_mailboxes])
|
||||
if not account_items:
|
||||
account_items.append('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])
|
||||
if not software_items:
|
||||
software_items.append('Benötigte Standardsoftware und tägliche Nutzung erklärt')
|
||||
|
||||
process_items = [
|
||||
'Passwortregeln und sicherer Umgang besprochen',
|
||||
'Dateiablage, Nextcloud und Freigaben erklärt',
|
||||
'Kommunikationswege und Support-Prozess erklärt',
|
||||
]
|
||||
if extra_hardware:
|
||||
process_items.extend([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)])
|
||||
if request_obj.successor_name:
|
||||
process_items.append(f'Übergabe-/Nachfolgekontext besprochen: {request_obj.successor_name}')
|
||||
|
||||
custom_intro_items = _build_intro_sections_from_admin(request_obj)
|
||||
intro_sections_raw = [
|
||||
('workplace', 'Geräte und Arbeitsplatz', workplace_items),
|
||||
('accounts', 'Konten und Berechtigungen', account_items),
|
||||
('software', 'Software und Tools', software_items),
|
||||
('process', 'Prozesse und Hinweise', process_items),
|
||||
]
|
||||
|
||||
sections = []
|
||||
for key, title, default_items in intro_sections_raw:
|
||||
merged_items = list(default_items)
|
||||
merged_items.extend(custom_intro_items.get(key, []))
|
||||
section_items = []
|
||||
for idx, label in enumerate(merged_items, start=1):
|
||||
section_items.append({'id': f'{key}_{idx}', 'label': label})
|
||||
if section_items:
|
||||
sections.append({'key': key, 'title': title, 'items': section_items})
|
||||
return sections
|
||||
|
||||
|
||||
def _send_workflow_email(
|
||||
subject: str,
|
||||
body: str,
|
||||
@@ -609,6 +714,107 @@ def _generate_onboarding_pdf(request_obj: OnboardingRequest) -> Path:
|
||||
return output_pdf
|
||||
|
||||
|
||||
def _generate_onboarding_intro_pdf(request_obj: OnboardingRequest) -> Path:
|
||||
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'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_template.html'
|
||||
letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf'
|
||||
|
||||
salutation = (request_obj.get_gender_display() or '').strip()
|
||||
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
|
||||
intro_sections = [
|
||||
{
|
||||
'title': section['title'],
|
||||
'rows': _chunk_list([item['label'] for item in section['items']], chunk_size=2),
|
||||
}
|
||||
for section in build_intro_sections_for_request(request_obj)
|
||||
]
|
||||
|
||||
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 '-'
|
||||
|
||||
context = {
|
||||
'DISPLAY_NAME': display_name,
|
||||
'ABTEILUNG': request_obj.department or '-',
|
||||
'BERUFSBEZEICHNUNG': request_obj.job_title or '-',
|
||||
'VERTRAGSBEGINN': request_obj.contract_start,
|
||||
'EMAIL': request_obj.work_email or '-',
|
||||
'REQUESTED_BY_NAME': requester_name,
|
||||
'REQUESTED_BY_EMAIL': requester_email,
|
||||
'INTRO_SECTIONS': intro_sections,
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
|
||||
|
||||
def _generate_onboarding_intro_session_pdf(session: OnboardingIntroductionSession, admin_signature_name: str = '-') -> Path:
|
||||
request_obj = session.onboarding_request
|
||||
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'
|
||||
temp_pdf = settings.PDF_OUTPUT_DIR / f'_temp_onboarding_intro_session_{safe_name}_{version}.pdf'
|
||||
|
||||
template_path = settings.PDF_TEMPLATES_DIR / 'onboarding_intro_session_pdf.html'
|
||||
letterhead_path = settings.PDF_TEMPLATES_DIR / 'templates.pdf'
|
||||
|
||||
salutation = (request_obj.get_gender_display() or '').strip()
|
||||
display_name = f"{salutation} {request_obj.full_name}".strip() if salutation else request_obj.full_name
|
||||
|
||||
raw_sections = build_intro_sections_for_request(request_obj)
|
||||
checked_map = session.checklist_state or {}
|
||||
exported_sections = []
|
||||
checked_count = 0
|
||||
total_count = 0
|
||||
for section in raw_sections:
|
||||
checked_items = []
|
||||
for item in section['items']:
|
||||
checked = bool(checked_map.get(item['id']))
|
||||
total_count += 1
|
||||
if checked:
|
||||
checked_count += 1
|
||||
checked_items.append({'label': item['label']})
|
||||
if checked_items:
|
||||
exported_sections.append({
|
||||
'title': section['title'],
|
||||
'rows': [checked_items[i:i + 2] for i in range(0, len(checked_items), 2)],
|
||||
})
|
||||
|
||||
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 '-'
|
||||
|
||||
context = {
|
||||
'DISPLAY_NAME': display_name,
|
||||
'ABTEILUNG': request_obj.department or '-',
|
||||
'BERUFSBEZEICHNUNG': request_obj.job_title or '-',
|
||||
'VERTRAGSBEGINN': request_obj.contract_start,
|
||||
'EMAIL': request_obj.work_email or '-',
|
||||
'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 '-',
|
||||
'INTRO_SECTIONS': exported_sections,
|
||||
}
|
||||
|
||||
html = _render_html(template_path, context)
|
||||
_generate_content_pdf(html, temp_pdf)
|
||||
_overlay_with_letterhead(temp_pdf, letterhead_path, output_pdf)
|
||||
|
||||
if temp_pdf.exists():
|
||||
temp_pdf.unlink(missing_ok=True)
|
||||
return output_pdf
|
||||
|
||||
|
||||
def _generate_offboarding_pdf(request_obj: OffboardingRequest) -> Path:
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user