HH/apps/physicians/tasks.py

383 lines
12 KiB
Python

"""
Physician Celery tasks
This module contains tasks for:
- Calculating monthly physician ratings from surveys
- Updating physician rankings
- Generating performance reports
"""
import logging
from decimal import Decimal
from celery import shared_task
from django.db import transaction
from django.db.models import Avg, Count, Q
from django.utils import timezone
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3)
def calculate_monthly_physician_ratings(self, year=None, month=None):
"""
Calculate physician monthly ratings from survey responses.
This task aggregates all survey responses that mention physicians
for a given month and creates/updates PhysicianMonthlyRating records.
Args:
year: Year to calculate (default: current year)
month: Month to calculate (default: current month)
Returns:
dict: Result with number of ratings calculated
"""
from apps.organizations.models import Staff
from apps.physicians.models import PhysicianMonthlyRating
from apps.surveys.models import SurveyInstance, SurveyResponse
try:
# Default to current month if not specified
now = timezone.now()
year = year or now.year
month = month or now.month
logger.info(f"Calculating physician ratings for {year}-{month:02d}")
# Get all active physicians
physicians = Staff.objects.filter(status='active')
ratings_created = 0
ratings_updated = 0
for physician in physicians:
# Find all completed surveys mentioning this physician
# This assumes surveys have a physician field or question
# Adjust based on your actual survey structure
# Option 1: If surveys have a direct physician field
surveys = SurveyInstance.objects.filter(
status='completed',
completed_at__year=year,
completed_at__month=month,
metadata__physician_id=str(physician.id)
)
# Option 2: If physician is mentioned in survey responses
# You may need to adjust this based on your question structure
physician_responses = SurveyResponse.objects.filter(
survey_instance__status='completed',
survey_instance__completed_at__year=year,
survey_instance__completed_at__month=month,
question__text__icontains='physician', # Adjust based on your questions
text_value__icontains=physician.get_full_name()
).values_list('survey_instance_id', flat=True).distinct()
# Combine both approaches
survey_ids = set(surveys.values_list('id', flat=True)) | set(physician_responses)
if not survey_ids:
logger.debug(f"No surveys found for physician {physician.get_full_name()}")
continue
# Get all surveys for this physician
physician_surveys = SurveyInstance.objects.filter(id__in=survey_ids)
# Calculate statistics
total_surveys = physician_surveys.count()
# Calculate average rating
avg_score = physician_surveys.aggregate(
avg=Avg('total_score')
)['avg']
if avg_score is None:
logger.debug(f"No scores found for physician {physician.get_full_name()}")
continue
# Count sentiment
positive_count = physician_surveys.filter(
total_score__gte=4.0
).count()
neutral_count = physician_surveys.filter(
total_score__gte=3.0,
total_score__lt=4.0
).count()
negative_count = physician_surveys.filter(
total_score__lt=3.0
).count()
# Get MD consult specific rating if available
md_consult_surveys = physician_surveys.filter(
survey_template__survey_type='md_consult'
)
md_consult_rating = md_consult_surveys.aggregate(
avg=Avg('total_score')
)['avg']
# Create or update rating
rating, created = PhysicianMonthlyRating.objects.update_or_create(
staff=physician,
year=year,
month=month,
defaults={
'average_rating': Decimal(str(avg_score)),
'total_surveys': total_surveys,
'positive_count': positive_count,
'neutral_count': neutral_count,
'negative_count': negative_count,
'md_consult_rating': Decimal(str(md_consult_rating)) if md_consult_rating else None,
'metadata': {
'calculated_at': timezone.now().isoformat(),
'survey_ids': [str(sid) for sid in survey_ids]
}
}
)
if created:
ratings_created += 1
else:
ratings_updated += 1
logger.debug(
f"{'Created' if created else 'Updated'} rating for {physician.get_full_name()}: "
f"{avg_score:.2f} ({total_surveys} surveys)"
)
# Update rankings
update_physician_rankings.delay(year, month)
logger.info(
f"Completed physician ratings calculation for {year}-{month:02d}: "
f"{ratings_created} created, {ratings_updated} updated"
)
return {
'status': 'success',
'year': year,
'month': month,
'ratings_created': ratings_created,
'ratings_updated': ratings_updated
}
except Exception as e:
error_msg = f"Error calculating physician ratings: {str(e)}"
logger.error(error_msg, exc_info=True)
# Retry the task
raise self.retry(exc=e, countdown=60 * (self.request.retries + 1))
@shared_task
def update_physician_rankings(year, month):
"""
Update hospital and department rankings for physicians.
This calculates the rank of each physician within their hospital
and department for the specified month.
Args:
year: Year
month: Month
Returns:
dict: Result with number of rankings updated
"""
from apps.organizations.models import Hospital, Department
from apps.physicians.models import PhysicianMonthlyRating
try:
logger.info(f"Updating physician rankings for {year}-{month:02d}")
rankings_updated = 0
# Update hospital rankings
hospitals = Hospital.objects.filter(status='active')
for hospital in hospitals:
# Get all ratings for this hospital
ratings = PhysicianMonthlyRating.objects.filter(
staff__hospital=hospital,
year=year,
month=month
).order_by('-average_rating')
# Assign ranks
for rank, rating in enumerate(ratings, start=1):
rating.hospital_rank = rank
rating.save(update_fields=['hospital_rank'])
rankings_updated += 1
# Update department rankings
departments = Department.objects.filter(status='active')
for department in departments:
# Get all ratings for this department
ratings = PhysicianMonthlyRating.objects.filter(
staff__department=department,
year=year,
month=month
).order_by('-average_rating')
# Assign ranks
for rank, rating in enumerate(ratings, start=1):
rating.department_rank = rank
rating.save(update_fields=['department_rank'])
logger.info(f"Updated {rankings_updated} physician rankings for {year}-{month:02d}")
return {
'status': 'success',
'year': year,
'month': month,
'rankings_updated': rankings_updated
}
except Exception as e:
error_msg = f"Error updating physician rankings: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'status': 'error', 'reason': error_msg}
@shared_task
def generate_physician_performance_report(physician_id, year, month):
"""
Generate detailed performance report for a physician.
This creates a comprehensive report including:
- Monthly rating
- Comparison to previous months
- Ranking within hospital/department
- Trend analysis
Args:
physician_id: UUID of Physician
year: Year
month: Month
Returns:
dict: Performance report data
"""
from apps.organizations.models import Staff
from apps.physicians.models import PhysicianMonthlyRating
try:
physician = Staff.objects.get(id=physician_id)
# Get current month rating
current_rating = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=year,
month=month
).first()
if not current_rating:
return {
'status': 'no_data',
'reason': f'No rating found for {year}-{month:02d}'
}
# Get previous month
prev_month = month - 1 if month > 1 else 12
prev_year = year if month > 1 else year - 1
previous_rating = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=prev_year,
month=prev_month
).first()
# Get year-to-date stats
ytd_ratings = PhysicianMonthlyRating.objects.filter(
staff=physician,
year=year
)
ytd_avg = ytd_ratings.aggregate(avg=Avg('average_rating'))['avg']
ytd_surveys = ytd_ratings.aggregate(total=Count('total_surveys'))['total']
# Calculate trend
trend = 'stable'
if previous_rating:
diff = float(current_rating.average_rating - previous_rating.average_rating)
if diff > 0.1:
trend = 'improving'
elif diff < -0.1:
trend = 'declining'
report = {
'status': 'success',
'physician': {
'id': str(physician.id),
'name': physician.get_full_name(),
'license': physician.license_number,
'specialization': physician.specialization
},
'current_month': {
'year': year,
'month': month,
'average_rating': float(current_rating.average_rating),
'total_surveys': current_rating.total_surveys,
'hospital_rank': current_rating.hospital_rank,
'department_rank': current_rating.department_rank
},
'previous_month': {
'average_rating': float(previous_rating.average_rating) if previous_rating else None,
'total_surveys': previous_rating.total_surveys if previous_rating else None
} if previous_rating else None,
'year_to_date': {
'average_rating': float(ytd_avg) if ytd_avg else None,
'total_surveys': ytd_surveys
},
'trend': trend
}
logger.info(f"Generated performance report for {physician.get_full_name()}")
return report
except Staff.DoesNotExist:
error_msg = f"Physician {physician_id} not found"
logger.error(error_msg)
return {'status': 'error', 'reason': error_msg}
except Exception as e:
error_msg = f"Error generating performance report: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'status': 'error', 'reason': error_msg}
@shared_task
def schedule_monthly_rating_calculation():
"""
Scheduled task to calculate physician ratings for the previous month.
This should be run on the 1st of each month to calculate ratings
for the previous month.
Returns:
dict: Result of calculation
"""
from dateutil.relativedelta import relativedelta
# Calculate for previous month
now = timezone.now()
prev_month = now - relativedelta(months=1)
year = prev_month.year
month = prev_month.month
logger.info(f"Scheduled calculation of physician ratings for {year}-{month:02d}")
# Trigger calculation
result = calculate_monthly_physician_ratings.delay(year, month)
return {
'status': 'scheduled',
'year': year,
'month': month,
'task_id': result.id
}