All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
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.
281 lines
12 KiB
Python
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
|