HH/apps/reports/models.py
2026-03-09 16:10:24 +03:00

363 lines
12 KiB
Python

"""
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