""" Reports models - Custom Report Builder for PX360. Supports creating, saving, and scheduling custom reports across multiple data sources (Complaints, Inquiries, Observations, etc.) """ from django.conf import settings from django.db import models from django.utils import timezone from apps.core.models import TimeStampedModel, UUIDModel class DataSource(models.TextChoices): """Available data sources for reports.""" COMPLAINTS = 'complaints', 'Complaints' INQUIRIES = 'inquiries', 'Inquiries' OBSERVATIONS = 'observations', 'Observations' PX_ACTIONS = 'px_actions', 'PX Actions' SURVEYS = 'surveys', 'Surveys' PHYSICIANS = 'physicians', 'Physician Ratings' class ReportFrequency(models.TextChoices): """Report schedule frequency options.""" ONCE = 'once', 'One Time' DAILY = 'daily', 'Daily' WEEKLY = 'weekly', 'Weekly' MONTHLY = 'monthly', 'Monthly' QUARTERLY = 'quarterly', 'Quarterly' class ReportFormat(models.TextChoices): """Export format options.""" HTML = 'html', 'View Online' PDF = 'pdf', 'PDF' EXCEL = 'excel', 'Excel' CSV = 'csv', 'CSV' class ChartType(models.TextChoices): """Chart type options.""" BAR = 'bar', 'Bar Chart' LINE = 'line', 'Line Chart' PIE = 'pie', 'Pie Chart' DONUT = 'donut', 'Donut Chart' AREA = 'area', 'Area Chart' TABLE = 'table', 'Table Only' class SavedReport(UUIDModel, TimeStampedModel): """ Saved report configuration. Stores all settings for a custom report that can be regenerated on demand or scheduled. """ name = models.CharField(max_length=200) description = models.TextField(blank=True) # Data source data_source = models.CharField( max_length=50, choices=DataSource.choices, default=DataSource.COMPLAINTS ) # Filter configuration (JSON) # Example: {"date_range": "30d", "hospital": "uuid", "status": ["open", "in_progress"]} filter_config = models.JSONField(default=dict, blank=True) # Column configuration (JSON) # Example: ["title", "status", "severity", "department__name", "created_at"] column_config = models.JSONField(default=list, blank=True) # Grouping configuration (JSON) # Example: {"field": "department__name", "aggregation": "count"} grouping_config = models.JSONField(default=dict, blank=True) # Sort configuration (JSON) # Example: [{"field": "created_at", "direction": "desc"}] sort_config = models.JSONField(default=list, blank=True) # Chart configuration (JSON) # Example: {"type": "bar", "x_axis": "department__name", "y_axis": "count"} chart_config = models.JSONField(default=dict, blank=True) # Owner and sharing created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='saved_reports' ) is_shared = models.BooleanField( default=False, help_text="Share with other users in the same hospital" ) is_template = models.BooleanField( default=False, help_text="Available as a template for others to use" ) # Organization scope hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='saved_reports', null=True, blank=True ) # Metadata last_run_at = models.DateTimeField(null=True, blank=True) last_run_count = models.IntegerField(default=0) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['data_source', '-created_at']), models.Index(fields=['created_by', '-created_at']), models.Index(fields=['hospital', 'is_shared']), ] def __str__(self): return self.name class ReportSchedule(UUIDModel, TimeStampedModel): """ Schedule for automated report generation and delivery. """ report = models.ForeignKey( SavedReport, on_delete=models.CASCADE, related_name='schedules' ) # Schedule settings frequency = models.CharField( max_length=20, choices=ReportFrequency.choices, default=ReportFrequency.WEEKLY ) # Delivery time (hour:minute in 24h format) # Stored as "HH:MM" string delivery_time = models.CharField( max_length=5, default='09:00', help_text="Time to send report (HH:MM format)" ) # Day of week for weekly reports (0=Monday, 6=Sunday) day_of_week = models.IntegerField( null=True, blank=True, help_text="For weekly reports: 0=Monday, 6=Sunday" ) # Day of month for monthly reports (1-31) day_of_month = models.IntegerField( null=True, blank=True, help_text="For monthly reports: 1-31" ) # Export format for scheduled reports export_format = models.CharField( max_length=20, choices=ReportFormat.choices, default=ReportFormat.PDF ) # Recipients recipients = models.JSONField( default=list, help_text="List of email addresses to receive the report" ) # Status is_active = models.BooleanField(default=True) last_run_at = models.DateTimeField(null=True, blank=True) next_run_at = models.DateTimeField(null=True, blank=True) last_error = models.TextField(blank=True) class Meta: ordering = ['next_run_at'] def __str__(self): return f"{self.report.name} - {self.get_frequency_display()}" def calculate_next_run(self): """Calculate the next run time based on frequency.""" from datetime import datetime, timedelta now = timezone.now() hour, minute = map(int, self.delivery_time.split(':')) if self.frequency == ReportFrequency.DAILY: next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0) if next_run <= now: next_run += timedelta(days=1) return next_run elif self.frequency == ReportFrequency.WEEKLY and self.day_of_week is not None: days_ahead = self.day_of_week - now.weekday() if days_ahead <= 0: days_ahead += 7 next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0) next_run += timedelta(days=days_ahead) return next_run elif self.frequency == ReportFrequency.MONTHLY and self.day_of_month: next_run = now.replace(day=min(self.day_of_month, 28), hour=hour, minute=minute, second=0, microsecond=0) if next_run <= now: if now.month == 12: next_run = next_run.replace(year=now.year + 1, month=1) else: next_run = next_run.replace(month=now.month + 1) return next_run return None class GeneratedReport(UUIDModel, TimeStampedModel): """ Record of a generated report instance. Stores the actual data and metadata for each report run. """ saved_report = models.ForeignKey( SavedReport, on_delete=models.CASCADE, related_name='generated_reports', null=True, blank=True ) # Report name (copied from saved report for standalone reports) name = models.CharField(max_length=200) data_source = models.CharField(max_length=50, choices=DataSource.choices) # Configuration snapshots filter_config = models.JSONField(default=dict) column_config = models.JSONField(default=list) grouping_config = models.JSONField(default=dict) chart_config = models.JSONField(default=dict) # Generated data data = models.JSONField(default=dict, help_text="The actual report data") summary = models.JSONField(default=dict, help_text="Summary statistics") chart_data = models.JSONField(default=dict, help_text="Chart-formatted data") # Row count row_count = models.IntegerField(default=0) # Generation metadata generated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='generated_reports' ) # Export files pdf_file = models.FileField(upload_to='reports/pdf/', null=True, blank=True) excel_file = models.FileField(upload_to='reports/excel/', null=True, blank=True) csv_file = models.FileField(upload_to='reports/csv/', null=True, blank=True) # Organization scope hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='generated_reports', null=True, blank=True ) # Expiry (auto-delete old reports) expires_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['data_source', '-created_at']), models.Index(fields=['generated_by', '-created_at']), models.Index(fields=['expires_at']), ] def __str__(self): return f"{self.name} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" def is_expired(self): """Check if the report has expired.""" if self.expires_at: return timezone.now() > self.expires_at return False class ReportTemplate(UUIDModel, TimeStampedModel): """ Pre-built report templates for quick access. These are system-provided templates that users can use as starting points for their custom reports. """ name = models.CharField(max_length=200) description = models.TextField(blank=True) category = models.CharField(max_length=100, blank=True) # Data source data_source = models.CharField( max_length=50, choices=DataSource.choices, default=DataSource.COMPLAINTS ) # Template configuration filter_config = models.JSONField(default=dict) column_config = models.JSONField(default=list) grouping_config = models.JSONField(default=dict) sort_config = models.JSONField(default=list) chart_config = models.JSONField(default=dict) # Display icon = models.CharField(max_length=50, blank=True, help_text="Bootstrap icon class") sort_order = models.IntegerField(default=0) is_active = models.BooleanField(default=True) class Meta: ordering = ['sort_order', 'name'] def __str__(self): return self.name def create_report(self, user, overrides=None): """Create a SavedReport from this template.""" from apps.reports.services import ReportBuilderService config = { 'filter_config': self.filter_config.copy(), 'column_config': self.column_config.copy(), 'grouping_config': self.grouping_config.copy(), 'sort_config': self.sort_config.copy(), 'chart_config': self.chart_config.copy(), } if overrides: for key, value in overrides.items(): if key in config and isinstance(config[key], dict): config[key].update(value) else: config[key] = value report = SavedReport.objects.create( name=f"{self.name} - {timezone.now().strftime('%Y-%m-%d')}", description=self.description, data_source=self.data_source, created_by=user, hospital=user.hospital, **config ) return report