Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

637 lines
28 KiB
Python

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