diff options
| author | matin <matin.kaufmann@gmail.com> | 2025-10-04 17:57:39 +0200 |
|---|---|---|
| committer | matin <matin.kaufmann@gmail.com> | 2025-10-04 17:57:39 +0200 |
| commit | 4b216267034d767c27001f54d6df5327e4be149f (patch) | |
| tree | 4f3a5fd6513245c8e22383d4ccbf28fa78c4348d | |
| parent | 1edb9c91bc1c61904b86d8f0e05527d1af6e50cc (diff) | |
changed pdf printer to xhtml2pdfdevelopement
| -rw-r--r-- | prediction_report_service.py | 350 |
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('&', '&') - .replace('<', '<') - .replace('>', '>')) - - 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('&', '&')
+ .replace('<', '<')
+ .replace('>', '>')
+ .replace('"', '"'))
+
+ 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 |
