1334 lines
37 KiB
Python
1334 lines
37 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 datetime import timedelta, datetime, date
|
|
from decimal import Decimal
|
|
import json
|
|
|
|
|
|
class Medication(models.Model):
|
|
"""
|
|
Medication model for drug database and formulary management.
|
|
"""
|
|
CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES = [
|
|
('CI', 'Schedule I'),
|
|
('CII', 'Schedule II'),
|
|
('CIII', 'Schedule III'),
|
|
('CIV', 'Schedule IV'),
|
|
('CV', 'Schedule V'),
|
|
('NON', 'Non-Controlled'),
|
|
]
|
|
DOSAGE_FORM_CHOICES = [
|
|
('TABLET', 'Tablet'),
|
|
('CAPSULE', 'Capsule'),
|
|
('LIQUID', 'Liquid'),
|
|
('INJECTION', 'Injection'),
|
|
('TOPICAL', 'Topical'),
|
|
('INHALER', 'Inhaler'),
|
|
('PATCH', 'Patch'),
|
|
('SUPPOSITORY', 'Suppository'),
|
|
('CREAM', 'Cream'),
|
|
('OINTMENT', 'Ointment'),
|
|
('DROPS', 'Drops'),
|
|
('SPRAY', 'Spray'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
UNIT_OF_MEASURE_CHOICES = [
|
|
('MG', 'Milligrams'),
|
|
('G', 'Grams'),
|
|
('MCG', 'Micrograms'),
|
|
('ML', 'Milliliters'),
|
|
('L', 'Liters'),
|
|
('UNITS', 'Units'),
|
|
('IU', 'International Units'),
|
|
('MEQ', 'Milliequivalents'),
|
|
('PERCENT', 'Percent'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
FORMULARY_STATUS_CHOICES = [
|
|
('PREFERRED', 'Preferred'),
|
|
('NON_PREFERRED', 'Non-Preferred'),
|
|
('RESTRICTED', 'Restricted'),
|
|
('NOT_COVERED', 'Not Covered'),
|
|
('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=CONTROLLED_SUBSTANCE_SCHEDULE_CHOICES,
|
|
default='NON',
|
|
help_text='DEA controlled substance schedule'
|
|
)
|
|
|
|
# Formulation
|
|
dosage_form = models.CharField(
|
|
max_length=50,
|
|
choices=DOSAGE_FORM_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=UNIT_OF_MEASURE_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=FORMULARY_STATUS_CHOICES,
|
|
default='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.
|
|
"""
|
|
QUANTITY_UNIT_CHOICES = [
|
|
('TABLETS', 'Tablets'),
|
|
('CAPSULES', 'Capsules'),
|
|
('ML', 'Milliliters'),
|
|
('GRAMS', 'Grams'),
|
|
('UNITS', 'Units'),
|
|
('PATCHES', 'Patches'),
|
|
('INHALERS', 'Inhalers'),
|
|
('BOTTLES', 'Bottles'),
|
|
('TUBES', 'Tubes'),
|
|
('VIALS', 'Vials'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
STATUS_CHOICES = [
|
|
('PENDING', 'Pending'),
|
|
('ACTIVE', 'Active'),
|
|
('DISPENSED', 'Dispensed'),
|
|
('PARTIALLY_DISPENSED', 'Partially Dispensed'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('EXPIRED', 'Expired'),
|
|
('ON_HOLD', 'On Hold'),
|
|
('TRANSFERRED', 'Transferred'),
|
|
('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=QUANTITY_UNIT_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=STATUS_CHOICES,
|
|
default='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'
|
|
)
|
|
|
|
# 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
|
|
|
|
|
|
class InventoryItem(models.Model):
|
|
"""
|
|
Inventory item model for pharmacy stock management.
|
|
"""
|
|
STATUS_CHOICES = [
|
|
('ACTIVE', 'Active'),
|
|
('QUARANTINE', 'Quarantine'),
|
|
('EXPIRED', 'Expired'),
|
|
('RECALLED', 'Recalled'),
|
|
('DAMAGED', 'Damaged'),
|
|
('RETURNED', 'Returned'),
|
|
]
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='pharmacy_inventory',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Medication
|
|
medication = models.ForeignKey(
|
|
Medication,
|
|
on_delete=models.CASCADE,
|
|
related_name='inventory_items',
|
|
help_text='Medication'
|
|
)
|
|
|
|
# Inventory Information
|
|
inventory_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique inventory identifier'
|
|
)
|
|
|
|
# Lot Information
|
|
lot_number = models.CharField(
|
|
max_length=50,
|
|
help_text='Lot/batch number'
|
|
)
|
|
expiration_date = models.DateField(
|
|
help_text='Expiration date'
|
|
)
|
|
|
|
# Quantity
|
|
quantity_on_hand = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Current quantity on hand'
|
|
)
|
|
quantity_allocated = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Quantity allocated for pending orders'
|
|
)
|
|
quantity_available = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Available quantity (on hand - allocated)'
|
|
)
|
|
|
|
# Reorder Information
|
|
reorder_point = models.PositiveIntegerField(
|
|
default=10,
|
|
help_text='Reorder point'
|
|
)
|
|
reorder_quantity = models.PositiveIntegerField(
|
|
default=100,
|
|
help_text='Reorder quantity'
|
|
)
|
|
|
|
# Location
|
|
storage_location = models.CharField(
|
|
max_length=50,
|
|
help_text='Storage location (e.g., Shelf A1, Refrigerator 2)'
|
|
)
|
|
bin_location = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Specific bin location'
|
|
)
|
|
|
|
# Cost Information
|
|
unit_cost = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
help_text='Cost per unit'
|
|
)
|
|
total_cost = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=2,
|
|
help_text='Total cost of inventory'
|
|
)
|
|
|
|
# Supplier Information
|
|
supplier = models.CharField(
|
|
max_length=100,
|
|
help_text='Supplier name'
|
|
)
|
|
purchase_order_number = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Purchase order number'
|
|
)
|
|
|
|
# Dates
|
|
received_date = models.DateField(
|
|
help_text='Date received'
|
|
)
|
|
last_counted = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Last physical count date'
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=STATUS_CHOICES,
|
|
default='ACTIVE',
|
|
help_text='Inventory status'
|
|
)
|
|
|
|
# Quality Control
|
|
quality_checked = models.BooleanField(
|
|
default=False,
|
|
help_text='Quality check completed'
|
|
)
|
|
quality_check_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Quality check date'
|
|
)
|
|
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)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_pharmacy_inventory_items',
|
|
help_text='User who created the inventory item'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'pharmacy_inventory_item'
|
|
verbose_name = 'Inventory Item'
|
|
verbose_name_plural = 'Inventory Items'
|
|
ordering = ['medication__generic_name', 'expiration_date']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'status']),
|
|
models.Index(fields=['medication']),
|
|
models.Index(fields=['lot_number']),
|
|
models.Index(fields=['expiration_date']),
|
|
models.Index(fields=['storage_location']),
|
|
]
|
|
unique_together = ['tenant', 'medication', 'lot_number']
|
|
|
|
def __str__(self):
|
|
return f"{self.medication.display_name} - Lot: {self.lot_number}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Calculate available quantity and total cost.
|
|
"""
|
|
self.quantity_available = max(0, self.quantity_on_hand - self.quantity_allocated)
|
|
self.total_cost = self.quantity_on_hand * self.unit_cost
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def is_expired(self):
|
|
"""
|
|
Check if inventory item is expired.
|
|
"""
|
|
return timezone.now().date() > self.expiration_date
|
|
|
|
@property
|
|
def days_until_expiry(self):
|
|
"""
|
|
Calculate days until expiry.
|
|
"""
|
|
return (self.expiration_date - timezone.now().date()).days
|
|
|
|
@property
|
|
def needs_reorder(self):
|
|
"""
|
|
Check if item needs reordering.
|
|
"""
|
|
return self.quantity_available <= self.reorder_point
|
|
|
|
|
|
class DispenseRecord(models.Model):
|
|
"""
|
|
Dispense record model for tracking medication dispensing.
|
|
"""
|
|
|
|
# Prescription relationship
|
|
prescription = models.ForeignKey(
|
|
Prescription,
|
|
on_delete=models.CASCADE,
|
|
related_name='dispense_records',
|
|
help_text='Related prescription'
|
|
)
|
|
|
|
# Inventory item
|
|
inventory_item = models.ForeignKey(
|
|
InventoryItem,
|
|
on_delete=models.CASCADE,
|
|
related_name='dispense_records',
|
|
help_text='Inventory item dispensed'
|
|
)
|
|
|
|
# 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=[
|
|
('DISPENSED', 'Dispensed'),
|
|
('PICKED_UP', 'Picked Up'),
|
|
('RETURNED', 'Returned'),
|
|
('CANCELLED', 'Cancelled'),
|
|
],
|
|
default='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_item']),
|
|
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 item.
|
|
"""
|
|
return self.inventory_item.medication
|
|
|
|
|
|
class MedicationAdministration(models.Model):
|
|
"""
|
|
Medication Administration Record (MAR) for inpatient medication tracking.
|
|
"""
|
|
|
|
# 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=[
|
|
('PO', 'Oral'),
|
|
('IV', 'Intravenous'),
|
|
('IM', 'Intramuscular'),
|
|
('SC', 'Subcutaneous'),
|
|
('SL', 'Sublingual'),
|
|
('TOP', 'Topical'),
|
|
('INH', 'Inhalation'),
|
|
('PR', 'Rectal'),
|
|
('PV', 'Vaginal'),
|
|
('NASAL', 'Nasal'),
|
|
('OPTH', 'Ophthalmic'),
|
|
('OTIC', 'Otic'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Route of administration'
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled'),
|
|
('GIVEN', 'Given'),
|
|
('NOT_GIVEN', 'Not Given'),
|
|
('HELD', 'Held'),
|
|
('REFUSED', 'Refused'),
|
|
('OMITTED', 'Omitted'),
|
|
],
|
|
default='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=[
|
|
('PATIENT_REFUSED', 'Patient Refused'),
|
|
('PATIENT_UNAVAILABLE', 'Patient Unavailable'),
|
|
('MEDICATION_UNAVAILABLE', 'Medication Unavailable'),
|
|
('HELD_BY_PROVIDER', 'Held by Provider'),
|
|
('PATIENT_NPO', 'Patient NPO'),
|
|
('PATIENT_ASLEEP', 'Patient Asleep'),
|
|
('ADVERSE_REACTION', 'Adverse Reaction'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
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.
|
|
"""
|
|
|
|
# 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=[
|
|
('MINOR', 'Minor'),
|
|
('MODERATE', 'Moderate'),
|
|
('MAJOR', 'Major'),
|
|
('CONTRAINDICATED', 'Contraindicated'),
|
|
],
|
|
help_text='Interaction severity'
|
|
)
|
|
|
|
# Interaction Details
|
|
interaction_type = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('PHARMACOKINETIC', 'Pharmacokinetic'),
|
|
('PHARMACODYNAMIC', 'Pharmacodynamic'),
|
|
('ADDITIVE', 'Additive'),
|
|
('SYNERGISTIC', 'Synergistic'),
|
|
('ANTAGONISTIC', 'Antagonistic'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
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=[
|
|
('ESTABLISHED', 'Established'),
|
|
('PROBABLE', 'Probable'),
|
|
('SUSPECTED', 'Suspected'),
|
|
('POSSIBLE', 'Possible'),
|
|
('UNLIKELY', 'Unlikely'),
|
|
],
|
|
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']
|
|
|