🎉 Refactor & Update Backend Code, Add Utils 🖥️📊
This commit is contained in:
909
backend/utils/report_generator.py
Normal file
909
backend/utils/report_generator.py
Normal file
@@ -0,0 +1,909 @@
|
||||
"""
|
||||
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[Drawing]:
|
||||
"""Erstellt ein Diagramm-Drawing"""
|
||||
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"
|
||||
]
|
Reference in New Issue
Block a user