526 lines
21 KiB
Python
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})"
|
|
|