914 lines
34 KiB
Python
914 lines
34 KiB
Python
"""
|
|
Multi-Format-Report-Generator für das MYP-System
|
|
===============================================
|
|
|
|
Dieses Modul stellt umfassende Report-Generierung in verschiedenen Formaten bereit:
|
|
- PDF-Reports mit professionellem Layout
|
|
- Excel-Reports mit Diagrammen und Formatierungen
|
|
- CSV-Export für Datenanalyse
|
|
- JSON-Export für API-Integration
|
|
"""
|
|
|
|
import os
|
|
import io
|
|
import json
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Any, Optional, Union, BinaryIO
|
|
from dataclasses import dataclass, asdict
|
|
from abc import ABC, abstractmethod
|
|
|
|
# PDF-Generation
|
|
try:
|
|
from reportlab.lib import colors
|
|
from reportlab.lib.pagesizes import A4, letter
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.units import inch, cm
|
|
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image, PageBreak
|
|
from reportlab.graphics.shapes import Drawing
|
|
from reportlab.graphics.charts.lineplots import LinePlot
|
|
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
|
from reportlab.graphics.charts.piecharts import Pie
|
|
from reportlab.lib.validators import Auto
|
|
PDF_AVAILABLE = True
|
|
except ImportError:
|
|
PDF_AVAILABLE = False
|
|
|
|
# Excel-Generation
|
|
try:
|
|
import xlsxwriter
|
|
from xlsxwriter.workbook import Workbook
|
|
from xlsxwriter.worksheet import Worksheet
|
|
EXCEL_AVAILABLE = True
|
|
except ImportError:
|
|
EXCEL_AVAILABLE = False
|
|
|
|
import csv
|
|
from flask import make_response, jsonify
|
|
|
|
from utils.logging_config import get_logger
|
|
from models import Job, User, Printer, Stats, GuestRequest, get_db_session
|
|
|
|
logger = get_logger("reports")
|
|
|
|
@dataclass
|
|
class ReportConfig:
|
|
"""Konfiguration für Report-Generierung"""
|
|
title: str
|
|
subtitle: str = ""
|
|
author: str = "MYP System"
|
|
date_range: tuple = None
|
|
include_charts: bool = True
|
|
include_summary: bool = True
|
|
template: str = "standard"
|
|
logo_path: str = None
|
|
footer_text: str = "Generiert vom MYP-System"
|
|
|
|
@dataclass
|
|
class ChartData:
|
|
"""Daten für Diagramme"""
|
|
chart_type: str # 'line', 'bar', 'pie'
|
|
title: str
|
|
data: List[Dict[str, Any]]
|
|
labels: List[str] = None
|
|
colors: List[str] = None
|
|
|
|
class BaseReportGenerator(ABC):
|
|
"""Abstrakte Basis-Klasse für Report-Generatoren"""
|
|
|
|
def __init__(self, config: ReportConfig):
|
|
self.config = config
|
|
self.data = {}
|
|
self.charts = []
|
|
|
|
@abstractmethod
|
|
def generate(self, output_stream: BinaryIO) -> bool:
|
|
"""Generiert den Report in den angegebenen Stream"""
|
|
pass
|
|
|
|
def add_data_section(self, name: str, data: List[Dict[str, Any]], headers: List[str] = None):
|
|
"""Fügt eine Datensektion hinzu"""
|
|
self.data[name] = {
|
|
'data': data,
|
|
'headers': headers or (list(data[0].keys()) if data else [])
|
|
}
|
|
|
|
def add_chart(self, chart: ChartData):
|
|
"""Fügt ein Diagramm hinzu"""
|
|
self.charts.append(chart)
|
|
|
|
class PDFReportGenerator(BaseReportGenerator):
|
|
"""PDF-Report-Generator mit professionellem Layout"""
|
|
|
|
def __init__(self, config: ReportConfig):
|
|
super().__init__(config)
|
|
if not PDF_AVAILABLE:
|
|
raise ImportError("ReportLab ist nicht installiert. Verwenden Sie: pip install reportlab")
|
|
|
|
self.doc = None
|
|
self.story = []
|
|
self.styles = getSampleStyleSheet()
|
|
self._setup_custom_styles()
|
|
|
|
def _setup_custom_styles(self):
|
|
"""Richtet benutzerdefinierte Styles ein"""
|
|
# Titel-Style
|
|
self.styles.add(ParagraphStyle(
|
|
name='CustomTitle',
|
|
parent=self.styles['Heading1'],
|
|
fontSize=24,
|
|
spaceAfter=30,
|
|
alignment=1, # Zentriert
|
|
textColor=colors.HexColor('#1f2937')
|
|
))
|
|
|
|
# Untertitel-Style
|
|
self.styles.add(ParagraphStyle(
|
|
name='CustomSubtitle',
|
|
parent=self.styles['Heading2'],
|
|
fontSize=16,
|
|
spaceAfter=20,
|
|
alignment=1,
|
|
textColor=colors.HexColor('#6b7280')
|
|
))
|
|
|
|
# Sektions-Header
|
|
self.styles.add(ParagraphStyle(
|
|
name='SectionHeader',
|
|
parent=self.styles['Heading2'],
|
|
fontSize=14,
|
|
spaceBefore=20,
|
|
spaceAfter=10,
|
|
textColor=colors.HexColor('#374151'),
|
|
borderWidth=1,
|
|
borderColor=colors.HexColor('#d1d5db'),
|
|
borderPadding=5
|
|
))
|
|
|
|
def generate(self, output_stream: BinaryIO) -> bool:
|
|
"""Generiert PDF-Report"""
|
|
try:
|
|
self.doc = SimpleDocTemplate(
|
|
output_stream,
|
|
pagesize=A4,
|
|
rightMargin=2*cm,
|
|
leftMargin=2*cm,
|
|
topMargin=2*cm,
|
|
bottomMargin=2*cm
|
|
)
|
|
|
|
self._build_header()
|
|
self._build_summary()
|
|
self._build_data_sections()
|
|
self._build_charts()
|
|
self._build_footer()
|
|
|
|
self.doc.build(self.story)
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei PDF-Generierung: {str(e)}")
|
|
return False
|
|
|
|
def _build_header(self):
|
|
"""Erstellt den Report-Header"""
|
|
# Logo (falls vorhanden)
|
|
if self.config.logo_path and os.path.exists(self.config.logo_path):
|
|
try:
|
|
logo = Image(self.config.logo_path, width=2*inch, height=1*inch)
|
|
self.story.append(logo)
|
|
self.story.append(Spacer(1, 0.2*inch))
|
|
except Exception as e:
|
|
logger.warning(f"Logo konnte nicht geladen werden: {str(e)}")
|
|
|
|
# Titel
|
|
title = Paragraph(self.config.title, self.styles['CustomTitle'])
|
|
self.story.append(title)
|
|
|
|
# Untertitel
|
|
if self.config.subtitle:
|
|
subtitle = Paragraph(self.config.subtitle, self.styles['CustomSubtitle'])
|
|
self.story.append(subtitle)
|
|
|
|
# Generierungsdatum
|
|
date_text = f"Generiert am: {datetime.now().strftime('%d.%m.%Y %H:%M')}"
|
|
date_para = Paragraph(date_text, self.styles['Normal'])
|
|
self.story.append(date_para)
|
|
|
|
# Autor
|
|
author_text = f"Erstellt von: {self.config.author}"
|
|
author_para = Paragraph(author_text, self.styles['Normal'])
|
|
self.story.append(author_para)
|
|
|
|
self.story.append(Spacer(1, 0.3*inch))
|
|
|
|
def _build_summary(self):
|
|
"""Erstellt die Zusammenfassung"""
|
|
if not self.config.include_summary:
|
|
return
|
|
|
|
header = Paragraph("Zusammenfassung", self.styles['SectionHeader'])
|
|
self.story.append(header)
|
|
|
|
# Sammle Statistiken aus den Daten
|
|
total_records = sum(len(section['data']) for section in self.data.values())
|
|
|
|
summary_data = [
|
|
['Gesamtanzahl Datensätze', str(total_records)],
|
|
['Berichtszeitraum', self._format_date_range()],
|
|
['Anzahl Sektionen', str(len(self.data))],
|
|
['Anzahl Diagramme', str(len(self.charts))]
|
|
]
|
|
|
|
summary_table = Table(summary_data, colWidths=[4*inch, 2*inch])
|
|
summary_table.setStyle(TableStyle([
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f3f4f6')),
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
|
|
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, 0), 12),
|
|
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
|
('BACKGROUND', (0, 1), (-1, -1), colors.white),
|
|
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#d1d5db'))
|
|
]))
|
|
|
|
self.story.append(summary_table)
|
|
self.story.append(Spacer(1, 0.2*inch))
|
|
|
|
def _build_data_sections(self):
|
|
"""Erstellt die Datensektionen"""
|
|
for section_name, section_data in self.data.items():
|
|
# Sektions-Header
|
|
header = Paragraph(section_name, self.styles['SectionHeader'])
|
|
self.story.append(header)
|
|
|
|
# Daten-Tabelle
|
|
table_data = [section_data['headers']]
|
|
table_data.extend([
|
|
[str(row.get(header, '')) for header in section_data['headers']]
|
|
for row in section_data['data']
|
|
])
|
|
|
|
# Spaltenbreiten berechnen
|
|
col_count = len(section_data['headers'])
|
|
col_width = (self.doc.width - 2*inch) / col_count
|
|
col_widths = [col_width] * col_count
|
|
|
|
table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
|
table.setStyle(TableStyle([
|
|
# Header-Styling
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#3b82f6')),
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
|
('FONTSIZE', (0, 0), (-1, 0), 10),
|
|
|
|
# Daten-Styling
|
|
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
|
('FONTSIZE', (0, 1), (-1, -1), 9),
|
|
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f9fafb')]),
|
|
|
|
# Rahmen
|
|
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#d1d5db')),
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
|
|
('LEFTPADDING', (0, 0), (-1, -1), 6),
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 6),
|
|
('TOPPADDING', (0, 0), (-1, -1), 8),
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
|
]))
|
|
|
|
self.story.append(table)
|
|
self.story.append(Spacer(1, 0.2*inch))
|
|
|
|
# Seitenumbruch bei vielen Daten
|
|
if len(section_data['data']) > 20:
|
|
self.story.append(PageBreak())
|
|
|
|
def _build_charts(self):
|
|
"""Erstellt die Diagramme"""
|
|
if not self.config.include_charts or not self.charts:
|
|
return
|
|
|
|
header = Paragraph("Diagramme", self.styles['SectionHeader'])
|
|
self.story.append(header)
|
|
|
|
for chart in self.charts:
|
|
chart_title = Paragraph(chart.title, self.styles['Heading3'])
|
|
self.story.append(chart_title)
|
|
|
|
# Diagramm basierend auf Typ erstellen
|
|
drawing = self._create_chart_drawing(chart)
|
|
if drawing:
|
|
self.story.append(drawing)
|
|
self.story.append(Spacer(1, 0.2*inch))
|
|
|
|
def _create_chart_drawing(self, chart: ChartData) -> Optional[Any]:
|
|
"""Erstellt ein Diagramm-Drawing"""
|
|
# Überprüfe ob PDF-Bibliotheken verfügbar sind
|
|
if not PDF_AVAILABLE:
|
|
logger.warning("PDF-Bibliotheken nicht verfügbar - Diagramm wird übersprungen")
|
|
return None
|
|
|
|
try:
|
|
drawing = Drawing(400, 300)
|
|
|
|
if chart.chart_type == 'bar':
|
|
bar_chart = VerticalBarChart()
|
|
bar_chart.x = 50
|
|
bar_chart.y = 50
|
|
bar_chart.height = 200
|
|
bar_chart.width = 300
|
|
|
|
# Daten vorbereiten
|
|
values = [[item.get('value', 0) for item in chart.data]]
|
|
categories = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)]
|
|
|
|
bar_chart.data = values
|
|
bar_chart.categoryAxis.categoryNames = categories
|
|
bar_chart.valueAxis.valueMin = 0
|
|
|
|
# Farben setzen
|
|
if chart.colors:
|
|
bar_chart.bars[0].fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6')
|
|
|
|
drawing.add(bar_chart)
|
|
|
|
elif chart.chart_type == 'pie':
|
|
pie_chart = Pie()
|
|
pie_chart.x = 150
|
|
pie_chart.y = 100
|
|
pie_chart.width = 100
|
|
pie_chart.height = 100
|
|
|
|
# Daten vorbereiten
|
|
pie_chart.data = [item.get('value', 0) for item in chart.data]
|
|
pie_chart.labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart.data)]
|
|
|
|
# Farben setzen
|
|
if chart.colors:
|
|
pie_chart.slices.fillColor = colors.HexColor(chart.colors[0] if chart.colors else '#3b82f6')
|
|
|
|
drawing.add(pie_chart)
|
|
|
|
return drawing
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Diagramm-Erstellung: {str(e)}")
|
|
return None
|
|
|
|
def _build_footer(self):
|
|
"""Erstellt den Report-Footer"""
|
|
footer_text = self.config.footer_text
|
|
footer = Paragraph(footer_text, self.styles['Normal'])
|
|
self.story.append(Spacer(1, 0.3*inch))
|
|
self.story.append(footer)
|
|
|
|
def _format_date_range(self) -> str:
|
|
"""Formatiert den Datumsbereich"""
|
|
if not self.config.date_range:
|
|
return "Alle verfügbaren Daten"
|
|
|
|
start_date, end_date = self.config.date_range
|
|
return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
|
|
|
|
class ExcelReportGenerator(BaseReportGenerator):
|
|
"""Excel-Report-Generator mit Diagrammen und Formatierungen"""
|
|
|
|
def __init__(self, config: ReportConfig):
|
|
super().__init__(config)
|
|
if not EXCEL_AVAILABLE:
|
|
raise ImportError("XlsxWriter ist nicht installiert. Verwenden Sie: pip install xlsxwriter")
|
|
|
|
self.workbook = None
|
|
self.formats = {}
|
|
|
|
def generate(self, output_stream: BinaryIO) -> bool:
|
|
"""Generiert Excel-Report"""
|
|
try:
|
|
self.workbook = xlsxwriter.Workbook(output_stream, {'in_memory': True})
|
|
self._setup_formats()
|
|
|
|
# Zusammenfassungs-Arbeitsblatt
|
|
if self.config.include_summary:
|
|
self._create_summary_worksheet()
|
|
|
|
# Daten-Arbeitsblätter
|
|
for section_name, section_data in self.data.items():
|
|
self._create_data_worksheet(section_name, section_data)
|
|
|
|
# Diagramm-Arbeitsblätter
|
|
if self.config.include_charts and self.charts:
|
|
self._create_charts_worksheet()
|
|
|
|
self.workbook.close()
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei Excel-Generierung: {str(e)}")
|
|
return False
|
|
|
|
def _setup_formats(self):
|
|
"""Richtet Excel-Formate ein"""
|
|
self.formats = {
|
|
'title': self.workbook.add_format({
|
|
'font_size': 18,
|
|
'bold': True,
|
|
'align': 'center',
|
|
'bg_color': '#1f2937',
|
|
'font_color': 'white',
|
|
'border': 1
|
|
}),
|
|
'header': self.workbook.add_format({
|
|
'font_size': 12,
|
|
'bold': True,
|
|
'bg_color': '#3b82f6',
|
|
'font_color': 'white',
|
|
'align': 'center',
|
|
'border': 1
|
|
}),
|
|
'data': self.workbook.add_format({
|
|
'align': 'center',
|
|
'border': 1
|
|
}),
|
|
'data_alt': self.workbook.add_format({
|
|
'align': 'center',
|
|
'bg_color': '#f9fafb',
|
|
'border': 1
|
|
}),
|
|
'number': self.workbook.add_format({
|
|
'num_format': '#,##0',
|
|
'align': 'right',
|
|
'border': 1
|
|
}),
|
|
'currency': self.workbook.add_format({
|
|
'num_format': '#,##0.00 €',
|
|
'align': 'right',
|
|
'border': 1
|
|
}),
|
|
'percentage': self.workbook.add_format({
|
|
'num_format': '0.00%',
|
|
'align': 'right',
|
|
'border': 1
|
|
}),
|
|
'date': self.workbook.add_format({
|
|
'num_format': 'dd.mm.yyyy',
|
|
'align': 'center',
|
|
'border': 1
|
|
})
|
|
}
|
|
|
|
def _create_summary_worksheet(self):
|
|
"""Erstellt das Zusammenfassungs-Arbeitsblatt"""
|
|
worksheet = self.workbook.add_worksheet('Zusammenfassung')
|
|
|
|
# Titel
|
|
worksheet.merge_range('A1:E1', self.config.title, self.formats['title'])
|
|
|
|
# Untertitel
|
|
if self.config.subtitle:
|
|
worksheet.merge_range('A2:E2', self.config.subtitle, self.formats['header'])
|
|
|
|
# Metadaten
|
|
row = 4
|
|
metadata = [
|
|
['Generiert am:', datetime.now().strftime('%d.%m.%Y %H:%M')],
|
|
['Erstellt von:', self.config.author],
|
|
['Berichtszeitraum:', self._format_date_range()],
|
|
['Anzahl Sektionen:', str(len(self.data))],
|
|
['Anzahl Diagramme:', str(len(self.charts))]
|
|
]
|
|
|
|
for label, value in metadata:
|
|
worksheet.write(row, 0, label, self.formats['header'])
|
|
worksheet.write(row, 1, value, self.formats['data'])
|
|
row += 1
|
|
|
|
# Statistiken pro Sektion
|
|
row += 2
|
|
worksheet.write(row, 0, 'Sektions-Übersicht:', self.formats['header'])
|
|
row += 1
|
|
|
|
for section_name, section_data in self.data.items():
|
|
worksheet.write(row, 0, section_name, self.formats['data'])
|
|
worksheet.write(row, 1, len(section_data['data']), self.formats['number'])
|
|
row += 1
|
|
|
|
# Spaltenbreiten anpassen
|
|
worksheet.set_column('A:A', 25)
|
|
worksheet.set_column('B:B', 20)
|
|
|
|
def _create_data_worksheet(self, section_name: str, section_data: Dict[str, Any]):
|
|
"""Erstellt ein Daten-Arbeitsblatt"""
|
|
# Ungültige Zeichen für Arbeitsblatt-Namen ersetzen
|
|
safe_name = ''.join(c for c in section_name if c.isalnum() or c in ' -_')[:31]
|
|
worksheet = self.workbook.add_worksheet(safe_name)
|
|
|
|
# Header schreiben
|
|
headers = section_data['headers']
|
|
for col, header in enumerate(headers):
|
|
worksheet.write(0, col, header, self.formats['header'])
|
|
|
|
# Daten schreiben
|
|
for row_idx, row_data in enumerate(section_data['data'], start=1):
|
|
for col_idx, header in enumerate(headers):
|
|
value = row_data.get(header, '')
|
|
|
|
# Format basierend auf Datentyp wählen
|
|
cell_format = self._get_cell_format(value, row_idx)
|
|
worksheet.write(row_idx, col_idx, value, cell_format)
|
|
|
|
# Autofilter hinzufügen
|
|
if section_data['data']:
|
|
worksheet.autofilter(0, 0, len(section_data['data']), len(headers) - 1)
|
|
|
|
# Spaltenbreiten anpassen
|
|
for col_idx, header in enumerate(headers):
|
|
max_length = max(
|
|
len(str(header)),
|
|
max(len(str(row.get(header, ''))) for row in section_data['data']) if section_data['data'] else 0
|
|
)
|
|
worksheet.set_column(col_idx, col_idx, min(max_length + 2, 50))
|
|
|
|
def _create_charts_worksheet(self):
|
|
"""Erstellt das Diagramm-Arbeitsblatt"""
|
|
worksheet = self.workbook.add_worksheet('Diagramme')
|
|
|
|
row = 0
|
|
for chart_idx, chart_data in enumerate(self.charts):
|
|
# Diagramm-Titel
|
|
worksheet.write(row, 0, chart_data.title, self.formats['header'])
|
|
row += 2
|
|
|
|
# Daten für Diagramm vorbereiten
|
|
data_worksheet_name = f'Chart_Data_{chart_idx}'
|
|
data_worksheet = self.workbook.add_worksheet(data_worksheet_name)
|
|
|
|
# Daten ins Data-Arbeitsblatt schreiben
|
|
labels = [item.get('label', f'Item {i}') for i, item in enumerate(chart_data.data)]
|
|
values = [item.get('value', 0) for item in chart_data.data]
|
|
|
|
data_worksheet.write_column('A1', ['Label'] + labels)
|
|
data_worksheet.write_column('B1', ['Value'] + values)
|
|
|
|
# Excel-Diagramm erstellen
|
|
if chart_data.chart_type == 'bar':
|
|
chart = self.workbook.add_chart({'type': 'column'})
|
|
elif chart_data.chart_type == 'line':
|
|
chart = self.workbook.add_chart({'type': 'line'})
|
|
elif chart_data.chart_type == 'pie':
|
|
chart = self.workbook.add_chart({'type': 'pie'})
|
|
else:
|
|
chart = self.workbook.add_chart({'type': 'column'})
|
|
|
|
# Datenreihe hinzufügen
|
|
chart.add_series({
|
|
'name': chart_data.title,
|
|
'categories': [data_worksheet_name, 1, 0, len(labels), 0],
|
|
'values': [data_worksheet_name, 1, 1, len(values), 1],
|
|
})
|
|
|
|
chart.set_title({'name': chart_data.title})
|
|
chart.set_x_axis({'name': 'Kategorien'})
|
|
chart.set_y_axis({'name': 'Werte'})
|
|
|
|
# Diagramm ins Arbeitsblatt einfügen
|
|
worksheet.insert_chart(row, 0, chart)
|
|
row += 15 # Platz für nächstes Diagramm
|
|
|
|
def _get_cell_format(self, value: Any, row_idx: int):
|
|
"""Bestimmt das Zellformat basierend auf dem Wert"""
|
|
# Alternierende Zeilenfarben
|
|
base_format = self.formats['data'] if row_idx % 2 == 1 else self.formats['data_alt']
|
|
|
|
# Spezielle Formate für Zahlen, Daten, etc.
|
|
if isinstance(value, (int, float)):
|
|
return self.formats['number']
|
|
elif isinstance(value, datetime):
|
|
return self.formats['date']
|
|
elif isinstance(value, str) and value.endswith('%'):
|
|
return self.formats['percentage']
|
|
elif isinstance(value, str) and '€' in value:
|
|
return self.formats['currency']
|
|
|
|
return base_format
|
|
|
|
def _format_date_range(self) -> str:
|
|
"""Formatiert den Datumsbereich"""
|
|
if not self.config.date_range:
|
|
return "Alle verfügbaren Daten"
|
|
|
|
start_date, end_date = self.config.date_range
|
|
return f"{start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}"
|
|
|
|
class CSVReportGenerator(BaseReportGenerator):
|
|
"""CSV-Report-Generator für Datenanalyse"""
|
|
|
|
def generate(self, output_stream: BinaryIO) -> bool:
|
|
"""Generiert CSV-Report"""
|
|
try:
|
|
# Text-Stream für CSV-Writer
|
|
text_stream = io.TextIOWrapper(output_stream, encoding='utf-8-sig', newline='')
|
|
writer = csv.writer(text_stream, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
|
|
|
# Header mit Metadaten
|
|
writer.writerow([f'# {self.config.title}'])
|
|
writer.writerow([f'# Generiert am: {datetime.now().strftime("%d.%m.%Y %H:%M")}'])
|
|
writer.writerow([f'# Erstellt von: {self.config.author}'])
|
|
writer.writerow(['']) # Leerzeile
|
|
|
|
# Daten-Sektionen
|
|
for section_name, section_data in self.data.items():
|
|
writer.writerow([f'# Sektion: {section_name}'])
|
|
|
|
# Headers
|
|
writer.writerow(section_data['headers'])
|
|
|
|
# Daten
|
|
for row in section_data['data']:
|
|
csv_row = [str(row.get(header, '')) for header in section_data['headers']]
|
|
writer.writerow(csv_row)
|
|
|
|
writer.writerow(['']) # Leerzeile zwischen Sektionen
|
|
|
|
text_stream.flush()
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei CSV-Generierung: {str(e)}")
|
|
return False
|
|
|
|
class JSONReportGenerator(BaseReportGenerator):
|
|
"""JSON-Report-Generator für API-Integration"""
|
|
|
|
def generate(self, output_stream: BinaryIO) -> bool:
|
|
"""Generiert JSON-Report"""
|
|
try:
|
|
report_data = {
|
|
'metadata': {
|
|
'title': self.config.title,
|
|
'subtitle': self.config.subtitle,
|
|
'author': self.config.author,
|
|
'generated_at': datetime.now().isoformat(),
|
|
'date_range': {
|
|
'start': self.config.date_range[0].isoformat() if self.config.date_range else None,
|
|
'end': self.config.date_range[1].isoformat() if self.config.date_range else None
|
|
} if self.config.date_range else None
|
|
},
|
|
'data': self.data,
|
|
'charts': [asdict(chart) for chart in self.charts] if self.charts else []
|
|
}
|
|
|
|
json_str = json.dumps(report_data, ensure_ascii=False, indent=2, default=str)
|
|
output_stream.write(json_str.encode('utf-8'))
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei JSON-Generierung: {str(e)}")
|
|
return False
|
|
|
|
class ReportFactory:
|
|
"""Factory für Report-Generatoren"""
|
|
|
|
GENERATORS = {
|
|
'pdf': PDFReportGenerator,
|
|
'excel': ExcelReportGenerator,
|
|
'xlsx': ExcelReportGenerator,
|
|
'csv': CSVReportGenerator,
|
|
'json': JSONReportGenerator
|
|
}
|
|
|
|
@classmethod
|
|
def create_generator(cls, format_type: str, config: ReportConfig) -> BaseReportGenerator:
|
|
"""Erstellt einen Report-Generator für das angegebene Format"""
|
|
format_type = format_type.lower()
|
|
|
|
if format_type not in cls.GENERATORS:
|
|
raise ValueError(f"Unbekanntes Report-Format: {format_type}")
|
|
|
|
generator_class = cls.GENERATORS[format_type]
|
|
return generator_class(config)
|
|
|
|
@classmethod
|
|
def get_available_formats(cls) -> List[str]:
|
|
"""Gibt verfügbare Report-Formate zurück"""
|
|
available = []
|
|
|
|
for format_type, generator_class in cls.GENERATORS.items():
|
|
try:
|
|
# Test ob Generator funktioniert
|
|
if format_type in ['pdf'] and not PDF_AVAILABLE:
|
|
continue
|
|
elif format_type in ['excel', 'xlsx'] and not EXCEL_AVAILABLE:
|
|
continue
|
|
|
|
available.append(format_type)
|
|
except ImportError:
|
|
continue
|
|
|
|
return available
|
|
|
|
# Vordefinierte Report-Templates
|
|
class JobReportBuilder:
|
|
"""Builder für Job-Reports"""
|
|
|
|
@staticmethod
|
|
def build_jobs_report(
|
|
start_date: datetime = None,
|
|
end_date: datetime = None,
|
|
user_id: int = None,
|
|
printer_id: int = None,
|
|
include_completed: bool = True,
|
|
include_cancelled: bool = False
|
|
) -> Dict[str, Any]:
|
|
"""Erstellt Job-Report-Daten"""
|
|
|
|
with get_db_session() as db_session:
|
|
query = db_session.query(Job)
|
|
|
|
# Filter anwenden
|
|
if start_date:
|
|
query = query.filter(Job.created_at >= start_date)
|
|
if end_date:
|
|
query = query.filter(Job.created_at <= end_date)
|
|
if user_id:
|
|
query = query.filter(Job.user_id == user_id)
|
|
if printer_id:
|
|
query = query.filter(Job.printer_id == printer_id)
|
|
|
|
status_filters = []
|
|
if include_completed:
|
|
status_filters.append('finished')
|
|
if include_cancelled:
|
|
status_filters.append('cancelled')
|
|
if not include_cancelled and not include_completed:
|
|
status_filters = ['scheduled', 'running', 'paused']
|
|
|
|
if status_filters:
|
|
query = query.filter(Job.status.in_(status_filters))
|
|
|
|
jobs = query.all()
|
|
|
|
# Daten vorbereiten
|
|
job_data = []
|
|
for job in jobs:
|
|
job_data.append({
|
|
'ID': job.id,
|
|
'Name': job.name,
|
|
'Benutzer': job.user.name if job.user else 'Unbekannt',
|
|
'Drucker': job.printer.name if job.printer else 'Unbekannt',
|
|
'Status': job.status,
|
|
'Erstellt': job.created_at.strftime('%d.%m.%Y %H:%M') if job.created_at else '',
|
|
'Gestartet': job.start_at.strftime('%d.%m.%Y %H:%M') if job.start_at else '',
|
|
'Beendet': job.end_at.strftime('%d.%m.%Y %H:%M') if job.end_at else '',
|
|
'Dauer (Min)': job.duration_minutes or 0,
|
|
'Material (g)': job.material_used or 0,
|
|
'Beschreibung': job.description or ''
|
|
})
|
|
|
|
return {
|
|
'data': job_data,
|
|
'headers': ['ID', 'Name', 'Benutzer', 'Drucker', 'Status', 'Erstellt', 'Gestartet', 'Beendet', 'Dauer (Min)', 'Material (g)', 'Beschreibung']
|
|
}
|
|
|
|
class UserReportBuilder:
|
|
"""Builder für Benutzer-Reports"""
|
|
|
|
@staticmethod
|
|
def build_users_report(include_inactive: bool = False) -> Dict[str, Any]:
|
|
"""Erstellt Benutzer-Report-Daten"""
|
|
|
|
with get_db_session() as db_session:
|
|
query = db_session.query(User)
|
|
|
|
if not include_inactive:
|
|
query = query.filter(User.active == True)
|
|
|
|
users = query.all()
|
|
|
|
# Daten vorbereiten
|
|
user_data = []
|
|
for user in users:
|
|
user_data.append({
|
|
'ID': user.id,
|
|
'Name': user.name,
|
|
'E-Mail': user.email,
|
|
'Benutzername': user.username,
|
|
'Rolle': user.role,
|
|
'Aktiv': 'Ja' if user.active else 'Nein',
|
|
'Abteilung': user.department or '',
|
|
'Position': user.position or '',
|
|
'Erstellt': user.created_at.strftime('%d.%m.%Y') if user.created_at else '',
|
|
'Letzter Login': user.last_login.strftime('%d.%m.%Y %H:%M') if user.last_login else 'Nie'
|
|
})
|
|
|
|
return {
|
|
'data': user_data,
|
|
'headers': ['ID', 'Name', 'E-Mail', 'Benutzername', 'Rolle', 'Aktiv', 'Abteilung', 'Position', 'Erstellt', 'Letzter Login']
|
|
}
|
|
|
|
class PrinterReportBuilder:
|
|
"""Builder für Drucker-Reports"""
|
|
|
|
@staticmethod
|
|
def build_printers_report(include_inactive: bool = False) -> Dict[str, Any]:
|
|
"""Erstellt Drucker-Report-Daten"""
|
|
|
|
with get_db_session() as db_session:
|
|
query = db_session.query(Printer)
|
|
|
|
if not include_inactive:
|
|
query = query.filter(Printer.active == True)
|
|
|
|
printers = query.all()
|
|
|
|
# Daten vorbereiten
|
|
printer_data = []
|
|
for printer in printers:
|
|
printer_data.append({
|
|
'ID': printer.id,
|
|
'Name': printer.name,
|
|
'Modell': printer.model or '',
|
|
'Standort': printer.location or '',
|
|
'IP-Adresse': printer.ip_address or '',
|
|
'MAC-Adresse': printer.mac_address,
|
|
'Plug-IP': printer.plug_ip,
|
|
'Status': printer.status,
|
|
'Aktiv': 'Ja' if printer.active else 'Nein',
|
|
'Erstellt': printer.created_at.strftime('%d.%m.%Y') if printer.created_at else '',
|
|
'Letzte Prüfung': printer.last_checked.strftime('%d.%m.%Y %H:%M') if printer.last_checked else 'Nie'
|
|
})
|
|
|
|
return {
|
|
'data': printer_data,
|
|
'headers': ['ID', 'Name', 'Modell', 'Standort', 'IP-Adresse', 'MAC-Adresse', 'Plug-IP', 'Status', 'Aktiv', 'Erstellt', 'Letzte Prüfung']
|
|
}
|
|
|
|
def generate_comprehensive_report(
|
|
format_type: str,
|
|
start_date: datetime = None,
|
|
end_date: datetime = None,
|
|
include_jobs: bool = True,
|
|
include_users: bool = True,
|
|
include_printers: bool = True,
|
|
user_id: int = None
|
|
) -> bytes:
|
|
"""Generiert einen umfassenden System-Report"""
|
|
|
|
# Konfiguration
|
|
config = ReportConfig(
|
|
title="MYP System Report",
|
|
subtitle="Umfassende Systemübersicht",
|
|
author="MYP System",
|
|
date_range=(start_date, end_date) if start_date and end_date else None,
|
|
include_charts=True,
|
|
include_summary=True
|
|
)
|
|
|
|
# Generator erstellen
|
|
generator = ReportFactory.create_generator(format_type, config)
|
|
|
|
# Daten hinzufügen
|
|
if include_jobs:
|
|
job_data = JobReportBuilder.build_jobs_report(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
user_id=user_id
|
|
)
|
|
generator.add_data_section("Jobs", job_data['data'], job_data['headers'])
|
|
|
|
# Job-Status-Diagramm
|
|
status_counts = {}
|
|
for job in job_data['data']:
|
|
status = job['Status']
|
|
status_counts[status] = status_counts.get(status, 0) + 1
|
|
|
|
chart_data = ChartData(
|
|
chart_type='pie',
|
|
title='Job-Status-Verteilung',
|
|
data=[{'label': status, 'value': count} for status, count in status_counts.items()]
|
|
)
|
|
generator.add_chart(chart_data)
|
|
|
|
if include_users:
|
|
user_data = UserReportBuilder.build_users_report()
|
|
generator.add_data_section("Benutzer", user_data['data'], user_data['headers'])
|
|
|
|
if include_printers:
|
|
printer_data = PrinterReportBuilder.build_printers_report()
|
|
generator.add_data_section("Drucker", printer_data['data'], printer_data['headers'])
|
|
|
|
# Report generieren
|
|
output = io.BytesIO()
|
|
success = generator.generate(output)
|
|
|
|
if success:
|
|
output.seek(0)
|
|
return output.getvalue()
|
|
else:
|
|
raise Exception("Report-Generierung fehlgeschlagen")
|
|
|
|
# Zusätzliche Abhängigkeiten zu requirements.txt hinzufügen
|
|
ADDITIONAL_REQUIREMENTS = [
|
|
"reportlab>=4.0.0",
|
|
"xlsxwriter>=3.0.0"
|
|
] |