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