All checks were successful
Build and Push Docker Image / build (push) Successful in 1m5s
209 lines
19 KiB
Python
209 lines
19 KiB
Python
# Generated by Django 6.0.1 on 2026-05-11 20:32
|
|
|
|
import apps.observations.models
|
|
import django.db.models.deletion
|
|
import django.utils.timezone
|
|
import uuid
|
|
from django.conf import settings
|
|
from django.db import migrations, models
|
|
|
|
|
|
class Migration(migrations.Migration):
|
|
|
|
initial = True
|
|
|
|
dependencies = [
|
|
('organizations', '0001_initial'),
|
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
]
|
|
|
|
operations = [
|
|
migrations.CreateModel(
|
|
name='ObservationAttachment',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('file', models.FileField(help_text='Uploaded file', upload_to='observations/%Y/%m/%d/')),
|
|
('filename', models.CharField(blank=True, max_length=500)),
|
|
('file_type', models.CharField(blank=True, max_length=100)),
|
|
('file_size', models.IntegerField(default=0, help_text='File size in bytes')),
|
|
('description', models.CharField(blank=True, max_length=500)),
|
|
],
|
|
options={
|
|
'ordering': ['-created_at'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='ObservationCategory',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
|
('name_ar', models.CharField(blank=True, max_length=200, verbose_name='Name (Arabic)')),
|
|
('description', models.TextField(blank=True)),
|
|
('is_active', models.BooleanField(db_index=True, default=True)),
|
|
('sort_order', models.IntegerField(default=0, help_text='Lower numbers appear first')),
|
|
('icon', models.CharField(blank=True, help_text='Bootstrap icon class', max_length=50)),
|
|
],
|
|
options={
|
|
'verbose_name': 'Observation Category',
|
|
'verbose_name_plural': 'Observation Categories',
|
|
'ordering': ['sort_order', 'name_en'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='ObservationNote',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('note', models.TextField()),
|
|
('is_internal', models.BooleanField(default=True, help_text='Internal notes are not visible to public')),
|
|
],
|
|
options={
|
|
'ordering': ['-created_at'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='ObservationSLAConfig',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('severity', models.CharField(blank=True, choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], help_text='Severity level for this SLA (optional = default config)', max_length=20, null=True)),
|
|
('sla_hours', models.IntegerField(help_text='Number of hours until SLA deadline')),
|
|
('first_reminder_hours_after', models.IntegerField(default=0, help_text='Send 1st reminder X hours after observation creation (0 = use reminder_hours_before)')),
|
|
('second_reminder_hours_after', models.IntegerField(default=0, help_text='Send 2nd reminder X hours after observation creation (0 = use second_reminder_hours_before)')),
|
|
('escalation_hours_after', models.IntegerField(default=0, help_text='Escalate observation X hours after creation if unresolved (0 = use overdue logic)')),
|
|
('reminder_hours_before', models.IntegerField(default=24, help_text='Send first reminder X hours before deadline')),
|
|
('second_reminder_enabled', models.BooleanField(default=False, help_text='Enable sending a second reminder')),
|
|
('second_reminder_hours_before', models.IntegerField(default=6, help_text='Send second reminder X hours before deadline')),
|
|
('is_active', models.BooleanField(default=True)),
|
|
('dept_response_hours', models.IntegerField(default=48, help_text='Hours for department to submit a response')),
|
|
('dept_response_reminder_hours_before', models.IntegerField(default=12, help_text='Send 1st reminder X hours before dept response deadline')),
|
|
('dept_response_second_reminder_enabled', models.BooleanField(default=True, help_text='Enable sending a second reminder for dept response')),
|
|
('dept_response_second_reminder_hours_before', models.IntegerField(default=4, help_text='Send 2nd reminder X hours before dept response deadline')),
|
|
('dept_response_auto_escalate_enabled', models.BooleanField(default=True, help_text='Auto-escalate to department manager if response overdue')),
|
|
('dept_response_escalation_hours_overdue', models.IntegerField(default=0, help_text='Escalate X hours after dept response deadline (0 = immediately)')),
|
|
],
|
|
options={
|
|
'verbose_name': 'Observation SLA Config',
|
|
'verbose_name_plural': 'Observation SLA Configs',
|
|
'ordering': ['hospital', 'severity'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='ObservationStatusLog',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('from_status', models.CharField(blank=True, choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response')], max_length=25)),
|
|
('to_status', models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response')], max_length=25)),
|
|
('comment', models.TextField(blank=True, help_text='Optional comment about the status change')),
|
|
],
|
|
options={
|
|
'verbose_name': 'Observation Status Log',
|
|
'verbose_name_plural': 'Observation Status Logs',
|
|
'ordering': ['-created_at'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='ObservationSubCategory',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('name_en', models.CharField(max_length=200, verbose_name='Name (English)')),
|
|
('name_ar', models.CharField(blank=True, max_length=200, verbose_name='Name (Arabic)')),
|
|
('description', models.TextField(blank=True)),
|
|
('is_active', models.BooleanField(db_index=True, default=True)),
|
|
('sort_order', models.IntegerField(default=0, help_text='Lower numbers appear first')),
|
|
],
|
|
options={
|
|
'verbose_name': 'Observation Sub-Category',
|
|
'verbose_name_plural': 'Observation Sub-Categories',
|
|
'ordering': ['sort_order', 'name_en'],
|
|
},
|
|
),
|
|
migrations.CreateModel(
|
|
name='Observation',
|
|
fields=[
|
|
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
('updated_at', models.DateTimeField(auto_now=True)),
|
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
|
('is_deleted', models.BooleanField(db_index=True, default=False)),
|
|
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
|
('tracking_code', models.CharField(default=apps.observations.models.generate_tracking_code, help_text='Unique code for tracking this observation', max_length=20, unique=True)),
|
|
('title', models.CharField(blank=True, help_text='Optional short title', max_length=300)),
|
|
('description', models.TextField(help_text='Detailed description of the observation')),
|
|
('description_en', models.TextField(blank=True, help_text='English description of the observation')),
|
|
('severity', models.CharField(choices=[('low', 'Low'), ('medium', 'Medium'), ('high', 'High'), ('critical', 'Critical')], db_index=True, default='medium', max_length=20)),
|
|
('location_text', models.CharField(blank=True, help_text='Where the issue was observed (building, floor, room, etc.)', max_length=500)),
|
|
('incident_datetime', models.DateTimeField(default=django.utils.timezone.now, help_text='When the issue was observed')),
|
|
('reporter_staff_id', models.CharField(blank=True, help_text='Optional staff ID of the reporter', max_length=50)),
|
|
('reporter_name', models.CharField(blank=True, help_text='Optional name of the reporter', max_length=200)),
|
|
('reporter_phone', models.CharField(blank=True, help_text='Optional phone number for follow-up', max_length=20)),
|
|
('reporter_email', models.EmailField(blank=True, help_text='Optional email for follow-up', max_length=254)),
|
|
('patient_file_number', models.CharField(blank=True, help_text='Medical record number / file number of the patient', max_length=100)),
|
|
('status', models.CharField(choices=[('new', 'New'), ('triaged', 'Triaged'), ('assigned', 'Assigned'), ('in_progress', 'In Progress'), ('resolved', 'Resolved'), ('closed', 'Closed'), ('rejected', 'Rejected'), ('duplicate', 'Duplicate'), ('contacted', 'Contacted'), ('contacted_no_response', 'Contacted, No Response')], db_index=True, default='new', max_length=25)),
|
|
('source', models.CharField(choices=[('staff_portal', 'Staff Portal'), ('web_form', 'Web Form'), ('mobile_app', 'Mobile App'), ('email', 'Email'), ('call_center', 'Call Center'), ('other', 'Other')], default='staff_portal', help_text='How the observation was submitted (legacy)', max_length=50)),
|
|
('assigned_at', models.DateTimeField(blank=True, null=True)),
|
|
('person_noted', models.CharField(blank=True, help_text='Person who was informed/notified about the observation', max_length=300)),
|
|
('communication_method', models.CharField(blank=True, help_text='How communication was made (extension, mobile, office, etc.)', max_length=50)),
|
|
('communication_datetime', models.DateTimeField(blank=True, help_text='When the person/department was contacted', null=True)),
|
|
('activated_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when observation was first activated (moved to IN_PROGRESS)', null=True)),
|
|
('due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline', null=True)),
|
|
('is_overdue', models.BooleanField(db_index=True, default=False)),
|
|
('breached_at', models.DateTimeField(blank=True, db_index=True, help_text='Timestamp when observation first breached SLA', null=True)),
|
|
('reminder_sent_at', models.DateTimeField(blank=True, help_text='First SLA reminder timestamp', null=True)),
|
|
('second_reminder_sent_at', models.DateTimeField(blank=True, help_text='Second SLA reminder timestamp', null=True)),
|
|
('escalated_at', models.DateTimeField(blank=True, null=True)),
|
|
('triaged_at', models.DateTimeField(blank=True, null=True)),
|
|
('department_response_en', models.TextField(blank=True, verbose_name='Department Response (English)')),
|
|
('department_response_ar', models.TextField(blank=True, verbose_name='Department Response (Arabic)')),
|
|
('department_response_summary_en', models.TextField(blank=True, verbose_name='AI Summary of Dept Response (EN)')),
|
|
('department_response_summary_ar', models.TextField(blank=True, verbose_name='AI Summary of Dept Response (AR)')),
|
|
('department_responded_at', models.DateTimeField(blank=True, null=True)),
|
|
('forwarded_to_dept_at', models.DateTimeField(blank=True, help_text='When the observation was sent to the department', null=True)),
|
|
('dept_response_sla_due_at', models.DateTimeField(blank=True, db_index=True, help_text='SLA deadline for department response', null=True)),
|
|
('dept_response_is_overdue', models.BooleanField(db_index=True, default=False, help_text='Department response is overdue')),
|
|
('dept_response_reminder_sent_at', models.DateTimeField(blank=True, help_text='First SLA reminder for dept response', null=True)),
|
|
('dept_response_second_reminder_sent_at', models.DateTimeField(blank=True, help_text='Second SLA reminder for dept response', null=True)),
|
|
('dept_response_escalated_at', models.DateTimeField(blank=True, help_text='When dept response was escalated to manager', null=True)),
|
|
('dept_response_acceptance_status', models.CharField(choices=[('pending', 'Pending Review'), ('acceptable', 'Acceptable'), ('not_acceptable', 'Not Acceptable')], default='pending', help_text='Review status of the department response', max_length=20)),
|
|
('dept_response_accepted_at', models.DateTimeField(blank=True, help_text='When the department response was reviewed', null=True)),
|
|
('dept_response_acceptance_notes', models.TextField(blank=True, help_text='Notes about the acceptance decision')),
|
|
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
|
('resolution_notes', models.TextField(blank=True)),
|
|
('closed_at', models.DateTimeField(blank=True, null=True)),
|
|
('action_id', models.UUIDField(blank=True, help_text='ID of linked PX Action if converted', null=True)),
|
|
('client_ip', models.GenericIPAddressField(blank=True, null=True)),
|
|
('user_agent', models.TextField(blank=True)),
|
|
('metadata', models.JSONField(blank=True, default=dict)),
|
|
('submitter_notified_at', models.DateTimeField(blank=True, help_text='When confirmation message was sent to submitter', null=True)),
|
|
('responsible_person_notified_at', models.DateTimeField(blank=True, help_text='When email was sent to responsible person', null=True)),
|
|
('monthly_follow_up_due_at', models.DateTimeField(blank=True, db_index=True, help_text='When monthly follow-up is due', null=True)),
|
|
('monthly_follow_up_completed_at', models.DateTimeField(blank=True, help_text='When monthly follow-up was completed', null=True)),
|
|
('monthly_follow_up_notes', models.TextField(blank=True, help_text='Notes from monthly follow-up')),
|
|
('assigned_department', models.ForeignKey(blank=True, help_text='Department responsible for handling this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to='organizations.department')),
|
|
('assigned_to', models.ForeignKey(blank=True, help_text='User assigned to handle this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_observations', to=settings.AUTH_USER_MODEL)),
|
|
('closed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='closed_observations', to=settings.AUTH_USER_MODEL)),
|
|
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_%(class)s_set', to=settings.AUTH_USER_MODEL)),
|
|
('department_noted', models.ForeignKey(blank=True, help_text='Department that was notified about this observation', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='noted_observations', to='organizations.department')),
|
|
('department_responded_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='department_observation_responses', to=settings.AUTH_USER_MODEL)),
|
|
('dept_response_accepted_by', models.ForeignKey(blank=True, help_text='User who reviewed the department response', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_observation_dept_responses', to=settings.AUTH_USER_MODEL)),
|
|
('hospital', models.ForeignKey(blank=True, help_text='Hospital where observation was made', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='organizations.hospital')),
|
|
('location', models.ForeignKey(blank=True, help_text='Location where the observation was made', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.location')),
|
|
('main_section', models.ForeignKey(blank=True, help_text='Main section within the location', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='observations', to='organizations.mainsection')),
|
|
('monthly_follow_up_completed_by', models.ForeignKey(blank=True, help_text='User who completed the monthly follow-up', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='completed_observation_followups', to=settings.AUTH_USER_MODEL)),
|
|
],
|
|
options={
|
|
'ordering': ['-created_at'],
|
|
'permissions': [('triage_observation', 'Can triage observations'), ('manage_categories', 'Can manage observation categories')],
|
|
},
|
|
),
|
|
]
|