This commit is contained in:
Marwan Alwali 2025-09-08 19:52:52 +03:00
parent 23158e9fbf
commit 84c1fb798e
167 changed files with 12990 additions and 4471 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.contrib.auth.models
import django.contrib.auth.validators

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion
@ -207,6 +207,26 @@ class Migration(migrations.Migration):
),
),
("is_active", models.BooleanField(default=True)),
(
"last_test_status",
models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
],
default="PENDING",
max_length=20,
),
),
("last_test_start_at", models.DateTimeField(blank=True, null=True)),
("last_test_end_at", models.DateTimeField(blank=True, null=True)),
(
"last_test_duration_seconds",
models.PositiveIntegerField(blank=True, null=True),
),
("last_test_error_message", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
],

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings

View File

@ -1,47 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-04 17:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("analytics", "0002_initial"),
]
operations = [
migrations.AddField(
model_name="datasource",
name="last_test_duration_seconds",
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_end_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_error_message",
field=models.TextField(blank=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_start_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="datasource",
name="last_test_status",
field=models.CharField(
choices=[
("PENDING", "Pending"),
("RUNNING", "Running"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
],
default="PENDING",
max_length=20,
),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings

View File

@ -10,12 +10,20 @@ app_name = 'appointments'
urlpatterns = [
# Main views
path('', views.AppointmentDashboardView.as_view(), name='dashboard'),
path('list/', views.AppointmentListView.as_view(), name='appointment_list'),
path('create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
path('detail/<int:pk>/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
path('requests/', views.AppointmentListView.as_view(), name='appointment_list'),
path('requests/create/', views.AppointmentRequestCreateView.as_view(), name='appointment_create'),
path('requests/<int:pk>/detail/', views.AppointmentDetailView.as_view(), name='appointment_detail'),
path('calendar/', views.SchedulingCalendarView.as_view(), name='scheduling_calendar'),
path('queue/', views.QueueManagementView.as_view(), name='queue_management'),
# Telemedicine
path('telemedicine/', views.TelemedicineView.as_view(), name='telemedicine'),
path('telemedicine/create/', views.TelemedicineSessionCreateView.as_view(), name='telemedicine_session_create'),
path('telemedicine/<int:pk>/', views.TelemedicineSessionDetailView.as_view(), name='telemedicine_session_detail'),
path('telemedicine/<int:pk>/update/', views.TelemedicineSessionUpdateView.as_view(), name='telemedicine_session_update'),
path('telemedicine/<int:pk>/start/', views.start_telemedicine_session, name='start_telemedicine_session'),
path('telemedicine/<int:pk>/end/', views.end_telemedicine_session, name='stop_telemedicine_session'),
path('telemedicine/<int:pk>/cancel/', views.cancel_telemedicine_session, name='cancel_telemedicine_session'),
# HTMX endpoints
path('search/', views.appointment_search, name='appointment_search'),
@ -27,9 +35,10 @@ urlpatterns = [
# Actions
path('check-in/<int:appointment_id>/', views.check_in_patient, name='check_in_patient'),
path('queue/<int:queue_id>/call-next/', views.call_next_patient, name='call_next_patient'),
path('telemedicine/<uuid:session_id>/start/', views.start_telemedicine_session, name='start_telemedicine_session'),
path('complete/<int:appointment_id>/', views.complete_appointment, name='complete_appointment'),
path('reschedule/<int:appointment_id>/', views.reschedule_appointment, name='reschedule_appointment'),
path('cancel/<int:appointment_id>/', views.cancel_appointment, name='cancel_appointment'),
# API endpoints
# path('api/', include('appointments.api.urls')),

View File

@ -31,10 +31,6 @@ from core.utils import AuditLogger
# ============================================================================
# DASHBOARD VIEW
# ============================================================================
class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
"""
Appointment dashboard view.
@ -85,10 +81,6 @@ class AppointmentDashboardView(LoginRequiredMixin, TemplateView):
return context
# ============================================================================
# APPOINTMENT REQUEST VIEWS (RESTRICTED CRUD - Clinical Data)
# ============================================================================
class AppointmentRequestListView(LoginRequiredMixin, ListView):
"""
List appointment requests.
@ -199,7 +191,7 @@ class AppointmentRequestCreateView(LoginRequiredMixin, PermissionRequiredMixin,
"""
model = AppointmentRequest
form_class = AppointmentRequestForm
template_name = 'appointments/appointment_request_form.html'
template_name = 'appointments/requests/appointment_form.html'
permission_required = 'appointments.add_appointmentrequest'
success_url = reverse_lazy('appointments:appointment_request_list')
@ -235,7 +227,7 @@ class AppointmentRequestUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
"""
model = AppointmentRequest
form_class = AppointmentRequestForm
template_name = 'appointments/appointment_request_form.html'
template_name = 'appointments/requests/appointment_form.html'
permission_required = 'appointments.change_appointmentrequest'
def get_queryset(self):
@ -276,7 +268,7 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
Cancel appointment request.
"""
model = AppointmentRequest
template_name = 'appointments/appointment_request_confirm_delete.html'
template_name = 'appointments/requests/appointment_confirm_delete.html'
permission_required = 'appointments.delete_appointmentrequest'
success_url = reverse_lazy('appointments:appointment_request_list')
@ -309,10 +301,6 @@ class AppointmentRequestDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
return redirect(self.success_url)
# ============================================================================
# SLOT AVAILABILITY VIEWS (LIMITED CRUD - Operational Data)
# ============================================================================
class SlotAvailabilityListView(LoginRequiredMixin, ListView):
"""
List slot availability.
@ -514,10 +502,6 @@ class SlotAvailabilityDeleteView(LoginRequiredMixin, PermissionRequiredMixin, De
return super().delete(request, *args, **kwargs)
# ============================================================================
# WAITING QUEUE VIEWS (FULL CRUD - Operational Data)
# ============================================================================
class WaitingQueueListView(LoginRequiredMixin, ListView):
"""
List waiting queues.
@ -733,10 +717,6 @@ class WaitingQueueDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Delete
return super().delete(request, *args, **kwargs)
# ============================================================================
# QUEUE ENTRY VIEWS (LIMITED CRUD - Operational Data)
# ============================================================================
class QueueEntryListView(LoginRequiredMixin, ListView):
"""
List queue entries.
@ -878,10 +858,6 @@ class QueueEntryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
return response
# ============================================================================
# TELEMEDICINE SESSION VIEWS (RESTRICTED CRUD - Clinical Data)
# ============================================================================
class TelemedicineSessionListView(LoginRequiredMixin, ListView):
"""
List telemedicine sessions.
@ -935,7 +911,7 @@ class TelemedicineSessionDetailView(LoginRequiredMixin, DetailView):
Display telemedicine session details.
"""
model = TelemedicineSession
template_name = 'appointments/telemedicine_session_detail.html'
template_name = 'appointments/telemedicine/telemedicine_session_detail.html'
context_object_name = 'session'
def get_queryset(self):
@ -1021,10 +997,6 @@ class TelemedicineSessionUpdateView(LoginRequiredMixin, PermissionRequiredMixin,
return response
# ============================================================================
# APPOINTMENT TEMPLATE VIEWS (FULL CRUD - Master Data)
# ============================================================================
class AppointmentTemplateListView(LoginRequiredMixin, ListView):
"""
List appointment templates.
@ -1217,10 +1189,6 @@ class AppointmentTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
return super().delete(request, *args, **kwargs)
# ============================================================================
# HTMX VIEWS FOR REAL-TIME UPDATES
# ============================================================================
@login_required
def appointment_search(request):
"""
@ -1370,7 +1338,7 @@ def queue_status(request, queue_id):
default=Value(None), output_field=IntegerField()
),
)
.select_related('assigned_provider', 'patient', 'appointment') # adjust if you need more
.select_related('assigned_provider', 'patient', 'appointment')
.order_by('queue_position', 'updated_at')
)
@ -1434,20 +1402,19 @@ def calendar_appointments(request):
)
if provider_id:
queryset = queryset.filter(provider_id=provider_id)
queryset = queryset.filter(provider__id=provider_id)
appointments = queryset.order_by('scheduled_datetime')
# providers = queryset.order_by('provider__first_name')
return render(request, 'appointments/partials/calendar_appointments.html', {
'appointments': appointments,
'selected_date': selected_date
'selected_date': selected_date,
# 'providers': providers
})
# ============================================================================
# ACTION VIEWS FOR WORKFLOW OPERATIONS
# ============================================================================
@login_required
def confirm_appointment(request, pk):
"""
@ -1543,6 +1510,38 @@ def complete_appointment(request, pk):
return redirect('appointments:appointment_request_detail', pk=pk)
@login_required
def cancel_appointment(request, pk):
"""
Complete an appointment.
"""
tenant = getattr(request, 'tenant', None)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:appointment_request_list')
appointment = get_object_or_404(AppointmentRequest, pk=pk, tenant=tenant)
appointment.status = 'CANCELLED'
# appointment.actual_end_time = timezone.now()
appointment.save()
# Log completion
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Appointment',
description=f'Cancelled appointment: {appointment.patient} with {appointment.provider}',
user=request.user,
content_object=appointment,
request=request
)
messages.success(request, f'Appointment for {appointment.patient} cancelled successfully.')
return redirect('appointments:appointment_request_detail', pk=pk)
@login_required
def next_in_queue(request, queue_id):
"""
@ -1663,7 +1662,7 @@ def start_telemedicine_session(request, pk):
)
messages.success(request, f'Telemedicine session started successfully.')
return redirect('appointments:telemedicine_session_detail', pk=pk)
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
@login_required
@ -1702,18 +1701,17 @@ def end_telemedicine_session(request, pk):
request=request
)
messages.success(request, f'Telemedicine session ended successfully.')
return redirect('appointments:telemedicine_session_detail', pk=pk)
messages.success(request, 'Telemedicine session ended successfully.')
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
# Missing Views - Placeholder implementations
class AppointmentListView(LoginRequiredMixin, ListView):
"""
List view for appointments.
"""
model = AppointmentRequest
template_name = 'appointments/appointment_list.html'
template_name = 'appointments/requests/appointment_list.html'
context_object_name = 'appointments'
paginate_by = 20
@ -1728,7 +1726,7 @@ class AppointmentDetailView(LoginRequiredMixin, DetailView):
Detail view for appointments.
"""
model = AppointmentRequest
template_name = 'appointments/appointment_detail.html'
template_name = 'appointments/requests/appointment_request_detail.html'
context_object_name = 'appointment'
def get_queryset(self):
@ -1775,11 +1773,19 @@ class QueueManagementView(LoginRequiredMixin, ListView):
return context
class TelemedicineView(LoginRequiredMixin, TemplateView):
class TelemedicineView(LoginRequiredMixin, ListView):
"""
Telemedicine appointments view.
"""
template_name = 'appointments/telemedicine.html'
model = TelemedicineSession
template_name = 'appointments/telemedicine/telemedicine.html'
context_object_name = 'sessions'
paginate_by = 20
def get_queryset(self):
return TelemedicineSession.objects.filter(
appointment__tenant=self.request.user.tenant
).order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -1790,6 +1796,38 @@ class TelemedicineView(LoginRequiredMixin, TemplateView):
return context
@login_required
def cancel_telemedicine_session(request, pk):
tenant = getattr(request, 'tenant', None)
if not tenant:
messages.error(request, 'No tenant found.')
return redirect('appointments:telemedicine_session_list')
session = get_object_or_404(TelemedicineSession, pk=pk)
session.status = 'CANCELLED'
session.save()
# Update appointment status
session.appointment.status = 'CANCELLED'
session.appointment.save()
# Log session start
AuditLogger.log_event(
tenant=tenant,
event_type='UPDATE',
event_category='APPOINTMENT_MANAGEMENT',
action='Cancel Telemedicine Session',
description='Cancelled telemedicine session',
user=request.user,
content_object=session,
request=request
)
messages.success(request, 'Telemedicine session cancelled successfully.')
return redirect('appointments:telemedicine_session_detail', pk=session.pk)
@login_required
def check_in_patient(request, appointment_id):
"""
Check in a patient for their appointment.
@ -1815,13 +1853,13 @@ def call_next_patient(request, queue_id):
return redirect('appointments:queue_management')
@login_required
def reschedule_appointment(request, appointment_id):
"""
Reschedule an appointment.
"""
appointment = get_object_or_404(AppointmentRequest,
request_id=appointment_id,
pk=appointment_id,
tenant=request.user.tenant
)
@ -1835,8 +1873,8 @@ def reschedule_appointment(request, appointment_id):
appointment.status = 'RESCHEDULED'
appointment.save()
messages.success(request, f'Appointment has been rescheduled to {new_date} at {new_time}.')
return redirect('appointments:appointment_detail', pk=appointment_id)
messages.success(request, 'Appointment has been rescheduled')
return redirect('appointments:appointment_detail', pk=appointment.pk)
return render(request, 'appointments/reschedule_appointment.html', {
'appointment': appointment

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import billing.utils
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings

View File

@ -30,6 +30,9 @@ urlpatterns = [
path('claims/', views.InsuranceClaimListView.as_view(), name='claim_list'),
path('claims/<uuid:claim_id>/', views.InsuranceClaimDetailView.as_view(), name='claim_detail'),
path('claims/create/', views.InsuranceClaimCreateView.as_view(), name='claim_create'),
path('claims/<uuid:claim_id>/edit/', views.InsuranceClaimUpdateView.as_view(), name='claim_update'),
path('claims/<uuid:claim_id>/appeal', views.claim_appeal, name='claim_appeal'),
path('bills/<uuid:bill_id>/claims/create/', views.InsuranceClaimCreateView.as_view(), name='bill_claim_create'),
path('payments/<uuid:payment_id>/receipt/', views.payment_receipt, name='payment_receipt'),
path('payments/<uuid:payment_id>/email/', views.payment_email, name='payment_email'),

View File

@ -33,12 +33,9 @@ from patients.models import PatientProfile, InsuranceInfo
from accounts.models import User
from emr.models import Encounter
from inpatients.models import Admission
from core.utils import AuditLogger
# ============================================================================
# DASHBOARD VIEW
# ============================================================================
class BillingDashboardView(LoginRequiredMixin, TemplateView):
"""
Billing dashboard view with comprehensive statistics and recent activity.
@ -103,10 +100,6 @@ class BillingDashboardView(LoginRequiredMixin, TemplateView):
return context
# ============================================================================
# MEDICAL BILL VIEWS
# ============================================================================
class MedicalBillListView(LoginRequiredMixin, ListView):
"""
List view for medical bills with filtering and search.
@ -311,10 +304,6 @@ class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteV
return response
# ============================================================================
# INSURANCE CLAIM VIEWS
# ============================================================================
class InsuranceClaimListView(LoginRequiredMixin, ListView):
"""
List view for insurance claims with filtering and search.
@ -461,9 +450,65 @@ class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
# ============================================================================
# PAYMENT VIEWS
# ============================================================================
class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
model = InsuranceClaim
form_class = InsuranceClaimForm
template_name = 'billing/claims/claim_form.html'
permission_required = 'billing.add_insuranceclaim'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
# Get medical bill from URL parameter
bill_id = self.kwargs.get('bill_id')
if bill_id:
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
)
kwargs['medical_bill'] = medical_bill
except MedicalBill.DoesNotExist:
pass
return kwargs
def form_valid(self, form):
# Prefer URL bill_id; otherwise read from POST("medical_bill")
bill_id = self.kwargs.get('bill_id') or self.request.POST.get('medical_bill')
if bill_id:
try:
medical_bill = MedicalBill.objects.get(
bill_id=bill_id,
tenant=getattr(self.request, 'tenant', None)
)
form.instance.medical_bill = medical_bill
except MedicalBill.DoesNotExist:
messages.error(self.request, 'Medical bill not found.')
return redirect('billing:bill_list')
else:
messages.error(self.request, 'Please select a medical bill.')
return redirect('billing:claim_create')
form.instance.created_by = self.request.user
if not form.instance.claim_number:
form.instance.claim_number = form.instance.generate_claim_number()
response = super().form_valid(form)
messages.success(self.request, f'Insurance claim {self.object.claim_number} updated successfully.')
return response
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
tenant = getattr(self.request, 'tenant', None)
ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
return ctx
def get_success_url(self):
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
class PaymentListView(LoginRequiredMixin, ListView):
"""
@ -598,40 +643,37 @@ class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView)
return reverse('billing:payment_detail', kwargs={'payment_id': self.object.payment_id})
# ============================================================================
# HTMX VIEWS
# ============================================================================
# @login_required
# def htmx_billing_stats(request):
# """
# HTMX endpoint for billing statistics.
# """
# tenant = getattr(request, 'tenant', None)
# if not tenant:
# return JsonResponse({'error': 'No tenant found'})
#
# today = timezone.now().date()
#
# # Calculate statistics
# bills = MedicalBill.objects.filter(tenant=tenant)
# stats = {
# 'total_bills': bills.count(),
# 'total_revenue': float(bills.aggregate(
# total=Sum('total_amount')
# )['total'] or 0),
# 'total_paid': float(bills.aggregate(
# total=Sum('paid_amount')
# )['total'] or 0),
# 'overdue_bills': bills.filter(
# due_date__lt=today,
# status__in=['draft', 'sent', 'partial_payment']
# ).count()
# }
#
# stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
#
# return JsonResponse(stats)
@login_required
def htmx_billing_stats(request):
"""
HTMX endpoint for billing statistics.
"""
tenant = getattr(request, 'tenant', None)
if not tenant:
return JsonResponse({'error': 'No tenant found'})
today = timezone.now().date()
# Calculate statistics
bills = MedicalBill.objects.filter(tenant=tenant)
stats = {
'total_bills': bills.count(),
'total_revenue': float(bills.aggregate(
total=Sum('total_amount')
)['total'] or 0),
'total_paid': float(bills.aggregate(
total=Sum('paid_amount')
)['total'] or 0),
'overdue_bills': bills.filter(
due_date__lt=today,
status__in=['draft', 'sent', 'partial_payment']
).count()
}
stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
return JsonResponse(stats)
@login_required
def billing_stats(request):
@ -715,6 +757,7 @@ def bill_details_api(request, bill_id):
}
return JsonResponse(data)
@login_required
def bill_search(request):
"""
@ -738,10 +781,6 @@ def bill_search(request):
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
# ============================================================================
# ACTION VIEWS
# ============================================================================
@login_required
@require_http_methods(["POST"])
def submit_bill(request, bill_id):
@ -768,50 +807,65 @@ def submit_bill(request, bill_id):
return JsonResponse({'success': False, 'error': 'Bill not found'})
# ============================================================================
# EXPORT VIEWS
# ============================================================================
@login_required
def export_bills(request):
"""
Export bills to CSV.
Export medical bills to CSV.
"""
tenant = getattr(request, 'tenant', None)
if not tenant:
return HttpResponse('No tenant found', status=400)
tenant = request.user.tenant
# Create CSV response
# Create HTTP response with CSV content type
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
writer = csv.writer(response)
# Write header row
writer.writerow([
'Bill Number', 'Patient Name', 'Bill Date', 'Due Date',
'Total Amount', 'Paid Amount', 'Balance', 'Status'
'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount',
'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date'
])
bills = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
# Write data rows
bills = MedicalBill.objects.filter(
tenant=tenant
).select_related(
'patient', 'attending_provider'
).order_by('-bill_date')
for bill in bills:
writer.writerow([
bill.bill_number,
bill.patient.get_full_name(),
bill.patient.mrn,
bill.get_bill_type_display(),
bill.bill_date.strftime('%Y-%m-%d'),
bill.due_date.strftime('%Y-%m-%d') if bill.due_date else '',
bill.due_date.strftime('%Y-%m-%d'),
bill.service_date_from.strftime('%Y-%m-%d'),
bill.service_date_to.strftime('%Y-%m-%d'),
str(bill.subtotal),
str(bill.tax_amount),
str(bill.total_amount),
str(bill.paid_amount),
str(bill.balance_amount),
bill.get_status_display()
bill.get_status_display(),
bill.attending_provider.get_full_name() if bill.attending_provider else '',
bill.created_at.strftime('%Y-%m-%d %H:%M:%S')
])
# Log audit event
AuditLogger.log_event(
request.user,
'BILLS_EXPORTED',
'MedicalBill',
None,
f"Exported {bills.count()} medical bills to CSV"
)
return response
# ============================================================================
# PRINT VIEWS
# ============================================================================
@login_required
def print_bills(request):
"""
@ -1127,7 +1181,7 @@ def payment_receipt(request, payment_id):
# Get payment with related objects
payment = Payment.objects.select_related(
'medical_bill', 'medical_bill__patient', 'processed_by'
).get(id=payment_id, medical_bill__tenant=tenant)
).get(payment_id=payment_id, medical_bill__tenant=tenant)
# Calculate payment details
payment_details = {
@ -1555,8 +1609,23 @@ def bill_line_items_api(request, bill_id=None):
})
@login_required
def claim_appeal(request, claim_id):
tenant = getattr(request, 'tenant', None)
if not tenant:
return HttpResponse('No tenant found', status=400)
#
claim = get_object_or_404(
InsuranceClaim,
medical_bill__tenant=tenant,
claim_id=claim_id
)
if claim.status in ['DENIED', 'REJECTED']:
claim.status = 'APPEALED'
claim.save()
messages.success(request, 'Claim has already been appealed.')
return redirect('billing:claim_detail', claim_id=claim.claim_id)
return JsonResponse({'success': False, 'error': 'check claim status'}, status=400)
#
# """
# Billing app views for hospital management system.
@ -2040,83 +2109,83 @@ def bill_line_items_api(request, bill_id=None):
# }
#
# return render(request, 'billing/partials/billing_stats.html', {'stats': stats})
#
#
# @login_required
# def htmx_bill_search(request):
# """
# HTMX view for medical bill search.
# """
# tenant = request.user.tenant
# search = request.GET.get('search', '')
#
# bills = MedicalBill.objects.filter(tenant=tenant)
#
# if search:
# bills = bills.filter(
# Q(bill_number__icontains=search) |
# Q(patient__first_name__icontains=search) |
# Q(patient__last_name__icontains=search) |
# Q(patient__mrn__icontains=search)
# )
#
# bills = bills.select_related(
# 'patient', 'encounter', 'attending_provider'
# ).order_by('-bill_date')[:10]
#
# return render(request, 'billing/partials/bill_list.html', {'bills': bills})
#
#
# @login_required
# def htmx_payment_search(request):
# """
# HTMX view for payment search.
# """
# tenant = request.user.tenant
# search = request.GET.get('search', '')
#
# payments = Payment.objects.filter(medical_bill__tenant=tenant)
#
# if search:
# payments = payments.filter(
# Q(payment_number__icontains=search) |
# Q(medical_bill__bill_number__icontains=search) |
# Q(medical_bill__patient__first_name__icontains=search) |
# Q(medical_bill__patient__last_name__icontains=search)
# )
#
# payments = payments.select_related(
# 'medical_bill', 'medical_bill__patient'
# ).order_by('-payment_date')[:10]
#
# return render(request, 'billing/partials/payment_list.html', {'payments': payments})
#
#
# @login_required
# def htmx_claim_search(request):
# """
# HTMX view for insurance claim search.
# """
# tenant = request.user.tenant
# search = request.GET.get('search', '')
#
# claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
#
# if search:
# claims = claims.filter(
# Q(claim_number__icontains=search) |
# Q(medical_bill__bill_number__icontains=search) |
# Q(medical_bill__patient__first_name__icontains=search) |
# Q(medical_bill__patient__last_name__icontains=search)
# )
#
# claims = claims.select_related(
# 'medical_bill', 'medical_bill__patient', 'insurance_info'
# ).order_by('-submission_date')[:10]
#
# return render(request, 'billing/partials/claim_list.html', {'claims': claims})
#
#
@login_required
def htmx_bill_search(request):
"""
HTMX view for medical bill search.
"""
tenant = request.user.tenant
search = request.GET.get('search', '')
bills = MedicalBill.objects.filter(tenant=tenant)
if search:
bills = bills.filter(
Q(bill_number__icontains=search) |
Q(patient__first_name__icontains=search) |
Q(patient__last_name__icontains=search) |
Q(patient__mrn__icontains=search)
)
bills = bills.select_related(
'patient', 'encounter', 'attending_provider'
).order_by('-bill_date')[:10]
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
@login_required
def htmx_payment_search(request):
"""
HTMX view for payment search.
"""
tenant = request.user.tenant
search = request.GET.get('search', '')
payments = Payment.objects.filter(medical_bill__tenant=tenant)
if search:
payments = payments.filter(
Q(payment_number__icontains=search) |
Q(medical_bill__bill_number__icontains=search) |
Q(medical_bill__patient__first_name__icontains=search) |
Q(medical_bill__patient__last_name__icontains=search)
)
payments = payments.select_related(
'medical_bill', 'medical_bill__patient'
).order_by('-payment_date')[:10]
return render(request, 'billing/partials/payment_list.html', {'payments': payments})
@login_required
def htmx_claim_search(request):
"""
HTMX view for insurance claim search.
"""
tenant = request.user.tenant
search = request.GET.get('search', '')
claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
if search:
claims = claims.filter(
Q(claim_number__icontains=search) |
Q(medical_bill__bill_number__icontains=search) |
Q(medical_bill__patient__first_name__icontains=search) |
Q(medical_bill__patient__last_name__icontains=search)
)
claims = claims.select_related(
'medical_bill', 'medical_bill__patient', 'insurance_info'
).order_by('-submission_date')[:10]
return render(request, 'billing/partials/claim_list.html', {'claims': claims})
# # Action Views
# @login_required
# @require_http_methods(["POST"])
@ -2146,171 +2215,115 @@ def bill_line_items_api(request, bill_id=None):
# messages.success(request, 'Medical bill submitted successfully')
#
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
@login_required
@require_http_methods(["POST"])
def process_payment(request, bill_id):
"""
Process payment for medical bill.
"""
bill = get_object_or_404(
MedicalBill,
bill_id=bill_id,
tenant=request.user.tenant
)
payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
payment_method = request.POST.get('payment_method', 'CASH')
payment_source = request.POST.get('payment_source', 'PATIENT')
if payment_amount > 0:
# Create payment record
payment = Payment.objects.create(
medical_bill=bill,
payment_amount=payment_amount,
payment_method=payment_method,
payment_source=payment_source,
payment_date=timezone.now().date(),
received_by=request.user,
processed_by=request.user,
status='PROCESSED'
)
# Update bill paid amount and status
bill.paid_amount += payment_amount
bill.balance_amount = bill.total_amount - bill.paid_amount
if bill.balance_amount <= 0:
bill.status = 'PAID'
elif bill.paid_amount > 0:
bill.status = 'PARTIAL_PAID'
bill.save()
# Log audit event
AuditLogger.log_event(
request.user,
'PAYMENT_PROCESSED',
'Payment',
str(payment.payment_id),
f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}"
)
messages.success(request, f'Payment of ${payment_amount} processed successfully')
return redirect('billing:bill_detail', bill_id=bill.bill_id)
#
#
# @login_required
# @require_http_methods(["POST"])
# def process_payment(request, bill_id):
# """
# Process payment for medical bill.
# """
# bill = get_object_or_404(
# MedicalBill,
# bill_id=bill_id,
# tenant=request.user.tenant
# )
#
# payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
# payment_method = request.POST.get('payment_method', 'CASH')
# payment_source = request.POST.get('payment_source', 'PATIENT')
#
# if payment_amount > 0:
# # Create payment record
# payment = Payment.objects.create(
# medical_bill=bill,
# payment_amount=payment_amount,
# payment_method=payment_method,
# payment_source=payment_source,
# payment_date=timezone.now().date(),
# received_by=request.user,
# processed_by=request.user,
# status='PROCESSED'
# )
#
# # Update bill paid amount and status
# bill.paid_amount += payment_amount
# bill.balance_amount = bill.total_amount - bill.paid_amount
#
# if bill.balance_amount <= 0:
# bill.status = 'PAID'
# elif bill.paid_amount > 0:
# bill.status = 'PARTIAL_PAID'
#
# bill.save()
#
# # Log audit event
# AuditLogger.log_event(
# request.user,
# 'PAYMENT_PROCESSED',
# 'Payment',
# str(payment.payment_id),
# f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}"
# )
#
# messages.success(request, f'Payment of ${payment_amount} processed successfully')
#
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
#
#
# @login_required
# @require_http_methods(["POST"])
# def submit_insurance_claim(request, bill_id):
# """
# Submit insurance claim for medical bill.
# """
# bill = get_object_or_404(
# MedicalBill,
# bill_id=bill_id,
# tenant=request.user.tenant
# )
#
# insurance_type = request.POST.get('insurance_type', 'PRIMARY')
#
# # Determine which insurance to use
# if insurance_type == 'PRIMARY' and bill.primary_insurance:
# insurance_info = bill.primary_insurance
# claim_type = 'PRIMARY'
# elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
# insurance_info = bill.secondary_insurance
# claim_type = 'SECONDARY'
# else:
# messages.error(request, 'No insurance information available for claim submission')
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
#
# # Create insurance claim
# claim = InsuranceClaim.objects.create(
# medical_bill=bill,
# insurance_info=insurance_info,
# claim_type=claim_type,
# submission_date=timezone.now().date(),
# service_date_from=bill.service_date_from,
# service_date_to=bill.service_date_to,
# billed_amount=bill.total_amount,
# status='SUBMITTED',
# created_by=request.user
# )
#
# # Log audit event
# AuditLogger.log_event(
# request.user,
# 'INSURANCE_CLAIM_SUBMITTED',
# 'InsuranceClaim',
# str(claim.claim_id),
# f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
# )
#
# messages.success(request, f'{claim_type.title()} insurance claim submitted successfully')
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
@login_required
@require_http_methods(["POST"])
def submit_insurance_claim(request, bill_id):
"""
Submit insurance claim for medical bill.
"""
bill = get_object_or_404(
MedicalBill,
bill_id=bill_id,
tenant=request.user.tenant
)
insurance_type = request.POST.get('insurance_type', 'PRIMARY')
# Determine which insurance to use
if insurance_type == 'PRIMARY' and bill.primary_insurance:
insurance_info = bill.primary_insurance
claim_type = 'PRIMARY'
elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
insurance_info = bill.secondary_insurance
claim_type = 'SECONDARY'
else:
messages.error(request, 'No insurance information available for claim submission')
return redirect('billing:bill_detail', bill_id=bill.bill_id)
# Create insurance claim
claim = InsuranceClaim.objects.create(
medical_bill=bill,
insurance_info=insurance_info,
claim_type=claim_type,
submission_date=timezone.now().date(),
service_date_from=bill.service_date_from,
service_date_to=bill.service_date_to,
billed_amount=bill.total_amount,
status='SUBMITTED',
created_by=request.user
)
# Log audit event
AuditLogger.log_event(
request.user,
'INSURANCE_CLAIM_SUBMITTED',
'InsuranceClaim',
str(claim.claim_id),
f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
)
messages.success(request, f'{claim_type.title()} insurance claim submitted successfully')
return redirect('billing:bill_detail', bill_id=bill.bill_id)
#
#
# # Export Views
# @login_required
# def export_bills(request):
# """
# Export medical bills to CSV.
# """
# tenant = request.user.tenant
#
# # Create HTTP response with CSV content type
# response = HttpResponse(content_type='text/csv')
# response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
#
# writer = csv.writer(response)
#
# # Write header row
# writer.writerow([
# 'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
# 'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount',
# 'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date'
# ])
#
# # Write data rows
# bills = MedicalBill.objects.filter(
# tenant=tenant
# ).select_related(
# 'patient', 'attending_provider'
# ).order_by('-bill_date')
#
# for bill in bills:
# writer.writerow([
# bill.bill_number,
# bill.patient.get_full_name(),
# bill.patient.mrn,
# bill.get_bill_type_display(),
# bill.bill_date.strftime('%Y-%m-%d'),
# bill.due_date.strftime('%Y-%m-%d'),
# bill.service_date_from.strftime('%Y-%m-%d'),
# bill.service_date_to.strftime('%Y-%m-%d'),
# str(bill.subtotal),
# str(bill.tax_amount),
# str(bill.total_amount),
# str(bill.paid_amount),
# str(bill.balance_amount),
# bill.get_status_display(),
# bill.attending_provider.get_full_name() if bill.attending_provider else '',
# bill.created_at.strftime('%Y-%m-%d %H:%M:%S')
# ])
#
# # Log audit event
# AuditLogger.log_event(
# request.user,
# 'BILLS_EXPORTED',
# 'MedicalBill',
# None,
# f"Exported {bills.count()} medical bills to CSV"
# )
#
# return response
#
#
# # Legacy view functions for backward compatibility

View File

@ -3,7 +3,7 @@ from accounts.models import User
from django.utils import timezone
from datetime import timedelta
from patients.models import PatientProfile
from core.models import Department
from hr.models import Department
from .models import (
BloodGroup, Donor, BloodComponent, BloodUnit, BloodTest, CrossMatch,
BloodRequest, BloodIssue, Transfusion, AdverseReaction, InventoryLocation,

File diff suppressed because it is too large Load Diff

View File

@ -1,26 +0,0 @@
# Generated by Django 5.2.4 on 2025-09-04 15:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blood_bank", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="donor",
name="national_id",
field=models.CharField(default=1129632798, max_length=10, unique=True),
preserve_default=False,
),
migrations.AlterField(
model_name="donor",
name="gender",
field=models.CharField(
choices=[("M", "Male"), ("F", "Female"), ("O", "Other")], max_length=10
),
),
]

View File

@ -0,0 +1,297 @@
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("blood_bank", "0001_initial"),
("hr", "0001_initial"),
("patients", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name="bloodrequest",
name="patient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="bloodrequest",
name="patient_blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodrequest",
name="processed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="processed_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_department",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="hr.department"
),
),
migrations.AddField(
model_name="bloodrequest",
name="requesting_physician",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_requests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_request",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="issues",
to="blood_bank.bloodrequest",
),
),
migrations.AddField(
model_name="bloodtest",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="bloodtest",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="bloodunit",
name="collected_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="collected_units",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="component",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.bloodcomponent",
),
),
migrations.AddField(
model_name="bloodtest",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tests",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="bloodissue",
name="blood_unit",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="issue",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="blood_unit",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="crossmatches",
to="blood_bank.bloodunit",
),
),
migrations.AddField(
model_name="crossmatch",
name="recipient",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="patients.patientprofile",
),
),
migrations.AddField(
model_name="crossmatch",
name="tested_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL
),
),
migrations.AddField(
model_name="crossmatch",
name="verified_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="verified_crossmatches",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodissue",
name="crossmatch",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="blood_bank.crossmatch",
),
),
migrations.AddField(
model_name="donor",
name="blood_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="blood_bank.bloodgroup"
),
),
migrations.AddField(
model_name="donor",
name="created_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="created_donors",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="bloodunit",
name="donor",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="blood_units",
to="blood_bank.donor",
),
),
migrations.AddField(
model_name="qualitycontrol",
name="capa_initiated_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="initiated_capas",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="performed_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="qualitycontrol",
name="reviewed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="reviewed_qc_tests",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="administered_by",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="administered_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="blood_issue",
field=models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="transfusion",
to="blood_bank.bloodissue",
),
),
migrations.AddField(
model_name="transfusion",
name="completed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="completed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="stopped_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="stopped_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="transfusion",
name="witnessed_by",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="witnessed_transfusions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AddField(
model_name="adversereaction",
name="transfusion",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="adverse_reactions",
to="blood_bank.transfusion",
),
),
migrations.AlterUniqueTogether(
name="bloodtest",
unique_together={("blood_unit", "test_type")},
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
import uuid

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
from django.conf import settings

View File

@ -1,5 +1,5 @@
from django.contrib import admin
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog, Department
from .models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog
@admin.register(Tenant)
@ -29,51 +29,51 @@ class TenantAdmin(admin.ModelAdmin):
)
@admin.register(Department)
class DepartmentAdmin(admin.ModelAdmin):
"""Admin interface for Department model"""
list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
search_fields = ['name', 'code', 'description']
readonly_fields = ['department_id', 'created_at', 'updated_at']
autocomplete_fields = ['parent_department', 'department_head', 'created_by']
fieldsets = (
('Basic Information', {
'fields': ('tenant', 'code', 'name', 'description', 'department_type')
}),
('Organizational Structure', {
'fields': ('parent_department', 'department_head')
}),
('Contact Information', {
'fields': ('phone', 'extension', 'email')
}),
('Location', {
'fields': ('building', 'floor', 'wing', 'room_numbers')
}),
('Operations', {
'fields': ('is_active', 'is_24_hour', 'operating_hours')
}),
('Financial', {
'fields': ('cost_center_code', 'budget_code')
}),
('Staffing', {
'fields': ('authorized_positions', 'current_staff_count')
}),
('Quality & Compliance', {
'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
}),
('Metadata', {
'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
'classes': ('collapse',)
})
)
def get_queryset(self, request):
"""Filter by tenant if user has tenant"""
qs = super().get_queryset(request)
if hasattr(request.user, 'tenant') and request.user.tenant:
qs = qs.filter(tenant=request.user.tenant)
return qs
# @admin.register(Department)
# class DepartmentAdmin(admin.ModelAdmin):
# """Admin interface for Department model"""
# list_display = ['name', 'code', 'department_type', 'department_head', 'is_active', 'tenant']
# list_filter = ['department_type', 'is_active', 'tenant', 'created_at']
# search_fields = ['name', 'code', 'description']
# readonly_fields = ['department_id', 'created_at', 'updated_at']
# autocomplete_fields = ['parent_department', 'department_head', 'created_by']
# fieldsets = (
# ('Basic Information', {
# 'fields': ('tenant', 'code', 'name', 'description', 'department_type')
# }),
# ('Organizational Structure', {
# 'fields': ('parent_department', 'department_head')
# }),
# ('Contact Information', {
# 'fields': ('phone', 'extension', 'email')
# }),
# ('Location', {
# 'fields': ('building', 'floor', 'wing', 'room_numbers')
# }),
# ('Operations', {
# 'fields': ('is_active', 'is_24_hour', 'operating_hours')
# }),
# ('Financial', {
# 'fields': ('cost_center_code', 'budget_code')
# }),
# ('Staffing', {
# 'fields': ('authorized_positions', 'current_staff_count')
# }),
# ('Quality & Compliance', {
# 'fields': ('accreditation_required', 'accreditation_body', 'last_inspection_date', 'next_inspection_date')
# }),
# ('Metadata', {
# 'fields': ('department_id', 'created_by', 'created_at', 'updated_at'),
# 'classes': ('collapse',)
# })
# )
#
# def get_queryset(self, request):
# """Filter by tenant if user has tenant"""
# qs = super().get_queryset(request)
# if hasattr(request.user, 'tenant') and request.user.tenant:
# qs = qs.filter(tenant=request.user.tenant)
# return qs
@admin.register(AuditLogEntry)

