""" 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']