update
This commit is contained in:
parent
4ca3f7159a
commit
ab2c4a36c5
175
.clinerules/01-coding.md
Normal file
175
.clinerules/01-coding.md
Normal 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');
|
||||
0
.clinerules/02-documentation.md
Normal file
0
.clinerules/02-documentation.md
Normal file
216
CENTRALIZED_INVENTORY_IMPLEMENTATION.md
Normal file
216
CENTRALIZED_INVENTORY_IMPLEMENTATION.md
Normal 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
307
DATA_GENERATION_README.md
Normal 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**
|
||||
BIN
__pycache__/accounts_data.cpython-312.pyc
Normal file
BIN
__pycache__/accounts_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/appointments_data.cpython-312.pyc
Normal file
BIN
__pycache__/appointments_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/billing_data.cpython-312.pyc
Normal file
BIN
__pycache__/billing_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/core_data.cpython-312.pyc
Normal file
BIN
__pycache__/core_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/emr_data.cpython-312.pyc
Normal file
BIN
__pycache__/emr_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/hr_data.cpython-312.pyc
Normal file
BIN
__pycache__/hr_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/inpatients_data.cpython-312.pyc
Normal file
BIN
__pycache__/inpatients_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/inventory_data.cpython-312.pyc
Normal file
BIN
__pycache__/inventory_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/lab_data.cpython-312.pyc
Normal file
BIN
__pycache__/lab_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/patients_data.cpython-312.pyc
Normal file
BIN
__pycache__/patients_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/pharmacy_data.cpython-312.pyc
Normal file
BIN
__pycache__/pharmacy_data.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/radiology_data.cpython-312.pyc
Normal file
BIN
__pycache__/radiology_data.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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'),
|
||||
|
||||
@ -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()
|
||||
#
|
||||
|
||||
@ -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()
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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"),
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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)
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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,
|
||||
),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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'
|
||||
)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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'),
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -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(
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
)
|
||||
|
||||
|
||||
@ -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
|
||||
#
|
||||
|
||||
@ -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
|
||||
|
||||
Binary file not shown.
@ -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
|
||||
229
core/models.py
229
core/models.py
@ -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
|
||||
|
||||
271
core/views.py
271
core/views.py
@ -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."""
|
||||
|
||||
@ -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
6
data_utils/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""
|
||||
Shared utilities for data generation scripts.
|
||||
Provides common constants, generators, and helper functions.
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
BIN
data_utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
data_utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
data_utils/__pycache__/base.cpython-312.pyc
Normal file
BIN
data_utils/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
data_utils/__pycache__/constants.cpython-312.pyc
Normal file
BIN
data_utils/__pycache__/constants.cpython-312.pyc
Normal file
Binary file not shown.
BIN
data_utils/__pycache__/generators.cpython-312.pyc
Normal file
BIN
data_utils/__pycache__/generators.cpython-312.pyc
Normal file
Binary file not shown.
BIN
data_utils/__pycache__/helpers.cpython-312.pyc
Normal file
BIN
data_utils/__pycache__/helpers.cpython-312.pyc
Normal file
Binary file not shown.
271
data_utils/base.py
Normal file
271
data_utils/base.py
Normal 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
328
data_utils/constants.py
Normal 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
357
data_utils/generators.py
Normal 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
371
data_utils/helpers.py
Normal 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
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user