summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--prediction_report_service.py350
1 files changed, 192 insertions, 158 deletions
diff --git a/prediction_report_service.py b/prediction_report_service.py
index b7e3620..4b2e521 100644
--- a/prediction_report_service.py
+++ b/prediction_report_service.py
@@ -1,158 +1,192 @@
-from datetime import datetime
-from typing import List
-
-from PyQt5.QtGui import QTextDocument
-from PyQt5.QtPrintSupport import QPrinter
-
-from calendar_manager import CalendarManager
-from prediction_controller import PredictionController
-from date_service import DateService
-
-
-class PredictionReportService:
- """Service to export the current prediction and entries into a PDF report."""
-
- @staticmethod
- def export_pdf(calendar_manager: CalendarManager,
- prediction_controller: PredictionController,
- out_path: str,
- dateformat: str = "dd.MM.yyyy") -> bool:
- try:
- # Gather data
- launch_dt = prediction_controller.get_launch_date()
- duration = prediction_controller.get_duration()
- prediction_dt = prediction_controller.get_prediction()
- entries = calendar_manager.list_entries()
-
- # Format helpers
- def fmt_date(dt):
- return DateService.format_date_for_display(dt, dateformat) if dt else ""
-
- launch_str = fmt_date(launch_dt)
- duration_str = f"{duration.years} Jahre" if duration and hasattr(duration, 'years') else ""
- prediction_str = fmt_date(prediction_dt)
-
- # Sum time_periods (months and days separately)
- total_months = 0
- total_days = 0
- for e in entries:
- tp = getattr(e, 'time_period', None)
- if not tp:
- continue
- try:
- parts = tp.split()
- if len(parts) >= 2:
- value = int(parts[0])
- unit = parts[1].lower()
- if value > 0:
- if 'monat' in unit:
- total_months += value
- elif 'tag' in unit:
- total_days += value
- except Exception:
- # Ignore unparsable time_periods
- pass
-
- sum_parts = []
- if total_months > 0:
- sum_parts.append(f"{total_months} Monate")
- if total_days > 0:
- sum_parts.append(f"{total_days} Tage")
- sum_str = ", ".join(sum_parts) if sum_parts else "0"
-
- # Build HTML
- created_stamp = datetime.now().strftime('%d.%m.%Y %H:%M:%S')
- html = []
- html.append("<html><head><meta charset='utf-8'>")
- html.append("""
- <style>
- body { font-family: Arial, sans-serif; color: #222; }
- h1 { font-size: 20px; margin-bottom: 6px; }
- .meta { margin-bottom: 12px; }
- .meta div { margin: 2px 0; }
- table { border-collapse: collapse; width: 100%; }
- th, td { border: 1px solid #e1e4ee; padding: 4px 6px; vertical-align: top; font-size: 150px; line-height: 1.3; }
- th { background: #f5f7fb; text-align: left; }
- tfoot td { font-weight: bold; }
- .stamp { margin-top: 16px; font-size: 10px; color: #666; }
- </style>
- """)
- html.append("</head><body>")
- html.append("<h1>Ausfallzeitenrechner — Bericht</h1>")
- html.append("<div class='meta'>")
- html.append(f"<div><strong>Promotionsdatum:</strong> {launch_str}</div>")
- html.append(f"<div><strong>Bewerbungszeitraum:</strong> {duration_str}</div>")
- html.append(f"<div><strong>Bewerbungsfrist:</strong> {prediction_str}</div>")
- html.append("</div>")
-
- # Table header
- html.append("<table>")
- html.append(
- "<thead><tr>"
- "<th>Beginn</th>"
- "<th>Ende</th>"
- "<th>Art</th>"
- "<th>Kommentar</th>"
- "<th>Beginn Anrechnung</th>"
- "<th>Ende Anrechnung</th>"
- "<th>Anrechnungszeitraum</th>"
- "</tr></thead><tbody>"
- )
-
- # Rows
- for e in entries:
- start_text = fmt_date(e.start_date)
- end_text = fmt_date(e.end_date)
- keyword_text = e.keyword
- commentary_text = getattr(e, 'commentary', '') or ''
- c_start_text = fmt_date(getattr(e, 'corrected_start_date', None))
- c_end_text = fmt_date(getattr(e, 'corrected_end_date', None))
- time_text = getattr(e, 'time_period', '') or ''
-
- # Escape
- def esc(s: str) -> str:
- return (s.replace('&', '&amp;')
- .replace('<', '&lt;')
- .replace('>', '&gt;'))
-
- html.append(
- f"<tr><td>{esc(start_text)}</td>"
- f"<td>{esc(end_text)}</td>"
- f"<td>{esc(keyword_text)}</td>"
- f"<td>{esc(commentary_text)}</td>"
- f"<td>{esc(c_start_text)}</td>"
- f"<td>{esc(c_end_text)}</td>"
- f"<td>{esc(time_text)}</td></tr>"
- )
-
- html.append("</tbody>")
- html.append(f"<tfoot><tr><td colspan='7'>Summe Anrechnungszeitraum: {sum_str}</td></tr></tfoot>")
- html.append("</table>")
-
- html.append(f"<div class='stamp'>Erstellt am: {created_stamp}</div>")
- html.append("</body></html>")
-
- # Ensure extension
- if not out_path.lower().endswith('.pdf'):
- out_path = out_path + '.pdf'
-
- # Render HTML to PDF
- printer = QPrinter(QPrinter.HighResolution)
- printer.setOutputFormat(QPrinter.PdfFormat)
- printer.setOutputFileName(out_path)
- # Try to set margins (not all bindings support Millimeter overload)
- try:
- printer.setPageMargins(12, 12, 12, 12, QPrinter.Millimeter)
- except Exception:
- pass
-
- doc = QTextDocument()
- doc.setHtml("".join(html))
- doc.print_(printer)
-
- return True
- except Exception as e:
- print(f"Error exporting PDF: {e}")
- return False
-
-
+from datetime import datetime
+from typing import List
+from io import BytesIO
+
+from xhtml2pdf import pisa
+
+from calendar_manager import CalendarManager
+from prediction_controller import PredictionController
+from date_service import DateService
+
+
+class PredictionReportService:
+ """Service to export the current prediction and entries into a PDF report."""
+
+ @staticmethod
+ def export_pdf(calendar_manager: CalendarManager,
+ prediction_controller: PredictionController,
+ out_path: str,
+ dateformat: str = "dd.MM.yyyy") -> bool:
+ try:
+ # Gather data
+ launch_dt = prediction_controller.get_launch_date()
+ duration = prediction_controller.get_duration()
+ prediction_dt = prediction_controller.get_prediction()
+ entries = calendar_manager.list_entries()
+
+ # Format helpers
+ def fmt_date(dt):
+ return DateService.format_date_for_display(dt, dateformat) if dt else ""
+
+ launch_str = fmt_date(launch_dt)
+ duration_str = f"{duration.years} Jahre" if duration and hasattr(duration, 'years') else ""
+ prediction_str = fmt_date(prediction_dt)
+
+ # Sum time_periods (months and days separately)
+ total_months = 0
+ total_days = 0
+ for e in entries:
+ tp = getattr(e, 'time_period', None)
+ if not tp:
+ continue
+ try:
+ parts = tp.split()
+ if len(parts) >= 2:
+ value = int(parts[0])
+ unit = parts[1].lower()
+ if value > 0:
+ if 'monat' in unit:
+ total_months += value
+ elif 'tag' in unit:
+ total_days += value
+ except Exception:
+ # Ignore unparsable time_periods
+ pass
+
+ sum_parts = []
+ if total_months > 0:
+ sum_parts.append(f"{total_months} Monate")
+ if total_days > 0:
+ sum_parts.append(f"{total_days} Tage")
+ sum_str = ", ".join(sum_parts) if sum_parts else "0"
+
+ # Build HTML
+ created_stamp = datetime.now().strftime('%d.%m.%Y %H:%M:%S')
+ html = []
+ html.append("<!DOCTYPE html>")
+ html.append("<html><head><meta charset='utf-8'>")
+ html.append("""
+ <style>
+ @page {
+ size: A4;
+ margin: 1.5cm;
+ }
+ body {
+ font-family: Arial, sans-serif;
+ color: #222;
+ font-size: 10pt;
+ }
+ h1 {
+ font-size: 14pt;
+ margin-bottom: 8px;
+ }
+ .meta {
+ margin-bottom: 16px;
+ }
+ .meta div {
+ margin: 3px 0;
+ font-size: 10pt;
+ }
+ table {
+ border-collapse: collapse;
+ width: 100%;
+ margin-top: 10px;
+ }
+ th, td {
+ border: 1px solid #e1e4ee;
+ padding: 3px 4px;
+ vertical-align: top;
+ font-size: 9pt;
+ line-height: 1.4;
+ }
+ th {
+ background-color: #f5f7fb;
+ text-align: left;
+ font-weight: bold;
+ }
+ tfoot td {
+ font-weight: bold;
+ background-color: #fafafa;
+ }
+ .stamp {
+ margin-top: 20px;
+ font-size: 8pt;
+ color: #666;
+ }
+ </style>
+ """)
+ html.append("</head><body>")
+ html.append("<h1>Ausfallzeitenrechner — Bericht</h1>")
+ html.append("<div class='meta'>")
+ html.append(f"<div><strong>Promotionsdatum:</strong> {launch_str}</div>")
+ html.append(f"<div><strong>Bewerbungszeitraum:</strong> {duration_str}</div>")
+ html.append(f"<div><strong>Bewerbungsfrist:</strong> {prediction_str}</div>")
+ html.append("</div>")
+
+ # Table header
+ html.append("<table>")
+ html.append(
+ "<thead><tr>"
+ "<th>Beginn</th>"
+ "<th>Ende</th>"
+ "<th>Art</th>"
+ "<th>Kommentar</th>"
+ "<th>Beginn Anrechnung</th>"
+ "<th>Ende Anrechnung</th>"
+ "<th>Zeitraum</th>"
+ "</tr></thead><tbody>"
+ )
+
+ # Rows
+ for e in entries:
+ start_text = fmt_date(e.start_date)
+ end_text = fmt_date(e.end_date)
+ keyword_text = e.keyword
+ commentary_text = getattr(e, 'commentary', '') or ''
+ c_start_text = fmt_date(getattr(e, 'corrected_start_date', None))
+ c_end_text = fmt_date(getattr(e, 'corrected_end_date', None))
+ time_text = getattr(e, 'time_period', '') or ''
+
+ # Escape HTML special characters
+ def esc(s: str) -> str:
+ return (s.replace('&', '&amp;')
+ .replace('<', '&lt;')
+ .replace('>', '&gt;')
+ .replace('"', '&quot;'))
+
+ html.append(
+ f"<tr><td>{esc(start_text)}</td>"
+ f"<td>{esc(end_text)}</td>"
+ f"<td>{esc(keyword_text)}</td>"
+ f"<td>{esc(commentary_text)}</td>"
+ f"<td>{esc(c_start_text)}</td>"
+ f"<td>{esc(c_end_text)}</td>"
+ f"<td>{esc(time_text)}</td></tr>"
+ )
+
+ html.append("</tbody>")
+ html.append(f"<tfoot><tr><td colspan='7'>Summe Anrechnungszeitraum: {sum_str}</td></tr></tfoot>")
+ html.append("</table>")
+
+ html.append(f"<div class='stamp'>Erstellt am: {created_stamp}</div>")
+ html.append("</body></html>")
+
+ # Ensure extension
+ if not out_path.lower().endswith('.pdf'):
+ out_path = out_path + '.pdf'
+
+ # Generate PDF using xhtml2pdf
+ html_content = "".join(html)
+
+ with open(out_path, "wb") as pdf_file:
+ pisa_status = pisa.CreatePDF(
+ html_content,
+ dest=pdf_file,
+ encoding='utf-8'
+ )
+
+ return not pisa_status.err
+
+ except Exception as e:
+ print(f"Error exporting PDF: {e}")
+ return False \ No newline at end of file