""" PDF generation utilities with corporate identity support """ import os import base64 from io import BytesIO from django.conf import settings from django.template.loader import render_to_string from django.http import HttpResponse from django.utils import timezone # Try to import WeasyPrint, fall back gracefully if not available try: from weasyprint import HTML, CSS 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""" PDF Export - {filename}

⚠️ PDF Generation Not Available

WeasyPrint dependencies are missing: {IMPORT_ERROR}

Showing content as HTML preview instead. You can print this page to PDF using your browser.

{html_content}
""" response = HttpResponse(error_html, content_type='text/html') response['Content-Disposition'] = f'inline; filename="{filename.replace(".pdf", "_preview.html")}"' return response try: # Create CSS string if css_content: css = CSS(string=css_content, font_config=self.font_config) else: css = None # Generate PDF html_doc = HTML(string=html_content) pdf_bytes = html_doc.write_pdf(stylesheets=[css] if css else None, font_config=self.font_config) # Create response response = HttpResponse(pdf_bytes, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="{filename}"' return response except Exception as e: # Fallback: return error message as HTML error_html = f""" PDF Generation Error

PDF Generation Error

An error occurred while generating the PDF:

{str(e)}

Showing content as HTML preview instead.

Original Content

{html_content}
""" response = HttpResponse(error_html, content_type='text/html') response['Content-Disposition'] = f'inline; filename="error_{filename.replace(".pdf", ".html")}"' return response def export_data_list_pdf(self, data, fields_config, title, filename_prefix, request_user=None): """ Export a list of data as formatted PDF Args: data: QuerySet or list of model instances fields_config: dict with field names as keys and display names as values title: Document title filename_prefix: Prefix for the generated filename request_user: User making the request (for audit purposes) """ corporate_settings = self.get_corporate_settings() logo_base64 = self.get_logo_base64(corporate_settings.get('logo_path', '')) # Prepare context context = { 'corporate_settings': corporate_settings, 'logo_base64': logo_base64, 'title': title, 'data': data, 'fields_config': fields_config, 'generation_date': timezone.now(), 'generated_by': (request_user.get_full_name() if hasattr(request_user, 'get_full_name') and request_user.get_full_name() else request_user.username if hasattr(request_user, 'username') and request_user.username else 'System'), 'total_count': len(data) if hasattr(data, '__len__') else data.count(), } # Render HTML html_content = render_to_string('pdf/data_list.html', context) # Generate CSS css_content = self.get_base_css(corporate_settings) # Generate filename timestamp = timezone.now().strftime('%Y%m%d_%H%M%S') filename = f"{filename_prefix}_{timestamp}.pdf" return self.generate_pdf_response(html_content, filename, css_content) # Global instance pdf_generator = PDFGenerator()