""" 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", null=True, blank=True, ) object_id = models.UUIDField(db_index=True, null=True, blank=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="rca_approved_items" ) 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=["department", "status", "-created_at"]), models.Index(fields=["assigned_to", "status", "-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""" if self.content_type: return self.content_type.model return None def get_related_item_display(self): """Get display name of related item""" try: return str(self.related_item) except Exception: return f"Item {self.object_id}" def get_related_item_url(self): """Get URL for the related item""" if not self.content_type or not self.object_id: return None try: obj = self.related_item if obj and hasattr(obj, "get_absolute_url"): return obj.get_absolute_url() except Exception: pass return None 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}"