View File

@ -6,9 +6,9 @@ notifications, departments, and search functionality.
from django import forms
from django.contrib.auth import get_user_model
from .models import Tenant, SystemConfiguration, SystemNotification, Department
User = get_user_model()
from .models import Tenant, SystemConfiguration, SystemNotification
from accounts.models import User
from hr.models import Department
class TenantForm(forms.ModelForm):
@ -227,106 +227,106 @@ class SystemNotificationForm(forms.ModelForm):
)
class DepartmentForm(forms.ModelForm):
"""Form for creating and managing hospital departments."""
class Meta:
model = Department
fields = [
'name', 'code', 'description', 'department_type',
'department_head', 'parent_department', 'building', 'floor',
'wing', 'room_numbers', 'phone', 'extension',
'email', 'is_active', 'is_24_hour'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter department name'
}),
'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter department code'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Enter department description'
}),
'department_type': forms.Select(attrs={
'class': 'form-select'
}),
'department_head': forms.Select(attrs={
'class': 'form-select'
}),
'parent_department': forms.Select(attrs={
'class': 'form-select'
}),
'building': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter building name/number'
}),
'floor': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter floor'
}),
'wing': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter wing/section'
}),
'room_numbers': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter room numbers'
}),
'phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter phone number'
}),
'extension': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter extension'
}),
'email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Enter email address'
}),
'is_active': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'is_24_hour': forms.CheckboxInput(attrs={
'class': 'form-check-input'
})
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if self.tenant:
# Filter department head by tenant and medical staff
self.fields['department_head'].queryset = User.objects.filter(
tenant=self.tenant,
is_active=True,
role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
)
# Filter parent department by tenant
self.fields['parent_department'].queryset = Department.objects.filter(
tenant=self.tenant,
is_active=True
)
def clean_code(self):
"""Validate department code uniqueness within tenant."""
code = self.cleaned_data.get('code')
if code and self.tenant:
queryset = Department.objects.filter(
tenant=self.tenant,
code=code
)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise forms.ValidationError('A department with this code already exists.')
return code
# class DepartmentForm(forms.ModelForm):
# """Form for creating and managing hospital departments."""
#
# class Meta:
# model = Department
# fields = [
# 'name', 'code', 'description', 'department_type',
# 'department_head', 'parent_department', 'building', 'floor',
# 'wing', 'room_numbers', 'phone', 'extension',
# 'email', 'is_active', 'is_24_hour'
# ]
# widgets = {
# 'name': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter department name'
# }),
# 'code': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter department code'
# }),
# 'description': forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 3,
# 'placeholder': 'Enter department description'
# }),
# 'department_type': forms.Select(attrs={
# 'class': 'form-select'
# }),
# 'department_head': forms.Select(attrs={
# 'class': 'form-select'
# }),
# 'parent_department': forms.Select(attrs={
# 'class': 'form-select'
# }),
# 'building': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter building name/number'
# }),
# 'floor': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter floor'
# }),
# 'wing': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter wing/section'
# }),
# 'room_numbers': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter room numbers'
# }),
# 'phone': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter phone number'
# }),
# 'extension': forms.TextInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter extension'
# }),
# 'email': forms.EmailInput(attrs={
# 'class': 'form-control',
# 'placeholder': 'Enter email address'
# }),
# 'is_active': forms.CheckboxInput(attrs={
# 'class': 'form-check-input'
# }),
# 'is_24_hour': forms.CheckboxInput(attrs={
# 'class': 'form-check-input'
# })
# }
#
# def __init__(self, *args, **kwargs):
# self.tenant = kwargs.pop('tenant', None)
# super().__init__(*args, **kwargs)
#
# if self.tenant:
# # Filter department head by tenant and medical staff
# self.fields['department_head'].queryset = User.objects.filter(
# tenant=self.tenant,
# is_active=True,
# role__in=['DOCTOR', 'NURSE_MANAGER', 'ADMINISTRATOR']
# )
# # Filter parent department by tenant
# self.fields['parent_department'].queryset = Department.objects.filter(
# tenant=self.tenant,
# is_active=True
# )
#
# def clean_code(self):
# """Validate department code uniqueness within tenant."""
# code = self.cleaned_data.get('code')
# if code and self.tenant:
# queryset = Department.objects.filter(
# tenant=self.tenant,
# code=code
# )
# if self.instance.pk:
# queryset = queryset.exclude(pk=self.instance.pk)
# if queryset.exists():
# raise forms.ValidationError('A department with this code already exists.')
# return code
class CoreSearchForm(forms.Form):

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion
@ -97,7 +97,7 @@ class Migration(migrations.Migration):
(
"country",
models.CharField(
default="United States", help_text="Country", max_length=100
default="Saudi Arabia", help_text="Country", max_length=100
),
),
(
@ -173,7 +173,7 @@ class Migration(migrations.Migration):
(
"currency",
models.CharField(
default="USD",
default="SAR",
help_text="Organization currency code",
max_length=3,
),
@ -676,277 +676,6 @@ class Migration(migrations.Migration):
],
},
),
migrations.CreateModel(
name="Department",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"department_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique department identifier",
unique=True,
),
),
(
"code",
models.CharField(
help_text="Department code (e.g., CARD, EMER, SURG)",
max_length=20,
),
),
("name", models.CharField(help_text="Department name", max_length=100)),
(
"description",
models.TextField(
blank=True, help_text="Department description", null=True
),
),
(
"department_type",
models.CharField(
choices=[
("CLINICAL", "Clinical Department"),
("ANCILLARY", "Ancillary Services"),
("SUPPORT", "Support Services"),
("ADMINISTRATIVE", "Administrative"),
("DIAGNOSTIC", "Diagnostic Services"),
("THERAPEUTIC", "Therapeutic Services"),
("EMERGENCY", "Emergency Services"),
("SURGICAL", "Surgical Services"),
("MEDICAL", "Medical Services"),
("NURSING", "Nursing Services"),
("PHARMACY", "Pharmacy"),
("LABORATORY", "Laboratory"),
("RADIOLOGY", "Radiology"),
("REHABILITATION", "Rehabilitation"),
("MENTAL_HEALTH", "Mental Health"),
("PEDIATRIC", "Pediatric"),
("OBSTETRIC", "Obstetric"),
("ONCOLOGY", "Oncology"),
("CARDIOLOGY", "Cardiology"),
("NEUROLOGY", "Neurology"),
("ORTHOPEDIC", "Orthopedic"),
("OTHER", "Other"),
],
help_text="Type of department",
max_length=30,
),
),
(
"phone",
models.CharField(
blank=True,
help_text="Department phone number",
max_length=20,
null=True,
),
),
(
"extension",
models.CharField(
blank=True,
help_text="Phone extension",
max_length=10,
null=True,
),
),
(
"email",
models.EmailField(
blank=True,
help_text="Department email",
max_length=254,
null=True,
),
),
(
"building",
models.CharField(
blank=True,
help_text="Building name or number",
max_length=50,
null=True,
),
),
(
"floor",
models.CharField(
blank=True,
help_text="Floor number or name",
max_length=20,
null=True,
),
),
(
"wing",
models.CharField(
blank=True,
help_text="Wing or section",
max_length=20,
null=True,
),
),
(
"room_numbers",
models.CharField(
blank=True,
help_text="Room numbers (e.g., 101-110, 201A-205C)",
max_length=100,
null=True,
),
),
(
"is_active",
models.BooleanField(default=True, help_text="Department is active"),
),
(
"is_24_hour",
models.BooleanField(
default=False, help_text="Department operates 24 hours"
),
),
(
"operating_hours",
models.JSONField(
blank=True,
default=dict,
help_text="Operating hours by day of week",
),
),
(
"cost_center_code",
models.CharField(
blank=True,
help_text="Cost center code for financial tracking",
max_length=20,
null=True,
),
),
(
"budget_code",
models.CharField(
blank=True, help_text="Budget code", max_length=20, null=True
),
),
(
"authorized_positions",
models.PositiveIntegerField(
default=0, help_text="Number of authorized positions"
),
),
(
"current_staff_count",
models.PositiveIntegerField(
default=0, help_text="Current number of staff members"
),
),
(
"accreditation_required",
models.BooleanField(
default=False,
help_text="Department requires special accreditation",
),
),
(
"accreditation_body",
models.CharField(
blank=True,
help_text="Accrediting body (e.g., Joint Commission, CAP)",
max_length=100,
null=True,
),
),
(
"last_inspection_date",
models.DateField(
blank=True, help_text="Last inspection date", null=True
),
),
(
"next_inspection_date",
models.DateField(
blank=True,
help_text="Next scheduled inspection date",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"created_by",
models.ForeignKey(
blank=True,
help_text="User who created the department",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="created_departments",
to=settings.AUTH_USER_MODEL,
),
),
(
"department_head",
models.ForeignKey(
blank=True,
help_text="Department head/manager",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="headed_departments",
to=settings.AUTH_USER_MODEL,
),
),
(
"parent_department",
models.ForeignKey(
blank=True,
help_text="Parent department (for hierarchical structure)",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="sub_departments",
to="core.department",
),
),
(
"tenant",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="departments",
to="core.tenant",
),
),
],
options={
"verbose_name": "Department",
"verbose_name_plural": "Departments",
"db_table": "core_department",
"ordering": ["name"],
"indexes": [
models.Index(
fields=["tenant", "department_type"],
name="core_depart_tenant__ef3e04_idx",
),
models.Index(fields=["code"], name="core_depart_code_5a5745_idx"),
models.Index(
fields=["is_active"], name="core_depart_is_acti_ae42f9_idx"
),
models.Index(
fields=["parent_department"],
name="core_depart_parent__70dda4_idx",
),
],
"unique_together": {("tenant", "code")},
},
),
migrations.CreateModel(
name="AuditLogEntry",
fields=[

View File

@ -854,246 +854,246 @@ class IntegrationLog(models.Model):
class Department(models.Model):
"""
Hospital department model for organizational structure.
Represents different departments within a healthcare organization.
"""
DEPARTMENT_TYPE_CHOICES = [
('CLINICAL', 'Clinical Department'),
('ANCILLARY', 'Ancillary Services'),
('SUPPORT', 'Support Services'),
('ADMINISTRATIVE', 'Administrative'),
('DIAGNOSTIC', 'Diagnostic Services'),
('THERAPEUTIC', 'Therapeutic Services'),
('EMERGENCY', 'Emergency Services'),
('SURGICAL', 'Surgical Services'),
('MEDICAL', 'Medical Services'),
('NURSING', 'Nursing Services'),
('PHARMACY', 'Pharmacy'),
('LABORATORY', 'Laboratory'),
('RADIOLOGY', 'Radiology'),
('REHABILITATION', 'Rehabilitation'),
('MENTAL_HEALTH', 'Mental Health'),
('PEDIATRIC', 'Pediatric'),
('OBSTETRIC', 'Obstetric'),
('ONCOLOGY', 'Oncology'),
('CARDIOLOGY', 'Cardiology'),
('NEUROLOGY', 'Neurology'),
('ORTHOPEDIC', 'Orthopedic'),
('OTHER', 'Other'),
]
# Tenant relationship
tenant = models.ForeignKey(
Tenant,
on_delete=models.CASCADE,
related_name='core_departments',
help_text='Organization tenant'
)
# Department Information
department_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique department identifier'
)
code = models.CharField(
max_length=20,
help_text='Department code (e.g., CARD, EMER, SURG)'
)
name = models.CharField(
max_length=100,
help_text='Department name'
)
description = models.TextField(
blank=True,
null=True,
help_text='Department description'
)
# Department Classification
department_type = models.CharField(
max_length=30,
choices=DEPARTMENT_TYPE_CHOICES,
help_text='Type of department'
)
# Organizational Structure
parent_department = models.ForeignKey(
'self',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='sub_departments',
help_text='Parent department (for hierarchical structure)'
)
# Management
department_head = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='headed_departments',
help_text='Department head/manager'
)
# Contact Information
phone = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Department phone number'
)
extension = models.CharField(
max_length=10,
blank=True,
null=True,
help_text='Phone extension'
)
email = models.EmailField(
blank=True,
null=True,
help_text='Department email'
)
# Location
building = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Building name or number'
)
floor = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Floor number or name'
)
wing = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Wing or section'
)
room_numbers = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Room numbers (e.g., 101-110, 201A-205C)'
)
# Operational Information
is_active = models.BooleanField(
default=True,
help_text='Department is active'
)
is_24_hour = models.BooleanField(
default=False,
help_text='Department operates 24 hours'
)
operating_hours = models.JSONField(
default=dict,
blank=True,
help_text='Operating hours by day of week'
)
# Budget and Cost Center
cost_center_code = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Cost center code for financial tracking'
)
budget_code = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Budget code'
)
# Staffing
authorized_positions = models.PositiveIntegerField(
default=0,
help_text='Number of authorized positions'
)
current_staff_count = models.PositiveIntegerField(
default=0,
help_text='Current number of staff members'
)
# Quality and Compliance
accreditation_required = models.BooleanField(
default=False,
help_text='Department requires special accreditation'
)
accreditation_body = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Accrediting body (e.g., Joint Commission, CAP)'
)
last_inspection_date = models.DateField(
blank=True,
null=True,
help_text='Last inspection date'
)
next_inspection_date = models.DateField(
blank=True,
null=True,
help_text='Next scheduled inspection date'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_departments',
help_text='User who created the department'
)
class Meta:
db_table = 'core_department'
verbose_name = 'Department'
verbose_name_plural = 'Departments'
ordering = ['name']
indexes = [
models.Index(fields=['tenant', 'department_type']),
models.Index(fields=['code']),
models.Index(fields=['is_active']),
models.Index(fields=['parent_department']),
]
unique_together = ['tenant', 'code']
def __str__(self):
return f"{self.name} ({self.code})"
@property
def full_name(self):
"""Return full department name with parent if applicable"""
if self.parent_department:
return f"{self.parent_department.name} - {self.name}"
return self.name
@property
def staffing_percentage(self):
"""Calculate current staffing percentage"""
if self.authorized_positions > 0:
return (self.current_staff_count / self.authorized_positions) * 100
return 0
def get_all_sub_departments(self):
"""Get all sub-departments recursively"""
sub_departments = []
for sub_dept in self.sub_departments.all():
sub_departments.append(sub_dept)
sub_departments.extend(sub_dept.get_all_sub_departments())
return sub_departments
# class Department(models.Model):
# """
# Hospital department model for organizational structure.
# Represents different departments within a healthcare organization.
# """
#
# DEPARTMENT_TYPE_CHOICES = [
# ('CLINICAL', 'Clinical Department'),
# ('ANCILLARY', 'Ancillary Services'),
# ('SUPPORT', 'Support Services'),
# ('ADMINISTRATIVE', 'Administrative'),
# ('DIAGNOSTIC', 'Diagnostic Services'),
# ('THERAPEUTIC', 'Therapeutic Services'),
# ('EMERGENCY', 'Emergency Services'),
# ('SURGICAL', 'Surgical Services'),
# ('MEDICAL', 'Medical Services'),
# ('NURSING', 'Nursing Services'),
# ('PHARMACY', 'Pharmacy'),
# ('LABORATORY', 'Laboratory'),
# ('RADIOLOGY', 'Radiology'),
# ('REHABILITATION', 'Rehabilitation'),
# ('MENTAL_HEALTH', 'Mental Health'),
# ('PEDIATRIC', 'Pediatric'),
# ('OBSTETRIC', 'Obstetric'),
# ('ONCOLOGY', 'Oncology'),
# ('CARDIOLOGY', 'Cardiology'),
# ('NEUROLOGY', 'Neurology'),
# ('ORTHOPEDIC', 'Orthopedic'),
# ('OTHER', 'Other'),
# ]
#
# # Tenant relationship
# tenant = models.ForeignKey(
# Tenant,
# on_delete=models.CASCADE,
# related_name='core_departments',
# help_text='Organization tenant'
# )
#
# # Department Information
# department_id = models.UUIDField(
# default=uuid.uuid4,
# unique=True,
# editable=False,
# help_text='Unique department identifier'
# )
# code = models.CharField(
# max_length=20,
# help_text='Department code (e.g., CARD, EMER, SURG)'
# )
# name = models.CharField(
# max_length=100,
# help_text='Department name'
# )
# description = models.TextField(
# blank=True,
# null=True,
# help_text='Department description'
# )
#
# # Department Classification
# department_type = models.CharField(
# max_length=30,
# choices=DEPARTMENT_TYPE_CHOICES,
# help_text='Type of department'
# )
#
# # Organizational Structure
# parent_department = models.ForeignKey(
# 'self',
# on_delete=models.SET_NULL,
# null=True,
# blank=True,
# related_name='sub_departments',
# help_text='Parent department (for hierarchical structure)'
# )
#
# # Management
# department_head = models.ForeignKey(
# settings.AUTH_USER_MODEL,
# on_delete=models.SET_NULL,
# null=True,
# blank=True,
# related_name='headed_departments',
# help_text='Department head/manager'
# )
#
# # Contact Information
# phone = models.CharField(
# max_length=20,
# blank=True,
# null=True,
# help_text='Department phone number'
# )
# extension = models.CharField(
# max_length=10,
# blank=True,
# null=True,
# help_text='Phone extension'
# )
# email = models.EmailField(
# blank=True,
# null=True,
# help_text='Department email'
# )
#
# # Location
# building = models.CharField(
# max_length=50,
# blank=True,
# null=True,
# help_text='Building name or number'
# )
# floor = models.CharField(
# max_length=20,
# blank=True,
# null=True,
# help_text='Floor number or name'
# )
# wing = models.CharField(
# max_length=20,
# blank=True,
# null=True,
# help_text='Wing or section'
# )
# room_numbers = models.CharField(
# max_length=100,
# blank=True,
# null=True,
# help_text='Room numbers (e.g., 101-110, 201A-205C)'
# )
#
# # Operational Information
# is_active = models.BooleanField(
# default=True,
# help_text='Department is active'
# )
# is_24_hour = models.BooleanField(
# default=False,
# help_text='Department operates 24 hours'
# )
# operating_hours = models.JSONField(
# default=dict,
# blank=True,
# help_text='Operating hours by day of week'
# )
#
# # Budget and Cost Center
# cost_center_code = models.CharField(
# max_length=20,
# blank=True,
# null=True,
# help_text='Cost center code for financial tracking'
# )
# budget_code = models.CharField(
# max_length=20,
# blank=True,
# null=True,
# help_text='Budget code'
# )
#
# # Staffing
# authorized_positions = models.PositiveIntegerField(
# default=0,
# help_text='Number of authorized positions'
# )
# current_staff_count = models.PositiveIntegerField(
# default=0,
# help_text='Current number of staff members'
# )
#
# # Quality and Compliance
# accreditation_required = models.BooleanField(
# default=False,
# help_text='Department requires special accreditation'
# )
# accreditation_body = models.CharField(
# max_length=100,
# blank=True,
# null=True,
# help_text='Accrediting body (e.g., Joint Commission, CAP)'
# )
# last_inspection_date = models.DateField(
# blank=True,
# null=True,
# help_text='Last inspection date'
# )
# next_inspection_date = models.DateField(
# blank=True,
# null=True,
# help_text='Next scheduled inspection date'
# )
#
# # Metadata
# created_at = models.DateTimeField(auto_now_add=True)
# updated_at = models.DateTimeField(auto_now=True)
# created_by = models.ForeignKey(
# settings.AUTH_USER_MODEL,
# on_delete=models.SET_NULL,
# null=True,
# blank=True,
# related_name='created_departments',
# help_text='User who created the department'
# )
#
# class Meta:
# db_table = 'core_department'
# verbose_name = 'Department'
# verbose_name_plural = 'Departments'
# ordering = ['name']
# indexes = [
# models.Index(fields=['tenant', 'department_type']),
# models.Index(fields=['code']),
# models.Index(fields=['is_active']),
# models.Index(fields=['parent_department']),
# ]
# unique_together = ['tenant', 'code']
#
# def __str__(self):
# return f"{self.name} ({self.code})"
#
# @property
# def full_name(self):
# """Return full department name with parent if applicable"""
# if self.parent_department:
# return f"{self.parent_department.name} - {self.name}"
# return self.name
#
# @property
# def staffing_percentage(self):
# """Calculate current staffing percentage"""
# if self.authorized_positions > 0:
# return (self.current_staff_count / self.authorized_positions) * 100
# return 0
#
# def get_all_sub_departments(self):
# """Get all sub-departments recursively"""
# sub_departments = []
# for sub_dept in self.sub_departments.all():
# sub_departments.append(sub_dept)
# sub_departments.extend(sub_dept.get_all_sub_departments())
# return sub_departments

