""" 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 Physician 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 = Physician.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( physician=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( physician__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( physician__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 Physician from apps.physicians.models import PhysicianMonthlyRating try: physician = Physician.objects.get(id=physician_id) # Get current month rating current_rating = PhysicianMonthlyRating.objects.filter( physician=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( physician=physician, year=prev_year, month=prev_month ).first() # Get year-to-date stats ytd_ratings = PhysicianMonthlyRating.objects.filter( physician=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 Physician.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 }