383 lines
12 KiB
Python
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
|
|
}
|