HH/apps/projects/export_utils.py
2026-04-19 10:53:12 +03:00

229 lines
9.1 KiB
Python

from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
NAVY_FILL = PatternFill(start_color="005696", end_color="005696", fill_type="solid")
BLUE_FILL = PatternFill(start_color="E0F2FE", end_color="E0F2FE", fill_type="solid")
GREEN_FILL = PatternFill(start_color="DCFCE7", end_color="DCFCE7", fill_type="solid")
LIGHT_FILL = PatternFill(start_color="F8FAFC", end_color="F8FAFC", fill_type="solid")
WHITE_FONT = Font(color="FFFFFF", bold=True, size=11)
HEADER_FONT = Font(color="FFFFFF", bold=True, size=12)
TITLE_FONT = Font(color="005696", bold=True, size=14)
SECTION_FONT = Font(color="005696", bold=True, size=11)
BODY_FONT = Font(size=10)
BOLD_FONT = Font(bold=True, size=10)
THIN_BORDER = Border(
left=Side(style="thin", color="E2E8F0"),
right=Side(style="thin", color="E2E8F0"),
top=Side(style="thin", color="E2E8F0"),
bottom=Side(style="thin", color="E2E8F0"),
)
WRAP_ALIGN = Alignment(wrap_text=True, vertical="top")
CENTER_ALIGN = Alignment(horizontal="center", vertical="center")
def _write_header_row(ws, row, headers, fill=None, font=None):
col = 1
for h in headers:
cell = ws.cell(row=row, column=col, value=h)
cell.font = font or WHITE_FONT
cell.fill = fill or NAVY_FILL
cell.alignment = CENTER_ALIGN
cell.border = THIN_BORDER
col += 1
def _write_row(ws, row, values, fill=None, font=None):
col = 1
for v in values:
cell = ws.cell(row=row, column=col, value=v)
cell.font = font or BODY_FONT
cell.fill = fill or PatternFill()
cell.alignment = WRAP_ALIGN
cell.border = THIN_BORDER
col += 1
def export_project_excel(project):
wb = Workbook()
# --- Sheet 1: Project Overview ---
ws_overview = wb.active
ws_overview.title = "Project Overview"
ws_overview.merge_cells("A1:D1")
title_cell = ws_overview.cell(row=1, column=1, value=f"QI Project: {project.name}")
title_cell.font = TITLE_FONT
info_data = [
("Description", project.description),
("Hospital", project.hospital.name if project.hospital else ""),
("Department", project.department.name if project.department else ""),
("Project Lead", project.project_lead.get_full_name() if project.project_lead else ""),
("Created By", project.created_by.get_full_name() if project.created_by else ""),
("Status", project.get_status_display()),
("Start Date", str(project.start_date) if project.start_date else ""),
("Target Completion", str(project.target_completion_date) if project.target_completion_date else ""),
("Actual Completion", str(project.actual_completion_date) if project.actual_completion_date else ""),
("Outcome", project.outcome_description or ""),
]
_write_header_row(ws_overview, 3, ["Field", "Value"])
for i, (field, value) in enumerate(info_data, start=4):
fill = LIGHT_FILL if i % 2 == 0 else None
_write_row(ws_overview, i, [field, value], fill=fill)
ws_overview.cell(row=i, column=1).font = BOLD_FONT
ws_overview.column_dimensions["A"].width = 25
ws_overview.column_dimensions["B"].width = 60
# Team Members
row = 4 + len(info_data) + 1
ws_overview.merge_cells(f"A{row}:D{row}")
ws_overview.cell(row=row, column=1, value="Team Members").font = SECTION_FONT
row += 1
_write_header_row(ws_overview, row, ["Name", "Email", "Role(s)"])
row += 1
for member in project.team_members.all():
roles = ", ".join(member.get_role_names()) if hasattr(member, "get_role_names") else ""
_write_row(ws_overview, row, [member.get_full_name(), member.email, roles])
row += 1
# --- Sheet 2: PDCA Phases ---
ws_pdca = wb.create_sheet("PDCA Cycle")
ws_pdca.merge_cells("A1:F1")
ws_pdca.cell(row=1, column=1, value="PDCA Cycle").font = TITLE_FONT
_write_header_row(ws_pdca, 3, ["Phase", "Title", "Status", "Owner", "Start Date", "Due Date"])
row = 4
phase_fills = {
"plan": PatternFill(start_color="FEF3C7", end_color="FEF3C7", fill_type="solid"),
"do": PatternFill(start_color="DBEAFE", end_color="DBEAFE", fill_type="solid"),
"check": PatternFill(start_color="F3E8FF", end_color="F3E8FF", fill_type="solid"),
"act": PatternFill(start_color="DCFCE7", end_color="DCFCE7", fill_type="solid"),
}
for phase_key, phase_label in [("plan", "Plan"), ("do", "Do"), ("check", "Check"), ("act", "Act")]:
pdca_phase = None
for p in project.pdca_phases.all():
if p.phase == phase_key:
pdca_phase = p
break
fill = phase_fills.get(phase_key, LIGHT_FILL)
if pdca_phase:
_write_row(
ws_pdca,
row,
[
phase_label,
pdca_phase.title,
pdca_phase.get_status_display(),
pdca_phase.owner.get_full_name() if pdca_phase.owner else "",
str(pdca_phase.start_date) if pdca_phase.start_date else "",
str(pdca_phase.due_date) if pdca_phase.due_date else "",
],
fill=fill,
)
row += 1
if pdca_phase.description:
ws_pdca.merge_cells(f"B{row}:F{row}")
desc_cell = ws_pdca.cell(row=row, column=2, value=f"Description: {pdca_phase.description}")
desc_cell.font = Font(size=9, italic=True, color="64748B")
desc_cell.alignment = WRAP_ALIGN
row += 1
if pdca_phase.findings:
ws_pdca.merge_cells(f"B{row}:F{row}")
find_cell = ws_pdca.cell(row=row, column=2, value=f"Findings: {pdca_phase.findings}")
find_cell.font = Font(size=9, italic=True, color="64748B")
find_cell.alignment = WRAP_ALIGN
row += 1
else:
_write_row(ws_pdca, row, [phase_label, "(Not started)", "", "", "", ""], fill=fill)
row += 1
row += 1
ws_pdca.column_dimensions["A"].width = 12
ws_pdca.column_dimensions["B"].width = 40
ws_pdca.column_dimensions["C"].width = 15
ws_pdca.column_dimensions["D"].width = 20
ws_pdca.column_dimensions["E"].width = 15
ws_pdca.column_dimensions["F"].width = 15
# --- Sheet 3: Phase Tasks ---
ws_tasks = wb.create_sheet("Phase Tasks")
ws_tasks.merge_cells("A1:G1")
ws_tasks.cell(row=1, column=1, value="Tasks by PDCA Phase").font = TITLE_FONT
row = 3
phase_order = ["plan", "do", "check", "act"]
for phase_key in phase_order:
phase_label = dict(PDCAPhaseChoices.choices).get(phase_key, phase_key)
phase_obj = None
for p in project.pdca_phases.all():
if p.phase == phase_key:
phase_obj = p
break
# Phase header
ws_tasks.merge_cells(f"A{row}:G{row}")
phase_header = ws_tasks.cell(row=row, column=1, value=f"{phase_label} Phase")
phase_header.font = SECTION_FONT
phase_header.fill = LIGHT_FILL
phase_header.alignment = CENTER_ALIGN
phase_header.border = THIN_BORDER
row += 1
_write_header_row(ws_tasks, row, ["Title", "Description", "Assigned To", "Status", "Due Date", "Completed"])
row += 1
if phase_obj:
phase_tasks = phase_obj.tasks.all().order_by("order")
if phase_tasks:
for task in phase_tasks:
fill = GREEN_FILL if task.status == "completed" else None
_write_row(
ws_tasks,
row,
[
task.title,
task.description,
task.assigned_to.get_full_name() if task.assigned_to else "",
task.get_status_display(),
str(task.due_date) if task.due_date else "",
str(task.completed_date) if task.completed_date else "",
],
fill=fill,
)
row += 1
else:
ws_tasks.merge_cells(f"A{row}:G{row}")
no_tasks = ws_tasks.cell(row=row, column=1, value="No tasks in this phase")
no_tasks.font = Font(size=10, italic=True, color="94A3B8")
no_tasks.alignment = CENTER_ALIGN
row += 1
else:
ws_tasks.merge_cells(f"A{row}:G{row}")
no_phase = ws_tasks.cell(row=row, column=1, value="Phase not started")
no_phase.font = Font(size=10, italic=True, color="94A3B8")
no_phase.alignment = CENTER_ALIGN
row += 1
row += 1 # spacing between phases
ws_tasks.column_dimensions["A"].width = 30
ws_tasks.column_dimensions["B"].width = 40
ws_tasks.column_dimensions["C"].width = 20
ws_tasks.column_dimensions["D"].width = 15
ws_tasks.column_dimensions["E"].width = 15
ws_tasks.column_dimensions["F"].width = 15
return wb