from django.db import models from django.conf import settings from django.core.validators import MinValueValidator, MaxValueValidator from decimal import Decimal class QualityIndicator(models.Model): """Quality metrics and performance indicators for monitoring and improvement""" CATEGORY_CHOICES = [ ('clinical', 'Clinical'), ('safety', 'Safety'), ('operational', 'Operational'), ('financial', 'Financial'), ('patient_satisfaction', 'Patient Satisfaction'), ('staff_satisfaction', 'Staff Satisfaction'), ('compliance', 'Compliance'), ('efficiency', 'Efficiency'), ('quality', 'Quality'), ('outcome', 'Outcome'), ] TYPE_CHOICES = [ ('structure', 'Structure'), ('process', 'Process'), ('outcome', 'Outcome'), ('balancing', 'Balancing'), ] FREQUENCY_CHOICES = [ ('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('quarterly', 'Quarterly'), ('annually', 'Annually'), ] tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='quality_indicators') name = models.CharField(max_length=200) description = models.TextField() category = models.CharField(max_length=50, choices=CATEGORY_CHOICES) type = models.CharField(max_length=20, choices=TYPE_CHOICES) measurement_unit = models.CharField(max_length=50) target_value = models.DecimalField(max_digits=10, decimal_places=2) current_value = models.DecimalField(max_digits=10, decimal_places=2) threshold_warning = models.DecimalField(max_digits=10, decimal_places=2) threshold_critical = models.DecimalField(max_digits=10, decimal_places=2) calculation_method = models.TextField() data_source = models.CharField(max_length=200) frequency = models.CharField(max_length=20, choices=FREQUENCY_CHOICES) responsible_department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True) responsible_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_quality_indicators') is_active = models.BooleanField(default=True) regulatory_requirement = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'quality_indicator' indexes = [ models.Index(fields=['tenant', 'category', 'type']), models.Index(fields=['is_active', 'regulatory_requirement']), models.Index(fields=['responsible_department', 'responsible_user']), ] ordering = ['name'] def __str__(self): return f"{self.name} ({self.category})" @property def latest_measurement(self): """Get the most recent measurement for this indicator""" return self.measurements.order_by('-measurement_date').first() @property def current_status(self): """Get current status based on latest measurement""" latest = self.latest_measurement if not latest: return 'no_data' return latest.status class QualityMeasurement(models.Model): """Individual measurements and values for quality indicators""" STATUS_CHOICES = [ ('within_target', 'Within Target'), ('warning', 'Warning'), ('critical', 'Critical'), ('improving', 'Improving'), ('declining', 'Declining'), ] tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='quality_measurements') indicator = models.ForeignKey(QualityIndicator, on_delete=models.CASCADE, related_name='measurements') measurement_date = models.DateField() value = models.DecimalField(max_digits=10, decimal_places=2) numerator = models.IntegerField(null=True, blank=True) denominator = models.IntegerField(null=True, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES) measurement_method = models.TextField(blank=True) sample_size = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) data_source = models.CharField(max_length=200, null=True, blank=True) notes = models.TextField(blank=True) data_source_reference = models.CharField(max_length=200, blank=True) verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_measurements') verified_at = models.DateTimeField(null=True, blank=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_measurements') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'quality_measurement' indexes = [ models.Index(fields=['tenant', 'indicator', 'measurement_date']), models.Index(fields=['status', 'verified_by']), models.Index(fields=['created_by', 'created_at']), ] ordering = ['-measurement_date'] unique_together = ['indicator', 'measurement_date'] def __str__(self): return f"{self.indicator.name} - {self.measurement_date}: {self.value}" def save(self, *args, **kwargs): """Auto-determine status based on thresholds""" if not self.status: if self.value >= self.indicator.threshold_critical: self.status = 'critical' elif self.value >= self.indicator.threshold_warning: self.status = 'warning' else: self.status = 'within_target' super().save(*args, **kwargs) class IncidentReport(models.Model): """Patient safety incidents, adverse events, and near-miss reporting""" INCIDENT_TYPE_CHOICES = [ ('medication_error', 'Medication Error'), ('fall', 'Fall'), ('infection', 'Infection'), ('equipment_failure', 'Equipment Failure'), ('documentation_error', 'Documentation Error'), ('communication_failure', 'Communication Failure'), ('surgical_complication', 'Surgical Complication'), ('diagnostic_error', 'Diagnostic Error'), ('treatment_delay', 'Treatment Delay'), ('other', 'Other'), ] SEVERITY_CHOICES = [ ('no_harm', 'No Harm'), ('minor_harm', 'Minor Harm'), ('moderate_harm', 'Moderate Harm'), ('severe_harm', 'Severe Harm'), ('death', 'Death'), ] CATEGORY_CHOICES = [ ('patient_safety', 'Patient Safety'), ('medication', 'Medication'), ('infection_control', 'Infection Control'), ('equipment', 'Equipment'), ('documentation', 'Documentation'), ('communication', 'Communication'), ('surgical', 'Surgical'), ('diagnostic', 'Diagnostic'), ('treatment', 'Treatment'), ('environmental', 'Environmental'), ] STATUS_CHOICES = [ ('reported', 'Reported'), ('under_investigation', 'Under Investigation'), ('investigated', 'Investigated'), ('closed', 'Closed'), ('cancelled', 'Cancelled'), ] PRIORITY_CHOICES = [ ('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('urgent', 'Urgent'), ] tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='incident_reports') incident_number = models.CharField(max_length=50, unique=True) title = models.CharField(max_length=200) description = models.TextField() incident_type = models.CharField(max_length=30, choices=INCIDENT_TYPE_CHOICES) severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES) category = models.CharField(max_length=30, choices=CATEGORY_CHOICES) location = models.CharField(max_length=200) incident_date = models.DateField() incident_time = models.TimeField() discovered_date = models.DateTimeField() patient = models.ForeignKey('patients.PatientProfile', on_delete=models.SET_NULL, null=True, blank=True, related_name='incident_reports') witness_information = models.TextField(null=True, blank=True) reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='reported_incidents') status = models.CharField(max_length=30, choices=STATUS_CHOICES, default='reported') priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium') root_cause = models.TextField(blank=True) contributing_factors = models.TextField(blank=True) corrective_actions = models.TextField(blank=True) preventive_actions = models.TextField(blank=True) assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='assigned_incidents') due_date = models.DateField(null=True, blank=True) closed_date = models.DateTimeField(null=True, blank=True) is_confidential = models.BooleanField(default=False) regulatory_notification = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'incident_report' indexes = [ models.Index(fields=['tenant', 'incident_type', 'severity']), models.Index(fields=['status', 'priority', 'assigned_to']), models.Index(fields=['incident_date', 'patient']), ] ordering = ['-incident_date'] def __str__(self): return f"{self.incident_number}: {self.title}" def save(self, *args, **kwargs): if not self.incident_number: # Generate incident number from django.utils import timezone year = timezone.now().year count = IncidentReport.objects.filter( tenant=self.tenant, created_at__year=year ).count() + 1 self.incident_number = f"INC-{year}-{count:04d}" super().save(*args, **kwargs) class RiskAssessment(models.Model): """Risk assessments and risk management activities""" class RiskCategory(models.TextChoices): CLINICAL = 'CLINICAL', 'Clinical' OPERATIONAL = 'OPERATIONAL', 'Operational' FINANCIAL = 'FINANCIAL', 'Financial' REGULATORY = 'REGULATORY', 'Regulatory' REPUTATIONAL = 'REPUTATIONAL', 'Reputational' STRATEGIC = 'STRATEGIC', 'Strategic' TECHNOLOGY = 'TECHNOLOGY', 'Technology' ENVIRONMENTAL = 'ENVIRONMENTAL', 'Environmental' SECURITY = 'SECURITY', 'Security' LEGAL = 'LEGAL', 'Legal' class RiskType(models.TextChoices): PATIENT_SAFETY = 'PATIENT_SAFETY', 'Patient Safety' QUALITY = 'QUALITY', 'Quality' COMPLIANCE = 'COMPLIANCE', 'Compliance' FINANCIAL = 'FINANCIAL', 'Financial' OPERATIONAL = 'OPERATIONAL', 'Operational' STRATEGIC = 'STRATEGIC', 'Strategic' TECHNOLOGY = 'TECHNOLOGY', 'Technology' ENVIRONMENTAL = 'ENVIRONMENTAL', 'Environmental' SECURITY = 'SECURITY', 'Security' LEGAL = 'LEGAL', 'Legal' class Likelihood(models.IntegerChoices): VERY_LOW = 1, 'Very Low' LOW = 2, 'Low' MEDIUM = 3, 'Medium' HIGH = 4, 'High' VERY_HIGH = 5, 'Very High' class Impact(models.IntegerChoices): VERY_LOW = 1, 'Very Low' LOW = 2, 'Low' MEDIUM = 3, 'Medium' HIGH = 4, 'High' VERY_HIGH = 5, 'Very High' class RiskLevel(models.TextChoices): LOW = 'LOW', 'Low' MEDIUM = 'MEDIUM', 'Medium' HIGH = 'HIGH', 'High' CRITICAL = 'CRITICAL', 'Critical' class ControlEffectiveness(models.TextChoices): POOR = 'POOR', 'Poor' FAIR = 'FAIR', 'Fair' GOOD = 'GOOD', 'Good' EXCELLENT = 'EXCELLENT', 'Excellent' class RiskStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' ACTIVE = 'ACTIVE', 'Active' UNDER_REVIEW = 'UNDER_REVIEW', 'Under Review' CLOSED = 'CLOSED', 'Closed' CANCELLED = 'CANCELLED', 'Cancelled' tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='risk_assessments') title = models.CharField(max_length=200) description = models.TextField() risk_category = models.CharField(max_length=20, choices=RiskCategory.choices) risk_type = models.CharField(max_length=20, choices=RiskType.choices) likelihood = models.IntegerField(choices=Likelihood.choices) impact = models.IntegerField(choices=Impact.choices) risk_score = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(25)]) risk_level = models.CharField(max_length=20, choices=RiskLevel.choices) current_controls = models.TextField() control_effectiveness = models.CharField(max_length=20, choices=ControlEffectiveness.choices) mitigation_plan = models.TextField() target_completion_date = models.DateTimeField() responsible_person = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_risks') review_date = models.DateField() review_notes = models.TextField(null=True, blank=True) status = models.CharField(max_length=20, choices=RiskStatus.choices, default=RiskStatus.DRAFT) incident_report = models.ForeignKey(IncidentReport, on_delete=models.SET_NULL, null=True, blank=True, related_name='risk_assessments') created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_risk_assessments') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'risk_assessment' indexes = [ models.Index(fields=['tenant', 'risk_category', 'risk_level']), models.Index(fields=['status', 'responsible_person']), models.Index(fields=['review_date', 'created_by']), ] ordering = ['-risk_score', '-created_at'] def __str__(self): return f"{self.title} ({self.risk_level})" def save(self, *args, **kwargs): """Auto-calculate risk scores and levels""" # Risk score calculation (1-25 scale) likelihood_values = {'very_low': 1, 'low': 2, 'medium': 3, 'high': 4, 'very_high': 5} impact_values = {'very_low': 1, 'low': 2, 'medium': 3, 'high': 4, 'very_high': 5} self.risk_score = likelihood_values[self.likelihood] * impact_values[self.impact] self.residual_risk_score = likelihood_values[self.residual_likelihood] * impact_values[self.residual_impact] # Risk level determination if self.risk_score >= 20: self.risk_level = 'critical' elif self.risk_score >= 12: self.risk_level = 'high' elif self.risk_score >= 6: self.risk_level = 'medium' else: self.risk_level = 'low' if self.residual_risk_score >= 20: self.residual_risk_level = 'critical' elif self.residual_risk_score >= 12: self.residual_risk_level = 'high' elif self.residual_risk_score >= 6: self.residual_risk_level = 'medium' else: self.residual_risk_level = 'low' super().save(*args, **kwargs) class AuditPlan(models.Model): """Quality audits and compliance monitoring plans""" class AuditType(models.TextChoices): INTERNAL = 'INTERNAL', 'Internal' EXTERNAL = 'EXTERNAL', 'External' REGULATORY = 'REGULATORY', 'Regulatory' ACCREDITATION = 'ACCREDITATION', 'Accreditation' QUALITY = 'QUALITY', 'Quality' COMPLIANCE = 'COMPLIANCE', 'Compliance' SAFETY = 'SAFETY', 'Safety' OPERATIONAL = 'OPERATIONAL', 'Operational' FINANCIAL = 'FINANCIAL', 'Financial' CLINICAL = 'CLINICAL', 'Clinical' class AuditStatus(models.TextChoices): PLANNED = 'PLANNED', 'Planned' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' POSTPONED = 'POSTPONED', 'Postponed' class AuditPriority(models.TextChoices): LOW = 'LOW', 'Low' MEDIUM = 'MEDIUM', 'Medium' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='audit_plans') title = models.CharField(max_length=200) description = models.TextField() audit_type = models.CharField(max_length=20, choices=AuditType.choices) scope = models.TextField() criteria = models.TextField() department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='audit_plans') auditor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='led_audits') audit_team = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='audit_team_memberships', blank=True) planned_start_date = models.DateField() planned_end_date = models.DateField() actual_start_date = models.DateField(null=True, blank=True) actual_end_date = models.DateField(null=True, blank=True) status = models.CharField(max_length=20, choices=AuditStatus.choices, default=AuditStatus.PLANNED) priority = models.CharField(max_length=10, choices=AuditPriority.choices, default=AuditPriority.MEDIUM) regulatory_requirement = models.BooleanField(default=False) accreditation_body = models.CharField(max_length=200, blank=True) objectives = models.TextField(blank=True) notes = models.TextField(blank=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_audit_plans') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'audit_plan' indexes = [ models.Index(fields=['tenant', 'audit_type', 'status']), models.Index(fields=['department', 'auditor']), models.Index(fields=['planned_start_date', 'planned_end_date']), ] ordering = ['planned_start_date'] def __str__(self): return f"{self.title} ({self.audit_type})" @property def findings_count(self): """Get count of findings for this audit""" return self.findings.count() @property def open_findings_count(self): """Get count of open findings""" return self.findings.exclude(status='closed').count() class AuditFinding(models.Model): """Audit findings, non-conformities, and observations""" class FindingType(models.TextChoices): NON_CONFORMITY = 'NON_CONFORMITY', 'Non-Conformity' OBSERVATION = 'OBSERVATION', 'Observation' OPPORTUNITY_FOR_IMPROVEMENT = 'OPPORTUNITY_FOR_IMPROVEMENT', 'Opportunity for Improvement' POSITIVE_FINDING = 'POSITIVE_FINDING', 'Positive Finding' RECOMMENDATION = 'RECOMMENDATION', 'Recommendation' class FindingSeverity(models.TextChoices): MINOR = 'MINOR', 'Minor' MAJOR = 'MAJOR', 'Major' CRITICAL = 'CRITICAL', 'Critical' class FindingCategory(models.TextChoices): DOCUMENTATION = 'DOCUMENTATION', 'Documentation' PROCESS = 'PROCESS', 'Process' TRAINING = 'TRAINING', 'Training' EQUIPMENT = 'EQUIPMENT', 'Equipment' ENVIRONMENT = 'ENVIRONMENT', 'Environment' MANAGEMENT = 'MANAGEMENT', 'Management' COMMUNICATION = 'COMMUNICATION', 'Communication' SAFETY = 'SAFETY', 'Safety' QUALITY = 'QUALITY', 'Quality' COMPLIANCE = 'COMPLIANCE', 'Compliance' class FindingStatus(models.TextChoices): OPEN = 'OPEN', 'Open' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' VERIFIED = 'VERIFIED', 'Verified' CLOSED = 'CLOSED', 'Closed' tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='audit_findings') audit_plan = models.ForeignKey(AuditPlan, on_delete=models.CASCADE, related_name='findings') finding_number = models.CharField(max_length=50) title = models.CharField(max_length=200) description = models.TextField() finding_type = models.CharField(max_length=30, choices=FindingType.choices) finding_date = models.DateField() severity = models.CharField(max_length=20, choices=FindingSeverity.choices) category = models.CharField(max_length=20, choices=FindingCategory.choices) criteria_reference = models.CharField(max_length=200) evidence = models.TextField() root_cause = models.TextField(blank=True) corrective_action_required = models.BooleanField(default=True) corrective_actions = models.TextField(blank=True) responsible_person = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='responsible_findings') target_completion_date = models.DateField(null=True, blank=True) actual_completion_date = models.DateField(null=True, blank=True) status = models.CharField(max_length=20, choices=FindingStatus.choices, default=FindingStatus.OPEN) verification_method = models.CharField(max_length=200, blank=True) verified_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_findings') verified_date = models.DateField(null=True, blank=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_findings') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'audit_finding' indexes = [ models.Index(fields=['tenant', 'audit_plan', 'finding_type']), models.Index(fields=['severity', 'status', 'responsible_person']), models.Index(fields=['target_completion_date', 'verified_by']), ] ordering = ['-severity', 'target_completion_date'] unique_together = ['audit_plan', 'finding_number'] def __str__(self): return f"{self.finding_number}: {self.title}" def save(self, *args, **kwargs): if not self.finding_number: # Generate finding number count = AuditFinding.objects.filter(audit_plan=self.audit_plan).count() + 1 self.finding_number = f"{self.audit_plan.id}-F{count:03d}" super().save(*args, **kwargs) class ImprovementProject(models.Model): """Quality improvement projects and initiatives""" class ProjectType(models.TextChoices): QUALITY_IMPROVEMENT = 'QUALITY_IMPROVEMENT', 'Quality Improvement' PROCESS_IMPROVEMENT = 'PROCESS_IMPROVEMENT', 'Process Improvement' SAFETY_INITIATIVE = 'SAFETY_INITIATIVE', 'Safety Initiative' COMPLIANCE_PROJECT = 'COMPLIANCE_PROJECT', 'Compliance Project' COST_REDUCTION = 'COST_REDUCTION', 'Cost Reduction' EFFICIENCY_IMPROVEMENT = 'EFFICIENCY_IMPROVEMENT', 'Efficiency Improvement' PATIENT_SATISFACTION = 'PATIENT_SATISFACTION', 'Patient Satisfaction' STAFF_SATISFACTION = 'STAFF_SATISFACTION', 'Staff Satisfaction' TECHNOLOGY_IMPLEMENTATION = 'TECHNOLOGY_IMPLEMENTATION', 'Technology Implementation' TRAINING_PROGRAM = 'TRAINING_PROGRAM', 'Training Program' class Methodology(models.TextChoices): PDSA = 'PDSA', 'PDSA (Plan-Do-Study-Act)' LEAN = 'LEAN', 'Lean' SIX_SIGMA = 'SIX_SIGMA', 'Six Sigma' KAIZEN = 'KAIZEN', 'Kaizen' ROOT_CAUSE_ANALYSIS = 'ROOT_CAUSE_ANALYSIS', 'Root Cause Analysis' FAILURE_MODE_ANALYSIS = 'FAILURE_MODE_ANALYSIS', 'Failure Mode Analysis' OTHER = 'OTHER', 'Other' class ProjectStatus(models.TextChoices): PLANNED = 'PLANNED', 'Planned' ACTIVE = 'ACTIVE', 'Active' ON_HOLD = 'ON_HOLD', 'On Hold' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' class ProjectPhase(models.TextChoices): DEFINE = 'DEFINE', 'Define' MEASURE = 'MEASURE', 'Measure' ANALYZE = 'ANALYZE', 'Analyze' IMPROVE = 'IMPROVE', 'Improve' CONTROL = 'CONTROL', 'Control' PLAN = 'PLAN', 'Plan' DO = 'DO', 'Do' STUDY = 'STUDY', 'Study' ACT = 'ACT', 'Act' tenant = models.ForeignKey('core.Tenant', on_delete=models.CASCADE, related_name='improvement_projects') project_number = models.CharField(max_length=50, unique=True) title = models.CharField(max_length=200) description = models.TextField() project_type = models.CharField(max_length=30, choices=ProjectType.choices) methodology = models.CharField(max_length=30, choices=Methodology.choices) problem_statement = models.TextField() goal_statement = models.TextField() success_metrics = models.TextField() baseline_data = models.TextField(blank=True) target_metrics = models.TextField() scope = models.TextField() project_manager = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_projects') project_team = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='project_team_memberships', blank=True) sponsor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='sponsored_projects') # stakeholders = models.ManyToManyField(User, related_name='stakeholder_memberships', blank=True) department = models.ForeignKey('hr.Department', on_delete=models.SET_NULL, null=True, blank=True, related_name='improvement_projects') planned_start_date = models.DateField() planned_end_date = models.DateField() actual_start_date = models.DateField(null=True, blank=True) actual_end_date = models.DateField(null=True, blank=True) status = models.CharField(max_length=20, choices=ProjectStatus.choices, default=ProjectStatus.PLANNED) phase = models.CharField(max_length=20, choices=ProjectPhase.choices, default=ProjectPhase.DEFINE) estimated_cost = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) resources_required = models.TextField(blank=True) actual_cost = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) roi_expected = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) roi_actual = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) lessons_learned = models.TextField(blank=True) notes = models.TextField(blank=True) sustainability_plan = models.TextField(blank=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='created_improvement_projects') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'improvement_project' indexes = [ models.Index(fields=['tenant', 'project_type', 'status']), models.Index(fields=['project_manager', 'sponsor', 'department']), models.Index(fields=['planned_start_date', 'planned_end_date']), ] ordering = ['planned_start_date'] def __str__(self): return f"{self.project_number}: {self.title}" def save(self, *args, **kwargs): if not self.project_number: # Generate project number from django.utils import timezone year = timezone.now().year count = ImprovementProject.objects.filter( tenant=self.tenant, created_at__year=year ).count() + 1 self.project_number = f"QI-{year}-{count:04d}" super().save(*args, **kwargs) @property def duration_planned(self): """Get planned project duration in days""" if self.planned_start_date and self.planned_end_date: return (self.planned_end_date - self.planned_start_date).days return None @property def duration_actual(self): """Get actual project duration in days""" if self.actual_start_date and self.actual_end_date: return (self.actual_end_date - self.actual_start_date).days return None