HH/apps/organizations/management/commands/import_staff_excel.py
ismail 7369d08012
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
feat: unified reference numbers + feedback modules QA audit
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001):
- new ReferenceSequence model + generate_reference() helper (apps/core)
- Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save()
- prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only)
- removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources
- migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012
- unit tests (format, sanitization, monthly reset, 40-thread concurrency)

QA audit:
- isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env)
- feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion)
- reports/feedback-modules-qa-report.md

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
2026-06-14 14:29:23 +03:00

281 lines
12 KiB
Python

import re
import pandas as pd
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.db import transaction
from apps.organizations.models import Department, Hospital, Staff
LOCATION_TO_HOSPITAL = {
"Suwaidi": "HH-S",
"Nuzha": "HH-N",
"Olaya": "HH-A",
"السويدي": "HH-S",
"النزهة": "HH-N",
"العليا": "HH-A",
}
HR_NAME_ALIASES = {
"Accident And Emergency": "Emergency Medicine Department",
"Corporate Administration": "Executive Administration",
"Senior Management Offices": "Executive Administration",
"Corporate Communication Department": "Patient Experience Department",
"Marketing Department": "Patient Experience Department",
"Porter Department": "Security Department",
"Transportation Department": "Security Department",
"Supply Chain": "Facility Management & Maintenance Department",
"Pharmacy Warehouse Alaziziyah": "Pharmacy Department",
"Family Medicine": "Outpatient Department",
"Business Development": "Executive Administration",
"Continuous Medical Education Managment": "Medical Administration",
"Academic Education And Training Affairs": "Medical Administration",
"Innovation Communication Management": "Medical Administration",
"Transformation And Change Management": "Medical Administration",
"Talent Acquisition Department": "HR Department",
"Internal Audit": "Financial Collection & Claims Department",
"Legal Affairs Department": "Executive Administration",
"Cybersecurity Management": "Information Technology Department",
"Rehabilitation Center": "Outpatient Department",
}
def normalize_name(name):
if not name:
return ""
return re.sub(r"\s+", " ", name.strip()).strip()
class Command(BaseCommand):
help = "Import staff from employees.xlsx (both Arabic Sheet1 + English Sheet2)"
def add_arguments(self, parser):
parser.add_argument("--file", default="data/employees.xlsx")
parser.add_argument("--update-existing", action="store_true")
def handle(self, *args, **options):
filepath = options["file"]
update_existing = options["update_existing"]
hospitals = {h.code: h for h in Hospital.objects.all()}
if not hospitals:
self.stderr.write("No hospitals found. Create them first.")
return
self.stdout.write(f"Available hospitals: {list(hospitals.keys())}")
df_ar = pd.read_excel(filepath, header=None, skiprows=3, sheet_name=0)
df_ar.columns = [
"_skip", "employee_number", "name_ar", "manager", "national_id",
"location_ar", "department_ar", "section_ar", "job_title_ar",
"mobile", "personal_email", "work_email",
]
df_ar = df_ar.drop(columns=["_skip"])
df_ar = df_ar[df_ar["employee_number"].notna()]
df_ar = df_ar[df_ar["name_ar"].notna()]
df_ar["employee_id"] = df_ar["employee_number"].apply(
lambda x: str(int(x)) if pd.notna(x) and str(x).strip().replace(".", "").isdigit() else None
)
df_en = pd.read_excel(filepath, header=None, skiprows=3, sheet_name="Sheet2")
df_en.columns = [
"_skip", "employee_number", "name", "manager", "national_id",
"location", "department", "section", "job_title", "country",
"mobile", "personal_email", "work_email",
]
df_en = df_en.drop(columns=["_skip"])
df_en = df_en[df_en["employee_number"].notna()]
df_en = df_en[df_en["name"].notna()]
df_en["employee_id"] = df_en["employee_number"].apply(
lambda x: str(int(x)) if pd.notna(x) and str(x).strip().replace(".", "").isdigit() else None
)
en_lookup = {}
for _, row in df_en.iterrows():
eid = row.get("employee_id")
if eid:
en_lookup[eid] = row
self.stdout.write(f"Arabic records: {len(df_ar)}, English records: {len(df_en)}")
dept_cache = {}
stats = {
"created": 0, "updated": 0, "skipped": 0,
"no_dept": 0, "no_hospital": 0, "errors": 0,
}
staff_map = {}
existing_staff = {s.employee_id: s for s in Staff.objects.all()}
with transaction.atomic():
for idx, (_, row_ar) in enumerate(df_ar.iterrows(), 1):
try:
emp_id = row_ar.get("employee_id")
if not emp_id:
continue
name_ar_raw = normalize_name(str(row_ar.get("name_ar", "")))
if not name_ar_raw:
continue
parts_ar = name_ar_raw.split(None, 1)
first_name_ar = parts_ar[0] if parts_ar else ""
last_name_ar = parts_ar[1] if len(parts_ar) > 1 else ""
row_en = en_lookup.get(emp_id)
if row_en is not None:
name_en = normalize_name(str(row_en.get("name", "")))
parts_en = name_en.split(None, 1)
first_name = parts_en[0] if parts_en else ""
last_name = parts_en[1] if len(parts_en) > 1 else ""
location_en = normalize_name(str(row_en.get("location", "")))
dept_en = normalize_name(str(row_en.get("department", "")))
section_en = normalize_name(str(row_en.get("section", "")))
job_title_en = normalize_name(str(row_en.get("job_title", "")))
country = normalize_name(str(row_en.get("country", "")))
mobile_en = normalize_name(str(row_en.get("mobile", "")))
work_email_en = normalize_name(str(row_en.get("work_email", "")))
personal_email_en = normalize_name(str(row_en.get("personal_email", "")))
else:
name_en = ""
first_name = first_name_ar
last_name = last_name_ar
location_en = ""
dept_en = ""
section_en = ""
job_title_en = ""
country = ""
mobile_en = ""
work_email_en = ""
personal_email_en = ""
manager_raw = str(row_ar.get("manager", "")).strip() if pd.notna(row_ar.get("manager")) else ""
national_id = str(row_ar.get("national_id", "")).strip() if pd.notna(row_ar.get("national_id")) else ""
job_title_ar_val = normalize_name(str(row_ar.get("job_title_ar", "")))
dept_ar = normalize_name(str(row_ar.get("department_ar", "")))
section_ar = normalize_name(str(row_ar.get("section_ar", "")))
location_ar = normalize_name(str(row_ar.get("location_ar", "")))
mobile_ar = normalize_name(str(row_ar.get("mobile", "")))
personal_email_ar = normalize_name(str(row_ar.get("personal_email", "")))
work_email_ar = normalize_name(str(row_ar.get("work_email", "")))
hospital_code = LOCATION_TO_HOSPITAL.get(location_en) or LOCATION_TO_HOSPITAL.get(location_ar)
hospital = hospitals.get(hospital_code) if hospital_code else None
if not hospital:
stats["no_hospital"] += 1
continue
department = None
if dept_en and hospital_code:
cache_key = (hospital_code, dept_en.lower())
if cache_key not in dept_cache:
dept_cache[cache_key] = self._find_dept(
hospitals[hospital_code], dept_en
)
department = dept_cache[cache_key]
if not department and dept_en:
stats["no_dept"] += 1
existing = existing_staff.get(emp_id)
staff_data = {
"name": name_en or name_ar_raw,
"first_name": first_name or first_name_ar,
"last_name": last_name or last_name_ar,
"name_ar": name_ar_raw,
"first_name_ar": first_name_ar,
"last_name_ar": last_name_ar,
"staff_type": "other",
"department_type": "",
"job_title": job_title_en or job_title_ar_val,
"job_title_ar": job_title_ar_val,
"specialization": "",
"email": work_email_en or personal_email_en or work_email_ar or personal_email_ar,
"phone": mobile_en or mobile_ar,
"hospital": hospital,
"department": department,
"civil_id": national_id,
"location": location_en or location_ar,
"location_ar": location_ar,
"department_name": dept_en or dept_ar,
"department_name_ar": dept_ar,
"section": section_en or section_ar,
"section_ar": section_ar,
"subsection": "",
"subsection_ar": "",
"country": country,
"status": "active",
}
if existing and update_existing:
for k, v in staff_data.items():
setattr(existing, k, v)
existing.save()
staff_map[emp_id] = existing
stats["updated"] += 1
elif existing:
staff_map[emp_id] = existing
stats["skipped"] += 1
else:
staff = Staff(employee_id=emp_id, **staff_data)
staff.save()
staff_map[emp_id] = staff
stats["created"] += 1
if idx % 500 == 0:
self.stdout.write(f" Processed {idx}/{len(df_ar)}...")
except Exception as e:
self.stdout.write(self.style.ERROR(f" [{idx}] Error: {e}"))
stats["errors"] += 1
self.stdout.write("\nLinking managers...")
manager_lookup = {}
for _, row in df_ar.iterrows():
eid = row.get("employee_id")
mgr_raw = str(row.get("manager", "")).strip() if pd.notna(row.get("manager")) else ""
if eid and mgr_raw:
manager_lookup[eid] = mgr_raw
linked = 0
for emp_id, staff in staff_map.items():
manager_raw = manager_lookup.get(emp_id, "")
if not manager_raw:
continue
m = re.match(r"^(\d+)\s*-", manager_raw)
if not m:
continue
mgr_id = m.group(1)
mgr = staff_map.get(mgr_id)
if mgr and staff.report_to != mgr:
staff.report_to = mgr
staff.save(update_fields=["report_to"])
linked += 1
self.stdout.write(f" Linked {linked} manager relationships")
self.stdout.write(self.style.SUCCESS(f"\nDone!"))
self.stdout.write(f" Created: {stats['created']}")
self.stdout.write(f" Updated: {stats['updated']}")
self.stdout.write(f" Skipped: {stats['skipped']}")
self.stdout.write(f" No dept match: {stats['no_dept']}")
self.stdout.write(f" No hospital match: {stats['no_hospital']}")
self.stdout.write(f" Errors: {stats['errors']}")
def _find_dept(self, hospital, hr_dept_name):
dept = Department.objects.filter(
hospital=hospital, status="active"
).filter(
Q(hr_name__iexact=hr_dept_name) | Q(name_en__iexact=hr_dept_name)
).first()
if dept:
return dept
alias_name_en = HR_NAME_ALIASES.get(hr_dept_name)
if alias_name_en:
return Department.objects.filter(
hospital=hospital, name_en__iexact=alias_name_en, status="active"
).first()
return None