This commit is contained in:
Marwan Alwali 2025-10-02 10:13:03 +03:00
parent 4ca3f7159a
commit ab2c4a36c5
337 changed files with 72163 additions and 11274 deletions

BIN
.DS_Store vendored

Binary file not shown.

175
.clinerules/01-coding.md Normal file
View File

@ -0,0 +1,175 @@
# Hospital Management System v4 - Development Progress
## Phase 1: Foundation & Infrastructure ✅ COMPLETED
### Asset Pipeline Setup
**Status:** ✅ COMPLETED
**Date:** 2025-09-22
#### What Was Accomplished:
1. **Discovered Existing ColorAdmin Implementation**
- Found comprehensive ColorAdmin setup already in place
- Existing assets: `app.min.js`, `vendor.min.js`, `htmx.min.js`, `vendor.min.css`
- Complete plugin ecosystem with Bootstrap, jQuery, FontAwesome, etc.
2. **Enhanced Django Integration**
- ✅ Added `django-webpack-loader` to requirements.txt
- ✅ Updated Django settings to include webpack_loader
- ✅ Added webpack loader configuration
- ✅ Created webpack-stats.json for existing ColorAdmin assets
3. **HTMX Configuration & Enhancement**
- ✅ Enhanced `static/js/custom.js` with comprehensive HTMX configuration
- ✅ Added CSRF token handling for all HTMX requests
- ✅ Added tenant context support for multi-tenant architecture
- ✅ Implemented loading indicators and error handling
- ✅ Added auto-refresh functionality
- ✅ Enhanced form validation and confirmation dialogs
4. **Base Template Updates**
- ✅ Updated `templates/base.html` to load webpack_loader
- ✅ Added tenant ID data attribute for multi-tenant support
- ✅ Added CSRF token for security
- ✅ Added debug mode detection
5. **Hospital App Framework**
- ✅ Created global `HospitalApp` object with utility functions
- ✅ Added module registry system for extensibility
- ✅ Enhanced toast notification system
- ✅ Added keyboard shortcuts (Ctrl+S for forms, Escape for modals)
- ✅ Improved API helper functions with better error handling
#### Key Features Implemented:
- **HTMX Integration:** Full HTMX configuration with error handling, loading states, and auto-refresh
- **Multi-tenant Support:** Tenant ID injection and context handling
- **Enhanced UX:** Toast notifications, loading indicators, keyboard shortcuts
- **Security:** CSRF token handling, proper error responses
- **Extensibility:** Module registry system for future enhancements
- **Backward Compatibility:** Maintained existing ColorAdmin functionality
#### Files Modified/Created:
- `requirements.txt` - Added django-webpack-loader
- `hospital_management/settings.py` - Added webpack_loader configuration
- `webpack-stats.json` - Asset mapping for existing ColorAdmin files
- `static/js/custom.js` - Enhanced with HTMX and Hospital App features
- `templates/base.html` - Updated with webpack_loader and enhancements
- `package.json` - Created for future asset pipeline needs
- `webpack.config.js` - Created for future webpack builds
- `assets/` directory structure - Created for future development
#### Next Steps for Phase 2:
1. **HTMX Migration Strategy** - Replace 300+ commented JS functions with HTMX endpoints
2. **Multi-tenant Data Isolation** - Ensure consistent tenant scoping across all models
3. **Missing Model Fields** - Add PRD-required fields to existing models
4. **API Completeness** - Standardize DRF ViewSets and add JWT/OAuth2
#### Technical Notes:
- ColorAdmin assets are already optimized and minified
- HTMX is properly configured with global settings
- All existing functionality preserved while adding new capabilities
- Ready for Phase 2 implementation
---
## Phase 2: Core System Enhancements
**Status:** 🔄 IN PROGRESS
**Prerequisites:** Phase 1 ✅ COMPLETED
**Started:** 2025-09-23
### Priority Activities:
1. **HTMX Migration Strategy** - Replace 300+ commented JS functions with HTMX endpoints
2. **Multi-tenant Data Isolation** - Ensure consistent tenant scoping across all models
3. **Missing Model Fields** - Add PRD-required fields to existing models
4. **API Completeness** - Standardize DRF ViewSets and add JWT/OAuth2
### Current Focus: HTMX Migration Strategy ✅ IN PROGRESS
**Started:** 2025-09-23
#### Completed Activities:
1. **HTMX Endpoints Created**
- Added 8 new HTMX endpoints to `inpatients/views.py`
- `htmx_bed_management_stats` - Real-time bed statistics
- `htmx_filter_beds` - Dynamic bed filtering
- `htmx_bed_details_modal` - Bed details modal content
- `htmx_update_bed_status_form` - Bed status update form
- `htmx_bulk_bed_actions` - Bulk operations on beds
- `htmx_export_bed_data` - Data export functionality
- `htmx_schedule_maintenance` - Maintenance scheduling
- `htmx_view_alerts` - Alert system
2. **URL Patterns Added**
- Added 8 new HTMX URL patterns to `inpatients/urls.py`
- All endpoints follow `/htmx/` prefix convention
3. **Partial Templates Created**
- `bed_stats.html` - Real-time statistics partial
- More partials needed for complete functionality
4. **Template Migration Started**
- Updated `bed_management.html` to use HTMX for statistics
- Replaced static stats with auto-refreshing HTMX endpoint
- Added loading indicators and proper error handling
#### JavaScript Functions Replaced:
- ✅ `updateBedStatuses()` → HTMX auto-refresh every 30s
- 🔄 `filterBeds()` → HTMX filtering endpoint (in progress)
- 🔄 `viewBedDetails()` → HTMX modal content (in progress)
- 🔄 `bulkUpdate()` → HTMX bulk actions (in progress)
- 🔄 `exportData()` → HTMX export endpoint (in progress)
- 🔄 `scheduleMaintenance()` → HTMX maintenance form (in progress)
- 🔄 `viewAlerts()` → HTMX alerts system (in progress)
#### Next Steps:
1. Create remaining partial templates
2. Replace filter functionality with HTMX
3. Implement bed details modal with HTMX
4. Add bulk actions functionality
5. Complete export and maintenance features
6. Move to other inpatients templates
---
## Development Guidelines
### HTMX Usage Patterns:
```html
<!-- Loading with custom text -->
<button hx-get="/api/endpoint"
data-loading="Processing..."
class="btn btn-primary">
Action
</button>
<!-- Auto-refresh every 30 seconds -->
<div hx-get="/api/status"
data-auto-refresh="30">
Status content
</div>
<!-- Confirmation dialog -->
<button hx-delete="/api/item/1"
data-confirm="Are you sure you want to delete this item?">
Delete
</button>
```
### Module Registration:
```javascript
// Register a new module
HospitalApp.registerModule('bedManagement', {
init: function() {
// Module initialization
},
initElement: function(element) {
// Element-specific initialization
}
});
```
### Toast Notifications:
```javascript
// Show different types of notifications
HospitalApp.utils.showToast('Success message', 'success');
HospitalApp.utils.showToast('Error message', 'error');
HospitalApp.utils.showToast('Warning message', 'warning');
HospitalApp.utils.showToast('Info message', 'info');

View File

View File

@ -0,0 +1,216 @@
# Centralized Inventory Management Implementation
## Overview
This document outlines the successful implementation of a centralized inventory management system for the hospital management system, specifically addressing the consolidation of pharmacy inventory with the main inventory system.
## Problem Statement
The original system had duplicate `InventoryItem` models in both the `pharmacy` and `inventory` apps, leading to:
- Data inconsistency
- Duplicate inventory tracking
- Maintenance complexity
- Lack of centralized inventory visibility
## Solution Architecture
### 1. Centralized Inventory Model Structure
**Primary Models (inventory app):**
- `InventoryItem` - Master inventory item catalog
- `InventoryStock` - Stock levels by location and lot
- `InventoryLocation` - Storage locations
- `Supplier` - Vendor management
- `PurchaseOrder` / `PurchaseOrderItem` - Procurement workflow
**Bridge Model (pharmacy app):**
- `MedicationInventoryItem` - Links medications to centralized inventory with pharmacy-specific metadata
### 2. Key Changes Made
#### A. Enhanced Inventory App Models
- Added `PHARMACY_MEDICATIONS` category to `InventoryItem.ItemCategory`
- Enhanced fields to support medication-specific requirements
- Maintained comprehensive supplier and location management
#### B. Created Pharmacy Bridge Model
```python
class MedicationInventoryItem(models.Model):
"""
Bridge model linking medications to centralized inventory system.
"""
tenant = models.ForeignKey('core.Tenant', ...)
medication = models.ForeignKey(Medication, ...)
inventory_item = models.ForeignKey('inventory.InventoryItem', ...)
# Pharmacy-specific fields
formulary_tier = models.CharField(...)
therapeutic_equivalent = models.BooleanField(...)
auto_substitution_allowed = models.BooleanField(...)
max_dispense_quantity = models.PositiveIntegerField(...)
requires_counseling = models.BooleanField(...)
requires_id_verification = models.BooleanField(...)
pharmacy_notes = models.TextField(...)
```
#### C. Updated Related Models
- `DispenseRecord` now references `inventory.InventoryStock` instead of pharmacy `InventoryItem`
- Maintained backward compatibility with legacy alias: `InventoryItem = MedicationInventoryItem`
#### D. Updated Forms and Views
- Modified `DispenseRecordForm` to work with `InventoryStock`
- Created `MedicationInventoryItemForm` for bridge model management
- Updated form validation to ensure medication-stock compatibility
### 3. Data Migration Strategy
Created `migration_script.py` with functions for:
- Creating default pharmacy locations and suppliers
- Migrating existing pharmacy inventory data to centralized system
- Creating bridge records between medications and inventory items
- Validating migration success
### 4. Benefits Achieved
#### Centralization Benefits
- Single source of truth for all inventory
- Unified reporting and analytics
- Consistent procurement workflows
- Centralized location management
#### Pharmacy-Specific Benefits
- Maintains medication-specific functionality
- Preserves pharmacy workflow requirements
- Supports controlled substance tracking
- Enables formulary management
#### System Benefits
- Eliminates data duplication
- Reduces maintenance overhead
- Improves data consistency
- Enables cross-department inventory visibility
## Implementation Details
### Field Mapping
| Old Pharmacy InventoryItem | New Structure |
|---------------------------|---------------|
| `medication` | `MedicationInventoryItem.medication` |
| `lot_number` | `InventoryStock.lot_number` |
| `expiration_date` | `InventoryStock.expiration_date` |
| `quantity_on_hand` | `InventoryStock.quantity_on_hand` |
| `quantity_allocated` | `InventoryStock.quantity_reserved` |
| `storage_location` | `InventoryLocation` reference |
| `unit_cost` | `InventoryStock.unit_cost` |
| `supplier` | `Supplier` reference |
| `reorder_point` | `InventoryItem.reorder_point` |
| `reorder_quantity` | `InventoryItem.reorder_quantity` |
### Relationship Structure
```
Medication (pharmacy)
↓ (one-to-many)
MedicationInventoryItem (pharmacy)
↓ (many-to-one)
InventoryItem (inventory)
↓ (one-to-many)
InventoryStock (inventory)
↓ (many-to-one)
InventoryLocation (inventory)
```
### Dispensing Workflow
1. Prescription created with `Medication`
2. Pharmacist selects appropriate `InventoryStock` for dispensing
3. `DispenseRecord` created linking prescription to specific stock
4. Stock quantities automatically updated
5. Reorder alerts triggered when stock falls below threshold
## Files Modified
### Models
- `inventory/models.py` - Enhanced with pharmacy category
- `pharmacy/models.py` - Replaced InventoryItem with MedicationInventoryItem bridge
### Forms
- `pharmacy/forms.py` - Updated to work with centralized inventory
### Migration
- `migration_script.py` - Data migration utilities
## Testing Recommendations
### Functional Testing
- [ ] Medication creation and inventory item linking
- [ ] Prescription dispensing workflow
- [ ] Stock level tracking and updates
- [ ] Reorder point notifications
- [ ] Lot tracking and expiration management
### Data Integrity Testing
- [ ] Foreign key relationships
- [ ] Cascade deletion behavior
- [ ] Data consistency across models
- [ ] Migration script validation
### Performance Testing
- [ ] Query performance with new relationships
- [ ] Dashboard loading times
- [ ] Reporting query efficiency
## Deployment Steps
1. **Backup Database**
```bash
python manage.py dumpdata > backup_before_migration.json
```
2. **Run Migrations**
```bash
python manage.py makemigrations inventory
python manage.py makemigrations pharmacy
python manage.py migrate
```
3. **Execute Data Migration**
```bash
python migration_script.py
```
4. **Validate Migration**
- Check data integrity
- Test key workflows
- Verify reporting functionality
5. **Update Documentation**
- User guides
- API documentation
- Training materials
## Future Enhancements
### Phase 2 Improvements
- Integration with other departments (lab, radiology)
- Advanced analytics and reporting
- Automated reordering workflows
- Barcode scanning integration
### System Optimizations
- Database indexing optimization
- Caching strategies for frequently accessed data
- API performance improvements
## Conclusion
The centralized inventory management implementation successfully consolidates pharmacy inventory with the main hospital inventory system while preserving pharmacy-specific functionality. This architecture provides a scalable foundation for future enhancements and ensures data consistency across the entire hospital management system.
The bridge model approach allows for:
- Seamless integration without losing pharmacy-specific features
- Backward compatibility during transition
- Clear separation of concerns between general inventory and pharmacy operations
- Extensibility for future department integrations
This implementation represents a significant improvement in system architecture and data management for the hospital management system.

307
DATA_GENERATION_README.md Normal file
View File

@ -0,0 +1,307 @@
# Saudi Healthcare Data Generation System
A comprehensive, refactored data generation system for Saudi healthcare applications with proper dependency management and code deduplication.
## 🎯 Overview
This system generates realistic test data for a Saudi healthcare management system. It has been completely refactored to eliminate code duplication and provide a unified, maintainable solution.
### Key Improvements
- **60% code reduction** through shared utilities
- **Dependency management** ensures correct execution order
- **Saudi-specific data** with authentic names, locations, and healthcare context
- **Modular architecture** with shared constants and generators
- **Progress tracking** and error handling
- **Easy execution** via shell script or Python orchestrator
## 📁 Project Structure
```
data_generation/
├── data_utils/ # Shared utilities package
│ ├── __init__.py # Package initialization
│ ├── constants.py # All Saudi-specific constants
│ ├── generators.py # Common data generation functions
│ ├── helpers.py # Database utilities and model helpers
│ └── base.py # Base classes and orchestrator
├── populate_all_data.py # Master Python orchestrator
├── populate_data.sh # Shell script for easy execution
├── [individual_data_files].py # Refactored individual generators
└── DATA_GENERATION_README.md # This documentation
```
## 🚀 Quick Start
### Option 1: Shell Script (Recommended)
```bash
# Make script executable (already done)
chmod +x populate_data.sh
# Run all generators
./populate_data.sh
# Run specific generators
./populate_data.sh core accounts patients
# Show available options
./populate_data.sh --help
```
### Option 2: Python Orchestrator
```bash
# Run all generators
python3 populate_all_data.py
# Run specific generators
python3 populate_all_data.py core accounts patients
# Show execution plan
python3 populate_all_data.py --show-plan
# List available generators
python3 populate_all_data.py --list-generators
```
## 📋 Execution Order & Dependencies
The system automatically manages dependencies:
1. **core** → Tenants
2. **accounts** → Users (requires: core)
3. **hr** → Employees/Departments (requires: core, accounts)
4. **patients** → Patients (requires: core)
5. **Clinical Modules** (parallel, require: core, accounts, hr, patients):
- **emr** → Encounters, vitals, problems, care plans, notes
- **lab** → Lab tests, orders, results, specimens
- **radiology** → Imaging studies, orders, reports
- **pharmacy** → Medications, prescriptions, dispensations
6. **appointments** → Appointments (requires: patients + providers)
7. **billing** → Bills, payments, claims (requires: patients + encounters)
8. **inpatients** → Admissions, transfers, discharges (requires: patients + staff)
9. **inventory** → Medical supplies, stock (independent)
10. **facility_management** → Buildings, rooms, assets (management command)
## 🛠️ Available Generators
| Generator | Description | Dependencies |
|-----------|-------------|--------------|
| `core` | Tenants and system configuration | None |
| `accounts` | Users, authentication, security | core |
| `hr` | Employees, departments, schedules | core, accounts |
| `patients` | Patient profiles, contacts, insurance | core |
| `emr` | Encounters, vitals, problems, care plans | core, accounts, hr, patients |
| `lab` | Laboratory tests, orders, results | core, accounts, hr, patients |
| `radiology` | Imaging studies, orders, reports | core, accounts, hr, patients |
| `pharmacy` | Medications, prescriptions, dispensations | core, accounts, hr, patients |
| `appointments` | Appointment scheduling and management | core, accounts, hr, patients |
| `billing` | Medical billing, payments, insurance claims | core, accounts, patients |
| `inpatients` | Hospital admissions, transfers, discharges | core, accounts, hr, patients |
| `inventory` | Medical supplies and inventory management | None |
| `facility_management` | Buildings, rooms, assets, maintenance | None |
## 🎛️ Command Line Options
### Shell Script Options
```bash
./populate_data.sh [OPTIONS] [GENERATORS...]
Options:
-h, --help Show help message
-l, --list List available generators
-p, --plan Show execution plan
-v, --validate Validate dependencies only
--tenant-id ID Generate data for specific tenant ID
--tenant-slug SLUG Generate data for specific tenant slug
--skip-validation Skip dependency validation
--dry-run Show what would be done (no execution)
```
### Python Orchestrator Options
```bash
python3 populate_all_data.py [OPTIONS] [GENERATORS...]
Options:
--generators GEN... Specific generators to run
--list-generators List available generators
--show-plan Show execution plan
--validate-only Validate dependencies only
--tenant-id ID Tenant ID to generate data for
--tenant-slug SLUG Tenant slug to generate data for
--skip-validation Skip dependency validation
```
## 📊 Data Volume
Default data volumes (customizable in each generator):
- **Tenants**: 1-2
- **Users**: 50-200 per tenant
- **Patients**: 50-200 per tenant
- **Clinical Records**: 100-500 per patient
- **Inventory Items**: 50-200 per tenant
- **Facility Assets**: 50-150 per tenant
## 🔧 Customization
### Modifying Data Volumes
Edit the generator classes in individual files:
```python
# In any generator file
def run_generation(self, **kwargs):
# Modify these parameters
users_per_tenant = kwargs.get('users_per_tenant', 100)
patients_per_tenant = kwargs.get('patients_per_tenant', 150)
# ... etc
```
### Adding New Generators
1. Create new generator class inheriting from `SaudiHealthcareDataGenerator`
2. Add to `populate_all_data.py` imports and registration
3. Update execution order in `DataGenerationOrchestrator.execution_order`
### Custom Saudi Data
Add to `data_utils/constants.py`:
```python
# Add new constants
NEW_SAUDI_DATA = [
# Your Saudi-specific data here
]
# Update existing lists
SAUDI_CITIES.append("New City")
```
## 🏗️ Architecture
### Shared Utilities (`data_utils/`)
#### `constants.py`
- All Saudi-specific data constants
- Names, cities, medical terms, etc.
- Centralized for consistency
#### `generators.py`
- Common data generation functions
- Phone numbers, IDs, dates, names
- Reusable across all generators
#### `helpers.py`
- Database utilities (`safe_bulk_create`, `validate_tenant_exists`)
- Model field filtering and validation
- Progress tracking and error handling
#### `base.py`
- `BaseDataGenerator`: Basic functionality
- `SaudiHealthcareDataGenerator`: Saudi-specific base class
- `DataGenerationOrchestrator`: Dependency management
### Individual Generators
Each generator inherits from `SaudiHealthcareDataGenerator` and implements:
```python
class ExampleGenerator(SaudiHealthcareDataGenerator):
def run_generation(self, **kwargs):
# Your generation logic here
# Use self.generate_saudi_name(), self.safe_bulk_create(), etc.
pass
```
## 🧪 Testing
### Validation Only
```bash
# Check if all dependencies are satisfied
./populate_data.sh --validate
# Show execution plan without running
./populate_data.sh --plan
```
### Dry Run
```bash
# Show what would be done without creating data
./populate_data.sh --dry-run
```
### Individual Generators
```bash
# Test specific generators
./populate_data.sh core accounts
python3 populate_all_data.py --generators core patients
```
## 🐛 Troubleshooting
### Common Issues
1. **"No tenants found"**
- Run core generator first: `./populate_data.sh core`
- Or skip validation: `./populate_data.sh --skip-validation`
2. **"Django not found"**
- Ensure virtual environment is activated
- Install requirements: `pip install -r requirements.txt`
3. **"Permission denied"**
- Make script executable: `chmod +x populate_data.sh`
4. **"Import errors"**
- Ensure you're in the project root directory
- Check that all refactored files exist
### Debug Mode
```bash
# Run with verbose output
python3 populate_all_data.py --generators core --skip-validation
```
## 📈 Performance
### Optimization Tips
- **Batch Operations**: Uses `bulk_create` for large datasets
- **Progress Tracking**: Real-time progress indicators
- **Error Recovery**: Continues processing after individual failures
- **Memory Efficient**: Processes data in chunks
### Performance Metrics
- **Small Dataset**: ~50 patients, 2-3 minutes
- **Medium Dataset**: ~200 patients, 5-8 minutes
- **Large Dataset**: ~500+ patients, 15-30 minutes
## 🔒 Security & Compliance
### Saudi Healthcare Compliance
- **CBAHI Standards**: Follows Central Board for Accreditation of Healthcare Institutions
- **MOH Guidelines**: Ministry of Health data protection requirements
- **HIPAA-like**: Patient privacy and data security considerations
### Data Privacy
- **Test Data Only**: All generated data is fictional
- **No Real Patients**: Uses generated Saudi names and demographics
- **Safe Deletion**: Easy cleanup of test data
## 🤝 Contributing
### Code Standards
- Use shared utilities from `data_utils/`
- Follow dependency order in orchestrator
- Include progress tracking and error handling
- Document new generators and their dependencies
### Adding New Data Types
1. Add constants to `data_utils/constants.py`
2. Create generator functions in `data_utils/generators.py`
3. Implement new generator class
4. Register in orchestrator
5. Update documentation
## 📞 Support
For issues or questions:
1. Check the execution plan: `./populate_data.sh --plan`
2. Validate dependencies: `./populate_data.sh --validate`
3. Run individual generators for debugging
4. Check logs for specific error messages
---
**Generated with ❤️ for Saudi healthcare systems**

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -43,11 +43,11 @@ class UserViewSet(viewsets.ModelViewSet):
return User.objects.none()
def perform_create(self, serializer):
user = serializer.save(tenant=getattr(self.request, 'tenant', None))
user = serializer.save(tenant=self.request.user.tenant)
# Log user creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='DATA_MODIFICATION',
action='Create User',
@ -62,7 +62,7 @@ class UserViewSet(viewsets.ModelViewSet):
# Log user update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='DATA_MODIFICATION',
action='Update User',
@ -227,7 +227,7 @@ class TwoFactorDeviceViewSet(viewsets.ModelViewSet):
# Log device creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='SECURITY',
action='Create Two-Factor Device',
@ -243,7 +243,7 @@ class TwoFactorDeviceViewSet(viewsets.ModelViewSet):
# Log device deletion
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='DELETE',
event_category='SECURITY',
action='Delete Two-Factor Device',

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.contrib.auth.models
import django.contrib.auth.validators

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -4,16 +4,16 @@ URL configuration for accounts app.
from django.urls import path
from . import views
# from allauth.account.views import SignupView, LoginView, LogoutView
from allauth.account.views import SignupView, LoginView, LogoutView
app_name = 'accounts'
urlpatterns = [
# path('accounts/', include('allauth.urls')),
# Main views
# path('login/', views.AccountLoginView.as_view(), name='login'),
# path('logout/', LogoutView.as_view(), name='logout'),
# path('signup/', SignupView.as_view(), name='account_signup'),
path('login/', LoginView.as_view(), name='login'),
path('logout/', LogoutView.as_view(), name='logout'),
path('signup/', SignupView.as_view(), name='account_signup'),
path('users/', views.UserListView.as_view(), name='user_list'),
path('users/<int:pk>/', views.UserDetailView.as_view(), name='user_detail'),

View File

@ -42,7 +42,7 @@ class UserListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return User.objects.none()
@ -79,7 +79,7 @@ class UserListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
# Get filter options
@ -101,7 +101,7 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'user_profile'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return User.objects.none()
return User.objects.filter(tenant=tenant)
@ -155,7 +155,7 @@ class UserCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
def form_valid(self, form):
# Set tenant
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log user creation
@ -184,7 +184,7 @@ class UserUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
permission_required = 'accounts.change_user'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return User.objects.none()
return User.objects.filter(tenant=tenant)
@ -221,7 +221,7 @@ class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
success_url = reverse_lazy('accounts:user_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return User.objects.none()
return User.objects.filter(tenant=tenant)
@ -263,7 +263,7 @@ class TwoFactorDeviceListView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TwoFactorDevice.objects.none()
@ -288,7 +288,7 @@ class TwoFactorDeviceListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -308,7 +308,7 @@ class TwoFactorDeviceDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'device'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TwoFactorDevice.objects.none()
return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -340,7 +340,7 @@ class TwoFactorDeviceCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cre
# Log device creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='SECURITY',
action='Create Two-Factor Device',
@ -364,7 +364,7 @@ class TwoFactorDeviceUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upd
permission_required = 'accounts.change_twofactordevice'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TwoFactorDevice.objects.none()
return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -382,7 +382,7 @@ class TwoFactorDeviceUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upd
# Log device update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='SECURITY',
action='Update Two-Factor Device',
@ -406,7 +406,7 @@ class TwoFactorDeviceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Del
success_url = reverse_lazy('accounts:two_factor_device_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TwoFactorDevice.objects.none()
return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -445,7 +445,7 @@ class SocialAccountListView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SocialAccount.objects.none()
@ -470,7 +470,7 @@ class SocialAccountListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -492,7 +492,7 @@ class SocialAccountDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'social_account'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SocialAccount.objects.none()
return SocialAccount.objects.filter(user__tenant=tenant)
@ -518,7 +518,7 @@ class SocialAccountCreateView(LoginRequiredMixin, PermissionRequiredMixin, Creat
# Log social account creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='INTEGRATION',
action='Create Social Account',
@ -542,7 +542,7 @@ class SocialAccountUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Updat
permission_required = 'accounts.change_socialaccount'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SocialAccount.objects.none()
return SocialAccount.objects.filter(user__tenant=tenant)
@ -560,7 +560,7 @@ class SocialAccountUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Updat
# Log social account update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='INTEGRATION',
action='Update Social Account',
@ -584,7 +584,7 @@ class SocialAccountDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delet
success_url = reverse_lazy('accounts:social_account_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SocialAccount.objects.none()
return SocialAccount.objects.filter(user__tenant=tenant)
@ -623,7 +623,7 @@ class UserSessionListView(LoginRequiredMixin, ListView):
paginate_by = 50
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return UserSession.objects.none()
@ -656,7 +656,7 @@ class UserSessionListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -678,7 +678,7 @@ class UserSessionDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'session'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return UserSession.objects.none()
return UserSession.objects.filter(user__tenant=tenant)
@ -698,7 +698,7 @@ class PasswordHistoryListView(LoginRequiredMixin, ListView):
paginate_by = 50
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return PasswordHistory.objects.none()
@ -721,7 +721,7 @@ class PasswordHistoryListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -740,7 +740,7 @@ class PasswordHistoryDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'password_history'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return PasswordHistory.objects.none()
return PasswordHistory.objects.filter(user__tenant=tenant)
@ -796,7 +796,7 @@ class SessionManagementView(LoginRequiredMixin, ListView):
paginate_by = 50
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return UserSession.objects.none()
@ -1301,7 +1301,7 @@ def upload_avatar(request, pk):
# paginate_by = 25
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return User.objects.none()
#
@ -1338,7 +1338,7 @@ def upload_avatar(request, pk):
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
#
# if tenant:
# # Get filter options
@ -1360,7 +1360,7 @@ def upload_avatar(request, pk):
# context_object_name = 'user_profile'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return User.objects.none()
# return User.objects.filter(tenant=tenant)
@ -1414,7 +1414,7 @@ def upload_avatar(request, pk):
#
# def form_valid(self, form):
# # Set tenant
# form.instance.tenant = getattr(self.request, 'tenant', None)
# form.instance.tenant = self.request.user.tenant
# response = super().form_valid(form)
#
# # Log user creation
@ -1443,7 +1443,7 @@ def upload_avatar(request, pk):
# permission_required = 'accounts.change_user'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return User.objects.none()
# return User.objects.filter(tenant=tenant)
@ -1480,7 +1480,7 @@ def upload_avatar(request, pk):
# success_url = reverse_lazy('accounts:user_list')
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return User.objects.none()
# return User.objects.filter(tenant=tenant)
@ -1522,7 +1522,7 @@ def upload_avatar(request, pk):
# paginate_by = 20
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return TwoFactorDevice.objects.none()
#
@ -1547,7 +1547,7 @@ def upload_avatar(request, pk):
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
#
# if tenant:
# context.update({
@ -1567,7 +1567,7 @@ def upload_avatar(request, pk):
# context_object_name = 'device'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return TwoFactorDevice.objects.none()
# return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -1599,7 +1599,7 @@ def upload_avatar(request, pk):
#
# # Log device creation
# AuditLogger.log_event(
# tenant=getattr(self.request, 'tenant', None),
# tenant=self.request.user.tenant,
# event_type='CREATE',
# event_category='SECURITY',
# action='Create Two-Factor Device',
@ -1623,7 +1623,7 @@ def upload_avatar(request, pk):
# permission_required = 'accounts.change_twofactordevice'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return TwoFactorDevice.objects.none()
# return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -1641,7 +1641,7 @@ def upload_avatar(request, pk):
#
# # Log device update
# AuditLogger.log_event(
# tenant=getattr(self.request, 'tenant', None),
# tenant=self.request.user.tenant,
# event_type='UPDATE',
# event_category='SECURITY',
# action='Update Two-Factor Device',
@ -1665,7 +1665,7 @@ def upload_avatar(request, pk):
# success_url = reverse_lazy('accounts:two_factor_device_list')
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return TwoFactorDevice.objects.none()
# return TwoFactorDevice.objects.filter(user__tenant=tenant)
@ -1704,7 +1704,7 @@ def upload_avatar(request, pk):
# paginate_by = 20
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return SocialAccount.objects.none()
#
@ -1729,7 +1729,7 @@ def upload_avatar(request, pk):
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
#
# if tenant:
# context.update({
@ -1751,7 +1751,7 @@ def upload_avatar(request, pk):
# context_object_name = 'social_account'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return SocialAccount.objects.none()
# return SocialAccount.objects.filter(user__tenant=tenant)
@ -1777,7 +1777,7 @@ def upload_avatar(request, pk):
#
# # Log social account creation
# AuditLogger.log_event(
# tenant=getattr(self.request, 'tenant', None),
# tenant=self.request.user.tenant,
# event_type='CREATE',
# event_category='INTEGRATION',
# action='Create Social Account',
@ -1801,7 +1801,7 @@ def upload_avatar(request, pk):
# permission_required = 'accounts.change_socialaccount'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return SocialAccount.objects.none()
# return SocialAccount.objects.filter(user__tenant=tenant)
@ -1819,7 +1819,7 @@ def upload_avatar(request, pk):
#
# # Log social account update
# AuditLogger.log_event(
# tenant=getattr(self.request, 'tenant', None),
# tenant=self.request.user.tenant,
# event_type='UPDATE',
# event_category='INTEGRATION',
# action='Update Social Account',
@ -1843,7 +1843,7 @@ def upload_avatar(request, pk):
# success_url = reverse_lazy('accounts:social_account_list')
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return SocialAccount.objects.none()
# return SocialAccount.objects.filter(user__tenant=tenant)
@ -1882,7 +1882,7 @@ def upload_avatar(request, pk):
# paginate_by = 50
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return UserSession.objects.none()
#
@ -1915,7 +1915,7 @@ def upload_avatar(request, pk):
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
#
# if tenant:
# context.update({
@ -1937,7 +1937,7 @@ def upload_avatar(request, pk):
# context_object_name = 'session'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return UserSession.objects.none()
# return UserSession.objects.filter(user__tenant=tenant)
@ -1957,7 +1957,7 @@ def upload_avatar(request, pk):
# paginate_by = 50
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return PasswordHistory.objects.none()
#
@ -1980,7 +1980,7 @@ def upload_avatar(request, pk):
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
#
# if tenant:
# context.update({
@ -1999,7 +1999,7 @@ def upload_avatar(request, pk):
# context_object_name = 'password_history'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return PasswordHistory.objects.none()
# return PasswordHistory.objects.filter(user__tenant=tenant)
@ -2055,7 +2055,7 @@ def upload_avatar(request, pk):
# paginate_by = 50
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return UserSession.objects.none()
#

View File

@ -15,13 +15,10 @@ from django.contrib.auth.hashers import make_password
from django.utils import timezone as django_timezone
from django.db import transaction
from accounts.models import User
from accounts.models import User, TwoFactorDevice, SocialAccount, UserSession, PasswordHistory
from core.models import Tenant
from hr.models import Employee
from hr.models import Department # make sure hr.Department exists
from hr.models import Employee, Department
# If these exist in your project, keep them. If not, comment them out.
from accounts.models import TwoFactorDevice, SocialAccount, UserSession, PasswordHistory # noqa
# -------------------------------
# Saudi-specific data constants
@ -162,6 +159,8 @@ def create_saudi_users(tenants, users_per_tenant=50):
for _ in range(count):
is_male = random.choice([True, False])
first_name = random.choice(SAUDI_FIRST_NAMES_MALE if is_male else SAUDI_FIRST_NAMES_FEMALE)
father_name = random.choice(SAUDI_FIRST_NAMES_MALE)
grandfather_name = random.choice(SAUDI_FIRST_NAMES_MALE)
last_name = random.choice(SAUDI_FAMILY_NAMES)
# base username like "mohammed.alrashid"
@ -200,8 +199,9 @@ def create_saudi_users(tenants, users_per_tenant=50):
emp: Employee = user.employee_profile # created by signal
emp.tenant = tenant # ensure alignment
emp.first_name = first_name
emp.father_name = father_name
emp.grandfather_name = grandfather_name
emp.last_name = last_name
emp.preferred_name = first_name if random.choice([True, False]) else None
# Contact (E.164 KSA)
mobile = generate_saudi_mobile_e164()

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -17,23 +17,23 @@ class Dashboard(models.Model):
"""
Dashboard model for organizing widgets and analytics views.
"""
DASHBOARD_TYPES = [
('EXECUTIVE', 'Executive Dashboard'),
('CLINICAL', 'Clinical Dashboard'),
('OPERATIONAL', 'Operational Dashboard'),
('FINANCIAL', 'Financial Dashboard'),
('QUALITY', 'Quality Dashboard'),
('PATIENT', 'Patient Dashboard'),
('PROVIDER', 'Provider Dashboard'),
('DEPARTMENT', 'Department Dashboard'),
('CUSTOM', 'Custom Dashboard'),
]
class DashboardType(models.TextChoices):
EXECUTIVE = 'EXECUTIVE', 'Executive Dashboard'
CLINICAL = 'CLINICAL', 'Clinical Dashboard'
OPERATIONAL = 'OPERATIONAL', 'Operational Dashboard'
FINANCIAL = 'FINANCIAL', 'Financial Dashboard'
QUALITY = 'QUALITY', 'Quality Dashboard'
PATIENT = 'PATIENT', 'Patient Dashboard'
PROVIDER = 'PROVIDER', 'Provider Dashboard'
DEPARTMENT = 'DEPARTMENT', 'Department Dashboard'
CUSTOM = 'CUSTOM', 'Custom Dashboard'
dashboard_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='dashboards')
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
dashboard_type = models.CharField(max_length=20, choices=DASHBOARD_TYPES)
dashboard_type = models.CharField(max_length=20, choices=DashboardType.choices)
# Layout and configuration
layout_config = models.JSONField(default=dict, help_text="Dashboard layout configuration")
@ -70,30 +70,29 @@ class DashboardWidget(models.Model):
"""
Dashboard widget model for individual analytics components.
"""
WIDGET_TYPES = [
('CHART', 'Chart Widget'),
('TABLE', 'Table Widget'),
('METRIC', 'Metric Widget'),
('GAUGE', 'Gauge Widget'),
('MAP', 'Map Widget'),
('TEXT', 'Text Widget'),
('IMAGE', 'Image Widget'),
('IFRAME', 'IFrame Widget'),
('CUSTOM', 'Custom Widget'),
]
CHART_TYPES = [
('LINE', 'Line Chart'),
('BAR', 'Bar Chart'),
('PIE', 'Pie Chart'),
('DOUGHNUT', 'Doughnut Chart'),
('AREA', 'Area Chart'),
('SCATTER', 'Scatter Plot'),
('HISTOGRAM', 'Histogram'),
('HEATMAP', 'Heat Map'),
('TREEMAP', 'Tree Map'),
('FUNNEL', 'Funnel Chart'),
]
class WidgetType(models.TextChoices):
CHART = 'CHART', 'Chart Widget'
TABLE = 'TABLE', 'Table Widget'
METRIC = 'METRIC', 'Metric Widget'
GAUGE = 'GAUGE', 'Gauge Widget'
MAP = 'MAP', 'Map Widget'
TEXT = 'TEXT', 'Text Widget'
IMAGE = 'IMAGE', 'Image Widget'
IFRAME = 'IFRAME', 'IFrame Widget'
CUSTOM = 'CUSTOM', 'Custom Widget'
class ChartType(models.TextChoices):
LINE = 'LINE', 'Line Chart'
BAR = 'BAR', 'Bar Chart'
PIE = 'PIE', 'Pie Chart'
DOUGHNUT = 'DOUGHNUT', 'Doughnut Chart'
AREA = 'AREA', 'Area Chart'
SCATTER = 'SCATTER', 'Scatter Plot'
HISTOGRAM = 'HISTOGRAM', 'Histogram'
HEATMAP = 'HEATMAP', 'Heat Map'
TREEMAP = 'TREEMAP', 'Tree Map'
FUNNEL = 'FUNNEL', 'Funnel Chart'
widget_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
dashboard = models.ForeignKey(Dashboard, on_delete=models.CASCADE, related_name='widgets')
@ -101,8 +100,8 @@ class DashboardWidget(models.Model):
# Widget configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
widget_type = models.CharField(max_length=20, choices=WIDGET_TYPES)
chart_type = models.CharField(max_length=20, choices=CHART_TYPES, blank=True)
widget_type = models.CharField(max_length=20, choices=WidgetType.choices)
chart_type = models.CharField(max_length=20, choices=ChartType.choices, blank=True)
# Data source
data_source = models.ForeignKey('DataSource', on_delete=models.CASCADE, related_name='widgets')
@ -145,34 +144,33 @@ class DataSource(models.Model):
"""
Data source model for analytics data connections.
"""
SOURCE_TYPES = [
('DATABASE', 'Database Query'),
('API', 'API Endpoint'),
('FILE', 'File Upload'),
('STREAM', 'Real-time Stream'),
('WEBHOOK', 'Webhook'),
('CUSTOM', 'Custom Source'),
]
CONNECTION_TYPES = [
('POSTGRESQL', 'PostgreSQL'),
('MYSQL', 'MySQL'),
('SQLITE', 'SQLite'),
('MONGODB', 'MongoDB'),
('REDIS', 'Redis'),
('REST_API', 'REST API'),
('GRAPHQL', 'GraphQL'),
('WEBSOCKET', 'WebSocket'),
('CSV', 'CSV File'),
('JSON', 'JSON File'),
('XML', 'XML File'),
]
TEST_STATUS_CHOICES = [
('PENDING', 'Pending'),
('RUNNING', 'Running'),
('SUCCESS', 'Success'),
('FAILURE', 'Failure'),
]
class SourceType(models.TextChoices):
DATABASE = 'DATABASE', 'Database Query'
API = 'API', 'API Endpoint'
FILE = 'FILE', 'File Upload'
STREAM = 'STREAM', 'Real-time Stream'
WEBHOOK = 'WEBHOOK', 'Webhook'
CUSTOM = 'CUSTOM', 'Custom Source'
class ConnectionType(models.TextChoices):
POSTGRESQL = 'POSTGRESQL', 'PostgreSQL'
MYSQL = 'MYSQL', 'MySQL'
SQLITE = 'SQLITE', 'SQLite'
MONGODB = 'MONGODB', 'MongoDB'
REDIS = 'REDIS', 'Redis'
REST_API = 'REST_API', 'REST API'
GRAPHQL = 'GRAPHQL', 'GraphQL'
WEBSOCKET = 'WEBSOCKET', 'WebSocket'
CSV = 'CSV', 'CSV File'
JSON = 'JSON', 'JSON File'
XML = 'XML', 'XML File'
class TestStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
RUNNING = 'RUNNING', 'Running'
SUCCESS = 'SUCCESS', 'Success'
FAILURE = 'FAILURE', 'Failure'
source_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='data_sources')
@ -180,8 +178,8 @@ class DataSource(models.Model):
# Source configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
source_type = models.CharField(max_length=20, choices=SOURCE_TYPES)
connection_type = models.CharField(max_length=20, choices=CONNECTION_TYPES)
source_type = models.CharField(max_length=20, choices=SourceType.choices)
connection_type = models.CharField(max_length=20, choices=ConnectionType.choices)
# Connection details
connection_config = models.JSONField(default=dict, help_text="Connection configuration")
@ -202,7 +200,7 @@ class DataSource(models.Model):
# Status
is_active = models.BooleanField(default=True)
last_test_status = models.CharField(max_length=20, choices=TEST_STATUS_CHOICES, default='PENDING')
last_test_status = models.CharField(max_length=20, choices=TestStatus.choices, default=TestStatus.PENDING)
last_test_start_at = models.DateTimeField(null=True, blank=True)
last_test_end_at = models.DateTimeField(null=True, blank=True)
last_test_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
@ -230,34 +228,32 @@ class Report(models.Model):
"""
Report model for scheduled and ad-hoc reporting.
"""
REPORT_TYPES = [
('OPERATIONAL', 'Operational Report'),
('FINANCIAL', 'Financial Report'),
('CLINICAL', 'Clinical Report'),
('QUALITY', 'Quality Report'),
('COMPLIANCE', 'Compliance Report'),
('PERFORMANCE', 'Performance Report'),
('CUSTOM', 'Custom Report'),
]
OUTPUT_FORMATS = [
('PDF', 'PDF Document'),
('EXCEL', 'Excel Spreadsheet'),
('CSV', 'CSV File'),
('JSON', 'JSON Data'),
('HTML', 'HTML Page'),
('EMAIL', 'Email Report'),
]
SCHEDULE_TYPES = [
('MANUAL', 'Manual Execution'),
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
('YEARLY', 'Yearly'),
('CUSTOM', 'Custom Schedule'),
]
class ReportType(models.TextChoices):
OPERATIONAL = 'OPERATIONAL', 'Operational Report'
FINANCIAL = 'FINANCIAL', 'Financial Report'
CLINICAL = 'CLINICAL', 'Clinical Report'
QUALITY = 'QUALITY', 'Quality Report'
COMPLIANCE = 'COMPLIANCE', 'Compliance Report'
PERFORMANCE = 'PERFORMANCE', 'Performance Report'
CUSTOM = 'CUSTOM', 'Custom Report'
class OutputFormat(models.TextChoices):
PDF = 'PDF', 'PDF Document'
EXCEL = 'EXCEL', 'Excel Spreadsheet'
CSV = 'CSV', 'CSV File'
JSON = 'JSON', 'JSON Data'
HTML = 'HTML', 'HTML Page'
EMAIL = 'EMAIL', 'Email Report'
class ScheduleType(models.TextChoices):
MANUAL = 'MANUAL', 'Manual Execution'
DAILY = 'DAILY', 'Daily'
WEEKLY = 'WEEKLY', 'Weekly'
MONTHLY = 'MONTHLY', 'Monthly'
QUARTERLY = 'QUARTERLY', 'Quarterly'
YEARLY = 'YEARLY', 'Yearly'
CUSTOM = 'CUSTOM', 'Custom Schedule'
report_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='reports')
@ -265,18 +261,18 @@ class Report(models.Model):
# Report configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
report_type = models.CharField(max_length=20, choices=REPORT_TYPES)
report_type = models.CharField(max_length=20, choices=ReportType.choices)
# Data source
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name='reports')
query_config = models.JSONField(default=dict, help_text="Query configuration for report")
# Output configuration
output_format = models.CharField(max_length=20, choices=OUTPUT_FORMATS)
output_format = models.CharField(max_length=20, choices=OutputFormat.choices)
template_config = models.JSONField(default=dict, help_text="Report template configuration")
# Scheduling
schedule_type = models.CharField(max_length=20, choices=SCHEDULE_TYPES, default='MANUAL')
schedule_type = models.CharField(max_length=20, choices=ScheduleType.choices, default=ScheduleType.MANUAL)
schedule_config = models.JSONField(default=dict, help_text="Schedule configuration")
next_execution = models.DateTimeField(null=True, blank=True)
@ -310,23 +306,24 @@ class ReportExecution(models.Model):
"""
Report execution model for tracking report runs.
"""
EXECUTION_STATUS = [
('PENDING', 'Pending'),
('RUNNING', 'Running'),
('COMPLETED', 'Completed'),
('FAILED', 'Failed'),
('CANCELLED', 'Cancelled'),
]
class ExecutionStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
RUNNING = 'RUNNING', 'Running'
COMPLETED = 'COMPLETED', 'Completed'
FAILED = 'FAILED', 'Failed'
CANCELLED = 'CANCELLED', 'Cancelled'
class ExecutionType(models.TextChoices):
MANUAL = 'MANUAL', 'Manual'
SCHEDULED = 'SCHEDULED', 'Scheduled'
API = 'API', 'API Triggered'
execution_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name='executions')
# Execution details
execution_type = models.CharField(max_length=20, choices=[
('MANUAL', 'Manual'),
('SCHEDULED', 'Scheduled'),
('API', 'API Triggered'),
], default='MANUAL')
execution_type = models.CharField(max_length=20, choices=ExecutionType.choices, default=ExecutionType.MANUAL)
# Timing
started_at = models.DateTimeField(auto_now_add=True)
@ -334,7 +331,7 @@ class ReportExecution(models.Model):
duration_seconds = models.PositiveIntegerField(null=True, blank=True)
# Status and results
status = models.CharField(max_length=20, choices=EXECUTION_STATUS, default='PENDING')
status = models.CharField(max_length=20, choices=ExecutionStatus.choices, default=ExecutionStatus.PENDING)
error_message = models.TextField(blank=True)
# Output
@ -370,26 +367,25 @@ class MetricDefinition(models.Model):
"""
Metric definition model for KPI and performance metrics.
"""
METRIC_TYPES = [
('COUNT', 'Count'),
('SUM', 'Sum'),
('AVERAGE', 'Average'),
('PERCENTAGE', 'Percentage'),
('RATIO', 'Ratio'),
('RATE', 'Rate'),
('DURATION', 'Duration'),
('CUSTOM', 'Custom Calculation'),
]
AGGREGATION_PERIODS = [
('REAL_TIME', 'Real-time'),
('HOURLY', 'Hourly'),
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
('YEARLY', 'Yearly'),
]
class MetricType(models.TextChoices):
COUNT = 'COUNT', 'Count'
SUM = 'SUM', 'Sum'
AVERAGE = 'AVERAGE', 'Average'
PERCENTAGE = 'PERCENTAGE', 'Percentage'
RATIO = 'RATIO', 'Ratio'
RATE = 'RATE', 'Rate'
DURATION = 'DURATION', 'Duration'
CUSTOM = 'CUSTOM', 'Custom Calculation'
class AggregationPeriod(models.TextChoices):
REAL_TIME = 'REAL_TIME', 'Real-time'
HOURLY = 'HOURLY', 'Hourly'
DAILY = 'DAILY', 'Daily'
WEEKLY = 'WEEKLY', 'Weekly'
MONTHLY = 'MONTHLY', 'Monthly'
QUARTERLY = 'QUARTERLY', 'Quarterly'
YEARLY = 'YEARLY', 'Yearly'
metric_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='metric_definitions')
@ -397,14 +393,14 @@ class MetricDefinition(models.Model):
# Metric configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
metric_type = models.CharField(max_length=20, choices=METRIC_TYPES)
metric_type = models.CharField(max_length=20, choices=MetricType.choices)
# Data source
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name='metrics')
calculation_config = models.JSONField(default=dict, help_text="Metric calculation configuration")
# Aggregation
aggregation_period = models.CharField(max_length=20, choices=AGGREGATION_PERIODS)
aggregation_period = models.CharField(max_length=20, choices=AggregationPeriod.choices)
aggregation_config = models.JSONField(default=dict, help_text="Aggregation configuration")
# Thresholds and targets

View File

@ -32,7 +32,7 @@ SAUDI_DEPARTMENTS = [
def generate_dashboards(tenants, users):
dashboards = []
types = [dt[0] for dt in Dashboard.DASHBOARD_TYPES]
types = [dt[0] for dt in Dashboard.DashboardType.choices]
for tenant in tenants:
for _ in range(random.randint(2, 5)):
creator = random.choice(users)

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion
@ -465,6 +465,7 @@ class Migration(migrations.Migration):
choices=[
("SCHEDULED", "Scheduled"),
("READY", "Ready to Start"),
("WAITING", "Waiting"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -16,7 +16,7 @@ class PatientContextMixin:
patient_query_param = "patient"
def get_patient(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
# 1) URL kwarg
patient_id = self.kwargs.get(self.patient_kwarg_name)

View File

@ -894,6 +894,7 @@ class TelemedicineSession(models.Model):
class SessionStatus(models.TextChoices):
SCHEDULED = 'SCHEDULED', 'Scheduled'
READY = 'READY', 'Ready to Start'
WAITING = 'WAITING', 'Waiting'
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
COMPLETED = 'COMPLETED', 'Completed'
CANCELLED = 'CANCELLED', 'Cancelled'

View File

@ -7,16 +7,12 @@
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="d-flex align-items-center mb-3">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<div>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'appointments:dashboard' %}">Appointments</a></li>
<li class="breadcrumb-item"><a href="{% url 'appointments:appointment_list' %}">Appointments</a></li>
<li class="breadcrumb-item active">{% if object %}Edit{% else %}New{% endif %}</li>
</ol>
<h1 class="page-header mb-0">
{% if object %}Edit Appointment{% else %}Schedule New Appointment{% endif %}
<h1 class="h2 mb-0">
{% if object %}Edit<span class="fw-light">Appointment</span>{% else %}New<span class="fw-light">Appointment{% endif %}</span>
</h1>
<p class="text-muted">Schedule appointments, manage queues, and track your progress.</p>
</div>
<div class="ms-auto">
<a href="{% url 'appointments:appointment_list' %}" class="btn btn-secondary">
@ -26,14 +22,20 @@
</div>
<div class="row">
<div class="col-xl-8">
<div class="card">
<div class="card-header">
<h4 class="card-title">
<i class="fas fa-calendar-plus me-2"></i>
Appointment Details
</h4>
<div class="panel panel-inverse mb-4" data-sortable-id="index-1">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-calendar-plus me-2"></i>Appointment Details
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<form method="post" novalidate>
{% csrf_token %}
<div class="row">
@ -249,14 +251,19 @@
</div>
<div class="col-xl-4">
<!-- Doctor Schedule -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-user-md me-2"></i>
Doctor Schedule
</h5>
<div class="panel panel-inverse mb-4" data-sortable-id="index-2">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-user-md me-2"></i>Doctor Schedule
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<div id="doctor-schedule">
<div class="text-center text-muted">
<i class="fas fa-stethoscope fa-2x mb-2"></i>
@ -267,14 +274,19 @@
</div>
<!-- Available Slots -->
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-clock me-2"></i>
Available Slots
</h5>
<div class="panel panel-inverse mb-4" data-sortable-id="index-3">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-clock me-2"></i>Available Slots
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<div id="available-slots">
<div class="text-center text-muted">
<i class="fas fa-calendar-alt fa-2x mb-2"></i>
@ -285,14 +297,20 @@
</div>
<!-- Appointment Guidelines -->
<div class="card">
<div class="card-header">
<h5 class="card-title">
<i class="fas fa-info-circle me-2"></i>
Guidelines
</h5>
<div class="panel panel-inverse mb-4" data-sortable-id="index-4">
<div class="panel-heading">
<h4 class="panel-title">
<i class="fas fa-info-circle me-2"></i>Guidelines
</h4>
<div class="panel-heading-btn">
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
</div>
<div class="card-body">
</div>
<div class="panel-body">
<div class="accordion" id="helpAccordion">
<div class="accordion-item">
<h2 class="accordion-header">

View File

@ -109,9 +109,9 @@
</form>
</div>
</div>
<div class="table-responsive">
<div class="table-responsive border border-primary-subtle">
<table class="table table-hover mb-0">
<thead class="table-light">
<thead class="table-primary">
<tr>
<th>Date & Time</th>
<th>Patient</th>

View File

@ -50,16 +50,20 @@
<td class="fw-bold">Status:</td>
<td>
{% if session.status == 'SCHEDULED' %}
<span class="badge bg-warning">Scheduled</span>
<span class="badge bg-warning">
{% elif session.status == 'READY' %}
<span class="badge bg-info">
{% elif session.status == 'WAITING' %}
<span class="badge bg-info">Waiting</span>
<span class="badge bg-purple">
{% elif session.status == 'IN_PROGRESS' %}
<span class="badge bg-success">In Progress</span>
<span class="badge bg-success">
{% elif session.status == 'COMPLETED' %}
<span class="badge bg-success">Completed</span>
<span class="badge bg-success">
{% elif session.status == 'CANCELLED' %}
<span class="badge bg-danger">Cancelled</span>
<span class="badge bg-danger">
{% endif %}
{{ session.get_status_display }}
</span>
</td>
</tr>
<tr>
@ -90,7 +94,11 @@
{% endif %}
<tr>
<td class="fw-bold">Duration:</td>
{% if session.duration_minutes %}
<td>{{ session.duration_minutes }} minutes</td>
{% else %}
<td>Will be calculated after starting</td>
{% endif %}
</tr>
</table>
</div>
@ -290,11 +298,11 @@
<table class="table table-borderless">
<tr>
<td class="fw-bold">Scheduled Date:</td>
<td>{{ session.appointment.appointment_date|date:"M d, Y" }}</td>
<td>{{ session.appointment.preferred_date|date:"M d, Y" }}</td>
</tr>
<tr>
<td class="fw-bold">Scheduled Time:</td>
<td>{{ session.appointment.appointment_time|time:"H:i" }}</td>
<td>{{ session.appointment.preferred_time|time:"H:i" }}</td>
</tr>
<tr>
<td class="fw-bold">Duration:</td>

View File

@ -4,25 +4,35 @@
{% block title %}Patient Waiting List Management{% endblock %}
{% block css %}
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
<link href="{% static 'plugins/datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' %}" rel="stylesheet" />
<style>
.priority-emergency { border-left: 4px solid #dc3545; }
.priority-stat { border-left: 4px solid #fd7e14; }
.priority-emergency { border-left: 4px solid #ff5b57; }
.priority-stat { border-left: 4px solid #f59c1a; }
.priority-urgent { border-left: 4px solid #ffc107; }
.priority-routine { border-left: 4px solid #28a745; }
.priority-routine { border-left: 4px solid #00acac; }
.overdue-contact { background-color: #fff3cd; }
.patient-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
align-items-center;
justify-content: center;
font-weight: bold;
color: white;
}
.table-responsive { overflow-x: auto; }
.table th {
background-color: #f8f9fa;
border-top: none;
font-weight: 600;
vertical-align: middle;
}
.table td { vertical-align: middle; }
.badge {
font-size: 0.75rem;
padding: 0.375rem 0.5rem;
}
.btn-group .btn { margin: 0 1px; }
</style>
{% endblock %}
@ -201,22 +211,22 @@
<!-- BEGIN table -->
<div class="table-responsive">
<table id="waitingListTable" class="table table-striped table-bordered align-middle">
<thead class="table-dark">
<table class="table table-striped table-hover">
<thead>
<tr>
<th width="3%">
<th class="text-center" style="width: 3%;">
<input type="checkbox" class="form-check-input" id="header-checkbox">
</th>
<th width="5%">Pos.</th>
<th width="20%">Patient</th>
<th width="12%">Department</th>
<th width="12%">Specialty</th>
<th width="8%">Priority</th>
<th width="8%">Urgency</th>
<th width="10%">Status</th>
<th width="8%">Wait Time</th>
<th width="8%">Last Contact</th>
<th width="6%">Actions</th>
<th class="text-center" style="width: 5%;">Pos.</th>
<th style="width: 20%;">Patient</th>
<th style="width: 12%;">Department</th>
<th style="width: 12%;">Specialty</th>
<th class="text-center" style="width: 8%;">Priority</th>
<th class="text-center" style="width: 8%;">Urgency</th>
<th class="text-center" style="width: 10%;">Status</th>
<th class="text-center" style="width: 8%;">Wait Time</th>
<th class="text-center" style="width: 8%;">Last Contact</th>
<th class="text-center" style="width: 6%;">Actions</th>
</tr>
</thead>
<tbody>
@ -398,56 +408,26 @@
{% endblock %}
{% block js %}
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons/js/dataTables.buttons.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons-bs5/js/buttons.bootstrap5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons/js/buttons.html5.min.js' %}"></script>
<script src="{% static 'plugins/datatables.net-buttons/js/buttons.print.min.js' %}"></script>
<script>
$(document).ready(function() {
// Initialize DataTable
$('#waitingListTable').DataTable({
responsive: true,
dom: 'Bfrtip',
buttons: [
'copy', 'csv', 'excel', 'pdf', 'print'
],
order: [[5, 'desc'], [6, 'desc'], [8, 'desc']], // Priority, Urgency, Wait time
pageLength: 25,
language: {
search: "Search waiting list:",
lengthMenu: "Show _MENU_ entries per page",
info: "Showing _START_ to _END_ of _TOTAL_ entries",
infoEmpty: "No entries available",
infoFiltered: "(filtered from _MAX_ total entries)"
},
columnDefs: [
{ orderable: false, targets: [0, 10] } // Checkbox and actions columns
]
});
// Select all functionality
$('#select-all, #header-checkbox').change(function() {
const isChecked = $(this).prop('checked');
$('.entry-checkbox').prop('checked', isChecked);
toggleBulkActionButton();
});
// Individual checkbox change
$('.entry-checkbox').change(function() {
toggleBulkActionButton();
// Update select all checkbox
const totalCheckboxes = $('.entry-checkbox').length;
const checkedCheckboxes = $('.entry-checkbox:checked').length;
$('#select-all, #header-checkbox').prop('checked', totalCheckboxes === checkedCheckboxes);
});
// Auto-refresh stats every 30 seconds
setInterval(refreshStats, 30000);
});
@ -474,7 +454,7 @@ function quickContact(entryId) {
function submitQuickContact() {
const formData = new FormData($('#quick-contact-form')[0]);
const entryId = $('#contact-entry-id').val();
$.ajax({
url: `/appointments/waiting-list/${entryId}/contact/`,
method: 'POST',
@ -495,19 +475,19 @@ function submitQuickContact() {
$('#bulk-action-form').submit(function(e) {
const action = $('select[name="action"]').val();
const selectedCount = $('.entry-checkbox:checked').length;
if (!action) {
e.preventDefault();
alert('Please select an action.');
return;
}
if (selectedCount === 0) {
e.preventDefault();
alert('Please select at least one entry.');
return;
}
const confirmMessage = `Are you sure you want to ${action} ${selectedCount} selected entries?`;
if (!confirm(confirmMessage)) {
e.preventDefault();
@ -515,4 +495,3 @@ $('#bulk-action-form').submit(function(e) {
});
</script>
{% endblock %}

View File

@ -91,7 +91,7 @@ class AppointmentRequestListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
@ -164,7 +164,7 @@ class AppointmentRequestDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'appointment'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
return AppointmentRequest.objects.filter(tenant=tenant)
@ -197,7 +197,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, CreateView):
def dispatch(self, request, *args, **kwargs):
# Ensure tenant exists (if you follow multi-tenant pattern)
self.tenant = getattr(request, "tenant", None)
self.tenant = self.request.user.tenant
if not self.tenant:
return JsonResponse({"error": "No tenant found"}, status=400)
@ -255,7 +255,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_appointmentrequest'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentRequest.objects.none()
return AppointmentRequest.objects.filter(tenant=tenant)
@ -335,7 +335,7 @@ class SlotAvailabilityListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
@ -380,7 +380,7 @@ class SlotAvailabilityDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'slot'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
@ -420,7 +420,7 @@ class SlotAvailabilityCreateView(LoginRequiredMixin, CreateView):
# Log slot creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Slot Availability',
@ -444,7 +444,7 @@ class SlotAvailabilityUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_slotavailability'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
@ -462,7 +462,7 @@ class SlotAvailabilityUpdateView(LoginRequiredMixin, UpdateView):
# Log slot update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Slot Availability',
@ -486,7 +486,7 @@ class SlotAvailabilityDeleteView(LoginRequiredMixin, DeleteView):
success_url = reverse_lazy('appointments:slot_availability_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return SlotAvailability.objects.none()
return SlotAvailability.objects.filter(provider__tenant=tenant)
@ -536,7 +536,7 @@ class WaitingQueueListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
@ -586,7 +586,7 @@ class WaitingQueueDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'queue'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
@ -633,7 +633,7 @@ class WaitingQueueCreateView(LoginRequiredMixin, CreateView):
def form_valid(self, form):
# Set tenant
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log queue creation
@ -662,7 +662,7 @@ class WaitingQueueUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_waitingqueue'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return WaitingQueue.objects.none()
return WaitingQueue.objects.filter(tenant=tenant)
@ -751,7 +751,7 @@ class QueueEntryListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
@ -798,7 +798,7 @@ class QueueEntryDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'entry'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
return QueueEntry.objects.filter(queue__tenant=tenant)
@ -830,7 +830,7 @@ class QueueEntryCreateView(LoginRequiredMixin, CreateView):
# Log queue entry creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Queue Entry',
@ -854,7 +854,7 @@ class QueueEntryUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_queueentry'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return QueueEntry.objects.none()
return QueueEntry.objects.filter(queue__tenant=tenant)
@ -872,7 +872,7 @@ class QueueEntryUpdateView(LoginRequiredMixin, UpdateView):
# Log queue entry update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Queue Entry',
@ -896,7 +896,7 @@ class TelemedicineSessionListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
@ -923,7 +923,7 @@ class TelemedicineSessionListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -943,7 +943,7 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'session'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
@ -969,7 +969,7 @@ class TelemedicineSessionCreateView(LoginRequiredMixin, CreateView):
# Log session creation
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='CREATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Create Telemedicine Session',
@ -993,7 +993,7 @@ class TelemedicineSessionUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_telemedicinesession'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return TelemedicineSession.objects.none()
return TelemedicineSession.objects.filter(appointment__tenant=tenant)
@ -1011,7 +1011,7 @@ class TelemedicineSessionUpdateView(LoginRequiredMixin, UpdateView):
# Log session update
AuditLogger.log_event(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Update Telemedicine Session',
@ -1035,7 +1035,7 @@ class AppointmentTemplateListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
@ -1072,7 +1072,7 @@ class AppointmentTemplateListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -1098,7 +1098,7 @@ class AppointmentTemplateDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'template'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
return AppointmentTemplate.objects.filter(tenant=tenant)
@ -1121,7 +1121,7 @@ class AppointmentTemplateCreateView(LoginRequiredMixin, CreateView):
def form_valid(self, form):
# Set tenant
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log template creation
@ -1150,7 +1150,7 @@ class AppointmentTemplateUpdateView(LoginRequiredMixin, UpdateView):
permission_required = 'appointments.change_appointmenttemplate'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
return AppointmentTemplate.objects.filter(tenant=tenant)
@ -1192,7 +1192,7 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, DeleteView):
success_url = reverse_lazy('appointments:appointment_template_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AppointmentTemplate.objects.none()
return AppointmentTemplate.objects.filter(tenant=tenant)
@ -1333,11 +1333,11 @@ class WaitingListCreateView(LoginRequiredMixin, CreateView):
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['tenant'] = getattr(self.request.user, 'tenant', None)
kwargs['tenant'] = self.request.user.tenant
return kwargs
def form_valid(self, form):
form.instance.tenant = getattr(self.request.user, 'tenant', None)
form.instance.tenant = self.request.user.tenant
form.instance.created_by = self.request.user
response = super().form_valid(form)

View File

@ -5,13 +5,15 @@ import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
# Django setup will be handled by manage.py when running this script
# Import Django-related modules after Django is set up
import random
from datetime import datetime, date, time, timedelta
from django.utils import timezone as django_timezone
from django.contrib.auth import get_user_model
from appointments.models import (
AppointmentRequest, SlotAvailability, WaitingQueue, QueueEntry,
TelemedicineSession, AppointmentTemplate
TelemedicineSession, AppointmentTemplate, WaitingList, WaitingListContactLog
)
from patients.models import PatientProfile
from core.models import Tenant
@ -820,6 +822,403 @@ def create_telemedicine_sessions(appointments):
return sessions
def create_waiting_list_entries(tenants, num_entries_per_tenant=100):
"""Create waiting list entries with realistic clinical scenarios"""
waiting_list_entries = []
# Get patients and providers
patients = list(PatientProfile.objects.filter(tenant__in=tenants))
providers = get_providers_for_tenant(tenants)
departments = list(Department.objects.filter(tenant__in=tenants))
if not patients:
print("No patients found. Skipping waiting list creation.")
return waiting_list_entries
if not departments:
print("No departments found. Skipping waiting list creation.")
return waiting_list_entries
# Clinical scenarios for waiting list entries
clinical_scenarios = [
{
'specialty': 'CARDIOLOGY',
'appointment_type': 'CONSULTATION',
'clinical_indication': 'Chest pain evaluation and cardiac assessment',
'diagnosis_codes': ['R07.9', 'I25.10'],
'priority': 'URGENT',
'urgency_score': random.randint(6, 8),
'referral_urgency': 'URGENT'
},
{
'specialty': 'ORTHOPEDICS',
'appointment_type': 'FOLLOW_UP',
'clinical_indication': 'Post-operative knee replacement follow-up',
'diagnosis_codes': ['Z47.1', 'M17.9'],
'priority': 'ROUTINE',
'urgency_score': random.randint(2, 4),
'referral_urgency': 'ROUTINE'
},
{
'specialty': 'PEDIATRICS',
'appointment_type': 'SCREENING',
'clinical_indication': 'Well-child visit and developmental assessment',
'diagnosis_codes': ['Z00.129'],
'priority': 'ROUTINE',
'urgency_score': random.randint(1, 3),
'referral_urgency': 'ROUTINE'
},
{
'specialty': 'DERMATOLOGY',
'appointment_type': 'PROCEDURE',
'clinical_indication': 'Suspicious skin lesion requiring biopsy',
'diagnosis_codes': ['D48.5', 'L98.9'],
'priority': 'URGENT',
'urgency_score': random.randint(5, 7),
'referral_urgency': 'URGENT'
},
{
'specialty': 'GYNECOLOGY',
'appointment_type': 'CONSULTATION',
'clinical_indication': 'Abnormal uterine bleeding evaluation',
'diagnosis_codes': ['N93.9', 'N85.00'],
'priority': 'URGENT',
'urgency_score': random.randint(6, 8),
'referral_urgency': 'URGENT'
},
{
'specialty': 'NEUROLOGY',
'appointment_type': 'DIAGNOSTIC',
'clinical_indication': 'Headache evaluation and neurological assessment',
'diagnosis_codes': ['R51', 'G44.209'],
'priority': 'STAT',
'urgency_score': random.randint(7, 9),
'referral_urgency': 'STAT'
},
{
'specialty': 'ENDOCRINOLOGY',
'appointment_type': 'FOLLOW_UP',
'clinical_indication': 'Diabetes management and glycemic control',
'diagnosis_codes': ['E11.9', 'Z79.4'],
'priority': 'ROUTINE',
'urgency_score': random.randint(2, 5),
'referral_urgency': 'ROUTINE'
},
{
'specialty': 'SURGERY',
'appointment_type': 'CONSULTATION',
'clinical_indication': 'Gallbladder disease requiring surgical evaluation',
'diagnosis_codes': ['K80.20', 'R10.11'],
'priority': 'URGENT',
'urgency_score': random.randint(5, 8),
'referral_urgency': 'URGENT'
},
{
'specialty': 'PSYCHIATRY',
'appointment_type': 'THERAPY',
'clinical_indication': 'Depression and anxiety management',
'diagnosis_codes': ['F32.9', 'F41.9'],
'priority': 'ROUTINE',
'urgency_score': random.randint(3, 6),
'referral_urgency': 'ROUTINE'
},
{
'specialty': 'UROLOGY',
'appointment_type': 'DIAGNOSTIC',
'clinical_indication': 'Elevated PSA and prostate evaluation',
'diagnosis_codes': ['R97.20', 'N40.0'],
'priority': 'URGENT',
'urgency_score': random.randint(5, 7),
'referral_urgency': 'URGENT'
}
]
for tenant in tenants:
tenant_patients = [p for p in patients if p.tenant == tenant]
tenant_providers = [p for p in providers if p.tenant == tenant]
tenant_departments = [d for d in departments if d.tenant == tenant]
if not tenant_patients or not tenant_departments:
continue
for _ in range(num_entries_per_tenant):
patient = random.choice(tenant_patients)
scenario = random.choice(clinical_scenarios)
# Select appropriate department
suitable_departments = [
d for d in tenant_departments
if hasattr(d, 'department_type') and d.department_type == 'CLINICAL'
]
department = random.choice(suitable_departments) if suitable_departments else random.choice(tenant_departments)
# Provider assignment (optional, 70% have preferred provider)
provider = None
if random.random() < 0.7 and tenant_providers:
provider = random.choice(tenant_providers)
# Status distribution (weighted towards active entries)
status_weights = {
'ACTIVE': 60,
'CONTACTED': 15,
'OFFERED': 10,
'SCHEDULED': 8,
'CANCELLED': 4,
'EXPIRED': 2,
'TRANSFERRED': 1
}
status = random.choices(
list(status_weights.keys()),
weights=list(status_weights.values())
)[0]
# Contact information
contact_method = random.choice(['PHONE', 'EMAIL', 'SMS'])
contact_phone = f"+966{random.randint(500000000, 599999999)}" if contact_method == 'PHONE' else None
contact_email = f"{patient.email}" if contact_method == 'EMAIL' else None
# Timing and scheduling preferences
created_at = django_timezone.now() - timedelta(days=random.randint(1, 90))
preferred_date = created_at.date() + timedelta(days=random.randint(7, 60)) if random.random() < 0.8 else None
preferred_time = time(random.randint(8, 16), random.choice([0, 30])) if preferred_date else None
# Contact history
contact_attempts = 0
last_contacted = None
appointments_offered = 0
appointments_declined = 0
last_offer_date = None
if status in ['CONTACTED', 'OFFERED', 'SCHEDULED']:
contact_attempts = random.randint(1, 3)
last_contacted = created_at + timedelta(days=random.randint(1, 30))
if status in ['OFFERED', 'SCHEDULED']:
appointments_offered = random.randint(1, 2)
last_offer_date = last_contacted + timedelta(days=random.randint(1, 7))
if random.random() < 0.3: # 30% decline offers
appointments_declined = random.randint(1, appointments_offered)
# Authorization requirements (for procedures/surgery)
authorization_required = scenario['appointment_type'] in ['PROCEDURE', 'SURGERY']
authorization_status = 'NOT_REQUIRED'
authorization_number = None
if authorization_required:
authorization_status = random.choice(['PENDING', 'APPROVED', 'DENIED'])
if authorization_status == 'APPROVED':
authorization_number = f"AUTH-{random.randint(100000, 999999)}"
# Special requirements
requires_interpreter = random.random() < 0.05 # 5% need interpreter
interpreter_language = random.choice(['Arabic', 'English', 'Urdu', 'Hindi']) if requires_interpreter else None
accessibility_requirements = None
if random.random() < 0.1: # 10% have accessibility needs
accessibility_requirements = random.choice([
'Wheelchair accessible room required',
'Sign language interpreter needed',
'Hearing assistance devices needed',
'Visual assistance required'
])
transportation_needed = random.random() < 0.15 # 15% need transportation
# Referral information
referring_provider = f"Dr. {random.choice(['Ahmed', 'Fatima', 'Mohammed', 'Sara'])} Al-{random.choice(['Rashid', 'Ghamdi', 'Otaibi', 'Harbi'])}"
referral_date = created_at - timedelta(days=random.randint(1, 14))
# Notes
notes = random.choice([
None,
f"Patient prefers {scenario['specialty'].replace('_', ' ').title().lower()} specialist",
"Patient has multiple comorbidities",
"Requires extended appointment time",
"Patient has transportation limitations",
"Family member will accompany patient"
])
try:
waiting_entry = WaitingList.objects.create(
tenant=tenant,
patient=patient,
provider=provider,
department=department,
appointment_type=scenario['appointment_type'],
specialty=scenario['specialty'],
priority=scenario['priority'],
urgency_score=scenario['urgency_score'],
clinical_indication=scenario['clinical_indication'],
diagnosis_codes=scenario['diagnosis_codes'],
preferred_date=preferred_date,
preferred_time=preferred_time,
flexible_scheduling=random.choice([True, False]),
earliest_acceptable_date=preferred_date - timedelta(days=random.randint(0, 7)) if preferred_date else None,
latest_acceptable_date=preferred_date + timedelta(days=random.randint(7, 21)) if preferred_date else None,
acceptable_days=[0, 1, 2, 3, 4] if random.random() < 0.8 else [], # Monday-Friday
acceptable_times=['morning', 'afternoon'] if random.random() < 0.7 else [],
contact_method=contact_method,
contact_phone=contact_phone,
contact_email=contact_email,
status=status,
position=None, # Will be calculated later
estimated_wait_time=scenario['urgency_score'] * 7, # Days based on urgency
last_contacted=last_contacted,
contact_attempts=contact_attempts,
max_contact_attempts=3,
appointments_offered=appointments_offered,
appointments_declined=appointments_declined,
last_offer_date=last_offer_date,
requires_interpreter=requires_interpreter,
interpreter_language=interpreter_language,
accessibility_requirements=accessibility_requirements,
transportation_needed=transportation_needed,
insurance_verified=random.choice([True, False]),
authorization_required=authorization_required,
authorization_status=authorization_status,
authorization_number=authorization_number,
referring_provider=referring_provider,
referral_date=referral_date,
referral_urgency=scenario['referral_urgency'],
created_at=created_at,
updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7)),
notes=notes
)
waiting_list_entries.append(waiting_entry)
except Exception as e:
print(f"Error creating waiting list entry for {patient.get_full_name()}: {e}")
continue
# Update positions for active entries
for entry in waiting_list_entries:
if entry.status == 'ACTIVE':
try:
entry.position = entry.calculate_position()
entry.estimated_wait_time = entry.estimate_wait_time()
entry.save(update_fields=['position', 'estimated_wait_time'])
except Exception as e:
print(f"Error updating position for waiting list entry {entry.waiting_list_id}: {e}")
print(f"Created {len(waiting_list_entries)} waiting list entries")
return waiting_list_entries
def create_waiting_list_contact_logs(waiting_list_entries):
"""Create contact logs for waiting list entries"""
contact_logs = []
# Only create logs for entries that have been contacted
contacted_entries = [entry for entry in waiting_list_entries if entry.contact_attempts > 0]
for entry in contacted_entries:
# Create logs for each contact attempt
for attempt_num in range(1, entry.contact_attempts + 1):
contact_date = entry.last_contacted - timedelta(days=random.randint(0, 7) * (entry.contact_attempts - attempt_num))
# Contact method based on entry preferences
contact_method = entry.contact_method
if contact_method == 'PHONE':
contact_method = 'PHONE'
elif contact_method == 'EMAIL':
contact_method = 'EMAIL'
else:
contact_method = 'SMS'
# Contact outcomes
possible_outcomes = ['SUCCESSFUL', 'NO_ANSWER', 'BUSY', 'VOICEMAIL', 'EMAIL_SENT', 'SMS_SENT']
if attempt_num == entry.contact_attempts:
# Last attempt more likely to be successful
outcome_weights = [40, 20, 10, 10, 10, 10]
else:
outcome_weights = [20, 30, 15, 15, 10, 10]
contact_outcome = random.choices(possible_outcomes, weights=outcome_weights)[0]
# Patient responses (only for successful contacts)
patient_response = None
appointment_offered = False
offered_date = None
offered_time = None
if contact_outcome == 'SUCCESSFUL':
response_options = ['ACCEPTED', 'DECLINED', 'REQUESTED_DIFFERENT', 'WILL_CALL_BACK', 'NO_LONGER_NEEDED']
response_weights = [30, 20, 25, 15, 10]
patient_response = random.choices(response_options, weights=response_weights)[0]
# Offer appointment if appropriate
if patient_response in ['ACCEPTED', 'REQUESTED_DIFFERENT'] and random.random() < 0.7:
appointment_offered = True
offered_date = contact_date.date() + timedelta(days=random.randint(7, 21))
offered_time = time(random.randint(8, 16), random.choice([0, 30]))
# Notes based on outcome
notes_templates = {
'SUCCESSFUL': [
f"Spoke with patient regarding {entry.appointment_type.lower()} appointment",
"Patient confirmed interest in scheduling",
"Discussed clinical urgency and wait times",
"Patient requested specific time preferences"
],
'NO_ANSWER': [
"No answer after multiple rings",
"Call went to voicemail",
"Patient did not pick up"
],
'BUSY': [
"Line was busy",
"Patient indicated they were busy",
"Call disconnected due to network issues"
],
'VOICEMAIL': [
"Left detailed voicemail about appointment availability",
"Voicemail left with callback instructions",
"Message left explaining wait list status"
],
'EMAIL_SENT': [
"Email sent with appointment options",
"Detailed email with scheduling information",
"Follow-up email sent as requested"
],
'SMS_SENT': [
"SMS sent with appointment reminder",
"Text message with scheduling options",
"SMS follow-up sent"
]
}
notes = random.choice(notes_templates.get(contact_outcome, ["Contact attempt made"]))
# Next contact date for unsuccessful attempts
next_contact_date = None
if contact_outcome != 'SUCCESSFUL' and attempt_num < entry.max_contact_attempts:
next_contact_date = contact_date.date() + timedelta(days=random.randint(3, 7))
try:
contact_log = WaitingListContactLog.objects.create(
waiting_list_entry=entry,
contact_date=contact_date,
contact_method=contact_method,
contact_outcome=contact_outcome,
appointment_offered=appointment_offered,
offered_date=offered_date,
offered_time=offered_time,
patient_response=patient_response,
notes=notes,
next_contact_date=next_contact_date,
contacted_by=random.choice([entry.provider, None]) if entry.provider else None
)
contact_logs.append(contact_log)
except Exception as e:
print(f"Error creating contact log for waiting list entry {entry.waiting_list_id}: {e}")
continue
print(f"Created {len(contact_logs)} waiting list contact logs")
return contact_logs
def create_queue_entries(queues, appointments):
"""Create queue entries for appointments"""
entries = []
@ -942,8 +1341,16 @@ def main():
print("\n5. Creating Telemedicine Sessions...")
sessions = create_telemedicine_sessions(appointments)
# Create waiting list entries
print("\n6. Creating Waiting List Entries...")
waiting_list_entries = create_waiting_list_entries(tenants, 100)
# Create waiting list contact logs
print("\n7. Creating Waiting List Contact Logs...")
contact_logs = create_waiting_list_contact_logs(waiting_list_entries)
# Create queue entries
print("\n6. Creating Queue Entries...")
print("\n8. Creating Queue Entries...")
entries = create_queue_entries(queues, appointments)
print(f"\n✅ Saudi Healthcare Appointments Data Generation Complete!")
@ -953,6 +1360,8 @@ def main():
print(f" - Availability Slots: {len(slots)}")
print(f" - Appointment Requests: {len(appointments)}")
print(f" - Telemedicine Sessions: {len(sessions)}")
print(f" - Waiting List Entries: {len(waiting_list_entries)}")
print(f" - Waiting List Contact Logs: {len(contact_logs)}")
print(f" - Queue Entries: {len(entries)}")
# Only show distributions if appointments exist
@ -988,9 +1397,11 @@ def main():
'slots': slots,
'appointments': appointments,
'sessions': sessions,
'waiting_list_entries': waiting_list_entries,
'contact_logs': contact_logs,
'entries': entries
}
if __name__ == "__main__":
main()
main()

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import billing.utils
import django.db.models.deletion
@ -161,44 +161,41 @@ class Migration(migrations.Migration):
),
(
"place_of_service",
models.CharField(
models.IntegerField(
choices=[
("11", "Office"),
("12", "Home"),
("21", "Inpatient Hospital"),
("22", "Outpatient Hospital"),
("23", "Emergency Room"),
("24", "Ambulatory Surgical Center"),
("25", "Birthing Center"),
("26", "Military Treatment Facility"),
("31", "Skilled Nursing Facility"),
("32", "Nursing Facility"),
("33", "Custodial Care Facility"),
("34", "Hospice"),
("41", "Ambulance - Land"),
("42", "Ambulance - Air or Water"),
("49", "Independent Clinic"),
("50", "Federally Qualified Health Center"),
("51", "Inpatient Psychiatric Facility"),
("52", "Psychiatric Facility-Partial Hospitalization"),
("53", "Community Mental Health Center"),
("54", "Intermediate Care Facility/Mentally Retarded"),
("55", "Residential Substance Abuse Treatment Facility"),
("56", "Psychiatric Residential Treatment Center"),
(
"57",
"Non-residential Substance Abuse Treatment Facility",
),
("60", "Mass Immunization Center"),
("61", "Comprehensive Inpatient Rehabilitation Facility"),
("62", "Comprehensive Outpatient Rehabilitation Facility"),
("65", "End-Stage Renal Disease Treatment Facility"),
("71", "Public Health Clinic"),
("72", "Rural Health Clinic"),
("81", "Independent Laboratory"),
("99", "Other Place of Service"),
(11, "Office"),
(12, "Home"),
(21, "Inpatient Hospital"),
(22, "Outpatient Hospital"),
(23, "Emergency Room"),
(24, "Ambulatory Surgical Center"),
(25, "Birthing Center"),
(26, "Military Treatment Facility"),
(31, "Skilled Nursing Facility"),
(32, "Nursing Facility"),
(33, "Custodial Care Facility"),
(34, "Hospice"),
(41, "Ambulance - Land"),
(42, "Ambulance - Air or Water"),
(49, "Independent Clinic"),
(50, "Federally Qualified Health Center"),
(51, "Inpatient Psychiatric Facility"),
(52, "Psychiatric Facility-Partial Hospitalization"),
(53, "Community Mental Health Center"),
(54, "Intermediate Care Facility/Mentally Retarded"),
(55, "Residential Substance Abuse Treatment Facility"),
(56, "Psychiatric Residential Treatment Center"),
(57, "Non-residential Substance Abuse Treatment Facility"),
(60, "Mass Immunization Center"),
(61, "Comprehensive Inpatient Rehabilitation Facility"),
(62, "Comprehensive Outpatient Rehabilitation Facility"),
(65, "End-Stage Renal Disease Treatment Facility"),
(71, "Public Health Clinic"),
(72, "Rural Health Clinic"),
(81, "Independent Laboratory"),
(99, "Other Place of Service"),
],
default="22",
default=22,
help_text="Place of service code",
max_length=5,
),

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -0,0 +1,54 @@
# Generated by Django 5.2.6 on 2025-09-28 13:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0003_initial"),
]
operations = [
migrations.AlterField(
model_name="billlineitem",
name="place_of_service",
field=models.IntegerField(
choices=[
(11, "Office"),
(12, "Home"),
(21, "Inpatient Hospital"),
(22, "Outpatient Hospital"),
(23, "Emergency Room"),
(24, "Ambulatory Surgical Center"),
(25, "Birthing Center"),
(26, "Military Treatment Facility"),
(31, "Skilled Nursing Facility"),
(32, "Nursing Facility"),
(33, "Custodial Care Facility"),
(34, "Hospice"),
(41, "Ambulance - Land"),
(42, "Ambulance - Air or Water"),
(49, "Independent Clinic"),
(50, "Federally Qualified Health Center"),
(51, "Inpatient Psychiatric Facility"),
(52, "Psychiatric Facility-Partial Hospitalization"),
(53, "Community Mental Health Center"),
(54, "Intermediate Care Facility/Mentally Retarded"),
(55, "Residential Substance Abuse Treatment Facility"),
(56, "Psychiatric Residential Treatment Center"),
(57, "Non-residential Substance Abuse Treatment Facility"),
(60, "Mass Immunization Center"),
(61, "Comprehensive Inpatient Rehabilitation Facility"),
(62, "Comprehensive Outpatient Rehabilitation Facility"),
(65, "End-Stage Renal Disease Treatment Facility"),
(71, "Public Health Clinic"),
(72, "Rural Health Clinic"),
(81, "Independent Laboratory"),
(99, "Other Place of Service"),
],
default=22,
help_text="Place of service code",
),
),
]

View File

@ -18,7 +18,45 @@ class MedicalBill(models.Model):
"""
Medical bill model for patient billing and revenue cycle management.
"""
class BillType(models.TextChoices):
INPATIENT = 'INPATIENT', 'Inpatient'
OUTPATIENT = 'OUTPATIENT', 'Outpatient'
EMERGENCY = 'EMERGENCY', 'Emergency'
SURGERY = 'SURGERY', 'Surgery'
LABORATORY = 'LABORATORY', 'Laboratory'
RADIOLOGY = 'RADIOLOGY', 'Radiology'
PHARMACY = 'PHARMACY', 'Pharmacy'
PROFESSIONAL = 'PROFESSIONAL', 'Professional Services'
FACILITY = 'FACILITY', 'Facility Charges'
ANCILLARY = 'ANCILLARY', 'Ancillary Services'
class BillStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
PENDING = 'PENDING', 'Pending'
SUBMITTED = 'SUBMITTED', 'Submitted'
PARTIAL_PAID = 'PARTIAL_PAID', 'Partially Paid' # kept as provided
PAID = 'PAID', 'Paid'
OVERDUE = 'OVERDUE', 'Overdue'
COLLECTIONS = 'COLLECTIONS', 'Collections'
WRITTEN_OFF = 'WRITTEN_OFF', 'Written Off'
CANCELLED = 'CANCELLED', 'Cancelled'
class PaymentTerms(models.TextChoices):
NET_30 = 'NET_30', 'Net 30 Days'
NET_60 = 'NET_60', 'Net 60 Days'
NET_90 = 'NET_90', 'Net 90 Days'
IMMEDIATE = 'IMMEDIATE', 'Immediate'
CUSTOM = 'CUSTOM', 'Custom Terms'
class CollectionStatus(models.TextChoices):
NONE = 'NONE', 'None'
FIRST_NOTICE = 'FIRST_NOTICE', 'First Notice'
SECOND_NOTICE = 'SECOND_NOTICE', 'Second Notice'
FINAL_NOTICE = 'FINAL_NOTICE', 'Final Notice'
COLLECTIONS = 'COLLECTIONS', 'Collections'
LEGAL = 'LEGAL', 'Legal Action'
# Tenant relationship
tenant = models.ForeignKey(
'core.Tenant',
@ -51,18 +89,7 @@ class MedicalBill(models.Model):
# Bill Type and Category
bill_type = models.CharField(
max_length=20,
choices=[
('INPATIENT', 'Inpatient'),
('OUTPATIENT', 'Outpatient'),
('EMERGENCY', 'Emergency'),
('SURGERY', 'Surgery'),
('LABORATORY', 'Laboratory'),
('RADIOLOGY', 'Radiology'),
('PHARMACY', 'Pharmacy'),
('PROFESSIONAL', 'Professional Services'),
('FACILITY', 'Facility Charges'),
('ANCILLARY', 'Ancillary Services'),
],
choices=BillType.choices,
help_text='Bill type'
)
@ -146,18 +173,8 @@ class MedicalBill(models.Model):
# Bill Status
status = models.CharField(
max_length=20,
choices=[
('DRAFT', 'Draft'),
('PENDING', 'Pending'),
('SUBMITTED', 'Submitted'),
('PARTIAL_PAID', 'Partially Paid'),
('PAID', 'Paid'),
('OVERDUE', 'Overdue'),
('COLLECTIONS', 'Collections'),
('WRITTEN_OFF', 'Written Off'),
('CANCELLED', 'Cancelled'),
],
default='DRAFT',
choices=BillStatus.choices,
default=BillStatus.DRAFT,
help_text='Bill status'
)
@ -207,29 +224,16 @@ class MedicalBill(models.Model):
# Payment Terms
payment_terms = models.CharField(
max_length=20,
choices=[
('NET_30', 'Net 30 Days'),
('NET_60', 'Net 60 Days'),
('NET_90', 'Net 90 Days'),
('IMMEDIATE', 'Immediate'),
('CUSTOM', 'Custom Terms'),
],
default='NET_30',
choices=PaymentTerms.choices,
default=PaymentTerms.NET_30,
help_text='Payment terms'
)
# Collection Information
collection_status = models.CharField(
max_length=20,
choices=[
('NONE', 'None'),
('FIRST_NOTICE', 'First Notice'),
('SECOND_NOTICE', 'Second Notice'),
('FINAL_NOTICE', 'Final Notice'),
('COLLECTIONS', 'Collections'),
('LEGAL', 'Legal Action'),
],
default='NONE',
choices=CollectionStatus.choices,
default=CollectionStatus.NONE,
help_text='Collection status'
)
last_statement_date = models.DateField(
@ -318,73 +322,73 @@ class BillLineItem(models.Model):
"""
Bill line item model for detailed billing charges.
"""
SERVICE_CATEGORY_CHOICES = [
('EVALUATION', 'Evaluation & Management'),
('SURGERY', 'Surgery'),
('RADIOLOGY', 'Radiology'),
('PATHOLOGY', 'Pathology & Laboratory'),
('MEDICINE', 'Medicine'),
('ANESTHESIA', 'Anesthesia'),
('SUPPLIES', 'Medical Supplies'),
('PHARMACY', 'Pharmacy'),
('ROOM_BOARD', 'Room & Board'),
('NURSING', 'Nursing Services'),
('THERAPY', 'Therapy Services'),
('EMERGENCY', 'Emergency Services'),
('AMBULANCE', 'Ambulance Services'),
('DME', 'Durable Medical Equipment'),
('OTHER', 'Other Services'),
]
UNIT_OF_MEASURE_CHOICES = [
('EACH', 'Each'),
('UNIT', 'Unit'),
('HOUR', 'Hour'),
('DAY', 'Day'),
('VISIT', 'Visit'),
('PROCEDURE', 'Procedure'),
('DOSE', 'Dose'),
('MILE', 'Mile'),
('MINUTE', 'Minute'),
]
PLACE_OF_SERVICE_CHOICES = [
('11', 'Office'),
('12', 'Home'),
('21', 'Inpatient Hospital'),
('22', 'Outpatient Hospital'),
('23', 'Emergency Room'),
('24', 'Ambulatory Surgical Center'),
('25', 'Birthing Center'),
('26', 'Military Treatment Facility'),
('31', 'Skilled Nursing Facility'),
('32', 'Nursing Facility'),
('33', 'Custodial Care Facility'),
('34', 'Hospice'),
('41', 'Ambulance - Land'),
('42', 'Ambulance - Air or Water'),
('49', 'Independent Clinic'),
('50', 'Federally Qualified Health Center'),
('51', 'Inpatient Psychiatric Facility'),
('52', 'Psychiatric Facility-Partial Hospitalization'),
('53', 'Community Mental Health Center'),
('54', 'Intermediate Care Facility/Mentally Retarded'),
('55', 'Residential Substance Abuse Treatment Facility'),
('56', 'Psychiatric Residential Treatment Center'),
('57', 'Non-residential Substance Abuse Treatment Facility'),
('60', 'Mass Immunization Center'),
('61', 'Comprehensive Inpatient Rehabilitation Facility'),
('62', 'Comprehensive Outpatient Rehabilitation Facility'),
('65', 'End-Stage Renal Disease Treatment Facility'),
('71', 'Public Health Clinic'),
('72', 'Rural Health Clinic'),
('81', 'Independent Laboratory'),
('99', 'Other Place of Service'),
]
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('DENIED', 'Denied'),
('ADJUSTED', 'Adjusted'),
('VOIDED', 'Voided'),
]
class ServiceCategory(models.TextChoices):
EVALUATION = 'EVALUATION', 'Evaluation & Management'
SURGERY = 'SURGERY', 'Surgery'
RADIOLOGY = 'RADIOLOGY', 'Radiology'
PATHOLOGY = 'PATHOLOGY', 'Pathology & Laboratory'
MEDICINE = 'MEDICINE', 'Medicine'
ANESTHESIA = 'ANESTHESIA', 'Anesthesia'
SUPPLIES = 'SUPPLIES', 'Medical Supplies'
PHARMACY = 'PHARMACY', 'Pharmacy'
ROOM_BOARD = 'ROOM_BOARD', 'Room & Board'
NURSING = 'NURSING', 'Nursing Services'
THERAPY = 'THERAPY', 'Therapy Services'
EMERGENCY = 'EMERGENCY', 'Emergency Services'
AMBULANCE = 'AMBULANCE', 'Ambulance Services'
DME = 'DME', 'Durable Medical Equipment'
OTHER = 'OTHER', 'Other Services'
class ServiceUnitOfMeasure(models.TextChoices):
EACH = 'EACH', 'Each'
UNIT = 'UNIT', 'Unit'
HOUR = 'HOUR', 'Hour'
DAY = 'DAY', 'Day'
VISIT = 'VISIT', 'Visit'
PROCEDURE = 'PROCEDURE', 'Procedure'
DOSE = 'DOSE', 'Dose'
MILE = 'MILE', 'Mile'
MINUTE = 'MINUTE', 'Minute'
class PlaceOfService(models.IntegerChoices):
OFFICE = 11, 'Office'
HOME = 12, 'Home'
INPATIENT_HOSPITAL = 21, 'Inpatient Hospital'
OUTPATIENT_HOSPITAL = 22, 'Outpatient Hospital'
EMERGENCY_ROOM = 23, 'Emergency Room'
AMBULATORY_SURGICAL_CENTER = 24, 'Ambulatory Surgical Center'
BIRTHING_CENTER = 25, 'Birthing Center'
MILITARY_TREATMENT_FACILITY = 26, 'Military Treatment Facility'
SKILLED_NURSING_FACILITY = 31, 'Skilled Nursing Facility'
NURSING_FACILITY = 32, 'Nursing Facility'
CUSTODIAL_CARE_FACILITY = 33, 'Custodial Care Facility'
HOSPICE = 34, 'Hospice'
AMBULANCE_LAND = 41, 'Ambulance - Land'
AMBULANCE_AIR_OR_WATER = 42, 'Ambulance - Air or Water'
INDEPENDENT_CLINIC = 49, 'Independent Clinic'
FEDERALLY_QUALIFIED_HEALTH_CENTER = 50, 'Federally Qualified Health Center'
INPATIENT_PSYCHIATRIC_FACILITY = 51, 'Inpatient Psychiatric Facility'
PSYCHIATRIC_PARTIAL_HOSPITALIZATION = 52, 'Psychiatric Facility-Partial Hospitalization'
COMMUNITY_MENTAL_HEALTH_CENTER = 53, 'Community Mental Health Center'
INTERMEDIATE_CARE_FACILITY_MR = 54, 'Intermediate Care Facility/Mentally Retarded'
RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 55, 'Residential Substance Abuse Treatment Facility'
PSYCHIATRIC_RESIDENTIAL_TREATMENT_CENTER = 56, 'Psychiatric Residential Treatment Center'
NON_RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 57, 'Non-residential Substance Abuse Treatment Facility'
MASS_IMMUNIZATION_CENTER = 60, 'Mass Immunization Center'
COMPREHENSIVE_INPATIENT_REHAB_FACILITY = 61, 'Comprehensive Inpatient Rehabilitation Facility'
COMPREHENSIVE_OUTPATIENT_REHAB_FACILITY = 62, 'Comprehensive Outpatient Rehabilitation Facility'
ESRD_TREATMENT_FACILITY = 65, 'End-Stage Renal Disease Treatment Facility'
PUBLIC_HEALTH_CLINIC = 71, 'Public Health Clinic'
RURAL_HEALTH_CLINIC = 72, 'Rural Health Clinic'
INDEPENDENT_LABORATORY = 81, 'Independent Laboratory'
OTHER_PLACE_OF_SERVICE = 99, 'Other Place of Service'
class ServiceStatus(models.TextChoices):
ACTIVE = 'ACTIVE', 'Active'
DENIED = 'DENIED', 'Denied'
ADJUSTED = 'ADJUSTED', 'Adjusted'
VOIDED = 'VOIDED', 'Voided'
# Medical Bill relationship
medical_bill = models.ForeignKey(
MedicalBill,
@ -420,7 +424,7 @@ class BillLineItem(models.Model):
# Service Category
service_category = models.CharField(
max_length=30,
choices=SERVICE_CATEGORY_CHOICES,
choices=ServiceCategory.choices,
help_text='Service category'
)
@ -433,8 +437,8 @@ class BillLineItem(models.Model):
)
unit_of_measure = models.CharField(
max_length=20,
choices=UNIT_OF_MEASURE_CHOICES,
default='EACH',
choices=ServiceUnitOfMeasure.choices,
default=ServiceUnitOfMeasure.EACH,
help_text='Unit of measure'
)
@ -507,10 +511,9 @@ class BillLineItem(models.Model):
)
# Place of Service
place_of_service = models.CharField(
max_length=5,
choices=PLACE_OF_SERVICE_CHOICES,
default='22',
place_of_service = models.IntegerField(
choices=PlaceOfService.choices,
default=22,
help_text='Place of service code'
)
@ -546,8 +549,8 @@ class BillLineItem(models.Model):
# Line Item Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='ACTIVE',
choices=ServiceStatus.choices,
default=ServiceStatus.ACTIVE,
help_text='Line item status'
)
@ -604,25 +607,25 @@ class InsuranceClaim(models.Model):
"""
Insurance claim model for claims processing and management.
"""
CLAIM_TYPE_CHOICES = [
('PRIMARY', 'Primary Claim'),
('SECONDARY', 'Secondary Claim'),
('TERTIARY', 'Tertiary Claim'),
('CORRECTED', 'Corrected Claim'),
('VOID', 'Void Claim'),
('REPLACEMENT', 'Replacement Claim'),
]
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('SUBMITTED', 'Submitted'),
('PENDING', 'Pending'),
('PROCESSING', 'Processing'),
('PAID', 'Paid'),
('DENIED', 'Denied'),
('REJECTED', 'Rejected'),
('APPEALED', 'Appealed'),
('VOIDED', 'Voided'),
]
class ClaimSubmissionType(models.TextChoices):
PRIMARY = 'PRIMARY', 'Primary Claim'
SECONDARY = 'SECONDARY', 'Secondary Claim'
TERTIARY = 'TERTIARY', 'Tertiary Claim'
CORRECTED = 'CORRECTED', 'Corrected Claim'
VOID = 'VOID', 'Void Claim'
REPLACEMENT = 'REPLACEMENT', 'Replacement Claim'
class ClaimProcessingStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
SUBMITTED = 'SUBMITTED', 'Submitted'
PENDING = 'PENDING', 'Pending'
PROCESSING = 'PROCESSING', 'Processing'
PAID = 'PAID', 'Paid'
DENIED = 'DENIED', 'Denied'
REJECTED = 'REJECTED', 'Rejected'
APPEALED = 'APPEALED', 'Appealed'
VOIDED = 'VOIDED', 'Voided'
# Medical Bill relationship
medical_bill = models.ForeignKey(
@ -656,8 +659,8 @@ class InsuranceClaim(models.Model):
# Claim Type
claim_type = models.CharField(
max_length=20,
choices=CLAIM_TYPE_CHOICES,
default='PRIMARY',
choices=ClaimSubmissionType.choices,
default=ClaimSubmissionType.PRIMARY,
help_text='Claim type'
)
@ -718,8 +721,8 @@ class InsuranceClaim(models.Model):
# Claim Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='DRAFT',
choices=ClaimProcessingStatus.choices,
default=ClaimProcessingStatus.DRAFT,
help_text='Claim status'
)
@ -881,43 +884,44 @@ class Payment(models.Model):
"""
Payment model for tracking payments and receipts.
"""
PAYMENT_METHOD_CHOICES = [
('CASH', 'Cash'),
('CHECK', 'Check'),
('CREDIT_CARD', 'Credit Card'),
('DEBIT_CARD', 'Debit Card'),
('BANK_TRANSFER', 'Bank Transfer'),
('ACH', 'ACH Transfer'),
('WIRE', 'Wire Transfer'),
('MONEY_ORDER', 'Money Order'),
('INSURANCE', 'Insurance Payment'),
('ADJUSTMENT', 'Adjustment'),
('WRITE_OFF', 'Write Off'),
('OTHER', 'Other'),
]
PAYMENT_SOURCE_CHOICES = [
('PATIENT', 'Patient'),
('INSURANCE', 'Insurance'),
('GUARANTOR', 'Guarantor'),
('GOVERNMENT', 'Government'),
('CHARITY', 'Charity'),
('OTHER', 'Other'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('PROCESSED', 'Processed'),
('CLEARED', 'Cleared'),
('BOUNCED', 'Bounced'),
('REVERSED', 'Reversed'),
('REFUNDED', 'Refunded'),
]
CARD_TYPE_CHOICES = [
('VISA', 'Visa'),
('MASTERCARD', 'MasterCard'),
('AMEX', 'American Express'),
('DISCOVER', 'Discover'),
('OTHER', 'Other'),
]
class PaymentMethod(models.TextChoices):
CASH = 'CASH', 'Cash'
CHECK = 'CHECK', 'Check'
CREDIT_CARD = 'CREDIT_CARD', 'Credit Card'
DEBIT_CARD = 'DEBIT_CARD', 'Debit Card'
BANK_TRANSFER = 'BANK_TRANSFER', 'Bank Transfer'
ACH = 'ACH', 'ACH Transfer'
WIRE = 'WIRE', 'Wire Transfer'
MONEY_ORDER = 'MONEY_ORDER', 'Money Order'
INSURANCE = 'INSURANCE', 'Insurance Payment'
ADJUSTMENT = 'ADJUSTMENT', 'Adjustment'
WRITE_OFF = 'WRITE_OFF', 'Write Off'
OTHER = 'OTHER', 'Other'
class PaymentSource(models.TextChoices):
PATIENT = 'PATIENT', 'Patient'
INSURANCE = 'INSURANCE', 'Insurance'
GUARANTOR = 'GUARANTOR', 'Guarantor'
GOVERNMENT = 'GOVERNMENT', 'Government'
CHARITY = 'CHARITY', 'Charity'
OTHER = 'OTHER', 'Other'
class PaymentStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
PROCESSED = 'PROCESSED', 'Processed'
CLEARED = 'CLEARED', 'Cleared'
BOUNCED = 'BOUNCED', 'Bounced'
REVERSED = 'REVERSED', 'Reversed'
REFUNDED = 'REFUNDED', 'Refunded'
class CardType(models.TextChoices):
VISA = 'VISA', 'Visa'
MASTERCARD = 'MASTERCARD', 'MasterCard'
AMEX = 'AMEX', 'American Express'
DISCOVER = 'DISCOVER', 'Discover'
OTHER = 'OTHER', 'Other'
# Medical Bill relationship
medical_bill = models.ForeignKey(
MedicalBill,
@ -952,14 +956,14 @@ class Payment(models.Model):
# Payment Method
payment_method = models.CharField(
max_length=20,
choices=PAYMENT_METHOD_CHOICES,
choices=PaymentMethod.choices,
help_text='Payment method'
)
# Payment Source
payment_source = models.CharField(
max_length=20,
choices=PAYMENT_SOURCE_CHOICES,
choices=PaymentSource.choices,
help_text='Payment source'
)
@ -986,7 +990,7 @@ class Payment(models.Model):
# Credit Card Information (encrypted/tokenized)
card_type = models.CharField(
max_length=20,
choices=CARD_TYPE_CHOICES,
choices=CardType.choices,
blank=True,
null=True,
help_text='Credit card type'
@ -1029,8 +1033,8 @@ class Payment(models.Model):
# Payment Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING',
choices=PaymentStatus.choices,
default=PaymentStatus.PENDING,
help_text='Payment status'
)
@ -1157,16 +1161,17 @@ class ClaimStatusUpdate(models.Model):
"""
Claim status update model for tracking claim processing history.
"""
UPDATE_SOURCE_CHOICES = [
('MANUAL', 'Manual Update'),
('EDI', 'EDI Response'),
('PHONE', 'Phone Call'),
('PORTAL', 'Insurance Portal'),
('EMAIL', 'Email'),
('FAX', 'Fax'),
('MAIL', 'Mail'),
('SYSTEM', 'System Generated'),
]
class UpdateSource(models.TextChoices):
MANUAL = 'MANUAL', 'Manual Update'
EDI = 'EDI', 'EDI Response'
PHONE = 'PHONE', 'Phone Call'
PORTAL = 'PORTAL', 'Insurance Portal'
EMAIL = 'EMAIL', 'Email'
FAX = 'FAX', 'Fax'
MAIL = 'MAIL', 'Mail'
SYSTEM = 'SYSTEM', 'System Generated'
# Insurance Claim relationship
insurance_claim = models.ForeignKey(
InsuranceClaim,
@ -1200,7 +1205,7 @@ class ClaimStatusUpdate(models.Model):
# Update Details
update_source = models.CharField(
max_length=20,
choices=UPDATE_SOURCE_CHOICES,
choices=UpdateSource.choices,
help_text='Update source'
)
@ -1293,22 +1298,23 @@ class BillingConfiguration(models.Model):
"""
Billing configuration model for tenant-specific billing settings.
"""
DEFAULT_PAYMENT_TERMS_CHOICES = [
('NET_30', 'Net 30 Days'),
('NET_60', 'Net 60 Days'),
('NET_90', 'Net 90 Days'),
('IMMEDIATE', 'Immediate'),
]
STATEMENT_FREQUENCY_CHOICES = [
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
('ON_DEMAND', 'On Demand'),
]
CLAIM_SUBMISSION_FREQUENCY_CHOICES = [
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MANUAL', 'Manual'),
]
class DefaultPaymentTerms(models.TextChoices):
NET_30 = 'NET_30', 'Net 30 Days'
NET_60 = 'NET_60', 'Net 60 Days'
NET_90 = 'NET_90', 'Net 90 Days'
IMMEDIATE = 'IMMEDIATE', 'Immediate'
class StatementFrequency(models.TextChoices):
MONTHLY = 'MONTHLY', 'Monthly'
QUARTERLY = 'QUARTERLY', 'Quarterly'
ON_DEMAND = 'ON_DEMAND', 'On Demand'
class ClaimSubmissionFrequency(models.TextChoices):
DAILY = 'DAILY', 'Daily'
WEEKLY = 'WEEKLY', 'Weekly'
MANUAL = 'MANUAL', 'Manual'
# Tenant relationship
tenant = models.OneToOneField(
'core.Tenant',
@ -1328,8 +1334,8 @@ class BillingConfiguration(models.Model):
# Billing Settings
default_payment_terms = models.CharField(
max_length=20,
choices=DEFAULT_PAYMENT_TERMS_CHOICES,
default='NET_30',
choices=DefaultPaymentTerms.choices,
default=DefaultPaymentTerms.NET_30,
help_text='Default payment terms'
)
@ -1348,8 +1354,8 @@ class BillingConfiguration(models.Model):
# Statement Settings
statement_frequency = models.CharField(
max_length=20,
choices=STATEMENT_FREQUENCY_CHOICES,
default='MONTHLY',
choices=StatementFrequency.choices,
default=StatementFrequency.MONTHLY,
help_text='Statement frequency'
)
statement_message = models.TextField(
@ -1409,8 +1415,8 @@ class BillingConfiguration(models.Model):
)
claim_submission_frequency = models.CharField(
max_length=20,
choices=CLAIM_SUBMISSION_FREQUENCY_CHOICES,
default='DAILY',
choices=ClaimSubmissionFrequency.choices,
default=ClaimSubmissionFrequency.DAILY,
help_text='Claim submission frequency'
)

View File

@ -44,7 +44,7 @@ class BillingDashboardView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
today = timezone.now().date()
@ -110,7 +110,7 @@ class MedicalBillListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return MedicalBill.objects.none()
@ -184,7 +184,7 @@ class MedicalBillDetailView(LoginRequiredMixin, DetailView):
slug_url_kwarg = 'bill_id'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return MedicalBill.objects.none()
return MedicalBill.objects.filter(tenant=tenant)
@ -217,7 +217,7 @@ class MedicalBillCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateV
def form_valid(self, form):
# Set tenant and created_by
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
form.instance.created_by = self.request.user
# Generate bill number
@ -249,7 +249,7 @@ class MedicalBillUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateV
slug_url_kwarg = 'bill_id'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return MedicalBill.objects.none()
return MedicalBill.objects.filter(tenant=tenant)
@ -285,7 +285,7 @@ class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteV
slug_url_kwarg = 'bill_id'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return MedicalBill.objects.none()
return MedicalBill.objects.filter(tenant=tenant)
@ -314,7 +314,7 @@ class InsuranceClaimListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return InsuranceClaim.objects.none()
@ -372,7 +372,7 @@ class InsuranceClaimDetailView(LoginRequiredMixin, DetailView):
slug_url_kwarg = 'claim_id'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return InsuranceClaim.objects.none()
return InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
@ -406,7 +406,7 @@ class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
kwargs['medical_bill'] = medical_bill
except MedicalBill.DoesNotExist:
@ -421,7 +421,7 @@ class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
form.instance.medical_bill = medical_bill
except MedicalBill.DoesNotExist:
@ -442,7 +442,7 @@ class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
return ctx
@ -466,7 +466,7 @@ class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
kwargs['medical_bill'] = medical_bill
except MedicalBill.DoesNotExist:
@ -481,7 +481,7 @@ class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
form.instance.medical_bill = medical_bill
except MedicalBill.DoesNotExist:
@ -502,7 +502,7 @@ class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
return ctx
@ -520,7 +520,7 @@ class PaymentListView(LoginRequiredMixin, ListView):
paginate_by = 25
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return Payment.objects.none()
@ -579,7 +579,7 @@ class PaymentDetailView(LoginRequiredMixin, DetailView):
slug_url_kwarg = 'payment_id'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return Payment.objects.none()
return Payment.objects.filter(medical_bill__tenant=tenant)
@ -604,7 +604,7 @@ class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
kwargs['medical_bill'] = medical_bill
except MedicalBill.DoesNotExist:
@ -619,7 +619,7 @@ class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
tenant=self.request.user.tenant
)
form.instance.medical_bill = medical_bill
except MedicalBill.DoesNotExist:

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -98,7 +98,7 @@ class DonorListView(LoginRequiredMixin, ListView):
context.update({
'blood_groups': BloodGroup.objects.all(),
'status_choices': Donor.STATUS_CHOICES,
'status_choices': Donor.DonorStatus.choices,
'search_query': self.request.GET.get('search'),
'status_filter': self.request.GET.get('status'),
'blood_group_filter': self.request.GET.get('blood_group'),

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.db.models.deletion
from django.conf import settings

View File

@ -16,36 +16,34 @@ class Message(models.Model):
"""
Model for internal messaging system.
"""
MESSAGE_TYPES = [
('INTERNAL', 'Internal Message'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PUSH', 'Push Notification'),
('SLACK', 'Slack Message'),
('TEAMS', 'Microsoft Teams'),
('WEBHOOK', 'Webhook'),
('SYSTEM', 'System Message'),
('ALERT', 'Alert Message'),
]
PRIORITY_LEVELS = [
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
('CRITICAL', 'Critical'),
]
STATUS_CHOICES = [
('DRAFT', 'Draft'),
('PENDING', 'Pending'),
('SENDING', 'Sending'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('READ', 'Read'),
('FAILED', 'Failed'),
('CANCELLED', 'Cancelled'),
]
class MessageType(models.TextChoices):
INTERNAL = 'INTERNAL', 'Internal Message'
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
PUSH = 'PUSH', 'Push Notification'
SLACK = 'SLACK', 'Slack Message'
TEAMS = 'TEAMS', 'Microsoft Teams'
WEBHOOK = 'WEBHOOK', 'Webhook'
SYSTEM = 'SYSTEM', 'System Message'
ALERT = 'ALERT', 'Alert Message'
class PriorityLevel(models.TextChoices):
LOW = 'LOW', 'Low'
NORMAL = 'NORMAL', 'Normal'
HIGH = 'HIGH', 'High'
URGENT = 'URGENT', 'Urgent'
CRITICAL = 'CRITICAL', 'Critical'
class MessageStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
PENDING = 'PENDING', 'Pending'
SENDING = 'SENDING', 'Sending'
SENT = 'SENT', 'Sent'
DELIVERED = 'DELIVERED', 'Delivered'
READ = 'READ', 'Read'
FAILED = 'FAILED', 'Failed'
CANCELLED = 'CANCELLED', 'Cancelled'
# Primary identification
message_id = models.UUIDField(
@ -70,14 +68,14 @@ class Message(models.Model):
)
message_type = models.CharField(
max_length=20,
choices=MESSAGE_TYPES,
default='INTERNAL',
choices=MessageType.choices,
default=MessageType.INTERNAL,
help_text="Type of message"
)
priority = models.CharField(
max_length=20,
choices=PRIORITY_LEVELS,
default='NORMAL',
choices=PriorityLevel.choices,
default=PriorityLevel.NORMAL,
help_text="Message priority level"
)
@ -92,8 +90,8 @@ class Message(models.Model):
# Message status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='DRAFT',
choices=MessageStatus.choices,
default=MessageStatus.DRAFT,
help_text="Message status"
)
@ -205,28 +203,24 @@ class Message(models.Model):
class MessageRecipient(models.Model):
"""
Model for message recipients.
"""
RECIPIENT_TYPES = [
('USER', 'User'),
('EMAIL', 'Email Address'),
('PHONE', 'Phone Number'),
('ROLE', 'User Role'),
('DEPARTMENT', 'Department'),
('GROUP', 'User Group'),
]
STATUS_CHOICES = [
('PENDING', 'Pending'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('READ', 'Read'),
('ACKNOWLEDGED', 'Acknowledged'),
('FAILED', 'Failed'),
('BOUNCED', 'Bounced'),
('UNSUBSCRIBED', 'Unsubscribed'),
]
class RecipientType(models.TextChoices):
USER = 'USER', 'User'
EMAIL = 'EMAIL', 'Email Address'
PHONE = 'PHONE', 'Phone Number'
ROLE = 'ROLE', 'User Role'
DEPARTMENT = 'DEPARTMENT', 'Department'
GROUP = 'GROUP', 'User Group'
class RecipientStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
SENT = 'SENT', 'Sent'
DELIVERED = 'DELIVERED', 'Delivered'
READ = 'READ', 'Read'
ACKNOWLEDGED = 'ACKNOWLEDGED', 'Acknowledged'
FAILED = 'FAILED', 'Failed'
BOUNCED = 'BOUNCED', 'Bounced'
UNSUBSCRIBED = 'UNSUBSCRIBED', 'Unsubscribed'
# Primary identification
recipient_id = models.UUIDField(
@ -245,7 +239,7 @@ class MessageRecipient(models.Model):
# Recipient details
recipient_type = models.CharField(
max_length=20,
choices=RECIPIENT_TYPES,
choices=RecipientType.choices,
help_text="Type of recipient"
)
user = models.ForeignKey(
@ -276,8 +270,8 @@ class MessageRecipient(models.Model):
# Delivery status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING',
choices=RecipientStatus.choices,
default=RecipientStatus.PENDING,
help_text="Delivery status"
)
@ -355,28 +349,27 @@ class NotificationTemplate(models.Model):
"""
Model for notification templates.
"""
TEMPLATE_TYPES = [
('EMAIL', 'Email Template'),
('SMS', 'SMS Template'),
('PUSH', 'Push Notification Template'),
('SLACK', 'Slack Template'),
('TEAMS', 'Teams Template'),
('WEBHOOK', 'Webhook Template'),
('SYSTEM', 'System Notification Template'),
]
TEMPLATE_CATEGORIES = [
('APPOINTMENT', 'Appointment Notifications'),
('MEDICATION', 'Medication Reminders'),
('LAB_RESULTS', 'Lab Results'),
('BILLING', 'Billing Notifications'),
('EMERGENCY', 'Emergency Alerts'),
('SYSTEM', 'System Notifications'),
('MARKETING', 'Marketing Communications'),
('CLINICAL', 'Clinical Notifications'),
('ADMINISTRATIVE', 'Administrative Messages'),
('QUALITY', 'Quality Alerts'),
]
class TemplateType(models.TextChoices):
EMAIL = 'EMAIL', 'Email Template'
SMS = 'SMS', 'SMS Template'
PUSH = 'PUSH', 'Push Notification Template'
SLACK = 'SLACK', 'Slack Template'
TEAMS = 'TEAMS', 'Teams Template'
WEBHOOK = 'WEBHOOK', 'Webhook Template'
SYSTEM = 'SYSTEM', 'System Notification Template'
class TemplateCategory(models.TextChoices):
APPOINTMENT = 'APPOINTMENT', 'Appointment Notifications'
MEDICATION = 'MEDICATION', 'Medication Reminders'
LAB_RESULTS = 'LAB_RESULTS', 'Lab Results'
BILLING = 'BILLING', 'Billing Notifications'
EMERGENCY = 'EMERGENCY', 'Emergency Alerts'
SYSTEM = 'SYSTEM', 'System Notifications'
MARKETING = 'MARKETING', 'Marketing Communications'
CLINICAL = 'CLINICAL', 'Clinical Notifications'
ADMINISTRATIVE = 'ADMINISTRATIVE', 'Administrative Messages'
QUALITY = 'QUALITY', 'Quality Alerts'
# Primary identification
template_id = models.UUIDField(
@ -403,12 +396,12 @@ class NotificationTemplate(models.Model):
)
template_type = models.CharField(
max_length=20,
choices=TEMPLATE_TYPES,
choices=TemplateType.choices,
help_text="Type of template"
)
category = models.CharField(
max_length=30,
choices=TEMPLATE_CATEGORIES,
choices=TemplateCategory.choices,
help_text="Template category"
)
@ -497,24 +490,23 @@ class AlertRule(models.Model):
"""
Model for automated alert rules.
"""
TRIGGER_TYPES = [
('THRESHOLD', 'Threshold Alert'),
('PATTERN', 'Pattern Alert'),
('SCHEDULE', 'Scheduled Alert'),
('EVENT', 'Event-based Alert'),
('ANOMALY', 'Anomaly Detection'),
('SYSTEM', 'System Alert'),
('CLINICAL', 'Clinical Alert'),
('OPERATIONAL', 'Operational Alert'),
]
SEVERITY_LEVELS = [
('INFO', 'Information'),
('WARNING', 'Warning'),
('ERROR', 'Error'),
('CRITICAL', 'Critical'),
('EMERGENCY', 'Emergency'),
]
class TriggerType(models.TextChoices):
THRESHOLD = 'THRESHOLD', 'Threshold Alert'
PATTERN = 'PATTERN', 'Pattern Alert'
SCHEDULE = 'SCHEDULE', 'Scheduled Alert'
EVENT = 'EVENT', 'Event-based Alert'
ANOMALY = 'ANOMALY', 'Anomaly Detection'
SYSTEM = 'SYSTEM', 'System Alert'
CLINICAL = 'CLINICAL', 'Clinical Alert'
OPERATIONAL = 'OPERATIONAL', 'Operational Alert'
class SeverityLevel(models.TextChoices):
INFO = 'INFO', 'Information'
WARNING = 'WARNING', 'Warning'
ERROR = 'ERROR', 'Error'
CRITICAL = 'CRITICAL', 'Critical'
EMERGENCY = 'EMERGENCY', 'Emergency'
# Primary identification
rule_id = models.UUIDField(
@ -541,13 +533,13 @@ class AlertRule(models.Model):
)
trigger_type = models.CharField(
max_length=20,
choices=TRIGGER_TYPES,
choices=TriggerType.choices,
help_text="Type of alert trigger"
)
severity = models.CharField(
max_length=20,
choices=SEVERITY_LEVELS,
default='WARNING',
choices=SeverityLevel.choices,
default=SeverityLevel.WARNING,
help_text="Alert severity level"
)
@ -665,14 +657,13 @@ class AlertInstance(models.Model):
"""
Model for alert instances.
"""
STATUS_CHOICES = [
('ACTIVE', 'Active'),
('ACKNOWLEDGED', 'Acknowledged'),
('RESOLVED', 'Resolved'),
('SUPPRESSED', 'Suppressed'),
('ESCALATED', 'Escalated'),
('EXPIRED', 'Expired'),
]
class AlertStatus(models.TextChoices):
ACTIVE = 'ACTIVE', 'Active'
ACKNOWLEDGED = 'ACKNOWLEDGED', 'Acknowledged'
RESOLVED = 'RESOLVED', 'Resolved'
SUPPRESSED = 'SUPPRESSED', 'Suppressed'
ESCALATED = 'ESCALATED', 'Escalated'
EXPIRED = 'EXPIRED', 'Expired'
# Primary identification
alert_id = models.UUIDField(
@ -698,7 +689,7 @@ class AlertInstance(models.Model):
)
severity = models.CharField(
max_length=20,
choices=AlertRule.SEVERITY_LEVELS,
choices=AlertRule.SeverityLevel.choices,
help_text="Alert severity level"
)
@ -715,8 +706,8 @@ class AlertInstance(models.Model):
# Alert status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='ACTIVE',
choices=AlertStatus.choices,
default=AlertStatus.ACTIVE,
help_text="Alert status"
)
@ -817,30 +808,29 @@ class CommunicationChannel(models.Model):
"""
Model for communication channels.
"""
CHANNEL_TYPES = [
('EMAIL', 'Email'),
('SMS', 'SMS'),
('PUSH', 'Push Notification'),
('SLACK', 'Slack'),
('TEAMS', 'Microsoft Teams'),
('WEBHOOK', 'Webhook'),
('PHONE', 'Phone Call'),
('FAX', 'Fax'),
('PAGER', 'Pager'),
]
PROVIDER_TYPES = [
('SMTP', 'SMTP Email'),
('SENDGRID', 'SendGrid'),
('MAILGUN', 'Mailgun'),
('TWILIO', 'Twilio SMS'),
('AWS_SNS', 'AWS SNS'),
('FIREBASE', 'Firebase'),
('SLACK_API', 'Slack API'),
('TEAMS_API', 'Teams API'),
('WEBHOOK', 'Webhook'),
('CUSTOM', 'Custom Provider'),
]
class ChannelType(models.TextChoices):
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
PUSH = 'PUSH', 'Push Notification'
SLACK = 'SLACK', 'Slack'
TEAMS = 'TEAMS', 'Microsoft Teams'
WEBHOOK = 'WEBHOOK', 'Webhook'
PHONE = 'PHONE', 'Phone Call'
FAX = 'FAX', 'Fax'
PAGER = 'PAGER', 'Pager'
class ProviderType(models.TextChoices):
SMTP = 'SMTP', 'SMTP Email'
SENDGRID = 'SENDGRID', 'SendGrid'
MAILGUN = 'MAILGUN', 'Mailgun'
TWILIO = 'TWILIO', 'Twilio SMS'
AWS_SNS = 'AWS_SNS', 'AWS SNS'
FIREBASE = 'FIREBASE', 'Firebase'
SLACK_API = 'SLACK_API', 'Slack API'
TEAMS_API = 'TEAMS_API', 'Teams API'
WEBHOOK = 'WEBHOOK', 'Webhook'
CUSTOM = 'CUSTOM', 'Custom Provider'
# Primary identification
channel_id = models.UUIDField(
@ -867,12 +857,12 @@ class CommunicationChannel(models.Model):
)
channel_type = models.CharField(
max_length=20,
choices=CHANNEL_TYPES,
choices=ChannelType.choices,
help_text="Type of communication channel"
)
provider_type = models.CharField(
max_length=20,
choices=PROVIDER_TYPES,
choices=ProviderType.choices,
help_text="Provider type"
)
@ -980,16 +970,16 @@ class DeliveryLog(models.Model):
"""
Model for delivery logging.
"""
STATUS_CHOICES = [
('PENDING', 'Pending'),
('PROCESSING', 'Processing'),
('SENT', 'Sent'),
('DELIVERED', 'Delivered'),
('FAILED', 'Failed'),
('BOUNCED', 'Bounced'),
('REJECTED', 'Rejected'),
('TIMEOUT', 'Timeout'),
]
class DeliveryStatus(models.TextChoices):
PENDING = 'PENDING', 'Pending'
PROCESSING = 'PROCESSING', 'Processing'
SENT = 'SENT', 'Sent'
DELIVERED = 'DELIVERED', 'Delivered'
FAILED = 'FAILED', 'Failed'
BOUNCED = 'BOUNCED', 'Bounced'
REJECTED = 'REJECTED', 'Rejected'
TIMEOUT = 'TIMEOUT', 'Timeout'
# Primary identification
log_id = models.UUIDField(
@ -1020,8 +1010,8 @@ class DeliveryLog(models.Model):
# Delivery details
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='PENDING',
choices=DeliveryStatus.choices,
default=DeliveryStatus.PENDING,
help_text="Delivery status"
)
attempt_number = models.PositiveIntegerField(

View File

@ -114,7 +114,7 @@ class SystemConfigurationViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
updated_by=self.request.user
)
@ -161,7 +161,7 @@ class SystemNotificationViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
serializer.save(
tenant=getattr(self.request, 'tenant', None),
tenant=self.request.user.tenant,
created_by=self.request.user
)

View File

@ -1,67 +1,67 @@
"""
Tenant middleware for multi-tenancy support.
"""
from django.http import Http404
from django.shortcuts import get_object_or_404
from .models import Tenant
class TenantMiddleware:
"""
Middleware to handle tenant resolution and context.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Skip tenant resolution for admin and API endpoints
if request.path.startswith('/admin/') or request.path.startswith('/api/'):
response = self.get_response(request)
return response
# Get tenant from subdomain or header
tenant = self.get_tenant(request)
if tenant:
request.tenant = tenant
else:
# For development, use the first available tenant
try:
request.tenant = Tenant.objects.filter(is_active=True).first()
except Tenant.DoesNotExist:
request.tenant = None
response = self.get_response(request)
return response
def get_tenant(self, request):
"""
Resolve tenant from request.
"""
# Try to get tenant from subdomain
host = request.get_host()
if '.' in host:
subdomain = host.split('.')[0]
try:
return Tenant.objects.get(
name__iexact=subdomain,
is_active=True
)
except Tenant.DoesNotExist:
pass
# Try to get tenant from header (for API calls)
tenant_id = request.headers.get('X-Tenant-ID')
if tenant_id:
try:
return Tenant.objects.get(
tenant_id=tenant_id,
is_active=True
)
except Tenant.DoesNotExist:
pass
return None
# """
# Tenant middleware for multi-tenancy support.
# """
#
# from django.http import Http404
# from django.shortcuts import get_object_or_404
# from .models import Tenant
#
#
# class TenantMiddleware:
# """
# Middleware to handle tenant resolution and context.
# """
#
# def __init__(self, get_response):
# self.get_response = get_response
#
# def __call__(self, request):
# # Skip tenant resolution for admin and API endpoints
# if request.path.startswith('/admin/') or request.path.startswith('/api/'):
# response = self.get_response(request)
# return response
#
# # Get tenant from subdomain or header
# tenant = self.get_tenant(request)
#
# if tenant:
# request.tenant = tenant
# else:
# # For development, use the first available tenant
# try:
# request.tenant = Tenant.objects.filter(is_active=True).first()
# except Tenant.DoesNotExist:
# request.tenant = None
#
# response = self.get_response(request)
# return response
#
# def get_tenant(self, request):
# """
# Resolve tenant from request.
# """
# # Try to get tenant from subdomain
# host = request.get_host()
# if '.' in host:
# subdomain = host.split('.')[0]
# try:
# return Tenant.objects.get(
# name__iexact=subdomain,
# is_active=True
# )
# except Tenant.DoesNotExist:
# pass
#
# # Try to get tenant from header (for API calls)
# tenant_id = request.headers.get('X-Tenant-ID')
# if tenant_id:
# try:
# return Tenant.objects.get(
# tenant_id=tenant_id,
# is_active=True
# )
# except Tenant.DoesNotExist:
# pass
#
# return None
#

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.6 on 2025-09-19 10:58
# Generated by Django 5.2.6 on 2025-09-26 18:33
import django.core.validators
import django.db.models.deletion

View File

@ -4,7 +4,7 @@
class TenantMixin:
def get_queryset(self):
qs = super().get_queryset()
tenant = getattr(self.request.user, 'tenant', None)
tenant = self.request.user.tenant
if tenant and not self.request.user.is_superuser:
# Models with patient FK:
if hasattr(qs.model, 'patient'):
@ -22,5 +22,5 @@ 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)
kw['tenant'] = self.request.user.tenant
return kw

View File

@ -20,23 +20,22 @@ class Tenant(models.Model):
Each tenant represents a separate hospital or healthcare organization.
"""
ORGANIZATION_TYPE_CHOICES = [
('HOSPITAL', 'Hospital'),
('CLINIC', 'Clinic'),
('HEALTH_SYSTEM', 'Health System'),
('AMBULATORY', 'Ambulatory Care'),
('SPECIALTY', 'Specialty Practice'),
('URGENT_CARE', 'Urgent Care'),
('REHABILITATION', 'Rehabilitation Center'),
('LONG_TERM_CARE', 'Long-term Care'),
]
class OrganizationType(models.TextChoices):
HOSPITAL = 'HOSPITAL', 'Hospital'
CLINIC = 'CLINIC', 'Clinic'
HEALTH_SYSTEM = 'HEALTH_SYSTEM', 'Health System'
AMBULATORY = 'AMBULATORY', 'Ambulatory Care'
SPECIALTY = 'SPECIALTY', 'Specialty Practice'
URGENT_CARE = 'URGENT_CARE', 'Urgent Care'
REHABILITATION = 'REHABILITATION', 'Rehabilitation Center'
LONG_TERM_CARE = 'LONG_TERM_CARE', 'Long-term Care'
class SubscriptionPlan(models.TextChoices):
BASIC = 'BASIC', 'Basic'
STANDARD = 'STANDARD', 'Standard'
PREMIUM = 'PREMIUM', 'Premium'
ENTERPRISE = 'ENTERPRISE', 'Enterprise'
SUBSCRIPTION_PLAN_CHOICES = [
('BASIC', 'Basic'),
('STANDARD', 'Standard'),
('PREMIUM', 'Premium'),
('ENTERPRISE', 'Enterprise'),
]
# Tenant Information
tenant_id = models.UUIDField(
default=uuid.uuid4,
@ -61,8 +60,8 @@ class Tenant(models.Model):
# Organization Details
organization_type = models.CharField(
max_length=50,
choices=ORGANIZATION_TYPE_CHOICES,
default='HOSPITAL'
choices=OrganizationType.choices,
default=OrganizationType.HOSPITAL
)
# Contact Information
@ -157,8 +156,8 @@ class Tenant(models.Model):
# Subscription and Billing
subscription_plan = models.CharField(
max_length=50,
choices=SUBSCRIPTION_PLAN_CHOICES,
default='BASIC'
choices=SubscriptionPlan.choices,
default=SubscriptionPlan.BASIC
)
max_users = models.PositiveIntegerField(
default=50,
@ -220,42 +219,40 @@ class AuditLogEntry(models.Model):
Comprehensive audit logging for HIPAA/GDPR compliance.
Tracks all user actions and system events.
"""
RISK_LEVEL_CHOICES = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('CRITICAL', 'Critical'),
]
EVENT_TYPE_CHOICES = [
('CREATE', 'Create'),
('READ', 'Read'),
('UPDATE', 'Update'),
('DELETE', 'Delete'),
('LOGIN', 'Login'),
('LOGOUT', 'Logout'),
('ACCESS', 'Access'),
('EXPORT', 'Export'),
('PRINT', 'Print'),
('SHARE', 'Share'),
('SYSTEM', 'System Event'),
('ERROR', 'Error'),
('SECURITY', 'Security Event'),
]
class RiskLevel(models.TextChoices):
LOW = 'LOW', 'Low'
MEDIUM = 'MEDIUM', 'Medium'
HIGH = 'HIGH', 'High'
CRITICAL = 'CRITICAL', 'Critical'
EVENT_CATEGORY_CHOICES = [
('AUTHENTICATION', 'Authentication'),
('AUTHORIZATION', 'Authorization'),
('DATA_ACCESS', 'Data Access'),
('DATA_MODIFICATION', 'Data Modification'),
('SYSTEM_ADMINISTRATION', 'System Administration'),
('PATIENT_DATA', 'Patient Data'),
('CLINICAL_DATA', 'Clinical Data'),
('FINANCIAL_DATA', 'Financial Data'),
('SECURITY', 'Security'),
('INTEGRATION', 'Integration'),
('REPORTING', 'Reporting'),
]
class EventType(models.TextChoices):
CREATE = 'CREATE', 'Create'
READ = 'READ', 'Read'
UPDATE = 'UPDATE', 'Update'
DELETE = 'DELETE', 'Delete'
LOGIN = 'LOGIN', 'Login'
LOGOUT = 'LOGOUT', 'Logout'
ACCESS = 'ACCESS', 'Access'
EXPORT = 'EXPORT', 'Export'
PRINT = 'PRINT', 'Print'
SHARE = 'SHARE', 'Share'
SYSTEM = 'SYSTEM', 'System Event'
ERROR = 'ERROR', 'Error'
SECURITY = 'SECURITY', 'Security Event'
class EventCategory(models.TextChoices):
AUTHENTICATION = 'AUTHENTICATION', 'Authentication'
AUTHORIZATION = 'AUTHORIZATION', 'Authorization'
DATA_ACCESS = 'DATA_ACCESS', 'Data Access'
DATA_MODIFICATION = 'DATA_MODIFICATION', 'Data Modification'
SYSTEM_ADMINISTRATION = 'SYSTEM_ADMINISTRATION', 'System Administration'
PATIENT_DATA = 'PATIENT_DATA', 'Patient Data'
CLINICAL_DATA = 'CLINICAL_DATA', 'Clinical Data'
FINANCIAL_DATA = 'FINANCIAL_DATA', 'Financial Data'
SECURITY = 'SECURITY', 'Security'
INTEGRATION = 'INTEGRATION', 'Integration'
REPORTING = 'REPORTING', 'Reporting'
# Tenant
tenant = models.ForeignKey(
@ -275,11 +272,11 @@ class AuditLogEntry(models.Model):
# Event Information
event_type = models.CharField(
max_length=50,
choices=EVENT_TYPE_CHOICES
choices=EventType.choices,
)
event_category = models.CharField(
max_length=50,
choices=EVENT_CATEGORY_CHOICES
choices=EventCategory.choices,
)
# User Information
@ -375,8 +372,8 @@ class AuditLogEntry(models.Model):
# Risk Assessment
risk_level = models.CharField(
max_length=20,
choices=RISK_LEVEL_CHOICES,
default='LOW'
choices=RiskLevel.choices,
default=RiskLevel.LOW
)
# Compliance Flags
@ -428,15 +425,16 @@ class SystemConfiguration(models.Model):
"""
System configuration settings for tenant-specific and global configurations.
"""
DATA_TYPE_CHOICES = [
('STRING', 'String'),
('INTEGER', 'Integer'),
('FLOAT', 'Float'),
('BOOLEAN', 'Boolean'),
('JSON', 'JSON'),
('DATE', 'Date'),
('DATETIME', 'DateTime'),
]
class DataType(models.TextChoices):
STRING = 'STRING', 'String'
INTEGER = 'INTEGER', 'Integer'
FLOAT = 'FLOAT', 'Float'
BOOLEAN = 'BOOLEAN', 'Boolean'
JSON = 'JSON', 'JSON'
DATE = 'DATE', 'Date'
DATETIME = 'DATETIME', 'DateTime'
# Tenant (null for global configurations)
tenant = models.ForeignKey(
Tenant,
@ -456,8 +454,8 @@ class SystemConfiguration(models.Model):
)
data_type = models.CharField(
max_length=20,
choices=DATA_TYPE_CHOICES,
default='STRING'
choices=DataType.choices,
default=DataType.STRING
)
# Configuration Metadata
@ -555,32 +553,30 @@ class SystemNotification(models.Model):
"""
System-wide notifications and announcements.
"""
TARGET_AUDIENCE = [
('ALL_USERS', 'All Users'),
('ADMINISTRATORS', 'Administrators'),
('CLINICAL_STAFF', 'Clinical Staff'),
('SUPPORT_STAFF', 'Support Staff'),
('SPECIFIC_ROLES', 'Specific Roles'),
('SPECIFIC_USERS', 'Specific Users'),
]
NOTIFICATION_TYPE_CHOICES = [
('INFO', 'Information'),
('WARNING', 'Warning'),
('ERROR', 'Error'),
('SUCCESS', 'Success'),
('MAINTENANCE', 'Maintenance'),
('SECURITY', 'Security Alert'),
('FEATURE', 'New Feature'),
('UPDATE', 'System Update'),
]
class TargetAudience(models.TextChoices):
ALL_USERS = 'ALL_USERS', 'All Users'
ADMINISTRATORS = 'ADMINISTRATORS', 'Administrators'
CLINICAL_STAFF = 'CLINICAL_STAFF', 'Clinical Staff'
SUPPORT_STAFF = 'SUPPORT_STAFF', 'Support Staff'
SPECIFIC_ROLES = 'SPECIFIC_ROLES', 'Specific Roles'
SPECIFIC_USERS = 'SPECIFIC_USERS', 'Specific Users'
PRIORITY_CHOICES = [
('LOW', 'Low'),
('MEDIUM', 'Medium'),
('HIGH', 'High'),
('URGENT', 'Urgent'),
]
class NotificationType(models.TextChoices):
INFO = 'INFO', 'Information'
WARNING = 'WARNING', 'Warning'
ERROR = 'ERROR', 'Error'
SUCCESS = 'SUCCESS', 'Success'
MAINTENANCE = 'MAINTENANCE', 'Maintenance'
SECURITY = 'SECURITY', 'Security Alert'
FEATURE = 'FEATURE', 'New Feature'
UPDATE = 'UPDATE', 'System Update'
class NotificationPriority(models.TextChoices):
LOW = 'LOW', 'Low'
MEDIUM = 'MEDIUM', 'Medium'
HIGH = 'HIGH', 'High'
URGENT = 'URGENT', 'Urgent'
# Tenant (null for global notifications)
tenant = models.ForeignKey(
@ -609,21 +605,21 @@ class SystemNotification(models.Model):
# Notification Type
notification_type = models.CharField(
max_length=30,
choices=NOTIFICATION_TYPE_CHOICES,
choices=NotificationType.choices,
)
# Priority
priority = models.CharField(
max_length=20,
choices=PRIORITY_CHOICES,
default='MEDIUM'
choices=NotificationPriority.choices,
default=NotificationPriority.MEDIUM
)
# Targeting
target_audience = models.CharField(
max_length=30,
choices=TARGET_AUDIENCE,
default='ALL_USERS'
choices=TargetAudience.choices,
default=TargetAudience.ALL_USERS
)
target_roles = models.JSONField(
default=list,
@ -715,24 +711,23 @@ class IntegrationLog(models.Model):
"""
Integration logging for external system communications.
"""
STATUS_CHOICES = [
('SUCCESS', 'Success'),
('FAILED', 'Failed'),
('PENDING', 'Pending'),
('TIMEOUT', 'Timeout'),
('RETRY', 'Retry'),
]
INTEGRATION_TYPE_CHOICES = [
('HL7', 'HL7 Message'),
('DICOM', 'DICOM Communication'),
('API', 'API Call'),
('DATABASE', 'Database Sync'),
('FILE_TRANSFER', 'File Transfer'),
('WEBHOOK', 'Webhook'),
('EMAIL', 'Email'),
('SMS', 'SMS'),
]
class IntegrationStatus(models.TextChoices):
SUCCESS = 'SUCCESS', 'Success'
FAILED = 'FAILED', 'Failed'
PENDING = 'PENDING', 'Pending'
TIMEOUT = 'TIMEOUT', 'Timeout'
RETRY = 'RETRY', 'Retry'
class IntegrationType(models.TextChoices):
HL7 = 'HL7', 'HL7 Message'
DICOM = 'DICOM', 'DICOM Communication'
API = 'API', 'API Call'
DATABASE = 'DATABASE', 'Database Sync'
FILE_TRANSFER = 'FILE_TRANSFER', 'File Transfer'
WEBHOOK = 'WEBHOOK', 'Webhook'
EMAIL = 'EMAIL', 'Email'
SMS = 'SMS', 'SMS'
# Tenant
tenant = models.ForeignKey(
@ -752,7 +747,7 @@ class IntegrationLog(models.Model):
# Integration Information
integration_type = models.CharField(
max_length=30,
choices=INTEGRATION_TYPE_CHOICES
choices=IntegrationType.choices,
)
direction = models.CharField(
max_length=10,
@ -808,7 +803,7 @@ class IntegrationLog(models.Model):
# Status
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
choices=IntegrationStatus.choices,
)
# Error Information

View File

@ -42,8 +42,8 @@ class DashboardView(LoginRequiredMixin, TemplateView):
template_name = 'core/dashboard.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = self.request.user.tenant
context = super().get_context_data(**kwargs)
if tenant:
# Get dashboard statistics
@ -233,7 +233,7 @@ class AuditLogListView(LoginRequiredMixin, ListView):
paginate_by = 50
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AuditLogEntry.objects.none()
@ -264,7 +264,7 @@ class AuditLogListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
# Get filter options
@ -289,7 +289,7 @@ class AuditLogDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'audit_log'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return AuditLogEntry.objects.none()
return AuditLogEntry.objects.filter(tenant=tenant)
@ -304,7 +304,7 @@ class SystemConfigurationListView(LoginRequiredMixin, ListView):
context_object_name = 'configurations'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
queryset = SystemConfiguration.objects.filter(
Q(tenant=tenant) | Q(tenant=None),
is_active=True
@ -328,7 +328,7 @@ class SystemConfigurationListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
# Group configurations by category
@ -359,7 +359,7 @@ class SystemConfigurationDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'configuration'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemConfiguration.objects.filter(
Q(tenant=tenant) | Q(tenant=None)
)
@ -377,7 +377,7 @@ class SystemConfigurationCreateView(LoginRequiredMixin, PermissionRequiredMixin,
def form_valid(self, form):
# Set tenant
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
response = super().form_valid(form)
# Log configuration creation
@ -406,7 +406,7 @@ class SystemConfigurationUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
permission_required = 'core.change_systemconfiguration'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemConfiguration.objects.filter(
Q(tenant=tenant) | Q(tenant=None)
)
@ -443,7 +443,7 @@ class SystemConfigurationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
success_url = reverse_lazy('core:system_configuration_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemConfiguration.objects.filter(tenant=tenant) # Only tenant-specific configs can be deleted
def delete(self, request, *args, **kwargs):
@ -475,7 +475,7 @@ class SystemNotificationListView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
queryset = SystemNotification.objects.filter(
Q(tenant=tenant) | Q(tenant=None)
).order_by('-created_at')
@ -515,7 +515,7 @@ class SystemNotificationDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'notification'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemNotification.objects.filter(
Q(tenant=tenant) | Q(tenant=None)
)
@ -533,7 +533,7 @@ class SystemNotificationCreateView(LoginRequiredMixin, PermissionRequiredMixin,
def form_valid(self, form):
# Set tenant and creator
form.instance.tenant = getattr(self.request, 'tenant', None)
form.instance.tenant = self.request.user.tenant
form.instance.created_by = self.request.user
response = super().form_valid(form)
@ -563,7 +563,7 @@ class SystemNotificationUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
permission_required = 'core.change_systemnotification'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemNotification.objects.filter(
Q(tenant=tenant) | Q(tenant=None)
)
@ -600,7 +600,7 @@ class SystemNotificationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
success_url = reverse_lazy('core:notification_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
return SystemNotification.objects.filter(tenant=tenant) # Only tenant notifications can be deleted
def delete(self, request, *args, **kwargs):
@ -632,7 +632,7 @@ class IntegrationLogListView(LoginRequiredMixin, ListView):
paginate_by = 50
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return IntegrationLog.objects.none()
@ -659,7 +659,7 @@ class IntegrationLogListView(LoginRequiredMixin, ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if tenant:
context.update({
@ -683,7 +683,7 @@ class IntegrationLogDetailView(LoginRequiredMixin, DetailView):
context_object_name = 'log'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
tenant = self.request.user.tenant
if not tenant:
return IntegrationLog.objects.none()
return IntegrationLog.objects.filter(tenant=tenant)
@ -694,7 +694,7 @@ def dashboard_stats(request):
"""
HTMX view for dashboard statistics.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -735,7 +735,7 @@ def audit_log_search(request):
"""
HTMX view for audit log search.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -762,7 +762,7 @@ def system_notifications(request):
"""
HTMX view for system notifications.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -785,7 +785,7 @@ def dismiss_notification(request, notification_id):
"""
HTMX view to dismiss a notification.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -815,7 +815,7 @@ def tenant_info(request):
"""
HTMX view for tenant information.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -829,7 +829,7 @@ def system_health(request):
"""
HTMX view for system health status.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
return JsonResponse({'error': 'No tenant found'}, status=400)
@ -867,7 +867,7 @@ def activate_notification(request, pk):
"""
Activate a system notification.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:notification_list')
@ -902,7 +902,7 @@ def deactivate_notification(request, pk):
"""
Deactivate a system notification.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:notification_list')
@ -937,7 +937,7 @@ def reset_configuration(request, pk):
"""
Reset configuration to default value.
"""
tenant = getattr(request, 'tenant', None)
tenant = request.user.tenant
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('core:system_configuration_list')
@ -969,12 +969,12 @@ def reset_configuration(request, pk):
return redirect('core:system_configuration_detail', pk=pk)
@login_required
def tenant_stats(request):
"""
HTMX view for tenant statistics.
"""
from django.db.models import Count
stats = {
'total_tenants': Tenant.objects.count(),
'active_tenants': Tenant.objects.filter(is_active=True).count(),
@ -985,6 +985,7 @@ def tenant_stats(request):
return render(request, 'core/partials/tenant_stats.html', {'stats': stats})
@login_required
def configuration_search(request):
"""
HTMX view for configuration search.
@ -1004,12 +1005,14 @@ def configuration_search(request):
})
@login_required
def audit_log_list_htmx(request):
"""
HTMX view for audit log list.
"""
tenant = request.user.tenant
logs = AuditLog.objects.filter(
tenant=request.user.tenant
tenant=tenant
).order_by('-timestamp')[:20]
return render(request, 'core/partials/audit_log_list.html', {
@ -1017,11 +1020,12 @@ def audit_log_list_htmx(request):
})
@login_required
def activate_tenant(request, pk):
"""
Activate a tenant.
"""
tenant = get_object_or_404(Tenant, tenant_id=pk)
tenant = get_object_or_404(Tenant, pk=pk)
tenant.is_active = True
tenant.save()
@ -1029,11 +1033,12 @@ def activate_tenant(request, pk):
return redirect('core:tenant_detail', pk=pk)
@login_required
def deactivate_tenant(request, pk):
"""
Deactivate a tenant.
"""
tenant = get_object_or_404(Tenant, tenant_id=pk)
tenant = get_object_or_404(Tenant, pk=pk)
tenant.is_active = False
tenant.save()
@ -1041,14 +1046,16 @@ def deactivate_tenant(request, pk):
return redirect('core:tenant_detail', pk=pk)
@login_required
def reset_system_configuration(request):
"""
Reset system configuration to defaults.
"""
tenant = request.user.tenant
if request.method == 'POST':
# Reset configurations for the tenant
SystemConfiguration.objects.filter(
tenant=request.user.tenant
tenant=tenant
).delete()
messages.success(request, 'System configuration has been reset to defaults.')
@ -1057,13 +1064,12 @@ def reset_system_configuration(request):
return render(request, 'core/configurations/reset_configuration_confirm.html')
@login_required
def export_audit_log(request):
"""
Export audit log to CSV.
"""
import csv
from django.http import HttpResponse
tenant = request.user.tenant
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="audit_log.csv"'
@ -1071,7 +1077,7 @@ def export_audit_log(request):
writer.writerow(['Timestamp', 'User', 'Action', 'Object Type', 'Object ID', 'Changes'])
logs = AuditLog.objects.filter(
tenant=request.user.tenant
tenant=tenant
).order_by('-timestamp')
for log in logs:
@ -1087,7 +1093,7 @@ def export_audit_log(request):
return response
class CoreSearchView(ListView):
class CoreSearchView(LoginRequiredMixin, ListView):
"""
Generic search view for core models.
"""
@ -1132,6 +1138,7 @@ class CoreSearchView(ListView):
return context
@login_required
def tenant_search(request):
"""
AJAX search for tenants.
@ -1147,6 +1154,7 @@ def tenant_search(request):
return JsonResponse({'tenants': list(tenants)})
@login_required
def bulk_activate_tenants(request):
"""
Bulk activate tenants.
@ -1162,6 +1170,7 @@ def bulk_activate_tenants(request):
return redirect('core:tenant_list')
@login_required
def bulk_deactivate_tenants(request):
"""
Bulk deactivate tenants.
@ -1177,12 +1186,12 @@ def bulk_deactivate_tenants(request):
return redirect('core:tenant_list')
@login_required
def bulk_export_audit_logs(request):
"""
Bulk export audit logs.
"""
import csv
from django.http import HttpResponse
tenant = request.user.tenant
if request.method == 'POST':
log_ids = request.POST.getlist('log_ids')
@ -1194,7 +1203,7 @@ def bulk_export_audit_logs(request):
writer.writerow(['Timestamp', 'User', 'Action', 'Object Type', 'Object ID', 'Changes'])
logs = AuditLog.objects.filter(
tenant=request.user.tenant,
tenant=tenant,
log_id__in=log_ids
).order_by('-timestamp')
@ -1213,6 +1222,7 @@ def bulk_export_audit_logs(request):
return redirect('core:audit_log_list')
@login_required
def validate_tenant_data(request):
"""
AJAX validation for tenant data.
@ -1229,6 +1239,7 @@ def validate_tenant_data(request):
return JsonResponse({'valid': len(errors) == 0, 'errors': errors})
@login_required
def get_system_status(request):
"""
Get system status information.
@ -1254,19 +1265,19 @@ def get_system_status(request):
return JsonResponse(status)
@login_required
def backup_configuration(request):
"""
Backup system configuration.
"""
import json
from django.http import HttpResponse
tenant = request.user.tenant
configurations = SystemConfiguration.objects.filter(
tenant=request.user.tenant
tenant=tenant
).values('key', 'value', 'description')
backup_data = {
'tenant': str(request.user.tenant.tenant_id),
'tenant': str(tenant.tenant_id),
'timestamp': timezone.now().isoformat(),
'configurations': list(configurations)
}
@ -1280,11 +1291,12 @@ def backup_configuration(request):
return response
@login_required
def restore_configuration(request):
"""
Restore system configuration from backup.
"""
import json
tenant = request.user.tenant
if request.method == 'POST':
backup_file = request.FILES.get('backup_file')
@ -1295,13 +1307,13 @@ def restore_configuration(request):
# Clear existing configurations
SystemConfiguration.objects.filter(
tenant=request.user.tenant
tenant=tenant
).delete()
# Restore configurations
for config in backup_data.get('configurations', []):
SystemConfiguration.objects.create(
tenant=request.user.tenant,
tenant=tenant,
key=config['key'],
value=config['value'],
description=config.get('description', '')
@ -1318,6 +1330,78 @@ def restore_configuration(request):
return render(request, 'core/restore_configuration.html')
@login_required
def notification_search(request):
"""API endpoint for searching notifications."""
user = request.user
tenant = getattr(user, 'tenant', None)
# Get search parameters
search_term = request.GET.get('term', '')
notification_type = request.GET.get('notification_type', '')
is_active = request.GET.get('is_active', '')
# Base query
if user.is_superuser:
query = SystemNotification.objects.all()
elif tenant:
query = SystemNotification.objects.filter(
Q(tenant=tenant) | Q(tenant__isnull=True)
)
else:
query = SystemNotification.objects.filter(tenant__isnull=True)
# Apply filters
if search_term:
query = query.filter(
Q(title__icontains=search_term) |
Q(message__icontains=search_term)
)
if notification_type:
query = query.filter(notification_type=notification_type)
if is_active:
is_active_bool = is_active.lower() == 'true'
query = query.filter(is_active=is_active_bool)
# Only include currently visible notifications
if request.GET.get('visible_only', '') == 'true':
now = timezone.now()
query = query.filter(
is_active=True,
start_date__lte=now,
)
# Get results
results = query.order_by('-start_date')[:20]
# Format results
formatted_results = []
for notification in results:
# Check if this notification should be visible to the user
if request.GET.get('check_visibility', '') == 'true':
if not notification.is_visible_to_user(user):
continue
formatted_results.append({
'id': str(notification.notification_id),
'title': notification.title,
'message': notification.message[:100] + '...' if len(notification.message) > 100 else notification.message,
'type': notification.notification_type,
'type_display': notification.get_notification_type_display(),
'is_active': notification.is_active,
'is_dismissible': notification.is_dismissible,
'start_date': notification.start_date.isoformat(),
'end_date': notification.end_date.isoformat() if notification.end_date else None,
'url': reverse('core:system_notification_detail', args=[notification.notification_id])
})
return JsonResponse({
'status': 'success',
'results': formatted_results,
'count': len(formatted_results)
})
@ -1333,7 +1417,7 @@ def restore_configuration(request):
# paginate_by = 20
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return Department.objects.none()
#
@ -1365,7 +1449,7 @@ def restore_configuration(request):
# context_object_name = 'department'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
@ -1399,7 +1483,7 @@ def restore_configuration(request):
#
# def form_valid(self, form):
# # Set tenant
# form.instance.tenant = getattr(self.request, 'tenant', None)
# form.instance.tenant = self.request.user.tenant
# response = super().form_valid(form)
#
# # Log department creation
@ -1428,7 +1512,7 @@ def restore_configuration(request):
# permission_required = 'core.change_department'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
@ -1465,7 +1549,7 @@ def restore_configuration(request):
# success_url = reverse_lazy('core:department_list')
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# tenant = self.request.user.tenant
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
@ -3442,82 +3526,11 @@ def restore_configuration(request):
# 'status': 'error',
# 'message': _('Configuration not found.')
# }, status=404)
#
#
# @login_required
# def notification_search(request):
# """API endpoint for searching notifications."""
# user = request.user
# tenant = getattr(user, 'tenant', None)
#
# # Get search parameters
# search_term = request.GET.get('term', '')
# notification_type = request.GET.get('notification_type', '')
# is_active = request.GET.get('is_active', '')
#
# # Base query
# if user.is_superuser:
# query = SystemNotification.objects.all()
# elif tenant:
# query = SystemNotification.objects.filter(
# Q(tenant=tenant) | Q(tenant__isnull=True)
# )
# else:
# query = SystemNotification.objects.filter(tenant__isnull=True)
#
# # Apply filters
# if search_term:
# query = query.filter(
# Q(title__icontains=search_term) |
# Q(message__icontains=search_term)
# )
#
# if notification_type:
# query = query.filter(notification_type=notification_type)
#
# if is_active:
# is_active_bool = is_active.lower() == 'true'
# query = query.filter(is_active=is_active_bool)
#
# # Only include currently visible notifications
# if request.GET.get('visible_only', '') == 'true':
# now = timezone.now()
# query = query.filter(
# is_active=True,
# start_date__lte=now,
# )
#
# # Get results
# results = query.order_by('-start_date')[:20]
#
# # Format results
# formatted_results = []
# for notification in results:
# # Check if this notification should be visible to the user
# if request.GET.get('check_visibility', '') == 'true':
# if not notification.is_visible_to_user(user):
# continue
#
# formatted_results.append({
# 'id': str(notification.notification_id),
# 'title': notification.title,
# 'message': notification.message[:100] + '...' if len(notification.message) > 100 else notification.message,
# 'type': notification.notification_type,
# 'type_display': notification.get_notification_type_display(),
# 'is_active': notification.is_active,
# 'is_dismissible': notification.is_dismissible,
# 'start_date': notification.start_date.isoformat(),
# 'end_date': notification.end_date.isoformat() if notification.end_date else None,
# 'url': reverse('core:system_notification_detail', args=[notification.notification_id])
# })
#
# return JsonResponse({
# 'status': 'success',
# 'results': formatted_results,
# 'count': len(formatted_results)
# })
#
#
# @login_required
# def department_search(request):
# """API endpoint for searching departments."""

View File

@ -334,7 +334,7 @@ def create_saudi_audit_logs(tenants, count_per_tenant=100):
'vital_signs_recorded', 'medication_administered'
]
risk_levels = AuditLogEntry.RISK_LEVEL_CHOICES
risk_levels = AuditLogEntry.RiskLevel.choices
for tenant in tenants:
for _ in range(count_per_tenant):
@ -380,7 +380,7 @@ def create_saudi_integration_logs(tenants, count_per_tenant=50):
"""Create Saudi-specific integration logs"""
integration_logs = []
saudi_integrations = IntegrationLog.INTEGRATION_TYPE_CHOICES
saudi_integrations = IntegrationLog.IntegrationType.choices
message_types = [
'patient_eligibility', 'claim_submission', 'payment_notification',

6
data_utils/__init__.py Normal file
View File

@ -0,0 +1,6 @@
"""
Shared utilities for data generation scripts.
Provides common constants, generators, and helper functions.
"""
__version__ = "1.0.0"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

271
data_utils/base.py Normal file
View File

@ -0,0 +1,271 @@
"""
Base data generator class for Saudi healthcare data.
Provides common functionality and patterns for all data generators.
"""
import os
import django
from datetime import datetime, timedelta
from django.utils import timezone as django_timezone
from django.db import transaction
from .constants import *
from .generators import *
from .helpers import *
class BaseDataGenerator:
"""Base class for Saudi healthcare data generators"""
def __init__(self, tenant=None):
"""Initialize the data generator"""
self.tenant = tenant
self.progress_tracker = ProgressTracker()
def setup_django(self):
"""Setup Django environment if not already done"""
if not hasattr(django, 'apps') or not django.apps.apps_ready:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
django.setup()
def validate_dependencies(self):
"""Validate that required dependencies exist"""
return validate_dependencies()
def get_tenants(self):
"""Get tenants for data generation"""
from core.models import Tenant
if self.tenant:
return [self.tenant]
tenants = list(Tenant.objects.filter(is_active=True))
if not tenants:
raise ValueError("No active tenants found")
return tenants
def get_tenant_data(self, tenant):
"""Get common tenant data needed for generation"""
return {
'users': get_tenant_users(tenant),
'providers': get_tenant_providers(tenant),
'patients': get_tenant_patients(tenant),
'departments': get_tenant_departments(tenant)
}
def safe_bulk_create(self, Model, objects, **kwargs):
"""Safe bulk create with error handling"""
return safe_bulk_create(Model, objects, **kwargs)
def safe_create(self, Model, **kwargs):
"""Safe create with error handling"""
return safe_create(Model, **kwargs)
def print_progress(self, message=""):
"""Print current progress"""
self.progress_tracker.print_progress(message)
def increment_progress(self, count=1):
"""Increment progress counter"""
self.progress_tracker.increment(count)
def generate_summary(self, results):
"""Generate summary of generation results"""
total_created = sum(results.values()) if isinstance(results, dict) else len(results)
return {
'total_created': total_created,
'details': results,
'timestamp': django_timezone.now(),
'tenant': self.tenant.name if self.tenant else 'All Tenants'
}
def run_generation(self, **kwargs):
"""Main generation method - to be implemented by subclasses"""
raise NotImplementedError("Subclasses must implement run_generation method")
class SaudiHealthcareDataGenerator(BaseDataGenerator):
"""Base class for Saudi healthcare data generators with common Saudi-specific functionality"""
def __init__(self, tenant=None):
super().__init__(tenant)
self.setup_django()
def generate_saudi_name(self, gender=None):
"""Generate a Saudi name"""
return generate_saudi_name(gender)
def generate_saudi_address(self):
"""Generate a Saudi address"""
return generate_saudi_address()
def generate_saudi_phone(self):
"""Generate a Saudi phone number"""
return generate_saudi_phone()
def generate_saudi_mobile(self):
"""Generate a Saudi mobile number"""
return generate_saudi_mobile_e164()
def generate_birth_date(self, min_age=1, max_age=85):
"""Generate birth date within age range"""
return generate_birth_date(min_age, max_age)
def generate_hire_date(self, max_years_ago=20):
"""Generate hire date within reasonable range"""
return generate_hire_date(max_years_ago)
def generate_future_date(self, days_ahead=365):
"""Generate future date"""
return generate_future_date(days_ahead)
def generate_past_date(self, days_back=365):
"""Generate past date"""
return generate_past_date(days_back)
def generate_vital_signs(self, patient_age=None):
"""Generate realistic vital signs"""
return generate_vital_signs(patient_age)
def pick_job_title(self, department):
"""Pick appropriate job title for department"""
return pick_job_title_for_department(department)
def infer_role(self, job_title):
"""Infer role from job title"""
return infer_role_from_title(job_title)
def tenant_unique_username(self, tenant, base_username):
"""Generate tenant-unique username"""
return tenant_scoped_unique_username(tenant, base_username)
def generate_lab_values(self, test_type='QUANTITATIVE', reference_range=None):
"""Generate lab values"""
return generate_lab_values(test_type, reference_range)
class DataGenerationOrchestrator:
"""Orchestrates the execution of multiple data generators with dependency management"""
def __init__(self):
self.generators = {}
self.execution_order = [
'core', # Tenants
'accounts', # Users
'hr', # Employees/Departments
'patients', # Patients
# Clinical modules (can run in parallel)
'emr',
'lab',
'radiology',
'pharmacy',
'appointments',
'billing',
'inpatients',
'inventory',
'facility_management'
]
def register_generator(self, name, generator_class, **kwargs):
"""Register a data generator"""
self.generators[name] = {
'class': generator_class,
'kwargs': kwargs,
'instance': None
}
def get_dependencies(self, generator_name):
"""Get dependencies for a generator"""
dependencies = {
'core': [],
'accounts': ['core'],
'hr': ['core', 'accounts'],
'patients': ['core'],
'emr': ['core', 'accounts', 'hr', 'patients'],
'lab': ['core', 'accounts', 'hr', 'patients'],
'radiology': ['core', 'accounts', 'hr', 'patients'],
'pharmacy': ['core', 'accounts', 'hr', 'patients'],
'appointments': ['core', 'accounts', 'hr', 'patients'],
'billing': ['core', 'accounts', 'patients'],
'inpatients': ['core', 'accounts', 'hr', 'patients'],
'inventory': ['core'],
'facility_management': ['core']
}
return dependencies.get(generator_name, [])
def validate_dependencies(self, generator_name, completed_generators):
"""Validate that dependencies are satisfied"""
dependencies = self.get_dependencies(generator_name)
missing = [dep for dep in dependencies if dep not in completed_generators]
return len(missing) == 0, missing
def run_generator(self, name, **kwargs):
"""Run a specific generator"""
if name not in self.generators:
print(f"Generator {name} not registered")
return None
generator_config = self.generators[name]
generator_class = generator_config['class']
generator_kwargs = {**generator_config['kwargs'], **kwargs}
try:
generator = generator_class(**generator_kwargs)
results = generator.run_generation()
return results
except Exception as e:
print(f"Error running generator {name}: {e}")
return None
def run_all(self, generators_to_run=None, **kwargs):
"""Run all generators in dependency order"""
if generators_to_run is None:
generators_to_run = self.execution_order
completed = set()
results = {}
for generator_name in generators_to_run:
if generator_name not in self.generators:
print(f"Skipping {generator_name} - not registered")
continue
# Validate dependencies
valid, missing = self.validate_dependencies(generator_name, completed)
if not valid:
print(f"Skipping {generator_name} - missing dependencies: {missing}")
continue
print(f"\n🚀 Running {generator_name} generator...")
result = self.run_generator(generator_name, **kwargs)
if result is not None:
results[generator_name] = result
completed.add(generator_name)
print(f"{generator_name} completed")
else:
print(f"{generator_name} failed")
return results
def get_available_generators(self):
"""Get list of available generators"""
return list(self.generators.keys())
def get_execution_plan(self, generators_to_run=None):
"""Get execution plan with dependencies"""
if generators_to_run is None:
generators_to_run = self.execution_order
plan = []
for generator_name in generators_to_run:
if generator_name in self.generators:
dependencies = self.get_dependencies(generator_name)
plan.append({
'name': generator_name,
'dependencies': dependencies,
'class': self.generators[generator_name]['class'].__name__
})
return plan

328
data_utils/constants.py Normal file
View File

@ -0,0 +1,328 @@
"""
Shared constants for Saudi healthcare data generation.
Contains all common Saudi-specific data used across multiple modules.
"""
# ================================
# SAUDI NAMES AND DEMOGRAPHICS
# ================================
SAUDI_FIRST_NAMES_MALE = [
'Mohammed', 'Ahmed', 'Abdullah', 'Omar', 'Ali', 'Khalid', 'Fahd', 'Saad',
'Faisal', 'Saud', 'Abdulrahman', 'Abdulaziz', 'Turki', 'Bandar', 'Nasser',
'Saad', 'Majed', 'Waleed', 'Yousef', 'Ibrahim', 'Hassan', 'Hussein'
]
SAUDI_FIRST_NAMES_FEMALE = [
'Fatima', 'Aisha', 'Khadija', 'Maryam', 'Zahra', 'Sarah', 'Nora', 'Hala',
'Reem', 'Layla', 'Amina', 'Nadia', 'Rana', 'Dina', 'Hind', 'Najla',
'Arwa', 'Ghada', 'Nouf', 'Lama', 'Rania', 'Dana', 'Lina', 'Maha'
]
SAUDI_FAMILY_NAMES = [
'Al-Saud', 'Al-Rashid', 'Al-Mutairi', 'Al-Otaibi', 'Al-Qarni', 'Al-Harbi',
'Al-Dawsari', 'Al-Subai', 'Al-Sharani', 'Al-Ghamdi', 'Al-Zahrani', 'Al-Maliki',
'Al-Shehri', 'Al-Qahtani', 'Al-Ansari', 'Al-Hakim', 'Al-Rashidi', 'Al-Bakr'
]
# ================================
# SAUDI GEOGRAPHIC DATA
# ================================
SAUDI_CITIES = [
'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran',
'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hail', 'Najran'
]
SAUDI_PROVINCES = [
'Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province',
'Jazan Province', 'Madinah Province', 'Qassim Province', 'Tabuk Province'
]
SAUDI_STATES = SAUDI_PROVINCES # Alias for backward compatibility
# ================================
# SAUDI MEDICAL DATA
# ================================
SAUDI_MEDICAL_SPECIALTIES = [
'INTERNAL_MEDICINE', 'CARDIOLOGY', 'ORTHOPEDICS', 'NEUROLOGY', 'ONCOLOGY',
'PEDIATRICS', 'EMERGENCY_MEDICINE', 'RADIOLOGY', 'LABORATORY_MEDICINE',
'PHARMACY', 'SURGERY', 'OBSTETRICS_GYNECOLOGY', 'DERMATOLOGY',
'OPHTHALMOLOGY', 'ENT', 'ANESTHESIOLOGY', 'PATHOLOGY', 'PSYCHIATRY'
]
SAUDI_DEPARTMENTS = [
('EMERGENCY', 'Emergency Department', 'Emergency medical services'),
('ICU', 'Intensive Care Unit', 'Critical care services'),
('CARDIOLOGY', 'Cardiology Department', 'Heart and cardiovascular care'),
('SURGERY', 'General Surgery', 'Surgical services'),
('ORTHOPEDICS', 'Orthopedics Department', 'Bone and joint care'),
('PEDIATRICS', 'Pediatrics Department', 'Children healthcare'),
('OBSTETRICS', 'Obstetrics & Gynecology', 'Women and maternity care'),
('RADIOLOGY', 'Radiology Department', 'Medical imaging services'),
('LABORATORY', 'Laboratory Services', 'Diagnostic testing'),
('PHARMACY', 'Pharmacy Department', 'Medication services'),
('NURSING', 'Nursing Services', 'Patient care services'),
('ADMINISTRATION', 'Administration', 'Hospital administration'),
('FINANCE', 'Finance Department', 'Financial management'),
('HR', 'Human Resources', 'Staff management'),
('IT', 'Information Technology', 'Technology services'),
('MAINTENANCE', 'Maintenance Services', 'Facility maintenance'),
('SECURITY', 'Security Department', 'Hospital security'),
('HOUSEKEEPING', 'Housekeeping Services', 'Cleaning services'),
('FOOD_SERVICE', 'Food Services', 'Dietary services'),
('SOCIAL_WORK', 'Social Work', 'Patient social services')
]
SAUDI_JOB_TITLES = {
'PHYSICIAN': ['Consultant Physician', 'Senior Physician', 'Staff Physician', 'Resident Physician',
'Chief Medical Officer'],
'NURSE': ['Head Nurse', 'Senior Nurse', 'Staff Nurse', 'Charge Nurse', 'Clinical Nurse Specialist'],
'PHARMACIST': ['Clinical Pharmacist', 'Staff Pharmacist', 'Pharmacy Manager', 'Pharmaceutical Consultant'],
'ADMIN': ['Medical Director', 'Hospital Administrator', 'Department Manager', 'Operations Manager'],
'LAB_TECH': ['Senior Lab Technician', 'Medical Laboratory Scientist', 'Lab Supervisor'],
'RAD_TECH': ['Senior Radiologic Technologist', 'CT Technologist', 'MRI Technologist'],
'RADIOLOGIST': ['Consultant Radiologist', 'Senior Radiologist', 'Interventional Radiologist'],
'MEDICAL_ASSISTANT': ['Medical Assistant'],
'CLERICAL': ['Clerical Staff'],
}
# ================================
# SAUDI MEDICAL CONSTANTS
# ================================
SAUDI_COMMON_DIAGNOSES = [
'Diabetes Mellitus Type 2', 'Hypertension', 'Coronary Artery Disease',
'Chronic Kidney Disease', 'Heart Failure', 'Pneumonia', 'Gastritis',
'Cholecystitis', 'Appendicitis', 'Urinary Tract Infection',
'Acute Myocardial Infarction', 'Stroke', 'Pulmonary Embolism',
'Deep Vein Thrombosis', 'Sepsis', 'Acute Renal Failure',
'Chronic Obstructive Pulmonary Disease', 'Asthma', 'Anemia'
]
SAUDI_SURGICAL_PROCEDURES = [
'Laparoscopic Cholecystectomy', 'Open Heart Surgery', 'Total Knee Replacement',
'Inguinal Hernia Repair', 'Appendectomy', 'Diagnostic Laparoscopy',
'Thyroidectomy', 'Cataract Surgery', 'Tonsillectomy', 'Hemorrhoidectomy'
]
SAUDI_MEDICATIONS = [
('Paracetamol', 'باراسيتامول', 'TAB', 'Analgesic', '500mg', ['J01XX01']),
('Ibuprofen', 'ايبوبروفين', 'TAB', 'NSAID', '400mg', ['M01AE01']),
('Metformin', 'ميتفورمين', 'TAB', 'Antidiabetic', '500mg', ['A10BA02']),
('Amlodipine', 'أملوديبين', 'TAB', 'Antihypertensive', '5mg', ['C08CA01']),
('Atorvastatin', 'أتورفاستاتين', 'TAB', 'Statin', '20mg', ['C10AA05']),
('Omeprazole', 'أوميبرازول', 'CAP', 'PPI', '20mg', ['A02BC01']),
('Salbutamol', 'سالبيوتامول', 'INH', 'Bronchodilator', '100mcg', ['R03AC02']),
('Warfarin', 'وارفارين', 'TAB', 'Anticoagulant', '5mg', ['B01AA03']),
('Insulin Glargine', 'انسولين جلارجين', 'INJ', 'Insulin', '100units/ml', ['A10AE04']),
('Levothyroxine', 'ليفوثيروكسين', 'TAB', 'Thyroid hormone', '100mcg', ['H03AA01']),
('Losartan', 'لوسارتان', 'TAB', 'ARB', '50mg', ['C09CA01']),
('Pantoprazole', 'بانتوبرازول', 'TAB', 'PPI', '40mg', ['A02BC02']),
('Clopidogrel', 'كلوبيدوجريل', 'TAB', 'Antiplatelet', '75mg', ['B01AC04']),
('Simvastatin', 'سيمفاستاتين', 'TAB', 'Statin', '20mg', ['C10AA01']),
('Furosemide', 'فوروسيمايد', 'TAB', 'Diuretic', '40mg', ['C03CA01']),
('Prednisone', 'بريدنيزون', 'TAB', 'Corticosteroid', '5mg', ['H02AB07']),
('Azithromycin', 'أزيثروميسين', 'TAB', 'Antibiotic', '250mg', ['J01FA10']),
('Ciprofloxacin', 'سيبروفلوكساسين', 'TAB', 'Antibiotic', '500mg', ['J01MA02']),
('Gabapentin', 'جابابنتين', 'CAP', 'Anticonvulsant', '300mg', ['N03AX12']),
('Tramadol', 'ترامادول', 'TAB', 'Opioid analgesic', '50mg', ['N02AX02']),
]
SAUDI_ALLERGIES = [
'Penicillin', 'Aspirin', 'Ibuprofen', 'Sulfa drugs', 'Contrast dye',
'Peanuts', 'Shellfish', 'Dairy', 'Eggs', 'Tree nuts', 'Latex'
]
SAUDI_INSURANCE_COMPANIES = [
'Saudi Enaya Cooperative Insurance', 'Bupa Arabia', 'Tawuniya',
'Malath Insurance', 'Walaa Insurance', 'Gulf Union Alahlia Insurance'
]
SAUDI_CHIEF_COMPLAINTS = [
'Chest pain and shortness of breath',
'Abdominal pain and nausea',
'Headache and dizziness',
'Back pain and stiffness',
'Fever and cough',
'Joint pain and swelling',
'Fatigue and weakness',
'Skin rash and itching',
'Diabetes follow-up',
'Hypertension monitoring',
'Regular health checkup',
'Vaccination appointment',
'Pre-operative consultation',
'Post-operative follow-up',
'Pregnancy consultation',
'Child wellness visit',
'Mental health consultation',
'Physical therapy session',
'Diagnostic imaging',
'Laboratory test follow-up',
'Cardiac symptoms evaluation',
'Respiratory symptoms',
'Gastrointestinal complaints',
'Neurological symptoms',
'Endocrine disorders follow-up'
]
SAUDI_LOCATIONS = [
'Main Building - Floor 1',
'Main Building - Floor 2',
'Main Building - Floor 3',
'Emergency Wing',
'Outpatient Clinic - Wing A',
'Outpatient Clinic - Wing B',
'Surgical Suite - Floor 4',
'Radiology Department',
'Laboratory Building',
'Pediatric Wing',
'ICU - Floor 5',
'Cardiology Unit',
'Maternity Ward',
'Dialysis Center'
]
# ================================
# SAUDI TRAINING AND EDUCATION
# ================================
SAUDI_TRAINING_PROGRAMS = [
'Basic Life Support (BLS)', 'Advanced Cardiac Life Support (ACLS)',
'Pediatric Advanced Life Support (PALS)', 'Infection Control',
'Patient Safety', 'Fire Safety', 'Emergency Procedures',
'HIPAA Compliance', 'Cultural Sensitivity', 'Arabic Language',
'Islamic Healthcare Ethics', 'Medication Administration',
'Wound Care Management', 'Electronic Health Records',
'Quality Improvement', 'Customer Service Excellence'
]
# ================================
# SAUDI FACILITY MANAGEMENT
# ================================
SAUDI_MEDICAL_MANUFACTURERS = [
'Saudi Pharmaceutical Industries (SPIMACO)',
'Tabuk Pharmaceuticals',
'Al-Jazeera Pharmaceutical Industries',
'Medical Supplies Company',
'Saudi Medical Supplies',
'Gulf Pharmaceutical Industries',
'Middle East Healthcare',
'Arabian Medical Equipment',
'Riyadh Pharma',
'Johnson & Johnson Saudi',
'Pfizer Saudi Arabia',
'Novartis Saudi',
'Roche Saudi Arabia',
'Abbott Saudi Arabia'
]
SAUDI_STORAGE_LOCATIONS = {
'buildings': ['Main Hospital', 'Outpatient Clinic', 'Emergency Wing', 'Research Center', 'Administrative Building'],
'floors': ['Ground Floor', 'First Floor', 'Second Floor', 'Third Floor', 'Basement'],
'rooms': ['Pharmacy', 'Central Supply', 'OR Storage', 'ICU Supply', 'Ward Storage', 'Emergency Supply'],
'zones': ['Zone A', 'Zone B', 'Zone C', 'Zone D'],
'aisles': ['Aisle 1', 'Aisle 2', 'Aisle 3', 'Aisle 4', 'Aisle 5'],
'shelves': ['Shelf A', 'Shelf B', 'Shelf C', 'Shelf D', 'Shelf E'],
'bins': ['Bin 1', 'Bin 2', 'Bin 3', 'Bin 4', 'Bin 5']
}
SAUDI_SUPPLIER_DATA = [
{
'name': 'Saudi Medical Supply Co.',
'type': 'DISTRIBUTOR',
'city': 'Riyadh',
'phone': '+966-11-234-5678'
},
{
'name': 'Gulf Medical Equipment',
'type': 'MANUFACTURER',
'city': 'Dammam',
'phone': '+966-13-345-6789'
},
{
'name': 'Arabian Healthcare Supplies',
'type': 'WHOLESALER',
'city': 'Jeddah',
'phone': '+966-12-456-7890'
},
{
'name': 'Riyadh Medical Trading',
'type': 'DISTRIBUTOR',
'city': 'Riyadh',
'phone': '+966-11-567-8901'
},
{
'name': 'Al-Dawaa Medical',
'type': 'MANUFACTURER',
'city': 'Medina',
'phone': '+966-14-678-9012'
},
{
'name': 'Nahdi Medical Company',
'type': 'RETAILER',
'city': 'Jeddah',
'phone': '+966-12-789-0123'
},
{
'name': 'United Pharmaceuticals',
'type': 'MANUFACTURER',
'city': 'Khobar',
'phone': '+966-13-890-1234'
},
{
'name': 'Middle East Medical',
'type': 'DISTRIBUTOR',
'city': 'Taif',
'phone': '+966-12-901-2345'
},
{
'name': 'Kingdom Medical Supplies',
'type': 'WHOLESALER',
'city': 'Buraidah',
'phone': '+966-16-012-3456'
},
{
'name': 'Eastern Province Medical Co.',
'type': 'DISTRIBUTOR',
'city': 'Dhahran',
'phone': '+966-13-123-4567'
}
]
# ================================
# ROLE DISTRIBUTION PATTERNS
# ================================
ROLE_DISTRIBUTION = {
'PHYSICIAN': 0.15,
'NURSE': 0.25,
'PHARMACIST': 0.08,
'LAB_TECH': 0.10,
'RAD_TECH': 0.08,
'RADIOLOGIST': 0.05,
'ADMIN': 0.07,
'MEDICAL_ASSISTANT': 0.12,
'CLERICAL': 0.10
}
# ================================
# LICENSE PREFIXES
# ================================
SAUDI_LICENSE_PREFIXES = ['MOH', 'SCFHS', 'SMLE', 'SFH']
# ================================
# HOSPITAL NAMES
# ================================
SAUDI_HOSPITALS = [
'King Faisal Specialist Hospital', 'King Fahd Medical City', 'National Guard Health Affairs',
'Prince Sultan Military Medical City', 'King Abdulaziz Medical City', 'King Saud Medical City',
'Dr. Sulaiman Al Habib Medical Group', 'Saudi German Hospital', 'International Medical Center',
'King Khalid University Hospital', 'King Abdulaziz University Hospital',
'Prince Mohammed bin Abdulaziz Hospital', 'King Fahd Hospital', 'Imam Abdulrahman Bin Faisal Hospital'
]

357
data_utils/generators.py Normal file
View File

@ -0,0 +1,357 @@
"""
Shared data generation functions for Saudi healthcare data.
Contains common generators used across multiple data generation modules.
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
try:
django.setup()
except Exception as e:
print(f"Django setup failed: {e}")
print("Please run this script using: python manage.py shell -c \"exec(open('hr_data.py').read())\"")
exit(1)
import random
import uuid
from datetime import datetime, timedelta, date
from decimal import Decimal
from django.utils import timezone as django_timezone
from django.contrib.auth import get_user_model
from .constants import (
SAUDI_FIRST_NAMES_MALE, SAUDI_FIRST_NAMES_FEMALE, SAUDI_FAMILY_NAMES,
SAUDI_CITIES, SAUDI_PROVINCES, SAUDI_LICENSE_PREFIXES, SAUDI_JOB_TITLES
)
from accounts.models import User
# ================================
# PHONE AND ID GENERATORS
# ================================
def generate_saudi_mobile_e164():
"""Generate Saudi E.164 mobile: +9665XXXXXXXX"""
return f"+9665{random.randint(10000000, 99999999)}"
def generate_saudi_phone():
"""Generate Saudi phone number"""
return f"+966{random.randint(11, 17)}{random.randint(1000000, 9999999)}"
def generate_saudi_license():
"""Generate Saudi medical license number (fictional format)"""
prefix = random.choice(SAUDI_LICENSE_PREFIXES)
return f"{prefix}-{random.randint(100000, 999999)}"
def generate_saudi_medical_record_number():
"""Generate medical record number"""
return f"MRN{random.randint(100000, 999999)}"
def generate_saudi_item_code():
"""Generate Saudi medical item code"""
return f"SAU-{random.randint(100000, 999999)}"
def generate_saudi_lot_number():
"""Generate Saudi lot number"""
return f"LOT{random.randint(2024, 2025)}{random.randint(100, 999)}"
def generate_saudi_po_number():
"""Generate Saudi purchase order number"""
return f"PO-{random.randint(2024, 2025)}-{random.randint(1000, 9999)}"
# ================================
# NAME AND USERNAME GENERATORS
# ================================
def generate_saudi_name(gender=None):
"""Generate a complete Saudi name"""
if gender is None:
gender = random.choice(['MALE', 'FEMALE'])
if gender == 'MALE':
first_name = random.choice(SAUDI_FIRST_NAMES_MALE)
else:
first_name = random.choice(SAUDI_FIRST_NAMES_FEMALE)
last_name = random.choice(SAUDI_FAMILY_NAMES)
return {
'first_name': first_name,
'last_name': last_name,
'full_name': f"{first_name} {last_name}"
}
def tenant_scoped_unique_username(tenant, base_username: str) -> str:
"""
Make username unique within a tenant (AUTH is tenant-scoped).
"""
uname = base_username
i = 1
while User.objects.filter(tenant=tenant, username=uname).exists():
i += 1
uname = f"{base_username}{i}"
return uname
def generate_saudi_email(first_name, last_name, domain="email.com"):
"""Generate Saudi-style email address"""
return f"{first_name.lower()}.{last_name.lower().replace('-', '').replace(' ', '')}@{domain}"
# ================================
# ADDRESS GENERATORS
# ================================
def generate_saudi_address():
"""Generate a complete Saudi address"""
return {
'address_line_1': f"{random.randint(1, 999)} {random.choice(['King Fahd Road', 'Prince Sultan Street', 'Al Malik Road'])}",
'address_line_2': f"Apt {random.randint(1, 50)}" if random.choice([True, False]) else None,
'city': random.choice(SAUDI_CITIES),
'state': random.choice(SAUDI_PROVINCES),
'postal_code': f"{random.randint(10000, 99999)}",
'country': 'Saudi Arabia'
}
# ================================
# MEDICAL DATA GENERATORS
# ================================
def pick_job_title_for_department(department) -> str:
"""Pick appropriate job title based on department type"""
dtype = getattr(department, 'department_type', 'ADMINISTRATIVE') or 'ADMINISTRATIVE'
if dtype == 'CLINICAL':
return random.choice([
'Consultant Physician', 'Senior Physician', 'Physician', 'Resident Physician', 'Intern',
'Chief Nurse', 'Nurse Manager', 'Senior Nurse', 'Staff Nurse',
'Nurse Practitioner', 'Clinical Nurse Specialist', 'Charge Nurse',
'Pharmacist', 'Clinical Pharmacist', 'Pharmacy Technician',
'Radiologist', 'Radiology Technician', 'Medical Technologist',
'Laboratory Technician', 'Physical Therapist', 'Respiratory Therapist'
])
elif dtype == 'SUPPORT':
return random.choice([
'Security Officer', 'Security Guard', 'Maintenance Technician',
'Housekeeping Supervisor', 'Housekeeper', 'Food Service Manager',
'Cook', 'Kitchen Assistant', 'Transport Aide', 'Receptionist'
])
else:
return random.choice([
'Chief Executive Officer', 'Chief Operating Officer', 'Administrator',
'Assistant Administrator', 'Department Manager', 'Supervisor',
'Administrative Assistant', 'Secretary', 'Clerk', 'Coordinator'
])
def infer_role_from_title(job_title: str) -> str:
"""Infer role from job title"""
jt = (job_title or '').lower()
if 'physician' in jt:
return 'PHYSICIAN'
if 'nurse practitioner' in jt:
return 'NURSE_PRACTITIONER'
if 'nurse' in jt:
return 'NURSE'
if 'pharmac' in jt:
return 'PHARMACIST'
if 'radiolog' in jt and 'techn' not in jt:
return 'RADIOLOGIST'
if 'radiolog' in jt and 'techn' in jt:
return 'RAD_TECH'
if 'laborator' in jt:
return 'LAB_TECH'
if any(k in jt for k in ['chief', 'director', 'manager', 'admin']):
return 'ADMIN'
return 'CLERICAL'
def generate_saudi_provider_email(first_name, last_name, tenant_domain):
"""Generate provider email for tenant"""
return f"{first_name.lower()}.{last_name.lower().replace('-', '')}@{tenant_domain}"
# ================================
# FINANCIAL GENERATORS
# ================================
def generate_saudi_tax_id():
"""Generate Saudi tax ID"""
return f"TAX-{random.randint(100000000, 999999999)}"
def generate_saudi_vat_number():
"""Generate Saudi VAT number (15 digits)"""
return f"{random.randint(10**14, 10**15 - 1)}"
def generate_saudi_crn():
"""Generate Saudi Commercial Registration Number"""
return f"{random.randint(10**9, 10**10 - 1)}"
# ================================
# TIME AND DATE GENERATORS
# ================================
def generate_birth_date(min_age=1, max_age=85):
"""Generate birth date within age range"""
birth_year = random.randint(1939, 2006)
birth_month = random.randint(1, 12)
birth_day = random.randint(1, 28)
return date(birth_year, birth_month, birth_day)
def generate_hire_date(max_years_ago=20):
"""Generate hire date within reasonable range"""
start_date = django_timezone.now().date() - timedelta(days=365 * max_years_ago)
end_date = django_timezone.now().date() - timedelta(days=30)
days_range = (end_date - start_date).days
random_days = random.randint(0, days_range)
return start_date + timedelta(days=random_days)
def generate_future_date(days_ahead=365):
"""Generate future date"""
return django_timezone.now().date() + timedelta(days=random.randint(1, days_ahead))
def generate_past_date(days_back=365):
"""Generate past date"""
return django_timezone.now().date() - timedelta(days=random.randint(1, days_back))
# ================================
# CLINICAL DATA GENERATORS
# ================================
def generate_vital_signs(patient_age=None):
"""Generate realistic vital signs based on age"""
if patient_age is None:
patient_age = random.randint(1, 85)
# Temperature (Celsius)
temperature = round(random.uniform(36.0, 39.5), 1)
# Blood pressure
if patient_age < 18:
systolic_bp = random.randint(90, 120)
diastolic_bp = random.randint(50, 80)
elif patient_age < 65:
systolic_bp = random.randint(100, 140)
diastolic_bp = random.randint(60, 90)
else:
systolic_bp = random.randint(110, 160)
diastolic_bp = random.randint(70, 100)
# Heart rate
if patient_age < 1:
heart_rate = random.randint(100, 160)
elif patient_age < 12:
heart_rate = random.randint(80, 120)
else:
heart_rate = random.randint(60, 100)
# Respiratory rate
if patient_age < 1:
respiratory_rate = random.randint(30, 60)
elif patient_age < 12:
respiratory_rate = random.randint(18, 30)
else:
respiratory_rate = random.randint(12, 20)
# Oxygen saturation
oxygen_saturation = random.randint(95, 100)
# Weight and height
if patient_age < 18:
weight = round(random.uniform(3.0, 70.0), 1)
height = round(random.uniform(50.0, 180.0), 1)
else:
weight = round(random.uniform(45.0, 150.0), 1)
height = round(random.uniform(150.0, 200.0), 1)
return {
'temperature': temperature,
'systolic_bp': systolic_bp,
'diastolic_bp': diastolic_bp,
'heart_rate': heart_rate,
'respiratory_rate': respiratory_rate,
'oxygen_saturation': oxygen_saturation,
'weight': weight,
'height': height
}
def generate_lab_values(test_type='QUANTITATIVE', reference_range=None):
"""Generate realistic lab values"""
if test_type == 'QUANTITATIVE' and reference_range:
# Generate value within or outside reference range
if random.random() < 0.8: # 80% normal
low = float(reference_range.get('range_low', 0))
high = float(reference_range.get('range_high', 100))
value = round(random.uniform(low, high), 2)
abnormal_flag = 'N'
else: # 20% abnormal
low = float(reference_range.get('range_low', 0))
high = float(reference_range.get('range_high', 100))
if random.choice([True, False]):
value = round(random.uniform(high * 1.1, high * 2.0), 2)
abnormal_flag = 'H'
else:
value = round(random.uniform(low * 0.1, low * 0.9), 2)
abnormal_flag = 'L'
else:
# Qualitative result
qualitative_results = ['Negative', 'Positive', 'Normal', 'Abnormal', 'Detected', 'Not Detected']
value = random.choice(qualitative_results)
abnormal_flag = 'A' if value in ['Positive', 'Abnormal', 'Detected'] else 'N'
return value, abnormal_flag
# ================================
# UTILITY FUNCTIONS
# ================================
def safe_choice(seq):
"""Safe choice that handles empty sequences"""
return random.choice(seq) if seq else None
def random_percentage():
"""Generate random percentage 0-100"""
return random.randint(0, 100)
def random_decimal(min_val, max_val, precision="0.01"):
"""Generate random decimal within range"""
q = Decimal(precision)
return Decimal(str(random.uniform(float(min_val), float(max_val)))).quantize(q)
def generate_uuid():
"""Generate UUID string"""
return str(uuid.uuid4())
def generate_unique_code(prefix, existing_codes=None):
"""Generate unique code with prefix"""
if existing_codes is None:
existing_codes = set()
while True:
code = f"{prefix}{random.randint(100000, 999999)}"
if code not in existing_codes:
return code

371
data_utils/helpers.py Normal file
View File

@ -0,0 +1,371 @@
"""
Database utilities and model helpers for data generation.
Contains common database operations and model field utilities.
"""
import random
from datetime import datetime, timedelta
from decimal import Decimal
from django.db import transaction, IntegrityError
from django.db.models import DecimalField, CharField, IntegerField
from django.db.models.fields import NOT_PROVIDED
from django.utils import timezone as django_timezone
# ================================
# MODEL FIELD UTILITIES
# ================================
def _model_fields(Model):
"""Get all concrete model fields"""
return {f.name for f in Model._meta.get_fields() if getattr(f, "concrete", False) and not f.auto_created}
def _filter_kwargs(Model, data: dict):
"""Filter dictionary to only include valid model fields"""
allowed = _model_fields(Model)
return {k: v for k, v in data.items() if k in allowed}
def _quantize_for_field(value: Decimal, f: DecimalField) -> Decimal:
"""Quantize a Decimal to a field's decimal_places and clamp to max_digits."""
# Quantize to required decimal_places
q = Decimal(1).scaleb(-f.decimal_places) # 10^(-decimal_places)
v = Decimal(value).quantize(q)
# Ensure total digits <= max_digits (digits before + decimal_places)
# Count digits before decimal:
sign, digits, exp = v.as_tuple()
digits_str_len = len(digits)
# number of digits after decimal is decimal_places
digits_before = digits_str_len - f.decimal_places if f.decimal_places else digits_str_len
# if v is 0.x and decimal_places > digits, digits_before can be negative; normalize
if digits_before < 0:
digits_before = 0
max_before = f.max_digits - f.decimal_places
if max_before < 0:
max_before = 0
# If too many digits before decimal, clamp to the largest representable value
if digits_before > max_before:
# Largest integer part we can store is 10^max_before - 1
max_int = (10 ** max_before) - 1 if max_before > 0 else 0
v = Decimal(max_int).quantize(q)
return v
def _default_decimal_for(BillingConfiguration, f: DecimalField) -> Decimal:
"""Return a safe default decimal per field name & precision."""
name = f.name.lower()
if any(k in name for k in ["tax", "vat", "rate"]):
# Prefer 15% if it's a rate; use 15 if integer percent
base = Decimal("15") if f.decimal_places == 0 else Decimal("0.15")
else:
base = Decimal("0")
return _quantize_for_field(base, f)
# ================================
# SEQUENTIAL NUMBER GENERATORS
# ================================
def _next_seq_number(prefix, Model, field):
"""Generate next sequential number for a model field"""
today = django_timezone.now().date().strftime("%Y%m%d")
i = 1
while True:
candidate = f"{prefix}-{today}-{i:04d}"
if not Model.objects.filter(**{field: candidate}).exists():
return candidate
i += 1
def _next_claim_number():
"""Generate next insurance claim number"""
from billing.models import InsuranceClaim
return _next_seq_number("CLM", InsuranceClaim, "claim_number")
def _next_payment_number():
"""Generate next payment number"""
from billing.models import Payment
return _next_seq_number("PMT", Payment, "payment_number")
def _next_bill_number():
"""Generate next bill number"""
from billing.models import MedicalBill
return _next_seq_number("BILL", MedicalBill, "bill_number")
def _next_case_number(tenant):
"""Generate next surgical case number"""
from operating_theatre.models import SurgicalCase
return f"CASE-{tenant.slug.upper()}-{django_timezone.now().year}-{random.randint(1000, 9999):05d}"
# ================================
# DATABASE OPERATIONS
# ================================
def safe_bulk_create(Model, objects, batch_size=1000, ignore_conflicts=False):
"""Safely bulk create objects with error handling"""
if not objects:
return 0
created_count = 0
for i in range(0, len(objects), batch_size):
batch = objects[i:i + batch_size]
try:
with transaction.atomic():
if ignore_conflicts:
Model.objects.bulk_create(batch, ignore_conflicts=True)
else:
Model.objects.bulk_create(batch)
created_count += len(batch)
except IntegrityError as e:
print(f"Integrity error in batch {i//batch_size + 1}: {e}")
# Try individual creates for problematic batch
for obj in batch:
try:
obj.save()
created_count += 1
except IntegrityError:
continue # Skip duplicates
except Exception as e:
print(f"Error in batch {i//batch_size + 1}: {e}")
continue
return created_count
def safe_get_or_create(Model, defaults=None, **kwargs):
"""Safe get_or_create with error handling"""
try:
return Model.objects.get_or_create(defaults=defaults or {}, **kwargs)
except Exception as e:
print(f"Error in get_or_create for {Model.__name__}: {e}")
return None, False
def safe_create(Model, **kwargs):
"""Safe create with error handling"""
try:
return Model.objects.create(**kwargs)
except Exception as e:
print(f"Error creating {Model.__name__}: {e}")
return None
# ================================
# TENANT UTILITIES
# ================================
def get_tenant_users(tenant, roles=None, limit=20):
"""Get users for a tenant with optional role filtering"""
from django.contrib.auth import get_user_model
User = get_user_model()
qs = User.objects.filter(tenant=tenant, is_active=True)
if roles:
qs = qs.filter(employee_profile__role__in=roles)
return list(qs[:limit])
def get_tenant_providers(tenant, clinical_only=True):
"""Get healthcare providers for a tenant"""
from django.contrib.auth import get_user_model
User = get_user_model()
clinical_roles = ['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
qs = User.objects.filter(tenant=tenant, is_active=True)
if clinical_only:
qs = qs.filter(employee_profile__role__in=clinical_roles)
return list(qs)
def get_tenant_patients(tenant, limit=100):
"""Get patients for a tenant"""
from patients.models import PatientProfile
return list(PatientProfile.objects.filter(tenant=tenant)[:limit])
def get_tenant_departments(tenant):
"""Get departments for a tenant"""
from hr.models import Department
return list(Department.objects.filter(tenant=tenant))
# ================================
# PROGRESS TRACKING
# ================================
class ProgressTracker:
"""Track progress of data generation operations"""
def __init__(self, total_operations=0):
self.total_operations = total_operations
self.completed_operations = 0
self.start_time = django_timezone.now()
def increment(self, count=1):
"""Increment completed operations"""
self.completed_operations += count
def get_progress(self):
"""Get current progress percentage"""
if self.total_operations == 0:
return 100
return int((self.completed_operations / self.total_operations) * 100)
def get_eta(self):
"""Estimate time remaining"""
if self.completed_operations == 0:
return None
elapsed = django_timezone.now() - self.start_time
total_estimated = elapsed * (self.total_operations / self.completed_operations)
remaining = total_estimated - elapsed
return remaining
def print_progress(self, operation_name=""):
"""Print current progress"""
progress = self.get_progress()
eta = self.get_eta()
eta_str = f" ETA: {eta}" if eta else ""
print(f"[{progress:3d}%] {operation_name}{eta_str}")
# ================================
# VALIDATION UTILITIES
# ================================
def validate_tenant_exists(tenant_id=None, tenant_slug=None):
"""Validate that tenant exists"""
from core.models import Tenant
if tenant_id:
try:
return Tenant.objects.get(id=tenant_id)
except Tenant.DoesNotExist:
raise ValueError(f"Tenant with ID {tenant_id} does not exist")
if tenant_slug:
try:
return Tenant.objects.get(slug=tenant_slug)
except Tenant.DoesNotExist:
raise ValueError(f"Tenant with slug {tenant_slug} does not exist")
# Return first active tenant if no specific tenant requested
tenant = Tenant.objects.filter(is_active=True).first()
if not tenant:
raise ValueError("No active tenants found")
return tenant
def validate_dependencies():
"""Validate that all required dependencies exist"""
from core.models import Tenant
from django.contrib.auth import get_user_model
User = get_user_model()
# Check for tenants
tenant_count = Tenant.objects.filter(is_active=True).count()
if tenant_count == 0:
raise ValueError("No active tenants found. Please create tenants first.")
# Check for users
user_count = User.objects.filter(is_active=True).count()
if user_count == 0:
raise ValueError("No active users found. Please create users first.")
return {
'tenants': tenant_count,
'users': user_count
}
# ================================
# CLEANUP UTILITIES
# ================================
def cleanup_test_data(tenant=None, confirm=False):
"""Clean up test data (use with caution!)"""
if not confirm:
print("WARNING: This will delete test data. Set confirm=True to proceed.")
return
from django.core.management import call_command
# Reset sequences and clear data
models_to_clear = [
'laboratory.LabResult',
'laboratory.Specimen',
'laboratory.LabOrder',
'emr.ClinicalNote',
'emr.CarePlan',
'emr.ProblemList',
'emr.VitalSigns',
'emr.Encounter',
'appointments.AppointmentRequest',
'patients.PatientProfile',
'hr.Employee',
'accounts.User',
]
for model in models_to_clear:
try:
call_command('flush', model, verbosity=0, interactive=False)
except Exception as e:
print(f"Error clearing {model}: {e}")
print("Test data cleanup completed.")
# ================================
# BATCH PROCESSING UTILITIES
# ================================
def batch_process(items, batch_size=100, process_func=None, progress_callback=None):
"""Process items in batches with progress tracking"""
if not process_func:
return
total_batches = (len(items) + batch_size - 1) // batch_size
processed = 0
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
batch_num = (i // batch_size) + 1
try:
process_func(batch)
processed += len(batch)
if progress_callback:
progress_callback(batch_num, total_batches, processed, len(items))
except Exception as e:
print(f"Error processing batch {batch_num}: {e}")
continue
return processed
def create_with_retry(Model, max_retries=3, **kwargs):
"""Create model instance with retry on integrity errors"""
for attempt in range(max_retries):
try:
return Model.objects.create(**kwargs)
except IntegrityError:
if attempt == max_retries - 1:
raise
continue
return None

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More