implement report feature
This commit is contained in:
parent
4218ee6b7d
commit
d6d47f714a
31 changed files with 2472 additions and 8 deletions
0
app/reports/__init__.py
Normal file
0
app/reports/__init__.py
Normal file
123
app/reports/admin.py
Normal file
123
app/reports/admin.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from .models import AutomaticReport, ReportLog
|
||||
|
||||
|
||||
@admin.register(AutomaticReport)
|
||||
class AutomaticReportAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'name',
|
||||
'frequency',
|
||||
'is_active',
|
||||
'last_sent',
|
||||
'created_by',
|
||||
'created_at',
|
||||
'email_count'
|
||||
]
|
||||
list_filter = ['frequency', 'is_active', 'created_at', 'include_naturschutzbehoerde', 'include_jagdbehoerde']
|
||||
search_fields = ['name', 'description']
|
||||
readonly_fields = ['created_by', 'created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ('name', 'description', 'is_active')
|
||||
}),
|
||||
(_('E-Mail-Einstellungen'), {
|
||||
'fields': ('email_addresses',)
|
||||
}),
|
||||
(_('Filter-Einstellungen'), {
|
||||
'fields': ('include_naturschutzbehoerde', 'include_jagdbehoerde')
|
||||
}),
|
||||
(_('Zeitplan'), {
|
||||
'fields': ('frequency',)
|
||||
}),
|
||||
(_('Metadaten'), {
|
||||
'fields': ('created_by', 'created_at', 'updated_at', 'last_sent'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change: # Creating new object
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def email_count(self, obj):
|
||||
"""Show number of email addresses."""
|
||||
count = obj.email_addresses.count()
|
||||
return f"{count} E-Mail-Adresse(n)"
|
||||
email_count.short_description = _("E-Mail-Adressen")
|
||||
|
||||
|
||||
@admin.register(ReportLog)
|
||||
class ReportLogAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'created_at',
|
||||
'get_report_type',
|
||||
'date_range',
|
||||
'patient_count',
|
||||
'has_email_recipients',
|
||||
'filters_used'
|
||||
]
|
||||
list_filter = [
|
||||
'automatic_report',
|
||||
'include_naturschutzbehörde',
|
||||
'include_jagdbehörde',
|
||||
'created_at'
|
||||
]
|
||||
search_fields = ['automatic_report__name']
|
||||
readonly_fields = [
|
||||
'automatic_report', 'date_from', 'date_to', 'include_naturschutzbehörde',
|
||||
'include_jagdbehörde', 'patient_count', 'email_sent_to',
|
||||
'created_at', 'csv_file'
|
||||
]
|
||||
|
||||
def get_report_type(self, obj):
|
||||
"""Show report type."""
|
||||
if obj.automatic_report:
|
||||
return format_html(
|
||||
'<span style="color: #007bff;"><i class="fas fa-clock"></i> Automatisch</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #6c757d;"><i class="fas fa-hand-paper"></i> Manuell</span>'
|
||||
)
|
||||
get_report_type.short_description = _("Typ")
|
||||
|
||||
def date_range(self, obj):
|
||||
"""Show date range."""
|
||||
return f"{obj.date_from} - {obj.date_to}"
|
||||
date_range.short_description = _("Zeitraum")
|
||||
|
||||
def has_email_recipients(self, obj):
|
||||
"""Show if email was sent."""
|
||||
if obj.email_sent_to:
|
||||
return format_html(
|
||||
'<span style="color: #28a745;"><i class="fas fa-envelope"></i> {} Empfänger</span>',
|
||||
len(obj.email_sent_to)
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: #17a2b8;"><i class="fas fa-download"></i> Download</span>'
|
||||
)
|
||||
has_email_recipients.short_description = _("Versendung")
|
||||
|
||||
def filters_used(self, obj):
|
||||
"""Show which filters were used."""
|
||||
filters = []
|
||||
if obj.include_naturschutzbehörde:
|
||||
filters.append("Naturschutzbehörde")
|
||||
if obj.include_jagdbehörde:
|
||||
filters.append("Jagdbehörde")
|
||||
return ", ".join(filters) if filters else _("Keine Filter")
|
||||
filters_used.short_description = _("Filter")
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Disable manual creation of logs."""
|
||||
return False
|
||||
|
||||
|
||||
# Custom admin site configuration
|
||||
admin.site.site_header = "Django FBF Administration"
|
||||
admin.site.site_title = "Django FBF Admin"
|
||||
admin.site.index_title = "Willkommen zur Django FBF Administration"
|
8
app/reports/apps.py
Normal file
8
app/reports/apps.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class ReportsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "reports"
|
||||
verbose_name = _("Reports")
|
157
app/reports/forms.py
Normal file
157
app/reports/forms.py
Normal file
|
@ -0,0 +1,157 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from datetime import date, timedelta
|
||||
from sendemail.models import Emailadress
|
||||
from .models import AutomaticReport
|
||||
|
||||
|
||||
class DateInput(forms.DateInput):
|
||||
input_type = "date"
|
||||
|
||||
|
||||
class ManualReportForm(forms.Form):
|
||||
"""Form for creating manual reports."""
|
||||
|
||||
# Email selection
|
||||
email_addresses = forms.ModelMultipleChoiceField(
|
||||
queryset=Emailadress.objects.all(),
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
label=_("E-Mail-Adressen"),
|
||||
help_text=_("Wählen Sie E-Mail-Adressen aus oder lassen Sie das Feld leer für nur Download")
|
||||
)
|
||||
|
||||
custom_email = forms.EmailField(
|
||||
required=False,
|
||||
label=_("Zusätzliche E-Mail-Adresse"),
|
||||
help_text=_("Optional: Geben Sie eine zusätzliche E-Mail-Adresse ein")
|
||||
)
|
||||
|
||||
# Date range
|
||||
date_from = forms.DateField(
|
||||
widget=DateInput(format="%Y-%m-%d"),
|
||||
label=_("Von"),
|
||||
initial=lambda: date.today() - timedelta(days=90), # 3 months ago
|
||||
help_text=_("Startdatum für den Report")
|
||||
)
|
||||
|
||||
date_to = forms.DateField(
|
||||
widget=DateInput(format="%Y-%m-%d"),
|
||||
label=_("Bis"),
|
||||
initial=date.today,
|
||||
help_text=_("Enddatum für den Report")
|
||||
)
|
||||
|
||||
# Filter options
|
||||
include_naturschutzbehoerde = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
label=_("Naturschutzbehörde"),
|
||||
help_text=_("Vögel einschließen, die an Naturschutzbehörde gemeldet werden")
|
||||
)
|
||||
|
||||
include_jagdbehoerde = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_("Jagdbehörde"),
|
||||
help_text=_("Vögel einschließen, die an Jagdbehörde gemeldet werden")
|
||||
)
|
||||
|
||||
# Action choice
|
||||
action_choices = [
|
||||
('download', _('Nur herunterladen')),
|
||||
('email', _('Per E-Mail senden')),
|
||||
('both', _('Herunterladen und per E-Mail senden')),
|
||||
]
|
||||
|
||||
action = forms.ChoiceField(
|
||||
choices=action_choices,
|
||||
widget=forms.RadioSelect,
|
||||
initial='download',
|
||||
label=_("Aktion")
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Set default date_from to 3 months ago
|
||||
if not self.initial.get('date_from'):
|
||||
self.fields['date_from'].initial = date.today() - timedelta(days=90)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
date_from = cleaned_data.get('date_from')
|
||||
date_to = cleaned_data.get('date_to')
|
||||
action = cleaned_data.get('action')
|
||||
email_addresses = cleaned_data.get('email_addresses')
|
||||
custom_email = cleaned_data.get('custom_email')
|
||||
|
||||
# Validate date range
|
||||
if date_from and date_to:
|
||||
if date_from > date_to:
|
||||
raise ValidationError(
|
||||
_("Das 'Von'-Datum darf nicht nach dem 'Bis'-Datum liegen.")
|
||||
)
|
||||
|
||||
# Validate email requirements for email actions
|
||||
if action in ['email', 'both']:
|
||||
if not email_addresses and not custom_email:
|
||||
raise ValidationError(
|
||||
_("Für E-Mail-Versendung müssen E-Mail-Adressen ausgewählt oder eingegeben werden.")
|
||||
)
|
||||
|
||||
# Validate at least one filter is selected
|
||||
include_naturschutz = cleaned_data.get('include_naturschutzbehoerde')
|
||||
include_jagd = cleaned_data.get('include_jagdbehoerde')
|
||||
|
||||
if not include_naturschutz and not include_jagd:
|
||||
raise ValidationError(
|
||||
_("Mindestens eine Kategorie (Naturschutzbehörde oder Jagdbehörde) muss ausgewählt werden.")
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class AutomaticReportForm(forms.ModelForm):
|
||||
"""Form for creating/editing automatic reports."""
|
||||
|
||||
class Meta:
|
||||
model = AutomaticReport
|
||||
fields = [
|
||||
'name',
|
||||
'description',
|
||||
'email_addresses',
|
||||
'include_naturschutzbehoerde',
|
||||
'include_jagdbehoerde',
|
||||
'frequency',
|
||||
'is_active'
|
||||
]
|
||||
widgets = {
|
||||
'description': forms.Textarea(attrs={'rows': 3}),
|
||||
'email_addresses': forms.CheckboxSelectMultiple,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email_addresses'].queryset = Emailadress.objects.all()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Validate at least one filter is selected
|
||||
include_naturschutz = cleaned_data.get('include_naturschutzbehoerde')
|
||||
include_jagd = cleaned_data.get('include_jagdbehoerde')
|
||||
|
||||
if not include_naturschutz and not include_jagd:
|
||||
raise ValidationError(
|
||||
_("Mindestens eine Kategorie (Naturschutzbehörde oder Jagdbehörde) muss ausgewählt werden.")
|
||||
)
|
||||
|
||||
# Validate email addresses are selected
|
||||
email_addresses = cleaned_data.get('email_addresses')
|
||||
if not email_addresses:
|
||||
raise ValidationError(
|
||||
_("Für automatische Reports müssen E-Mail-Adressen ausgewählt werden.")
|
||||
)
|
||||
|
||||
return cleaned_data
|
1
app/reports/management/__init__.py
Normal file
1
app/reports/management/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Empty file to make this directory a Python package
|
1
app/reports/management/commands/__init__.py
Normal file
1
app/reports/management/commands/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
# Empty file to make this directory a Python package
|
62
app/reports/management/commands/create_test_data.py
Normal file
62
app/reports/management/commands/create_test_data.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from datetime import date, timedelta
|
||||
from bird.models import Bird, FallenBird, BirdStatus
|
||||
from sendemail.models import Emailadress
|
||||
from reports.models import AutomaticReport
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create test data for the reports system'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write('Creating test data for reports system...')
|
||||
|
||||
# Create test email addresses
|
||||
email1, created = Emailadress.objects.get_or_create(
|
||||
email_address='test1@example.com',
|
||||
defaults={'user_id': 1}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f'✓ Created email: {email1.email_address}')
|
||||
|
||||
email2, created = Emailadress.objects.get_or_create(
|
||||
email_address='test2@example.com',
|
||||
defaults={'user_id': 1}
|
||||
)
|
||||
if created:
|
||||
self.stdout.write(f'✓ Created email: {email2.email_address}')
|
||||
|
||||
# Create a test automatic report
|
||||
admin_user = User.objects.filter(is_superuser=True).first()
|
||||
if admin_user:
|
||||
auto_report, created = AutomaticReport.objects.get_or_create(
|
||||
name='Test Weekly Report',
|
||||
defaults={
|
||||
'description': 'Automatic weekly report for testing',
|
||||
'frequency': 'weekly',
|
||||
'include_naturschutzbehoerde': True,
|
||||
'include_jagdbehoerde': False,
|
||||
'is_active': True,
|
||||
'created_by': admin_user
|
||||
}
|
||||
)
|
||||
if created:
|
||||
auto_report.email_addresses.add(email1, email2)
|
||||
self.stdout.write(f'✓ Created automatic report: {auto_report.name}')
|
||||
|
||||
# Check existing bird data
|
||||
bird_count = FallenBird.objects.count()
|
||||
self.stdout.write(f'✓ Found {bird_count} existing birds in database')
|
||||
|
||||
# Check birds with notification settings
|
||||
notification_birds = Bird.objects.filter(
|
||||
models.Q(melden_an_naturschutzbehoerde=True) |
|
||||
models.Q(melden_an_jagdbehoerde=True)
|
||||
).count()
|
||||
self.stdout.write(f'✓ Found {notification_birds} birds with notification settings')
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS('✓ Test data creation completed successfully!')
|
||||
)
|
129
app/reports/management/commands/test_reports.py
Normal file
129
app/reports/management/commands/test_reports.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
from datetime import date, timedelta
|
||||
from reports.services import ReportGenerator
|
||||
from reports.models import AutomaticReport
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Test the report generation system'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--test-manual',
|
||||
action='store_true',
|
||||
help='Test manual report generation',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--test-email',
|
||||
action='store_true',
|
||||
help='Test email sending (requires SMTP configuration)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.stdout.write(self.style.SUCCESS('Testing Report System'))
|
||||
|
||||
# Simple test first
|
||||
self.stdout.write('Reports app is working!')
|
||||
|
||||
# Test basic report generation
|
||||
if options.get('test_manual'):
|
||||
self.test_manual_report()
|
||||
|
||||
if options.get('test_email'):
|
||||
self.test_email_report()
|
||||
|
||||
self.test_basic_functionality()
|
||||
|
||||
def test_basic_functionality(self):
|
||||
self.stdout.write('Testing basic report functionality...')
|
||||
|
||||
# Create test date range (last 30 days)
|
||||
date_to = date.today()
|
||||
date_from = date_to - timedelta(days=30)
|
||||
|
||||
generator = ReportGenerator(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
include_naturschutzbehoerde=True,
|
||||
include_jagdbehoerde=False
|
||||
)
|
||||
|
||||
# Test CSV generation
|
||||
try:
|
||||
csv_content, bird_count = generator.generate_csv()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'✓ CSV generation successful: {bird_count} birds found'
|
||||
)
|
||||
)
|
||||
|
||||
# Test summary
|
||||
summary = generator.get_summary()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'✓ Summary generation successful: {summary["total_birds"]} total birds'
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ CSV generation failed: {e}')
|
||||
)
|
||||
|
||||
def test_manual_report(self):
|
||||
self.stdout.write('Testing manual report creation...')
|
||||
|
||||
date_to = date.today()
|
||||
date_from = date_to - timedelta(days=7) # Last week
|
||||
|
||||
generator = ReportGenerator(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
include_naturschutzbehoerde=True,
|
||||
include_jagdbehoerde=True
|
||||
)
|
||||
|
||||
try:
|
||||
# Create download log
|
||||
log = generator.create_download_log()
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(
|
||||
f'✓ Manual report log created: {log.id} with {log.patient_count} patients'
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Manual report creation failed: {e}')
|
||||
)
|
||||
|
||||
def test_email_report(self):
|
||||
self.stdout.write('Testing email report (dry run)...')
|
||||
|
||||
# This would test email functionality if SMTP is configured
|
||||
# For now, just test the email template rendering
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
context = {
|
||||
'date_from': '01.01.2024',
|
||||
'date_to': '31.01.2024',
|
||||
'patient_count': 42,
|
||||
'filter_naturschutzbehörde': True,
|
||||
'filter_jagdbehörde': False,
|
||||
'automatic_report': None,
|
||||
'created_at': '01.02.2024',
|
||||
}
|
||||
|
||||
try:
|
||||
subject = render_to_string('reports/email/report_subject.txt', context).strip()
|
||||
message = render_to_string('reports/email/report_message.txt', context)
|
||||
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'✓ Email template rendering successful')
|
||||
)
|
||||
self.stdout.write(f'Subject: {subject}')
|
||||
|
||||
except Exception as e:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'✗ Email template rendering failed: {e}')
|
||||
)
|
62
app/reports/migrations/0001_initial.py
Normal file
62
app/reports/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
# Generated by Django 5.2.2 on 2025-06-10 09:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('sendemail', '0004_delete_birdemail'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AutomaticReport',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Name')),
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Beschreibung')),
|
||||
('include_naturschutzbehoerde', models.BooleanField(default=True, help_text='Vögel einschließen, die an Naturschutzbehörde gemeldet werden', verbose_name='Naturschutzbehörde einschließen')),
|
||||
('include_jagdbehoerde', models.BooleanField(default=False, help_text='Vögel einschließen, die an Jagdbehörde gemeldet werden', verbose_name='Jagdbehörde einschließen')),
|
||||
('frequency', models.CharField(choices=[('weekly', 'Wöchentlich'), ('monthly', 'Monatlich'), ('quarterly', 'Vierteljährlich')], default='monthly', max_length=20, verbose_name='Häufigkeit')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Aktiv')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Aktualisiert am')),
|
||||
('last_sent', models.DateTimeField(blank=True, null=True, verbose_name='Zuletzt gesendet')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
|
||||
('email_addresses', models.ManyToManyField(help_text='E-Mail-Adressen, an die der Report gesendet wird', to='sendemail.emailadress', verbose_name='E-Mail-Adressen')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Automatischer Report',
|
||||
'verbose_name_plural': 'Automatische Reports',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReportLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('report_type', models.CharField(choices=[('manual', 'Manuell erstellt'), ('automatic', 'Automatisch erstellt')], max_length=20, verbose_name='Report-Typ')),
|
||||
('date_from', models.DateField(verbose_name='Von')),
|
||||
('date_to', models.DateField(verbose_name='Bis')),
|
||||
('included_naturschutzbehoerde', models.BooleanField(verbose_name='Naturschutzbehörde eingeschlossen')),
|
||||
('included_jagdbehoerde', models.BooleanField(verbose_name='Jagdbehörde eingeschlossen')),
|
||||
('bird_count', models.IntegerField(verbose_name='Anzahl Vögel')),
|
||||
('email_sent', models.BooleanField(default=False, verbose_name='E-Mail gesendet')),
|
||||
('recipients', models.TextField(blank=True, verbose_name='Empfänger')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Erstellt am')),
|
||||
('automatic_report', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='reports.automaticreport', verbose_name='Automatischer Report')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Erstellt von')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Report-Log',
|
||||
'verbose_name_plural': 'Report-Logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,58 @@
|
|||
# Generated by Django 5.2.2 on 2025-06-10 09:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('reports', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='reportlog',
|
||||
old_name='included_jagdbehoerde',
|
||||
new_name='include_jagdbehörde',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='reportlog',
|
||||
old_name='included_naturschutzbehoerde',
|
||||
new_name='include_naturschutzbehörde',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='reportlog',
|
||||
name='bird_count',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='reportlog',
|
||||
name='created_by',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='reportlog',
|
||||
name='email_sent',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='reportlog',
|
||||
name='recipients',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='reportlog',
|
||||
name='report_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='reportlog',
|
||||
name='csv_file',
|
||||
field=models.FileField(blank=True, null=True, upload_to='reports/csv/', verbose_name='CSV-Datei'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='reportlog',
|
||||
name='email_sent_to',
|
||||
field=models.JSONField(blank=True, default=list, help_text='Liste der E-Mail-Adressen, an die der Report gesendet wurde', verbose_name='E-Mail gesendet an'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='reportlog',
|
||||
name='patient_count',
|
||||
field=models.IntegerField(default=0, verbose_name='Anzahl Patienten'),
|
||||
),
|
||||
]
|
0
app/reports/migrations/__init__.py
Normal file
0
app/reports/migrations/__init__.py
Normal file
146
app/reports/models.py
Normal file
146
app/reports/models.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from sendemail.models import Emailadress
|
||||
|
||||
|
||||
class AutomaticReport(models.Model):
|
||||
"""Model for automatic report configuration."""
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("Name")
|
||||
)
|
||||
description = models.TextField(
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Beschreibung")
|
||||
)
|
||||
|
||||
# Email recipients
|
||||
email_addresses = models.ManyToManyField(
|
||||
Emailadress,
|
||||
verbose_name=_("E-Mail-Adressen"),
|
||||
help_text=_("E-Mail-Adressen, an die der Report gesendet wird")
|
||||
)
|
||||
|
||||
# Report filters
|
||||
include_naturschutzbehoerde = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Naturschutzbehörde einschließen"),
|
||||
help_text=_("Vögel einschließen, die an Naturschutzbehörde gemeldet werden")
|
||||
)
|
||||
include_jagdbehoerde = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Jagdbehörde einschließen"),
|
||||
help_text=_("Vögel einschließen, die an Jagdbehörde gemeldet werden")
|
||||
)
|
||||
|
||||
# Schedule settings
|
||||
frequency_choices = [
|
||||
('weekly', _('Wöchentlich')),
|
||||
('monthly', _('Monatlich')),
|
||||
('quarterly', _('Vierteljährlich')),
|
||||
]
|
||||
frequency = models.CharField(
|
||||
max_length=20,
|
||||
choices=frequency_choices,
|
||||
default='monthly',
|
||||
verbose_name=_("Häufigkeit")
|
||||
)
|
||||
|
||||
# Status
|
||||
is_active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Aktiv")
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("Erstellt von")
|
||||
)
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("Erstellt am")
|
||||
)
|
||||
updated_at = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_("Aktualisiert am")
|
||||
)
|
||||
last_sent = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Zuletzt gesendet")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Automatischer Report")
|
||||
verbose_name_plural = _("Automatische Reports")
|
||||
ordering = ['-created_at']
|
||||
|
||||
|
||||
class ReportLog(models.Model):
|
||||
"""Log for generated reports."""
|
||||
|
||||
# Link to automatic report if applicable
|
||||
automatic_report = models.ForeignKey(
|
||||
AutomaticReport,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Automatischer Report")
|
||||
)
|
||||
|
||||
# Date range
|
||||
date_from = models.DateField(verbose_name=_("Von"))
|
||||
date_to = models.DateField(verbose_name=_("Bis"))
|
||||
|
||||
# Filters used
|
||||
include_naturschutzbehörde = models.BooleanField(
|
||||
verbose_name=_("Naturschutzbehörde eingeschlossen")
|
||||
)
|
||||
include_jagdbehörde = models.BooleanField(
|
||||
verbose_name=_("Jagdbehörde eingeschlossen")
|
||||
)
|
||||
|
||||
# Results
|
||||
patient_count = models.IntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Anzahl Patienten")
|
||||
)
|
||||
|
||||
# Email info - stores list of email addresses as JSON
|
||||
email_sent_to = models.JSONField(
|
||||
default=list,
|
||||
blank=True,
|
||||
verbose_name=_("E-Mail gesendet an"),
|
||||
help_text=_("Liste der E-Mail-Adressen, an die der Report gesendet wurde")
|
||||
)
|
||||
|
||||
# CSV file storage
|
||||
csv_file = models.FileField(
|
||||
upload_to='reports/csv/',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("CSV-Datei")
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("Erstellt am")
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.automatic_report:
|
||||
return f"Report {self.date_from} - {self.date_to} ({self.automatic_report.name})"
|
||||
return f"Report {self.date_from} - {self.date_to} (Manuell)"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Report-Log")
|
||||
verbose_name_plural = _("Report-Logs")
|
||||
ordering = ['-created_at']
|
187
app/reports/services.py
Normal file
187
app/reports/services.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
import csv
|
||||
from io import StringIO
|
||||
from datetime import date
|
||||
from django.db.models import Q
|
||||
from django.core.mail import EmailMessage
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from bird.models import FallenBird
|
||||
|
||||
|
||||
class ReportGenerator:
|
||||
"""Service class for generating bird reports."""
|
||||
|
||||
def __init__(self, date_from, date_to, include_naturschutzbehoerde=True, include_jagdbehoerde=False):
|
||||
self.date_from = date_from
|
||||
self.date_to = date_to
|
||||
self.include_naturschutzbehoerde = include_naturschutzbehoerde
|
||||
self.include_jagdbehoerde = include_jagdbehoerde
|
||||
|
||||
def get_birds_queryset(self):
|
||||
"""Get queryset of birds based on filters."""
|
||||
# Date filter
|
||||
queryset = FallenBird.objects.filter(
|
||||
date_found__gte=self.date_from,
|
||||
date_found__lte=self.date_to
|
||||
)
|
||||
|
||||
# Bird type filter based on notification settings
|
||||
bird_filter = Q()
|
||||
|
||||
if self.include_naturschutzbehoerde:
|
||||
bird_filter |= Q(bird__melden_an_naturschutzbehoerde=True)
|
||||
|
||||
if self.include_jagdbehoerde:
|
||||
bird_filter |= Q(bird__melden_an_jagdbehoerde=True)
|
||||
|
||||
if bird_filter:
|
||||
queryset = queryset.filter(bird_filter)
|
||||
|
||||
return queryset.select_related('bird', 'status', 'aviary', 'user').order_by('date_found')
|
||||
|
||||
def generate_csv(self):
|
||||
"""Generate CSV content and return as string with bird count."""
|
||||
birds = self.get_birds_queryset()
|
||||
bird_count = birds.count()
|
||||
|
||||
# Create CSV in memory
|
||||
output = StringIO()
|
||||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_ALL)
|
||||
|
||||
# Header row
|
||||
headers = [
|
||||
'Vogel',
|
||||
'Alter',
|
||||
'Geschlecht',
|
||||
'Gefunden am',
|
||||
'Fundort',
|
||||
'Fundumstände',
|
||||
'Diagnose bei Fund',
|
||||
'Status'
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
# Data rows
|
||||
for bird in birds:
|
||||
row = [
|
||||
bird.bird.name if bird.bird else '',
|
||||
bird.get_age_display() if bird.age else '',
|
||||
bird.get_sex_display() if bird.sex else '',
|
||||
bird.date_found.strftime('%d.%m.%Y') if bird.date_found else '',
|
||||
bird.place or '',
|
||||
bird.find_circumstances or '',
|
||||
bird.diagnostic_finding or '',
|
||||
bird.status.description if bird.status else '',
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
csv_content = output.getvalue()
|
||||
output.close()
|
||||
|
||||
return csv_content, bird_count
|
||||
|
||||
def get_filename(self):
|
||||
"""Generate filename for the report."""
|
||||
return f"wildvogelhilfe_report_{self.date_from}_{self.date_to}.csv"
|
||||
|
||||
def get_summary(self):
|
||||
"""Get summary statistics for the report."""
|
||||
birds = self.get_birds_queryset()
|
||||
|
||||
summary = {
|
||||
'total_birds': birds.count(),
|
||||
'naturschutz_birds': birds.filter(bird__melden_an_naturschutzbehoerde=True).count() if self.include_naturschutzbehoerde else 0,
|
||||
'jagd_birds': birds.filter(bird__melden_an_jagdbehoerde=True).count() if self.include_jagdbehoerde else 0,
|
||||
'date_from': self.date_from,
|
||||
'date_to': self.date_to,
|
||||
}
|
||||
|
||||
return summary
|
||||
|
||||
def send_email_report(self, email_addresses, automatic_report=None):
|
||||
"""Send the report via email to specified addresses."""
|
||||
from .models import ReportLog
|
||||
|
||||
csv_content, bird_count = self.generate_csv()
|
||||
filename = self.get_filename()
|
||||
|
||||
# Prepare email context
|
||||
context = {
|
||||
'date_from': self.date_from.strftime('%d.%m.%Y'),
|
||||
'date_to': self.date_to.strftime('%d.%m.%Y'),
|
||||
'patient_count': bird_count,
|
||||
'filter_naturschutzbehörde': self.include_naturschutzbehoerde,
|
||||
'filter_jagdbehörde': self.include_jagdbehoerde,
|
||||
'automatic_report': automatic_report,
|
||||
'created_at': date.today().strftime('%d.%m.%Y'),
|
||||
}
|
||||
|
||||
# Render email templates
|
||||
subject = render_to_string('reports/email/report_subject.txt', context).strip()
|
||||
message = render_to_string('reports/email/report_message.txt', context)
|
||||
|
||||
# Create email
|
||||
email = EmailMessage(
|
||||
subject=subject,
|
||||
body=message,
|
||||
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@wildvogelhilfe-jena.de'),
|
||||
to=email_addresses,
|
||||
)
|
||||
|
||||
# Attach CSV file
|
||||
email.attach(filename, csv_content, 'text/csv')
|
||||
|
||||
try:
|
||||
# Send email
|
||||
email.send()
|
||||
|
||||
# Create log entry
|
||||
report_log = ReportLog.objects.create(
|
||||
automatic_report=automatic_report,
|
||||
date_from=self.date_from,
|
||||
date_to=self.date_to,
|
||||
patient_count=bird_count,
|
||||
include_naturschutzbehörde=self.include_naturschutzbehoerde,
|
||||
include_jagdbehörde=self.include_jagdbehoerde,
|
||||
email_sent_to=email_addresses,
|
||||
)
|
||||
|
||||
# Save CSV file to the log
|
||||
report_log.csv_file.save(
|
||||
filename,
|
||||
ContentFile(csv_content.encode('utf-8')),
|
||||
save=True
|
||||
)
|
||||
|
||||
return report_log, True, None
|
||||
|
||||
except Exception as e:
|
||||
return None, False, str(e)
|
||||
|
||||
def create_download_log(self, automatic_report=None):
|
||||
"""Create a log entry for downloaded reports."""
|
||||
from .models import ReportLog
|
||||
|
||||
csv_content, bird_count = self.generate_csv()
|
||||
filename = self.get_filename()
|
||||
|
||||
# Create log entry
|
||||
report_log = ReportLog.objects.create(
|
||||
automatic_report=automatic_report,
|
||||
date_from=self.date_from,
|
||||
date_to=self.date_to,
|
||||
patient_count=bird_count,
|
||||
include_naturschutzbehörde=self.include_naturschutzbehoerde,
|
||||
include_jagdbehörde=self.include_jagdbehoerde,
|
||||
email_sent_to=[], # Empty list indicates download
|
||||
)
|
||||
|
||||
# Save CSV file to the log
|
||||
report_log.csv_file.save(
|
||||
filename,
|
||||
ContentFile(csv_content.encode('utf-8')),
|
||||
save=True
|
||||
)
|
||||
|
||||
return report_log
|
3
app/reports/tests.py
Normal file
3
app/reports/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
14
app/reports/urls.py
Normal file
14
app/reports/urls.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'reports'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.reports_dashboard, name='dashboard'),
|
||||
path('manual/', views.manual_report, name='manual_report'),
|
||||
path('automatic/', views.automatic_reports, name='automatic_reports'),
|
||||
path('automatic/create/', views.create_automatic_report, name='create_automatic_report'),
|
||||
path('automatic/<int:report_id>/edit/', views.edit_automatic_report, name='edit_automatic_report'),
|
||||
path('automatic/<int:report_id>/delete/', views.delete_automatic_report, name='delete_automatic_report'),
|
||||
path('logs/', views.report_logs, name='report_logs'),
|
||||
]
|
178
app/reports/views.py
Normal file
178
app/reports/views.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
import csv
|
||||
from datetime import date, timedelta
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.admin.views.decorators import staff_member_required
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.core.mail import EmailMessage
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.db.models import Q
|
||||
|
||||
from bird.models import FallenBird
|
||||
from .forms import ManualReportForm, AutomaticReportForm
|
||||
from .models import AutomaticReport, ReportLog
|
||||
from .services import ReportGenerator
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def reports_dashboard(request):
|
||||
"""Main reports dashboard."""
|
||||
context = {
|
||||
'title': 'Reports Dashboard',
|
||||
}
|
||||
return render(request, 'admin/reports/dashboard.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def manual_report(request):
|
||||
"""Create and send/download manual reports."""
|
||||
if request.method == 'POST':
|
||||
form = ManualReportForm(request.POST)
|
||||
if form.is_valid():
|
||||
# Handle form submission based on action
|
||||
action = request.POST.get('action')
|
||||
|
||||
if action == 'download':
|
||||
# Generate CSV and return as download
|
||||
generator = ReportGenerator(
|
||||
date_from=form.cleaned_data['date_from'],
|
||||
date_to=form.cleaned_data['date_to'],
|
||||
include_naturschutzbehoerde=form.cleaned_data['include_naturschutzbehörde'],
|
||||
include_jagdbehoerde=form.cleaned_data['include_jagdbehörde']
|
||||
)
|
||||
|
||||
csv_content, bird_count = generator.generate_csv()
|
||||
filename = generator.get_filename()
|
||||
|
||||
# Create download log
|
||||
generator.create_download_log()
|
||||
|
||||
response = HttpResponse(csv_content, content_type='text/csv')
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
|
||||
messages.success(request, f'Report mit {bird_count} Patienten wurde heruntergeladen.')
|
||||
return response
|
||||
|
||||
elif action == 'email':
|
||||
# Send via email
|
||||
email_addresses = form.cleaned_data['email_addresses']
|
||||
email_list = [email.email for email in email_addresses]
|
||||
|
||||
# Add custom email if provided
|
||||
if form.cleaned_data.get('custom_email'):
|
||||
email_list.append(form.cleaned_data['custom_email'])
|
||||
|
||||
if not email_list:
|
||||
messages.error(request, 'Bitte wählen Sie mindestens eine E-Mail-Adresse aus.')
|
||||
return render(request, 'admin/reports/manual_report.html', {'form': form, 'title': 'Manuellen Report erstellen'})
|
||||
|
||||
generator = ReportGenerator(
|
||||
date_from=form.cleaned_data['date_from'],
|
||||
date_to=form.cleaned_data['date_to'],
|
||||
include_naturschutzbehoerde=form.cleaned_data['include_naturschutzbehörde'],
|
||||
include_jagdbehoerde=form.cleaned_data['include_jagdbehörde']
|
||||
)
|
||||
|
||||
report_log, success, error = generator.send_email_report(email_list)
|
||||
|
||||
if success:
|
||||
messages.success(
|
||||
request,
|
||||
f'Report wurde erfolgreich an {len(email_list)} E-Mail-Adresse(n) gesendet.'
|
||||
)
|
||||
return redirect('reports:dashboard')
|
||||
else:
|
||||
messages.error(request, f'Fehler beim Senden des Reports: {error}')
|
||||
else:
|
||||
form = ManualReportForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Manuellen Report erstellen',
|
||||
}
|
||||
return render(request, 'admin/reports/manual_report.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def automatic_reports(request):
|
||||
"""List and manage automatic reports."""
|
||||
reports = AutomaticReport.objects.all()
|
||||
context = {
|
||||
'reports': reports,
|
||||
'title': 'Automatische Reports',
|
||||
}
|
||||
return render(request, 'admin/reports/automatic_reports.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def create_automatic_report(request):
|
||||
"""Create new automatic report."""
|
||||
if request.method == 'POST':
|
||||
form = AutomaticReportForm(request.POST)
|
||||
if form.is_valid():
|
||||
report = form.save(commit=False)
|
||||
report.created_by = request.user
|
||||
report.save()
|
||||
form.save_m2m() # Save many-to-many relationships
|
||||
messages.success(request, 'Automatischer Report wurde erfolgreich erstellt.')
|
||||
return redirect('reports:automatic_reports')
|
||||
else:
|
||||
form = AutomaticReportForm()
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'title': 'Automatischen Report erstellen',
|
||||
}
|
||||
return render(request, 'admin/reports/automatic_report_form.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def edit_automatic_report(request, report_id):
|
||||
"""Edit automatic report."""
|
||||
report = get_object_or_404(AutomaticReport, id=report_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AutomaticReportForm(request.POST, instance=report)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, 'Automatischer Report wurde erfolgreich aktualisiert.')
|
||||
return redirect('reports:automatic_reports')
|
||||
else:
|
||||
form = AutomaticReportForm(instance=report)
|
||||
|
||||
context = {
|
||||
'form': form,
|
||||
'report': report,
|
||||
'title': f'Report bearbeiten: {report.name}',
|
||||
}
|
||||
return render(request, 'admin/reports/automatic_report_form.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def delete_automatic_report(request, report_id):
|
||||
"""Delete automatic report."""
|
||||
report = get_object_or_404(AutomaticReport, id=report_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
report.delete()
|
||||
messages.success(request, 'Automatischer Report wurde erfolgreich gelöscht.')
|
||||
return redirect('reports:automatic_reports')
|
||||
|
||||
context = {
|
||||
'report': report,
|
||||
'title': f'Report löschen: {report.name}',
|
||||
}
|
||||
return render(request, 'admin/reports/automatic_report_confirm_delete.html', context)
|
||||
|
||||
|
||||
@staff_member_required
|
||||
def report_logs(request):
|
||||
"""View report logs."""
|
||||
logs = ReportLog.objects.all().order_by('-created_at')[:100] # Show last 100 logs
|
||||
context = {
|
||||
'report_logs': logs,
|
||||
'title': 'Report-Protokoll',
|
||||
}
|
||||
return render(request, 'admin/reports/report_logs.html', context)
|
Loading…
Add table
Add a link
Reference in a new issue