View File

@ -23,24 +23,19 @@ urlpatterns = [
path('tenants/<int:pk>/deactivate/', views.deactivate_tenant, name='deactivate_tenant'),
# Department CRUD URLs
path('departments/', views.DepartmentListView.as_view(), name='department_list'),
path('departments/create/', views.DepartmentCreateView.as_view(), name='department_create'),
path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
path('departments/<int:pk>/edit/', views.DepartmentUpdateView.as_view(), name='department_update'),
path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
path('departments/<int:pk>/activate/', views.activate_department, name='activate_department'),
path('departments/<int:pk>/deactivate/', views.deactivate_department, name='deactivate_department'),
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
path('departments/<int:pk>/assign-head/', views.assign_department_head, name='assign_department_head'),
# path('departments/', views.DepartmentListView.as_view(), name='department_list'),
# path('departments/create/', views.DepartmentCreateView.as_view(), name='department_create'),
# path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
# path('departments/<int:pk>/edit/', views.DepartmentUpdateView.as_view(), name='department_update'),
# path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
# System Configuration CRUD URLs
path('system-configuration/create/', views.SystemConfigurationCreateView.as_view(), name='system_configuration_create'),
path('system-configuration/<int:pk>/', views.SystemConfigurationDetailView.as_view(), name='system_configuration_detail'),
path('system-configuration/<int:pk>/edit/', views.SystemConfigurationUpdateView.as_view(), name='system_configuration_update'),
path('system-configuration/<int:pk>/delete/', views.SystemConfigurationDeleteView.as_view(), name='system_configuration_delete'),
path('api/department-hierarchy/', views.get_department_hierarchy, name='get_department_hierarchy'),
path('htmx/department-tree/', views.department_tree, name='department_tree'),
# System Notification CRUD URLs
path('notifications/', views.SystemNotificationListView.as_view(), name='system_notification_list'),
@ -79,14 +74,12 @@ urlpatterns = [
# Search and Filter URLs
path('search/', views.CoreSearchView.as_view(), name='search'),
path('search/tenants/', views.tenant_search, name='tenant_search'),
path('search/departments/', views.department_search, name='department_search'),
path('search/audit-logs/', views.audit_log_search, name='audit_log_search'),
# Bulk Operations
path('tenants/bulk-activate/', views.bulk_activate_tenants, name='bulk_activate_tenants'),
path('tenants/bulk-deactivate/', views.bulk_deactivate_tenants, name='bulk_deactivate_tenants'),
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
path('audit-log/bulk-export/', views.bulk_export_audit_logs, name='bulk_export_audit_logs'),
# API-like endpoints for AJAX

View File

@ -13,19 +13,20 @@ from django.db.models import Count, Q
from django.utils import timezone
from django.contrib import messages
from django.urls import reverse_lazy, reverse
from django.contrib.auth import get_user_model
from accounts.models import User
from datetime import timedelta
from .models import (
Tenant, AuditLogEntry, SystemConfiguration, SystemNotification,
IntegrationLog, Department
IntegrationLog
)
from hr.models import Department
from hr.forms import DepartmentForm
# Create aliases for models to match the views
AuditLog = AuditLogEntry
User = get_user_model()
from .forms import (
TenantForm, SystemConfigurationForm, SystemNotificationForm,
DepartmentForm, CoreSearchForm
TenantForm, SystemConfigurationForm, SystemNotificationForm,CoreSearchForm
)
from .utils import AuditLogger
@ -66,10 +67,6 @@ class DashboardView(LoginRequiredMixin, TemplateView):
return context
# ============================================================================
# TENANT VIEWS (FULL CRUD - Master Data)
# ============================================================================
class TenantListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
"""
List all tenants (Super admin only).
@ -226,10 +223,6 @@ class TenantDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
return redirect(self.success_url)
# ============================================================================
# AUDIT LOG VIEWS (READ-ONLY - System Generated)
# ============================================================================
class AuditLogListView(LoginRequiredMixin, ListView):
"""
Audit log listing view.
@ -302,10 +295,6 @@ class AuditLogDetailView(LoginRequiredMixin, DetailView):
return AuditLogEntry.objects.filter(tenant=tenant)
# ============================================================================
# SYSTEM CONFIGURATION VIEWS (FULL CRUD - Master Data)
# ============================================================================
class SystemConfigurationListView(LoginRequiredMixin, ListView):
"""
System configuration view.
@ -476,10 +465,6 @@ class SystemConfigurationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
return super().delete(request, *args, **kwargs)
# ============================================================================
# SYSTEM NOTIFICATION VIEWS (FULL CRUD - Operational Data)
# ============================================================================
class SystemNotificationListView(LoginRequiredMixin, ListView):
"""
List system notifications.
@ -637,10 +622,6 @@ class SystemNotificationDeleteView(LoginRequiredMixin, PermissionRequiredMixin,
return super().delete(request, *args, **kwargs)
# ============================================================================
# INTEGRATION LOG VIEWS (READ-ONLY - System Generated)
# ============================================================================
class IntegrationLogListView(LoginRequiredMixin, ListView):
"""
List integration logs.
@ -708,189 +689,6 @@ class IntegrationLogDetailView(LoginRequiredMixin, DetailView):
return IntegrationLog.objects.filter(tenant=tenant)
# ============================================================================
# DEPARTMENT VIEWS (FULL CRUD - Master Data)
# ============================================================================
class DepartmentListView(LoginRequiredMixin, ListView):
"""
List departments.
"""
model = Department
template_name = 'core/department_list.html'
context_object_name = 'departments'
paginate_by = 20
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
if not tenant:
return Department.objects.none()
queryset = Department.objects.filter(tenant=tenant).order_by('name')
# Apply search filter
search = self.request.GET.get('search')
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(description__icontains=search) |
Q(location__icontains=search)
)
# Apply status filter
status = self.request.GET.get('status')
if status:
queryset = queryset.filter(is_active=(status == 'active'))
return queryset
class DepartmentDetailView(LoginRequiredMixin, DetailView):
"""
Display department details.
"""
model = Department
template_name = 'core/department_detail.html'
context_object_name = 'department'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
if not tenant:
return Department.objects.none()
return Department.objects.filter(tenant=tenant)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
department = self.object
# Get department statistics
context.update({
'employee_count': department.current_staff_count,
'recent_activity': AuditLogEntry.objects.filter(
tenant=department.tenant,
object_id=str(department.pk),
content_type__model='department'
).order_by('-timestamp')[:10],
})
return context
class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Create new department.
"""
model = Department
form_class = DepartmentForm
template_name = 'core/department_form.html'
permission_required = 'core.add_department'
success_url = reverse_lazy('core:department_list')
def form_valid(self, form):
# Set tenant
form.instance.tenant = getattr(self.request, 'tenant', None)
response = super().form_valid(form)
# Log department creation
AuditLogger.log_event(
tenant=form.instance.tenant,
event_type='CREATE',
event_category='SYSTEM_ADMINISTRATION',
action='Create Department',
description=f'Created department: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Department "{self.object.name}" created successfully.')
return response
class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Update department.
"""
model = Department
form_class = DepartmentForm
template_name = 'core/department_form.html'
permission_required = 'core.change_department'
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
if not tenant:
return Department.objects.none()
return Department.objects.filter(tenant=tenant)
def get_success_url(self):
return reverse('core:department_detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
response = super().form_valid(form)
# Log department update
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action='Update Department',
description=f'Updated department: {self.object.name}',
user=self.request.user,
content_object=self.object,
request=self.request
)
messages.success(self.request, f'Department "{self.object.name}" updated successfully.')
return response
class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
"""
Delete department (soft delete to inactive).
"""
model = Department
template_name = 'core/department_confirm_delete.html'
permission_required = 'core.delete_department'
success_url = reverse_lazy('core:department_list')
def get_queryset(self):
tenant = getattr(self.request, 'tenant', None)
if not tenant:
return Department.objects.none()
return Department.objects.filter(tenant=tenant)
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
# Check if department has employees
if self.object.get_employee_count() > 0:
messages.error(request, 'Cannot delete department with active employees.')
return redirect('core:department_detail', pk=self.object.pk)
# Soft delete - set to inactive
self.object.is_active = False
self.object.save()
# Log department deletion
AuditLogger.log_event(
tenant=self.object.tenant,
event_type='DELETE',
event_category='SYSTEM_ADMINISTRATION',
action='Deactivate Department',
description=f'Deactivated department: {self.object.name}',
user=request.user,
content_object=self.object,
request=request
)
messages.success(request, f'Department "{self.object.name}" deactivated successfully.')
return redirect(self.success_url)
# ============================================================================
# HTMX VIEWS FOR REAL-TIME UPDATES
# ============================================================================
@login_required
def dashboard_stats(request):
"""
@ -1064,10 +862,6 @@ def system_health(request):
})
# ============================================================================
# ACTION VIEWS FOR WORKFLOW OPERATIONS
# ============================================================================
@login_required
def activate_notification(request, pk):
"""
@ -1175,9 +969,6 @@ def reset_configuration(request, pk):
return redirect('core:system_configuration_detail', pk=pk)
# Missing HTMX Views
def tenant_stats(request):
"""
HTMX view for tenant statistics.
@ -1194,20 +985,6 @@ def tenant_stats(request):
return render(request, 'core/partials/tenant_stats.html', {'stats': stats})
def department_tree(request):
"""
HTMX view for department tree structure.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
parent=None
).prefetch_related('children')
return render(request, 'core/partials/department_tree.html', {
'departments': departments
})
def configuration_search(request):
"""
HTMX view for configuration search.
@ -1240,7 +1017,6 @@ def audit_log_list_htmx(request):
})
# Missing Action Views
def activate_tenant(request, pk):
"""
Activate a tenant.
@ -1265,30 +1041,6 @@ def deactivate_tenant(request, pk):
return redirect('core:tenant_detail', pk=pk)
def activate_department(request, pk):
"""
Activate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = True
department.save()
messages.success(request, f'Department "{department.name}" has been activated.')
return redirect('core:department_detail', pk=pk)
def deactivate_department(request, pk):
"""
Deactivate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = False
department.save()
messages.success(request, f'Department "{department.name}" has been deactivated.')
return redirect('core:department_detail', pk=pk)
def reset_system_configuration(request):
"""
Reset system configuration to defaults.
@ -1302,7 +1054,7 @@ def reset_system_configuration(request):
messages.success(request, 'System configuration has been reset to defaults.')
return redirect('core:system_configuration_list')
return render(request, 'core/reset_configuration_confirm.html')
return render(request, 'core/configurations/reset_configuration_confirm.html')
def export_audit_log(request):
@ -1335,7 +1087,6 @@ def export_audit_log(request):
return response
# Missing Search Views
class CoreSearchView(ListView):
"""
Generic search view for core models.
@ -1396,23 +1147,6 @@ def tenant_search(request):
return JsonResponse({'tenants': list(tenants)})
def department_search(request):
"""
AJAX search for departments.
"""
query = request.GET.get('q', '')
departments = []
if query:
departments = Department.objects.filter(
tenant=request.user.tenant,
name__icontains=query
).values('department_id', 'name', 'department_type')[:10]
return JsonResponse({'departments': list(departments)})
# Missing Bulk Operation Views
def bulk_activate_tenants(request):
"""
Bulk activate tenants.
@ -1443,38 +1177,6 @@ def bulk_deactivate_tenants(request):
return redirect('core:tenant_list')
def bulk_activate_departments(request):
"""
Bulk activate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=True)
messages.success(request, f'{count} departments have been activated.')
return redirect('core:department_list')
def bulk_deactivate_departments(request):
"""
Bulk deactivate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=False)
messages.success(request, f'{count} departments have been deactivated.')
return redirect('core:department_list')
def bulk_export_audit_logs(request):
"""
Bulk export audit logs.
@ -1511,7 +1213,6 @@ def bulk_export_audit_logs(request):
return redirect('core:audit_log_list')
# Missing API Views
def validate_tenant_data(request):
"""
AJAX validation for tenant data.
@ -1528,31 +1229,6 @@ def validate_tenant_data(request):
return JsonResponse({'valid': len(errors) == 0, 'errors': errors})
def get_department_hierarchy(request):
"""
Get department hierarchy as JSON.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
is_active=True
).select_related('parent')
def build_tree(parent=None):
children = []
for dept in departments:
if dept.parent == parent:
children.append({
'id': str(dept.department_id),
'name': dept.name,
'type': dept.department_type,
'children': build_tree(dept)
})
return children
hierarchy = build_tree()
return JsonResponse({'hierarchy': hierarchy})
def get_system_status(request):
"""
Get system status information.
@ -1604,8 +1280,6 @@ def backup_configuration(request):
return response
def restore_configuration(request):
"""
Restore system configuration from backup.
@ -1644,111 +1318,184 @@ def restore_configuration(request):
return render(request, 'core/restore_configuration.html')
@login_required
def assign_department_head(request, pk):
"""
Assign a department head to a department.
"""
department = get_object_or_404(Department, pk=pk, tenant=request.user.tenant)
if request.method == 'POST':
user_id = request.POST.get('user_id')
if user_id:
try:
user = User.objects.get(id=user_id, tenant=request.user.tenant)
# Remove current department head if exists
if department.department_head:
old_head = department.department_head
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'old_department_head_id': old_head.id,
'old_department_head_name': old_head.get_full_name()
}
)
# Assign new department head
department.department_head = user
department.save()
# Log the assignment
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Assigned department head to {department.name}',
description=f'Assigned {user.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'new_department_head_id': user.id,
'new_department_head_name': user.get_full_name()
}
)
messages.success(
request,
f'{user.get_full_name()} has been assigned as head of {department.name}.'
)
return redirect('core:department_detail', pk=department.pk)
except User.DoesNotExist:
messages.error(request, 'Selected user not found.')
else:
# Remove department head
if department.department_head:
old_head = department.department_head
department.department_head = None
department.save()
# Log the removal
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'removed_department_head_id': old_head.id,
'removed_department_head_name': old_head.get_full_name()
}
)
messages.success(
request,
f'Department head has been removed from {department.name}.'
)
else:
messages.info(request, 'No department head was assigned.')
return redirect('core:department_detail', pk=department.pk)
# Get eligible users (staff members who can be department heads)
eligible_users = User.objects.filter(
tenant=request.user.tenant,
is_active=True,
is_staff=True
).exclude(
id=department.department_head.id if department.department_head else None
).order_by('first_name', 'last_name')
context = {
'department': department,
'eligible_users': eligible_users,
'current_head': department.department_head,
}
return render(request, 'core/assign_department_head.html', context)
# Department Views
# class DepartmentListView(LoginRequiredMixin, ListView):
# """
# List departments.
# """
# model = Department
# template_name = 'core/department_list.html'
# context_object_name = 'departments'
# paginate_by = 20
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# if not tenant:
# return Department.objects.none()
#
# queryset = Department.objects.filter(tenant=tenant).order_by('name')
#
# # Apply search filter
# search = self.request.GET.get('search')
# if search:
# queryset = queryset.filter(
# Q(name__icontains=search) |
# Q(description__icontains=search) |
# Q(location__icontains=search)
# )
#
# # Apply status filter
# status = self.request.GET.get('status')
# if status:
# queryset = queryset.filter(is_active=(status == 'active'))
#
# return queryset
#
#
# class DepartmentDetailView(LoginRequiredMixin, DetailView):
# """
# Display department details.
# """
# model = Department
# template_name = 'core/department_detail.html'
# context_object_name = 'department'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
#
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# department = self.object
#
# # Get department statistics
# context.update({
# 'employee_count': department.current_staff_count,
# 'recent_activity': AuditLogEntry.objects.filter(
# tenant=department.tenant,
# object_id=str(department.pk),
# content_type__model='department'
# ).order_by('-timestamp')[:10],
# })
#
# return context
#
#
# class DepartmentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
# """
# Create new department.
# """
# model = Department
# form_class = DepartmentForm
# template_name = 'core/department_form.html'
# permission_required = 'core.add_department'
# success_url = reverse_lazy('core:department_list')
#
# def form_valid(self, form):
# # Set tenant
# form.instance.tenant = getattr(self.request, 'tenant', None)
# response = super().form_valid(form)
#
# # Log department creation
# AuditLogger.log_event(
# tenant=form.instance.tenant,
# event_type='CREATE',
# event_category='SYSTEM_ADMINISTRATION',
# action='Create Department',
# description=f'Created department: {self.object.name}',
# user=self.request.user,
# content_object=self.object,
# request=self.request
# )
#
# messages.success(self.request, f'Department "{self.object.name}" created successfully.')
# return response
#
#
# class DepartmentUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
# """
# Update department.
# """
# model = Department
# form_class = DepartmentForm
# template_name = 'core/department_form.html'
# permission_required = 'core.change_department'
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
#
# def get_success_url(self):
# return reverse('core:department_detail', kwargs={'pk': self.object.pk})
#
# def form_valid(self, form):
# response = super().form_valid(form)
#
# # Log department update
# AuditLogger.log_event(
# tenant=self.object.tenant,
# event_type='UPDATE',
# event_category='SYSTEM_ADMINISTRATION',
# action='Update Department',
# description=f'Updated department: {self.object.name}',
# user=self.request.user,
# content_object=self.object,
# request=self.request
# )
#
# messages.success(self.request, f'Department "{self.object.name}" updated successfully.')
# return response
#
#
# class DepartmentDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
# """
# Delete department (soft delete to inactive).
# """
# model = Department
# template_name = 'core/department_confirm_delete.html'
# permission_required = 'core.delete_department'
# success_url = reverse_lazy('core:department_list')
#
# def get_queryset(self):
# tenant = getattr(self.request, 'tenant', None)
# if not tenant:
# return Department.objects.none()
# return Department.objects.filter(tenant=tenant)
#
# def delete(self, request, *args, **kwargs):
# self.object = self.get_object()
#
# # Check if department has employees
# if self.object.get_employee_count() > 0:
# messages.error(request, 'Cannot delete department with active employees.')
# return redirect('core:department_detail', pk=self.object.pk)
#
# # Soft delete - set to inactive
# self.object.is_active = False
# self.object.save()
#
# # Log department deletion
# AuditLogger.log_event(
# tenant=self.object.tenant,
# event_type='DELETE',
# event_category='SYSTEM_ADMINISTRATION',
# action='Deactivate Department',
# description=f'Deactivated department: {self.object.name}',
# user=request.user,
# content_object=self.object,
# request=request
# )
#
# messages.success(request, f'Department "{self.object.name}" deactivated successfully.')
# return redirect(self.success_url)
#
# import json
#

