Patient Information
@@ -830,7 +1016,7 @@
-
+
Related Information
@@ -878,7 +1064,7 @@
-
+
Encounter Timeline
@@ -938,6 +1124,24 @@
+
+
+
+
+
+
{% endblock %}
{% block js %}
@@ -945,6 +1149,8 @@
+
+
diff --git a/emr/templates/emr/partials/problem_form_modal.html b/emr/templates/emr/partials/problem_form_modal.html
new file mode 100644
index 00000000..a9084455
--- /dev/null
+++ b/emr/templates/emr/partials/problem_form_modal.html
@@ -0,0 +1,183 @@
+
+
+
+
+
diff --git a/emr/urls.py b/emr/urls.py
index 8c61c736..bc8185dc 100644
--- a/emr/urls.py
+++ b/emr/urls.py
@@ -56,7 +56,8 @@ urlpatterns = [
# Actions
# path('record-create/', views.RecordCreateView.as_view(), name='record_create'),
path('encounter/
/vitals/add/', views.add_vital_signs, name='add_vital_signs'),
- path('patient//problem/add/', views.add_problem, name='add_problem'),
+ path('encounter//problem/add/', views.add_problem, name='add_problem'),
+ path('encounter//care-plan/add/', views.add_care_plan, name='add_care_plan'),
path('encounter//status/', views.update_encounter_status, name='update_encounter_status'),
path('note//sign/', views.sign_note, name='sign_note'),
path('problem//resolve/', views.resolve_problem, name='resolve_problem'),
diff --git a/emr/views.py b/emr/views.py
index a21dbce9..9efec5ab 100644
--- a/emr/views.py
+++ b/emr/views.py
@@ -229,7 +229,9 @@ class EncounterDetailView(LoginRequiredMixin, DetailView):
).prefetch_related(
'vital_signs__measured_by',
'clinical_notes__author',
- 'problems_identified__diagnosing_provider'
+ 'problems_identified__diagnosing_provider',
+ 'problems_identified__care_plans',
+ 'problems_identified',
)
def get_context_data(self, **kwargs):
@@ -1863,52 +1865,127 @@ def add_vital_signs(request, pk):
@login_required
-def add_problem(request, patient_id):
+def add_problem(request, encounter_id):
"""
- HTMX endpoint for adding a problem.
+ HTMX endpoint for adding a problem to an encounter.
"""
+ tenant = request.user.tenant
+ encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
+
if request.method == 'POST':
- patient = get_object_or_404(
- PatientProfile,
- id=patient_id,
- tenant=request.user.tenant
- )
-
- problem_data = {
- 'tenant': request.user.tenant,
- 'patient': patient,
- 'problem_name': request.POST.get('problem_name'),
- 'problem_type': request.POST.get('problem_type', 'DIAGNOSIS'),
- 'severity': request.POST.get('severity'),
- 'priority': request.POST.get('priority', 'MEDIUM'),
+ form = ProblemListForm(request.POST, tenant=tenant)
+ if form.is_valid():
+ problem = form.save(commit=False)
+ problem.tenant = tenant
+ problem.patient = encounter.patient
+ problem.diagnosing_provider = request.user
+ problem.related_encounter = encounter
+ problem.save()
+
+ # Log the action
+ try:
+ AuditLogEntry.objects.create(
+ tenant=tenant,
+ user=request.user,
+ event_type='CREATE',
+ event_category='CLINICAL_DATA',
+ action='Add Problem',
+ description=f'Problem added: {problem.problem_name}',
+ content_type=ContentType.objects.get_for_model(ProblemList),
+ object_id=problem.pk,
+ object_repr=str(problem),
+ patient_id=str(encounter.patient.patient_id),
+ patient_mrn=encounter.patient.mrn,
+ changes={'problem_type': problem.problem_type, 'status': problem.status}
+ )
+ except Exception as e:
+ print(f"Audit logging failed: {e}")
+
+ messages.success(request, f'Problem "{problem.problem_name}" added successfully')
+ return redirect('emr:encounter_detail', pk=encounter_id)
+ else:
+ # Return form with errors
+ return render(request, 'emr/partials/problem_form_modal.html', {
+ 'form': form,
+ 'encounter': encounter,
+ 'patient': encounter.patient
+ })
+ else:
+ # GET request - show form
+ form = ProblemListForm(tenant=tenant, initial={
+ 'patient': encounter.patient,
'diagnosing_provider': request.user,
- 'clinical_notes': request.POST.get('clinical_notes', ''),
- }
+ 'related_encounter': encounter,
+ 'status': 'ACTIVE',
+ 'priority': 'MEDIUM'
+ })
+ return render(request, 'emr/partials/problem_form_modal.html', {
+ 'form': form,
+ 'encounter': encounter,
+ 'patient': encounter.patient
+ })
- # Add optional fields
- if request.POST.get('problem_code'):
- problem_data['problem_code'] = request.POST.get('problem_code')
- problem_data['coding_system'] = request.POST.get('coding_system', 'ICD10')
- if request.POST.get('onset_date'):
- problem_data['onset_date'] = request.POST.get('onset_date')
-
- # Create problem
- problem = ProblemList.objects.create(**problem_data)
-
- # Log the action
- AuditLogger.log_event(
- user=request.user,
- action='PROBLEM_ADDED',
- model='ProblemList',
- object_id=problem.id,
- details=f"Problem added for {patient.get_full_name()}: {problem.problem_name}"
- )
-
- messages.success(request, f'Problem "{problem.problem_name}" added successfully')
- return JsonResponse({'success': True, 'problem_id': problem.id})
-
- return JsonResponse({'error': 'Invalid request'}, status=400)
+@login_required
+def add_care_plan(request, encounter_id):
+ """
+ HTMX endpoint for adding a care plan to an encounter.
+ """
+ tenant = request.user.tenant
+ encounter = get_object_or_404(Encounter, id=encounter_id, tenant=tenant)
+
+ if request.method == 'POST':
+ form = CarePlanForm(request.POST, tenant=tenant)
+ if form.is_valid():
+ care_plan = form.save(commit=False)
+ care_plan.tenant = tenant
+ care_plan.patient = encounter.patient
+ care_plan.primary_provider = request.user
+ care_plan.save()
+ form.save_m2m() # Save many-to-many relationships
+
+ # Log the action
+ try:
+ AuditLogEntry.objects.create(
+ tenant=tenant,
+ user=request.user,
+ event_type='CREATE',
+ event_category='CARE_PLAN',
+ action='Add Care Plan',
+ description=f'Care plan created: {care_plan.title}',
+ content_type=ContentType.objects.get_for_model(CarePlan),
+ object_id=care_plan.pk,
+ object_repr=str(care_plan),
+ patient_id=str(encounter.patient.patient_id),
+ patient_mrn=encounter.patient.mrn,
+ changes={'plan_type': care_plan.plan_type, 'status': care_plan.status}
+ )
+ except Exception as e:
+ print(f"Audit logging failed: {e}")
+
+ messages.success(request, f'Care plan "{care_plan.title}" created successfully')
+ return redirect('emr:encounter_detail', pk=encounter_id)
+ else:
+ # Return form with errors
+ return render(request, 'emr/partials/care_plan_form_modal.html', {
+ 'form': form,
+ 'encounter': encounter,
+ 'patient': encounter.patient
+ })
+ else:
+ # GET request - show form
+ form = CarePlanForm(tenant=tenant, initial={
+ 'patient': encounter.patient,
+ 'primary_provider': request.user,
+ 'start_date': timezone.now().date(),
+ 'status': 'DRAFT',
+ 'priority': 'ROUTINE'
+ })
+ return render(request, 'emr/partials/care_plan_form_modal.html', {
+ 'form': form,
+ 'encounter': encounter,
+ 'patient': encounter.patient
+ })
@login_required
@@ -2012,7 +2089,6 @@ def get_status_class(status):
return status_classes.get(status, 'secondary')
-
def _norm_code(s: str) -> str:
return (s or "").upper().replace(" ", "")
diff --git a/facility_management/__pycache__/forms.cpython-312.pyc b/facility_management/__pycache__/forms.cpython-312.pyc
index 90335704..2c60cf00 100644
Binary files a/facility_management/__pycache__/forms.cpython-312.pyc and b/facility_management/__pycache__/forms.cpython-312.pyc differ
diff --git a/facility_management/forms.py b/facility_management/forms.py
index 69323da3..2a86e507 100644
--- a/facility_management/forms.py
+++ b/facility_management/forms.py
@@ -21,7 +21,6 @@ class BuildingForm(forms.ModelForm):
'name': forms.TextInput(attrs={'class': 'form-control'}),
'code': forms.TextInput(attrs={'class': 'form-control'}),
'building_type': forms.Select(attrs={'class': 'form-select'}),
- 'airport_code': forms.TextInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'floor_count': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
'total_area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
@@ -185,8 +184,9 @@ class MaintenanceRequestForm(forms.ModelForm):
model = MaintenanceRequest
fields = [
'title', 'description', 'maintenance_type', 'building',
- 'floor', 'room', 'asset', 'priority', 'scheduled_date', 'actual_cost',
- 'estimated_cost', 'notes', 'assigned_to', 'scheduled_date', 'estimated_hours', 'status',
+ 'floor', 'room', 'asset', 'priority', 'scheduled_date',
+ 'estimated_cost', 'actual_cost', 'notes', 'assigned_to',
+ 'estimated_hours', 'status', 'completion_notes'
]
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
@@ -199,11 +199,12 @@ class MaintenanceRequestForm(forms.ModelForm):
'priority': forms.Select(attrs={'class': 'form-select'}),
'scheduled_date': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
'estimated_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
+ 'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'assigned_to': forms.Select(attrs={'class': 'form-select'}),
- 'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
+ 'estimated_hours': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.5', 'min': '0'}),
'status': forms.Select(attrs={'class': 'form-select'}),
- 'actual_cost': forms.NumberInput(attrs={'class': 'form-control', 'type': 'number'}),
+ 'completion_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
}
def __init__(self, *args, **kwargs):
@@ -428,4 +429,3 @@ class MaintenanceFilterForm(forms.Form):
super().__init__(*args, **kwargs)
self.fields['assigned_to'].queryset = User.objects.filter(is_active=True)
-
diff --git a/facility_management/migrations/0001_initial.py b/facility_management/migrations/0001_initial.py
deleted file mode 100644
index 7b7a96e7..00000000
--- a/facility_management/migrations/0001_initial.py
+++ /dev/null
@@ -1,1194 +0,0 @@
-# Generated by Django 5.2.6 on 2025-09-26 18:33
-
-import django.core.validators
-import django.db.models.deletion
-import uuid
-from decimal import Decimal
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ("core", "0001_initial"),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- migrations.CreateModel(
- name="AssetCategory",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=100, unique=True)),
- ("code", models.CharField(max_length=20, unique=True)),
- ("description", models.TextField(blank=True)),
- ("is_active", models.BooleanField(default=True)),
- (
- "parent_category",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.assetcategory",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Asset Categories",
- "db_table": "facility_management_asset_categories",
- "ordering": ["name"],
- },
- ),
- migrations.CreateModel(
- name="Building",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=100)),
- (
- "building_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- ("code", models.CharField(max_length=10, unique=True)),
- (
- "building_type",
- models.CharField(
- choices=[
- ("CLINICAL", "Clinical"),
- ("NON_CLINICAL", "Non Clinical"),
- ("OTHER", "Other"),
- ],
- default="CLINICAL",
- max_length=20,
- ),
- ),
- ("address", models.TextField()),
- (
- "latitude",
- models.DecimalField(
- blank=True, decimal_places=6, max_digits=9, null=True
- ),
- ),
- (
- "longitude",
- models.DecimalField(
- blank=True, decimal_places=6, max_digits=9, null=True
- ),
- ),
- ("floor_count", models.PositiveIntegerField(default=1)),
- (
- "total_area_sqm",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- (
- "construction_year",
- models.PositiveIntegerField(blank=True, null=True),
- ),
- ("architect", models.CharField(blank=True, max_length=100)),
- ("contractor", models.CharField(blank=True, max_length=100)),
- ("is_active", models.BooleanField(default=True)),
- ("last_major_renovation", models.DateField(blank=True, null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "facility_manager",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "tenant",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="buildings",
- to="core.tenant",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Buildings",
- "db_table": "facility_management_buildings",
- "ordering": ["code", "name"],
- },
- ),
- migrations.CreateModel(
- name="EnergyMeter",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("meter_id", models.CharField(max_length=50, unique=True)),
- (
- "meter_type",
- models.CharField(
- choices=[
- ("ELECTRICITY", "Electricity"),
- ("GAS", "Natural Gas"),
- ("WATER", "Water"),
- ("STEAM", "Steam"),
- ("CHILLED_WATER", "Chilled Water"),
- ("OTHER", "Other"),
- ],
- help_text="Meter type",
- max_length=20,
- ),
- ),
- ("location_description", models.CharField(max_length=200)),
- ("manufacturer", models.CharField(blank=True, max_length=100)),
- ("model", models.CharField(blank=True, max_length=100)),
- ("serial_number", models.CharField(blank=True, max_length=100)),
- ("installation_date", models.DateField(blank=True, null=True)),
- (
- "current_reading",
- models.DecimalField(
- decimal_places=2, default=Decimal("0.00"), max_digits=12
- ),
- ),
- ("last_reading_date", models.DateTimeField(blank=True, null=True)),
- ("is_active", models.BooleanField(default=True)),
- ("calibration_date", models.DateField(blank=True, null=True)),
- ("next_calibration_date", models.DateField(blank=True, null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "building",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.building",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Energy Meters",
- "db_table": "facility_management_energy_meters",
- "ordering": ["building", "meter_type", "meter_id"],
- },
- ),
- migrations.CreateModel(
- name="EnergyReading",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("reading_date", models.DateTimeField()),
- ("reading_value", models.DecimalField(decimal_places=2, max_digits=12)),
- (
- "consumption",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=12, null=True
- ),
- ),
- (
- "cost",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- ("is_estimated", models.BooleanField(default=False)),
- ("notes", models.TextField(blank=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- (
- "meter",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="readings",
- to="facility_management.energymeter",
- ),
- ),
- (
- "read_by",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- options={
- "ordering": ["-reading_date"],
- },
- ),
- migrations.CreateModel(
- name="Floor",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "floor_number",
- models.IntegerField(
- help_text="Use negative numbers for basement levels"
- ),
- ),
- (
- "name",
- models.CharField(
- help_text="e.g., Ground Floor, Mezzanine, B1", max_length=50
- ),
- ),
- (
- "area_sqm",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- (
- "ceiling_height_m",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- ("is_public_access", models.BooleanField(default=False)),
- (
- "building",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="floors",
- to="facility_management.building",
- ),
- ),
- ],
- options={
- "db_table": "facility_management_floors",
- "ordering": ["building", "floor_number"],
- },
- ),
- migrations.CreateModel(
- name="Asset",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "asset_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- ("name", models.CharField(max_length=200)),
- ("location_description", models.CharField(blank=True, max_length=200)),
- ("manufacturer", models.CharField(blank=True, max_length=100)),
- ("model", models.CharField(blank=True, max_length=100)),
- ("serial_number", models.CharField(blank=True, max_length=100)),
- ("purchase_date", models.DateField(blank=True, null=True)),
- (
- "purchase_cost",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=12, null=True
- ),
- ),
- (
- "current_value",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=12, null=True
- ),
- ),
- (
- "depreciation_rate",
- models.DecimalField(
- decimal_places=2,
- default=Decimal("10.00"),
- help_text="Annual depreciation percentage",
- max_digits=5,
- ),
- ),
- ("warranty_start_date", models.DateField(blank=True, null=True)),
- ("warranty_end_date", models.DateField(blank=True, null=True)),
- ("service_provider", models.CharField(blank=True, max_length=100)),
- (
- "service_contract_number",
- models.CharField(blank=True, max_length=50),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("OPERATIONAL", "Operational"),
- ("MAINTENANCE", "Under Maintenance"),
- ("REPAIR", "Needs Repair"),
- ("RETIRED", "Retired"),
- ("DISPOSED", "Disposed"),
- ],
- default="OPERATIONAL",
- max_length=20,
- ),
- ),
- (
- "condition",
- models.CharField(
- choices=[
- ("EXCELLENT", "Excellent"),
- ("GOOD", "Good"),
- ("FAIR", "Fair"),
- ("POOR", "Poor"),
- ("CRITICAL", "Critical"),
- ],
- default="GOOD",
- help_text="Current condition of the asset",
- max_length=20,
- ),
- ),
- ("last_inspection_date", models.DateField(blank=True, null=True)),
- ("next_maintenance_date", models.DateField(blank=True, null=True)),
- ("notes", models.TextField(blank=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "assigned_to",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "category",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.assetcategory",
- ),
- ),
- (
- "building",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.building",
- ),
- ),
- (
- "floor",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.floor",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Assets",
- "db_table": "facility_management_assets",
- "ordering": ["asset_id"],
- },
- ),
- migrations.CreateModel(
- name="MaintenanceType",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=100, unique=True)),
- ("code", models.CharField(max_length=20, unique=True)),
- ("description", models.TextField(blank=True)),
- (
- "estimated_duration_hours",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- ("is_active", models.BooleanField(default=True)),
- ],
- options={
- "verbose_name_plural": "Maintenance Types",
- "db_table": "facility_management_maintenance_types",
- "ordering": ["name"],
- "indexes": [
- models.Index(fields=["code"], name="facility_ma_code_54340b_idx")
- ],
- },
- ),
- migrations.CreateModel(
- name="Room",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("room_number", models.CharField(max_length=20)),
- ("name", models.CharField(blank=True, max_length=100)),
- (
- "area_sqm",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=8, null=True
- ),
- ),
- (
- "capacity",
- models.PositiveIntegerField(
- blank=True, help_text="Maximum occupancy", null=True
- ),
- ),
- (
- "occupancy_status",
- models.CharField(
- choices=[
- ("VACANT", "Vacant"),
- ("OCCUPIED", "Occupied"),
- ("MAINTENANCE", "Under Maintenance"),
- ("RESERVED", "Reserved"),
- ],
- default="VACANT",
- max_length=20,
- ),
- ),
- (
- "is_accessible",
- models.BooleanField(
- default=True, help_text="ADA/accessibility compliant"
- ),
- ),
- ("notes", models.TextField(blank=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "floor",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="rooms",
- to="facility_management.floor",
- ),
- ),
- ],
- options={
- "db_table": "facility_management_rooms",
- "ordering": ["floor", "room_number"],
- },
- ),
- migrations.CreateModel(
- name="MaintenanceSchedule",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=200)),
- ("description", models.TextField()),
- (
- "frequency_interval",
- models.IntegerField(
- choices=[
- (1, "Daily"),
- (7, "Weekly"),
- (30, "Monthly"),
- (90, "Quarterly"),
- (182, "Semi-Annual"),
- (365, "Annual"),
- ],
- default=365,
- ),
- ),
- ("start_date", models.DateField()),
- ("end_date", models.DateField(blank=True, null=True)),
- (
- "estimated_duration_hours",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- ("is_active", models.BooleanField(default=True)),
- ("last_generated_date", models.DateField(blank=True, null=True)),
- ("next_due_date", models.DateField(blank=True, null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- (
- "asset",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.asset",
- ),
- ),
- (
- "assigned_to",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "building",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.building",
- ),
- ),
- (
- "maintenance_type",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.maintenancetype",
- ),
- ),
- (
- "room",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.room",
- ),
- ),
- ],
- options={
- "db_table": "facility_management_maintenance_schedules",
- "ordering": ["next_due_date"],
- },
- ),
- migrations.CreateModel(
- name="MaintenanceRequest",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "request_id",
- models.CharField(editable=False, max_length=50, unique=True),
- ),
- ("title", models.CharField(max_length=200)),
- ("description", models.TextField()),
- (
- "priority",
- models.CharField(
- choices=[
- ("LOW", "Low"),
- ("MEDIUM", "Medium"),
- ("HIGH", "High"),
- ("URGENT", "Urgent"),
- ("EMERGENCY", "Emergency"),
- ],
- default="MEDIUM",
- max_length=20,
- ),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("SUBMITTED", "Submitted"),
- ("ASSIGNED", "Assigned"),
- ("IN_PROGRESS", "In Progress"),
- ("COMPLETED", "Completed"),
- ("CANCELLED", "Cancelled"),
- ("ON_HOLD", "On Hold"),
- ],
- default="SUBMITTED",
- help_text="Current status of the request",
- max_length=20,
- ),
- ),
- ("requested_date", models.DateTimeField(auto_now_add=True)),
- ("scheduled_date", models.DateTimeField(blank=True, null=True)),
- ("started_date", models.DateTimeField(blank=True, null=True)),
- ("completed_date", models.DateTimeField(blank=True, null=True)),
- (
- "estimated_hours",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- (
- "estimated_cost",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- (
- "actual_cost",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- ("notes", models.TextField(blank=True)),
- ("completion_notes", models.TextField(blank=True)),
- (
- "asset",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.asset",
- ),
- ),
- (
- "assigned_to",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="assigned_maintenance",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "building",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.building",
- ),
- ),
- (
- "floor",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.floor",
- ),
- ),
- (
- "requested_by",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="maintenance_requests",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "maintenance_type",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.maintenancetype",
- ),
- ),
- (
- "room",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.room",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Maintenance Requests",
- "db_table": "facility_management_maintenance_requests",
- "ordering": ["-requested_date"],
- },
- ),
- migrations.CreateModel(
- name="Inspection",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("inspection_id", models.CharField(max_length=50, unique=True)),
- (
- "inspection_type",
- models.CharField(
- choices=[
- ("SAFETY", "Safety Inspection"),
- ("FIRE", "Fire Safety"),
- ("HEALTH", "Health Inspection"),
- ("SECURITY", "Security Audit"),
- ("ENVIRONMENTAL", "Environmental"),
- ("STRUCTURAL", "Structural"),
- ("ELECTRICAL", "Electrical"),
- ("HVAC", "HVAC"),
- ("OTHER", "Other"),
- ],
- help_text="Type of inspection",
- max_length=20,
- ),
- ),
- ("title", models.CharField(max_length=200)),
- ("description", models.TextField(blank=True)),
- ("scheduled_date", models.DateTimeField()),
- (
- "estimated_duration_hours",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- (
- "inspector_external",
- models.CharField(
- blank=True, help_text="External inspector name", max_length=100
- ),
- ),
- (
- "inspector_organization",
- models.CharField(blank=True, max_length=100),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("SCHEDULED", "Scheduled"),
- ("IN_PROGRESS", "In Progress"),
- ("COMPLETED", "Completed"),
- ("CANCELLED", "Cancelled"),
- ],
- default="SCHEDULED",
- help_text="Current status of the inspection",
- max_length=20,
- ),
- ),
- ("started_date", models.DateTimeField(blank=True, null=True)),
- ("completed_date", models.DateTimeField(blank=True, null=True)),
- (
- "overall_rating",
- models.CharField(
- blank=True, help_text="Pass/Fail/Conditional", max_length=20
- ),
- ),
- ("findings", models.TextField(blank=True)),
- ("recommendations", models.TextField(blank=True)),
- ("requires_followup", models.BooleanField(default=False)),
- ("followup_date", models.DateField(blank=True, null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "assets",
- models.ManyToManyField(blank=True, to="facility_management.asset"),
- ),
- (
- "building",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.building",
- ),
- ),
- (
- "floors",
- models.ManyToManyField(blank=True, to="facility_management.floor"),
- ),
- (
- "inspector",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="inspections_conducted",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "rooms",
- models.ManyToManyField(blank=True, to="facility_management.room"),
- ),
- ],
- options={
- "verbose_name_plural": "Inspections",
- "db_table": "facility_management_inspections",
- "ordering": ["-scheduled_date"],
- },
- ),
- migrations.AddField(
- model_name="asset",
- name="room",
- field=models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.room",
- ),
- ),
- migrations.CreateModel(
- name="SpaceReservation",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("reservation_id", models.CharField(max_length=50, unique=True)),
- ("title", models.CharField(max_length=200)),
- ("description", models.TextField(blank=True)),
- ("start_datetime", models.DateTimeField()),
- ("end_datetime", models.DateTimeField()),
- ("contact_person", models.CharField(blank=True, max_length=100)),
- ("contact_email", models.EmailField(blank=True, max_length=254)),
- ("contact_phone", models.CharField(blank=True, max_length=20)),
- (
- "expected_attendees",
- models.PositiveIntegerField(blank=True, null=True),
- ),
- ("setup_requirements", models.TextField(blank=True)),
- ("catering_required", models.BooleanField(default=False)),
- (
- "av_equipment_required",
- models.BooleanField(
- default=False,
- help_text="Whether audio/visual equipment (projector, microphone, etc.) is needed",
- ),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("PENDING", "Pending"),
- ("CONFIRMED", "Confirmed"),
- ("CANCELLED", "Cancelled"),
- ("COMPLETED", "Completed"),
- ("NO_SHOW", "No Show"),
- ],
- default="PENDING",
- max_length=20,
- ),
- ),
- ("approved_at", models.DateTimeField(blank=True, null=True)),
- (
- "hourly_rate",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=8, null=True
- ),
- ),
- (
- "total_cost",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- ("notes", models.TextField(blank=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "approved_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="approved_reservations",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "reserved_by",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "room",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.room",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Space Reservations",
- "db_table": "facility_management_space_reservations",
- "ordering": ["-start_datetime"],
- },
- ),
- migrations.CreateModel(
- name="Vendor",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=200)),
- (
- "vendor_type",
- models.CharField(
- choices=[
- ("MAINTENANCE", "Maintenance Contractor"),
- ("CLEANING", "Cleaning Service"),
- ("SECURITY", "Security Service"),
- ("LANDSCAPING", "Landscaping"),
- ("HVAC", "HVAC Service"),
- ("ELECTRICAL", "Electrical Service"),
- ("PLUMBING", "Plumbing Service"),
- ("IT", "IT Service"),
- ("OTHER", "Other"),
- ],
- max_length=20,
- ),
- ),
- ("contact_person", models.CharField(max_length=100)),
- ("email", models.EmailField(max_length=254)),
- ("phone", models.CharField(max_length=20)),
- ("address", models.TextField()),
- (
- "crn",
- models.CharField(
- blank=True,
- max_length=10,
- null=True,
- verbose_name="Commercial Registration Number",
- ),
- ),
- (
- "vrn",
- models.CharField(
- blank=True,
- max_length=15,
- null=True,
- verbose_name="VAT Registration Number",
- ),
- ),
- (
- "rating",
- models.DecimalField(
- blank=True,
- decimal_places=2,
- max_digits=3,
- null=True,
- validators=[
- django.core.validators.MinValueValidator(Decimal("0.00")),
- django.core.validators.MaxValueValidator(Decimal("5.00")),
- ],
- ),
- ),
- ("total_contracts", models.PositiveIntegerField(default=0)),
- ("is_active", models.BooleanField(default=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "tenant",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE, to="core.tenant"
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Vendors",
- "db_table": "facility_management_vendors",
- "ordering": ["name"],
- },
- ),
- migrations.CreateModel(
- name="ServiceContract",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("contract_number", models.CharField(max_length=50, unique=True)),
- ("title", models.CharField(max_length=200)),
- ("description", models.TextField()),
- ("start_date", models.DateField()),
- ("end_date", models.DateField()),
- (
- "contract_value",
- models.DecimalField(decimal_places=2, max_digits=12),
- ),
- (
- "payment_terms",
- models.CharField(help_text="e.g., Net 30 days", max_length=100),
- ),
- (
- "service_areas",
- models.TextField(help_text="Description of service areas"),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("DRAFT", "Draft"),
- ("ACTIVE", "Active"),
- ("EXPIRED", "Expired"),
- ("TERMINATED", "Terminated"),
- ],
- default="DRAFT",
- max_length=20,
- ),
- ),
- ("auto_renewal", models.BooleanField(default=False)),
- ("renewal_notice_days", models.PositiveIntegerField(default=30)),
- ("notes", models.TextField(blank=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "buildings",
- models.ManyToManyField(
- blank=True, to="facility_management.building"
- ),
- ),
- (
- "contract_manager",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "vendor",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- to="facility_management.vendor",
- ),
- ),
- ],
- options={
- "verbose_name_plural": "Service Contracts",
- "db_table": "facility_management_service_contracts",
- "ordering": ["-start_date"],
- },
- ),
- migrations.AddIndex(
- model_name="assetcategory",
- index=models.Index(fields=["code"], name="facility_ma_code_466577_idx"),
- ),
- migrations.AddIndex(
- model_name="building",
- index=models.Index(
- fields=["code", "name"], name="facility_ma_code_61bb9d_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="building",
- unique_together={("code", "name")},
- ),
- migrations.AlterUniqueTogether(
- name="energyreading",
- unique_together={("meter", "reading_date")},
- ),
- migrations.AddIndex(
- model_name="floor",
- index=models.Index(
- fields=["building", "floor_number"],
- name="facility_ma_buildin_884b34_idx",
- ),
- ),
- migrations.AlterUniqueTogether(
- name="floor",
- unique_together={("building", "floor_number")},
- ),
- migrations.AddIndex(
- model_name="room",
- index=models.Index(
- fields=["floor", "room_number"], name="facility_ma_floor_i_1f4028_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="room",
- unique_together={("floor", "room_number")},
- ),
- migrations.AddIndex(
- model_name="maintenanceschedule",
- index=models.Index(
- fields=["asset", "building", "room"],
- name="facility_ma_asset_i_fe5ae7_idx",
- ),
- ),
- migrations.AlterUniqueTogether(
- name="maintenanceschedule",
- unique_together={("asset", "building", "room")},
- ),
- migrations.AddIndex(
- model_name="maintenancerequest",
- index=models.Index(
- fields=["building", "floor", "room", "asset"],
- name="facility_ma_buildin_06372a_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="asset",
- index=models.Index(
- fields=["building", "floor", "room", "serial_number"],
- name="facility_ma_buildin_2c82c7_idx",
- ),
- ),
- migrations.AlterUniqueTogether(
- name="asset",
- unique_together={("building", "floor", "room", "serial_number")},
- ),
- migrations.AddIndex(
- model_name="vendor",
- index=models.Index(
- fields=["tenant", "name"], name="facility_ma_tenant__cc4314_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="servicecontract",
- index=models.Index(
- fields=["vendor"], name="facility_ma_vendor__870e50_idx"
- ),
- ),
- ]
diff --git a/facility_management/migrations/__pycache__/0001_initial.cpython-312.pyc b/facility_management/migrations/__pycache__/0001_initial.cpython-312.pyc
deleted file mode 100644
index 864953d6..00000000
Binary files a/facility_management/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ
diff --git a/facility_management/templates/facility_management/buildings/form.html b/facility_management/templates/facility_management/buildings/form.html
index 33838559..0b871a44 100644
--- a/facility_management/templates/facility_management/buildings/form.html
+++ b/facility_management/templates/facility_management/buildings/form.html
@@ -50,7 +50,7 @@
-
+
Building Type *
{{ form.building_type }}
@@ -59,16 +59,6 @@
{% endif %}
-
-
-
Airport Code *
- {{ form.airport_code }}
- {% if form.airport_code.errors %}
-
{{ form.airport_code.errors.0 }}
- {% endif %}
-
IATA airport code
-
-
@@ -256,4 +246,3 @@
{% endblock %}
-
diff --git a/facility_management/templates/facility_management/maintenance/form.html b/facility_management/templates/facility_management/maintenance/form.html
index 1add56a5..da3d8bb8 100644
--- a/facility_management/templates/facility_management/maintenance/form.html
+++ b/facility_management/templates/facility_management/maintenance/form.html
@@ -144,6 +144,29 @@
+
+ {% if object %}
+
+
+
+
Started Date
+ {{ form.started_date }}
+ {% if form.started_date.errors %}
+
{{ form.started_date.errors.0 }}
+ {% endif %}
+
+
+
+
+
Completed Date
+ {{ form.completed_date }}
+ {% if form.completed_date.errors %}
+
{{ form.completed_date.errors.0 }}
+ {% endif %}
+
+
+
+ {% endif %}
@@ -179,30 +202,8 @@
- {% if object and object.status in 'completed,cancelled' %}
Completion Details
-
-
-
-
Completed Date
- {{ form.completed_date }}
- {% if form.completed_date.errors %}
-
{{ form.completed_date.errors.0 }}
- {% endif %}
-
-
-
-
-
Actual Hours
- {{ form.actual_hours }}
- {% if form.actual_hours.errors %}
-
{{ form.actual_hours.errors.0 }}
- {% endif %}
-
-
-
-
Completion Notes
{{ form.completion_notes }}
@@ -211,7 +212,6 @@
{% endif %}
Work performed, parts used, recommendations
- {% endif %}
@@ -298,4 +298,3 @@
{% endblock %}
-
diff --git a/fix_all_constraints.py b/fix_all_constraints.py
new file mode 100644
index 00000000..ef5d11f2
--- /dev/null
+++ b/fix_all_constraints.py
@@ -0,0 +1,58 @@
+"""
+Comprehensive fix for all database foreign key constraints.
+"""
+import sqlite3
+
+# Connect to SQLite database
+db_path = 'db.sqlite3'
+conn = sqlite3.connect(db_path)
+cursor = conn.cursor()
+
+print("Fixing all database constraints...")
+
+# Disable foreign key checks
+cursor.execute("PRAGMA foreign_keys = OFF;")
+
+# Get all tables with tenant_id that have violations
+tables_with_violations = [
+ 'radiology_imaging_study',
+ 'accounts_user',
+ 'hr_department',
+ 'hr_training_program',
+ 'operating_theatre_operating_room',
+ 'patients_patient_profile',
+]
+
+for table in tables_with_violations:
+ try:
+ # Delete rows with invalid tenant_id
+ cursor.execute(f"""
+ DELETE FROM {table}
+ WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
+ """)
+ affected = cursor.rowcount
+ if affected > 0:
+ print(f" Deleted {affected} rows from {table} with invalid tenant_id")
+ except sqlite3.OperationalError as e:
+ print(f" Error fixing {table}: {e}")
+
+# Commit changes
+conn.commit()
+
+# Re-enable foreign key checks
+cursor.execute("PRAGMA foreign_keys = ON;")
+
+# Verify constraints
+cursor.execute("PRAGMA foreign_key_check;")
+violations = cursor.fetchall()
+
+if violations:
+ print(f"\n⚠ Still have {len(violations)} foreign key violations")
+ print("First 10 violations:")
+ for violation in violations[:10]:
+ print(f" {violation}")
+else:
+ print("\n✓ All foreign key constraints are now valid!")
+
+conn.close()
+print("\nDatabase fix completed!")
diff --git a/fix_db_constraints.py b/fix_db_constraints.py
new file mode 100644
index 00000000..579e401b
--- /dev/null
+++ b/fix_db_constraints.py
@@ -0,0 +1,70 @@
+"""
+Fix database foreign key constraints before running new migrations.
+"""
+import os
+import django
+import sqlite3
+
+# Setup Django
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
+django.setup()
+
+# Connect to SQLite database
+db_path = 'db.sqlite3'
+conn = sqlite3.connect(db_path)
+cursor = conn.cursor()
+
+print("Fixing database constraints...")
+
+# Disable foreign key checks temporarily
+cursor.execute("PRAGMA foreign_keys = OFF;")
+
+# Fix operating_theatre_surgical_note_template with invalid tenant_id
+print("Fixing operating_theatre_surgical_note_template...")
+cursor.execute("""
+ DELETE FROM operating_theatre_surgical_note_template
+ WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
+""")
+affected = cursor.rowcount
+print(f" Deleted {affected} rows with invalid tenant_id")
+
+# Check for other tables with similar issues
+tables_to_check = [
+ 'facility_management_maintenancerequest',
+ 'facility_management_workorder',
+ 'facility_management_asset',
+]
+
+for table in tables_to_check:
+ try:
+ cursor.execute(f"""
+ UPDATE {table}
+ SET tenant_id = 1
+ WHERE tenant_id NOT IN (SELECT id FROM core_tenant)
+ """)
+ affected = cursor.rowcount
+ if affected > 0:
+ print(f" Fixed {affected} rows in {table}")
+ except sqlite3.OperationalError as e:
+ # Table might not exist yet
+ print(f" Skipping {table}: {e}")
+
+# Commit changes
+conn.commit()
+
+# Re-enable foreign key checks
+cursor.execute("PRAGMA foreign_keys = ON;")
+
+# Verify constraints
+cursor.execute("PRAGMA foreign_key_check;")
+violations = cursor.fetchall()
+
+if violations:
+ print("\nRemaining foreign key violations:")
+ for violation in violations:
+ print(f" {violation}")
+else:
+ print("\n✓ All foreign key constraints are now valid!")
+
+conn.close()
+print("\nDatabase constraints fixed successfully!")
diff --git a/hospital_management/__pycache__/settings.cpython-312.pyc b/hospital_management/__pycache__/settings.cpython-312.pyc
index 7be632df..6d22f801 100644
Binary files a/hospital_management/__pycache__/settings.cpython-312.pyc and b/hospital_management/__pycache__/settings.cpython-312.pyc differ
diff --git a/hospital_management/settings.py b/hospital_management/settings.py
index 909f0ae8..f8b0c1b3 100644
--- a/hospital_management/settings.py
+++ b/hospital_management/settings.py
@@ -72,6 +72,7 @@ LOCAL_APPS = [
'integration',
'quality',
'facility_management',
+ 'insurance_approvals.apps.InsuranceApprovalsConfig',
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
diff --git a/hr/__pycache__/admin.cpython-312.pyc b/hr/__pycache__/admin.cpython-312.pyc
index 732956e6..6770d38f 100644
Binary files a/hr/__pycache__/admin.cpython-312.pyc and b/hr/__pycache__/admin.cpython-312.pyc differ
diff --git a/hr/__pycache__/forms.cpython-312.pyc b/hr/__pycache__/forms.cpython-312.pyc
index e6c382ad..05624b30 100644
Binary files a/hr/__pycache__/forms.cpython-312.pyc and b/hr/__pycache__/forms.cpython-312.pyc differ
diff --git a/hr/__pycache__/urls.cpython-312.pyc b/hr/__pycache__/urls.cpython-312.pyc
index f61b9100..209369dc 100644
Binary files a/hr/__pycache__/urls.cpython-312.pyc and b/hr/__pycache__/urls.cpython-312.pyc differ
diff --git a/hr/__pycache__/views.cpython-312.pyc b/hr/__pycache__/views.cpython-312.pyc
index 49fedc93..10f215b2 100644
Binary files a/hr/__pycache__/views.cpython-312.pyc and b/hr/__pycache__/views.cpython-312.pyc differ
diff --git a/hr/admin.py b/hr/admin.py
index 9562e9ec..53d16815 100644
--- a/hr/admin.py
+++ b/hr/admin.py
@@ -6,11 +6,16 @@ from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
+from django.db.models import Count, Avg, Sum
from decimal import Decimal
-from datetime import date
+from datetime import date, timedelta
from .models import *
+# ============================================================================
+# INLINE ADMIN CLASSES
+# ============================================================================
+
class ScheduleInline(admin.TabularInline):
"""
Inline admin for employee schedules.
@@ -46,7 +51,7 @@ class PerformanceReviewInline(admin.TabularInline):
fields = [
'review_date', 'review_type', 'overall_rating', 'status'
]
-
+ readonly_fields = ['review_id']
class TrainingRecordInline(admin.TabularInline):
@@ -56,12 +61,67 @@ class TrainingRecordInline(admin.TabularInline):
model = TrainingRecord
extra = 0
fields = [
- 'completion_date',
+ 'program', 'session', 'completion_date',
'status', 'passed'
]
readonly_fields = ['record_id']
+class ProgramModuleInline(admin.TabularInline):
+ """
+ Inline admin for training program modules.
+ """
+ model = ProgramModule
+ extra = 0
+ fields = ['order', 'title', 'hours']
+ ordering = ['order']
+
+
+class ProgramPrerequisiteInline(admin.TabularInline):
+ """
+ Inline admin for training program prerequisites.
+ """
+ model = ProgramPrerequisite
+ extra = 0
+ fk_name = 'program'
+ fields = ['required_program']
+
+
+class TrainingSessionInline(admin.TabularInline):
+ """
+ Inline admin for training sessions.
+ """
+ model = TrainingSession
+ extra = 0
+ fields = [
+ 'title', 'start_at', 'end_at', 'instructor',
+ 'delivery_method', 'capacity'
+ ]
+ readonly_fields = ['session_id']
+
+
+class TrainingAttendanceInline(admin.TabularInline):
+ """
+ Inline admin for training attendance.
+ """
+ model = TrainingAttendance
+ extra = 0
+ fields = [
+ 'checked_in_at', 'checked_out_at', 'status', 'notes'
+ ]
+
+
+class TrainingAssessmentInline(admin.TabularInline):
+ """
+ Inline admin for training assessments.
+ """
+ model = TrainingAssessment
+ extra = 0
+ fields = [
+ 'name', 'max_score', 'score', 'passed', 'taken_at'
+ ]
+
+
@admin.register(Employee)
class EmployeeAdmin(admin.ModelAdmin):
"""
@@ -741,121 +801,581 @@ class PerformanceReviewAdmin(admin.ModelAdmin):
return qs.select_related('employee', 'reviewer')
-# @admin.register(TrainingRecord)
-# class TrainingRecordAdmin(admin.ModelAdmin):
-# """
-# Admin interface for training records.
-# """
-# list_display = [
-# 'employee_name', 'completion_date', 'status',
-# 'passed', 'expiry_status_display'
-# ]
-#
-# search_fields = [
-# 'employee__first_name', 'employee__last_name',
-# 'employee__employee_id', 'training_name'
-# ]
-# readonly_fields = [
-# 'record_id', 'created_at', 'updated_at'
-# ]
-# fieldsets = [
-# ('Training Information', {
-# 'fields': [
-# 'record_id', 'employee', 'training_description'
-# ]
-# }),
-#
-# ('Training Provider', {
-# 'fields': [
-# 'training_provider', 'instructor'
-# ]
-# }),
-# ('Training Dates', {
-# 'fields': [
-# 'completion_date', 'expiry_date'
-# ]
-# }),
-# ('Training Details', {
-# 'fields': [
-# 'duration_hours', 'credits_earned'
-# ]
-# }),
-# ('Training Status', {
-# 'fields': [
-# 'status'
-# ]
-# }),
-# ('Results', {
-# 'fields': [
-# 'score', 'passed'
-# ]
-# }),
-# ('Certification Information', {
-# 'fields': [
-# 'certificate_number', 'certification_body'
-# ]
-# }),
-# ('Cost Information', {
-# 'fields': [
-# 'training_cost'
-# ]
-# }),
-# ('Expiry Information', {
-# 'fields': [
-# 'is_expired', 'days_to_expiry', 'is_due_for_renewal'
-# ],
-# 'classes': ['collapse']
-# }),
-# ('Notes', {
-# 'fields': [
-# 'notes'
-# ],
-# 'classes': ['collapse']
-# }),
-# ('Related Information', {
-# 'fields': [
-# 'employee__tenant'
-# ],
-# 'classes': ['collapse']
-# }),
-# ('Metadata', {
-# 'fields': [
-# 'created_at', 'updated_at', 'created_by'
-# ],
-# 'classes': ['collapse']
-# })
-# ]
-#
-#
-# def employee_name(self, obj):
-# """Display employee name."""
-# return obj.employee.get_full_name()
-# employee_name.short_description = 'Employee'
-#
-# def expiry_status_display(self, obj):
-# """Display expiry status with color coding."""
-# if not obj.expiry_date:
-# return format_html('
No Expiry ')
-#
-# if obj.is_expired:
-# return format_html('
⚠️ Expired ')
-#
-# if obj.is_due_for_renewal:
-# return format_html('
⚠️ Due Soon ')
-#
-# return format_html('
✓ Valid ')
-# expiry_status_display.short_description = 'Expiry Status'
-#
-# def get_queryset(self, request):
-# """Filter by user's tenant."""
-# qs = super().get_queryset(request)
-# if hasattr(request.user, 'tenant'):
-# qs = qs.filter(employee__tenant=request.user.tenant)
-# return qs.select_related('employee', 'created_by')
+# ============================================================================
+# TRAINING ADMIN CLASSES
+# ============================================================================
+@admin.register(TrainingPrograms)
+class TrainingProgramsAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training programs.
+ """
+ list_display = [
+ 'name', 'program_type', 'program_provider', 'instructor',
+ 'duration_hours', 'cost', 'is_certified', 'validity_display'
+ ]
+ list_filter = [
+ 'tenant', 'program_type', 'is_certified', 'program_provider'
+ ]
+ search_fields = [
+ 'name', 'description', 'program_provider'
+ ]
+ readonly_fields = [
+ 'program_id', 'created_at', 'updated_at'
+ ]
+ fieldsets = [
+ ('Program Information', {
+ 'fields': [
+ 'program_id', 'tenant', 'name', 'description'
+ ]
+ }),
+ ('Program Details', {
+ 'fields': [
+ 'program_type', 'program_provider', 'instructor'
+ ]
+ }),
+ ('Schedule Information', {
+ 'fields': [
+ 'start_date', 'end_date', 'duration_hours'
+ ]
+ }),
+ ('Cost Information', {
+ 'fields': [
+ 'cost'
+ ]
+ }),
+ ('Certification Information', {
+ 'fields': [
+ 'is_certified', 'validity_days', 'notify_before_days'
+ ]
+ }),
+ ('Metadata', {
+ 'fields': [
+ 'created_at', 'updated_at', 'created_by'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ inlines = [ProgramModuleInline, ProgramPrerequisiteInline, TrainingSessionInline]
+ date_hierarchy = 'start_date'
+
+ def validity_display(self, obj):
+ """Display validity information."""
+ if obj.is_certified and obj.validity_days:
+ return f"{obj.validity_days} days"
+ return "No expiry"
+ validity_display.short_description = 'Validity'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(tenant=request.user.tenant)
+ return qs.select_related('instructor', 'created_by')
+
+
+@admin.register(ProgramModule)
+class ProgramModuleAdmin(admin.ModelAdmin):
+ """
+ Admin interface for program modules.
+ """
+ list_display = [
+ 'program', 'order', 'title', 'hours'
+ ]
+ list_filter = [
+ 'program__tenant', 'program'
+ ]
+ search_fields = [
+ 'title', 'program__name'
+ ]
+ ordering = ['program', 'order']
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(program__tenant=request.user.tenant)
+ return qs.select_related('program')
+
+
+@admin.register(ProgramPrerequisite)
+class ProgramPrerequisiteAdmin(admin.ModelAdmin):
+ """
+ Admin interface for program prerequisites.
+ """
+ list_display = [
+ 'program', 'required_program'
+ ]
+ list_filter = [
+ 'program__tenant', 'program'
+ ]
+ search_fields = [
+ 'program__name', 'required_program__name'
+ ]
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(program__tenant=request.user.tenant)
+ return qs.select_related('program', 'required_program')
+
+
+@admin.register(TrainingSession)
+class TrainingSessionAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training sessions.
+ """
+ list_display = [
+ 'program', 'title_display', 'instructor', 'start_at',
+ 'end_at', 'delivery_method', 'capacity', 'enrollment_count'
+ ]
+ list_filter = [
+ 'program__tenant', 'delivery_method', 'start_at'
+ ]
+ search_fields = [
+ 'title', 'program__name', 'instructor__first_name', 'instructor__last_name'
+ ]
+ readonly_fields = [
+ 'session_id', 'enrollment_count', 'created_at'
+ ]
+ fieldsets = [
+ ('Session Information', {
+ 'fields': [
+ 'session_id', 'program', 'title'
+ ]
+ }),
+ ('Instructor Information', {
+ 'fields': [
+ 'instructor'
+ ]
+ }),
+ ('Schedule Information', {
+ 'fields': [
+ 'start_at', 'end_at'
+ ]
+ }),
+ ('Delivery Information', {
+ 'fields': [
+ 'delivery_method', 'location'
+ ]
+ }),
+ ('Capacity Information', {
+ 'fields': [
+ 'capacity', 'enrollment_count'
+ ]
+ }),
+ ('Cost Override', {
+ 'fields': [
+ 'cost_override', 'hours_override'
+ ]
+ }),
+ ('Metadata', {
+ 'fields': [
+ 'created_at', 'created_by'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ date_hierarchy = 'start_at'
+
+ def title_display(self, obj):
+ """Display session title or program name."""
+ return obj.title or obj.program.name
+ title_display.short_description = 'Title'
+
+ def enrollment_count(self, obj):
+ """Display enrollment count."""
+ return obj.enrollments.count()
+ enrollment_count.short_description = 'Enrollments'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(program__tenant=request.user.tenant)
+ return qs.select_related('program', 'instructor', 'created_by')
+
+
+@admin.register(TrainingRecord)
+class TrainingRecordAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training records (enrollments).
+ """
+ list_display = [
+ 'employee_name', 'program', 'session', 'enrolled_at',
+ 'completion_date', 'status', 'passed', 'expiry_status_display'
+ ]
+ list_filter = [
+ 'employee__tenant', 'status', 'passed', 'program__program_type',
+ 'enrolled_at', 'completion_date'
+ ]
+ search_fields = [
+ 'employee__first_name', 'employee__last_name',
+ 'employee__employee_id', 'program__name'
+ ]
+ readonly_fields = [
+ 'record_id', 'enrolled_at', 'hours', 'effective_cost',
+ 'eligible_for_certificate', 'completion_percentage',
+ 'created_at', 'updated_at'
+ ]
+ fieldsets = [
+ ('Enrollment Information', {
+ 'fields': [
+ 'record_id', 'employee', 'program', 'session'
+ ]
+ }),
+ ('Enrollment Dates', {
+ 'fields': [
+ 'enrolled_at', 'started_at', 'completion_date', 'expiry_date'
+ ]
+ }),
+ ('Status Information', {
+ 'fields': [
+ 'status', 'passed'
+ ]
+ }),
+ ('Performance Information', {
+ 'fields': [
+ 'credits_earned', 'score'
+ ]
+ }),
+ ('Cost Information', {
+ 'fields': [
+ 'cost_paid', 'effective_cost'
+ ]
+ }),
+ ('Calculated Information', {
+ 'fields': [
+ 'hours', 'eligible_for_certificate', 'completion_percentage'
+ ],
+ 'classes': ['collapse']
+ }),
+ ('Notes', {
+ 'fields': [
+ 'notes'
+ ],
+ 'classes': ['collapse']
+ }),
+ ('Metadata', {
+ 'fields': [
+ 'created_at', 'updated_at', 'created_by'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ inlines = [TrainingAttendanceInline, TrainingAssessmentInline]
+ date_hierarchy = 'enrolled_at'
+
+ def employee_name(self, obj):
+ """Display employee name."""
+ return obj.employee.get_full_name()
+ employee_name.short_description = 'Employee'
+
+ def expiry_status_display(self, obj):
+ """Display expiry status with color coding."""
+ if not obj.expiry_date:
+ return format_html('
No Expiry ')
+
+ days_to_expiry = (obj.expiry_date - date.today()).days
+ if days_to_expiry < 0:
+ return format_html('
⚠️ Expired ')
+ elif days_to_expiry <= 30:
+ return format_html('
⚠️ Expires Soon ')
+ else:
+ return format_html('
✓ Valid ')
+ expiry_status_display.short_description = 'Expiry Status'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(employee__tenant=request.user.tenant)
+ return qs.select_related('employee', 'program', 'session', 'created_by')
+
+
+@admin.register(TrainingAttendance)
+class TrainingAttendanceAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training attendance.
+ """
+ list_display = [
+ 'employee_name', 'program_name', 'checked_in_at',
+ 'checked_out_at', 'status', 'duration_display'
+ ]
+ list_filter = [
+ 'enrollment__employee__tenant', 'status', 'checked_in_at'
+ ]
+ search_fields = [
+ 'enrollment__employee__first_name', 'enrollment__employee__last_name',
+ 'enrollment__program__name'
+ ]
+ readonly_fields = [
+ 'duration_display'
+ ]
+ fieldsets = [
+ ('Attendance Information', {
+ 'fields': [
+ 'enrollment'
+ ]
+ }),
+ ('Check-in/out Times', {
+ 'fields': [
+ 'checked_in_at', 'checked_out_at'
+ ]
+ }),
+ ('Status Information', {
+ 'fields': [
+ 'status'
+ ]
+ }),
+ ('Duration Information', {
+ 'fields': [
+ 'duration_display'
+ ],
+ 'classes': ['collapse']
+ }),
+ ('Notes', {
+ 'fields': [
+ 'notes'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ date_hierarchy = 'checked_in_at'
+
+ def employee_name(self, obj):
+ """Display employee name."""
+ return obj.enrollment.employee.get_full_name()
+ employee_name.short_description = 'Employee'
+
+ def program_name(self, obj):
+ """Display program name."""
+ return obj.enrollment.program.name
+ program_name.short_description = 'Program'
+
+ def duration_display(self, obj):
+ """Display attendance duration."""
+ if obj.checked_in_at and obj.checked_out_at:
+ duration = obj.checked_out_at - obj.checked_in_at
+ hours = duration.total_seconds() / 3600
+ return f"{hours:.2f} hours"
+ return "Incomplete"
+ duration_display.short_description = 'Duration'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
+ return qs.select_related('enrollment__employee', 'enrollment__program')
+
+
+@admin.register(TrainingAssessment)
+class TrainingAssessmentAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training assessments.
+ """
+ list_display = [
+ 'employee_name', 'program_name', 'name',
+ 'score', 'max_score', 'percentage_display', 'passed', 'taken_at'
+ ]
+ list_filter = [
+ 'enrollment__employee__tenant', 'passed', 'taken_at'
+ ]
+ search_fields = [
+ 'enrollment__employee__first_name', 'enrollment__employee__last_name',
+ 'enrollment__program__name', 'name'
+ ]
+ readonly_fields = [
+ 'percentage_display'
+ ]
+ fieldsets = [
+ ('Assessment Information', {
+ 'fields': [
+ 'enrollment', 'name'
+ ]
+ }),
+ ('Score Information', {
+ 'fields': [
+ 'max_score', 'score', 'percentage_display', 'passed'
+ ]
+ }),
+ ('Date Information', {
+ 'fields': [
+ 'taken_at'
+ ]
+ }),
+ ('Notes', {
+ 'fields': [
+ 'notes'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ date_hierarchy = 'taken_at'
+
+ def employee_name(self, obj):
+ """Display employee name."""
+ return obj.enrollment.employee.get_full_name()
+ employee_name.short_description = 'Employee'
+
+ def program_name(self, obj):
+ """Display program name."""
+ return obj.enrollment.program.name
+ program_name.short_description = 'Program'
+
+ def percentage_display(self, obj):
+ """Display score percentage."""
+ if obj.score is not None and obj.max_score > 0:
+ percentage = (obj.score / obj.max_score) * 100
+ color = 'green' if percentage >= 70 else 'orange' if percentage >= 50 else 'red'
+ return format_html(
+ '
{:.1f}% ',
+ color, percentage
+ )
+ return "N/A"
+ percentage_display.short_description = 'Percentage'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
+ return qs.select_related('enrollment__employee', 'enrollment__program')
+
+
+@admin.register(TrainingCertificates)
+class TrainingCertificatesAdmin(admin.ModelAdmin):
+ """
+ Admin interface for training certificates.
+ """
+ list_display = [
+ 'employee_name', 'certificate_name', 'program',
+ 'issued_date', 'expiry_date', 'expiry_status_display',
+ 'certificate_number'
+ ]
+ list_filter = [
+ 'employee__tenant', 'program', 'issued_date', 'expiry_date'
+ ]
+ search_fields = [
+ 'employee__first_name', 'employee__last_name',
+ 'certificate_name', 'certificate_number', 'program__name'
+ ]
+ readonly_fields = [
+ 'certificate_id', 'is_expired', 'days_to_expiry',
+ 'created_at', 'updated_at'
+ ]
+ fieldsets = [
+ ('Certificate Information', {
+ 'fields': [
+ 'certificate_id', 'employee', 'program', 'enrollment'
+ ]
+ }),
+ ('Certificate Details', {
+ 'fields': [
+ 'certificate_name', 'certificate_number', 'certification_body'
+ ]
+ }),
+ ('Date Information', {
+ 'fields': [
+ 'issued_date', 'expiry_date'
+ ]
+ }),
+ ('File Information', {
+ 'fields': [
+ 'file'
+ ]
+ }),
+ ('Signature Information', {
+ 'fields': [
+ 'created_by', 'signed_by'
+ ]
+ }),
+ ('Status Information', {
+ 'fields': [
+ 'is_expired', 'days_to_expiry'
+ ],
+ 'classes': ['collapse']
+ }),
+ ('Metadata', {
+ 'fields': [
+ 'created_at', 'updated_at'
+ ],
+ 'classes': ['collapse']
+ })
+ ]
+ date_hierarchy = 'issued_date'
+
+ def employee_name(self, obj):
+ """Display employee name."""
+ return obj.employee.get_full_name()
+ employee_name.short_description = 'Employee'
+
+ def expiry_status_display(self, obj):
+ """Display expiry status with color coding."""
+ if not obj.expiry_date:
+ return format_html('
No Expiry ')
+
+ if obj.is_expired:
+ return format_html('
⚠️ Expired ')
+
+ if obj.days_to_expiry is not None and obj.days_to_expiry <= 30:
+ return format_html('
⚠️ Expires Soon ')
+
+ return format_html('
✓ Valid ')
+ expiry_status_display.short_description = 'Expiry Status'
+
+ def get_queryset(self, request):
+ """Filter by user's tenant."""
+ qs = super().get_queryset(request)
+ if hasattr(request.user, 'tenant'):
+ qs = qs.filter(employee__tenant=request.user.tenant)
+ return qs.select_related('employee', 'program', 'enrollment', 'created_by', 'signed_by')
+
+
+# ============================================================================
+# CUSTOM ADMIN ACTIONS
+# ============================================================================
+
+def mark_training_completed(modeladmin, request, queryset):
+ """Mark selected training records as completed."""
+ updated = queryset.update(status='COMPLETED', completion_date=date.today())
+ modeladmin.message_user(request, f'{updated} training records marked as completed.')
+mark_training_completed.short_description = "Mark selected training as completed"
+
+def mark_training_passed(modeladmin, request, queryset):
+ """Mark selected training records as passed."""
+ updated = queryset.update(passed=True)
+ modeladmin.message_user(request, f'{updated} training records marked as passed.')
+mark_training_passed.short_description = "Mark selected training as passed"
+
+def generate_certificates(modeladmin, request, queryset):
+ """Generate certificates for completed training."""
+ count = 0
+ for record in queryset.filter(status='COMPLETED', passed=True):
+ if record.program.is_certified and not hasattr(record, 'certificate'):
+ TrainingCertificates.objects.create(
+ program=record.program,
+ employee=record.employee,
+ enrollment=record,
+ certificate_name=f"{record.program.name} Certificate",
+ expiry_date=TrainingCertificates.compute_expiry(record.program, date.today()),
+ created_by=request.user
+ )
+ count += 1
+ modeladmin.message_user(request, f'{count} certificates generated.')
+generate_certificates.short_description = "Generate certificates for completed training"
+
+# Add actions to TrainingRecord admin
+TrainingRecordAdmin.actions = [mark_training_completed, mark_training_passed, generate_certificates]
+
+
+# ============================================================================
+# ADMIN SITE CUSTOMIZATION
+# ============================================================================
# Customize admin site
admin.site.site_header = "Hospital Management System - HR"
admin.site.site_title = "HR Admin"
admin.site.index_title = "Human Resources Administration"
-
diff --git a/hr/forms.py b/hr/forms.py
index 5be22434..58475279 100644
--- a/hr/forms.py
+++ b/hr/forms.py
@@ -36,128 +36,128 @@ class EmployeeForm(forms.ModelForm):
]
widgets = {
'first_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'First name'
}),
'last_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Last name'
}),
'father_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Father name'
}),
'grandfather_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Grandfather name'
}),
'email': forms.EmailInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'employee@hospital.com'
}),
'phone': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': '+1-555-123-4567'
}),
'mobile_phone': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': '+1-555-123-4567'
}),
'address_line_1': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Street address'
}),
'address_line_2': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Apt, Suite, etc.'
}),
'city': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'City'
}),
'state': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'State/Province'
}),
'postal_code': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Postal/ZIP code'
}),
'country': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Country'
}),
- 'department': forms.Select(attrs={'class': 'form-control'}),
+ 'department': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'job_title': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Job title/position'
}),
- 'employment_status': forms.Select(attrs={'class': 'form-control'}),
- 'employment_type': forms.Select(attrs={'class': 'form-control'}),
+ 'employment_status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'employment_type': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'hire_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
'termination_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
'annual_salary': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'hourly_rate': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
- 'supervisor': forms.Select(attrs={'class': 'form-control'}),
+ 'supervisor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'emergency_contact_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Emergency contact name'
}),
'emergency_contact_relationship': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Relationship to employee'
}),
'emergency_contact_phone': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Emergency contact phone'
}),
'date_of_birth': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
- 'gender': forms.Select(attrs={'class': 'form-control'}),
- 'marital_status': forms.Select(attrs={'class': 'form-control'}),
+ 'gender': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'marital_status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'standard_hours_per_week': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0',
'max': '168'
}),
'fte_percentage': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '1',
'min': '0',
'max': '100'
}),
'license_number': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Professional license number'
}),
'license_expiry_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
'certifications': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'List of certifications'
}),
'notes': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Additional notes'
}),
@@ -263,10 +263,6 @@ class EmployeeForm(forms.ModelForm):
return cleaned_data
-# ===========================
-# Training Program Forms
-# ===========================
-
class TrainingProgramForm(forms.ModelForm):
"""
Form for creating and updating training programs.
@@ -281,45 +277,45 @@ class TrainingProgramForm(forms.ModelForm):
]
widgets = {
'name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Training program name'
}),
'description': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Program description'
}),
- 'program_type': forms.Select(attrs={'class': 'form-control'}),
+ 'program_type': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'program_provider': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Training provider/organization'
}),
- 'instructor': forms.Select(attrs={'class': 'form-control'}),
+ 'instructor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'start_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
'end_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
'duration_hours': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
'cost': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'is_certified': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'validity_days': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'min': '1'
}),
'notify_before_days': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'min': '1'
}),
}
@@ -373,36 +369,36 @@ class TrainingSessionForm(forms.ModelForm):
'cost_override', 'hours_override'
]
widgets = {
- 'program': forms.Select(attrs={'class': 'form-control'}),
+ 'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'title': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Session title (optional)'
}),
- 'instructor': forms.Select(attrs={'class': 'form-control'}),
- 'delivery_method': forms.Select(attrs={'class': 'form-control'}),
+ 'instructor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'delivery_method': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'start_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'end_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'location': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Training location'
}),
'capacity': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'min': '1'
}),
'cost_override': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'hours_override': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
@@ -451,17 +447,17 @@ class ProgramModuleForm(forms.ModelForm):
model = ProgramModule
fields = ['program', 'title', 'order', 'hours']
widgets = {
- 'program': forms.Select(attrs={'class': 'form-control'}),
+ 'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'title': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Module title'
}),
'order': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'min': '1'
}),
'hours': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
@@ -491,18 +487,18 @@ class TrainingAttendanceForm(forms.ModelForm):
model = TrainingAttendance
fields = ['enrollment', 'checked_in_at', 'checked_out_at', 'status', 'notes']
widgets = {
- 'enrollment': forms.Select(attrs={'class': 'form-control'}),
+ 'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'checked_in_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'checked_out_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
- 'status': forms.Select(attrs={'class': 'form-control'}),
+ 'status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'notes': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Attendance notes'
}),
}
@@ -536,28 +532,28 @@ class TrainingAssessmentForm(forms.ModelForm):
model = TrainingAssessment
fields = ['enrollment', 'name', 'max_score', 'score', 'passed', 'taken_at', 'notes']
widgets = {
- 'enrollment': forms.Select(attrs={'class': 'form-control'}),
+ 'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Assessment name'
}),
'max_score': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'score': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'passed': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'taken_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'notes': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Assessment notes'
}),
@@ -592,26 +588,26 @@ class TrainingCertificateForm(forms.ModelForm):
'signed_by'
]
widgets = {
- 'program': forms.Select(attrs={'class': 'form-control'}),
- 'employee': forms.Select(attrs={'class': 'form-control'}),
- 'enrollment': forms.Select(attrs={'class': 'form-control'}),
+ 'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'employee': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'certificate_name': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Certificate name'
}),
'certificate_number': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Certificate number'
}),
'certification_body': forms.TextInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'placeholder': 'Issuing organization'
}),
'expiry_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
- 'signed_by': forms.Select(attrs={'class': 'form-control'}),
+ 'signed_by': forms.Select(attrs={'class': 'form-select form-select-sm'}),
}
help_texts = {
'certificate_number': 'Unique certificate identifier',
@@ -662,10 +658,6 @@ class TrainingCertificateForm(forms.ModelForm):
return cleaned_data
-# ===========================
-# Search and Filter Forms
-# ===========================
-
class TrainingSearchForm(forms.Form):
"""
Form for searching and filtering training records.
@@ -744,7 +736,7 @@ class DepartmentForm(forms.ModelForm):
'class': 'form-control',
'placeholder': 'Department name'
}),
- 'department_code': forms.TextInput(attrs={
+ 'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Unique department code'
}),
@@ -777,7 +769,7 @@ class DepartmentForm(forms.ModelForm):
}),
}
help_texts = {
- 'department_code': 'Unique code to identify this department',
+ 'code': 'Unique code to identify this department',
'department_head': 'Department manager (optional)',
'annual_budget': 'Annual budget for this department',
'parent_department': 'Parent department (if any)',
@@ -804,18 +796,18 @@ class DepartmentForm(forms.ModelForm):
pk=self.instance.pk
)
- def clean_department_code(self):
- department_code = self.cleaned_data.get('department_code')
- if department_code:
+ def clean_code(self):
+ code = self.cleaned_data.get('code')
+ if code:
# Check for uniqueness within tenant (excluding current instance)
- queryset = Department.objects.filter(department_code=department_code)
+ queryset = Department.objects.filter(code=code)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise ValidationError('Department code must be unique.')
- return department_code
+ return code
class ScheduleForm(forms.ModelForm):
@@ -823,6 +815,34 @@ class ScheduleForm(forms.ModelForm):
Form for creating and updating schedules.
"""
+ # Additional fields to match template expectations
+ start_date = forms.DateField(
+ required=True,
+ widget=forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ help_text='Schedule start date'
+ )
+
+ department = forms.ModelChoiceField(
+ queryset=Department.objects.none(),
+ required=False,
+ widget=forms.Select(attrs={'class': 'form-control'}),
+ help_text='Department for this schedule'
+ )
+
+ status = forms.ChoiceField(
+ choices=[
+ ('DRAFT', 'Draft'),
+ ('PUBLISHED', 'Published'),
+ ('ARCHIVED', 'Archived'),
+ ],
+ initial='DRAFT',
+ widget=forms.Select(attrs={'class': 'form-control'}),
+ help_text='Schedule status'
+ )
+
class Meta:
model = Schedule
fields = [
@@ -842,10 +862,7 @@ class ScheduleForm(forms.ModelForm):
'placeholder': 'Schedule description'
}),
'schedule_type': forms.Select(attrs={'class': 'form-control'}),
- 'effective_date': forms.DateInput(attrs={
- 'class': 'form-control',
- 'type': 'date'
- }),
+ 'effective_date': forms.HiddenInput(),
'end_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
@@ -855,7 +872,7 @@ class ScheduleForm(forms.ModelForm):
'rows': 5,
'placeholder': '{"monday": {"start": "09:00", "end": "17:00"}, ...}'
}),
- 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
+ 'is_active': forms.HiddenInput(),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
@@ -874,21 +891,64 @@ class ScheduleForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
- # Filter employee by tenant
+ # Filter employee and department by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
+
+ self.fields['department'].queryset = Department.objects.filter(
+ tenant=user.tenant,
+ is_active=True
+ ).order_by('name')
+
+ # Initialize template-compatible fields from model fields
+ if self.instance and self.instance.pk:
+ self.fields['start_date'].initial = self.instance.effective_date
+
+ # Set status based on is_active field
+ if self.instance.is_active:
+ self.fields['status'].initial = 'PUBLISHED'
+ else:
+ self.fields['status'].initial = 'DRAFT'
- def clean_end_date(self):
- end_date = self.cleaned_data.get('end_date')
- effective_date = self.cleaned_data.get('effective_date')
+ def clean(self):
+ cleaned_data = super().clean()
+
+ # Map template fields to model fields
+ start_date = cleaned_data.get('start_date')
+ status = cleaned_data.get('status')
+ end_date = cleaned_data.get('end_date')
+
+ # Set model field values from template fields
+ if start_date:
+ cleaned_data['effective_date'] = start_date
+
+ if status:
+ cleaned_data['is_active'] = (status == 'PUBLISHED')
+
+ # Validation
+ effective_date = cleaned_data.get('effective_date')
if end_date and effective_date:
if end_date < effective_date:
- raise ValidationError('End date cannot be before effective date.')
+ self.add_error('end_date', 'End date cannot be before start date.')
- return end_date
+ return cleaned_data
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+
+ # Ensure model fields are set from template fields
+ if hasattr(self, 'cleaned_data'):
+ if self.cleaned_data.get('start_date'):
+ instance.effective_date = self.cleaned_data['start_date']
+ if self.cleaned_data.get('status'):
+ instance.is_active = (self.cleaned_data['status'] == 'PUBLISHED')
+
+ if commit:
+ instance.save()
+ return instance
class ScheduleAssignmentForm(forms.ModelForm):
@@ -981,237 +1041,372 @@ class ScheduleAssignmentForm(forms.ModelForm):
class TimeEntryForm(forms.ModelForm):
"""
- Form for creating and updating time entries.
+ Form for creating and updating time entries with template-compatible field names.
"""
-
+
+ # Template-compatible field names
+ date = forms.DateField(
+ required=True,
+ widget=forms.DateInput(attrs={
+ 'class': 'form-control',
+ 'type': 'date'
+ }),
+ help_text='Work date'
+ )
+
+ start_time = forms.TimeField(
+ required=True,
+ widget=forms.TimeInput(attrs={
+ 'class': 'form-control',
+ 'type': 'time'
+ }),
+ help_text='Clock in time'
+ )
+
+ end_time = forms.TimeField(
+ required=True,
+ widget=forms.TimeInput(attrs={
+ 'class': 'form-control',
+ 'type': 'time'
+ }),
+ help_text='Clock out time'
+ )
+
+ hours = forms.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ required=True,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'step': '0.01',
+ 'readonly': True
+ }),
+ help_text='Total hours (calculated automatically)'
+ )
+
+ break_duration = forms.IntegerField(
+ required=False,
+ min_value=0,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control',
+ 'min': '0'
+ }),
+ help_text='Break duration in minutes'
+ )
+
+ description = forms.CharField(
+ required=False,
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control',
+ 'rows': 3,
+ 'placeholder': 'Provide details about the work performed during this time period'
+ }),
+ help_text='Work description'
+ )
+
+ schedule_assignment = forms.ModelChoiceField(
+ queryset=ScheduleAssignment.objects.none(),
+ required=False,
+ widget=forms.Select(attrs={
+ 'class': 'form-control'
+ }),
+ help_text='Link this time entry to a schedule assignment'
+ )
+
+ is_overtime = forms.BooleanField(
+ required=False,
+ widget=forms.HiddenInput(),
+ help_text='Is this overtime work'
+ )
+
+ auto_approve = forms.BooleanField(
+ required=False,
+ widget=forms.CheckboxInput(attrs={
+ 'class': 'form-check-input'
+ }),
+ help_text='Auto-approve this time entry'
+ )
+
class Meta:
model = TimeEntry
fields = [
- 'employee', 'work_date', 'clock_in_time', 'clock_out_time',
- 'break_start_time', 'break_end_time', 'lunch_start_time',
- 'lunch_end_time', 'entry_type', 'department', 'location',
- 'status', 'notes'
+ 'employee', 'department', 'location', 'entry_type', 'status'
]
widgets = {
- 'employee': forms.Select(attrs={'class': 'form-control'}),
- 'work_date': forms.DateInput(attrs={
- 'class': 'form-control',
- 'type': 'date'
+ 'employee': forms.Select(attrs={
+ 'class': 'form-control'
}),
- 'clock_in_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
+ 'department': forms.Select(attrs={
+ 'class': 'form-control'
}),
- 'clock_out_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
- }),
- 'break_start_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
- }),
- 'break_end_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
- }),
- 'lunch_start_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
- }),
- 'lunch_end_time': forms.DateTimeInput(attrs={
- 'class': 'form-control',
- 'type': 'datetime-local'
- }),
- 'entry_type': forms.Select(attrs={'class': 'form-control'}),
- 'department': forms.Select(attrs={'class': 'form-control'}),
'location': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Work location'
}),
- 'status': forms.Select(attrs={'class': 'form-control'}),
- 'notes': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 2,
- 'placeholder': 'Time entry notes'
- }),
+ 'entry_type': forms.HiddenInput(),
+ 'status': forms.HiddenInput(),
}
- help_texts = {
- 'work_date': 'Date of work',
- 'clock_in_time': 'Clock in date and time',
- 'clock_out_time': 'Clock out date and time',
- 'entry_type': 'Type of time entry',
- 'status': 'Current status of the time entry',
- }
-
+
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
-
+
if user and hasattr(user, 'tenant'):
# Filter employee and department by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
-
+
self.fields['department'].queryset = Department.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('name')
-
+
+ # Filter schedule assignments for today
+ today = timezone.now().date()
+ self.fields['schedule_assignment'].queryset = ScheduleAssignment.objects.filter(
+ schedule__employee__tenant=user.tenant,
+ assignment_date=today
+ ).select_related('schedule__employee')
+
+ # Set initial values for template compatibility
+ if self.instance and self.instance.pk:
+ self.fields['date'].initial = self.instance.work_date
+ self.fields['start_time'].initial = self.instance.clock_in_time.time() if self.instance.clock_in_time else None
+ self.fields['end_time'].initial = self.instance.clock_out_time.time() if self.instance.clock_out_time else None
+ self.fields['hours'].initial = self.instance.total_hours
+ self.fields['description'].initial = self.instance.notes
+
+ # Calculate break duration
+ if self.instance.break_start_time and self.instance.break_end_time:
+ break_duration = (self.instance.break_end_time - self.instance.break_start_time).total_seconds() / 60
+ self.fields['break_duration'].initial = int(break_duration)
+
+ # Set overtime based on entry type
+ self.fields['is_overtime'].initial = (self.instance.entry_type == 'OVERTIME')
+
def clean(self):
cleaned_data = super().clean()
+
+ # Map template fields to model fields
+ date = cleaned_data.get('date')
+ start_time = cleaned_data.get('start_time')
+ end_time = cleaned_data.get('end_time')
+ hours = cleaned_data.get('hours')
+ break_duration = cleaned_data.get('break_duration', 0)
+ description = cleaned_data.get('description')
+ schedule_assignment = cleaned_data.get('schedule_assignment')
+ is_overtime = cleaned_data.get('is_overtime')
+
+ # Set model field values
+ if date:
+ cleaned_data['work_date'] = date
+
+ # Combine date and time for datetime fields
+ if date and start_time:
+ cleaned_data['clock_in_time'] = timezone.datetime.combine(date, start_time)
+
+ if date and end_time:
+ cleaned_data['clock_out_time'] = timezone.datetime.combine(date, end_time)
+
+ if hours:
+ cleaned_data['total_hours'] = hours
+
+ if description:
+ cleaned_data['notes'] = description
+
+ if is_overtime:
+ cleaned_data['entry_type'] = 'OVERTIME'
+ else:
+ cleaned_data['entry_type'] = 'REGULAR'
+
+ # Set status based on auto_approve
+ auto_approve = cleaned_data.get('auto_approve', False)
+ if auto_approve:
+ cleaned_data['status'] = 'APPROVED'
+ else:
+ cleaned_data['status'] = 'SUBMITTED'
+
+ # Validation
clock_in_time = cleaned_data.get('clock_in_time')
clock_out_time = cleaned_data.get('clock_out_time')
- break_start_time = cleaned_data.get('break_start_time')
- break_end_time = cleaned_data.get('break_end_time')
- lunch_start_time = cleaned_data.get('lunch_start_time')
- lunch_end_time = cleaned_data.get('lunch_end_time')
-
+
if clock_in_time and clock_out_time:
if clock_out_time <= clock_in_time:
- self.add_error('clock_out_time', 'Clock out time must be after clock in time.')
-
+ self.add_error('end_time', 'End time must be after start time.')
+
# Check for reasonable shift length (max 24 hours)
duration = clock_out_time - clock_in_time
if duration.total_seconds() > 24 * 3600:
- self.add_error('clock_out_time', 'Shift duration cannot exceed 24 hours.')
-
- # Validate break times
- if break_start_time and not break_end_time:
- self.add_error('break_end_time', 'Break end time is required if break start time is provided.')
-
- if break_end_time and not break_start_time:
- self.add_error('break_start_time', 'Break start time is required if break end time is provided.')
-
- if break_start_time and break_end_time:
- if break_end_time <= break_start_time:
- self.add_error('break_end_time', 'Break end time must be after break start time.')
-
- if clock_in_time and break_start_time < clock_in_time:
- self.add_error('break_start_time', 'Break start time must be after clock in time.')
-
- if clock_out_time and break_end_time > clock_out_time:
- self.add_error('break_end_time', 'Break end time must be before clock out time.')
-
- # Validate lunch times
- if lunch_start_time and not lunch_end_time:
- self.add_error('lunch_end_time', 'Lunch end time is required if lunch start time is provided.')
-
- if lunch_end_time and not lunch_start_time:
- self.add_error('lunch_start_time', 'Lunch start time is required if lunch end time is provided.')
-
- if lunch_start_time and lunch_end_time:
- if lunch_end_time <= lunch_start_time:
- self.add_error('lunch_end_time', 'Lunch end time must be after lunch start time.')
-
- if clock_in_time and lunch_start_time < clock_in_time:
- self.add_error('lunch_start_time', 'Lunch start time must be after clock in time.')
-
- if clock_out_time and lunch_end_time > clock_out_time:
- self.add_error('lunch_end_time', 'Lunch end time must be before clock out time.')
-
+ self.add_error('end_time', 'Shift duration cannot exceed 24 hours.')
+
return cleaned_data
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+
+ # Ensure model fields are set from template fields
+ if hasattr(self, 'cleaned_data'):
+ if self.cleaned_data.get('work_date'):
+ instance.work_date = self.cleaned_data['work_date']
+ if self.cleaned_data.get('clock_in_time'):
+ instance.clock_in_time = self.cleaned_data['clock_in_time']
+ if self.cleaned_data.get('clock_out_time'):
+ instance.clock_out_time = self.cleaned_data['clock_out_time']
+ if self.cleaned_data.get('total_hours'):
+ instance.total_hours = self.cleaned_data['total_hours']
+ if self.cleaned_data.get('notes'):
+ instance.notes = self.cleaned_data['notes']
+ if self.cleaned_data.get('entry_type'):
+ instance.entry_type = self.cleaned_data['entry_type']
+ if self.cleaned_data.get('status'):
+ instance.status = self.cleaned_data['status']
+
+ if commit:
+ instance.save()
+ return instance
+
class PerformanceReviewForm(forms.ModelForm):
"""
- Form for creating and updating performance reviews.
+ Form for creating and updating performance reviews with enhanced template compatibility.
"""
+ # Additional fields to match template expectations
+ period_start = forms.DateField(
+ required=False,
+ widget=forms.HiddenInput(),
+ help_text='Review period start date'
+ )
+ period_end = forms.DateField(
+ required=False,
+ widget=forms.HiddenInput(),
+ help_text='Review period end date'
+ )
+ due_date = forms.DateField(
+ required=True,
+ widget=forms.DateInput(attrs={
+ 'class': 'form-control form-control-sm',
+ 'type': 'date'
+ }),
+ help_text='Review due date'
+ )
+ overall_score = forms.DecimalField(
+ required=False,
+ max_digits=3,
+ decimal_places=1,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control form-control-sm',
+ 'step': '0.1',
+ 'min': '1',
+ 'max': '5'
+ }),
+ help_text='Overall score (1-5)'
+ )
+ completion_percentage = forms.IntegerField(
+ required=False,
+ widget=forms.NumberInput(attrs={
+ 'class': 'form-control form-control-sm',
+ 'min': '0',
+ 'max': '100'
+ }),
+ help_text='Completion percentage for in-progress reviews'
+ )
+ reviewer_comments = forms.CharField(
+ required=False,
+ widget=forms.Textarea(attrs={
+ 'class': 'form-control form-control-sm',
+ 'rows': 4,
+ 'placeholder': 'Reviewer comments and feedback'
+ }),
+ help_text='Comments from the reviewer'
+ )
+ attachments = forms.FileField(
+ required=False,
+ widget=forms.FileInput(attrs={
+ 'class': 'form-control form-control-sm'
+ }),
+ help_text='Upload supporting documents'
+ )
+
class Meta:
model = PerformanceReview
fields = [
- 'employee', 'review_period_start', 'review_period_end',
- 'review_date', 'review_type', 'overall_rating',
- 'competency_ratings', 'goals_achieved', 'goals_not_achieved',
- 'future_goals', 'strengths', 'areas_for_improvement',
- 'development_plan', 'training_recommendations',
- 'employee_comments', 'employee_signature_date',
- 'status', 'notes'
+ 'employee', 'reviewer', 'review_type', 'status',
+ 'review_period_start', 'review_period_end', 'review_date',
+ 'overall_rating', 'competency_ratings', 'goals_achieved',
+ 'goals_not_achieved', 'future_goals', 'strengths',
+ 'areas_for_improvement', 'development_plan',
+ 'training_recommendations', 'employee_comments',
+ 'employee_signature_date', 'notes'
]
widgets = {
- 'employee': forms.Select(attrs={'class': 'form-control'}),
- 'review_period_start': forms.DateInput(attrs={
- 'class': 'form-control',
- 'type': 'date'
+ 'employee': forms.Select(attrs={
+ 'class': 'form-select form-select-sm',
+ 'data-placeholder': 'Select employee...'
}),
- 'review_period_end': forms.DateInput(attrs={
- 'class': 'form-control',
- 'type': 'date'
+ 'reviewer': forms.Select(attrs={
+ 'class': 'form-select form-select-sm',
+ 'data-placeholder': 'Select reviewer...'
}),
- 'review_date': forms.DateInput(attrs={
- 'class': 'form-control',
- 'type': 'date'
+ 'review_type': forms.Select(attrs={
+ 'class': 'form-select form-select-sm'
}),
- 'review_type': forms.Select(attrs={'class': 'form-control'}),
- 'overall_rating': forms.NumberInput(attrs={
- 'class': 'form-control',
- 'step': '0.1',
- 'min': '1',
- 'max': '5'
- }),
- 'competency_ratings': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 5,
- 'placeholder': '{"communication": 4, "teamwork": 5, ...}'
- }),
- 'goals_achieved': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Goals achieved during review period'
- }),
- 'goals_not_achieved': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Goals not achieved during review period'
- }),
- 'future_goals': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Goals for next review period'
+ 'status': forms.Select(attrs={
+ 'class': 'form-select form-select-sm'
}),
+ 'review_period_start': forms.HiddenInput(),
+ 'review_period_end': forms.HiddenInput(),
+ 'review_date': forms.HiddenInput(),
+ 'overall_rating': forms.HiddenInput(),
+ 'competency_ratings': forms.HiddenInput(),
+ 'goals_achieved': forms.HiddenInput(),
+ 'goals_not_achieved': forms.HiddenInput(),
+ 'future_goals': forms.HiddenInput(),
'strengths': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Employee strengths'
+ 'class': 'form-control form-control-sm',
+ 'rows': 4,
+ 'placeholder': 'List employee strengths (one per line)'
}),
'areas_for_improvement': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Areas for improvement'
+ 'class': 'form-control form-control-sm',
+ 'rows': 4,
+ 'placeholder': 'List areas for improvement (one per line)'
}),
'development_plan': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Professional development plan'
+ 'class': 'form-control form-control-sm',
+ 'rows': 4,
+ 'placeholder': 'Outline professional development plan'
}),
'training_recommendations': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
- 'placeholder': 'Training recommendations'
+ 'placeholder': 'Recommended training programs'
}),
'employee_comments': forms.Textarea(attrs={
- 'class': 'form-control',
- 'rows': 3,
- 'placeholder': 'Employee comments'
+ 'class': 'form-control form-control-sm',
+ 'rows': 4,
+ 'placeholder': 'Employee self-assessment and comments'
}),
'employee_signature_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
- 'status': forms.Select(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
- 'placeholder': 'Additional notes'
+ 'placeholder': 'Additional notes and observations'
}),
}
help_texts = {
+ 'employee': 'Employee being reviewed',
+ 'reviewer': 'Person conducting the review',
'review_type': 'Type of performance review',
- 'overall_rating': 'Overall rating (1-5)',
- 'competency_ratings': 'Individual competency ratings as JSON',
'status': 'Current status of the review',
+ 'overall_rating': 'Overall performance rating (1-5)',
+ 'competency_ratings': 'Individual competency ratings as JSON',
+ 'employee_signature_date': 'Date employee acknowledged review',
}
def __init__(self, *args, **kwargs):
@@ -1219,14 +1414,49 @@ class PerformanceReviewForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
- # Filter employee by tenant
+ # Filter employee and reviewer by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
+
+ self.fields['reviewer'].queryset = Employee.objects.filter(
+ tenant=user.tenant,
+ employment_status='ACTIVE'
+ ).order_by('last_name', 'first_name')
+
+ # Initialize template-compatible fields from model fields
+ if self.instance and self.instance.pk:
+ self.fields['period_start'].initial = self.instance.review_period_start
+ self.fields['period_end'].initial = self.instance.review_period_end
+ self.fields['due_date'].initial = self.instance.review_date
+ self.fields['overall_score'].initial = self.instance.overall_rating
+ # Set reviewer_comments from notes field for backward compatibility
+ self.fields['reviewer_comments'].initial = self.instance.notes
def clean(self):
cleaned_data = super().clean()
+
+ # Map template fields to model fields
+ period_start = cleaned_data.get('period_start')
+ period_end = cleaned_data.get('period_end')
+ due_date = cleaned_data.get('due_date')
+ overall_score = cleaned_data.get('overall_score')
+ reviewer_comments = cleaned_data.get('reviewer_comments')
+
+ # Set model field values from template fields
+ if period_start:
+ cleaned_data['review_period_start'] = period_start
+ if period_end:
+ cleaned_data['review_period_end'] = period_end
+ if due_date:
+ cleaned_data['review_date'] = due_date
+ if overall_score:
+ cleaned_data['overall_rating'] = overall_score
+ if reviewer_comments:
+ cleaned_data['notes'] = reviewer_comments
+
+ # Validation
review_period_start = cleaned_data.get('review_period_start')
review_period_end = cleaned_data.get('review_period_end')
review_date = cleaned_data.get('review_date')
@@ -1235,16 +1465,36 @@ class PerformanceReviewForm(forms.ModelForm):
if review_period_start and review_period_end:
if review_period_end < review_period_start:
- self.add_error('review_period_end', 'Review period end date must be after start date.')
+ self.add_error('period_end', 'Review period end date must be after start date.')
if review_date and review_period_end:
if review_date < review_period_end:
- self.add_error('review_date', 'Review date should be on or after the review period end date.')
+ self.add_error('due_date', 'Review date should be on or after the review period end date.')
if status == 'ACKNOWLEDGED' and not employee_signature_date:
self.add_error('employee_signature_date', 'Employee signature date is required for acknowledged reviews.')
return cleaned_data
+
+ def save(self, commit=True):
+ instance = super().save(commit=False)
+
+ # Ensure model fields are set from template fields
+ if hasattr(self, 'cleaned_data'):
+ if self.cleaned_data.get('period_start'):
+ instance.review_period_start = self.cleaned_data['period_start']
+ if self.cleaned_data.get('period_end'):
+ instance.review_period_end = self.cleaned_data['period_end']
+ if self.cleaned_data.get('due_date'):
+ instance.review_date = self.cleaned_data['due_date']
+ if self.cleaned_data.get('overall_score'):
+ instance.overall_rating = self.cleaned_data['overall_score']
+ if self.cleaned_data.get('reviewer_comments'):
+ instance.notes = self.cleaned_data['reviewer_comments']
+
+ if commit:
+ instance.save()
+ return instance
class TrainingRecordForm(forms.ModelForm):
@@ -1259,37 +1509,37 @@ class TrainingRecordForm(forms.ModelForm):
'status', 'credits_earned', 'score', 'passed', 'notes', 'cost_paid'
]
widgets = {
- 'employee': forms.Select(attrs={'class': 'form-control'}),
- 'program': forms.Select(attrs={'class': 'form-control'}),
- 'session': forms.Select(attrs={'class': 'form-control'}),
+ 'employee': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
+ 'session': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'started_at': forms.DateTimeInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'completion_date': forms.DateInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'type': 'date'
}),
- 'status': forms.Select(attrs={'class': 'form-control'}),
+ 'status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'credits_earned': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.1',
'min': '0'
}),
'score': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0',
'max': '100'
}),
'passed': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'cost_paid': forms.NumberInput(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'notes': forms.Textarea(attrs={
- 'class': 'form-control',
+ 'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Training notes and comments'
}),
diff --git a/hr/migrations/0001_initial.py b/hr/migrations/0001_initial.py
deleted file mode 100644
index 6bd0996d..00000000
--- a/hr/migrations/0001_initial.py
+++ /dev/null
@@ -1,1921 +0,0 @@
-# Generated by Django 5.2.6 on 2025-09-26 18:33
-
-import django.core.validators
-import django.db.models.deletion
-import uuid
-from decimal import Decimal
-from django.conf import settings
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ("core", "0001_initial"),
- migrations.swappable_dependency(settings.AUTH_USER_MODEL),
- ]
-
- operations = [
- 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"),
- ("ADMINISTRATIVE", "Administrative"),
- ("SUPPORT", "Support"),
- ("ANCILLARY", "Ancillary"),
- ("EXECUTIVE", "Executive"),
- ],
- help_text="Department type",
- 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(
- blank=True,
- decimal_places=2,
- help_text="Annual budget",
- max_digits=12,
- null=True,
- ),
- ),
- (
- "cost_center",
- models.CharField(
- blank=True,
- help_text="Cost center code",
- max_length=20,
- null=True,
- ),
- ),
- (
- "authorized_positions",
- models.PositiveIntegerField(
- default=0, help_text="Number of authorized positions"
- ),
- ),
- (
- "location",
- models.CharField(
- blank=True,
- help_text="Department location",
- 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",
- ),
- ),
- (
- "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(
- blank=True, help_text="Department notes", null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "parent_department",
- models.ForeignKey(
- blank=True,
- help_text="Parent department",
- null=True,
- on_delete=django.db.models.deletion.CASCADE,
- related_name="sub_departments",
- to="hr.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": "hr_department",
- "ordering": ["name"],
- },
- ),
- migrations.CreateModel(
- name="Employee",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "employee_id",
- models.CharField(editable=False, max_length=50, unique=True),
- ),
- ("identification_number", models.CharField(blank=True, max_length=20)),
- (
- "id_type",
- models.CharField(
- choices=[
- ("NATIONAL_ID", "National ID"),
- ("IQAMA", "IQAMA"),
- ("PASSPORT", "Passport"),
- ("OTHER", "Other"),
- ],
- default="NATIONAL_ID",
- max_length=20,
- ),
- ),
- ("first_name", models.CharField(blank=True, max_length=50)),
- (
- "father_name",
- models.CharField(blank=True, max_length=100, null=True),
- ),
- (
- "grandfather_name",
- models.CharField(blank=True, max_length=100, null=True),
- ),
- ("last_name", models.CharField(blank=True, max_length=50)),
- ("email", models.EmailField(blank=True, max_length=254, null=True)),
- (
- "phone",
- models.CharField(
- blank=True,
- max_length=16,
- null=True,
- validators=[
- django.core.validators.RegexValidator(
- message="Use E.164 format: +9665XXXXXXXX",
- regex="^\\+?9665\\d{8}$",
- )
- ],
- ),
- ),
- (
- "mobile_phone",
- models.CharField(
- blank=True,
- max_length=16,
- null=True,
- validators=[
- django.core.validators.RegexValidator(
- message="Use E.164 format: +9665XXXXXXXX",
- regex="^\\+?9665\\d{8}$",
- )
- ],
- ),
- ),
- (
- "address_line_1",
- models.CharField(blank=True, max_length=100, null=True),
- ),
- (
- "address_line_2",
- models.CharField(blank=True, max_length=100, null=True),
- ),
- ("city", models.CharField(blank=True, max_length=50, null=True)),
- ("postal_code", models.CharField(blank=True, max_length=10, null=True)),
- ("country", models.CharField(blank=True, max_length=50, null=True)),
- ("date_of_birth", models.DateField(blank=True, null=True)),
- (
- "gender",
- models.CharField(
- blank=True,
- choices=[
- ("MALE", "Male"),
- ("FEMALE", "Female"),
- ("OTHER", "Other"),
- ],
- max_length=20,
- null=True,
- ),
- ),
- (
- "marital_status",
- models.CharField(
- blank=True,
- choices=[
- ("SINGLE", "Single"),
- ("MARRIED", "Married"),
- ("DIVORCED", "Divorced"),
- ("WIDOWED", "Widowed"),
- ("SEPARATED", "Separated"),
- ("OTHER", "Other"),
- ],
- max_length=20,
- null=True,
- ),
- ),
- (
- "user_timezone",
- models.CharField(default="Asia/Riyadh", max_length=50),
- ),
- ("language", models.CharField(default="ar", max_length=10)),
- (
- "theme",
- models.CharField(
- choices=[
- ("LIGHT", "Light"),
- ("DARK", "Dark"),
- ("AUTO", "Auto"),
- ],
- default="LIGHT",
- max_length=20,
- ),
- ),
- (
- "role",
- models.CharField(
- choices=[
- ("SUPER_ADMIN", "Super Administrator"),
- ("ADMIN", "Administrator"),
- ("PHYSICIAN", "Physician"),
- ("SURGEON", "Surgeon"),
- ("NURSE", "Nurse"),
- ("NURSE_PRACTITIONER", "Nurse Practitioner"),
- ("PHYSICIAN_ASSISTANT", "Physician Assistant"),
- ("SURGICAL_TECHNICIAN", "Surgical Technician"),
- ("ANESTHESIOLOGIST", "Anesthesiologist"),
- (
- "ANESTHESIOLOGIST_ASSOCIATE",
- "Anesthesiologist Associate",
- ),
- ("CLINICAL_NURSE_ASSOCIATE", "Clinical Nurse Associate"),
- ("CLINICAL_NURSE_SPECIALIST", "Clinical Nurse Specialist"),
- ("CLINICAL_NURSE_MANAGER", "Clinical Nurse Manager"),
- ("CLINICAL_NURSE_TECHNICIAN", "Clinical Nurse Technician"),
- (
- "CLINICAL_NURSE_COORDINATOR",
- "Clinical Nurse Coordinator",
- ),
- ("FELLOW", "Fellow"),
- ("INTERN", "Intern"),
- ("INTERNSHIP", "Internship"),
- ("RESIDENT", "Resident"),
- ("WORK_FROM_HOME", "Work from Home"),
- ("WORK_FROM_HOME_PART_TIME", "Work from Home Part-time"),
- ("PHARMACIST", "Pharmacist"),
- ("PHARMACY_TECH", "Pharmacy Technician"),
- ("LAB_TECH", "Laboratory Technician"),
- ("RADIOLOGIST", "Radiologist"),
- ("RAD_TECH", "Radiology Technician"),
- ("RAD_SUPERVISOR", "Radiology Supervisor"),
- ("THERAPIST", "Therapist"),
- ("SOCIAL_WORKER", "Social Worker"),
- ("CASE_MANAGER", "Case Manager"),
- ("BILLING_SPECIALIST", "Billing Specialist"),
- ("REGISTRATION", "Registration Staff"),
- ("SCHEDULER", "Scheduler"),
- ("MEDICAL_ASSISTANT", "Medical Assistant"),
- ("CLERICAL", "Clerical Staff"),
- ("IT_SUPPORT", "IT Support"),
- ("QUALITY_ASSURANCE", "Quality Assurance"),
- ("COMPLIANCE", "Compliance Officer"),
- ("SECURITY", "Security"),
- ("MAINTENANCE", "Maintenance"),
- ("VOLUNTEER", "Volunteer"),
- ("STUDENT", "Student"),
- ("RESEARCHER", "Researcher"),
- ("CONSULTANT", "Consultant"),
- ("VENDOR", "Vendor"),
- ("GUEST", "Guest"),
- ],
- default="GUEST",
- max_length=50,
- ),
- ),
- (
- "job_title",
- models.CharField(
- blank=True, help_text="Job title", max_length=100, null=True
- ),
- ),
- (
- "license_number",
- models.CharField(
- blank=True,
- help_text="Professional license number",
- max_length=50,
- null=True,
- ),
- ),
- (
- "license_expiry_date",
- models.DateField(
- blank=True, help_text="License expiry date", null=True
- ),
- ),
- (
- "license_state",
- models.CharField(
- blank=True,
- help_text="Issuing state/authority",
- max_length=50,
- null=True,
- ),
- ),
- (
- "dea_number",
- models.CharField(
- blank=True,
- help_text="DEA number (if applicable)",
- max_length=20,
- null=True,
- ),
- ),
- (
- "npi_number",
- models.CharField(
- blank=True,
- help_text="NPI (if applicable)",
- max_length=10,
- null=True,
- ),
- ),
- (
- "employment_status",
- models.CharField(
- choices=[
- ("ACTIVE", "Active"),
- ("INACTIVE", "Inactive"),
- ("TERMINATED", "Terminated"),
- ("SUSPENDED", "Suspended"),
- ("LEAVE", "On Leave"),
- ("RETIRED", "Retired"),
- ],
- default="ACTIVE",
- max_length=20,
- ),
- ),
- (
- "employment_type",
- models.CharField(
- blank=True,
- choices=[
- ("FULL_TIME", "Full Time"),
- ("PART_TIME", "Part Time"),
- ("CONTRACT", "Contract"),
- ("TEMPORARY", "Temporary"),
- ("INTERN", "Intern"),
- ("VOLUNTEER", "Volunteer"),
- ("PER_DIEM", "Per Diem"),
- ("CONSULTANT", "Consultant"),
- ],
- help_text="Employment type",
- max_length=20,
- null=True,
- ),
- ),
- (
- "hire_date",
- models.DateField(blank=True, help_text="Hire date", null=True),
- ),
- (
- "termination_date",
- models.DateField(
- blank=True, help_text="Termination date", null=True
- ),
- ),
- (
- "hourly_rate",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- (
- "standard_hours_per_week",
- models.DecimalField(
- decimal_places=2, default=Decimal("40.00"), max_digits=5
- ),
- ),
- (
- "annual_salary",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=12, null=True
- ),
- ),
- (
- "fte_percentage",
- models.DecimalField(
- decimal_places=2,
- default=Decimal("100.00"),
- max_digits=5,
- validators=[
- django.core.validators.MinValueValidator(0),
- django.core.validators.MaxValueValidator(100),
- ],
- ),
- ),
- (
- "profile_picture",
- models.ImageField(
- blank=True, null=True, upload_to="profile_pictures/"
- ),
- ),
- (
- "bio",
- models.TextField(
- blank=True, help_text="Professional bio", null=True
- ),
- ),
- (
- "emergency_contact_name",
- models.CharField(blank=True, max_length=100, null=True),
- ),
- (
- "emergency_contact_relationship",
- models.CharField(blank=True, max_length=50, null=True),
- ),
- (
- "emergency_contact_phone",
- models.CharField(blank=True, max_length=20, null=True),
- ),
- ("notes", models.TextField(blank=True, null=True)),
- ("is_verified", models.BooleanField(default=False)),
- ("is_approved", models.BooleanField(default=False)),
- ("approval_date", models.DateTimeField(blank=True, null=True)),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "approved_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="approved_employees",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_employees",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "department",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="employees",
- to="hr.department",
- ),
- ),
- (
- "supervisor",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="direct_reports",
- to="hr.employee",
- ),
- ),
- (
- "tenant",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="employees",
- to="core.tenant",
- ),
- ),
- (
- "user",
- models.OneToOneField(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="employee_profile",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- options={
- "verbose_name": "Employee",
- "verbose_name_plural": "Employees",
- "db_table": "hr_employee",
- "ordering": ["last_name", "first_name"],
- },
- ),
- migrations.AddField(
- model_name="department",
- name="created_by",
- field=models.ForeignKey(
- blank=True,
- help_text="User who created the department",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_hr_departments",
- to="hr.employee",
- ),
- ),
- migrations.AddField(
- model_name="department",
- name="department_head",
- field=models.ForeignKey(
- blank=True,
- help_text="Department head",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="headed_departments",
- to="hr.employee",
- ),
- ),
- migrations.CreateModel(
- name="PerformanceReview",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "review_id",
- models.UUIDField(
- default=uuid.uuid4,
- editable=False,
- help_text="Unique review identifier",
- ),
- ),
- (
- "review_period_start",
- models.DateField(help_text="Review period start date"),
- ),
- (
- "review_period_end",
- models.DateField(help_text="Review period end date"),
- ),
- ("review_date", models.DateField(help_text="Review date")),
- (
- "review_type",
- models.CharField(
- choices=[
- ("ANNUAL", "Annual Review"),
- ("PROBATIONARY", "Probationary Review"),
- ("MID_YEAR", "Mid-Year Review"),
- ("PROJECT", "Project Review"),
- ("DISCIPLINARY", "Disciplinary Review"),
- ("PROMOTION", "Promotion Review"),
- ("OTHER", "Other Review"),
- ],
- help_text="Review type",
- max_length=20,
- ),
- ),
- (
- "overall_rating",
- models.DecimalField(
- decimal_places=1,
- help_text="Overall rating (1-5)",
- max_digits=3,
- validators=[
- django.core.validators.MinValueValidator(1),
- django.core.validators.MaxValueValidator(5),
- ],
- ),
- ),
- (
- "competency_ratings",
- models.JSONField(
- default=dict, help_text="Individual competency ratings"
- ),
- ),
- (
- "goals_achieved",
- models.TextField(
- blank=True,
- help_text="Goals achieved during review period",
- null=True,
- ),
- ),
- (
- "goals_not_achieved",
- models.TextField(
- blank=True,
- help_text="Goals not achieved during review period",
- null=True,
- ),
- ),
- (
- "future_goals",
- models.TextField(
- blank=True, help_text="Goals for next review period", null=True
- ),
- ),
- (
- "strengths",
- models.TextField(
- blank=True, help_text="Employee strengths", null=True
- ),
- ),
- (
- "areas_for_improvement",
- models.TextField(
- blank=True, help_text="Areas for improvement", null=True
- ),
- ),
- (
- "development_plan",
- models.TextField(
- blank=True, help_text="Professional development plan", null=True
- ),
- ),
- (
- "training_recommendations",
- models.TextField(
- blank=True, help_text="Training recommendations", null=True
- ),
- ),
- (
- "employee_comments",
- models.TextField(
- blank=True, help_text="Employee comments", null=True
- ),
- ),
- (
- "employee_signature_date",
- models.DateField(
- blank=True, help_text="Employee signature date", null=True
- ),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("DRAFT", "Draft"),
- ("SUBMITTED", "Submitted"),
- ("IN_PROGRESS", "In Progress"),
- ("COMPLETED", "Completed"),
- ("ACKNOWLEDGED", "Acknowledged by Employee"),
- ("DISPUTED", "Disputed"),
- ],
- default="DRAFT",
- help_text="Review status",
- max_length=20,
- ),
- ),
- (
- "notes",
- models.TextField(
- blank=True, help_text="Additional notes", null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "employee",
- models.ForeignKey(
- help_text="Employee being reviewed",
- on_delete=django.db.models.deletion.CASCADE,
- related_name="performance_reviews",
- to="hr.employee",
- ),
- ),
- (
- "reviewer",
- models.ForeignKey(
- blank=True,
- help_text="Reviewer",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="conducted_reviews",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- ],
- options={
- "verbose_name": "Performance Review",
- "verbose_name_plural": "Performance Reviews",
- "db_table": "hr_performance_review",
- "ordering": ["-review_date"],
- },
- ),
- migrations.CreateModel(
- name="Schedule",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "schedule_id",
- models.UUIDField(
- default=uuid.uuid4,
- editable=False,
- help_text="Unique schedule identifier",
- unique=True,
- ),
- ),
- ("name", models.CharField(help_text="Schedule name", max_length=100)),
- (
- "description",
- models.TextField(
- blank=True, help_text="Schedule description", null=True
- ),
- ),
- (
- "schedule_type",
- models.CharField(
- choices=[
- ("REGULAR", "Regular"),
- ("ROTATING", "Rotating"),
- ("FLEXIBLE", "Flexible"),
- ("ON_CALL", "On-Call"),
- ("TEMPORARY", "Temporary"),
- ],
- help_text="Schedule type",
- max_length=20,
- ),
- ),
- ("effective_date", models.DateField(help_text="Effective date")),
- (
- "end_date",
- models.DateField(blank=True, help_text="End date", null=True),
- ),
- (
- "schedule_pattern",
- models.JSONField(
- default=dict, help_text="Schedule pattern configuration"
- ),
- ),
- (
- "is_active",
- models.BooleanField(default=True, help_text="Schedule is active"),
- ),
- (
- "approval_date",
- models.DateTimeField(
- blank=True, help_text="Approval date and time", null=True
- ),
- ),
- (
- "notes",
- models.TextField(blank=True, help_text="Schedule notes", null=True),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "approved_by",
- models.ForeignKey(
- blank=True,
- help_text="User who approved the schedule",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="approved_schedules",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- help_text="User who created the schedule",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_schedules",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "employee",
- models.ForeignKey(
- help_text="Employee",
- on_delete=django.db.models.deletion.CASCADE,
- related_name="schedules",
- to="hr.employee",
- ),
- ),
- ],
- options={
- "verbose_name": "Schedule",
- "verbose_name_plural": "Schedules",
- "db_table": "hr_schedule",
- "ordering": ["-effective_date"],
- },
- ),
- migrations.CreateModel(
- name="ScheduleAssignment",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "assignment_id",
- models.UUIDField(
- default=uuid.uuid4,
- editable=False,
- help_text="Unique assignment identifier",
- unique=True,
- ),
- ),
- ("assignment_date", models.DateField(help_text="Assignment date")),
- ("start_time", models.TimeField(help_text="Start time")),
- ("end_time", models.TimeField(help_text="End time")),
- (
- "shift_type",
- models.CharField(
- choices=[
- ("DAY", "Day Shift"),
- ("EVENING", "Evening Shift"),
- ("NIGHT", "Night Shift"),
- ("WEEKEND", "Weekend Shift"),
- ("HOLIDAY", "Holiday Shift"),
- ("ON_CALL", "On-Call"),
- ("OVERTIME", "Overtime"),
- ],
- help_text="Shift type",
- max_length=20,
- ),
- ),
- (
- "location",
- models.CharField(
- blank=True,
- help_text="Specific location",
- max_length=100,
- null=True,
- ),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("SCHEDULED", "Scheduled"),
- ("CONFIRMED", "Confirmed"),
- ("COMPLETED", "Completed"),
- ("CANCELLED", "Cancelled"),
- ("NO_SHOW", "No Show"),
- ],
- default="SCHEDULED",
- help_text="Assignment status",
- max_length=20,
- ),
- ),
- (
- "break_minutes",
- models.PositiveIntegerField(
- default=0, help_text="Break time in minutes"
- ),
- ),
- (
- "lunch_minutes",
- models.PositiveIntegerField(
- default=0, help_text="Lunch time in minutes"
- ),
- ),
- (
- "notes",
- models.TextField(
- blank=True, help_text="Assignment notes", null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "department",
- models.ForeignKey(
- blank=True,
- help_text="Department",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="schedule_assignments",
- to="hr.department",
- ),
- ),
- (
- "schedule",
- models.ForeignKey(
- help_text="Schedule",
- on_delete=django.db.models.deletion.CASCADE,
- related_name="assignments",
- to="hr.schedule",
- ),
- ),
- ],
- options={
- "verbose_name": "Schedule Assignment",
- "verbose_name_plural": "Schedule Assignments",
- "db_table": "hr_schedule_assignment",
- "ordering": ["assignment_date", "start_time"],
- },
- ),
- migrations.CreateModel(
- name="TimeEntry",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "entry_id",
- models.UUIDField(
- default=uuid.uuid4,
- editable=False,
- help_text="Unique time entry identifier",
- unique=True,
- ),
- ),
- ("work_date", models.DateField(help_text="Work date")),
- (
- "clock_in_time",
- models.DateTimeField(
- blank=True, help_text="Clock in time", null=True
- ),
- ),
- (
- "clock_out_time",
- models.DateTimeField(
- blank=True, help_text="Clock out time", null=True
- ),
- ),
- (
- "break_start_time",
- models.DateTimeField(
- blank=True, help_text="Break start time", null=True
- ),
- ),
- (
- "break_end_time",
- models.DateTimeField(
- blank=True, help_text="Break end time", null=True
- ),
- ),
- (
- "lunch_start_time",
- models.DateTimeField(
- blank=True, help_text="Lunch start time", null=True
- ),
- ),
- (
- "lunch_end_time",
- models.DateTimeField(
- blank=True, help_text="Lunch end time", null=True
- ),
- ),
- (
- "regular_hours",
- models.DecimalField(
- decimal_places=2,
- default=Decimal("0.00"),
- help_text="Regular hours worked",
- max_digits=5,
- ),
- ),
- (
- "overtime_hours",
- models.DecimalField(
- decimal_places=2,
- default=Decimal("0.00"),
- help_text="Overtime hours worked",
- max_digits=5,
- ),
- ),
- (
- "total_hours",
- models.DecimalField(
- decimal_places=2,
- default=Decimal("0.00"),
- help_text="Total hours worked",
- max_digits=5,
- ),
- ),
- (
- "entry_type",
- models.CharField(
- choices=[
- ("REGULAR", "Regular Time"),
- ("OVERTIME", "Overtime"),
- ("HOLIDAY", "Holiday"),
- ("VACATION", "Vacation"),
- ("SICK", "Sick Leave"),
- ("PERSONAL", "Personal Time"),
- ("TRAINING", "Training"),
- ],
- default="REGULAR",
- help_text="Entry type",
- max_length=20,
- ),
- ),
- (
- "location",
- models.CharField(
- blank=True, help_text="Work location", max_length=100, null=True
- ),
- ),
- (
- "approval_date",
- models.DateTimeField(
- blank=True, help_text="Approval date and time", null=True
- ),
- ),
- (
- "status",
- models.CharField(
- choices=[
- ("DRAFT", "Draft"),
- ("SUBMITTED", "Submitted"),
- ("APPROVED", "Approved"),
- ("REJECTED", "Rejected"),
- ("PAID", "Paid"),
- ],
- default="DRAFT",
- help_text="Entry status",
- max_length=20,
- ),
- ),
- (
- "notes",
- models.TextField(
- blank=True, help_text="Time entry notes", null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "approved_by",
- models.ForeignKey(
- blank=True,
- help_text="User who approved the time entry",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="approved_time_entries",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "department",
- models.ForeignKey(
- blank=True,
- help_text="Department",
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="time_entries",
- to="hr.department",
- ),
- ),
- (
- "employee",
- models.ForeignKey(
- help_text="Employee",
- on_delete=django.db.models.deletion.CASCADE,
- related_name="time_entries",
- to="hr.employee",
- ),
- ),
- ],
- options={
- "verbose_name": "Time Entry",
- "verbose_name_plural": "Time Entries",
- "db_table": "hr_time_entry",
- "ordering": ["-work_date"],
- },
- ),
- migrations.CreateModel(
- name="TrainingPrograms",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "program_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- ("name", models.CharField(max_length=200)),
- ("description", models.TextField(blank=True, null=True)),
- (
- "program_type",
- models.CharField(
- choices=[
- ("ORIENTATION", "Orientation"),
- ("MANDATORY", "Mandatory Training"),
- ("CONTINUING_ED", "Continuing Education"),
- ("CERTIFICATION", "Certification"),
- ("SKILLS", "Skills Training"),
- ("SAFETY", "Safety Training"),
- ("COMPLIANCE", "Compliance Training"),
- ("LEADERSHIP", "Leadership Development"),
- ("TECHNICAL", "Technical Training"),
- ("OTHER", "Other"),
- ],
- max_length=20,
- ),
- ),
- (
- "program_provider",
- models.CharField(blank=True, max_length=200, null=True),
- ),
- ("start_date", models.DateField(blank=True, null=True)),
- ("end_date", models.DateField(blank=True, null=True)),
- (
- "duration_hours",
- models.DecimalField(
- decimal_places=2, default=Decimal("0.00"), max_digits=5
- ),
- ),
- (
- "cost",
- models.DecimalField(
- decimal_places=2, default=Decimal("0.00"), max_digits=10
- ),
- ),
- ("is_certified", models.BooleanField(default=False)),
- ("validity_days", models.PositiveIntegerField(blank=True, null=True)),
- (
- "notify_before_days",
- models.PositiveIntegerField(blank=True, null=True),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_training_programs",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "instructor",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="instructor_programs",
- to="hr.employee",
- ),
- ),
- (
- "tenant",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="training_programs",
- to="core.tenant",
- ),
- ),
- ],
- options={
- "db_table": "hr_training_program",
- "ordering": ["name"],
- },
- ),
- migrations.CreateModel(
- name="ProgramPrerequisite",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="prerequisites",
- to="hr.trainingprograms",
- ),
- ),
- (
- "required_program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="unlocking_programs",
- to="hr.trainingprograms",
- ),
- ),
- ],
- options={
- "db_table": "hr_training_program_prerequisite",
- },
- ),
- migrations.CreateModel(
- name="ProgramModule",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("title", models.CharField(max_length=200)),
- ("order", models.PositiveIntegerField(default=1)),
- (
- "hours",
- models.DecimalField(
- decimal_places=2, default=Decimal("0.00"), max_digits=5
- ),
- ),
- (
- "program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="modules",
- to="hr.trainingprograms",
- ),
- ),
- ],
- options={
- "db_table": "hr_training_program_module",
- "ordering": ["program", "order"],
- },
- ),
- migrations.CreateModel(
- name="TrainingRecord",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "record_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- ("enrolled_at", models.DateTimeField(auto_now_add=True)),
- ("started_at", models.DateTimeField(blank=True, null=True)),
- ("completion_date", models.DateField(blank=True, null=True)),
- ("expiry_date", models.DateField(blank=True, null=True)),
- (
- "status",
- models.CharField(
- choices=[
- ("SCHEDULED", "Scheduled"),
- ("IN_PROGRESS", "In Progress"),
- ("COMPLETED", "Completed"),
- ("CANCELLED", "Cancelled"),
- ("NO_SHOW", "No Show"),
- ("FAILED", "Failed"),
- ("WAITLISTED", "Waitlisted"),
- ],
- default="SCHEDULED",
- max_length=20,
- ),
- ),
- (
- "credits_earned",
- models.DecimalField(
- decimal_places=2, default=Decimal("0.00"), max_digits=5
- ),
- ),
- (
- "score",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- ("passed", models.BooleanField(default=False)),
- ("notes", models.TextField(blank=True, null=True)),
- (
- "cost_paid",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_training_records",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "employee",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="training_records",
- to="hr.employee",
- ),
- ),
- (
- "program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.PROTECT,
- related_name="training_records",
- to="hr.trainingprograms",
- ),
- ),
- ],
- options={
- "verbose_name": "Training Enrollment",
- "verbose_name_plural": "Training Enrollments",
- "db_table": "hr_training_record",
- "ordering": ["-enrolled_at"],
- },
- ),
- migrations.CreateModel(
- name="TrainingCertificates",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "certificate_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- ("certificate_name", models.CharField(max_length=200)),
- (
- "certificate_number",
- models.CharField(blank=True, max_length=50, null=True),
- ),
- (
- "certification_body",
- models.CharField(blank=True, max_length=200, null=True),
- ),
- ("issued_date", models.DateField(auto_now_add=True)),
- ("expiry_date", models.DateField(blank=True, null=True)),
- (
- "file",
- models.FileField(blank=True, null=True, upload_to="certificates/"),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- ("updated_at", models.DateTimeField(auto_now=True)),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_training_certificates",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "employee",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="training_certificates",
- to="hr.employee",
- ),
- ),
- (
- "signed_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="signed_training_certificates",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.PROTECT,
- related_name="certificates",
- to="hr.trainingprograms",
- ),
- ),
- (
- "enrollment",
- models.OneToOneField(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="certificate",
- to="hr.trainingrecord",
- ),
- ),
- ],
- options={
- "verbose_name": "Training Certificate",
- "verbose_name_plural": "Training Certificates",
- "db_table": "hr_training_certificate",
- "ordering": ["-issued_date"],
- },
- ),
- migrations.CreateModel(
- name="TrainingAttendance",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("checked_in_at", models.DateTimeField(blank=True, null=True)),
- ("checked_out_at", models.DateTimeField(blank=True, null=True)),
- (
- "status",
- models.CharField(
- choices=[
- ("PRESENT", "Present"),
- ("LATE", "Late"),
- ("ABSENT", "Absent"),
- ("EXCUSED", "Excused"),
- ],
- default="PRESENT",
- max_length=10,
- ),
- ),
- ("notes", models.CharField(blank=True, max_length=255, null=True)),
- (
- "enrollment",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="attendance",
- to="hr.trainingrecord",
- ),
- ),
- ],
- options={
- "db_table": "hr_training_attendance",
- "ordering": ["enrollment_id", "checked_in_at"],
- },
- ),
- migrations.CreateModel(
- name="TrainingAssessment",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- ("name", models.CharField(max_length=200)),
- (
- "max_score",
- models.DecimalField(decimal_places=2, default=100, max_digits=7),
- ),
- (
- "score",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=7, null=True
- ),
- ),
- ("passed", models.BooleanField(default=False)),
- ("taken_at", models.DateTimeField(blank=True, null=True)),
- ("notes", models.TextField(blank=True, null=True)),
- (
- "enrollment",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="assessments",
- to="hr.trainingrecord",
- ),
- ),
- ],
- options={
- "db_table": "hr_training_assessment",
- "ordering": ["-taken_at"],
- },
- ),
- migrations.CreateModel(
- name="TrainingSession",
- fields=[
- (
- "id",
- models.BigAutoField(
- auto_created=True,
- primary_key=True,
- serialize=False,
- verbose_name="ID",
- ),
- ),
- (
- "session_id",
- models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
- ),
- (
- "title",
- models.CharField(
- blank=True,
- help_text="Optional run title; falls back to program name",
- max_length=200,
- null=True,
- ),
- ),
- (
- "delivery_method",
- models.CharField(
- choices=[
- ("IN_PERSON", "In Person"),
- ("VIRTUAL", "Virtual"),
- ("HYBRID", "Hybrid"),
- ("SELF_PACED", "Self Paced"),
- ],
- default="IN_PERSON",
- max_length=12,
- ),
- ),
- ("start_at", models.DateTimeField()),
- ("end_at", models.DateTimeField()),
- ("location", models.CharField(blank=True, max_length=200, null=True)),
- ("capacity", models.PositiveIntegerField(default=0)),
- (
- "cost_override",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=10, null=True
- ),
- ),
- (
- "hours_override",
- models.DecimalField(
- blank=True, decimal_places=2, max_digits=5, null=True
- ),
- ),
- ("created_at", models.DateTimeField(auto_now_add=True)),
- (
- "created_by",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="created_training_sessions",
- to=settings.AUTH_USER_MODEL,
- ),
- ),
- (
- "instructor",
- models.ForeignKey(
- blank=True,
- null=True,
- on_delete=django.db.models.deletion.SET_NULL,
- related_name="instructed_sessions",
- to="hr.employee",
- ),
- ),
- (
- "program",
- models.ForeignKey(
- on_delete=django.db.models.deletion.CASCADE,
- related_name="sessions",
- to="hr.trainingprograms",
- ),
- ),
- ],
- options={
- "verbose_name": "Training Session",
- "verbose_name_plural": "Training Sessions",
- "db_table": "hr_training_session",
- "ordering": ["-start_at"],
- },
- ),
- migrations.AddField(
- model_name="trainingrecord",
- name="session",
- field=models.ForeignKey(
- on_delete=django.db.models.deletion.PROTECT,
- related_name="enrollments",
- to="hr.trainingsession",
- ),
- ),
- migrations.AddIndex(
- model_name="employee",
- index=models.Index(
- fields=["tenant", "employee_id"], name="hr_employee_tenant__d723cf_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="employee",
- index=models.Index(
- fields=["tenant", "role"], name="hr_employee_tenant__1c6def_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="employee",
- index=models.Index(
- fields=["tenant", "department"], name="hr_employee_tenant__1baa3c_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="employee",
- index=models.Index(
- fields=["tenant", "employment_status"],
- name="hr_employee_tenant__54b199_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="department",
- index=models.Index(
- fields=["tenant", "department_type"],
- name="hr_departme_tenant__87fa85_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="department",
- index=models.Index(fields=["code"], name="hr_departme_code_d27daf_idx"),
- ),
- migrations.AddIndex(
- model_name="department",
- index=models.Index(fields=["name"], name="hr_departme_name_00be75_idx"),
- ),
- migrations.AddIndex(
- model_name="department",
- index=models.Index(
- fields=["is_active"], name="hr_departme_is_acti_b443d7_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="department",
- unique_together={("tenant", "code")},
- ),
- migrations.AddIndex(
- model_name="performancereview",
- index=models.Index(
- fields=["employee", "review_date"],
- name="hr_performa_employe_604c29_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="performancereview",
- index=models.Index(
- fields=["review_type"], name="hr_performa_review__a7c664_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="performancereview",
- index=models.Index(fields=["status"], name="hr_performa_status_460939_idx"),
- ),
- migrations.AddIndex(
- model_name="performancereview",
- index=models.Index(
- fields=["overall_rating"], name="hr_performa_overall_0257f6_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="schedule",
- index=models.Index(
- fields=["employee", "effective_date"],
- name="hr_schedule_employe_2bbf02_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="schedule",
- index=models.Index(
- fields=["schedule_type"], name="hr_schedule_schedul_19e2a5_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="schedule",
- index=models.Index(
- fields=["is_active"], name="hr_schedule_is_acti_2e906a_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="scheduleassignment",
- index=models.Index(
- fields=["schedule", "assignment_date"],
- name="hr_schedule_schedul_85a37e_idx",
- ),
- ),
- migrations.AddIndex(
- model_name="scheduleassignment",
- index=models.Index(
- fields=["assignment_date"], name="hr_schedule_assignm_cf8fba_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="scheduleassignment",
- index=models.Index(fields=["status"], name="hr_schedule_status_a9da0d_idx"),
- ),
- migrations.AddIndex(
- model_name="timeentry",
- index=models.Index(
- fields=["employee", "work_date"], name="hr_time_ent_employe_9f8566_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="timeentry",
- index=models.Index(
- fields=["work_date"], name="hr_time_ent_work_da_898420_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="timeentry",
- index=models.Index(fields=["status"], name="hr_time_ent_status_c67efb_idx"),
- ),
- migrations.AddIndex(
- model_name="timeentry",
- index=models.Index(
- fields=["entry_type"], name="hr_time_ent_entry_t_eba769_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="trainingprograms",
- index=models.Index(
- fields=["tenant", "program_type"], name="hr_training_tenant__d8062d_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="trainingprograms",
- index=models.Index(
- fields=["tenant", "is_certified"], name="hr_training_tenant__4892af_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="trainingprograms",
- unique_together={("tenant", "name")},
- ),
- migrations.AlterUniqueTogether(
- name="programprerequisite",
- unique_together={("program", "required_program")},
- ),
- migrations.AddIndex(
- model_name="programmodule",
- index=models.Index(
- fields=["program", "order"], name="hr_training_program_f99b01_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="programmodule",
- unique_together={("program", "order")},
- ),
- migrations.AddIndex(
- model_name="trainingcertificates",
- index=models.Index(
- fields=["certificate_number"], name="hr_training_certifi_928fdb_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="trainingcertificates",
- unique_together={("employee", "program", "enrollment")},
- ),
- migrations.AddIndex(
- model_name="trainingattendance",
- index=models.Index(
- fields=["enrollment"], name="hr_training_enrollm_104874_idx"
- ),
- ),
- migrations.AddIndex(
- model_name="trainingassessment",
- index=models.Index(
- fields=["enrollment"], name="hr_training_enrollm_71763a_idx"
- ),
- ),
- migrations.AlterUniqueTogether(
- name="trainingrecord",
- unique_together={("employee", "session")},
- ),
- ]
diff --git a/hr/migrations/__pycache__/0001_initial.cpython-312.pyc b/hr/migrations/__pycache__/0001_initial.cpython-312.pyc
deleted file mode 100644
index a2ec4e8d..00000000
Binary files a/hr/migrations/__pycache__/0001_initial.cpython-312.pyc and /dev/null differ
diff --git a/hr/templates/hr/employee_confirm_delete.html b/hr/templates/forDelete/employee_confirm_delete.html
similarity index 100%
rename from hr/templates/hr/employee_confirm_delete.html
rename to hr/templates/forDelete/employee_confirm_delete.html
diff --git a/hr/templates/hr/assignments/schedule_assignment_list.html b/hr/templates/hr/assignments/schedule_assignment_list.html
index 2aaa0202..8f930a49 100644
--- a/hr/templates/hr/assignments/schedule_assignment_list.html
+++ b/hr/templates/hr/assignments/schedule_assignment_list.html
@@ -441,45 +441,7 @@
{% if is_paginated %}
-
-
-
-
-
+ {% include 'partial/pagination.html' %}
{% endif %}