""" RCA (Root Cause Analysis) models This module implements the Root Cause Analysis system that: - Tracks RCAs for complaints, inquiries, observations, and feedback - Manages root causes and contributing factors - Tracks corrective actions and their effectiveness - Maintains audit trail with notes and status changes - Supports attachments and documentation """ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils import timezone from apps.core.models import PriorityChoices, TimeStampedModel, UUIDModel class RCAStatus(models.TextChoices): """RCA status choices""" DRAFT = 'draft', 'Draft' IN_PROGRESS = 'in_progress', 'In Progress' REVIEW = 'review', 'Under Review' APPROVED = 'approved', 'Approved' CLOSED = 'closed', 'Closed' class RCASeverity(models.TextChoices): """RCA severity choices""" LOW = 'low', 'Low' MEDIUM = 'medium', 'Medium' HIGH = 'high', 'High' CRITICAL = 'critical', 'Critical' class RCAActionType(models.TextChoices): """Corrective action type choices""" PREVENTIVE = 'preventive', 'Preventive' CORRECTIVE = 'corrective', 'Corrective' IMMEDIATE = 'immediate', 'Immediate Action' LONG_TERM = 'long_term', 'Long-term Solution' class RCAActionStatus(models.TextChoices): """Corrective action status choices""" NOT_STARTED = 'not_started', 'Not Started' IN_PROGRESS = 'in_progress', 'In Progress' COMPLETED = 'completed', 'Completed' CANCELLED = 'cancelled', 'Cancelled' class RootCauseCategory(models.TextChoices): """Root cause category choices""" PROCESS = 'process', 'Process/Procedure' PEOPLE = 'people', 'People/Training' EQUIPMENT = 'equipment', 'Equipment/Resources' COMMUNICATION = 'communication', 'Communication' POLICY = 'policy', 'Policy/Regulation' ENVIRONMENT = 'environment', 'Environment' TECHNOLOGY = 'technology', 'Technology/Systems' OTHER = 'other', 'Other' class RootCauseAnalysis(UUIDModel, TimeStampedModel): """ Root Cause Analysis model. Links to complaints, inquiries, observations, or feedback via GenericForeignKey. Tracks the complete RCA process from creation to closure. """ # Related item (can be Complaint, Inquiry, Observation, or Feedback) content_type = models.ForeignKey( ContentType, on_delete=models.PROTECT, related_name='related_rcas', help_text="Type of the related item" ) object_id = models.UUIDField( db_index=True, help_text="ID of the related item" ) related_item = GenericForeignKey('content_type', 'object_id') # Organization hospital = models.ForeignKey( 'organizations.Hospital', on_delete=models.CASCADE, related_name='rcas' ) department = models.ForeignKey( 'organizations.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='rcas' ) # RCA details title = models.CharField(max_length=500) description = models.TextField(help_text="Description of the incident/issue") background = models.TextField( blank=True, help_text="Background information and context" ) # Status and severity status = models.CharField( max_length=20, choices=RCAStatus.choices, default=RCAStatus.DRAFT, db_index=True ) severity = models.CharField( max_length=20, choices=RCASeverity.choices, default=RCASeverity.MEDIUM, db_index=True ) # Priority priority = models.CharField( max_length=20, choices=PriorityChoices.choices, default=PriorityChoices.MEDIUM, db_index=True ) # Root cause analysis summary root_cause_summary = models.TextField( blank=True, help_text="Summary of root cause analysis findings" ) # Assignment assigned_to = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_rcas', help_text="Person responsible for RCA" ) assigned_at = models.DateTimeField(null=True, blank=True) # Created by created_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='created_rcas' ) # Approval approved_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_rcas' ) approved_at = models.DateTimeField(null=True, blank=True) approval_notes = models.TextField(blank=True) # Closure closed_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='closed_rcas' ) closed_at = models.DateTimeField(null=True, blank=True) closure_notes = models.TextField(blank=True) # Dates target_completion_date = models.DateField( null=True, blank=True, help_text="Target date for RCA completion" ) actual_completion_date = models.DateField(null=True, blank=True) # Metadata metadata = models.JSONField(default=dict, blank=True) # Soft delete is_deleted = models.BooleanField(default=False, db_index=True) deleted_at = models.DateTimeField(null=True, blank=True) deleted_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='deleted_rcas' ) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['status', '-created_at']), models.Index(fields=['hospital', 'status', '-created_at']), models.Index(fields=['severity', '-created_at']), models.Index(fields=['is_deleted', '-created_at']), models.Index(fields=['content_type', 'object_id']), ] verbose_name = 'Root Cause Analysis' verbose_name_plural = 'Root Cause Analyses' def __str__(self): return f"{self.title} ({self.get_status_display()})" def get_related_item_type(self): """Get the type of related item""" return self.content_type.model def get_related_item_display(self): """Get display name of related item""" try: return str(self.related_item) except: return f"Item {self.object_id}" def get_root_causes_count(self): """Get count of root causes""" return self.root_causes.count() def get_corrective_actions_count(self): """Get count of corrective actions""" return self.corrective_actions.count() def get_completed_actions_count(self): """Get count of completed corrective actions""" return self.corrective_actions.filter( status=RCAActionStatus.COMPLETED ).count() def soft_delete(self, user=None): """Soft delete RCA""" self.is_deleted = True self.deleted_at = timezone.now() self.deleted_by = user self.save(update_fields=['is_deleted', 'deleted_at', 'deleted_by']) class RCARootCause(UUIDModel, TimeStampedModel): """ Individual root cause identified in an RCA. Multiple root causes can be identified per RCA. """ rca = models.ForeignKey( RootCauseAnalysis, on_delete=models.CASCADE, related_name='root_causes' ) # Root cause details description = models.TextField(help_text="Description of root cause") category = models.CharField( max_length=50, choices=RootCauseCategory.choices, db_index=True ) # Contributing factors contributing_factors = models.TextField( blank=True, help_text="Factors that contributed to this root cause" ) # Impact assessment likelihood = models.IntegerField( null=True, blank=True, help_text="Likelihood score (1-5)" ) impact = models.IntegerField( null=True, blank=True, help_text="Impact score (1-5)" ) risk_score = models.IntegerField( null=True, blank=True, help_text="Risk score (likelihood * impact)" ) # Evidence evidence = models.TextField( blank=True, help_text="Evidence supporting this root cause" ) # Verification is_verified = models.BooleanField(default=False) verified_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_root_causes' ) verified_at = models.DateTimeField(null=True, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['rca', '-created_at']), models.Index(fields=['category', '-created_at']), ] verbose_name = 'Root Cause' verbose_name_plural = 'Root Causes' def __str__(self): return f"{self.rca.title} - {self.description[:50]}" def save(self, *args, **kwargs): """Calculate risk score before saving""" if self.likelihood and self.impact: self.risk_score = self.likelihood * self.impact super().save(*args, **kwargs) class RCACorrectiveAction(UUIDModel, TimeStampedModel): """ Corrective action to address identified root causes. Each action is tracked for completion and effectiveness. """ rca = models.ForeignKey( RootCauseAnalysis, on_delete=models.CASCADE, related_name='corrective_actions' ) root_cause = models.ForeignKey( RCARootCause, on_delete=models.SET_NULL, null=True, blank=True, related_name='corrective_actions', help_text="Root cause this action addresses" ) # Action details description = models.TextField(help_text="Description of corrective action") action_type = models.CharField( max_length=20, choices=RCAActionType.choices, default=RCAActionType.CORRECTIVE, db_index=True ) # Responsibility responsible_person = models.ForeignKey( 'organizations.Staff', on_delete=models.SET_NULL, null=True, blank=True, related_name='corrective_actions', help_text="Person responsible for implementing the action" ) # Dates target_date = models.DateField( null=True, blank=True, help_text="Target date for completion" ) completion_date = models.DateField( null=True, blank=True, help_text="Actual completion date" ) # Status status = models.CharField( max_length=20, choices=RCAActionStatus.choices, default=RCAActionStatus.NOT_STARTED, db_index=True ) # Effectiveness effectiveness_measure = models.TextField( blank=True, help_text="How effectiveness will be measured" ) effectiveness_assessment = models.TextField( blank=True, help_text="Assessment of action effectiveness" ) effectiveness_score = models.IntegerField( null=True, blank=True, help_text="Effectiveness score (1-5)" ) # Obstacles obstacles = models.TextField( blank=True, help_text="Obstacles encountered during implementation" ) # Metadata metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['target_date', '-created_at'] indexes = [ models.Index(fields=['rca', '-created_at']), models.Index(fields=['status', 'target_date']), models.Index(fields=['action_type', '-created_at']), ] verbose_name = 'Corrective Action' verbose_name_plural = 'Corrective Actions' def __str__(self): return f"{self.description[:50]} - {self.get_status_display()}" class RCAAttachment(UUIDModel, TimeStampedModel): """Attachments for RCA documentation""" rca = models.ForeignKey( RootCauseAnalysis, on_delete=models.CASCADE, related_name='attachments' ) file = models.FileField(upload_to='rca/%Y/%m/%d/') filename = models.CharField(max_length=500) file_type = models.CharField(max_length=100, blank=True) file_size = models.IntegerField(help_text="File size in bytes") uploaded_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='rca_attachments' ) description = models.TextField(blank=True) class Meta: ordering = ['-created_at'] verbose_name = 'RCA Attachment' verbose_name_plural = 'RCA Attachments' def __str__(self): return f"{self.rca.title} - {self.filename}" class RCANote(UUIDModel, TimeStampedModel): """Internal notes for RCA""" rca = models.ForeignKey( RootCauseAnalysis, on_delete=models.CASCADE, related_name='notes' ) note = models.TextField() created_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='rca_notes' ) is_internal = models.BooleanField( default=True, help_text="Internal note (not visible in reports)" ) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['rca', '-created_at']), ] verbose_name = 'RCA Note' verbose_name_plural = 'RCA Notes' def __str__(self): return f"{self.rca.title} - {self.created_at.strftime('%Y-%m-%d %H:%M')}" class RCAStatusLog(UUIDModel, TimeStampedModel): """Audit trail for RCA status changes""" rca = models.ForeignKey( RootCauseAnalysis, on_delete=models.CASCADE, related_name='status_logs' ) old_status = models.CharField(max_length=20, blank=True) new_status = models.CharField(max_length=20, db_index=True) changed_by = models.ForeignKey( 'accounts.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='rca_status_changes' ) notes = models.TextField(blank=True) metadata = models.JSONField(default=dict, blank=True) class Meta: ordering = ['-created_at'] indexes = [ models.Index(fields=['rca', '-created_at']), models.Index(fields=['new_status', '-created_at']), ] verbose_name = 'RCA Status Log' verbose_name_plural = 'RCA Status Logs' def __str__(self): return f"{self.rca.title}: {self.old_status} → {self.new_status}"