HH/apps/dashboard/services/census_export.py
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

337 lines
13 KiB
Python

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