421 lines
15 KiB
Python
421 lines
15 KiB
Python
"""
|
|
Executive Summary Celery Tasks
|
|
|
|
Scheduled tasks for:
|
|
- Generating weekly/monthly executive reports
|
|
- Calculating daily metrics
|
|
- Generating predictive insights
|
|
- Creating AI recommendations
|
|
- Sending PDF reports via email
|
|
"""
|
|
|
|
from celery import shared_task
|
|
from django.utils import timezone
|
|
from django.core.files.base import ContentFile
|
|
from celery import shared_task
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@shared_task
|
|
def generate_weekly_executive_summary():
|
|
"""
|
|
Generate weekly executive summary report.
|
|
|
|
Runs every Monday at 6 AM.
|
|
Creates an ExecutiveReport with AI-generated narrative for the past week.
|
|
"""
|
|
from .models import ExecutiveReport
|
|
from .services import AINarrativeService
|
|
|
|
try:
|
|
logger.info("Starting weekly executive summary generation")
|
|
|
|
end_date = timezone.now().date()
|
|
start_date = end_date - timedelta(days=7)
|
|
|
|
# Check if report already exists
|
|
existing = ExecutiveReport.objects.filter(
|
|
report_type="weekly",
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
).first()
|
|
|
|
if existing:
|
|
logger.info(f"Weekly report already exists for {start_date} to {end_date}")
|
|
return {"status": "skipped", "report_id": str(existing.id)}
|
|
|
|
# Generate narratives using AI
|
|
narrative_service = AINarrativeService()
|
|
|
|
# Generate English narrative
|
|
logger.info("Generating English narrative...")
|
|
en_result = narrative_service.generate_weekly_narrative(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
)
|
|
|
|
# Generate Arabic narrative
|
|
logger.info("Generating Arabic narrative...")
|
|
ar_result = narrative_service.generate_arabic_narrative(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
report_type="weekly",
|
|
)
|
|
|
|
# Create report
|
|
report = ExecutiveReport.objects.create(
|
|
report_type="weekly",
|
|
status=en_result.get("status", "completed"),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
narrative_en=en_result.get("narrative_en", ""),
|
|
narrative_ar=ar_result.get("narrative_ar", ""),
|
|
highlights_en=en_result.get("highlights_en", []),
|
|
highlights_ar=ar_result.get("highlights_ar", []),
|
|
concerns_en=en_result.get("concerns_en", []),
|
|
concerns_ar=ar_result.get("concerns_ar", []),
|
|
ai_model=en_result.get("ai_model", ""),
|
|
generation_time_ms=en_result.get("generation_time_ms"),
|
|
error_message=en_result.get("error", "") or ar_result.get("error", ""),
|
|
)
|
|
|
|
# Generate and save PDF
|
|
try:
|
|
from .pdf_service import generate_executive_pdf
|
|
|
|
pdf_bytes = generate_executive_pdf(report)
|
|
filename = f"executive_report_{report.id}_{report.start_date}.pdf"
|
|
report.pdf_file.save(filename, ContentFile(pdf_bytes), save=True)
|
|
logger.info(f"PDF saved for report {report.id}")
|
|
except Exception as pdf_err:
|
|
logger.error(f"Failed to generate PDF for report {report.id}: {pdf_err}", exc_info=True)
|
|
|
|
logger.info(f"Weekly executive summary generated: {report.id}")
|
|
return {
|
|
"status": "success",
|
|
"report_id": str(report.id),
|
|
"report_type": "weekly",
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate weekly executive summary: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
@shared_task
|
|
def generate_monthly_executive_summary():
|
|
"""
|
|
Generate monthly executive summary report.
|
|
|
|
Runs on 1st of each month at 7 AM.
|
|
Creates an ExecutiveReport with AI-generated narrative for the past month.
|
|
"""
|
|
from .models import ExecutiveReport
|
|
from .services import AINarrativeService
|
|
|
|
try:
|
|
logger.info("Starting monthly executive summary generation")
|
|
|
|
end_date = timezone.now().date()
|
|
start_date = end_date - timedelta(days=30)
|
|
|
|
# Check if report already exists
|
|
existing = ExecutiveReport.objects.filter(
|
|
report_type="monthly",
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
).first()
|
|
|
|
if existing:
|
|
logger.info(f"Monthly report already exists for {start_date} to {end_date}")
|
|
return {"status": "skipped", "report_id": str(existing.id)}
|
|
|
|
# Generate narratives using AI
|
|
narrative_service = AINarrativeService()
|
|
|
|
# Generate English narrative
|
|
logger.info("Generating English narrative...")
|
|
en_result = narrative_service.generate_monthly_narrative(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
)
|
|
|
|
# Generate Arabic narrative
|
|
logger.info("Generating Arabic narrative...")
|
|
ar_result = narrative_service.generate_arabic_narrative(
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
report_type="monthly",
|
|
)
|
|
|
|
# Create report
|
|
report = ExecutiveReport.objects.create(
|
|
report_type="monthly",
|
|
status=en_result.get("status", "completed"),
|
|
start_date=start_date,
|
|
end_date=end_date,
|
|
narrative_en=en_result.get("narrative_en", ""),
|
|
narrative_ar=ar_result.get("narrative_ar", ""),
|
|
highlights_en=en_result.get("highlights_en", []),
|
|
highlights_ar=ar_result.get("highlights_ar", []),
|
|
concerns_en=en_result.get("concerns_en", []),
|
|
concerns_ar=ar_result.get("concerns_ar", []),
|
|
ai_model=en_result.get("ai_model", ""),
|
|
generation_time_ms=en_result.get("generation_time_ms"),
|
|
error_message=en_result.get("error", "") or ar_result.get("error", ""),
|
|
)
|
|
|
|
# Generate and save PDF
|
|
try:
|
|
from .pdf_service import generate_executive_pdf
|
|
|
|
pdf_bytes = generate_executive_pdf(report)
|
|
filename = f"executive_report_{report.id}_{report.start_date}.pdf"
|
|
report.pdf_file.save(filename, ContentFile(pdf_bytes), save=True)
|
|
logger.info(f"PDF saved for report {report.id}")
|
|
except Exception as pdf_err:
|
|
logger.error(f"Failed to generate PDF for report {report.id}: {pdf_err}", exc_info=True)
|
|
|
|
logger.info(f"Monthly executive summary generated: {report.id}")
|
|
return {
|
|
"status": "success",
|
|
"report_id": str(report.id),
|
|
"report_type": "monthly",
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate monthly executive summary: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
@shared_task
|
|
def calculate_daily_metrics():
|
|
"""
|
|
Calculate and store daily metrics for all hospitals.
|
|
|
|
Runs daily at 1 AM.
|
|
Aggregates metrics from complaints, surveys, actions, observations, etc.
|
|
"""
|
|
from decimal import Decimal
|
|
from .models import ExecutiveMetric
|
|
from .services import ExecutiveSummaryService
|
|
from apps.organizations.models import Hospital
|
|
|
|
try:
|
|
logger.info("Starting daily metrics calculation")
|
|
|
|
target_date = (timezone.now() - timedelta(days=1)).date()
|
|
summary_service = ExecutiveSummaryService()
|
|
|
|
def _save_metrics(metrics, date, hospital=None):
|
|
saved = 0
|
|
for metric_type, metric_value in metrics.items():
|
|
try:
|
|
value = Decimal(str(metric_value))
|
|
except Exception:
|
|
continue
|
|
|
|
# Calculate simple variance vs previous day
|
|
previous = (
|
|
ExecutiveMetric.objects.filter(
|
|
metric_type=metric_type,
|
|
hospital=hospital,
|
|
metric_date__lt=date,
|
|
)
|
|
.order_by("-metric_date")
|
|
.first()
|
|
)
|
|
|
|
variance = None
|
|
variance_direction = "neutral"
|
|
if previous and previous.metric_value is not None:
|
|
prev_val = float(previous.metric_value)
|
|
curr_val = float(value)
|
|
if prev_val != 0:
|
|
variance = Decimal(str(((curr_val - prev_val) / prev_val) * 100))
|
|
else:
|
|
variance = Decimal("0")
|
|
if variance > 0:
|
|
variance_direction = "up"
|
|
elif variance < 0:
|
|
variance_direction = "down"
|
|
|
|
ExecutiveMetric.objects.update_or_create(
|
|
metric_date=date,
|
|
metric_type=metric_type,
|
|
hospital=hospital,
|
|
defaults={
|
|
"metric_value": value,
|
|
"variance": variance,
|
|
"variance_direction": variance_direction,
|
|
},
|
|
)
|
|
saved += 1
|
|
return saved
|
|
|
|
# Calculate system-wide metrics
|
|
logger.info(f"Calculating system-wide metrics for {target_date}")
|
|
metrics = summary_service.aggregate_daily_metrics(target_date, hospital=None)
|
|
system_saved = _save_metrics(metrics, target_date, hospital=None)
|
|
|
|
# Calculate per-hospital metrics
|
|
hospitals = Hospital.objects.filter(status="active")
|
|
total_hospital_saved = 0
|
|
for hospital in hospitals:
|
|
logger.info(f"Calculating metrics for {hospital.name}")
|
|
metrics = summary_service.aggregate_daily_metrics(target_date, hospital=hospital)
|
|
total_hospital_saved += _save_metrics(metrics, target_date, hospital=hospital)
|
|
|
|
logger.info(
|
|
f"Daily metrics calculated for {target_date}: {system_saved} system-wide, {total_hospital_saved} hospital-level"
|
|
)
|
|
return {
|
|
"status": "success",
|
|
"date": target_date.isoformat(),
|
|
"hospitals_processed": hospitals.count(),
|
|
"system_metrics_saved": system_saved,
|
|
"hospital_metrics_saved": total_hospital_saved,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to calculate daily metrics: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
@shared_task
|
|
def generate_predictive_insights():
|
|
"""
|
|
Generate predictive insights from data analysis.
|
|
|
|
Runs every 6 hours.
|
|
Analyzes trends, anomalies, SLA breach risks, and creates PredictiveInsight objects.
|
|
"""
|
|
from .services import PredictiveAnalyticsService
|
|
|
|
try:
|
|
logger.info("Starting predictive insights generation")
|
|
|
|
predictive_service = PredictiveAnalyticsService()
|
|
result = predictive_service.generate_predictive_insights()
|
|
|
|
logger.info(f"Predictive insights generated: {result.get('insights_created', 0)} insights created")
|
|
return {
|
|
"status": "success",
|
|
"insights_created": result.get("insights_created", 0),
|
|
"insights_updated": result.get("insights_updated", 0),
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate predictive insights: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
@shared_task
|
|
def generate_ai_recommendations():
|
|
"""
|
|
Generate AI recommendations from predictive insights.
|
|
|
|
Runs daily at 3 AM.
|
|
Creates AIRecommendation objects based on unhandled PredictiveInsights.
|
|
"""
|
|
from .services import RecommendationService
|
|
|
|
try:
|
|
logger.info("Starting AI recommendations generation")
|
|
|
|
recommendation_service = RecommendationService()
|
|
result = recommendation_service.generate_recommendations_from_insights()
|
|
|
|
logger.info(f"AI recommendations generated: {result.get('recommendations_created', 0)}")
|
|
return {
|
|
"status": "success",
|
|
"recommendations_created": result.get("recommendations_created", 0),
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to generate AI recommendations: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|
|
|
|
|
|
@shared_task
|
|
def send_executive_pdf_report():
|
|
"""
|
|
Send PDF reports to executives via email.
|
|
|
|
Runs weekly and monthly after report generation.
|
|
Emails the latest executive PDF report to all executive users.
|
|
"""
|
|
from django.core.mail import EmailMessage
|
|
from django.conf import settings
|
|
from .models import ExecutiveReport
|
|
from apps.accounts.models import User
|
|
|
|
try:
|
|
logger.info("Starting executive PDF report email delivery")
|
|
|
|
# Get latest completed report
|
|
latest_report = (
|
|
ExecutiveReport.objects.filter(status="completed", pdf_file__isnull=False).order_by("-created_at").first()
|
|
)
|
|
|
|
if not latest_report:
|
|
logger.info("No completed PDF report found to send")
|
|
return {"status": "skipped", "reason": "no_pdf_report"}
|
|
|
|
# Get executive users
|
|
executives = User.objects.filter(
|
|
groups__name="Executive",
|
|
is_active=True,
|
|
).distinct()
|
|
|
|
if not executives.exists():
|
|
logger.info("No executive users found to send report to")
|
|
return {"status": "skipped", "reason": "no_executives"}
|
|
|
|
# Prepare email
|
|
subject = f"Executive Summary Report - {latest_report.get_report_type_display()}"
|
|
body = f"""Dear Executive,
|
|
|
|
Please find attached the {latest_report.get_report_type_display().lower()} summary report for the period {latest_report.start_date} to {latest_report.end_date}.
|
|
|
|
Key Highlights:
|
|
{chr(10).join(f"• {highlight}" for highlight in latest_report.highlights_en[:5])}
|
|
|
|
Key Concerns:
|
|
{chr(10).join(f"• {concern}" for concern in latest_report.concerns_en[:5])}
|
|
|
|
This report was generated automatically by the PX360 AI system.
|
|
|
|
Best regards,
|
|
PX360 Team
|
|
"""
|
|
|
|
# Send emails
|
|
emails_sent = 0
|
|
for executive in executives:
|
|
if executive.email:
|
|
try:
|
|
email = EmailMessage(
|
|
subject=subject,
|
|
body=body,
|
|
to=[executive.email],
|
|
)
|
|
email.attach_file(latest_report.pdf_file.path)
|
|
email.send()
|
|
emails_sent += 1
|
|
logger.info(f"Report sent to {executive.email}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send report to {executive.email}: {str(e)}")
|
|
|
|
logger.info(f"PDF reports sent to {emails_sent} executives")
|
|
return {
|
|
"status": "success",
|
|
"emails_sent": emails_sent,
|
|
"report_id": str(latest_report.id),
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send executive PDF reports: {str(e)}", exc_info=True)
|
|
return {"status": "error", "error": str(e)}
|