""" PDF generation utilities with corporate identity support """ import base64 import os from io import BytesIO from django.conf import settings from django.http import HttpResponse from django.template.loader import render_to_string from django.utils import timezone # Try to import WeasyPrint, fall back gracefully if not available try: from weasyprint import CSS, HTML from weasyprint.text.fonts import FontConfiguration WEASYPRINT_AVAILABLE = True IMPORT_ERROR = None except ImportError as e: # WeasyPrint dependencies not available HTML = None CSS = None FontConfiguration = None WEASYPRINT_AVAILABLE = False IMPORT_ERROR = str(e) except OSError as e: # System dependencies missing (like pango) HTML = None CSS = None FontConfiguration = None WEASYPRINT_AVAILABLE = False IMPORT_ERROR = str(e) from stiftung.models import AppConfiguration class PDFGenerator: """Corporate identity PDF generator""" def __init__(self): if WEASYPRINT_AVAILABLE: self.font_config = FontConfiguration() else: self.font_config = None def is_available(self): """Check if PDF generation is available""" return WEASYPRINT_AVAILABLE def get_corporate_settings(self): """Get corporate identity settings from configuration""" return { "stiftung_name": AppConfiguration.get_setting( "corporate_stiftung_name", "Stiftung" ), "logo_path": AppConfiguration.get_setting("corporate_logo_path", ""), "primary_color": AppConfiguration.get_setting( "corporate_primary_color", "#2c3e50" ), "secondary_color": AppConfiguration.get_setting( "corporate_secondary_color", "#3498db" ), "address_line1": AppConfiguration.get_setting( "corporate_address_line1", "" ), "address_line2": AppConfiguration.get_setting( "corporate_address_line2", "" ), "phone": AppConfiguration.get_setting("corporate_phone", ""), "email": AppConfiguration.get_setting("corporate_email", ""), "website": AppConfiguration.get_setting("corporate_website", ""), "footer_text": AppConfiguration.get_setting( "corporate_footer_text", "Dieser Bericht wurde automatisch generiert." ), } def get_logo_base64(self, logo_path): """Convert logo to base64 for embedding in PDF""" if not logo_path: return None # Try different possible paths possible_paths = [ logo_path, os.path.join(settings.MEDIA_ROOT, logo_path), os.path.join(settings.STATIC_ROOT or "", logo_path), os.path.join(settings.BASE_DIR, "static", logo_path), ] for path in possible_paths: if os.path.exists(path): try: with open(path, "rb") as img_file: img_data = base64.b64encode(img_file.read()).decode("utf-8") # Determine MIME type ext = os.path.splitext(path)[1].lower() if ext in [".jpg", ".jpeg"]: mime_type = "image/jpeg" elif ext == ".png": mime_type = "image/png" elif ext == ".svg": mime_type = "image/svg+xml" else: mime_type = "image/png" # default return f"data:{mime_type};base64,{img_data}" except Exception: continue return None def get_base_css(self, corporate_settings): """Generate base CSS for corporate identity""" primary_color = corporate_settings.get("primary_color", "#2c3e50") secondary_color = corporate_settings.get("secondary_color", "#3498db") return f""" @page {{ size: A4; margin: 2cm 1.5cm 2cm 1.5cm; @bottom-center {{ content: "Seite " counter(page) " von " counter(pages); font-size: 10pt; color: #666; }} }} body {{ font-family: 'Segoe UI', 'DejaVu Sans', Arial, sans-serif; font-size: 10pt; line-height: 1.4; color: #333; margin: 0; padding: 0; }} .header {{ border-bottom: 2px solid {primary_color}; padding-bottom: 15px; margin-bottom: 25px; page-break-inside: avoid; }} .header-content {{ display: flex; justify-content: space-between; align-items: flex-start; }} .header-left {{ flex: 1; }} .header-right {{ text-align: right; flex-shrink: 0; margin-left: 20px; }} .logo {{ max-height: 60px; max-width: 150px; margin-bottom: 10px; }} .stiftung-name {{ font-size: 20pt; font-weight: bold; color: {primary_color}; margin: 0; line-height: 1.2; }} .document-title {{ font-size: 16pt; color: {secondary_color}; margin: 5px 0 0 0; }} .header-info {{ font-size: 9pt; color: #666; margin-top: 10px; }} .contact-info {{ font-size: 8pt; color: #666; line-height: 1.3; }} h1, h2, h3 {{ color: {primary_color}; page-break-inside: avoid; page-break-after: avoid; }} h1 {{ font-size: 14pt; margin: 20px 0 15px 0; border-bottom: 1px solid {secondary_color}; padding-bottom: 5px; }} h2 {{ font-size: 12pt; margin: 15px 0 10px 0; }} h3 {{ font-size: 11pt; margin: 10px 0 8px 0; }} table {{ width: 100%; border-collapse: collapse; margin: 10px 0; page-break-inside: avoid; }} th, td {{ border: 1px solid #ddd; padding: 6px 8px; text-align: left; vertical-align: top; }} th {{ background-color: #f8f9fa; font-weight: 600; color: {primary_color}; }} tr:nth-child(even) {{ background-color: #f9f9f9; }} .amount {{ text-align: right; font-family: 'Courier New', monospace; font-weight: 500; }} .status-badge {{ padding: 2px 6px; border-radius: 3px; font-size: 8pt; font-weight: 500; }} .status-beantragt {{ background-color: #fff3cd; color: #856404; }} .status-genehmigt {{ background-color: #d1ecf1; color: #0c5460; }} .status-ausgezahlt {{ background-color: #d4edda; color: #155724; }} .status-abgelehnt {{ background-color: #f8d7da; color: #721c24; }} .status-storniert {{ background-color: #e2e3e5; color: #383d41; }} .footer {{ margin-top: 30px; padding-top: 15px; border-top: 1px solid #ddd; font-size: 8pt; color: #666; text-align: center; }} .stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; margin: 15px 0; }} .stat-card {{ border: 1px solid #ddd; border-radius: 4px; padding: 12px; text-align: center; background: #f8f9fa; }} .stat-value {{ font-size: 14pt; font-weight: bold; color: {primary_color}; }} .stat-label {{ font-size: 8pt; color: #666; margin-top: 3px; }} .section {{ margin-bottom: 20px; page-break-inside: avoid; }} .no-page-break {{ page-break-inside: avoid; }} .page-break-before {{ page-break-before: always; }} """ def generate_pdf_response(self, html_content, filename, css_content=None): """Generate PDF response from HTML content""" if not WEASYPRINT_AVAILABLE: # Return HTML fallback if WeasyPrint is not available error_html = f"""
WeasyPrint dependencies are missing: {IMPORT_ERROR}
Showing content as HTML preview instead. You can print this page to PDF using your browser.
An error occurred while generating the PDF:
{str(e)}
Showing content as HTML preview instead.