manage-your-printer/utils/report_generator.py
2025-06-04 10:03:22 +02:00

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"
]