337 lines
13 KiB
Python
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
|