From ef3464569123d8f91a7300490a818d0ca299e398 Mon Sep 17 00:00:00 2001 From: matin Date: Tue, 30 Sep 2025 16:55:49 +0200 Subject: =?UTF-8?q?Berichfunktion=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + calendar_gui.py | 31 +++++++++ prediction_report_service.py | 158 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 prediction_report_service.py diff --git a/.gitignore b/.gitignore index 8691330..ec38f61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ *.json +*.pdf \ No newline at end of file diff --git a/calendar_gui.py b/calendar_gui.py index f0edb52..ab0cf65 100644 --- a/calendar_gui.py +++ b/calendar_gui.py @@ -16,6 +16,7 @@ from prediction_controller import PredictionController from event_type_handler import EventTypeHandler from date_service import DateService from config import EventConfig +from prediction_report_service import PredictionReportService class EventDialog(QDialog): def __init__(self, entry=None, parent=None): @@ -230,6 +231,14 @@ class CalendarManagerGUI(QMainWindow): load_action.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) load_action.triggered.connect(self.load_file) file_menu.addAction(load_action) + + # Export PDF action + export_pdf_action = QAction('Als PDF exportieren', self) + export_pdf_action.setShortcut('Ctrl+P') + # Fallback icon; SP_DialogPrintButton may not exist in some Qt styles + export_pdf_action.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) + export_pdf_action.triggered.connect(self.export_pdf) + file_menu.addAction(export_pdf_action) # Note: Saving/Loading now operate on complete prediction state @@ -643,6 +652,28 @@ class CalendarManagerGUI(QMainWindow): except Exception as e: QMessageBox.critical(self, "Error", f"Fehler beim Laden des Komplettzustands: {str(e)}") + def export_pdf(self): + """Export a formatted report to PDF including metadata and full table.""" + out_path, _ = QFileDialog.getSaveFileName( + self, + "PDF exportieren", + "", + "PDF (*.pdf)" + ) + if not out_path: + return + success = PredictionReportService.export_pdf( + self.calendar_manager, + self.prediction_controller, + out_path, + self.dateformat, + ) + if success: + if not out_path.lower().endswith('.pdf'): + out_path = out_path + '.pdf' + QMessageBox.information(self, "Export erfolgreich", f"PDF exportiert nach {out_path}") + else: + QMessageBox.critical(self, "Fehler", "PDF-Export fehlgeschlagen.") def clear_entries(self): """Clear all calendar entries""" diff --git a/prediction_report_service.py b/prediction_report_service.py new file mode 100644 index 0000000..b7e3620 --- /dev/null +++ b/prediction_report_service.py @@ -0,0 +1,158 @@ +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.append(""" + + """) + html.append("") + html.append("

Ausfallzeitenrechner — Bericht

") + html.append("
") + html.append(f"
Promotionsdatum: {launch_str}
") + html.append(f"
Bewerbungszeitraum: {duration_str}
") + html.append(f"
Bewerbungsfrist: {prediction_str}
") + html.append("
") + + # Table header + html.append("") + html.append( + "" + "" + "" + "" + "" + "" + "" + "" + "" + ) + + # 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"" + f"" + f"" + f"" + f"" + f"" + f"" + ) + + html.append("") + html.append(f"") + html.append("
BeginnEndeArtKommentarBeginn AnrechnungEnde AnrechnungAnrechnungszeitraum
{esc(start_text)}{esc(end_text)}{esc(keyword_text)}{esc(commentary_text)}{esc(c_start_text)}{esc(c_end_text)}{esc(time_text)}
Summe Anrechnungszeitraum: {sum_str}
") + + html.append(f"
Erstellt am: {created_stamp}
") + html.append("") + + # 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 + + -- cgit v1.1