2025-10-03 20:11:25 +03:00

1361 lines
39 KiB
Python

"""
Pharmacy app models for hospital management system.
Provides medication management, prescription processing, and pharmacy operations.
"""
import uuid
from django.db import models
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
from django.utils import timezone
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from datetime import timedelta, datetime, date
from decimal import Decimal
import json
class Medication(models.Model):
"""
Medication model for drug database and formulary management.
"""
class ControlledSubstanceSchedule(models.TextChoices):
CI = 'CI', 'Schedule I'
CII = 'CII', 'Schedule II'
CIII = 'CIII', 'Schedule III'
CIV = 'CIV', 'Schedule IV'
CV = 'CV', 'Schedule V'
NON = 'NON', 'Non-Controlled'
class DosageForm(models.TextChoices):
TABLET = 'TABLET', 'Tablet'
CAPSULE = 'CAPSULE', 'Capsule'
LIQUID = 'LIQUID', 'Liquid'
INJECTION = 'INJECTION', 'Injection'
TOPICAL = 'TOPICAL', 'Topical'
INHALER = 'INHALER', 'Inhaler'
PATCH = 'PATCH', 'Patch'
SUPPOSITORY = 'SUPPOSITORY', 'Suppository'
CREAM = 'CREAM', 'Cream'
OINTMENT = 'OINTMENT', 'Ointment'
DROPS = 'DROPS', 'Drops'
SPRAY = 'SPRAY', 'Spray'
OTHER = 'OTHER', 'Other'
class UnitOfMeasure(models.TextChoices):
MG = 'MG', 'Milligrams'
G = 'G', 'Grams'
MCG = 'MCG', 'Micrograms'
ML = 'ML', 'Milliliters'
L = 'L', 'Liters'
UNITS = 'UNITS', 'Units'
IU = 'IU', 'International Units'
MEQ = 'MEQ', 'Milliequivalents'
PERCENT = 'PERCENT', 'Percent'
OTHER = 'OTHER', 'Other'
class FormularyStatus(models.TextChoices):
PREFERRED = 'PREFERRED', 'Preferred'
NON_PREFERRED = 'NON_PREFERRED', 'Non-Preferred'
RESTRICTED = 'RESTRICTED', 'Restricted'
NOT_COVERED = 'NOT_COVERED', 'Not Covered'
PRIOR_AUTH = 'PRIOR_AUTH', 'Prior Authorization Required'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='medications',
help_text='Organization tenant'
)
# Medication Information
medication_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique medication identifier'
)
# Drug Identification
generic_name = models.CharField(
max_length=200,
help_text='Generic drug name'
)
brand_name = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Brand/trade name'
)
# NDC and Coding
ndc_number = models.CharField(
max_length=20,
blank=True,
null=True,
validators=[RegexValidator(r'^\d{4,5}-\d{3,4}-\d{1,2}$')],
help_text='National Drug Code (NDC) number'
)
rxcui = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='RxNorm Concept Unique Identifier'
)
# Drug Classification
drug_class = models.CharField(
max_length=100,
help_text='Therapeutic drug class'
)
controlled_substance_schedule = models.CharField(
max_length=5,
choices=ControlledSubstanceSchedule.choices,
default=ControlledSubstanceSchedule.NON,
help_text='DEA controlled substance schedule'
)
# Formulation
dosage_form = models.CharField(
max_length=50,
choices=DosageForm.choices,
help_text='Dosage form'
)
strength = models.CharField(
max_length=50,
help_text='Drug strength (e.g., 500mg, 10mg/ml)'
)
unit_of_measure = models.CharField(
max_length=20,
choices=UnitOfMeasure.choices,
help_text='Unit of measure'
)
# Clinical Information
indications = models.TextField(
help_text='Clinical indications'
)
contraindications = models.TextField(
blank=True,
null=True,
help_text='Contraindications'
)
side_effects = models.TextField(
blank=True,
null=True,
help_text='Common side effects'
)
warnings = models.TextField(
blank=True,
null=True,
help_text='Warnings and precautions'
)
# Dosing Information
adult_dose_range = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Adult dosing range'
)
pediatric_dose_range = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Pediatric dosing range'
)
max_daily_dose = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Maximum daily dose'
)
# Administration
routes_of_administration = models.JSONField(
default=list,
help_text='Routes of administration'
)
administration_instructions = models.TextField(
blank=True,
null=True,
help_text='Administration instructions'
)
# Storage and Handling
storage_requirements = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Storage requirements'
)
special_handling = models.TextField(
blank=True,
null=True,
help_text='Special handling requirements'
)
# Formulary Status
formulary_status = models.CharField(
max_length=20,
choices=FormularyStatus.choices,
default=FormularyStatus.PREFERRED,
help_text='Formulary status'
)
# Availability
is_active = models.BooleanField(
default=True,
help_text='Medication is active and available'
)
is_available = models.BooleanField(
default=True,
help_text='Currently available in inventory'
)
# Pricing
unit_cost = models.DecimalField(
max_digits=10,
decimal_places=2,
blank=True,
null=True,
help_text='Cost per unit'
)
awp = models.DecimalField(
max_digits=10,
decimal_places=2,
blank=True,
null=True,
help_text='Average Wholesale Price'
)
# Manufacturer Information
manufacturer = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Manufacturer name'
)
manufacturer_ndc = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Manufacturer NDC'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_medications',
help_text='User who created the medication record'
)
class Meta:
db_table = 'pharmacy_medication'
verbose_name = 'Medication'
verbose_name_plural = 'Medications'
ordering = ['generic_name']
indexes = [
models.Index(fields=['tenant', 'is_active']),
models.Index(fields=['generic_name']),
models.Index(fields=['brand_name']),
models.Index(fields=['ndc_number']),
models.Index(fields=['drug_class']),
models.Index(fields=['controlled_substance_schedule']),
]
unique_together = ['tenant', 'ndc_number']
def __str__(self):
if self.brand_name:
return f"{self.generic_name} ({self.brand_name}) {self.strength}"
return f"{self.generic_name} {self.strength}"
@property
def is_controlled_substance(self):
"""
Check if medication is a controlled substance.
"""
return self.controlled_substance_schedule != 'NON'
@property
def display_name(self):
"""
Get display name for medication.
"""
return str(self)
class Prescription(models.Model):
"""
Prescription model for electronic prescription management.
"""
class QuantityUnit(models.TextChoices):
TABLETS = 'TABLETS', 'Tablets'
CAPSULES = 'CAPSULES', 'Capsules'
ML = 'ML', 'Milliliters'
GRAMS = 'GRAMS', 'Grams'
UNITS = 'UNITS', 'Units'
PATCHES = 'PATCHES', 'Patches'
INHALERS = 'INHALERS', 'Inhalers'
BOTTLES = 'BOTTLES', 'Bottles'
TUBES = 'TUBES', 'Tubes'
VIALS = 'VIALS', 'Vials'
OTHER = 'OTHER', 'Other'
class PrescriptionStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
ACTIVE = 'ACTIVE', 'Active'
DISPENSED = 'DISPENSED', 'Dispensed'
PARTIALLY_DISPENSED = 'PARTIALLY_DISPENSED', 'Partially Dispensed'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'
EXPIRED = 'EXPIRED', 'Expired'
ON_HOLD = 'ON_HOLD', 'On Hold'
TRANSFERRED = 'TRANSFERRED', 'Transferred'
DRAFT = 'DRAFT', 'Draft'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='prescriptions',
help_text='Organization tenant'
)
# Prescription Information
prescription_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique prescription identifier'
)
prescription_number = models.CharField(
max_length=20,
unique=True,
help_text='Prescription number'
)
# Patient and Provider
patient = models.ForeignKey(
'patients.PatientProfile',
on_delete=models.CASCADE,
related_name='prescriptions',
help_text='Patient'
)
prescriber = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='prescribed_medications',
help_text='Prescribing provider'
)
# Medication
medication = models.ForeignKey(
Medication,
on_delete=models.CASCADE,
related_name='prescriptions',
help_text='Prescribed medication'
)
# Prescription Details
quantity_prescribed = models.PositiveIntegerField(
help_text='Quantity prescribed'
)
quantity_unit = models.CharField(
max_length=20,
choices=QuantityUnit.choices,
help_text='Unit of quantity'
)
# Dosing Instructions
dosage_instructions = models.TextField(
help_text='Dosing instructions (SIG)'
)
frequency = models.CharField(
max_length=50,
help_text='Frequency of administration'
)
duration = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Duration of therapy'
)
# Refills
refills_authorized = models.PositiveIntegerField(
default=0,
help_text='Number of refills authorized'
)
refills_remaining = models.PositiveIntegerField(
default=0,
help_text='Number of refills remaining'
)
# Dates
date_prescribed = models.DateTimeField(
default=timezone.now,
help_text='Date and time prescribed'
)
date_written = models.DateField(
help_text='Date written on prescription'
)
expiration_date = models.DateField(
blank=True,
null=True,
help_text='Prescription expiration date'
)
# Status
status = models.CharField(
max_length=20,
choices=PrescriptionStatus.choices,
default=PrescriptionStatus.PENDING,
help_text='Prescription status'
)
# Clinical Information
indication = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Indication for prescription'
)
diagnosis_code = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='ICD-10 diagnosis code'
)
# Special Instructions
pharmacy_notes = models.TextField(
blank=True,
null=True,
help_text='Notes for pharmacy'
)
patient_instructions = models.TextField(
blank=True,
null=True,
help_text='Patient instructions'
)
# Prior Authorization
prior_authorization_required = models.BooleanField(
default=False,
help_text='Prior authorization required'
)
prior_authorization_number = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Prior authorization number'
)
prior_authorization_expiry = models.DateField(
blank=True,
null=True,
help_text='Prior authorization expiry date'
)
# Generic Substitution
generic_substitution_allowed = models.BooleanField(
default=True,
help_text='Generic substitution allowed'
)
dispense_as_written = models.BooleanField(
default=False,
help_text='Dispense as written (DAW)'
)
# Electronic Prescription
electronic_prescription = models.BooleanField(
default=True,
help_text='Electronic prescription'
)
e_prescription_id = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Electronic prescription ID'
)
# Related Information
encounter = models.ForeignKey(
'emr.Encounter',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='prescriptions',
help_text='Related encounter'
)
# Verification
verified = models.BooleanField(
default=False,
help_text='Prescription has been verified'
)
verified_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='verified_prescriptions',
help_text='Pharmacist who verified prescription'
)
verified_datetime = models.DateTimeField(
blank=True,
null=True,
help_text='Date and time of verification'
)
# Insurance Approval Integration
approval_requests = GenericRelation(
'insurance_approvals.InsuranceApprovalRequest',
content_type_field='content_type',
object_id_field='object_id',
related_query_name='prescription'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'pharmacy_prescription'
verbose_name = 'Prescription'
verbose_name_plural = 'Prescriptions'
ordering = ['-date_prescribed']
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['patient', 'status']),
models.Index(fields=['prescriber']),
models.Index(fields=['medication']),
models.Index(fields=['date_prescribed']),
models.Index(fields=['prescription_number']),
]
def __str__(self):
return f"{self.prescription_number} - {self.patient.get_full_name()} - {self.medication.display_name}"
def save(self, *args, **kwargs):
"""
Generate prescription number if not provided.
"""
if not self.prescription_number:
# Generate prescription number (simple implementation)
last_rx = Prescription.objects.filter(tenant=self.tenant).order_by('-id').first()
if last_rx:
last_number = int(last_rx.prescription_number.split('-')[-1])
self.prescription_number = f"RX-{self.tenant.id}-{last_number + 1:06d}"
else:
self.prescription_number = f"RX-{self.tenant.id}-000001"
# Set expiration date if not provided
if not self.expiration_date:
if self.medication.is_controlled_substance:
# Controlled substances expire in 6 months
self.expiration_date = self.date_written + timedelta(days=180)
else:
# Non-controlled substances expire in 1 year
self.expiration_date = self.date_written + timedelta(days=365)
# Set refills remaining
if self.refills_remaining == 0 and self.refills_authorized > 0:
self.refills_remaining = self.refills_authorized
super().save(*args, **kwargs)
@property
def is_expired(self):
"""
Check if prescription is expired.
"""
return timezone.now().date() > self.expiration_date
@property
def days_until_expiry(self):
"""
Calculate days until expiry.
"""
if self.expiration_date:
return (self.expiration_date - timezone.now().date()).days
return None
def has_valid_approval(self):
"""
Check if prescription has a valid insurance approval.
"""
from django.utils import timezone
return self.approval_requests.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
expiration_date__gte=timezone.now().date()
).exists()
def get_active_approval(self):
"""
Get the active insurance approval for this prescription.
"""
from django.utils import timezone
return self.approval_requests.filter(
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
expiration_date__gte=timezone.now().date()
).first()
def requires_approval(self):
"""
Check if prescription requires insurance approval.
Returns True if patient has insurance and no valid approval exists.
Also checks if prior authorization is required.
"""
if not self.patient.insurance_info.exists():
return False
# Check if prior authorization is explicitly required
if self.prior_authorization_required and not self.prior_authorization_number:
return True
# Check if medication requires approval based on formulary status
if self.medication.formulary_status in ['RESTRICTED', 'PRIOR_AUTH']:
return not self.has_valid_approval()
return False
@property
def approval_status(self):
"""
Get current approval status for display.
"""
if not self.patient.insurance_info.exists():
return 'NO_INSURANCE'
# Check prior authorization
if self.prior_authorization_required:
if self.prior_authorization_number:
# Check if prior auth is expired
if self.prior_authorization_expiry and self.prior_authorization_expiry < timezone.now().date():
return 'PRIOR_AUTH_EXPIRED'
return 'PRIOR_AUTH_APPROVED'
return 'PRIOR_AUTH_REQUIRED'
# Check approval requests
latest_approval = self.approval_requests.order_by('-created_at').first()
if not latest_approval:
if self.medication.formulary_status in ['RESTRICTED', 'PRIOR_AUTH']:
return 'APPROVAL_REQUIRED'
return 'NO_APPROVAL_NEEDED'
if self.has_valid_approval():
return 'APPROVED'
return latest_approval.status
class MedicationInventoryItem(models.Model):
"""
Bridge model linking medications to centralized inventory system.
This model provides medication-specific metadata for inventory items.
"""
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='medication_inventory_items',
help_text='Organization tenant'
)
# Medication relationship
medication = models.ForeignKey(
Medication,
on_delete=models.CASCADE,
related_name='medication_inventory_items',
help_text='Related medication'
)
# Centralized inventory item relationship
inventory_item = models.ForeignKey(
'inventory.InventoryItem',
on_delete=models.CASCADE,
related_name='medication_inventory_items',
help_text='Centralized inventory item'
)
# Medication-specific information
medication_inventory_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique medication inventory identifier'
)
# Pharmacy-specific fields
formulary_tier = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Formulary tier for this medication'
)
therapeutic_equivalent = models.BooleanField(
default=False,
help_text='Therapeutic equivalent available'
)
auto_substitution_allowed = models.BooleanField(
default=True,
help_text='Automatic substitution allowed'
)
pharmacy_notes = models.TextField(
blank=True,
null=True,
help_text='Pharmacy-specific notes'
)
# Dispensing information
max_dispense_quantity = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Maximum quantity that can be dispensed at once'
)
requires_counseling = models.BooleanField(
default=False,
help_text='Requires patient counseling'
)
requires_id_verification = models.BooleanField(
default=False,
help_text='Requires ID verification for pickup'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_medication_inventory_items',
help_text='User who created the medication inventory item'
)
class Meta:
db_table = 'pharmacy_medication_inventory_item'
verbose_name = 'Medication Inventory Item'
verbose_name_plural = 'Medication Inventory Items'
ordering = ['medication__generic_name']
indexes = [
models.Index(fields=['tenant', 'medication']),
models.Index(fields=['inventory_item']),
models.Index(fields=['medication']),
]
unique_together = ['tenant', 'medication', 'inventory_item']
def __str__(self):
return f"{self.medication.display_name} -> {self.inventory_item.item_name}"
@property
def current_stock(self):
"""
Get current stock from related inventory stocks.
"""
return sum(
stock.quantity_on_hand
for stock in self.inventory_item.inventory_stocks.filter(
quality_status='GOOD'
)
)
@property
def available_stock(self):
"""
Get available stock from related inventory stocks.
"""
return sum(
stock.quantity_available
for stock in self.inventory_item.inventory_stocks.filter(
quality_status='GOOD'
)
)
@property
def needs_reorder(self):
"""
Check if medication needs reordering.
"""
return self.current_stock <= self.inventory_item.reorder_point
# Legacy alias for backward compatibility during migration
InventoryItem = MedicationInventoryItem
class DispenseRecord(models.Model):
"""
Dispense record model for tracking medication dispensing.
"""
class DispenseStatus(models.TextChoices):
DISPENSED = 'DISPENSED', 'Dispensed'
PICKED_UP = 'PICKED_UP', 'Picked Up'
RETURNED = 'RETURNED', 'Returned'
CANCELLED = 'CANCELLED', 'Cancelled'
# Prescription relationship
prescription = models.ForeignKey(
Prescription,
on_delete=models.CASCADE,
related_name='dispense_records',
help_text='Related prescription'
)
# Inventory stock from centralized system
inventory_stock = models.ForeignKey(
'inventory.InventoryStock',
on_delete=models.CASCADE,
related_name='dispense_records',
help_text='Inventory stock dispensed from'
)
# Dispense Information
dispense_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique dispense identifier'
)
# Quantity
quantity_dispensed = models.PositiveIntegerField(
help_text='Quantity dispensed'
)
quantity_remaining = models.PositiveIntegerField(
help_text='Quantity remaining on prescription'
)
# Dates
date_dispensed = models.DateTimeField(
default=timezone.now,
help_text='Date and time dispensed'
)
# Staff
dispensed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='dispensed_medications',
help_text='Pharmacist who dispensed medication'
)
verified_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='verified_dispenses',
help_text='Pharmacist who verified dispense'
)
# Pricing
unit_price = models.DecimalField(
max_digits=10,
decimal_places=2,
help_text='Price per unit'
)
total_price = models.DecimalField(
max_digits=10,
decimal_places=2,
help_text='Total price'
)
copay_amount = models.DecimalField(
max_digits=8,
decimal_places=2,
default=Decimal('0.00'),
help_text='Patient copay amount'
)
insurance_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=Decimal('0.00'),
help_text='Insurance payment amount'
)
# Patient Information
patient_counseled = models.BooleanField(
default=False,
help_text='Patient was counseled'
)
counseling_notes = models.TextField(
blank=True,
null=True,
help_text='Counseling notes'
)
# Refill Information
is_refill = models.BooleanField(
default=False,
help_text='This is a refill'
)
refill_number = models.PositiveIntegerField(
default=0,
help_text='Refill number'
)
# Status
status = models.CharField(
max_length=20,
choices=DispenseStatus.choices,
default=DispenseStatus.DISPENSED,
help_text='Dispense status'
)
# Pickup Information
picked_up_by = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Person who picked up medication'
)
pickup_datetime = models.DateTimeField(
blank=True,
null=True,
help_text='Date and time of pickup'
)
identification_verified = models.BooleanField(
default=False,
help_text='Identification was verified'
)
# Quality Control
quality_check_performed = models.BooleanField(
default=False,
help_text='Quality check performed'
)
quality_notes = models.TextField(
blank=True,
null=True,
help_text='Quality control notes'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'pharmacy_dispense_record'
verbose_name = 'Dispense Record'
verbose_name_plural = 'Dispense Records'
ordering = ['-date_dispensed']
indexes = [
models.Index(fields=['prescription']),
models.Index(fields=['inventory_stock']),
models.Index(fields=['date_dispensed']),
models.Index(fields=['dispensed_by']),
models.Index(fields=['status']),
]
def __str__(self):
return f"{self.prescription.prescription_number} - {self.quantity_dispensed} units"
def save(self, *args, **kwargs):
"""
Calculate total price and update prescription status.
"""
self.total_price = self.quantity_dispensed * self.unit_price
# Update prescription quantity remaining
if self.prescription:
total_dispensed = self.prescription.dispense_records.aggregate(
total=models.Sum('quantity_dispensed')
)['total'] or 0
if not self.pk: # New dispense record
total_dispensed += self.quantity_dispensed
self.quantity_remaining = max(0, self.prescription.quantity_prescribed - total_dispensed)
# Update prescription status
if self.quantity_remaining == 0:
self.prescription.status = 'COMPLETED'
elif total_dispensed > 0:
self.prescription.status = 'PARTIALLY_DISPENSED'
else:
self.prescription.status = 'DISPENSED'
self.prescription.save()
super().save(*args, **kwargs)
@property
def patient(self):
"""
Get patient from prescription.
"""
return self.prescription.patient
@property
def medication(self):
"""
Get medication from inventory stock.
"""
return self.inventory_stock.inventory_item.medication_inventory_items.first().medication
class MedicationAdministration(models.Model):
"""
Medication Administration Record (MAR) for inpatient medication tracking.
"""
class RouteGiven(models.TextChoices):
PO = 'PO', 'Oral'
IV = 'IV', 'Intravenous'
IM = 'IM', 'Intramuscular'
SC = 'SC', 'Subcutaneous'
SL = 'SL', 'Sublingual'
TOP = 'TOP', 'Topical'
INH = 'INH', 'Inhalation'
PR = 'PR', 'Rectal'
PV = 'PV', 'Vaginal'
NASAL = 'NASAL', 'Nasal'
OPTH = 'OPTH', 'Ophthalmic'
OTIC = 'OTIC', 'Otic'
OTHER = 'OTHER', 'Other'
class MedicationStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
GIVEN = 'GIVEN', 'Given'
NOT_GIVEN = 'NOT_GIVEN', 'Not Given'
HELD = 'HELD', 'Held'
REFUSED = 'REFUSED', 'Refused'
OMITTED = 'OMITTED', 'Omitted'
class ReasonNotGiven(models.TextChoices):
PATIENT_REFUSED = 'PATIENT_REFUSED', 'Patient Refused'
PATIENT_UNAVAILABLE = 'PATIENT_UNAVAILABLE', 'Patient Unavailable'
MEDICATION_UNAVAILABLE = 'MEDICATION_UNAVAILABLE', 'Medication Unavailable'
HELD_BY_PROVIDER = 'HELD_BY_PROVIDER', 'Held by Provider'
PATIENT_NPO = 'PATIENT_NPO', 'Patient NPO'
PATIENT_ASLEEP = 'PATIENT_ASLEEP', 'Patient Asleep'
ADVERSE_REACTION = 'ADVERSE_REACTION', 'Adverse Reaction'
OTHER = 'OTHER', 'Other'
# Prescription relationship
prescription = models.ForeignKey(
Prescription,
on_delete=models.CASCADE,
related_name='administration_records',
help_text='Related prescription'
)
# Patient and Encounter
patient = models.ForeignKey(
'patients.PatientProfile',
on_delete=models.CASCADE,
related_name='medication_administrations',
help_text='Patient'
)
encounter = models.ForeignKey(
'emr.Encounter',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='medication_administrations',
help_text='Related encounter'
)
# Administration Information
administration_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique administration identifier'
)
# Scheduled vs Actual
scheduled_datetime = models.DateTimeField(
help_text='Scheduled administration time'
)
actual_datetime = models.DateTimeField(
blank=True,
null=True,
help_text='Actual administration time'
)
# Dosage
dose_given = models.CharField(
max_length=50,
help_text='Dose given'
)
route_given = models.CharField(
max_length=30,
choices=RouteGiven.choices,
help_text='Route of administration'
)
# Status
status = models.CharField(
max_length=20,
choices=MedicationStatus.choices,
default=MedicationStatus.SCHEDULED,
help_text='Administration status'
)
# Staff
administered_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='administered_medications',
help_text='Nurse who administered medication'
)
witnessed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='witnessed_administrations',
help_text='Witness (for controlled substances)'
)
# Reason for Not Given
reason_not_given = models.CharField(
max_length=100,
blank=True,
null=True,
choices=ReasonNotGiven.choices,
help_text='Reason medication was not given'
)
reason_notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes about reason'
)
# Patient Response
patient_response = models.TextField(
blank=True,
null=True,
help_text='Patient response to medication'
)
side_effects_observed = models.TextField(
blank=True,
null=True,
help_text='Side effects observed'
)
# Site Information (for injections)
injection_site = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Injection site'
)
site_condition = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Condition of injection site'
)
# Verification
double_checked = models.BooleanField(
default=False,
help_text='Medication was double-checked'
)
double_checked_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='double_checked_administrations',
help_text='Second nurse who verified medication'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'pharmacy_medication_administration'
verbose_name = 'Medication Administration'
verbose_name_plural = 'Medication Administrations'
ordering = ['-scheduled_datetime']
indexes = [
models.Index(fields=['patient', 'scheduled_datetime']),
models.Index(fields=['prescription']),
models.Index(fields=['encounter']),
models.Index(fields=['status']),
models.Index(fields=['administered_by']),
]
def __str__(self):
return f"{self.patient.get_full_name()} - {self.prescription.medication.display_name} - {self.scheduled_datetime}"
@property
def is_overdue(self):
"""
Check if administration is overdue.
"""
if self.status == 'SCHEDULED':
return timezone.now() > self.scheduled_datetime + timedelta(minutes=30)
return False
@property
def medication(self):
"""
Get medication from prescription.
"""
return self.prescription.medication
class DrugInteraction(models.Model):
"""
Drug interaction model for clinical decision support.
"""
class Severity(models.TextChoices):
MINOR = 'MINOR', 'Minor'
MODERATE = 'MODERATE', 'Moderate'
MAJOR = 'MAJOR', 'Major'
CONTRAINDICATED = 'CONTRAINDICATED', 'Contraindicated'
class InteractionType(models.TextChoices):
PHARMACOKINETIC = 'PHARMACOKINETIC', 'Pharmacokinetic'
PHARMACODYNAMIC = 'PHARMACODYNAMIC', 'Pharmacodynamic'
ADDITIVE = 'ADDITIVE', 'Additive'
SYNERGISTIC = 'SYNERGISTIC', 'Synergistic'
ANTAGONISTIC = 'ANTAGONISTIC', 'Antagonistic'
OTHER = 'OTHER', 'Other'
class EvidenceLevel(models.TextChoices):
ESTABLISHED = 'ESTABLISHED', 'Established'
PROBABLE = 'PROBABLE', 'Probable'
SUSPECTED = 'SUSPECTED', 'Suspected'
POSSIBLE = 'POSSIBLE', 'Possible'
UNLIKELY = 'UNLIKELY', 'Unlikely'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='drug_interactions',
help_text='Organization tenant'
)
# Medications
medication_1 = models.ForeignKey(
Medication,
on_delete=models.CASCADE,
related_name='interactions_as_drug1',
help_text='First medication'
)
medication_2 = models.ForeignKey(
Medication,
on_delete=models.CASCADE,
related_name='interactions_as_drug2',
help_text='Second medication'
)
# Interaction Information
interaction_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique interaction identifier'
)
# Severity
severity = models.CharField(
max_length=20,
choices=Severity.choices,
help_text='Interaction severity'
)
# Interaction Details
interaction_type = models.CharField(
max_length=30,
choices=InteractionType.choices,
help_text='Type of interaction'
)
mechanism = models.TextField(
help_text='Mechanism of interaction'
)
clinical_effect = models.TextField(
help_text='Clinical effect of interaction'
)
# Management
management_recommendations = models.TextField(
help_text='Management recommendations'
)
monitoring_parameters = models.TextField(
blank=True,
null=True,
help_text='Parameters to monitor'
)
# Evidence
evidence_level = models.CharField(
max_length=20,
choices=EvidenceLevel.choices,
help_text='Level of evidence'
)
# References
references = models.TextField(
blank=True,
null=True,
help_text='Literature references'
)
# Status
is_active = models.BooleanField(
default=True,
help_text='Interaction is active'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_drug_interactions',
help_text='User who created the interaction record'
)
class Meta:
db_table = 'pharmacy_drug_interaction'
verbose_name = 'Drug Interaction'
verbose_name_plural = 'Drug Interactions'
ordering = ['-severity', 'medication_1__generic_name']
indexes = [
models.Index(fields=['tenant', 'is_active']),
models.Index(fields=['medication_1']),
models.Index(fields=['medication_2']),
models.Index(fields=['severity']),
]
unique_together = ['tenant', 'medication_1', 'medication_2']
def __str__(self):
return f"{self.medication_1.display_name} + {self.medication_2.display_name} ({self.severity})"
@property
def is_major_interaction(self):
"""
Check if interaction is major or contraindicated.
"""
return self.severity in ['MAJOR', 'CONTRAINDICATED']