View File

@ -7,13 +7,12 @@ django.setup()
import random
from datetime import datetime, timedelta, timezone
from django.contrib.auth import get_user_model
from accounts.models import User
from django.utils import timezone as django_timezone
from core.models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog, Department
from core.models import Tenant, AuditLogEntry, SystemConfiguration, SystemNotification, IntegrationLog
import uuid
import json
User = get_user_model()
# Saudi-specific data constants
SAUDI_CITIES = [
@ -119,64 +118,64 @@ def create_super_user():
tenant=tenant1 # assumes your User model has a ForeignKey to Tenant named `tenant`
)
def create_saudi_departments(tenants, departments_per_tenant=15):
"""Create Saudi healthcare departments"""
departments = []
department_types = [
('clinical', 'Clinical Department'),
('support', 'Support Department'),
('administrative', 'Administrative Department'),
('diagnostic', 'Diagnostic Department')
]
for tenant in tenants:
# Create main departments
for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
dept_type = random.choice(department_types)
department = Department.objects.create(
tenant=tenant,
department_id=uuid.uuid4(),
code=specialty.replace(' ', '').upper()[:10],
name=f"Department of {specialty}",
description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
department_type=dept_type[0],
parent_department=None, # Main departments
phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
extension=f"{random.randint(1000, 9999)}",
email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
floor=f"Floor {random.randint(1, 10)}",
wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
is_active=True,
is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
operating_hours={
"sunday": {"open": "07:00", "close": "20:00"},
"monday": {"open": "07:00", "close": "20:00"},
"tuesday": {"open": "07:00", "close": "20:00"},
"wednesday": {"open": "07:00", "close": "20:00"},
"thursday": {"open": "07:00", "close": "20:00"},
"friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
"saturday": {"open": "07:00", "close": "20:00"}
},
cost_center_code=f"CC-{random.randint(1000, 9999)}",
budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
authorized_positions=random.randint(5, 50),
current_staff_count=random.randint(3, 45),
accreditation_required=True,
accreditation_body="CBAHI",
last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
next_inspection_date=django_timezone.now() + timedelta(days=random.randint(30, 365)),
created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
updated_at=django_timezone.now()
)
departments.append(department)
print(f"Created {departments_per_tenant} departments for {tenant.name}")
return departments
# def create_saudi_departments(tenants, departments_per_tenant=15):
# """Create Saudi healthcare departments"""
# departments = []
#
# department_types = [
# ('clinical', 'Clinical Department'),
# ('support', 'Support Department'),
# ('administrative', 'Administrative Department'),
# ('diagnostic', 'Diagnostic Department')
# ]
#
# for tenant in tenants:
# # Create main departments
# for specialty in SAUDI_MEDICAL_SPECIALTIES[:departments_per_tenant]:
# dept_type = random.choice(department_types)
#
# department = Department.objects.create(
# tenant=tenant,
# department_id=uuid.uuid4(),
# code=specialty.replace(' ', '').upper()[:10],
# name=f"Department of {specialty}",
# description=f"Specialized {specialty.lower()} department providing comprehensive medical care",
# department_type=dept_type[0],
# parent_department=None, # Main departments
# phone=f"+966-{random.randint(1, 9)}-{random.randint(100, 999)}-{random.randint(1000, 9999)}",
# extension=f"{random.randint(1000, 9999)}",
# email=f"{specialty.lower().replace(' ', '').replace('and', '')}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa",
# building=f"Building {random.choice(['A', 'B', 'C', 'D', 'Medical Tower'])}",
# floor=f"Floor {random.randint(1, 10)}",
# wing=random.choice(['North Wing', 'South Wing', 'East Wing', 'West Wing', 'Central Wing']),
# room_numbers=f"{random.randint(100, 999)}-{random.randint(100, 999)}",
# is_active=True,
# is_24_hour=specialty in ['Emergency Medicine', 'Internal Medicine', 'Cardiology'],
# operating_hours={
# "sunday": {"open": "07:00", "close": "20:00"},
# "monday": {"open": "07:00", "close": "20:00"},
# "tuesday": {"open": "07:00", "close": "20:00"},
# "wednesday": {"open": "07:00", "close": "20:00"},
# "thursday": {"open": "07:00", "close": "20:00"},
# "friday": {"open": "14:00", "close": "20:00"}, # Friday afternoon
# "saturday": {"open": "07:00", "close": "20:00"}
# },
# cost_center_code=f"CC-{random.randint(1000, 9999)}",
# budget_code=f"BG-{specialty.replace(' ', '').upper()[:6]}",
# authorized_positions=random.randint(5, 50),
# current_staff_count=random.randint(3, 45),
# accreditation_required=True,
# accreditation_body="CBAHI",
# last_inspection_date=django_timezone.now() - timedelta(days=random.randint(30, 365)),
# next_inspection_date=django_timezone.now() + timedelta(days=random.randint(30, 365)),
# created_at=django_timezone.now() - timedelta(days=random.randint(1, 180)),
# updated_at=django_timezone.now()
# )
# departments.append(department)
#
# print(f"Created {departments_per_tenant} departments for {tenant.name}")
#
# return departments
def create_saudi_system_configurations(tenants):
@ -439,8 +438,8 @@ def main():
create_super_user()
# Create departments
print("\n2. Creating Saudi Medical Departments...")
departments = create_saudi_departments(tenants, 12)
# print("\n2. Creating Saudi Medical Departments...")
# departments = create_saudi_departments(tenants, 12)
# Create system configurations
print("\n3. Creating Saudi System Configurations...")
@ -461,7 +460,7 @@ def main():
print(f"\n✅ Saudi Healthcare Data Generation Complete!")
print(f"📊 Summary:")
print(f" - Tenants: {len(tenants)}")
print(f" - Departments: {len(departments)}")
# print(f" - Departments: {len(departments)}")
print(f" - System Configurations: {len(configurations)}")
print(f" - Notifications: {len(notifications)}")
print(f" - Audit Logs: {len(audit_logs)}")
@ -469,7 +468,7 @@ def main():
return {
'tenants': tenants,
'departments': departments,
# 'departments': departments,
'configurations': configurations,
'notifications': notifications,
'audit_logs': audit_logs,

Binary file not shown.

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion

View File

@ -537,7 +537,7 @@ def create_encounters(tenants, days_back=30):
suitable_admissions = [
adm for adm in admissions
if adm.patient == patient and
adm.admission_date <= encounter_date
adm.admission_datetime <= start_datetime
]
if suitable_admissions:
linked_admission = random.choice(suitable_admissions)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -213,7 +213,7 @@ class DepartmentAdmin(admin.ModelAdmin):
Admin interface for departments.
"""
list_display = [
'department_code', 'name', 'department_type',
'code', 'name', 'department_type',
'department_head', 'employee_count_display',
'total_fte_display', 'is_active'
]
@ -221,7 +221,7 @@ class DepartmentAdmin(admin.ModelAdmin):
'tenant', 'department_type', 'is_active'
]
search_fields = [
'department_code', 'name', 'description'
'code', 'name', 'description'
]
readonly_fields = [
'department_id', 'employee_count', 'total_fte',
@ -230,7 +230,7 @@ class DepartmentAdmin(admin.ModelAdmin):
fieldsets = [
('Department Information', {
'fields': [
'department_id', 'tenant', 'department_code', 'name', 'description'
'department_id', 'tenant', 'code', 'name', 'description'
]
}),
('Department Type', {

View File

@ -274,7 +274,7 @@ class DepartmentForm(forms.ModelForm):
class Meta:
model = Department
fields = [
'name', 'department_code', 'description', 'department_type',
'name', 'code', 'description', 'department_type',
'parent_department', 'department_head', 'annual_budget',
'cost_center', 'location', 'is_active', 'notes'
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion
@ -40,8 +40,11 @@ class Migration(migrations.Migration):
),
),
(
"department_code",
models.CharField(help_text="Department code", max_length=20),
"code",
models.CharField(
help_text="Department code (e.g., CARD, EMER, SURG)",
max_length=20,
),
),
("name", models.CharField(help_text="Department name", max_length=100)),
(
@ -64,6 +67,33 @@ class Migration(migrations.Migration):
max_length=20,
),
),
(
"phone",
models.CharField(
blank=True,
help_text="Department phone number",
max_length=20,
null=True,
),
),
(
"extension",
models.CharField(
blank=True,
help_text="Phone extension",
max_length=10,
null=True,
),
),
(
"email",
models.EmailField(
blank=True,
help_text="Department email",
max_length=254,
null=True,
),
),
(
"annual_budget",
models.DecimalField(
@ -83,6 +113,12 @@ class Migration(migrations.Migration):
null=True,
),
),
(
"authorized_positions",
models.PositiveIntegerField(
default=0, help_text="Number of authorized positions"
),
),
(
"location",
models.CharField(
@ -96,6 +132,50 @@ class Migration(migrations.Migration):
"is_active",
models.BooleanField(default=True, help_text="Department is active"),
),
(
"is_24_hour",
models.BooleanField(
default=False, help_text="Department operates 24 hours"
),
),
(
"operating_hours",
models.JSONField(
blank=True,
default=dict,
help_text="Operating hours by day of week",
),
),
(
"accreditation_required",
models.BooleanField(
default=False,
help_text="Department requires special accreditation",
),
),
(
"accreditation_body",
models.CharField(
blank=True,
help_text="Accrediting body (e.g., Joint Commission, CAP)",
max_length=100,
null=True,
),
),
(
"last_inspection_date",
models.DateField(
blank=True, help_text="Last inspection date", null=True
),
),
(
"next_inspection_date",
models.DateField(
blank=True,
help_text="Next scheduled inspection date",
null=True,
),
),
(
"notes",
models.TextField(
@ -122,7 +202,7 @@ class Migration(migrations.Migration):
help_text="Parent department",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="child_departments",
related_name="sub_departments",
to="hr.department",
),
),
@ -131,7 +211,7 @@ class Migration(migrations.Migration):
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="hr_departments",
related_name="departments",
to="core.tenant",
),
),
@ -248,6 +328,16 @@ class Migration(migrations.Migration):
blank=True, help_text="Country", max_length=50, null=True
),
),
(
"national_id",
models.CharField(
blank=True,
help_text="National ID",
max_length=10,
null=True,
unique=True,
),
),
(
"date_of_birth",
models.DateField(blank=True, help_text="Date of birth", null=True),
@ -1193,6 +1283,12 @@ class Migration(migrations.Migration):
"passed",
models.BooleanField(default=False, help_text="Training passed"),
),
(
"is_certified",
models.BooleanField(
default=False, help_text="Training is certified"
),
),
(
"certificate_number",
models.CharField(
@ -1299,9 +1395,7 @@ class Migration(migrations.Migration):
),
migrations.AddIndex(
model_name="department",
index=models.Index(
fields=["department_code"], name="hr_departme_departm_078f94_idx"
),
index=models.Index(fields=["code"], name="hr_departme_code_d27daf_idx"),
),
migrations.AddIndex(
model_name="department",
@ -1315,7 +1409,7 @@ class Migration(migrations.Migration):
),
migrations.AlterUniqueTogether(
name="department",
unique_together={("tenant", "department_code")},
unique_together={("tenant", "code")},
),
migrations.AddIndex(
model_name="performancereview",

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.4 on 2025-09-07 14:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("hr", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="trainingrecord",
name="is_certified",
field=models.BooleanField(default=False, help_text="Training is certified"),
),
]

View File

@ -422,7 +422,7 @@ class Department(models.Model):
editable=False,
help_text='Unique department identifier'
)
department_code = models.CharField(
code = models.CharField(
max_length=20,
help_text='Department code (e.g., CARD, EMER, SURG)'
)
@ -570,14 +570,14 @@ class Department(models.Model):
ordering = ['name']
indexes = [
models.Index(fields=['tenant', 'department_type']),
models.Index(fields=['department_code']),
models.Index(fields=['code']),
models.Index(fields=['name']),
models.Index(fields=['is_active']),
]
unique_together = ['tenant', 'department_code']
unique_together = ['tenant', 'code']
def __str__(self):
return f"{self.department_code} - {self.name}"
return f"{self.code} - {self.name}"
@property
def full_name(self):

View File

@ -30,6 +30,14 @@ urlpatterns = [
path('departments/<int:pk>/', views.DepartmentDetailView.as_view(), name='department_detail'),
path('departments/<int:pk>/update/', views.DepartmentUpdateView.as_view(), name='department_update'),
path('departments/<int:pk>/delete/', views.DepartmentDeleteView.as_view(), name='department_delete'),
path('departments/<int:pk>/activate/', views.activate_department, name='activate_department'),
path('departments/<int:pk>/deactivate/', views.deactivate_department, name='deactivate_department'),
path('departments/bulk-activate/', views.bulk_activate_departments, name='bulk_activate_departments'),
path('departments/bulk-deactivate/', views.bulk_deactivate_departments, name='bulk_deactivate_departments'),
path('departments/<int:pk>/assign-head/', views.assign_department_head, name='assign_department_head'),
path('api/department-hierarchy/', views.get_department_hierarchy, name='get_department_hierarchy'),
path('htmx/department-tree/', views.department_tree, name='department_tree'),
path('search/departments/', views.department_search, name='department_search'),
# ============================================================================
# SCHEDULE URLS (LIMITED CRUD - Operational Data)

View File

@ -17,7 +17,7 @@ from django.core.paginator import Paginator
from django.db import transaction
from datetime import datetime, timedelta, date
import json
from accounts.models import User
from .models import (
Employee, Department, Schedule, ScheduleAssignment,
TimeEntry, PerformanceReview, TrainingRecord
@ -26,6 +26,7 @@ from .forms import (
EmployeeForm, DepartmentForm, ScheduleForm, ScheduleAssignmentForm,
TimeEntryForm, PerformanceReviewForm, TrainingRecordForm
)
from core.utils import AuditLogger
class HRDashboardView(LoginRequiredMixin, TemplateView):
@ -1284,6 +1285,222 @@ def api_department_list(request):
return JsonResponse({'departments': list(departments)})
def department_tree(request):
"""
HTMX view for department tree structure.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
parent=None
).prefetch_related('children')
return render(request, 'core/partials/department_tree.html', {
'departments': departments
})
def activate_department(request, pk):
"""
Activate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = True
department.save()
messages.success(request, f'Department "{department.name}" has been activated.')
return redirect('hr:department_detail', pk=pk)
def deactivate_department(request, pk):
"""
Deactivate a department.
"""
department = get_object_or_404(Department, department_id=pk)
department.is_active = False
department.save()
messages.success(request, f'Department "{department.name}" has been deactivated.')
return redirect('hr:department_detail', pk=pk)
def department_search(request):
"""
AJAX search for departments.
"""
query = request.GET.get('q', '')
departments = []
if query:
departments = Department.objects.filter(
tenant=request.user.tenant,
name__icontains=query
).values('department_id', 'name', 'department_type')[:10]
return JsonResponse({'departments': list(departments)})
def bulk_activate_departments(request):
"""
Bulk activate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=True)
messages.success(request, f'{count} departments have been activated.')
return redirect('hr:department_list')
def bulk_deactivate_departments(request):
"""
Bulk deactivate departments.
"""
if request.method == 'POST':
department_ids = request.POST.getlist('department_ids')
count = Department.objects.filter(
tenant=request.user.tenant,
department_id__in=department_ids
).update(is_active=False)
messages.success(request, f'{count} departments have been deactivated.')
return redirect('hr:department_list')
def get_department_hierarchy(request):
"""
Get department hierarchy as JSON.
"""
departments = Department.objects.filter(
tenant=request.user.tenant,
is_active=True
).select_related('parent')
def build_tree(parent=None):
children = []
for dept in departments:
if dept.parent == parent:
children.append({
'id': str(dept.department_id),
'name': dept.name,
'type': dept.department_type,
'children': build_tree(dept)
})
return children
hierarchy = build_tree()
return JsonResponse({'hierarchy': hierarchy})
@login_required
def assign_department_head(request, pk):
"""
Assign a department head to a department.
"""
department = get_object_or_404(Department, pk=pk, tenant=request.user.tenant)
if request.method == 'POST':
user_id = request.POST.get('user_id')
if user_id:
try:
user = User.objects.get(id=user_id, tenant=request.user.tenant)
# Remove current department head if exists
if department.department_head:
old_head = department.department_head
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'old_department_head_id': old_head.id,
'old_department_head_name': old_head.get_full_name()
}
)
# Assign new department head
department.department_head = user
department.save()
# Log the assignment
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Assigned department head to {department.name}',
description=f'Assigned {user.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'new_department_head_id': user.id,
'new_department_head_name': user.get_full_name()
}
)
messages.success(
request,
f'{user.get_full_name()} has been assigned as head of {department.name}.'
)
return redirect('core:department_detail', pk=department.pk)
except User.DoesNotExist:
messages.error(request, 'Selected user not found.')
else:
# Remove department head
if department.department_head:
old_head = department.department_head
department.department_head = None
department.save()
# Log the removal
AuditLogger.log_event(
request=request,
event_type='UPDATE',
event_category='SYSTEM_ADMINISTRATION',
action=f'Removed department head from {department.name}',
description=f'Removed {old_head.get_full_name()} as head of {department.name}',
content_object=department,
additional_data={
'removed_department_head_id': old_head.id,
'removed_department_head_name': old_head.get_full_name()
}
)
messages.success(
request,
f'Department head has been removed from {department.name}.'
)
else:
messages.info(request, 'No department head was assigned.')
return redirect('core:department_detail', pk=department.pk)
# Get eligible users (staff members who can be department heads)
eligible_users = User.objects.filter(
tenant=request.user.tenant,
is_active=True,
is_staff=True
).exclude(
id=department.department_head.id if department.department_head else None
).order_by('first_name', 'last_name')
context = {
'department': department,
'eligible_users': eligible_users,
'current_head': department.department_head,
}
return render(request, 'hr/departments/assign_department_head.html', context)
# Query patterns to use if needed
# # All upcoming sessions for a tenant (next 30 days)
# TrainingSession.objects.filter(

