from io import BytesIO from openpyxl import Workbook from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter from .observation_report import SOURCE_MAP BLUE_FILL = PatternFill(start_color="2F75B5", end_color="2F75B5", fill_type="solid") HEADER_FONT = Font(name="Calibri", size=11, bold=True, color="FFFFFF") HEADER_ALIGN = Alignment(horizontal="center", vertical="center", wrap_text=True) DATA_FONT = Font(name="Calibri", size=11) DATA_ALIGN = Alignment(horizontal="center", vertical="center", wrap_text=True) RTL_ALIGN = Alignment(horizontal="right", vertical="center", wrap_text=True) THIN = Side(style="thin") THIN_BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) ROW1_AR = [ ("A1", "F1", "Portal "), ("G1", "L1", "Portal "), ("M1", "M1", "اسم الشخص الذي تم تبليغه"), ("N1", "N1", "القسم الذي تم تبليغه"), ("O1", "O1", "وسيلة التواصل"), ("P1", "P1", "تاريخ التواصل"), ("Q1", "Q1", "وقت التواصل"), ("R1", "R1", "خطة العمل او الاجراء المتخذ في الوقت الراهن "), ("S1", "S1", "المتابعة - هل تم حل المشكلة"), ("T1", "T1", "حلول واقتراحات "), ] ROW2_EN = [ "Note NO.", "تاريخ ارسال الرسالة", "وقت إرسال الرسالة", "رقم الجوال المرسل إليه", "رقم الملف ", "الرقم الوظيفي للمرسل", "مصدر الملاحظة", "التصنيف الرئيسي", "التصنيف الفرعي", "موضوع الملاحظة", "كتابة الملاحظة", "English", "Person Noted ", "Department Noted", "Cummunication Via ", "Date", "Time", "Action Plan", "Follow-Up", "Follow-Up", ] def _apply_header_style(ws): for start, end, val in ROW1_AR: start_col = ord(start[0]) - ord("A") + 1 end_col = ord(end[0]) - ord("A") + 1 cell = ws.cell(row=1, column=start_col, value=val) cell.font = HEADER_FONT cell.fill = BLUE_FILL cell.alignment = HEADER_ALIGN cell.border = THIN_BORDER if start != end: ws.merge_cells(start_row=1, start_column=start_col, end_row=1, end_column=end_col) for c in range(start_col + 1, end_col + 1): bc = ws.cell(row=1, column=c) bc.font = HEADER_FONT bc.fill = BLUE_FILL bc.border = THIN_BORDER for i, h in enumerate(ROW2_EN): cell = ws.cell(row=2, column=i + 1, value=h) cell.font = HEADER_FONT cell.fill = BLUE_FILL cell.alignment = HEADER_ALIGN cell.border = THIN_BORDER def _parse_notes(obs): action_taken = "" resolved = "" solutions = "" for note in obs.notes.all().order_by("created_at"): text = note.note or "" text_lower = text.lower() if not action_taken and ("action" in text_lower or "taken" in text_lower or "تواصل" in text or "تنبيه" in text or "توجيه" in text): action_taken = text if "resolved" in text_lower or "done" in text_lower or "تم حل" in text or "تمت" in text: resolved = "Yes" if "suggestion" in text_lower or "solution" in text_lower or "حلول" in text or "اقتراح" in text: solutions = text if not resolved and obs.status in ("resolved", "closed"): resolved = "Yes" return action_taken, resolved, solutions def _write_data_sheet(ws, queryset): _apply_header_style(ws) ordered_qs = queryset.order_by("created_at") row_num = 3 note_num = 1 for obs in ordered_qs: _write_obs_row(ws, row_num, obs, note_num) row_num += 1 note_num += 1 col_widths = { 1: 16, 2: 8, 3: 14, 4: 10, 5: 14, 6: 14, 7: 14, 8: 18, 9: 18, 10: 18, 11: 20, 12: 40, 13: 40, 14: 18, 15: 18, 16: 14, 17: 14, 18: 10, 19: 35, 20: 14, 21: 35, } for col, w in col_widths.items(): ws.column_dimensions[get_column_letter(col)].width = w ws.freeze_panes = "C3" def _write_obs_row(ws, row, obs, note_num): action_taken, resolved, solutions = _parse_notes(obs) obs_source = SOURCE_MAP.get(obs.source, obs.source or "") values = [ note_num, obs.incident_datetime.date() if obs.incident_datetime else "", obs.incident_datetime.strftime("%H:%M") if obs.incident_datetime else "", obs.reporter_phone or "", obs.patient_file_number or "", obs.reporter_staff_id or "", obs_source, obs.category.name_en if obs.category else "", obs.sub_category.name_en if obs.sub_category else "", obs.title or "", obs.description or "", obs.description_en or "", obs.person_noted or "", obs.department_noted.name if obs.department_noted else "", obs.communication_method or "", obs.communication_datetime.date() if obs.communication_datetime else "", obs.communication_datetime.strftime("%H:%M") if obs.communication_datetime else "", action_taken, resolved, solutions, ] for col, val in enumerate(values, 2): cell = ws.cell(row=row, column=col, value=val) cell.font = DATA_FONT cell.alignment = RTL_ALIGN if col in (12, 19, 21) else DATA_ALIGN cell.border = THIN_BORDER def _write_calculations_sheet(wb, employee_breakdown): ws = wb.create_sheet("Calculations") ws.cell(row=2, column=2, value="Total Number of Notes").font = Font(name="Calibri", size=11, bold=True) ws.cell(row=2, column=6, value="Total Number of Notes").font = Font(name="Calibri", size=11, bold=True) ws.cell(row=3, column=2, value="Total Number of Notes").font = DATA_FONT ws.cell(row=4, column=2, value="1st Half of the month, From 1 to 15").font = DATA_FONT ws.cell(row=5, column=2, value="2nd Half of the month, From 16 to 31").font = DATA_FONT ws.cell(row=2, column=9, value="Employees").font = Font(name="Calibri", size=11, bold=True) ws.cell(row=2, column=10, value="Total Number").font = Font(name="Calibri", size=11, bold=True) ws.cell(row=2, column=11, value="Percentage").font = Font(name="Calibri", size=11, bold=True) total = sum(e["total"] for e in employee_breakdown) ws.cell(row=3, column=3, value=total) ws.cell(row=4, column=3, value=sum(1 for e in employee_breakdown for _ in range(e["total"]))) ws.cell(row=5, column=3, value=0) ws.cell(row=4, column=4, value="=C4/C3" if total > 0 else 0) ws.cell(row=5, column=4, value="=C5/C3" if total > 0 else 0) ws.cell(row=3, column=7, value="=G3+G4+G5+G6+G7+G8") for i in range(3, 9): ws.cell(row=i, column=6, value=f"Week {i - 3}") ws.cell(row=9, column=6, value="Total") ws.cell(row=9, column=7, value="=G3+G4+G5+G6+G7+G8") for i, emp in enumerate(employee_breakdown): r = 3 + i ws.cell(row=r, column=9, value=emp["name"]).font = DATA_FONT ws.cell(row=r, column=10, value=emp["total"]).font = DATA_FONT pct_cell = ws.cell(row=r, column=11, value=emp["total"] / total if total > 0 else 0) pct_cell.font = DATA_FONT pct_cell.number_format = "0.0%" total_row = 3 + len(employee_breakdown) ws.cell(row=total_row, column=9, value="Total").font = Font(name="Calibri", size=11, bold=True) ws.cell(row=total_row, column=10, value=total).font = Font(name="Calibri", size=11, bold=True) ws.cell(row=total_row, column=11, value=1).font = Font(name="Calibri", size=11, bold=True) ws.cell(row=total_row, column=11).number_format = "0.0%" for col in range(2, 12): ws.column_dimensions[get_column_letter(col)].width = 20 def generate_observation_excel(service): queryset = service._base_qs().prefetch_related("notes") employee_breakdown = service.get_employee_breakdown() date_label = f"{service.date_from.isoformat()} to {service.date_to.isoformat()}" wb = Workbook() ws = wb.active ws.title = "Observations" _write_data_sheet(ws, queryset) _write_calculations_sheet(wb, employee_breakdown) buf = BytesIO() wb.save(buf) buf.seek(0) return buf