""" Reusable PDF Generation Service for Clinical Documents This module provides a base PDF generator that can be extended by all clinical modules to create consistent, professional PDFs with bilingual support (English/Arabic). Features: - Tenant branding (logo + name) - Arabic font support - Bilingual labels and content - Consistent styling - Email functionality """ from io import BytesIO import os from typing import List, Dict, Any, Optional, Tuple from django.conf import settings from django.http import HttpResponse from django.core.mail import EmailMessage from django.utils import timezone from django.utils.translation import gettext as _ from reportlab.lib.pagesizes import A4 from reportlab.lib.units import inch from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont import arabic_reshaper from bidi.algorithm import get_display class BasePDFGenerator: """ Base class for generating PDFs with consistent styling and bilingual support. All clinical document PDF generators should extend this class and implement the abstract methods to customize content. """ # Class-level flag for Arabic font availability ARABIC_FONT_AVAILABLE = False ARABIC_FONT_REGISTERED = False def __init__(self, document, request=None): """ Initialize PDF generator. Args: document: The document object (appointment, consultation, etc.) request: Optional HTTP request object for user context """ self.document = document self.request = request self.buffer = BytesIO() self.elements = [] self.styles = getSampleStyleSheet() # Register Arabic font if not already done if not BasePDFGenerator.ARABIC_FONT_REGISTERED: self._register_arabic_font() @classmethod def _register_arabic_font(cls): """Register Arabic font for PDF generation.""" try: pdfmetrics.registerFont(TTFont('Arabic', '/System/Library/Fonts/SFArabic.ttf')) cls.ARABIC_FONT_AVAILABLE = True cls.ARABIC_FONT_REGISTERED = True except Exception: cls.ARABIC_FONT_AVAILABLE = False cls.ARABIC_FONT_REGISTERED = True @staticmethod def format_arabic(text: str) -> str: """ Format Arabic text for proper display in PDF. Args: text: Text containing Arabic characters Returns: str: Reshaped text ready for PDF rendering """ if not text: return "" try: reshaped_text = arabic_reshaper.reshape(text) return get_display(reshaped_text) except Exception: return text def create_custom_styles(self): """Create custom paragraph styles for the PDF.""" self.title_style = ParagraphStyle( 'CustomTitle', parent=self.styles['Heading1'], fontSize=18, textColor=colors.HexColor('#0d6efd'), spaceAfter=20, alignment=TA_CENTER ) self.heading_style = ParagraphStyle( 'CustomHeading', parent=self.styles['Heading2'], fontSize=14, textColor=colors.HexColor('#212529'), spaceAfter=12, spaceBefore=12 ) self.cell_style = ParagraphStyle( 'Cell', parent=self.styles['Normal'], fontSize=10 ) self.label_style = ParagraphStyle( 'Label', parent=self.styles['Normal'], fontSize=10, fontName='Helvetica-Bold' ) def add_header(self, tenant): """ Add header with tenant logo and name. Args: tenant: Tenant object """ # Try to load logo logo = self._get_tenant_logo(tenant) if logo: # Header with logo tenant_info_html = f'{tenant.name}
' if tenant.name_ar and self.ARABIC_FONT_AVAILABLE: tenant_info_html += f'{self.format_arabic(tenant.name_ar)}
' header_data = [[logo, Paragraph(tenant_info_html, ParagraphStyle( 'TenantInfo', parent=self.styles['Normal'], fontSize=12, alignment=TA_CENTER ))]] header_table = Table(header_data, colWidths=[2*inch, 4*inch]) header_table.setStyle(TableStyle([ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('ALIGN', (0, 0), (0, 0), 'LEFT'), ('ALIGN', (1, 0), (1, 0), 'CENTER'), ])) self.elements.append(header_table) else: # Header without logo tenant_name_html = f'{tenant.name}
' if tenant.name_ar and self.ARABIC_FONT_AVAILABLE: tenant_name_html += f'{self.format_arabic(tenant.name_ar)}' tenant_name = Paragraph( tenant_name_html, ParagraphStyle('TenantName', parent=self.styles['Heading1'], fontSize=16, alignment=TA_CENTER) ) self.elements.append(tenant_name) self.elements.append(Spacer(1, 0.15*inch)) def _get_tenant_logo(self, tenant) -> Optional[Image]: """ Get tenant logo as ReportLab Image object. Args: tenant: Tenant object Returns: Image object or None """ try: from core.models import TenantSetting logo_setting = TenantSetting.objects.filter( tenant=tenant, template__key='basic_logo' ).first() if logo_setting and logo_setting.file_value: logo_path = os.path.join(settings.MEDIA_ROOT, str(logo_setting.file_value)) if os.path.exists(logo_path): return Image(logo_path, width=0.8*inch, height=0.8*inch) except Exception: pass return None def add_title(self, title_en: str, title_ar: str = ""): """ Add bilingual title to PDF. Args: title_en: Title in English title_ar: Title in Arabic (optional) """ title_html = f"{title_en}
" if title_ar and self.ARABIC_FONT_AVAILABLE: title_html += f'{self.format_arabic(title_ar)}' title = Paragraph(title_html, self.title_style) self.elements.append(title) self.elements.append(Spacer(1, 0.15*inch)) def add_section_heading(self, heading_en: str, heading_ar: str = ""): """ Add bilingual section heading. Args: heading_en: Heading in English heading_ar: Heading in Arabic (optional) """ heading_html = f"{heading_en}" if heading_ar and self.ARABIC_FONT_AVAILABLE: heading_html += f' / {self.format_arabic(heading_ar)}' self.elements.append(Paragraph(heading_html, self.heading_style)) def create_bilingual_table(self, data: List[Tuple[str, str, str, str]], col_widths: List[float] = None): """ Create a table with bilingual labels. Args: data: List of tuples (label_en, label_ar, value, value_ar) col_widths: Column widths in inches Returns: Table object """ if col_widths is None: col_widths = [2.5*inch, 3.5*inch] table_data = [] for label_en, label_ar, value, value_ar in data: # Create label with bilingual support label_html = label_en if label_ar and self.ARABIC_FONT_AVAILABLE: label_html += f' / {self.format_arabic(label_ar)}' # Create value with bilingual support value_html = str(value) if value_ar and self.ARABIC_FONT_AVAILABLE: value_html += f' / {self.format_arabic(value_ar)}' table_data.append([ Paragraph(label_html + ':', self.label_style), Paragraph(value_html, self.cell_style) ]) table = Table(table_data, colWidths=col_widths) table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (0, -1), colors.HexColor('#f8f9fa')), ('TEXTCOLOR', (0, 0), (-1, -1), colors.black), ('ALIGN', (0, 0), (0, -1), 'RIGHT'), ('ALIGN', (1, 0), (1, -1), 'LEFT'), ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'), ('FONTNAME', (1, 0), (1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 10), ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('LEFTPADDING', (0, 0), (-1, -1), 8), ('RIGHTPADDING', (0, 0), (-1, -1), 8), ('TOPPADDING', (0, 0), (-1, -1), 6), ('BOTTOMPADDING', (0, 0), (-1, -1), 6), ])) return table def add_footer(self): """Add footer with generation timestamp.""" self.elements.append(Spacer(1, 0.5*inch)) footer_text = f"Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}" footer = Paragraph(footer_text, ParagraphStyle( 'Footer', parent=self.styles['Normal'], fontSize=8, textColor=colors.grey, alignment=TA_CENTER )) self.elements.append(footer) # Abstract methods to be implemented by subclasses def get_document_title(self) -> Tuple[str, str]: """ Get document title in English and Arabic. Returns: Tuple of (title_en, title_ar) """ raise NotImplementedError("Subclasses must implement get_document_title()") def get_document_sections(self) -> List[Dict[str, Any]]: """ Get document sections to be rendered. Returns: List of section dictionaries with keys: - heading_en: Section heading in English - heading_ar: Section heading in Arabic - content: Section content (table data or paragraphs) - type: 'table' or 'text' """ raise NotImplementedError("Subclasses must implement get_document_sections()") def generate_pdf(self, view_mode: str = 'download') -> HttpResponse: """ Generate PDF and return as HTTP response. Args: view_mode: 'inline' for browser viewing, 'download' for download Returns: HttpResponse with PDF content """ # Create PDF document doc = SimpleDocTemplate( self.buffer, pagesize=A4, rightMargin=0.75*inch, leftMargin=0.75*inch, topMargin=1.5*inch, bottomMargin=0.75*inch ) # Create custom styles self.create_custom_styles() # Add header tenant = getattr(self.document, 'tenant', None) if tenant: self.add_header(tenant) # Add title title_en, title_ar = self.get_document_title() self.add_title(title_en, title_ar) # Add sections sections = self.get_document_sections() for section in sections: # Add section heading self.add_section_heading( section.get('heading_en', ''), section.get('heading_ar', '') ) # Add section content if section.get('type') == 'table': table = self.create_bilingual_table( section.get('content', []), section.get('col_widths') ) self.elements.append(table) elif section.get('type') == 'text': for text in section.get('content', []): para = Paragraph(text, self.cell_style) self.elements.append(para) self.elements.append(Spacer(1, 0.1*inch)) self.elements.append(Spacer(1, 0.3*inch)) # Add footer self.add_footer() # Build PDF doc.build(self.elements) # Get PDF content pdf_content = self.buffer.getvalue() self.buffer.close() # Create HTTP response response = HttpResponse(content_type='application/pdf') # Set content disposition based on view mode filename = self.get_pdf_filename() if view_mode == 'inline': response['Content-Disposition'] = f'inline; filename="{filename}"' else: response['Content-Disposition'] = f'attachment; filename="{filename}"' response.write(pdf_content) return response def get_pdf_filename(self) -> str: """ Get PDF filename. Returns: str: Filename for the PDF """ # Default implementation - subclasses should override return f"document_{timezone.now().strftime('%Y%m%d_%H%M%S')}.pdf" def send_email(self, email_address: str, subject: str, body: str, custom_message: str = "") -> Tuple[bool, str]: """ Send PDF via email. Args: email_address: Recipient email address subject: Email subject body: Email body custom_message: Optional custom message to append Returns: Tuple of (success: bool, message: str) """ try: # Generate PDF content self.buffer = BytesIO() self.elements = [] # Create PDF document doc = SimpleDocTemplate( self.buffer, pagesize=A4, rightMargin=0.75*inch, leftMargin=0.75*inch, topMargin=1.5*inch, bottomMargin=0.75*inch ) # Create custom styles self.create_custom_styles() # Add header tenant = getattr(self.document, 'tenant', None) if tenant: self.add_header(tenant) # Add title title_en, title_ar = self.get_document_title() self.add_title(title_en, title_ar) # Add sections sections = self.get_document_sections() for section in sections: self.add_section_heading( section.get('heading_en', ''), section.get('heading_ar', '') ) if section.get('type') == 'table': table = self.create_bilingual_table( section.get('content', []), section.get('col_widths') ) self.elements.append(table) elif section.get('type') == 'text': for text in section.get('content', []): para = Paragraph(text, self.cell_style) self.elements.append(para) self.elements.append(Spacer(1, 0.1*inch)) self.elements.append(Spacer(1, 0.3*inch)) # Add footer self.add_footer() # Build PDF doc.build(self.elements) # Get PDF content pdf_content = self.buffer.getvalue() self.buffer.close() # Append custom message if provided if custom_message: body += f"\n\n{custom_message}\n\n" # Create email email = EmailMessage( subject=subject, body=body, from_email=None, # Uses DEFAULT_FROM_EMAIL from settings to=[email_address], ) # Attach PDF email.attach( self.get_pdf_filename(), pdf_content, 'application/pdf' ) # Send email email.send(fail_silently=False) return True, _('Email sent successfully!') except Exception as e: return False, str(e)