import calendar from datetime import date from io import BytesIO from openpyxl import Workbook from openpyxl.chart import BarChart, Reference from openpyxl.chart.series import SeriesLabel from openpyxl.styles import Alignment, Border, Font, PatternFill, Side from openpyxl.utils import get_column_letter from .census import AREA_ORDER, CensusService BLUE_FILL = PatternFill("solid", fgColor="FF2F75B5") GREEN_FILL = PatternFill("solid", fgColor="FF00B050") RED_FILL = PatternFill("solid", fgColor="FFFF0000") LIGHT_FILL = PatternFill("solid", fgColor="FFD6E4F0") HEADER_FONT = Font(name="Calibri", size=12, bold=True, color="FF000000") HEADER_WHITE_FONT = Font(name="Calibri", size=12, bold=True, color="FFFFFFFF") DATA_FONT = Font(name="Calibri", size=11) TOTAL_FONT = Font(name="Calibri", size=12, bold=False, color="FFFFFFFF") TITLE_FONT = Font(name="Calibri", size=72) SUBTITLE_FONT = Font(name="Calibri", size=12, bold=True, color="FFFFFFFF") COMP_TITLE_FONT = Font(name="Calibri", size=26, bold=True) NUM_FMT = "#,##0" DATE_FMT = "d-mmm" THIN = Side(style="thin") MEDIUM = Side(style="medium") BORDER_ALL = Border(left=THIN, right=THIN, top=THIN, bottom=THIN) BORDER_LEFT_MED = Border(left=MEDIUM, right=THIN, top=THIN, bottom=THIN) BORDER_RIGHT_MED = Border(left=THIN, right=MEDIUM, top=THIN, bottom=THIN) BORDER_BOTH_MED = Border(left=MEDIUM, right=MEDIUM, top=THIN, bottom=THIN) BORDER_BOTTOM_MED = Border(left=THIN, right=THIN, top=THIN, bottom=MEDIUM) CENTER = Alignment(horizontal="center", vertical="center") def _write_year_block(ws, start_col, year, quarterly_data, year_totals): first_data_row = 7 months_per_q = {1: [1, 2, 3], 2: [4, 5, 6], 3: [7, 8, 9], 4: [10, 11, 12]} title_cell = ws.cell(row=1, column=start_col, value=year) title_cell.font = TITLE_FONT title_cell.alignment = CENTER title_cell.border = Border(left=MEDIUM, right=MEDIUM, top=MEDIUM, bottom=MEDIUM) ws.merge_cells( start_row=1, start_column=start_col, end_row=4, end_column=start_col + 4 ) sub_row = 5 sub_cell = ws.cell( row=sub_row, column=start_col, value=f"{year} - Auto Calculated" ) sub_cell.font = SUBTITLE_FONT sub_cell.fill = PatternFill("solid", fgColor="FF44546A") sub_cell.alignment = CENTER sub_cell.border = Border(left=MEDIUM, right=THIN, top=MEDIUM, bottom=THIN) ws.merge_cells( start_row=sub_row, start_column=start_col, end_row=sub_row, end_column=start_col + 4 ) total_col = start_col + 5 for r in [5, 6]: tc = ws.cell(row=r, column=total_col, value="Total" if r == 5 else None) tc.font = TOTAL_FONT tc.fill = RED_FILL tc.alignment = CENTER tc.border = Border(left=MEDIUM, right=MEDIUM, top=MEDIUM, bottom=THIN) ws.merge_cells(start_row=5, start_column=total_col, end_row=6, end_column=total_col) headers = ["Area/Unit"] + [f"Q{i}.{year}" for i in range(1, 5)] for i, h in enumerate(headers): c = ws.cell(row=6, column=start_col + i, value=h) c.font = HEADER_WHITE_FONT c.fill = BLUE_FILL c.alignment = CENTER c.border = BORDER_LEFT_MED if i == 0 else (BORDER_RIGHT_MED if i == 4 else BORDER_ALL) q_totals_by_area = {area: [0, 0, 0, 0] for area in AREA_ORDER} for qi in range(4): q_num = qi + 1 q_data = quarterly_data.get(q_num, {"totals": {a: 0 for a in AREA_ORDER}}) for area in AREA_ORDER: q_totals_by_area[area][qi] = q_data["totals"].get(area, 0) for ai, area in enumerate(AREA_ORDER): row = first_data_row + ai label = ws.cell(row=row, column=start_col, value=area) label.font = DATA_FONT label.alignment = CENTER label.border = BORDER_LEFT_MED for qi in range(4): val = q_totals_by_area[area][qi] c = ws.cell(row=row, column=start_col + 1 + qi, value=val) c.font = DATA_FONT c.number_format = NUM_FMT c.alignment = CENTER c.border = BORDER_RIGHT_MED if qi == 3 else BORDER_ALL col_letter = get_column_letter(start_col + 4) total_cell = ws.cell(row=row, column=total_col) parts = [f"{get_column_letter(start_col + 1 + qi)}{row}" for qi in range(4)] total_cell.value = "=" + "+".join(parts) total_cell.font = DATA_FONT total_cell.fill = LIGHT_FILL total_cell.number_format = NUM_FMT total_cell.alignment = CENTER total_cell.border = BORDER_BOTH_MED detail_start_row = 11 for qi in range(4): q_num = qi + 1 row_offset = detail_start_row + qi * 4 q_months = months_per_q[q_num] q_data = quarterly_data.get(q_num, {"months": {}}) ws.cell(row=row_offset, column=start_col, value="Area/Unit").font = HEADER_FONT ws.cell(row=row_offset, column=start_col).fill = BLUE_FILL ws.cell(row=row_offset, column=start_col).alignment = CENTER ws.cell(row=row_offset, column=start_col).border = BORDER_LEFT_MED for mi, m in enumerate(q_months): dt_str = f"{calendar.month_abbr[m]}" c = ws.cell(row=row_offset, column=start_col + 1 + mi, value=dt_str) c.font = HEADER_FONT c.fill = BLUE_FILL c.alignment = CENTER c.number_format = DATE_FMT c.border = BORDER_ALL tc = ws.cell(row=row_offset, column=start_col + 4, value="Total") tc.font = HEADER_FONT tc.fill = BLUE_FILL tc.alignment = CENTER tc.border = BORDER_RIGHT_MED for ai, area in enumerate(AREA_ORDER): dr = row_offset + 1 + ai lc = ws.cell(row=dr, column=start_col, value=area) lc.font = DATA_FONT lc.alignment = CENTER lc.border = BORDER_LEFT_MED for mi, m in enumerate(q_months): m_data = q_data.get("months", {}).get(m, {"OPD": 0, "ER": 0, "IP": 0}) val = m_data.get(area, 0) c = ws.cell(row=dr, column=start_col + 1 + mi, value=val) c.font = DATA_FONT c.number_format = NUM_FMT c.alignment = CENTER c.border = BORDER_ALL col_letters = [get_column_letter(start_col + 1 + mi) for mi in range(3)] formula = f"={col_letters[0]}{dr}+{col_letters[1]}{dr}+{col_letters[2]}{dr}" t_cell = ws.cell(row=dr, column=start_col + 4, value=formula) t_cell.font = DATA_FONT t_cell.fill = LIGHT_FILL if year != date.today().year else GREEN_FILL t_cell.number_format = NUM_FMT t_cell.alignment = CENTER t_cell.border = BORDER_BOTH_MED if ai == 2: for ci in range(start_col, start_col + 6): cell = ws.cell(row=dr, column=ci) cell.border = Border( left=MEDIUM if ci == start_col else THIN, right=MEDIUM if ci in (start_col + 4, total_col) else THIN, top=THIN, bottom=MEDIUM, ) return total_col def _write_comparison_table(ws, start_row, start_col, area_label, comparison_data, years, total_col_ref=None): label_font = Font(name="Calibri", size=12, bold=True) header_row = start_row headers = [area_label, "Q1", "Q2", "Q3", "Q4", "Total"] for i, h in enumerate(headers): c = ws.cell(row=header_row, column=start_col + i, value=h) c.font = HEADER_WHITE_FONT if i < 5 else Font(name="Calibri", size=12, bold=True, color="FFFFFFFF") c.fill = BLUE_FILL if i < 5 else GREEN_FILL c.alignment = CENTER c.border = BORDER_LEFT_MED if i == 0 else (BORDER_RIGHT_MED if i == 5 else BORDER_ALL) for yi, year in enumerate(years): r = header_row + 1 + yi ws.cell(row=r, column=start_col, value=year).font = label_font ws.cell(row=r, column=start_col).alignment = CENTER ws.cell(row=r, column=start_col).border = BORDER_LEFT_MED ws.cell(row=r, column=start_col).fill = PatternFill("solid", fgColor="FFFFFFFF") q_data = comparison_data.get(year, {}).get("quarterly", {}) for qi in range(4): q_num = qi + 1 val = q_data.get(q_num, {}).get("totals", {}).get(area_label, 0) c = ws.cell(row=r, column=start_col + 1 + qi, value=val) c.font = DATA_FONT c.number_format = NUM_FMT c.alignment = CENTER c.fill = LIGHT_FILL c.border = BORDER_RIGHT_MED if qi == 3 else BORDER_ALL cl = get_column_letter(start_col + 1) cl2 = get_column_letter(start_col + 4) formula = f"=SUM({cl}{r}:{cl2}{r})" tc = ws.cell(row=r, column=start_col + 5, value=formula) tc.font = DATA_FONT tc.number_format = NUM_FMT tc.alignment = CENTER tc.fill = LIGHT_FILL tc.border = BORDER_BOTH_MED return header_row + 1, header_row + len(years) def _write_comparison_charts(ws, start_row, start_col, area_label, comparison_data, years, chart_width=8, chart_height=14): data_start_row = start_row + 1 data_end_row = data_start_row + len(years) - 1 chart = BarChart() chart.type = "col" chart.title = f"{area_label} Total Visits" chart.style = 10 chart.width = chart_width chart.height = chart_height data_ref = Reference(ws, min_col=start_col + 1, min_row=data_start_row, max_col=start_col + 4, max_row=data_end_row) cats_ref = Reference(ws, min_col=start_col, min_row=data_start_row, max_row=data_end_row) for i, year in enumerate(years): series_data = Reference( ws, min_col=start_col + 1 + i, min_row=data_start_row, max_row=data_start_row, ) chart.add_data(data_ref, from_rows=True, titles_from_data=False) chart.set_categories(cats_ref) for i, year in enumerate(years): chart.series[i].tx = SeriesLabel(v=str(year)) chart_row = start_row + 2 chart_col = start_col + 6 ws.add_chart(chart, f"{get_column_letter(chart_col)}{chart_row}") return chart def generate_census_excel(census_service, years=None): if years is None: years = sorted( set( [ census_service.year - 2, census_service.year - 1, census_service.year, ] ) ) comparison = census_service.get_comparison_data(years) wb = Workbook() ws = wb.active ws.title = "Census" year_blocks = {} for i, year in enumerate(years): col = 2 + i * 7 q_data = comparison[year]["quarterly"] y_totals = comparison[year]["yearly"] _write_year_block(ws, col, year, q_data, y_totals) year_blocks[year] = col ws.merge_cells("A29:O29") title_cell = ws.cell( row=29, column=1, value=f"Comparison of Total Outpatient/Inpatient/ER Visits for {', '.join(str(y) for y in years)}", ) title_cell.font = COMP_TITLE_FONT title_cell.alignment = CENTER title_cell.border = Border(left=MEDIUM, right=MEDIUM, top=MEDIUM, bottom=MEDIUM) ws.row_dimensions[29].height = 34.5 area_labels = [("OPD", 31), ("IP", 49), ("ER", 68)] for area_label, table_row in area_labels: data_start, data_end = _write_comparison_table( ws, table_row, 1, area_label, comparison, years ) _write_comparison_charts(ws, table_row, 1, area_label, comparison, years) summary_row = table_row + len(years) + 3 label_cell = ws.cell(row=summary_row, column=5, value=area_label) label_cell.font = Font(name="Calibri", size=12, bold=True, color="FFFFFFFF") label_cell.fill = GREEN_FILL label_cell.alignment = CENTER label_cell.border = BORDER_BOTH_MED total_header = ws.cell(row=summary_row, column=6, value="Total") total_header.font = Font(name="Calibri", size=12, bold=True, color="FFFFFFFF") total_header.fill = GREEN_FILL total_header.alignment = CENTER total_header.border = BORDER_BOTH_MED for yi, year in enumerate(years): r = summary_row + 1 + yi yr_cell = ws.cell(row=r, column=5, value=year) yr_cell.font = DATA_FONT yr_cell.alignment = CENTER yr_cell.border = BORDER_LEFT_MED total_row = table_row + 1 + yi ref = f"F{total_row}" tv = ws.cell(row=r, column=6, value=f"={ref}") tv.font = DATA_FONT tv.number_format = NUM_FMT tv.alignment = CENTER tv.fill = LIGHT_FILL tv.border = BORDER_BOTH_MED ws.column_dimensions["A"].width = 17.29 for col_idx in range(2, 50): letter = get_column_letter(col_idx) if letter in ("G", "N", "U"): ws.column_dimensions[letter].width = 9.29 elif letter in ("H", "O"): ws.column_dimensions[letter].width = 14.0 elif col_idx <= 22: ws.column_dimensions[letter].width = 12.29 buf = BytesIO() wb.save(buf) buf.seek(0) return buf