651 lines
28 KiB
Python
651 lines
28 KiB
Python
from datetime import timedelta
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
from django.utils.translation import get_language, gettext as _
|
|
|
|
from .branding import get_company_email_domain
|
|
from .form_builder import LOCKED_SECTION_RULES, OFFBOARDING_PAGE_ORDER, ensure_form_section_configs, get_section_definitions
|
|
from .forms import OffboardingRequestForm, OnboardingRequestForm
|
|
from .models import AdminAuditLog, EmployeeProfile, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, WorkflowConfig
|
|
from .roles import user_has_capability
|
|
from .tasks import _generate_onboarding_intro_pdf, _generate_onboarding_intro_session_pdf, build_intro_sections_for_request, process_offboarding_request, process_onboarding_request
|
|
|
|
|
|
def request_timeline_page_impl(request, kind: str, request_id: int, *, request_target_label_fn, request_custom_field_details_fn, audit_action_label_fn):
|
|
if kind == 'onboarding':
|
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
|
elif kind == 'offboarding':
|
|
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
|
else:
|
|
messages.error(request, f'Unbekannter Typ: {kind}')
|
|
return redirect('requests_dashboard')
|
|
|
|
request_label = request_target_label_fn(obj, kind)
|
|
custom_field_details = request_custom_field_details_fn(obj, kind, getattr(request, 'LANGUAGE_CODE', None))
|
|
audit_rows = list(
|
|
AdminAuditLog.objects.select_related('actor')
|
|
.filter(target_type__in=[kind, 'request'])
|
|
.filter(Q(target_id=request_id) | Q(target_label__icontains=(obj.full_name or '').strip()))
|
|
.order_by('-created_at', '-id')[:200]
|
|
)
|
|
|
|
timeline_rows = [
|
|
{
|
|
'created_at': obj.created_at,
|
|
'kind': 'system',
|
|
'title': _('Anfrage erstellt'),
|
|
'summary': request_label,
|
|
'meta': _('Status: %(status)s') % {'status': obj.get_processing_status_display()},
|
|
'details': {item['label']: item['value'] for item in custom_field_details},
|
|
}
|
|
]
|
|
|
|
contract_start = getattr(obj, 'contract_start', None)
|
|
if contract_start:
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': timezone.make_aware(timezone.datetime.combine(contract_start, timezone.datetime.min.time())),
|
|
'kind': 'milestone',
|
|
'title': _('Vertragsbeginn'),
|
|
'summary': str(contract_start),
|
|
'meta': _('Geplanter Start'),
|
|
}
|
|
)
|
|
|
|
handover_date = getattr(obj, 'handover_date', None)
|
|
if handover_date:
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': timezone.make_aware(timezone.datetime.combine(handover_date, timezone.datetime.min.time())),
|
|
'kind': 'milestone',
|
|
'title': _('Geräteübergabe / Hardware-Abholung'),
|
|
'summary': str(handover_date),
|
|
'meta': _('Geplanter Hardware-Termin'),
|
|
}
|
|
)
|
|
|
|
if getattr(obj, 'generated_pdf_path', ''):
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': obj.created_at,
|
|
'kind': 'document',
|
|
'title': _('PDF verfügbar'),
|
|
'summary': Path(obj.generated_pdf_path).name,
|
|
'meta': '',
|
|
'url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}",
|
|
}
|
|
)
|
|
|
|
for row in audit_rows:
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': row.created_at,
|
|
'kind': 'audit',
|
|
'title': audit_action_label_fn(row.action),
|
|
'summary': row.target_label or row.target_type or '-',
|
|
'meta': row.actor_display or '-',
|
|
'details': row.details,
|
|
}
|
|
)
|
|
|
|
if kind == 'onboarding':
|
|
intro_session = OnboardingIntroductionSession.objects.filter(onboarding_request=obj).first()
|
|
if intro_session:
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': intro_session.updated_at,
|
|
'kind': 'session',
|
|
'title': _('Einweisungssitzung'),
|
|
'summary': intro_session.get_status_display(),
|
|
'meta': intro_session.completed_by_name or '-',
|
|
'url': (f"/media/pdfs/{Path(intro_session.exported_pdf_path).name}" if intro_session.exported_pdf_path else ''),
|
|
}
|
|
)
|
|
welcome_email = ScheduledWelcomeEmail.objects.filter(onboarding_request=obj).first()
|
|
if welcome_email:
|
|
timeline_rows.append(
|
|
{
|
|
'created_at': welcome_email.updated_at,
|
|
'kind': 'email',
|
|
'title': _('Welcome E-Mail'),
|
|
'summary': welcome_email.get_status_display(),
|
|
'meta': welcome_email.recipient_email,
|
|
}
|
|
)
|
|
|
|
timeline_rows.sort(key=lambda item: item['created_at'])
|
|
|
|
return render(
|
|
request,
|
|
'workflows/request_timeline.html',
|
|
{
|
|
'request_kind': kind,
|
|
'request_obj': obj,
|
|
'request_label': request_label,
|
|
'timeline_rows': timeline_rows,
|
|
'custom_field_details': custom_field_details,
|
|
'contract_start': getattr(obj, 'contract_start', None),
|
|
'handover_date': getattr(obj, 'handover_date', None),
|
|
},
|
|
)
|
|
|
|
def requests_dashboard_impl(request, *, audit_fn, request_target_label_fn, request_status_label_fn):
|
|
if not user_has_capability(request.user, 'access_requests_dashboard'):
|
|
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
|
return redirect('home')
|
|
|
|
if request.method == 'POST':
|
|
if not user_has_capability(request.user, 'delete_requests'):
|
|
messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.'))
|
|
return redirect('requests_dashboard')
|
|
|
|
selected = request.POST.getlist('selected_requests')
|
|
single_delete = (request.POST.get('single_delete') or '').strip()
|
|
if single_delete:
|
|
selected = [single_delete]
|
|
|
|
if not selected:
|
|
messages.warning(request, _('Keine Einträge ausgewählt.'))
|
|
return redirect('requests_dashboard')
|
|
|
|
deleted_count = 0
|
|
invalid_count = 0
|
|
deleted_labels = []
|
|
for token in selected:
|
|
try:
|
|
kind, raw_id = token.split(':', 1)
|
|
request_id = int(raw_id)
|
|
except (ValueError, TypeError):
|
|
invalid_count += 1
|
|
continue
|
|
|
|
model = None
|
|
if kind == 'onboarding':
|
|
model = OnboardingRequest
|
|
elif kind == 'offboarding':
|
|
model = OffboardingRequest
|
|
else:
|
|
invalid_count += 1
|
|
continue
|
|
|
|
obj = model.objects.filter(id=request_id).first()
|
|
if not obj:
|
|
continue
|
|
deleted_labels.append(request_target_label_fn(obj, kind))
|
|
obj.delete()
|
|
deleted_count += 1
|
|
|
|
if deleted_count:
|
|
audit_fn(
|
|
request,
|
|
'requests_deleted',
|
|
target_type='request',
|
|
target_label='Dashboard bulk/single delete',
|
|
details={
|
|
'deleted_count': deleted_count,
|
|
'invalid_count': invalid_count,
|
|
'selected': selected,
|
|
'request_labels': deleted_labels,
|
|
},
|
|
)
|
|
messages.success(request, _('%(count)s Eintrag/Einträge gelöscht.') % {'count': deleted_count})
|
|
if invalid_count:
|
|
messages.warning(request, _('%(count)s Auswahl(en) konnten nicht verarbeitet werden.') % {'count': invalid_count})
|
|
if not deleted_count and not invalid_count:
|
|
messages.info(request, _('Keine passenden Einträge gefunden.'))
|
|
return redirect('requests_dashboard')
|
|
|
|
search_query = request.GET.get('q', '').strip()
|
|
type_filter = (request.GET.get('type') or '').strip().lower()
|
|
status_filter = (request.GET.get('status') or '').strip().lower()
|
|
department_filter = (request.GET.get('department') or '').strip()
|
|
date_from = (request.GET.get('date_from') or '').strip()
|
|
date_to = (request.GET.get('date_to') or '').strip()
|
|
|
|
onboarding_qs = OnboardingRequest.objects.order_by('-created_at')
|
|
offboarding_qs = OffboardingRequest.objects.order_by('-created_at')
|
|
all_onboarding = OnboardingRequest.objects.all()
|
|
all_offboarding = OffboardingRequest.objects.all()
|
|
|
|
if search_query:
|
|
onboarding_qs = onboarding_qs.filter(Q(full_name__icontains=search_query) | Q(work_email__icontains=search_query))
|
|
offboarding_qs = offboarding_qs.filter(Q(full_name__icontains=search_query) | Q(work_email__icontains=search_query))
|
|
if status_filter in {'submitted', 'processing', 'completed', 'failed'}:
|
|
onboarding_qs = onboarding_qs.filter(processing_status=status_filter)
|
|
offboarding_qs = offboarding_qs.filter(processing_status=status_filter)
|
|
if department_filter:
|
|
onboarding_qs = onboarding_qs.filter(department=department_filter)
|
|
offboarding_qs = offboarding_qs.filter(department=department_filter)
|
|
if date_from:
|
|
onboarding_qs = onboarding_qs.filter(created_at__date__gte=date_from)
|
|
offboarding_qs = offboarding_qs.filter(created_at__date__gte=date_from)
|
|
if date_to:
|
|
onboarding_qs = onboarding_qs.filter(created_at__date__lte=date_to)
|
|
offboarding_qs = offboarding_qs.filter(created_at__date__lte=date_to)
|
|
|
|
if type_filter == 'onboarding':
|
|
offboarding_qs = offboarding_qs.none()
|
|
elif type_filter == 'offboarding':
|
|
onboarding_qs = onboarding_qs.none()
|
|
|
|
onboarding_items = onboarding_qs[:50]
|
|
offboarding_items = offboarding_qs[:50]
|
|
language_code = (
|
|
request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
|
|
or getattr(request, 'LANGUAGE_CODE', '')
|
|
or get_language()
|
|
or 'de'
|
|
).split('-')[0].lower()
|
|
|
|
rows = []
|
|
for obj in onboarding_items:
|
|
intro_session = OnboardingIntroductionSession.objects.filter(onboarding_request=obj).first()
|
|
if intro_session and intro_session.exported_pdf_path:
|
|
intro_session.exported_pdf_url = f"/media/pdfs/{Path(intro_session.exported_pdf_path).name}"
|
|
rows.append(
|
|
{
|
|
'id': obj.id,
|
|
'kind': 'Onboarding',
|
|
'kind_slug': 'onboarding',
|
|
'name': obj.full_name,
|
|
'work_email': obj.work_email,
|
|
'created_at': obj.created_at,
|
|
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
|
'intro_pdf_url': f"/media/pdfs/{Path(obj.intro_pdf_path).name}" if obj.intro_pdf_path else None,
|
|
'intro_session': intro_session,
|
|
'status': request_status_label_fn(obj.processing_status, language_code),
|
|
'status_key': obj.processing_status,
|
|
'last_error': obj.last_error,
|
|
}
|
|
)
|
|
for obj in offboarding_items:
|
|
rows.append(
|
|
{
|
|
'id': obj.id,
|
|
'kind': 'Offboarding',
|
|
'kind_slug': 'offboarding',
|
|
'name': obj.full_name,
|
|
'work_email': obj.work_email,
|
|
'created_at': obj.created_at,
|
|
'pdf_url': f"/media/pdfs/{Path(obj.generated_pdf_path).name}" if obj.generated_pdf_path else None,
|
|
'intro_pdf_url': None,
|
|
'intro_session': None,
|
|
'status': request_status_label_fn(obj.processing_status, language_code),
|
|
'status_key': obj.processing_status,
|
|
'last_error': obj.last_error,
|
|
}
|
|
)
|
|
|
|
rows.sort(key=lambda x: x['created_at'], reverse=True)
|
|
|
|
today = timezone.localdate()
|
|
start_date = today - timedelta(days=13)
|
|
onboarding_daily = {}
|
|
offboarding_daily = {}
|
|
for i in range(14):
|
|
day = start_date + timedelta(days=i)
|
|
onboarding_daily[day] = 0
|
|
offboarding_daily[day] = 0
|
|
|
|
for dt in onboarding_qs.filter(created_at__date__gte=start_date).values_list('created_at', flat=True):
|
|
onboarding_daily[timezone.localtime(dt).date()] += 1
|
|
for dt in offboarding_qs.filter(created_at__date__gte=start_date).values_list('created_at', flat=True):
|
|
offboarding_daily[timezone.localtime(dt).date()] += 1
|
|
|
|
chart_points = []
|
|
max_total = 1
|
|
for i in range(14):
|
|
day = start_date + timedelta(days=i)
|
|
on_count = onboarding_daily[day]
|
|
off_count = offboarding_daily[day]
|
|
total = on_count + off_count
|
|
max_total = max(max_total, total)
|
|
chart_points.append(
|
|
{
|
|
'label': day.strftime('%d.%m'),
|
|
'onboarding': on_count,
|
|
'offboarding': off_count,
|
|
'total': total,
|
|
}
|
|
)
|
|
|
|
for point in chart_points:
|
|
point['height'] = max(8, int((point['total'] / max_total) * 84))
|
|
|
|
onboarding_total = onboarding_qs.count()
|
|
offboarding_total = offboarding_qs.count()
|
|
departments = sorted(
|
|
{
|
|
value.strip()
|
|
for value in list(all_onboarding.exclude(department='').values_list('department', flat=True))
|
|
+ list(all_offboarding.exclude(department='').values_list('department', flat=True))
|
|
if value and value.strip()
|
|
},
|
|
key=str.lower,
|
|
)
|
|
status_choices = [
|
|
{'value': 'submitted', 'label': request_status_label_fn('submitted', language_code)},
|
|
{'value': 'processing', 'label': request_status_label_fn('processing', language_code)},
|
|
{'value': 'completed', 'label': request_status_label_fn('completed', language_code)},
|
|
{'value': 'failed', 'label': request_status_label_fn('failed', language_code)},
|
|
]
|
|
has_filters = any([search_query, type_filter, status_filter, department_filter, date_from, date_to])
|
|
column_count = 4
|
|
if user_has_capability(request.user, 'delete_requests'):
|
|
column_count += 1
|
|
if user_has_capability(request.user, 'run_intro_session') or user_has_capability(request.user, 'generate_intro_pdfs'):
|
|
column_count += 1
|
|
if user_has_capability(request.user, 'access_requests_dashboard'):
|
|
column_count += 1
|
|
return render(
|
|
request,
|
|
'workflows/requests_dashboard.html',
|
|
{
|
|
'rows': rows[:60],
|
|
'search_query': search_query,
|
|
'selected_type': type_filter,
|
|
'selected_status': status_filter,
|
|
'selected_department': department_filter,
|
|
'date_from': date_from,
|
|
'date_to': date_to,
|
|
'departments': departments,
|
|
'status_choices': status_choices,
|
|
'has_filters': has_filters,
|
|
'column_count': column_count,
|
|
'onboarding_total': onboarding_total,
|
|
'offboarding_total': offboarding_total,
|
|
'combined_total': onboarding_total + offboarding_total,
|
|
'chart_points': chart_points,
|
|
},
|
|
)
|
|
|
|
def onboarding_create_impl(
|
|
request,
|
|
*,
|
|
build_onboarding_layout_fn,
|
|
build_onboarding_sections_fn,
|
|
normalized_conditional_rule_payload_fn,
|
|
display_user_name_fn,
|
|
onboarding_inline_checks,
|
|
onboarding_checkbox_lists,
|
|
):
|
|
config = WorkflowConfig.objects.order_by('id').first()
|
|
legal_text = (
|
|
config.legal_text
|
|
if config and config.legal_text
|
|
else 'Eine Ausrüstungsvereinbarung erlaubt es einem Mitarbeitenden, die Ausrüstung des Unternehmens im Außendienst oder zu Hause zu nutzen und mitzunehmen.'
|
|
)
|
|
|
|
if request.method == 'POST':
|
|
form = OnboardingRequestForm(request.POST, request.FILES, requester_email=request.user.email)
|
|
if form.is_valid():
|
|
obj = form.save()
|
|
obj.onboarded_by_name = display_user_name_fn(request.user)
|
|
obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0])
|
|
obj.save(update_fields=['onboarded_by_name', 'preferred_language'])
|
|
process_onboarding_request.delay(obj.id)
|
|
return redirect(f"/onboarding/new/?saved=1&id={obj.id}")
|
|
else:
|
|
form = OnboardingRequestForm(requester_email=request.user.email)
|
|
|
|
onboarding_blocks = build_onboarding_layout_fn(form)
|
|
field_pages = getattr(form, '_field_page_keys', {})
|
|
section_configs = ensure_form_section_configs('onboarding')
|
|
visible_section_keys = set()
|
|
for section in get_section_definitions('onboarding'):
|
|
key = section['key']
|
|
if section.get('is_custom'):
|
|
if section.get('is_active', True):
|
|
visible_section_keys.add(key)
|
|
elif key in LOCKED_SECTION_RULES.get('onboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible:
|
|
visible_section_keys.add(key)
|
|
onboarding_sections = build_onboarding_sections_fn(onboarding_blocks, field_pages, visible_section_keys=visible_section_keys)
|
|
onboarding_conditional_rules = normalized_conditional_rule_payload_fn('onboarding')
|
|
|
|
return render(
|
|
request,
|
|
'workflows/onboarding_form.html',
|
|
{
|
|
'form': form,
|
|
'onboarding_blocks': onboarding_blocks,
|
|
'onboarding_sections': onboarding_sections,
|
|
'onboarding_inline_checks': onboarding_inline_checks,
|
|
'onboarding_checkbox_lists': onboarding_checkbox_lists,
|
|
'onboarding_conditional_rules': onboarding_conditional_rules,
|
|
'legal_text': legal_text,
|
|
'saved': request.GET.get('saved') == '1',
|
|
'saved_request_id': request.GET.get('id', ''),
|
|
'portal_email_domain': get_company_email_domain(),
|
|
},
|
|
)
|
|
|
|
def onboarding_success_impl(request, request_id: int):
|
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
|
pdf_url = None
|
|
if obj.generated_pdf_path:
|
|
pdf_url = f"/media/pdfs/{Path(obj.generated_pdf_path).name}"
|
|
return render(request, 'workflows/onboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
|
|
|
def generate_onboarding_intro_pdf_impl(request, request_id: int, *, audit_fn):
|
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
|
pdf_path = _generate_onboarding_intro_pdf(obj, language_code=get_language())
|
|
obj.intro_pdf_path = str(pdf_path)
|
|
obj.save(update_fields=['intro_pdf_path'])
|
|
audit_fn(request, 'intro_pdf_generated', target_type='onboarding', target_id=obj.id, target_label=obj.full_name)
|
|
messages.success(request, _('Einweisungs- und Übergabeprotokoll wurde erzeugt.'))
|
|
return redirect('requests_dashboard')
|
|
|
|
def generate_onboarding_intro_session_pdf_impl(request, request_id: int, *, audit_fn, display_user_name_fn):
|
|
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
|
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
|
|
pdf_path = _generate_onboarding_intro_session_pdf(
|
|
session,
|
|
admin_signature_name=display_user_name_fn(request.user),
|
|
language_code=get_language(),
|
|
)
|
|
session.exported_pdf_path = str(pdf_path)
|
|
session.save(update_fields=['exported_pdf_path'])
|
|
audit_fn(request, 'intro_live_pdf_generated', target_type='onboarding', target_id=onboarding.id, target_label=onboarding.full_name)
|
|
messages.success(request, _('Einweisungsprotokoll aus Live-Status wurde erzeugt.'))
|
|
return redirect('onboarding_intro_session_page', request_id=request_id)
|
|
|
|
def onboarding_intro_session_page_impl(request, request_id: int, *, audit_fn, display_user_name_fn):
|
|
onboarding = get_object_or_404(OnboardingRequest, id=request_id)
|
|
session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding)
|
|
sections = build_intro_sections_for_request(onboarding, language_code=get_language())
|
|
|
|
if request.method == 'POST':
|
|
checked_ids = set(request.POST.getlist('checked_items'))
|
|
checklist_state = {}
|
|
for section in sections:
|
|
for item in section['items']:
|
|
checklist_state[item['id']] = item['id'] in checked_ids
|
|
|
|
action = (request.POST.get('session_action') or 'save').strip()
|
|
session.checklist_state = checklist_state
|
|
session.notes = (request.POST.get('notes') or '').strip()
|
|
if action == 'reset':
|
|
session.checklist_state = {}
|
|
session.notes = ''
|
|
session.status = 'draft'
|
|
session.completed_at = None
|
|
session.completed_by_name = ''
|
|
session.exported_pdf_path = ''
|
|
session.save(update_fields=['checklist_state', 'notes', 'status', 'completed_at', 'completed_by_name', 'exported_pdf_path'])
|
|
audit_fn(request, 'intro_session_reset', target_type='onboarding', target_id=onboarding.id, target_label=onboarding.full_name)
|
|
messages.success(request, _('Einweisung wurde zurückgesetzt.'))
|
|
return redirect('onboarding_intro_session_page', request_id=request_id)
|
|
if action == 'complete':
|
|
session.status = 'completed'
|
|
session.completed_at = timezone.now()
|
|
session.completed_by_name = display_user_name_fn(request.user)
|
|
audit_fn(
|
|
request,
|
|
'intro_session_completed',
|
|
target_type='onboarding',
|
|
target_id=onboarding.id,
|
|
target_label=onboarding.full_name,
|
|
details={'checked_count': len([value for value in checklist_state.values() if value])},
|
|
)
|
|
messages.success(request, _('Einweisung wurde als abgeschlossen gespeichert.'))
|
|
else:
|
|
session.status = 'draft'
|
|
session.completed_at = None
|
|
session.completed_by_name = ''
|
|
audit_fn(
|
|
request,
|
|
'intro_session_saved',
|
|
target_type='onboarding',
|
|
target_id=onboarding.id,
|
|
target_label=onboarding.full_name,
|
|
details={'checked_count': len([value for value in checklist_state.values() if value])},
|
|
)
|
|
messages.success(request, _('Einweisung wurde als Entwurf gespeichert.'))
|
|
session.save()
|
|
return redirect('onboarding_intro_session_page', request_id=request_id)
|
|
|
|
checked_map = session.checklist_state or {}
|
|
checked_count = 0
|
|
total_count = 0
|
|
for section in sections:
|
|
for item in section['items']:
|
|
item['checked'] = bool(checked_map.get(item['id']))
|
|
total_count += 1
|
|
if item['checked']:
|
|
checked_count += 1
|
|
|
|
salutation = (onboarding.get_gender_display() or '').strip()
|
|
display_name = f"{salutation} {onboarding.full_name}".strip() if salutation else onboarding.full_name
|
|
progress_percent = int((checked_count / total_count) * 100) if total_count else 0
|
|
|
|
return render(
|
|
request,
|
|
'workflows/onboarding_intro_session.html',
|
|
{
|
|
'onboarding': onboarding,
|
|
'session': session,
|
|
'display_name': display_name,
|
|
'sections': sections,
|
|
'checked_count': checked_count,
|
|
'total_count': total_count,
|
|
'progress_percent': progress_percent,
|
|
'session_pdf_url': f"/media/pdfs/{Path(session.exported_pdf_path).name}" if session.exported_pdf_path else None,
|
|
},
|
|
)
|
|
|
|
def offboarding_create_impl(request, *, build_offboarding_sections_fn, display_user_name_fn):
|
|
profile_id = request.GET.get('profile')
|
|
search_query = request.GET.get('q', '').strip()
|
|
selected_profile = None
|
|
|
|
if profile_id:
|
|
selected_profile = EmployeeProfile.objects.filter(id=profile_id).first()
|
|
|
|
search_results = []
|
|
if search_query:
|
|
search_results = list(
|
|
EmployeeProfile.objects.filter(full_name__icontains=search_query)[:10]
|
|
) + list(
|
|
EmployeeProfile.objects.filter(work_email__icontains=search_query)[:10]
|
|
)
|
|
# preserve order while removing duplicates
|
|
seen = set()
|
|
unique = []
|
|
for r in search_results:
|
|
if r.id not in seen:
|
|
unique.append(r)
|
|
seen.add(r.id)
|
|
search_results = unique[:10]
|
|
|
|
if request.method == 'POST':
|
|
form = OffboardingRequestForm(request.POST, prefill_profile=selected_profile)
|
|
if form.is_valid():
|
|
obj = form.save(commit=False)
|
|
if selected_profile:
|
|
obj.employee_profile = selected_profile
|
|
requester_email = (request.user.email or '').strip().lower()
|
|
company_suffix = f"@{get_company_email_domain()}"
|
|
if requester_email and requester_email.endswith(company_suffix):
|
|
obj.requested_by_email = requester_email
|
|
else:
|
|
obj.requested_by_email = settings.DEFAULT_FROM_EMAIL
|
|
obj.requested_by_name = display_user_name_fn(request.user)
|
|
obj.preferred_language = ((getattr(request, 'LANGUAGE_CODE', '') or get_language() or 'de').split('-')[0])
|
|
obj.save()
|
|
process_offboarding_request.delay(obj.id)
|
|
return redirect(f"/offboarding/new/?saved=1&id={obj.id}")
|
|
else:
|
|
form = OffboardingRequestForm(prefill_profile=selected_profile, initial={'search_query': search_query})
|
|
|
|
field_pages = getattr(form, '_field_page_keys', {})
|
|
section_configs = ensure_form_section_configs('offboarding')
|
|
visible_section_keys = {
|
|
key for key in OFFBOARDING_PAGE_ORDER
|
|
if key in LOCKED_SECTION_RULES.get('offboarding', set()) or section_configs.get(key, None) is None or section_configs[key].is_visible
|
|
}
|
|
offboarding_sections = build_offboarding_sections_fn(form, visible_section_keys=visible_section_keys)
|
|
|
|
return render(
|
|
request,
|
|
'workflows/offboarding_form.html',
|
|
{
|
|
'form': form,
|
|
'search_results': search_results,
|
|
'selected_profile': selected_profile,
|
|
'search_query': search_query,
|
|
'saved': request.GET.get('saved') == '1',
|
|
'saved_request_id': request.GET.get('id', ''),
|
|
'portal_email_domain': get_company_email_domain(),
|
|
'offboarding_sections': offboarding_sections,
|
|
},
|
|
)
|
|
|
|
def offboarding_success_impl(request, request_id: int):
|
|
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
|
pdf_url = None
|
|
if obj.generated_pdf_path:
|
|
pdf_url = f"/media/pdfs/{Path(obj.generated_pdf_path).name}"
|
|
return render(request, 'workflows/offboarding_success.html', {'obj': obj, 'pdf_url': pdf_url})
|
|
|
|
def delete_request_from_dashboard_impl(request, kind: str, request_id: int, *, audit_fn, request_target_label_fn):
|
|
if kind == 'onboarding':
|
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
|
elif kind == 'offboarding':
|
|
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
|
else:
|
|
messages.error(request, f'Unbekannter Typ: {kind}')
|
|
return redirect('requests_dashboard')
|
|
|
|
target_label = request_target_label_fn(obj, kind)
|
|
obj.delete()
|
|
audit_fn(request, 'request_deleted', target_type=kind, target_id=request_id, target_label=target_label)
|
|
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde gelöscht.')
|
|
return redirect('requests_dashboard')
|
|
|
|
def retry_request_from_dashboard_impl(request, kind: str, request_id: int, *, audit_fn, request_target_label_fn):
|
|
if kind == 'onboarding':
|
|
obj = get_object_or_404(OnboardingRequest, id=request_id)
|
|
obj.processing_status = 'submitted'
|
|
obj.last_error = ''
|
|
obj.save(update_fields=['processing_status', 'last_error'])
|
|
process_onboarding_request.delay(obj.id)
|
|
audit_fn(request, 'request_retried', target_type='onboarding', target_id=obj.id, target_label=request_target_label_fn(obj, 'onboarding'))
|
|
elif kind == 'offboarding':
|
|
obj = get_object_or_404(OffboardingRequest, id=request_id)
|
|
obj.processing_status = 'submitted'
|
|
obj.last_error = ''
|
|
obj.save(update_fields=['processing_status', 'last_error'])
|
|
process_offboarding_request.delay(obj.id)
|
|
audit_fn(request, 'request_retried', target_type='offboarding', target_id=obj.id, target_label=request_target_label_fn(obj, 'offboarding'))
|
|
else:
|
|
messages.error(request, f'Unbekannter Typ: {kind}')
|
|
return redirect('requests_dashboard')
|
|
|
|
messages.success(request, f'{kind.capitalize()}-Anfrage #{request_id} wurde erneut angestoßen.')
|
|
return redirect('requests_dashboard')
|