View File

@ -106,7 +106,7 @@ def create_saudi_departments(tenants):
for tenant in tenants:
# Check for existing departments to avoid duplicates
existing_dept_codes = set(
Department.objects.filter(tenant=tenant).values_list('department_code', flat=True)
Department.objects.filter(tenant=tenant).values_list('code', flat=True)
)
for dept_code, dept_name, dept_desc in SAUDI_DEPARTMENTS:
@ -129,7 +129,7 @@ def create_saudi_departments(tenants):
try:
department = Department.objects.create(
tenant=tenant,
department_code=dept_code,
code=dept_code,
name=dept_name,
description=dept_desc,
department_type=dept_type,

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion
@ -403,6 +403,15 @@ class Migration(migrations.Migration):
max_length=30,
),
),
(
"is_operational",
models.BooleanField(default=True, help_text="Operational status"),
),
(
"is_active",
models.BooleanField(default=True, help_text="Active status"),
),
("is_active_out_of_service", models.BooleanField(default=True)),
(
"room_type",
models.CharField(
@ -518,10 +527,12 @@ class Migration(migrations.Migration):
models.CharField(
blank=True,
choices=[
("WINDOW", "Window Side"),
("DOOR", "Door Side"),
("CENTER", "Center"),
("CORNER", "Corner"),
("A", "A"),
("B", "B"),
("C", "C"),
("D", "D"),
("E", "E"),
("F", "F"),
],
help_text="Position within room",
max_length=20,
@ -619,7 +630,7 @@ class Migration(migrations.Migration):
),
(
"ward_id",
models.CharField(help_text="Unique ward identifier", max_length=20),
models.CharField(help_text="Unique ward identifier", max_length=50),
),
("name", models.CharField(help_text="Ward name", max_length=200)),
(

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-19 15:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inpatients", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="ward",
name="ward_id",
field=models.CharField(help_text="Unique ward identifier", max_length=50),
),
]

View File

@ -1,31 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-19 19:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inpatients", "0002_alter_ward_ward_id"),
]
operations = [
migrations.AlterField(
model_name="bed",
name="bed_position",
field=models.CharField(
blank=True,
choices=[
("A", "A"),
("B", "B"),
("C", "C"),
("D", "D"),
("E", "E"),
("F", "F"),
],
help_text="Position within room",
max_length=20,
null=True,
),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 5.2.4 on 2025-09-03 15:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inpatients", "0003_alter_bed_bed_position"),
]
operations = [
migrations.AddField(
model_name="bed",
name="is_active",
field=models.BooleanField(default=True, help_text="Active status"),
),
migrations.AddField(
model_name="bed",
name="is_active_out_of_service",
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name="bed",
name="is_operational",
field=models.BooleanField(default=True, help_text="Operational status"),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.core.validators
import django.db.models.deletion
@ -325,6 +325,12 @@ class Migration(migrations.Migration):
default=0, help_text="Reorder quantity"
),
),
(
"min_stock_level",
models.PositiveIntegerField(
blank=True, help_text="Minimum stock level", null=True
),
),
(
"max_stock_level",
models.PositiveIntegerField(

View File

@ -1,20 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-05 13:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("inventory", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="inventoryitem",
name="min_stock_level",
field=models.PositiveIntegerField(
blank=True, help_text="Minimum stock level", null=True
),
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
import django.utils.timezone
@ -19,6 +19,177 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name="LabOrder",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"order_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique order identifier",
unique=True,
),
),
(
"order_number",
models.CharField(
help_text="Lab order number", max_length=20, unique=True
),
),
(
"order_datetime",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Date and time order was placed",
),
),
(
"priority",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("STAT", "STAT"),
("ASAP", "ASAP"),
("TIMED", "Timed"),
],
default="ROUTINE",
help_text="Order priority",
max_length=20,
),
),
(
"clinical_indication",
models.TextField(
blank=True, help_text="Clinical indication for tests", null=True
),
),
(
"diagnosis_code",
models.CharField(
blank=True,
help_text="ICD-10 diagnosis code",
max_length=20,
null=True,
),
),
(
"clinical_notes",
models.TextField(blank=True, help_text="Clinical notes", null=True),
),
(
"collection_datetime",
models.DateTimeField(
blank=True,
help_text="Requested collection date and time",
null=True,
),
),
(
"collection_location",
models.CharField(
blank=True,
help_text="Collection location",
max_length=100,
null=True,
),
),
(
"fasting_status",
models.CharField(
choices=[
("FASTING", "Fasting"),
("NON_FASTING", "Non-Fasting"),
("UNKNOWN", "Unknown"),
],
default="UNKNOWN",
help_text="Patient fasting status",
max_length=20,
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("SCHEDULED", "Scheduled"),
("COLLECTED", "Collected"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("CANCELLED", "Cancelled"),
("ON_HOLD", "On Hold"),
],
default="PENDING",
help_text="Order status",
max_length=20,
),
),
(
"special_instructions",
models.TextField(
blank=True,
help_text="Special instructions for collection or processing",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"encounter",
models.ForeignKey(
blank=True,
help_text="Related encounter",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="lab_orders",
to="emr.encounter",
),
),
(
"ordering_provider",
models.ForeignKey(
help_text="Ordering provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="ordered_lab_tests",
to=settings.AUTH_USER_MODEL,
),
),
(
"patient",
models.ForeignKey(
help_text="Patient",
on_delete=django.db.models.deletion.CASCADE,
related_name="lab_orders",
to="patients.patientprofile",
),
),
(
"tenant",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="lab_orders",
to="core.tenant",
),
),
],
options={
"verbose_name": "Lab Order",
"verbose_name_plural": "Lab Orders",
"db_table": "laboratory_lab_order",
"ordering": ["-order_datetime"],
},
),
migrations.CreateModel(
name="LabTest",
fields=[
@ -352,7 +523,7 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name="LabOrder",
name="LabResult",
fields=[
(
"id",
@ -364,89 +535,123 @@ class Migration(migrations.Migration):
),
),
(
"order_id",
"result_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique order identifier",
help_text="Unique result identifier",
unique=True,
),
),
(
"order_number",
"result_value",
models.TextField(
blank=True, help_text="Test result value", null=True
),
),
(
"result_unit",
models.CharField(
help_text="Lab order number", max_length=20, unique=True
blank=True,
help_text="Result unit of measure",
max_length=20,
null=True,
),
),
(
"order_datetime",
models.DateTimeField(
default=django.utils.timezone.now,
help_text="Date and time order was placed",
),
),
(
"priority",
"result_type",
models.CharField(
choices=[
("ROUTINE", "Routine"),
("URGENT", "Urgent"),
("STAT", "STAT"),
("ASAP", "ASAP"),
("TIMED", "Timed"),
("NUMERIC", "Numeric"),
("TEXT", "Text"),
("CODED", "Coded"),
("NARRATIVE", "Narrative"),
],
default="ROUTINE",
help_text="Order priority",
default="NUMERIC",
help_text="Type of result",
max_length=20,
),
),
(
"clinical_indication",
models.TextField(
blank=True, help_text="Clinical indication for tests", null=True
),
),
(
"diagnosis_code",
"reference_range",
models.CharField(
blank=True,
help_text="ICD-10 diagnosis code",
max_length=20,
null=True,
),
),
(
"clinical_notes",
models.TextField(blank=True, help_text="Clinical notes", null=True),
),
(
"collection_datetime",
models.DateTimeField(
blank=True,
help_text="Requested collection date and time",
null=True,
),
),
(
"collection_location",
models.CharField(
blank=True,
help_text="Collection location",
help_text="Reference range",
max_length=100,
null=True,
),
),
(
"fasting_status",
"abnormal_flag",
models.CharField(
blank=True,
choices=[
("FASTING", "Fasting"),
("NON_FASTING", "Non-Fasting"),
("UNKNOWN", "Unknown"),
("N", "Normal"),
("H", "High"),
("L", "Low"),
("HH", "Critical High"),
("LL", "Critical Low"),
("A", "Abnormal"),
],
default="UNKNOWN",
help_text="Patient fasting status",
max_length=20,
help_text="Abnormal flag",
max_length=10,
null=True,
),
),
(
"is_critical",
models.BooleanField(
default=False, help_text="Result is critical value"
),
),
(
"critical_called",
models.BooleanField(
default=False, help_text="Critical value was called to provider"
),
),
(
"critical_called_datetime",
models.DateTimeField(
blank=True,
help_text="Date and time critical value was called",
null=True,
),
),
(
"critical_called_to",
models.CharField(
blank=True,
help_text="Person critical value was called to",
max_length=100,
null=True,
),
),
(
"analyzed_datetime",
models.DateTimeField(
blank=True, help_text="Date and time analyzed", null=True
),
),
(
"analyzer",
models.CharField(
blank=True,
help_text="Analyzer/instrument used",
max_length=100,
null=True,
),
),
(
"verified",
models.BooleanField(
default=False, help_text="Result has been verified"
),
),
(
"verified_datetime",
models.DateTimeField(
blank=True, help_text="Date and time of verification", null=True
),
),
(
@ -454,82 +659,108 @@ class Migration(migrations.Migration):
models.CharField(
choices=[
("PENDING", "Pending"),
("SCHEDULED", "Scheduled"),
("COLLECTED", "Collected"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("VERIFIED", "Verified"),
("AMENDED", "Amended"),
("CANCELLED", "Cancelled"),
("ON_HOLD", "On Hold"),
],
default="PENDING",
help_text="Order status",
help_text="Result status",
max_length=20,
),
),
(
"special_instructions",
"technician_comments",
models.TextField(
blank=True, help_text="Technician comments", null=True
),
),
(
"pathologist_comments",
models.TextField(
blank=True, help_text="Pathologist comments", null=True
),
),
(
"qc_passed",
models.BooleanField(
default=True, help_text="Quality control passed"
),
),
(
"qc_notes",
models.TextField(
blank=True, help_text="Quality control notes", null=True
),
),
(
"reported_datetime",
models.DateTimeField(
blank=True,
help_text="Special instructions for collection or processing",
help_text="Date and time result was reported",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"encounter",
"analyzed_by",
models.ForeignKey(
blank=True,
help_text="Related encounter",
help_text="Lab technician who analyzed",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="lab_orders",
to="emr.encounter",
),
),
(
"ordering_provider",
models.ForeignKey(
help_text="Ordering provider",
on_delete=django.db.models.deletion.CASCADE,
related_name="ordered_lab_tests",
related_name="analyzed_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"patient",
"order",
models.ForeignKey(
help_text="Patient",
help_text="Related lab order",
on_delete=django.db.models.deletion.CASCADE,
related_name="lab_orders",
to="patients.patientprofile",
related_name="results",
to="laboratory.laborder",
),
),
(
"tenant",
"verified_by",
models.ForeignKey(
help_text="Organization tenant",
on_delete=django.db.models.deletion.CASCADE,
related_name="lab_orders",
to="core.tenant",
blank=True,
help_text="Lab professional who verified result",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verified_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"tests",
models.ManyToManyField(
help_text="Ordered tests",
related_name="orders",
"test",
models.ForeignKey(
help_text="Lab test",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.labtest",
),
),
],
options={
"verbose_name": "Lab Order",
"verbose_name_plural": "Lab Orders",
"db_table": "laboratory_lab_order",
"ordering": ["-order_datetime"],
"verbose_name": "Lab Result",
"verbose_name_plural": "Lab Results",
"db_table": "laboratory_lab_result",
"ordering": ["-analyzed_datetime"],
},
),
migrations.AddField(
model_name="laborder",
name="tests",
field=models.ManyToManyField(
help_text="Ordered tests",
related_name="orders",
to="laboratory.labtest",
),
),
migrations.CreateModel(
name="QualityControl",
fields=[
@ -647,6 +878,14 @@ class Migration(migrations.Migration):
to=settings.AUTH_USER_MODEL,
),
),
(
"result",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="quality_controls",
to="laboratory.labresult",
),
),
(
"reviewed_by",
models.ForeignKey(
@ -1033,244 +1272,15 @@ class Migration(migrations.Migration):
"ordering": ["-collected_datetime"],
},
),
migrations.CreateModel(
name="LabResult",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"result_id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
help_text="Unique result identifier",
unique=True,
),
),
(
"result_value",
models.TextField(
blank=True, help_text="Test result value", null=True
),
),
(
"result_unit",
models.CharField(
blank=True,
help_text="Result unit of measure",
max_length=20,
null=True,
),
),
(
"result_type",
models.CharField(
choices=[
("NUMERIC", "Numeric"),
("TEXT", "Text"),
("CODED", "Coded"),
("NARRATIVE", "Narrative"),
],
default="NUMERIC",
help_text="Type of result",
max_length=20,
),
),
(
"reference_range",
models.CharField(
blank=True,
help_text="Reference range",
max_length=100,
null=True,
),
),
(
"abnormal_flag",
models.CharField(
blank=True,
choices=[
("N", "Normal"),
("H", "High"),
("L", "Low"),
("HH", "Critical High"),
("LL", "Critical Low"),
("A", "Abnormal"),
],
help_text="Abnormal flag",
max_length=10,
null=True,
),
),
(
"is_critical",
models.BooleanField(
default=False, help_text="Result is critical value"
),
),
(
"critical_called",
models.BooleanField(
default=False, help_text="Critical value was called to provider"
),
),
(
"critical_called_datetime",
models.DateTimeField(
blank=True,
help_text="Date and time critical value was called",
null=True,
),
),
(
"critical_called_to",
models.CharField(
blank=True,
help_text="Person critical value was called to",
max_length=100,
null=True,
),
),
(
"analyzed_datetime",
models.DateTimeField(
blank=True, help_text="Date and time analyzed", null=True
),
),
(
"analyzer",
models.CharField(
blank=True,
help_text="Analyzer/instrument used",
max_length=100,
null=True,
),
),
(
"verified",
models.BooleanField(
default=False, help_text="Result has been verified"
),
),
(
"verified_datetime",
models.DateTimeField(
blank=True, help_text="Date and time of verification", null=True
),
),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("IN_PROGRESS", "In Progress"),
("COMPLETED", "Completed"),
("VERIFIED", "Verified"),
("AMENDED", "Amended"),
("CANCELLED", "Cancelled"),
],
default="PENDING",
help_text="Result status",
max_length=20,
),
),
(
"technician_comments",
models.TextField(
blank=True, help_text="Technician comments", null=True
),
),
(
"pathologist_comments",
models.TextField(
blank=True, help_text="Pathologist comments", null=True
),
),
(
"qc_passed",
models.BooleanField(
default=True, help_text="Quality control passed"
),
),
(
"qc_notes",
models.TextField(
blank=True, help_text="Quality control notes", null=True
),
),
(
"reported_datetime",
models.DateTimeField(
blank=True,
help_text="Date and time result was reported",
null=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"analyzed_by",
models.ForeignKey(
blank=True,
help_text="Lab technician who analyzed",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="analyzed_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"order",
models.ForeignKey(
help_text="Related lab order",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.laborder",
),
),
(
"verified_by",
models.ForeignKey(
blank=True,
help_text="Lab professional who verified result",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="verified_results",
to=settings.AUTH_USER_MODEL,
),
),
(
"test",
models.ForeignKey(
help_text="Lab test",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.labtest",
),
),
(
"specimen",
models.ForeignKey(
help_text="Specimen used for test",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.specimen",
),
),
],
options={
"verbose_name": "Lab Result",
"verbose_name_plural": "Lab Results",
"db_table": "laboratory_lab_result",
"ordering": ["-analyzed_datetime"],
},
migrations.AddField(
model_name="labresult",
name="specimen",
field=models.ForeignKey(
help_text="Specimen used for test",
on_delete=django.db.models.deletion.CASCADE,
related_name="results",
to="laboratory.specimen",
),
),
migrations.AddIndex(
model_name="labtest",

View File

@ -1,25 +0,0 @@
# Generated by Django 5.2.4 on 2025-08-28 19:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("laboratory", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="qualitycontrol",
name="result",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
related_name="quality_controls",
to="laboratory.labresult",
),
preserve_default=False,
),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.4 on 2025-08-04 04:41
# Generated by Django 5.2.6 on 2025-09-08 07:28
import django.db.models.deletion
import uuid
@ -1283,7 +1283,7 @@ class Migration(migrations.Migration):
models.ForeignKey(
help_text="Operating surgeon",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgical_notes",
related_name="surgeon_surgical_notes",
to=settings.AUTH_USER_MODEL,
),
),
@ -1292,7 +1292,7 @@ class Migration(migrations.Migration):
models.OneToOneField(
help_text="Related surgical case",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgical_note",
related_name="surgical_notes",
to="operating_theatre.surgicalcase",
),
),

