From 4d751d861d4c5131a24d3b7786d38e3b9ff8a79f Mon Sep 17 00:00:00 2001 From: SysAdmin Agent Date: Sat, 21 Mar 2026 22:43:01 +0000 Subject: [PATCH] =?UTF-8?q?DSGVO-Compliance:=20Einwilligung,=20Datenschutz?= =?UTF-8?q?erkl=C3=A4rung=20&=20Consent-Logging=20im=20Upload-Portal=20(ST?= =?UTF-8?q?I-89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Datenschutzerklärung unter /portal/datenschutz/ öffentlich erreichbar - Link zur Datenschutzerklärung in Nachweis-Aufforderungs-E-Mails (HTML + TXT) - Einwilligungs-Checkbox vor Upload mit Server-Side-Validierung - Consent-Logging: einwilligung_erteilt_am auf UploadToken (Art. 7 Abs. 1 DSGVO) - Regelsatz-Korrektur: 449€→563€ in Onboarding-Template (Stand 01/2024) Co-Authored-By: Claude Opus 4.6 --- ..._einwilligung_erteilt_am_to_uploadtoken.py | 18 +++++++++++++++ app/stiftung/models/destinataere.py | 4 ++++ app/stiftung/portal_urls.py | 7 ++++++ app/stiftung/tasks.py | 2 ++ app/stiftung/views/portal.py | 22 +++++++++++++++++++ .../email/nachweis_aufforderung.html | 3 ++- app/templates/email/nachweis_aufforderung.txt | 1 + .../portal/einwilligung_onboarding.html | 2 +- app/templates/portal/upload_formular.html | 15 +++++++++++++ 9 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 app/stiftung/migrations/0064_add_einwilligung_erteilt_am_to_uploadtoken.py diff --git a/app/stiftung/migrations/0064_add_einwilligung_erteilt_am_to_uploadtoken.py b/app/stiftung/migrations/0064_add_einwilligung_erteilt_am_to_uploadtoken.py new file mode 100644 index 0000000..e23217d --- /dev/null +++ b/app/stiftung/migrations/0064_add_einwilligung_erteilt_am_to_uploadtoken.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2026-03-21 22:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('stiftung', '0063_add_anrede_to_destinataer'), + ] + + operations = [ + migrations.AddField( + model_name='uploadtoken', + name='einwilligung_erteilt_am', + field=models.DateTimeField(blank=True, help_text='Zeitpunkt der DSGVO-Einwilligung beim Upload (Art. 7 Abs. 1 DSGVO)', null=True, verbose_name='Einwilligung erteilt am'), + ), + ] diff --git a/app/stiftung/models/destinataere.py b/app/stiftung/models/destinataere.py index ba716dc..00926d4 100644 --- a/app/stiftung/models/destinataere.py +++ b/app/stiftung/models/destinataere.py @@ -1362,6 +1362,10 @@ class UploadToken(models.Model): erinnerung_gesendet = models.BooleanField( default=False, verbose_name="Erinnerung gesendet" ) + einwilligung_erteilt_am = models.DateTimeField( + null=True, blank=True, verbose_name="Einwilligung erteilt am", + help_text="Zeitpunkt der DSGVO-Einwilligung beim Upload (Art. 7 Abs. 1 DSGVO)" + ) class Meta: verbose_name = "Upload-Token" diff --git a/app/stiftung/portal_urls.py b/app/stiftung/portal_urls.py index 8635555..17a2221 100644 --- a/app/stiftung/portal_urls.py +++ b/app/stiftung/portal_urls.py @@ -6,6 +6,7 @@ Diese URLs sind ohne Login zugänglich (tokenbasierte Authentifizierung). from django.urls import path from stiftung.views.portal import ( + datenschutzerklaerung, onboarding_danke, onboarding_schritt, upload_danke, @@ -15,6 +16,12 @@ from stiftung.views.portal import ( app_name = "portal" urlpatterns = [ + # Datenschutzerklärung (öffentlich, kein Token erforderlich) + path( + "datenschutz/", + datenschutzerklaerung, + name="datenschutzerklaerung", + ), # Upload-Portal (bestehende Destinatäre – Token-basiert) path( "upload//", diff --git a/app/stiftung/tasks.py b/app/stiftung/tasks.py index a7ca3bd..449dd59 100644 --- a/app/stiftung/tasks.py +++ b/app/stiftung/tasks.py @@ -547,6 +547,7 @@ def send_nachweis_aufforderung(self, destinataer_id, nachweis_id, base_url=None) "gueltig_bis": gueltig_bis, "halbjahr_label": halbjahr_label, "quartal_label": quartal_label, + "datenschutz_url": f"{base_url}/portal/datenschutz/", } subject = f"Nachweis-Aufforderung: {quartal_label} ({halbjahr_label}) – vHTV-Stiftung" @@ -618,6 +619,7 @@ def send_nachweis_erinnerung(self, token_id, base_url=None): "gueltig_bis": upload_token.gueltig_bis, "halbjahr_label": halbjahr_label, "ist_erinnerung": True, + "datenschutz_url": f"{base_url}/portal/datenschutz/", } subject = f"Erinnerung: Nachweis-Upload noch ausstehend – {halbjahr_label}" diff --git a/app/stiftung/views/portal.py b/app/stiftung/views/portal.py index 81924f4..7a68026 100644 --- a/app/stiftung/views/portal.py +++ b/app/stiftung/views/portal.py @@ -33,6 +33,11 @@ from django.views.decorators.http import require_http_methods from stiftung.models import DokumentDatei, OnboardingEinladung, UploadToken, VierteljahresNachweis + +def datenschutzerklaerung(request): + """Datenschutzerklärung für das öffentliche Portal.""" + return render(request, "portal/datenschutzerklaerung.html") + logger = logging.getLogger(__name__) # Erlaubte Dateitypen für Uploads @@ -105,6 +110,19 @@ def upload_formular(request, token): if request.method == "GET": return render(request, "portal/upload_formular.html", base_context) + # POST: Einwilligung prüfen + einwilligung = request.POST.get("einwilligung") + if not einwilligung: + ctx = { + **base_context, + "einwilligung_fehler": "Bitte erteilen Sie Ihre Einwilligung zur Datenverarbeitung, um fortzufahren.", + } + for kat in [ + "studiennachweis", "einkommenssituation", "vermogenssituation", "weitere_dokumente" + ]: + ctx[f"{kat}_text"] = request.POST.get(f"{kat}_text", "") + return render(request, "portal/upload_formular.html", ctx) + # POST: Kategorisierte Dateien und Texte verarbeiten # Kategorien mit ihren DMS-Kontext-Werten und FK-Feldern auf VierteljahresNachweis KATEGORIEN = [ @@ -228,6 +246,10 @@ def upload_formular(request, token): if nachweis_update_fields: nachweis.save(update_fields=list(set(nachweis_update_fields))) + # DSGVO-Einwilligung protokollieren (Art. 7 Abs. 1 DSGVO) + upload_token.einwilligung_erteilt_am = timezone.now() + upload_token.save(update_fields=["einwilligung_erteilt_am"]) + # Token einlösen ip = _get_client_ip(request) upload_token.einloesen(ip_address=ip) diff --git a/app/templates/email/nachweis_aufforderung.html b/app/templates/email/nachweis_aufforderung.html index 561c188..9791617 100644 --- a/app/templates/email/nachweis_aufforderung.html +++ b/app/templates/email/nachweis_aufforderung.html @@ -71,7 +71,8 @@ diff --git a/app/templates/email/nachweis_aufforderung.txt b/app/templates/email/nachweis_aufforderung.txt index 68ec341..634bfac 100644 --- a/app/templates/email/nachweis_aufforderung.txt +++ b/app/templates/email/nachweis_aufforderung.txt @@ -35,3 +35,4 @@ Tel. 02858/836780 --- Diese E-Mail wurde automatisch erzeugt. Bitte antworten Sie nicht direkt auf diese E-Mail. +Datenschutzerklärung: {{ datenschutz_url }} diff --git a/app/templates/portal/einwilligung_onboarding.html b/app/templates/portal/einwilligung_onboarding.html index cddc496..9119b50 100644 --- a/app/templates/portal/einwilligung_onboarding.html +++ b/app/templates/portal/einwilligung_onboarding.html @@ -108,7 +108,7 @@
Aktuelle Grenzwerte gemäß § 53 Nr. 2 AO (Stand 01/2024): - Bezüge max. 2.245 € monatlich (5× Regelsatz 449 €); Vermögen max. 15.500 €. + Bezüge max. 2.815 € monatlich (5× Regelsatz 563 €); Vermögen max. 15.500 €. Bei Haushaltsangehörigen erhöhen sich die Grenzen entsprechend. Maßgeblich sind die jeweils gültigen Werte zum Zeitpunkt der Prüfung.
diff --git a/app/templates/portal/upload_formular.html b/app/templates/portal/upload_formular.html index c56cbe4..c84ca22 100644 --- a/app/templates/portal/upload_formular.html +++ b/app/templates/portal/upload_formular.html @@ -41,6 +41,10 @@ .deadline { font-size: 13px; color: #888; margin-top: 16px; text-align: center; } .footer { text-align: center; margin-top: 24px; font-size: 12px; color: #aaa; } .pflicht-hinweis { font-size: 12px; color: #888; margin-bottom: 16px; } + .einwilligung-box { background: #f0f6ff; border: 1px solid #b0cce8; border-radius: 6px; padding: 14px 16px; margin: 20px 0; font-size: 14px; } + .einwilligung-box label { font-weight: normal; display: flex; align-items: flex-start; gap: 10px; cursor: pointer; } + .einwilligung-box input[type="checkbox"] { margin-top: 2px; flex-shrink: 0; width: 16px; height: 16px; cursor: pointer; } + .einwilligung-box .einwilligung-fehler { color: #c00; font-size: 13px; margin-top: 6px; } @@ -128,6 +132,17 @@ +
+ + {% if einwilligung_fehler %} +

{{ einwilligung_fehler }}

+ {% endif %} +
+