# import random # import uuid # from datetime import datetime, timedelta # from decimal import Decimal # from django.utils import timezone as django_timezone # from django.contrib.auth import get_user_model # # from core.models import Tenant # from inventory.models import InventoryItem, InventoryStock, InventoryLocation, PurchaseOrder, PurchaseOrderItem, \ # Supplier # # User = get_user_model() # # # Saudi Arabian Inventory Data # SAUDI_MEDICAL_CATEGORIES = [ # 'Pharmaceuticals', # 'Medical Devices', # 'Surgical Instruments', # 'Laboratory Supplies', # 'PPE & Safety', # 'IV Therapy', # 'Emergency Supplies' # ] # # SAUDI_SUPPLIERS = [ # 'Saudi Medical Supply Co.', # 'Gulf Medical Equipment', # 'Arabian Healthcare Supplies', # 'Riyadh Medical Trading', # 'Al-Dawaa Medical', # 'Nahdi Medical Company', # 'United Pharmaceuticals' # ] # # SAUDI_CITIES = ['Riyadh', 'Jeddah', 'Dammam', 'Medina', 'Taif', 'Khobar'] # # MEDICAL_ITEMS = [ # {'name': 'Paracetamol 500mg', 'category': 'Pharmaceuticals', 'unit': 'TAB'}, # {'name': 'Disposable Syringe 5ml', 'category': 'Medical Devices', 'unit': 'PCS'}, # {'name': 'Surgical Gloves Size M', 'category': 'PPE & Safety', 'unit': 'PAIR'}, # {'name': 'Blood Collection Tube', 'category': 'Laboratory Supplies', 'unit': 'PCS'}, # {'name': 'IV Bag Normal Saline', 'category': 'IV Therapy', 'unit': 'BAG'}, # {'name': 'Emergency Oxygen Mask', 'category': 'Emergency Supplies', 'unit': 'PCS'} # ] # # # def create_saudi_suppliers(tenants): # """Create Saudi suppliers""" # suppliers = [] # # for tenant in tenants: # print(f"Creating suppliers for {tenant.name}...") # # for i, supplier_name in enumerate(SAUDI_SUPPLIERS): # supplier_code = f"SUP-{tenant.id}-{i + 1:03d}" # # try: # supplier = Supplier.objects.create( # tenant=tenant, # supplier_code=supplier_code, # name=supplier_name, # supplier_type='DISTRIBUTOR', # city=random.choice(SAUDI_CITIES), # country='Saudi Arabia', # is_active=True # ) # suppliers.append(supplier) # print(f" ✓ Created supplier: {supplier_name}") # # except Exception as e: # print(f" ✗ Error creating supplier {supplier_name}: {e}") # continue # # print(f"Created {len(suppliers)} suppliers") # return suppliers # # # def create_saudi_inventory_locations(tenants): # """Create Saudi inventory locations""" # locations = [] # # storage_rooms = ['Pharmacy', 'Central Supply', 'OR Storage', 'ICU Supply', 'Ward Storage'] # # for tenant in tenants: # print(f"Creating locations for {tenant.name}...") # # for i, room in enumerate(storage_rooms): # location_code = f"LOC-{tenant.id}-{i + 1:03d}" # # try: # location = InventoryLocation.objects.create( # tenant=tenant, # location_code=location_code, # name=f"{room} - {tenant.city}", # description=f"Storage location in {room}", # location_type='WAREHOUSE', # building='Main Hospital', # floor='Ground Floor', # room=room, # is_active=True # ) # locations.append(location) # print(f" ✓ Created location: {location.name}") # # except Exception as e: # print(f" ✗ Error creating location {room}: {e}") # continue # # print(f"Created {len(locations)} locations") # return locations # # # def create_saudi_inventory_items(tenants): # """Create Saudi inventory items""" # items = [] # # for tenant in tenants: # print(f"Creating items for {tenant.name}...") # # for i, item_data in enumerate(MEDICAL_ITEMS): # item_code = f"ITM-{tenant.id}-{i + 1:03d}" # # try: # item = InventoryItem.objects.create( # tenant=tenant, # item_code=item_code, # item_name=item_data['name'], # description=f"Medical item: {item_data['name']}", # category=item_data['category'], # subcategory=item_data['category'], # item_type='STOCK', # manufacturer='Saudi Medical Industries', # unit_of_measure=item_data['unit'], # package_size=1, # unit_cost=Decimal(str(random.uniform(10, 100))), # list_price=Decimal(str(random.uniform(15, 150))), # has_expiration=item_data['category'] == 'Pharmaceuticals', # is_active=True, # is_tracked=True, # reorder_point=random.randint(10, 50), # reorder_quantity=random.randint(100, 500), # max_stock_level=random.randint(500, 1000) # ) # items.append(item) # print(f" ✓ Created item: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating item {item_data['name']}: {e}") # continue # # print(f"Created {len(items)} items") # return items # # # def create_saudi_inventory_stock(items, locations): # """Create Saudi inventory stock entries""" # stocks = [] # # for item in items: # print(f"Creating stock for {item.item_name}...") # # # Get locations for this tenant # tenant_locations = [loc for loc in locations if loc.tenant == item.tenant] # if not tenant_locations: # continue # # location = random.choice(tenant_locations) # # try: # stock = InventoryStock.objects.create( # inventory_item=item, # location=location, # quantity_on_hand=random.randint(50, 500), # quantity_reserved=random.randint(0, 20), # received_date=django_timezone.now().date() - timedelta(days=random.randint(1, 90)), # expiration_date=django_timezone.now().date() + timedelta(days=365) if item.has_expiration else None, # unit_cost=item.unit_cost, # quality_status='AVAILABLE' # ) # stocks.append(stock) # print(f" ✓ Created stock for: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating stock for {item.item_name}: {e}") # continue # # print(f"Created {len(stocks)} stock entries") # return stocks # # # def create_saudi_purchase_orders(tenants, suppliers): # """Create Saudi purchase orders""" # orders = [] # # for tenant in tenants: # print(f"Creating purchase orders for {tenant.name}...") # # # Get suppliers for this tenant # tenant_suppliers = [supplier for supplier in suppliers if supplier.tenant == tenant] # if not tenant_suppliers: # print(f" No suppliers found for {tenant.name}, skipping...") # continue # # # Get delivery locations # try: # locations = InventoryLocation.objects.filter(tenant=tenant) # delivery_location = locations.first() if locations.exists() else None # except: # delivery_location = None # # for i in range(3): # Create 3 orders per tenant # po_number = f"PO-{tenant.id}-{django_timezone.now().year}-{i + 1:04d}" # supplier = random.choice(tenant_suppliers) # # try: # order = PurchaseOrder.objects.create( # tenant=tenant, # po_number=po_number, # supplier=supplier, # order_date=django_timezone.now().date() - timedelta(days=random.randint(1, 30)), # requested_delivery_date=django_timezone.now().date() + timedelta(days=random.randint(7, 30)), # order_type='STANDARD', # priority='NORMAL', # subtotal=Decimal(str(random.uniform(1000, 10000))), # tax_amount=Decimal('0.00'), # shipping_amount=Decimal('0.00'), # total_amount=Decimal(str(random.uniform(1000, 10000))), # status='DRAFT', # delivery_location=delivery_location, # payment_terms='NET_30' # ) # orders.append(order) # print(f" ✓ Created PO: {po_number}") # # except Exception as e: # print(f" ✗ Error creating PO {po_number}: {e}") # continue # # print(f"Created {len(orders)} purchase orders") # return orders # # # def create_saudi_purchase_order_items(orders, items): # """Create Saudi purchase order items""" # po_items = [] # # for order in orders: # print(f"Creating items for PO {order.po_number}...") # # # Get items for this tenant # tenant_items = [item for item in items if item.tenant == order.tenant] # if not tenant_items: # continue # # # Create 2-3 items per order # num_items = min(3, len(tenant_items)) # selected_items = random.sample(tenant_items, num_items) # # for line_num, item in enumerate(selected_items, 1): # quantity_ordered = random.randint(10, 100) # unit_price = item.unit_cost * Decimal(str(random.uniform(0.9, 1.1))) # total_price = unit_price * quantity_ordered # # try: # po_item = PurchaseOrderItem.objects.create( # purchase_order=order, # line_number=line_num, # inventory_item=item, # quantity_ordered=quantity_ordered, # quantity_received=0, # unit_price=unit_price, # total_price=total_price, # requested_delivery_date=order.requested_delivery_date, # status='PENDING' # ) # po_items.append(po_item) # print(f" ✓ Created PO item: {item.item_name}") # # except Exception as e: # print(f" ✗ Error creating PO item for {item.item_name}: {e}") # continue # # print(f"Created {len(po_items)} purchase order items") # return po_items # # # def main(): # """Main function to create all Saudi inventory data""" # print("🏥 Starting Saudi Inventory Data Generation...") # # # Get tenants # try: # tenants = list(Tenant.objects.filter(is_active=True)[:5]) # Limit to first 5 tenants # if not tenants: # print("❌ No active tenants found. Please run core_data.py first.") # return # # print(f"📋 Found {len(tenants)} active tenants") # except Exception as e: # print(f"❌ Error getting tenants: {e}") # return # # # Create data step by step # print("\n1️⃣ Creating Suppliers...") # suppliers = create_saudi_suppliers(tenants) # if not suppliers: # print("❌ No suppliers created. Stopping.") # return # # print("\n2️⃣ Creating Locations...") # locations = create_saudi_inventory_locations(tenants) # if not locations: # print("❌ No locations created. Stopping.") # return # # print("\n3️⃣ Creating Items...") # items = create_saudi_inventory_items(tenants) # if not items: # print("❌ No items created. Stopping.") # return # # print("\n4️⃣ Creating Stock...") # stocks = create_saudi_inventory_stock(items, locations) # # print("\n5️⃣ Creating Purchase Orders...") # orders = create_saudi_purchase_orders(tenants, suppliers) # # print("\n6️⃣ Creating Purchase Order Items...") # po_items = create_saudi_purchase_order_items(orders, items) # # print("\n🎉 Saudi Inventory Data Generation Complete!") # print(f"📊 Summary:") # print(f" - Suppliers: {len(suppliers)}") # print(f" - Locations: {len(locations)}") # print(f" - Items: {len(items)}") # print(f" - Stock Entries: {len(stocks)}") # print(f" - Purchase Orders: {len(orders)}") # print(f" - PO Items: {len(po_items)}") # # # if __name__ == "__main__": # main() class Employee(models.Model): # """ # Employee model for hospital staff management. # """ # GENDER_CHOICES = [ # ('MALE', 'Male'), # ('FEMALE', 'Female'), # ('OTHER', 'Other'), # ('UNKNOWN', 'Unknown'), # ] # MARITAL_STATUS_CHOICES = [ # ('SINGLE', 'Single'), # ('MARRIED', 'Married'), # ('DIVORCED', 'Divorced'), # ('WIDOWED', 'Widowed'), # ('SEPARATED', 'Separated'), # ('OTHER', 'Other'), # ] # EMPLOYMENT_TYPE_CHOICES = [ # ('FULL_TIME', 'Full Time'), # ('PART_TIME', 'Part Time'), # ('CONTRACT', 'Contract'), # ('TEMPORARY', 'Temporary'), # ('INTERN', 'Intern'), # ('VOLUNTEER', 'Volunteer'), # ('PER_DIEM', 'Per Diem'), # ('CONSULTANT', 'Consultant'), # ] # EMPLOYMENT_STATUS_CHOICES = [ # ('ACTIVE', 'Active'), # ('INACTIVE', 'Inactive'), # ('TERMINATED', 'Terminated'), # ('SUSPENDED', 'Suspended'), # ('LEAVE', 'On Leave'), # ('RETIRED', 'Retired'), # ] # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='employees', # help_text='Organization tenant' # ) # # # User relationship (optional - for employees who have system access) # user = models.OneToOneField( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='employee_profile', # help_text='Associated user account' # ) # # # Employee Information # employee_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique employee identifier' # ) # employee_number = models.CharField( # max_length=20, # help_text='Employee number' # ) # # # Personal Information # first_name = models.CharField( # max_length=50, # help_text='First name' # ) # last_name = models.CharField( # max_length=50, # help_text='Last name' # ) # middle_name = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Middle name' # ) # preferred_name = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Preferred name' # ) # # # Contact Information # email = models.EmailField( # blank=True, # null=True, # help_text='Email address' # ) # phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Phone number' # ) # mobile_phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Mobile phone number' # ) # # # Address Information # address_line_1 = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Address line 1' # ) # address_line_2 = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Address line 2' # ) # city = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='City' # ) # state = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='State/Province' # ) # postal_code = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Postal/ZIP code' # ) # country = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Country' # ) # national_id = models.CharField( # max_length=10, # blank=True, # null=True, # unique=True, # help_text='National ID' # ) # # Personal Details # date_of_birth = models.DateField( # blank=True, # null=True, # help_text='Date of birth' # ) # gender = models.CharField( # max_length=10, # choices=GENDER_CHOICES, # blank=True, # null=True, # help_text='Gender' # ) # marital_status = models.CharField( # max_length=20, # choices=MARITAL_STATUS_CHOICES, # blank=True, # null=True, # help_text='Marital status' # ) # # # Employment Information # department = models.ForeignKey( # 'Department', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='employees', # help_text='Department' # ) # job_title = models.CharField( # max_length=100, # help_text='Job title' # ) # employment_type = models.CharField( # max_length=20, # choices=EMPLOYMENT_TYPE_CHOICES, # help_text='Employment type' # ) # employment_status = models.CharField( # max_length=20, # choices=EMPLOYMENT_STATUS_CHOICES, # default='ACTIVE', # help_text='Employment status' # ) # # # Employment Dates # hire_date = models.DateField( # help_text='Hire date' # ) # termination_date = models.DateField( # blank=True, # null=True, # help_text='Termination date' # ) # # # Supervisor Information # supervisor = models.ForeignKey( # 'self', # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='direct_reports', # help_text='Direct supervisor' # ) # # # Work Schedule Information # standard_hours_per_week = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('40.00'), # help_text='Standard hours per week' # ) # fte_percentage = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('100.00'), # validators=[MinValueValidator(0), MaxValueValidator(100)], # help_text='FTE percentage' # ) # # # Compensation Information # hourly_rate = models.DecimalField( # max_digits=10, # decimal_places=2, # blank=True, # null=True, # help_text='Hourly rate' # ) # annual_salary = models.DecimalField( # max_digits=12, # decimal_places=2, # blank=True, # null=True, # help_text='Annual salary' # ) # # # Professional Information # license_number = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Professional license number' # ) # license_expiry_date = models.DateField( # blank=True, # null=True, # help_text='License expiry date' # ) # certifications = models.JSONField( # default=list, # help_text='Professional certifications' # ) # # # Emergency Contact # emergency_contact_name = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Emergency contact name' # ) # emergency_contact_relationship = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Emergency contact relationship' # ) # emergency_contact_phone = models.CharField( # max_length=20, # blank=True, # null=True, # help_text='Emergency contact phone' # ) # # # Notes # notes = models.TextField( # blank=True, # null=True, # help_text='Employee 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_employees', # help_text='User who created the employee record' # ) # # class Meta: # db_table = 'hr_employee' # verbose_name = 'Employee' # verbose_name_plural = 'Employees' # ordering = ['last_name', 'first_name'] # indexes = [ # models.Index(fields=['tenant', 'employment_status']), # models.Index(fields=['employee_number']), # models.Index(fields=['last_name', 'first_name']), # models.Index(fields=['department']), # models.Index(fields=['hire_date']), # ] # unique_together = ['tenant', 'employee_number'] # # def __str__(self): # return f"{self.employee_number} - {self.get_full_name()}" # # def get_full_name(self): # """ # Get employee's full name. # """ # if self.middle_name: # return f"{self.first_name} {self.middle_name} {self.last_name}" # return f"{self.first_name} {self.last_name}" # # def get_display_name(self): # """ # Get employee's display name (preferred name if available). # """ # if self.preferred_name: # return f"{self.preferred_name} {self.last_name}" # return self.get_full_name() # # @property # def age(self): # """ # Calculate employee's age. # """ # if self.date_of_birth: # today = date.today() # return today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)) # return None # # @property # def years_of_service(self): # """ # Calculate years of service. # """ # if self.hire_date: # end_date = self.termination_date or date.today() # return (end_date - self.hire_date).days / 365.25 # return 0 # # @property # def is_license_expired(self): # """ # Check if professional license is expired. # """ # if self.license_expiry_date: # return self.license_expiry_date < date.today() # return False # # @property # def full_address(self): # """ # Get full address. # """ # parts = [ # self.address_line_1, # self.address_line_2, # f"{self.city}, {self.state} {self.postal_code}", # self.country # ] # return "\n".join([part for part in parts if part]) # class TrainingRecord(models.Model): # """ # Training record model for employee training and certifications. # """ # # class TrainingType(models.TextChoices): # ORIENTATION = 'ORIENTATION', 'Orientation' # MANDATORY = 'MANDATORY', 'Mandatory Training' # CONTINUING_ED = 'CONTINUING_ED', 'Continuing Education' # CERTIFICATION = 'CERTIFICATION', 'Certification' # SKILLS = 'SKILLS', 'Skills Training' # SAFETY = 'SAFETY', 'Safety Training' # COMPLIANCE = 'COMPLIANCE', 'Compliance Training' # LEADERSHIP = 'LEADERSHIP', 'Leadership Development' # TECHNICAL = 'TECHNICAL', 'Technical Training' # OTHER = 'OTHER', 'Other' # # class TrainingStatus(models.TextChoices): # SCHEDULED = 'SCHEDULED', 'Scheduled' # IN_PROGRESS = 'IN_PROGRESS', 'In Progress' # COMPLETED = 'COMPLETED', 'Completed' # CANCELLED = 'CANCELLED', 'Cancelled' # NO_SHOW = 'NO_SHOW', 'No Show' # FAILED = 'FAILED', 'Failed' # # # Employee relationship # employee = models.ForeignKey( # Employee, # on_delete=models.CASCADE, # related_name='training_records', # help_text='Employee' # ) # # # Training Information # record_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique training record identifier' # ) # training_name = models.CharField( # max_length=200, # help_text='Training name' # ) # training_description = models.TextField( # blank=True, # null=True, # help_text='Training description' # ) # # # Training Type # training_type = models.CharField( # max_length=20, # choices=TrainingType.choices, # help_text='Training type' # ) # # # Training Provider # training_provider = models.CharField( # max_length=200, # blank=True, # null=True, # help_text='Training provider' # ) # instructor = models.CharField( # max_length=100, # blank=True, # null=True, # help_text='Instructor name' # ) # # # Training Dates # training_date = models.DateField( # help_text='Training date' # ) # completion_date = models.DateField( # blank=True, # null=True, # help_text='Completion date' # ) # expiry_date = models.DateField( # blank=True, # null=True, # help_text='Certification expiry date' # ) # # # Training Details # duration_hours = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('0.00'), # help_text='Training duration in hours' # ) # credits_earned = models.DecimalField( # max_digits=5, # decimal_places=2, # default=Decimal('0.00'), # help_text='Credits earned' # ) # # # Training Status # status = models.CharField( # max_length=20, # choices=TrainingStatus.choices, # default='SCHEDULED', # help_text='Training status' # ) # # # Results # score = models.DecimalField( # max_digits=5, # decimal_places=2, # blank=True, # null=True, # help_text='Training score/grade' # ) # passed = models.BooleanField( # default=False, # help_text='Training passed' # ) # is_certified = models.BooleanField( # default=False, # help_text='Training is certified' # ) # # Certification Information # certificate_number = models.CharField( # max_length=50, # blank=True, # null=True, # help_text='Certificate number' # ) # certification_body = models.CharField( # max_length=200, # blank=True, # null=True, # help_text='Certification body' # ) # # # Cost Information # training_cost = models.DecimalField( # max_digits=10, # decimal_places=2, # default=Decimal('0.00'), # help_text='Training cost' # ) # # # Notes # notes = models.TextField( # blank=True, # null=True, # help_text='Training 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_training_records', # help_text='User who created the training record' # ) # # class Meta: # db_table = 'hr_training_record' # verbose_name = 'Training Record' # verbose_name_plural = 'Training Records' # ordering = ['-training_date'] # indexes = [ # models.Index(fields=['employee', 'training_date']), # models.Index(fields=['training_type']), # models.Index(fields=['status']), # models.Index(fields=['expiry_date']), # ] # # def __str__(self): # return f"{self.employee.get_full_name()} - {self.training_name}" # # @property # def tenant(self): # """ # Get tenant from employee. # """ # return self.employee.tenant # # @property # def is_expired(self): # """ # Check if certification is expired. # """ # if self.expiry_date: # return self.expiry_date < date.today() # return False # # @property # def days_to_expiry(self): # """ # Calculate days to expiry. # """ # if self.expiry_date: # return (self.expiry_date - date.today()).days # return None # # @property # def is_due_for_renewal(self): # """ # Check if certification is due for renewal (within 30 days). # """ # if self.expiry_date: # return (self.expiry_date - date.today()).days <= 30 # return False # class Certification(models.Model): # tenant = models.ForeignKey('core.Tenant', on_delete=models.PROTECT, related_name='certifications') # name = models.CharField(max_length=100) # issuer = models.CharField(max_length=150, blank=True, null=True) # is_clinical = models.BooleanField(default=False) # # class Meta: # unique_together = [('tenant', 'name')] # # class EmployeeCertification(models.Model): # employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='employee_certifications') # certification = models.ForeignKey(Certification, on_delete=models.PROTECT) # credential_id = models.CharField(max_length=100, blank=True, null=True) # issued_on = models.DateField(blank=True, null=True) # expires_on = models.DateField(blank=True, null=True) # # class Meta: # constraints = [ # models.UniqueConstraint(fields=['employee', 'certification'], name='uq_employee_cert_once') # ] # class DrugInteraction(models.Model): # """ # Drug interaction model for tracking medication interactions. # """ # # class InteractionSeverity(models.TextChoices): # MINOR = 'MINOR', 'Minor' # MODERATE = 'MODERATE', 'Moderate' # MAJOR = 'MAJOR', 'Major' # CONTRAINDICATED = 'CONTRAINDICATED', 'Contraindicated' # # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='drug_interactions', # help_text='Organization tenant' # ) # # # Patient relationship # patient = models.ForeignKey( # 'patients.PatientProfile', # on_delete=models.CASCADE, # related_name='drug_interactions', # help_text='Patient' # ) # # # Interaction details # interaction_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique interaction identifier' # ) # # drug1 = models.CharField( # max_length=100, # help_text='First drug name' # ) # drug2 = models.CharField( # max_length=100, # help_text='Second drug name' # ) # # severity = models.CharField( # max_length=20, # choices=InteractionSeverity.choices, # help_text='Interaction severity' # ) # # description = models.TextField( # help_text='Interaction description and clinical significance' # ) # # # Management # management = models.TextField( # blank=True, # null=True, # help_text='Management recommendations' # ) # # # Status # resolved = models.BooleanField( # default=False, # help_text='Interaction has been resolved' # ) # resolved_at = models.DateTimeField( # blank=True, # null=True, # help_text='Date interaction was resolved' # ) # resolved_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.SET_NULL, # null=True, # blank=True, # related_name='resolved_interactions', # help_text='Provider who resolved the interaction' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # detected_at = models.DateTimeField( # default=timezone.now, # help_text='When interaction was detected' # ) # # class Meta: # db_table = 'emr_drug_interaction' # verbose_name = 'Drug Interaction' # verbose_name_plural = 'Drug Interactions' # ordering = ['-detected_at'] # indexes = [ # models.Index(fields=['tenant', 'resolved']), # models.Index(fields=['patient', 'resolved']), # models.Index(fields=['severity']), # models.Index(fields=['detected_at']), # ] # # def __str__(self): # return f"{self.patient.get_full_name()} - {self.drug1} + {self.drug2}" # class RiskAssessment(models.Model): # """ # Risk assessment model for tracking patient risk scores. # """ # # class RiskLevel(models.TextChoices): # LOW = 'LOW', 'Low Risk' # MODERATE = 'MODERATE', 'Moderate Risk' # HIGH = 'HIGH', 'High Risk' # CRITICAL = 'CRITICAL', 'Critical Risk' # # # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Organization tenant' # ) # # # Patient relationship # patient = models.ForeignKey( # 'patients.PatientProfile', # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Patient' # ) # # # Assessment details # assessment_id = models.UUIDField( # default=uuid.uuid4, # unique=True, # editable=False, # help_text='Unique assessment identifier' # ) # # assessment_type = models.CharField( # max_length=100, # help_text='Type of risk assessment (e.g., Fall Risk, Cardiac Risk)' # ) # # score = models.DecimalField( # max_digits=5, # decimal_places=2, # help_text='Risk score value' # ) # # risk_level = models.CharField( # max_length=20, # choices=RiskLevel.choices, # help_text='Calculated risk level' # ) # # description = models.TextField( # blank=True, # null=True, # help_text='Assessment description and interpretation' # ) # # # Assessment details # assessment_date = models.DateTimeField( # default=timezone.now, # help_text='Date and time of assessment' # ) # # factors = models.JSONField( # default=dict, # help_text='Risk factors considered' # ) # # recommendations = models.JSONField( # default=list, # help_text='Recommendations based on assessment' # ) # # # Provider information # assessed_by = models.ForeignKey( # settings.AUTH_USER_MODEL, # on_delete=models.CASCADE, # related_name='risk_assessments', # help_text='Provider who performed assessment' # ) # # # Metadata # created_at = models.DateTimeField(auto_now_add=True) # updated_at = models.DateTimeField(auto_now=True) # # class Meta: # db_table = 'emr_risk_assessment' # verbose_name = 'Risk Assessment' # verbose_name_plural = 'Risk Assessments' # ordering = ['-assessment_date'] # indexes = [ # models.Index(fields=['tenant']), # models.Index(fields=['patient']), # models.Index(fields=['assessment_type']), # models.Index(fields=['risk_level']), # models.Index(fields=['assessment_date']), # ] # # def __str__(self): # return f"{self.patient.get_full_name()} - {self.assessment_type}: {self.score}" # # # ClinicalNote -------------------------------------------------------------------- # # class ClinicalNoteListView(LoginRequiredMixin, TenantMixin, ListView): # model = ClinicalNote # template_name = 'emr/clinical_note_list.html' # context_object_name = 'notes' # paginate_by = 20 # # # class ClinicalNoteDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = ClinicalNote # template_name = 'emr/clinical_note_detail.html' # context_object_name = 'note' # # # class ClinicalNoteCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note created successfully.') # # def form_valid(self, form): # form.instance.author = self.request.user # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = ClinicalNote # form_class = ClinicalNoteForm # template_name = 'emr/clinical_note_form.html' # success_message = _('Clinical note updated successfully.') # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='ClinicalNote', # object_id=str(self.object.pk), # changes={'status': 'Clinical note updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:clinical_note_detail', kwargs={'pk': self.object.pk}) # # # class ClinicalNoteDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = ClinicalNote # template_name = 'emr/clinical_note_confirm_delete.html' # success_url = reverse_lazy('emr:clinical_note_list') # success_message = _('Clinical note deleted successfully.') # # def delete(self, request, *args, **kwargs): # cn = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='ClinicalNote', # object_id=str(cn.pk), # changes={'status': 'Clinical note deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # NoteTemplate ------------------------------------------------------------------- # # # encounters/views.py # # from django.shortcuts import get_object_or_404, redirect, render # from django.urls import reverse_lazy # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin # from django.contrib import messages # from django.views.generic import ( # TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView # ) # from django.views.generic.edit import FormMixin # from django.contrib.messages.views import SuccessMessageMixin # from django.db.models import Q, Avg # from django.utils import timezone # from django.http import JsonResponse # # from .models import * # from .forms import * # from core.models import AuditLogEntry # from patients.models import PatientProfile # from django.utils.translation import gettext_lazy as _ # # # # Mixins ------------------------------------------------------------------------- # # class TenantMixin: # def get_queryset(self): # qs = super().get_queryset() # tenant = getattr(self.request.user, 'tenant', None) # if tenant and not self.request.user.is_superuser: # # Models with patient FK: # if hasattr(qs.model, 'patient'): # return qs.filter(patient__tenant=tenant) # # NoteTemplate uses tenant directly: # return qs.filter(tenant=tenant) # return qs # # def get_object(self, queryset=None): # qs = queryset or self.get_queryset() # return super().get_object(qs) # # # class FormKwargsMixin: # def get_form_kwargs(self): # kw = super().get_form_kwargs() # kw['user'] = self.request.user # kw['tenant'] = getattr(self.request.user, 'tenant', None) # return kw # # # # Dashboard ---------------------------------------------------------------------- # # class DashboardView(LoginRequiredMixin, TemplateView): # template_name = 'emr/dashboard.html' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # tenant = getattr(self.request.user, 'tenant', None) # today = timezone.now().date() # week_ago = today - timezone.timedelta(days=7) # # enc = Encounter.objects.filter(patient__tenant=tenant) # vs = VitalSigns.objects.filter(encounter__patient__tenant=tenant) # pb = ProblemList.objects.filter(patient__tenant=tenant) # cp = CarePlan.objects.filter(patient__tenant=tenant) # cn = ClinicalNote.objects.filter(encounter__patient__tenant=tenant) # # ctx.update({ # 'total_encounters': enc.count(), # 'encounters_today': enc.filter(scheduled_datetime__date=today).count(), # 'encounters_this_week': enc.filter(scheduled_datetime__date__gte=week_ago).count(), # 'active_encounters': enc.filter(status='IN_PROGRESS').count(), # # 'total_vital_signs': vs.count(), # 'vital_signs_today': vs.filter(recorded_at__date=today).count(), # 'avg_temp_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('temperature'))['temperature__avg'], # 'avg_hr_week': vs.filter(recorded_at__date__gte=week_ago).aggregate(Avg('heart_rate'))['heart_rate__avg'], # # 'total_problems': pb.count(), # 'active_problems': pb.filter(status='ACTIVE').count(), # 'resolved_problems': pb.filter(status='RESOLVED').count(), # # 'total_care_plans': cp.count(), # 'active_care_plans': cp.filter(status='ACTIVE').count(), # 'completed_care_plans': cp.filter(status='COMPLETED').count(), # # 'total_notes': cn.count(), # 'notes_today': cn.filter(created_at__date=today).count(), # # 'recent_encounters': enc.select_related('patient','provider').order_by('-scheduled_datetime')[:5], # 'recent_vitals': vs.select_related('encounter','recorded_by').order_by('-recorded_at')[:5], # 'recent_notes': cn.select_related('encounter','author').order_by('-created_at')[:5], # }) # return ctx # # # # Encounter ---------------------------------------------------------------------- # # class EncounterListView(LoginRequiredMixin, TenantMixin, FormMixin, ListView): # model = Encounter # template_name = 'emr/encounter_list.html' # context_object_name = 'encounters' # paginate_by = 20 # form_class = EMRSearchForm # # def get_queryset(self): # qs = super().get_queryset().select_related('patient','provider').order_by('-scheduled_datetime') # if self.request.GET: # form = self.get_form() # if form.is_valid(): # cd = form.cleaned_data # if cd.get('search'): # qs = qs.filter( # Q(patient__first_name__icontains=cd['search']) | # Q(patient__last_name__icontains=cd['search']) | # Q(chief_complaint__icontains=cd['search']) # ) # for fld in ('patient','provider','encounter_type','status'): # if cd.get(fld): # qs = qs.filter(**{fld: cd[fld]}) # if cd.get('date_from'): # qs = qs.filter(scheduled_datetime__date__gte=cd['date_from']) # if cd.get('date_to'): # qs = qs.filter(scheduled_datetime__date__lte=cd['date_to']) # return qs # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # ctx['search_form'] = self.get_form() # ctx['total_count'] = self.get_queryset().count() # return ctx # # # class EncounterDetailView(LoginRequiredMixin, TenantMixin, DetailView): # model = Encounter # template_name = 'emr/encounter_detail.html' # context_object_name = 'encounter' # # def get_context_data(self, **kwargs): # ctx = super().get_context_data(**kwargs) # enc = self.object # ctx.update({ # 'vital_signs': VitalSigns.objects.filter(encounter=enc).order_by('-recorded_at'), # 'clinical_notes': ClinicalNote.objects.filter(encounter=enc).order_by('-created_at'), # 'problems': ProblemList.objects.filter(patient=enc.patient, status='ACTIVE'), # 'care_plans': CarePlan.objects.filter(patient=enc.patient, status='ACTIVE'), # }) # return ctx # # # class EncounterCreateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, CreateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s created successfully.') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter created'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterUpdateView( # LoginRequiredMixin, FormKwargsMixin, # SuccessMessageMixin, TenantMixin, UpdateView # ): # model = Encounter # form_class = EncounterForm # template_name = 'emr/encounter_form.html' # success_message = _('Encounter for %(patient)s updated successfully.') # # def dispatch(self, request, *args, **kwargs): # enc = self.get_object() # if enc.status == 'COMPLETED' and not request.user.is_superuser: # messages.error(request, _('Cannot modify a completed encounter.')) # return redirect('emr:encounter_detail', pk=enc.pk) # return super().dispatch(request, *args, **kwargs) # # def form_valid(self, form): # response = super().form_valid(form) # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='Encounter', # object_id=str(self.object.pk), # changes={'status': 'Encounter updated'} # ) # return response # # def get_success_url(self): # return reverse_lazy('emr:encounter_detail', kwargs={'pk': self.object.pk}) # # # class EncounterDeleteView( # LoginRequiredMixin, TenantMixin, # SuccessMessageMixin, DeleteView # ): # model = Encounter # template_name = 'emr/encounter_confirm_delete.html' # success_url = reverse_lazy('emr:encounter_list') # success_message = _('Encounter deleted successfully.') # # def delete(self, request, *args, **kwargs): # enc = self.get_object() # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='Encounter', # object_id=str(enc.pk), # changes={'status': 'Encounter deleted'} # ) # messages.success(request, self.success_message) # return super().delete(request, *args, **kwargs) # # # # VitalSigns ---------------------------------------------------------------------- # # class VitalSignsListView(LoginRequiredMixin, TenantMixin, ListView): # model = VitalSigns # template_name = 'emr/vital_signs_list.html' # context_object_name = 'vital_signs' # paginate_by = 20 # # def get_queryset(self): # qs = super().get_queryset().select_related('encounter','recorded_by').order_by('-recorded_at') # # (Search/filter logic would use a VitalSignsSearchForm — omitted for brevity) # return qs # # class VitalSignsUpdateView(LoginRequiredMixin, FormKwargsMixin, SuccessMessageMixin, TenantMixin, UpdateView): # model = VitalSigns # form_class = VitalSignsForm # template_name = 'emr/vital_signs_form.html' # success_message = _('Vital signs updated successfully.') # # implement form_valid, get_success_url, etc. # # class VitalSignsDeleteView(LoginRequiredMixin, TenantMixin, SuccessMessageMixin, DeleteView): # model = VitalSigns # template_name = 'emr/vital_signs_confirm_delete.html' # success_url = reverse_lazy('emr:vitalsigns_list') # success_message = _('Vital signs entry deleted.') # # """ # Radiology app views with healthcare-focused CRUD operations. # Implements appropriate access patterns for radiology and imaging workflows. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F # from django.utils import timezone # from django.contrib import messages # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from django.template.loader import render_to_string # from datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # ImagingOrder, ImagingStudy, ImagingSeries, DICOMImage, # RadiologyReport, ReportTemplate # ) # from .forms import ( # ImagingOrderForm, ImagingStudyForm, RadiologyReportForm, # ReportTemplateForm, ImagingSeriesForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class RadiologyDashboardView(LoginRequiredMixin, TemplateView): # """ # Main radiology dashboard with key metrics and recent activity. # """ # template_name = 'radiology/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # # # Dashboard statistics # context.update({ # 'pending_orders': ImagingOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'scheduled_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='SCHEDULED' # ).count(), # 'in_progress_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'studies_completed_today': ImagingStudy.objects.filter( # tenant=tenant, # completed_datetime__date=today, # status='COMPLETED' # ).count(), # 'reports_pending': RadiologyReport.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # 'reports_signed_today': RadiologyReport.objects.filter( # tenant=tenant, # signed_datetime__date=today, # status='SIGNED' # ).count(), # 'critical_findings': RadiologyReport.objects.filter( # tenant=tenant, # has_critical_findings=True, # status='SIGNED', # signed_datetime__date=today # ).count(), # 'total_images_today': DICOMImage.objects.filter( # tenant=tenant, # created_at__date=today # ).count(), # }) # # # Recent orders # context['recent_orders'] = ImagingOrder.objects.filter( # tenant=tenant # ).select_related('patient', 'ordering_provider').order_by('-order_datetime')[:10] # # # Recent studies # context['recent_studies'] = ImagingStudy.objects.filter( # tenant=tenant # ).select_related('order__patient').order_by('-study_datetime')[:10] # # # Recent reports # context['recent_reports'] = RadiologyReport.objects.filter( # tenant=tenant, # status='SIGNED' # ).select_related('study__order__patient').order_by('-signed_datetime')[:10] # # return context # # # # ============================================================================ # # REPORT TEMPLATE VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class ReportTemplateListView(LoginRequiredMixin, ListView): # """ # List all radiology report templates with filtering and search. # """ # model = ReportTemplate # template_name = 'radiology/report_template_list.html' # context_object_name = 'report_templates' # paginate_by = 25 # # def get_queryset(self): # queryset = ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(template_name__icontains=search) | # Q(modality__icontains=search) | # Q(body_part__icontains=search) # ) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(modality=modality) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # return queryset.order_by('template_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'modalities': ReportTemplate._meta.get_field('modality').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class ReportTemplateDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a report template. # """ # model = ReportTemplate # template_name = 'radiology/report_template_detail.html' # context_object_name = 'report_template' # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # template = self.object # # # Get recent reports using this template # context['recent_reports'] = RadiologyReport.objects.filter( # template=template, # tenant=self.request.user.tenant # ).select_related('study__order__patient').order_by('-created_at')[:10] # # return context # # # class ReportTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new report template. # """ # model = ReportTemplate # form_class = ReportTemplateForm # template_name = 'radiology/report_template_form.html' # permission_required = 'radiology.add_reporttemplate' # success_url = reverse_lazy('radiology:report_template_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REPORT_TEMPLATE_CREATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'modality': self.object.modality # } # ) # # messages.success(self.request, f'Report template "{self.object.template_name}" created successfully.') # return response # # # class ReportTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing report template. # """ # model = ReportTemplate # form_class = ReportTemplateForm # template_name = 'radiology/report_template_form.html' # permission_required = 'radiology.change_reporttemplate' # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:report_template_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REPORT_TEMPLATE_UPDATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={ # 'template_name': self.object.template_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Report template "{self.object.template_name}" updated successfully.') # return response # # # class ReportTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a report template (soft delete by deactivating). # """ # model = ReportTemplate # template_name = 'radiology/report_template_confirm_delete.html' # permission_required = 'radiology.delete_reporttemplate' # success_url = reverse_lazy('radiology:report_template_list') # # def get_queryset(self): # return ReportTemplate.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Soft delete by deactivating instead of actual deletion # self.object.is_active = False # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_TEMPLATE_DEACTIVATED', # model='ReportTemplate', # object_id=str(self.object.id), # details={'template_name': self.object.template_name} # ) # # messages.success(request, f'Report template "{self.object.template_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # IMAGING ORDER VIEWS (RESTRICTED CRUD - Clinical Orders) # # ============================================================================ # # class ImagingOrderListView(LoginRequiredMixin, ListView): # """ # List all imaging orders with filtering and search. # """ # model = ImagingOrder # template_name = 'radiology/imaging_order_list.html' # context_object_name = 'imaging_orders' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(study_description__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(modality=modality) # # # Filter by priority # priority = self.request.GET.get('priority') # if priority: # queryset = queryset.filter(priority=priority) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(order_datetime__date__gte=date_from) # if date_to: # queryset = queryset.filter(order_datetime__date__lte=date_to) # # return queryset.select_related( # 'patient', 'ordering_provider' # ).order_by('-order_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': ImagingOrder._meta.get_field('status').choices, # 'modalities': ImagingOrder._meta.get_field('modality').choices, # 'priorities': ImagingOrder._meta.get_field('priority').choices, # }) # return context # # # class ImagingOrderDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging order. # """ # model = ImagingOrder # template_name = 'radiology/imaging_order_detail.html' # context_object_name = 'imaging_order' # # def get_queryset(self): # return ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_order = self.object # # # Get studies for this order # context['studies'] = imaging_order.studies.all().order_by('-study_datetime') # # # Get reports for this order # context['reports'] = RadiologyReport.objects.filter( # study__order=imaging_order, # tenant=self.request.user.tenant # ).order_by('-created_at') # # return context # # # class ImagingOrderCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging order. # """ # model = ImagingOrder # form_class = ImagingOrderForm # template_name = 'radiology/imaging_order_form.html' # permission_required = 'radiology.add_imagingorder' # success_url = reverse_lazy('radiology:imaging_order_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.ordering_provider = self.request.user # form.instance.order_datetime = timezone.now() # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_ORDER_CREATED', # model='ImagingOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'modality': self.object.modality, # 'study_description': self.object.study_description, # 'priority': self.object.priority # } # ) # # messages.success(self.request, 'Imaging order created successfully.') # return response # # # class ImagingOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update imaging order (limited to status and notes only). # """ # model = ImagingOrder # fields = ['status', 'notes'] # Restricted fields for clinical orders # template_name = 'radiology/imaging_order_update_form.html' # permission_required = 'radiology.change_imagingorder' # # def get_queryset(self): # return ImagingOrder.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:imaging_order_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_ORDER_UPDATED', # model='ImagingOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Imaging order updated successfully.') # return response # # # # ============================================================================ # # IMAGING STUDY VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class ImagingStudyListView(LoginRequiredMixin, ListView): # """ # List all imaging studies with filtering and search. # """ # model = ImagingStudy # template_name = 'radiology/imaging_study_list.html' # context_object_name = 'imaging_studies' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(study_id__icontains=search) | # Q(order__patient__first_name__icontains=search) | # Q(order__patient__last_name__icontains=search) | # Q(order__patient__mrn__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by modality # modality = self.request.GET.get('modality') # if modality: # queryset = queryset.filter(order__modality=modality) # # return queryset.select_related('order__patient').order_by('-study_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': ImagingStudy._meta.get_field('status').choices, # 'modalities': ImagingOrder._meta.get_field('modality').choices, # }) # return context # # # class ImagingStudyDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging study. # """ # model = ImagingStudy # template_name = 'radiology/imaging_study_detail.html' # context_object_name = 'imaging_study' # # def get_queryset(self): # return ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_study = self.object # # # Get series for this study # context['series'] = imaging_study.series.all().order_by('series_number') # # # Get reports for this study # context['reports'] = imaging_study.reports.all().order_by('-created_at') # # # Get total image count # context['total_images'] = DICOMImage.objects.filter( # series__study=imaging_study, # tenant=self.request.user.tenant # ).count() # # return context # # # class ImagingStudyCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging study. # """ # model = ImagingStudy # form_class = ImagingStudyForm # template_name = 'radiology/imaging_study_form.html' # permission_required = 'radiology.add_imagingstudy' # success_url = reverse_lazy('radiology:imaging_study_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.technologist = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_STUDY_CREATED', # model='ImagingStudy', # object_id=str(self.object.study_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'modality': self.object.order.modality # } # ) # # messages.success(self.request, 'Imaging study created successfully.') # return response # # # class ImagingStudyUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update imaging study (limited to status and technical notes). # """ # model = ImagingStudy # fields = ['status', 'technical_notes'] # Restricted fields # template_name = 'radiology/imaging_study_update_form.html' # permission_required = 'radiology.change_imagingstudy' # # def get_queryset(self): # return ImagingStudy.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('radiology:imaging_study_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_STUDY_UPDATED', # model='ImagingStudy', # object_id=str(self.object.study_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Imaging study updated successfully.') # return response # # # # ============================================================================ # # IMAGING SERIES VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class ImagingSeriesListView(LoginRequiredMixin, ListView): # """ # List all imaging series with filtering and search. # """ # model = ImagingSeries # template_name = 'radiology/imaging_series_list.html' # context_object_name = 'imaging_series' # paginate_by = 25 # # def get_queryset(self): # queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant) # # # Filter by study # study_id = self.request.GET.get('study') # if study_id: # queryset = queryset.filter(study_id=study_id) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(series_description__icontains=search) | # Q(study__order__patient__first_name__icontains=search) | # Q(study__order__patient__last_name__icontains=search) # ) # # return queryset.select_related('study__order__patient').order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'studies': ImagingStudy.objects.filter( # tenant=self.request.user.tenant # ).select_related('order__patient').order_by('-study_datetime')[:50], # }) # return context # # # class ImagingSeriesDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an imaging series. # """ # model = ImagingSeries # template_name = 'radiology/imaging_series_detail.html' # context_object_name = 'imaging_series' # # def get_queryset(self): # return ImagingSeries.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # imaging_series = self.object # # # Get images for this series # context['images'] = imaging_series.images.all().order_by('instance_number') # # return context # # # class ImagingSeriesCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new imaging series. # """ # model = ImagingSeries # form_class = ImagingSeriesForm # template_name = 'radiology/imaging_series_form.html' # permission_required = 'radiology.add_imagingseries' # success_url = reverse_lazy('radiology:imaging_series_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='IMAGING_SERIES_CREATED', # model='ImagingSeries', # object_id=str(self.object.series_id), # details={ # 'series_description': self.object.series_description, # 'study_id': str(self.object.study.study_id) # } # ) # # messages.success(self.request, 'Imaging series created successfully.') # return response # # # # ============================================================================ # # DICOM IMAGE VIEWS (READ-ONLY - System Generated) # # ============================================================================ # # class DICOMImageListView(LoginRequiredMixin, ListView): # """ # List all DICOM images with filtering and search. # """ # model = DICOMImage # template_name = 'radiology/dicom_image_list.html' # context_object_name = 'dicom_images' # paginate_by = 50 # # def get_queryset(self): # queryset = DICOMImage.objects.filter(tenant=self.request.user.tenant) # # # Filter by series # series_id = self.request.GET.get('series') # if series_id: # queryset = queryset.filter(series_id=series_id) # # # Filter by study # study_id = self.request.GET.get('study') # if study_id: # queryset = queryset.filter(series__study_id=study_id) # # return queryset.select_related('series__study__order__patient').order_by('instance_number') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'series': ImagingSeries.objects.filter( # tenant=self.request.user.tenant # ).select_related('study__order__patient').order_by('-created_at')[:50], # }) # return context # # # class DICOMImageDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a DICOM image. # """ # model = DICOMImage # template_name = 'radiology/dicom_image_detail.html' # context_object_name = 'dicom_image' # # def get_queryset(self): # return DICOMImage.objects.filter(tenant=self.request.user.tenant) # # # # ============================================================================ # # RADIOLOGY REPORT VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class RadiologyReportListView(LoginRequiredMixin, ListView): # """ # List all radiology reports with filtering and search. # """ # model = RadiologyReport # template_name = 'radiology/radiology_report_list.html' # context_object_name = 'radiology_reports' # paginate_by = 25 # # def get_queryset(self): # queryset = RadiologyReport.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(study__order__patient__first_name__icontains=search) | # Q(study__order__patient__last_name__icontains=search) | # Q(study__order__patient__mrn__icontains=search) | # Q(findings__icontains=search) | # Q(impression__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by critical findings # critical_only = self.request.GET.get('critical_only') # if critical_only: # queryset = queryset.filter(has_critical_findings=True) # # # Filter by radiologist # radiologist_id = self.request.GET.get('radiologist') # if radiologist_id: # queryset = queryset.filter(radiologist_id=radiologist_id) # # return queryset.select_related( # 'study__order__patient', 'radiologist', 'template' # ).order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': RadiologyReport._meta.get_field('status').choices, # }) # return context # # # class RadiologyReportDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a radiology report. # """ # model = RadiologyReport # template_name = 'radiology/radiology_report_detail.html' # context_object_name = 'radiology_report' # # def get_queryset(self): # return RadiologyReport.objects.filter(tenant=self.request.user.tenant) # # # class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new radiology report. # """ # model = RadiologyReport # form_class = RadiologyReportForm # template_name = 'radiology/radiology_report_form.html' # permission_required = 'radiology.add_radiologyreport' # success_url = reverse_lazy('radiology:radiology_report_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.radiologist = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='RADIOLOGY_REPORT_CREATED', # model='RadiologyReport', # object_id=str(self.object.report_id), # details={ # 'patient_name': f"{self.object.study.order.patient.first_name} {self.object.study.order.patient.last_name}", # 'has_critical_findings': self.object.has_critical_findings # } # ) # # messages.success(self.request, 'Radiology report created successfully.') # return response # # # # Note: No UpdateView or DeleteView for RadiologyReport - Append-only for clinical records # # Reports can only be amended through addendum process after signing # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def radiology_stats(request): # """ # HTMX endpoint for radiology statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # # stats = { # 'pending_orders': ImagingOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'scheduled_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='SCHEDULED' # ).count(), # 'in_progress_studies': ImagingStudy.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'reports_pending': RadiologyReport.objects.filter( # tenant=tenant, # status='DRAFT' # ).count(), # 'critical_findings': RadiologyReport.objects.filter( # tenant=tenant, # has_critical_findings=True, # status='SIGNED', # signed_datetime__date=today # ).count(), # } # # return render(request, 'radiology/partials/radiology_stats.html', {'stats': stats}) # # # @login_required # def order_search(request): # """ # HTMX endpoint for imaging order search. # """ # search = request.GET.get('search', '') # status = request.GET.get('status', '') # modality = request.GET.get('modality', '') # # queryset = ImagingOrder.objects.filter(tenant=request.user.tenant) # # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(study_description__icontains=search) # ) # # if status: # queryset = queryset.filter(status=status) # # if modality: # queryset = queryset.filter(modality=modality) # # orders = queryset.select_related( # 'patient', 'ordering_provider' # ).order_by('-order_datetime')[:20] # # return render(request, 'radiology/partials/order_list.html', {'orders': orders}) # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def schedule_study(request, order_id): # """ # Schedule an imaging study for an order. # """ # if request.method == 'POST': # order = get_object_or_404( # ImagingOrder, # id=order_id, # tenant=request.user.tenant # ) # # scheduled_datetime = request.POST.get('scheduled_datetime') # if scheduled_datetime: # scheduled_datetime = timezone.datetime.fromisoformat(scheduled_datetime) # # # Create or update study # study, created = ImagingStudy.objects.get_or_create( # order=order, # tenant=request.user.tenant, # defaults={ # 'study_datetime': scheduled_datetime, # 'status': 'SCHEDULED', # 'technologist': request.user # } # ) # # if not created: # study.study_datetime = scheduled_datetime # study.status = 'SCHEDULED' # study.save() # # # Update order status # order.status = 'SCHEDULED' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_SCHEDULED', # model='ImagingOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'scheduled_time': scheduled_datetime.isoformat() # } # ) # # messages.success(request, 'Study scheduled successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/order_status.html', {'order': order}) # # return redirect('radiology:imaging_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required # def start_study(request, study_id): # """ # Start an imaging study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # study.status = 'IN_PROGRESS' # study.started_datetime = timezone.now() # study.technologist = request.user # study.save() # # # Update order status # study.order.status = 'IN_PROGRESS' # study.order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_STARTED', # model='ImagingStudy', # object_id=str(study.study_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'modality': study.order.modality # } # ) # # messages.success(request, 'Study started successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/study_status.html', {'study': study}) # # return redirect('radiology:imaging_study_detail', pk=study.pk) # # return JsonResponse({'success': False}) # # # @login_required # def complete_study(request, study_id): # """ # Complete an imaging study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # study.status = 'COMPLETED' # study.completed_datetime = timezone.now() # study.save() # # # Update order status # study.order.status = 'COMPLETED' # study.order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='STUDY_COMPLETED', # model='ImagingStudy', # object_id=str(study.study_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'modality': study.order.modality # } # ) # # messages.success(request, 'Study completed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/study_status.html', {'study': study}) # # return redirect('radiology:imaging_study_detail', pk=study.pk) # # return JsonResponse({'success': False}) # # # @login_required # def sign_report(request, report_id): # """ # Sign a radiology report. # """ # if request.method == 'POST': # report = get_object_or_404( # RadiologyReport, # id=report_id, # tenant=request.user.tenant # ) # # # Only allow signing if report is in draft status # if report.status != 'DRAFT': # messages.error(request, 'Only draft reports can be signed.') # return redirect('radiology:radiology_report_detail', pk=report.pk) # # report.status = 'SIGNED' # report.signed_datetime = timezone.now() # report.signed_by = request.user # report.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_SIGNED', # model='RadiologyReport', # object_id=str(report.report_id), # details={ # 'patient_name': f"{report.study.order.patient.first_name} {report.study.order.patient.last_name}", # 'has_critical_findings': report.has_critical_findings # } # ) # # messages.success(request, 'Report signed successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'radiology/partials/report_status.html', {'report': report}) # # return redirect('radiology:radiology_report_detail', pk=report.pk) # # return JsonResponse({'success': False}) # # # @login_required # def dictate_report(request, study_id): # """ # Start dictating a report for a study. # """ # if request.method == 'POST': # study = get_object_or_404( # ImagingStudy, # id=study_id, # tenant=request.user.tenant # ) # # template_id = request.POST.get('template_id') # template = None # if template_id: # template = get_object_or_404( # ReportTemplate, # id=template_id, # tenant=request.user.tenant # ) # # # Create report if it doesn't exist # report, created = RadiologyReport.objects.get_or_create( # study=study, # tenant=request.user.tenant, # defaults={ # 'radiologist': request.user, # 'template': template, # 'status': 'DRAFT' # } # ) # # if created: # # Log the action # AuditLogger.log_action( # user=request.user, # action='REPORT_DICTATION_STARTED', # model='RadiologyReport', # object_id=str(report.report_id), # details={ # 'patient_name': f"{study.order.patient.first_name} {study.order.patient.last_name}", # 'template_used': template.template_name if template else None # } # ) # # messages.success(request, 'Report dictation started.') # # return redirect('radiology:radiology_report_detail', pk=report.pk) # # return JsonResponse({'success': False}) # # # #