View File

@ -1,36 +0,0 @@
# Generated by Django 5.2.4 on 2025-09-04 15:07
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("operating_theatre", "0001_initial"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name="surgicalnote",
name="surgeon",
field=models.ForeignKey(
help_text="Operating surgeon",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgeon_surgical_notes",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterField(
model_name="surgicalnote",
name="surgical_case",
field=models.OneToOneField(
help_text="Related surgical case",
on_delete=django.db.models.deletion.CASCADE,
related_name="surgical_notes",
to="operating_theatre.surgicalcase",
),
),
]

View File

@ -76,13 +76,14 @@ class EmergencyContactForm(forms.ModelForm):
"""
class Meta:
model = EmergencyContact
exclude = ["patient"]
fields = [
'patient', 'first_name', 'last_name', 'relationship', 'phone_number',
'first_name', 'last_name', 'relationship', 'phone_number',
'mobile_number', 'email', 'address_line_1', 'address_line_2', 'city',
'state', 'zip_code', 'priority', 'is_authorized_for_medical_decisions'
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-select'}),
# 'patient': forms.Select(attrs={'class': 'form-select'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'relationship': forms.Select(attrs={'class': 'form-select'}),
@ -102,11 +103,11 @@ class EmergencyContactForm(forms.ModelForm):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('last_name', 'first_name')
# if user and hasattr(user, 'tenant'):
# self.fields['patient'].queryset = PatientProfile.objects.filter(
# tenant=user.tenant,
# is_active=True
# ).order_by('last_name', 'first_name')
class InsuranceInfoForm(forms.ModelForm):
@ -115,15 +116,15 @@ class InsuranceInfoForm(forms.ModelForm):
"""
class Meta:
model = InsuranceInfo
exclude = ["patient"]
fields = [
'patient', 'insurance_type', 'insurance_company', 'plan_name', 'plan_type',
'insurance_type', 'insurance_company', 'plan_name', 'plan_type',
'policy_number', 'group_number', 'subscriber_name', 'subscriber_relationship',
'subscriber_dob', 'subscriber_ssn', 'effective_date', 'termination_date',
'copay_amount', 'deductible_amount', 'out_of_pocket_max', 'is_verified',
'requires_authorization'
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-select'}),
'insurance_type': forms.Select(attrs={'class': 'form-select'}),
'insurance_company': forms.TextInput(attrs={'class': 'form-control'}),
'plan_name': forms.TextInput(attrs={'class': 'form-control'}),
@ -147,11 +148,11 @@ class InsuranceInfoForm(forms.ModelForm):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('last_name', 'first_name')
# if user and hasattr(user, 'tenant'):
# self.fields['patient'].queryset = PatientProfile.objects.filter(
# tenant=user.tenant,
# is_active=True
# ).order_by('last_name', 'first_name')
class ConsentTemplateForm(forms.ModelForm):

File diff suppressed because it is too large Load Diff

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