HH/apps/complaints/management/commands/import_2025_complaints_basic.py
2026-03-28 14:03:56 +03:00

303 lines
11 KiB
Python

"""
Import 2025 complaints from Excel with basic fields (no AI, skip missing columns).
2025 has different structure than 2022-2024:
- No 4-level taxonomy (skip)
- No Staff ID column (use staff_name text only)
- No Rightful Side column (skip)
Usage:
python manage.py import_2025_complaints_basic "Complaints Report - 2025.xlsx" --sheet="JAN"
"""
import logging
import re
from datetime import datetime
from typing import Dict, Optional
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from apps.accounts.models import User
from apps.complaints.models import Complaint
from apps.organizations.models import Hospital, Location, MainSection, SubSection
logger = logging.getLogger(__name__)
DEFAULT_HOSPITAL_CODE = "NUZHA-DEV"
# 2025 Column mapping (different from 2022-2024)
COLUMN_MAPPING = {
"complaint_num": 3, # رقم الشكوى
"mrn": 4, # رقم الملف
"source": 5, # جهة الشكوى
"location_name": 6, # الموقع
"main_dept_name": 7, # القسم الرئيس
"sub_dept_name": 8, # القسم الفرعي
"date_received": 9, # تاريخ إستلام الشكوى
"data_entry_person": 10, # المدخل
"response_date": 48, # تاريخ الرد (was Staff ID in 2022-2024)
"staff_name": 51, # اسم الشخص المشتكى عليه (was col 49)
# Skip cols 52-53 (Complain Classification, Main Subject)
"description_ar": 54, # محتوى الشكوى (عربي)
"description_en": 55, # محتوى الشكوى (English)
"satisfaction": 56, # توثيق تذكيرات للقسم المشتكى عليه
"reminder_date": 57, # تاريخ التذكير
}
# Month mapping for 2025 sheet names (3-letter abbreviations)
MONTH_MAP = {
"JAN": "01", "FEB": "02", "MAR": "03", "APR": "04",
"MAY": "05", "JUN": "06", "JUL": "07", "AUG": "08",
"SEP": "09", "OCT": "10", "NOV": "11", "DEC": "12",
}
class Command(BaseCommand):
help = "Import 2025 complaints with basic fields (no taxonomy, no staff linking)"
def add_arguments(self, parser):
parser.add_argument("excel_file", type=str)
parser.add_argument("--sheet", type=str, default="JAN")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--start-row", type=int, default=3)
def handle(self, *args, **options):
self.excel_file = options["excel_file"]
self.sheet_name = options["sheet"]
self.dry_run = options["dry_run"]
self.start_row = options["start_row"]
# Load hospital
self.hospital = self._load_hospital()
if not self.hospital:
raise CommandError(f'Hospital "{DEFAULT_HOSPITAL_CODE}" not found')
self.stdout.write(f"Using hospital: {self.hospital.name}")
# Load Excel
try:
import openpyxl
self.wb = openpyxl.load_workbook(self.excel_file)
except ImportError:
raise CommandError("openpyxl required: pip install openpyxl")
if self.sheet_name not in self.wb.sheetnames:
available = ", ".join(self.wb.sheetnames)
raise CommandError(f'Sheet "{self.sheet_name}" not found. Available: {available}')
self.ws = self.wb[self.sheet_name]
self.stdout.write(f"Processing sheet: {self.sheet_name}")
# Stats
self.stats = {"processed": 0, "success": 0, "failed": 0}
self.errors = []
# Process
self._process_sheet()
self._print_report()
def _load_hospital(self) -> Optional[Hospital]:
try:
return Hospital.objects.get(code=DEFAULT_HOSPITAL_CODE)
except Hospital.DoesNotExist:
return None
def _process_sheet(self):
row_num = self.start_row
while row_num <= self.ws.max_row:
try:
row_data = self._extract_row_data(row_num)
if not row_data.get("complaint_num"):
row_num += 1
continue
self.stats["processed"] += 1
# Build reference number
ref_num = self._build_reference_number(row_data["complaint_num"])
# Check for duplicate
if Complaint.objects.filter(reference_number=ref_num).exists():
row_num += 1
continue
# Parse dates
date_received = self._parse_datetime(row_data.get("date_received"))
created_at = date_received or timezone.now()
response_date = self._parse_datetime(row_data.get("response_date"))
reminder_date = self._parse_datetime(row_data.get("reminder_date"))
# Resolve location/departments
location = self._resolve_location(row_data.get("location_name"))
main_section = self._resolve_section(row_data.get("main_dept_name"))
subsection = self._resolve_subsection(row_data.get("sub_dept_name"))
# Get/create data entry user
assigned_to_user = self._get_or_create_data_entry_user(row_data.get("data_entry_person"))
# Determine status
status = "open"
if response_date:
status = "resolved"
if not self.dry_run:
with transaction.atomic():
complaint = Complaint.objects.create(
reference_number=ref_num,
hospital=self.hospital,
location=location,
main_section=main_section,
subsection=subsection,
title=self._build_title(row_data),
description=self._build_description(row_data),
patient_name="Unknown",
national_id="",
relation_to_patient="patient",
staff=None, # No staff linking for 2025
staff_name=row_data.get("staff_name") or "",
# No taxonomy fields for 2025
domain=None,
category=None,
subcategory_obj=None,
classification_obj=None,
status=status,
assigned_to=assigned_to_user,
resolved_by=assigned_to_user if response_date else None,
# Timeline
created_at=created_at,
explanation_requested=bool(date_received),
explanation_requested_at=date_received,
explanation_received_at=response_date,
reminder_sent_at=reminder_date,
metadata={
"import_source": "2025_excel_basic",
"original_sheet": self.sheet_name,
"complaint_num": row_data.get("complaint_num"),
},
)
self.stats["success"] += 1
except Exception as e:
self.stats["failed"] += 1
self.errors.append({"row": row_num, "error": str(e)})
logger.error(f"Row {row_num}: {e}", exc_info=True)
row_num += 1
def _extract_row_data(self, row_num: int) -> Dict:
data = {}
for field, col in COLUMN_MAPPING.items():
cell_value = self.ws.cell(row_num, col).value
data[field] = cell_value
return data
def _build_reference_number(self, complaint_num) -> str:
sheet_parts = self.sheet_name.strip().split()
year = "2025"
month_part = sheet_parts[0].upper()
month_code = MONTH_MAP.get(month_part, "00")
return f"CMP-{year}-{month_code}-{int(complaint_num):04d}"
def _parse_datetime(self, value) -> Optional[datetime]:
if not value:
return None
if isinstance(value, datetime):
return value
if isinstance(value, str):
try:
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except ValueError:
try:
return datetime.strptime(value, "%Y-%m-%d")
except ValueError:
return None
return None
def _resolve_location(self, name_ar: str) -> Optional[Location]:
if not name_ar:
return None
return Location.objects.filter(name_ar=name_ar).first()
def _resolve_section(self, name_ar: str) -> Optional[MainSection]:
if not name_ar:
return None
return MainSection.objects.filter(name_ar=name_ar).first()
def _resolve_subsection(self, name_ar: str) -> Optional[SubSection]:
if not name_ar:
return None
return SubSection.objects.filter(name_ar=name_ar).first()
def _get_or_create_data_entry_user(self, arabic_name: str) -> Optional[User]:
if not arabic_name:
return None
try:
from unidecode import unidecode
except ImportError:
return None
parts = arabic_name.split()
if len(parts) >= 2:
first, last = parts[0], parts[-1]
else:
first, last = arabic_name, "coordinator"
username_first = re.sub(r"[^a-z0-9]", "", unidecode(first).lower().strip())
username_last = re.sub(r"[^a-z0-9]", "", unidecode(last).lower().strip())
if not username_first:
username_first = "user"
if not username_last:
username_last = "coordinator"
username = f"{username_first}.{username_last}"
user = User.objects.filter(username=username).first()
if user:
return user
try:
user = User(
username=username,
first_name=arabic_name,
last_name="",
email=f"{username}@alhammadi.med.sa",
is_active=True,
)
user.save()
return user
except Exception:
return None
def _build_title(self, row_data: Dict) -> str:
desc = row_data.get("description_en") or row_data.get("description_ar") or ""
return desc[:500] if desc else "No description"
def _build_description(self, row_data: Dict) -> str:
desc_en = row_data.get("description_en") or ""
desc_ar = row_data.get("description_ar") or ""
if desc_en and desc_ar:
return f"{desc_en}\n\n[Arabic]:\n{desc_ar}"
return desc_en or desc_ar or "No description provided"
def _print_report(self):
self.stdout.write("\n" + "=" * 60)
self.stdout.write("IMPORT REPORT - 2025 BASIC")
self.stdout.write("=" * 60)
self.stdout.write(f"Sheet: {self.sheet_name}")
self.stdout.write(f"Mode: {'DRY RUN' if self.dry_run else 'ACTUAL'}")
self.stdout.write(f"\nProcessed: {self.stats['processed']}")
self.stdout.write(f"Success: {self.stats['success']}")
self.stdout.write(f"Failed: {self.stats['failed']}")
if self.errors:
self.stdout.write(f"\nErrors: {len(self.errors)}")
for error in self.errors[:5]:
self.stdout.write(f" Row {error['row']}: {error['error']}")