Marwan Alwali 610e165e17 update
2025-09-04 19:19:52 +03:00

526 lines
21 KiB
Python

from django.db import models
# from django.contrib.auth.models import User
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from datetime import timedelta
from core.models import Department
from patients.models import PatientProfile
from accounts.models import User
class BloodGroup(models.Model):
"""Blood group types (A, B, AB, O) with Rh factor"""
ABO_CHOICES = [
('A', 'A'),
('B', 'B'),
('AB', 'AB'),
('O', 'O'),
]
RH_CHOICES = [
('POS', 'Positive'),
('NEG', 'Negative'),
]
abo_type = models.CharField(max_length=2, choices=ABO_CHOICES)
rh_factor = models.CharField(max_length=8, choices=RH_CHOICES)
class Meta:
unique_together = ['abo_type', 'rh_factor']
ordering = ['abo_type', 'rh_factor']
def __str__(self):
return f"{self.abo_type} {self.rh_factor.capitalize()}"
@property
def display_name(self):
rh_symbol = '+' if self.rh_factor == 'POS' else '-'
return f"{self.abo_type}{rh_symbol}"
class Donor(models.Model):
"""Blood donor information and eligibility"""
DONOR_TYPE_CHOICES = [
('voluntary', 'Voluntary'),
('replacement', 'Replacement'),
('autologous', 'Autologous'),
('directed', 'Directed'),
]
STATUS_CHOICES = [
('active', 'Active'),
('deferred', 'Deferred'),
('permanently_deferred', 'Permanently Deferred'),
('inactive', 'Inactive'),
]
donor_id = models.CharField(max_length=20, unique=True)
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
date_of_birth = models.DateField()
gender = models.CharField(max_length=10, choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')])
national_id = models.CharField(max_length=10, unique=True)
blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
phone = models.CharField(max_length=20)
email = models.EmailField(blank=True)
address = models.TextField()
emergency_contact_name = models.CharField(max_length=100)
emergency_contact_phone = models.CharField(max_length=20)
donor_type = models.CharField(max_length=20, choices=DONOR_TYPE_CHOICES, default='voluntary')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='active')
registration_date = models.DateTimeField(auto_now_add=True)
last_donation_date = models.DateTimeField(null=True, blank=True)
total_donations = models.PositiveIntegerField(default=0)
weight = models.FloatField(validators=[MinValueValidator(45.0)]) # Minimum weight for donation
height = models.FloatField(validators=[MinValueValidator(140.0)]) # In cm
notes = models.TextField(blank=True)
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='created_donors')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-registration_date']
def __str__(self):
return f"{self.donor_id} - {self.first_name} {self.last_name}"
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def age(self):
today = timezone.now().date()
return today.year - self.date_of_birth.year - (
(today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day))
@property
def is_eligible_for_donation(self):
"""Check if donor is eligible for donation based on last donation date"""
if self.status != 'active':
return False
if not self.last_donation_date:
return True
# Minimum 56 days between whole blood donations
days_since_last = (timezone.now() - self.last_donation_date).days
return days_since_last >= 56
@property
def next_eligible_date(self):
"""Calculate next eligible donation date"""
if not self.last_donation_date:
return timezone.now().date()
return (self.last_donation_date + timedelta(days=56)).date()
class BloodComponent(models.Model):
"""Types of blood components (Whole Blood, RBC, Plasma, Platelets, etc.)"""
COMPONENT_CHOICES = [
('whole_blood', 'Whole Blood'),
('packed_rbc', 'Packed Red Blood Cells'),
('fresh_frozen_plasma', 'Fresh Frozen Plasma'),
('platelets', 'Platelets'),
('cryoprecipitate', 'Cryoprecipitate'),
('granulocytes', 'Granulocytes'),
]
name = models.CharField(max_length=50, choices=COMPONENT_CHOICES, unique=True)
description = models.TextField()
shelf_life_days = models.PositiveIntegerField() # Storage duration in days
storage_temperature = models.CharField(max_length=50) # Storage requirements
volume_ml = models.PositiveIntegerField() # Standard volume in ml
is_active = models.BooleanField(default=True)
class Meta:
ordering = ['name']
def __str__(self):
return self.get_name_display()
class BloodUnit(models.Model):
"""Individual blood unit from donation to disposal"""
STATUS_CHOICES = [
('collected', 'Collected'),
('testing', 'Testing'),
('quarantine', 'Quarantine'),
('available', 'Available'),
('reserved', 'Reserved'),
('issued', 'Issued'),
('transfused', 'Transfused'),
('expired', 'Expired'),
('discarded', 'Discarded'),
]
unit_number = models.CharField(max_length=20, unique=True)
donor = models.ForeignKey(Donor, on_delete=models.PROTECT, related_name='blood_units')
component = models.ForeignKey(BloodComponent, on_delete=models.PROTECT)
blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
collection_date = models.DateTimeField()
expiry_date = models.DateTimeField()
volume_ml = models.PositiveIntegerField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='collected')
location = models.CharField(max_length=100) # Storage location/refrigerator
bag_type = models.CharField(max_length=50)
anticoagulant = models.CharField(max_length=50, default='CPDA-1')
collection_site = models.CharField(max_length=100)
collected_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='collected_units')
notes = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-collection_date']
def __str__(self):
return f"{self.unit_number} - {self.component} ({self.blood_group})"
@property
def is_expired(self):
return timezone.now() > self.expiry_date
@property
def days_to_expiry(self):
delta = self.expiry_date - timezone.now()
return delta.days if delta.days > 0 else 0
@property
def is_available(self):
return self.status == 'available' and not self.is_expired
class BloodTest(models.Model):
"""Blood testing results for infectious diseases and compatibility"""
TEST_TYPE_CHOICES = [
('abo_rh', 'ABO/Rh Typing'),
('antibody_screen', 'Antibody Screening'),
('hiv', 'HIV'),
('hbv', 'Hepatitis B'),
('hcv', 'Hepatitis C'),
('syphilis', 'Syphilis'),
('htlv', 'HTLV'),
('cmv', 'CMV'),
('malaria', 'Malaria'),
]
RESULT_CHOICES = [
('positive', 'Positive'),
('negative', 'Negative'),
('indeterminate', 'Indeterminate'),
('pending', 'Pending'),
]
blood_unit = models.ForeignKey(BloodUnit, on_delete=models.CASCADE, related_name='tests')
test_type = models.CharField(max_length=20, choices=TEST_TYPE_CHOICES)
result = models.CharField(max_length=15, choices=RESULT_CHOICES, default='pending')
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
equipment_used = models.CharField(max_length=100, blank=True)
lot_number = models.CharField(max_length=50, blank=True)
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_tests', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ['blood_unit', 'test_type']
ordering = ['-test_date']
def __str__(self):
return f"{self.blood_unit.unit_number} - {self.get_test_type_display()}: {self.result}"
class CrossMatch(models.Model):
"""Cross-matching tests between donor blood and recipient"""
COMPATIBILITY_CHOICES = [
('compatible', 'Compatible'),
('incompatible', 'Incompatible'),
('pending', 'Pending'),
]
TEST_TYPE_CHOICES = [
('major', 'Major Crossmatch'),
('minor', 'Minor Crossmatch'),
('immediate_spin', 'Immediate Spin'),
('antiglobulin', 'Antiglobulin Test'),
]
blood_unit = models.ForeignKey(BloodUnit, on_delete=models.CASCADE, related_name='crossmatches')
recipient = models.ForeignKey(PatientProfile, on_delete=models.PROTECT)
test_type = models.CharField(max_length=20, choices=TEST_TYPE_CHOICES)
compatibility = models.CharField(max_length=15, choices=COMPATIBILITY_CHOICES, default='pending')
test_date = models.DateTimeField()
tested_by = models.ForeignKey(User, on_delete=models.PROTECT)
temperature = models.CharField(max_length=20, default='37°C')
incubation_time = models.PositiveIntegerField(default=15) # minutes
notes = models.TextField(blank=True)
verified_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='verified_crossmatches', null=True,
blank=True)
verified_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-test_date']
def __str__(self):
return f"{self.blood_unit.unit_number} x {self.recipient} - {self.compatibility}"
class BloodRequest(models.Model):
"""Blood transfusion requests from clinical departments"""
URGENCY_CHOICES = [
('routine', 'Routine'),
('urgent', 'Urgent'),
('emergency', 'Emergency'),
]
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('ready', 'Ready'),
('issued', 'Issued'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
]
request_number = models.CharField(max_length=20, unique=True)
patient = models.ForeignKey(PatientProfile, on_delete=models.PROTECT, related_name='blood_requests')
requesting_department = models.ForeignKey(Department, on_delete=models.PROTECT)
requesting_physician = models.ForeignKey(User, on_delete=models.PROTECT, related_name='blood_requests')
component_requested = models.ForeignKey(BloodComponent, on_delete=models.PROTECT)
units_requested = models.PositiveIntegerField(validators=[MinValueValidator(1)])
urgency = models.CharField(max_length=10, choices=URGENCY_CHOICES, default='routine')
indication = models.TextField() # Clinical indication for transfusion
special_requirements = models.TextField(blank=True) # CMV negative, irradiated, etc.
patient_blood_group = models.ForeignKey(BloodGroup, on_delete=models.PROTECT)
hemoglobin_level = models.FloatField(null=True, blank=True)
platelet_count = models.IntegerField(null=True, blank=True)
status = models.CharField(max_length=15, choices=STATUS_CHOICES, default='pending')
request_date = models.DateTimeField(auto_now_add=True)
required_by = models.DateTimeField()
processed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='processed_requests', null=True,
blank=True)
processed_at = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
# Cancellation fields
cancellation_reason = models.TextField(blank=True)
cancelled_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='cancelled_requests', null=True,
blank=True)
cancellation_date = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-request_date']
def __str__(self):
return f"{self.request_number} - {self.patient} ({self.component_requested})"
@property
def is_overdue(self):
return timezone.now() > self.required_by and self.status not in ['completed', 'cancelled']
class BloodIssue(models.Model):
"""Blood unit issuance to patients"""
blood_request = models.ForeignKey(BloodRequest, on_delete=models.PROTECT, related_name='issues')
blood_unit = models.OneToOneField(BloodUnit, on_delete=models.PROTECT, related_name='issue')
crossmatch = models.ForeignKey(CrossMatch, on_delete=models.PROTECT, null=True, blank=True)
issued_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='issued_units')
issued_to = models.ForeignKey(User, on_delete=models.PROTECT, related_name='received_units') # Nurse/physician
issue_date = models.DateTimeField(auto_now_add=True)
expiry_time = models.DateTimeField() # 4 hours from issue for RBC
returned = models.BooleanField(default=False)
return_date = models.DateTimeField(null=True, blank=True)
return_reason = models.TextField(blank=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ['-issue_date']
def __str__(self):
return f"{self.blood_unit.unit_number} issued to {self.blood_request.patient}"
@property
def is_expired(self):
return timezone.now() > self.expiry_time and not self.returned
class Transfusion(models.Model):
"""Blood transfusion administration records"""
STATUS_CHOICES = [
('started', 'Started'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('stopped', 'Stopped'),
('adverse_reaction', 'Adverse Reaction'),
]
blood_issue = models.OneToOneField(BloodIssue, on_delete=models.PROTECT, related_name='transfusion')
start_time = models.DateTimeField()
end_time = models.DateTimeField(null=True, blank=True)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='started')
volume_transfused = models.PositiveIntegerField(null=True, blank=True) # ml
transfusion_rate = models.CharField(max_length=50, blank=True) # ml/hour
administered_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='administered_transfusions')
witnessed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='witnessed_transfusions', null=True,
blank=True)
pre_transfusion_vitals = models.JSONField(default=dict) # BP, HR, Temp, etc.
post_transfusion_vitals = models.JSONField(default=dict)
vital_signs_history = models.JSONField(default=list) # Array of vital signs during transfusion
current_blood_pressure = models.CharField(max_length=20, blank=True)
current_heart_rate = models.IntegerField(null=True, blank=True)
current_temperature = models.FloatField(null=True, blank=True)
current_respiratory_rate = models.IntegerField(null=True, blank=True)
current_oxygen_saturation = models.IntegerField(null=True, blank=True)
last_vitals_check = models.DateTimeField(null=True, blank=True)
patient_consent = models.BooleanField(default=False)
consent_date = models.DateTimeField(null=True, blank=True)
notes = models.TextField(blank=True)
# Completion/Stop fields
stop_reason = models.TextField(blank=True)
stopped_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='stopped_transfusions', null=True,
blank=True)
completed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='completed_transfusions', null=True,
blank=True)
completion_notes = models.TextField(blank=True)
class Meta:
ordering = ['-start_time']
def __str__(self):
return f"Transfusion: {self.blood_issue.blood_unit.unit_number} to {self.blood_issue.blood_request.patient}"
@property
def duration_minutes(self):
if self.end_time:
return int((self.end_time - self.start_time).total_seconds() / 60)
return None
class AdverseReaction(models.Model):
"""Adverse transfusion reactions"""
SEVERITY_CHOICES = [
('mild', 'Mild'),
('moderate', 'Moderate'),
('severe', 'Severe'),
('life_threatening', 'Life Threatening'),
]
REACTION_TYPE_CHOICES = [
('febrile', 'Febrile Non-Hemolytic'),
('allergic', 'Allergic'),
('hemolytic_acute', 'Acute Hemolytic'),
('hemolytic_delayed', 'Delayed Hemolytic'),
('anaphylactic', 'Anaphylactic'),
('septic', 'Septic'),
('circulatory_overload', 'Circulatory Overload'),
('lung_injury', 'Transfusion-Related Acute Lung Injury'),
('other', 'Other'),
]
transfusion = models.ForeignKey(Transfusion, on_delete=models.CASCADE, related_name='adverse_reactions')
reaction_type = models.CharField(max_length=30, choices=REACTION_TYPE_CHOICES)
severity = models.CharField(max_length=20, choices=SEVERITY_CHOICES)
onset_time = models.DateTimeField()
symptoms = models.TextField()
treatment_given = models.TextField()
outcome = models.TextField()
reported_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reported_reactions')
investigated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='investigated_reactions',
null=True, blank=True)
investigation_notes = models.TextField(blank=True)
regulatory_reported = models.BooleanField(default=False)
report_date = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-onset_time']
def __str__(self):
return f"{self.get_reaction_type_display()} - {self.severity} ({self.transfusion.blood_issue.blood_request.patient})"
class InventoryLocation(models.Model):
"""Blood bank storage locations"""
LOCATION_TYPE_CHOICES = [
('refrigerator', 'Refrigerator'),
('freezer', 'Freezer'),
('platelet_agitator', 'Platelet Agitator'),
('quarantine', 'Quarantine'),
('testing', 'Testing Area'),
]
name = models.CharField(max_length=100, unique=True)
location_type = models.CharField(max_length=20, choices=LOCATION_TYPE_CHOICES)
temperature_range = models.CharField(max_length=50)
temperature = models.FloatField(null=True, blank=True) # Current temperature
capacity = models.PositiveIntegerField() # Number of units
current_stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
notes = models.TextField(blank=True)
class Meta:
ordering = ['name']
def __str__(self):
return f"{self.name} ({self.get_location_type_display()})"
@property
def utilization_percentage(self):
if self.capacity == 0:
return 0
return (self.current_stock / self.capacity) * 100
class QualityControl(models.Model):
"""Quality control tests and monitoring"""
TEST_TYPE_CHOICES = [
('temperature_monitoring', 'Temperature Monitoring'),
('equipment_calibration', 'Equipment Calibration'),
('reagent_testing', 'Reagent Testing'),
('proficiency_testing', 'Proficiency Testing'),
('process_validation', 'Process Validation'),
]
STATUS_CHOICES = [
('pass', 'Pass'),
('fail', 'Fail'),
('pending', 'Pending'),
]
test_type = models.CharField(max_length=30, choices=TEST_TYPE_CHOICES)
test_date = models.DateTimeField()
equipment_tested = models.CharField(max_length=100, blank=True)
parameters_tested = models.TextField()
expected_results = models.TextField()
actual_results = models.TextField()
status = models.CharField(max_length=10, choices=STATUS_CHOICES)
performed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='qc_tests')
reviewed_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='reviewed_qc_tests', null=True,
blank=True)
review_date = models.DateTimeField(null=True, blank=True)
review_notes = models.TextField(blank=True)
corrective_action = models.TextField(blank=True)
next_test_date = models.DateTimeField(null=True, blank=True)
# CAPA (Corrective and Preventive Action) fields
capa_initiated = models.BooleanField(default=False)
capa_number = models.CharField(max_length=50, blank=True)
capa_priority = models.CharField(max_length=10, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High')],
blank=True)
capa_initiated_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name='initiated_capas', null=True,
blank=True)
capa_date = models.DateTimeField(null=True, blank=True)
capa_assessment = models.TextField(blank=True)
capa_status = models.CharField(max_length=20,
choices=[('open', 'Open'), ('in_progress', 'In Progress'), ('closed', 'Closed')],
blank=True)
class Meta:
ordering = ['-test_date']
def __str__(self):
return f"{self.get_test_type_display()} - {self.test_date.strftime('%Y-%m-%d')} ({self.status})"