diff --git a/accountability_combined.xlsx b/accountability_combined.xlsx deleted file mode 100644 index 44715ca..0000000 Binary files a/accountability_combined.xlsx and /dev/null differ diff --git a/accountability_formatted.xlsx b/accountability_formatted.xlsx deleted file mode 100644 index f5d608a..0000000 Binary files a/accountability_formatted.xlsx and /dev/null differ diff --git a/create_accountability_report.py b/create_accountability_report.py index f11b2e3..8638cbc 100644 --- a/create_accountability_report.py +++ b/create_accountability_report.py @@ -1,13 +1,16 @@ import pandas as pd +from datetime import date from pathlib import Path from openpyxl import load_workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter -INVENTORY_DIR = Path("xls_reports") +INVENTORY_DIR = Path("xls_reports") DESTRUCTION_DIR = Path("xls_ip_destruction") -OUTPUT_FILE = "accountability_combined.xlsx" -SHEET_NAME = "CountryMedicationOverview" +OUTPUT_DIR = Path("output") +OUTPUT_FILE = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} 42847922MDD3003 CZ IWRS overview.xlsx" + +# ── Shared constants ────────────────────────────────────────────────────────── COLUMN_RENAMES = { "Site": "Site", @@ -35,33 +38,35 @@ COLUMN_RENAMES = { DATE_COLUMNS = { "Orig Exp Date", "Exp Date", "Rcv Date", - "Date Asgn", "Disp Date", "Date Ret", "Destroyed", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", "Max Visit Date", } COLUMN_WIDTHS = { - "Site": 14, - "Med ID": 10, - "Lot No.": 12, - "Orig Exp Date": 16, - "Exp Date": 14, - "Rcv Date": 14, - "Rcpt User": 22, - "Subject ID": 14, - "Qty Asgn": 9, - "IRT Tx": 8, - "Date Asgn": 14, - "Asgn User": 20, - "Disp Status": 16, - "Disp Date": 14, - "Qty Disp": 9, - "Disp User": 20, - "Qty Ret": 10, - "Date Ret": 14, - "Ret User": 18, - "Destroyed": 14, - "Basket No.": 12, + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, + "Max Visit Date": 16, } +# ── Helpers ─────────────────────────────────────────────────────────────────── def read_inventory(path): df = pd.read_excel(path, header=None) @@ -94,52 +99,15 @@ def read_destruction_lookup(): return lookup -def main(): - lookup = read_destruction_lookup() - print(f"Loaded {len(lookup)} kits from destruction reports") - - all_rows = [] - for path in sorted(INVENTORY_DIR.glob("onsite_inventory_detail_*.xlsx")): - df, meta = read_inventory(path) - df["DestroyedOn"] = df["Medication ID"].apply( - lambda x: lookup.get(int(x), (None, None))[1] if pd.notna(x) else None - ) - df["Basket number"] = df["Medication ID"].apply( - lambda x: lookup.get(int(x), (None, None))[0] if pd.notna(x) else None - ) - df.insert(0, "Site", meta.get("site", path.stem)) - all_rows.append(df) - print(f" {path.name}: {len(df)} kits") - - combined = pd.concat(all_rows, ignore_index=True) - - # Rename columns - combined.rename(columns=COLUMN_RENAMES, inplace=True) - - # Convert date columns - for col in DATE_COLUMNS: - if col in combined.columns: - combined[col] = pd.to_datetime(combined[col], dayfirst=True, errors="coerce") - - # Sort - combined.sort_values(["Site", "Rcv Date", "Med ID"], inplace=True, ignore_index=True) - - combined.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) - - # ── Formatting ──────────────────────────────────────────────────────────── - wb = load_workbook(OUTPUT_FILE) - ws = wb[SHEET_NAME] - - header_fill = PatternFill("solid", start_color="1F4E79") +def format_sheet(ws, header_color, highlight_col=None, highlight_color=None): + thin = Side(style="thin", color="000000") + border = Border(left=thin, right=thin, top=thin, bottom=thin) + header_fill = PatternFill("solid", start_color=header_color) header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) - new_col_fill = PatternFill("solid", start_color="E2EFDA") - row_font = Font(name="Arial", size=10) + row_font = Font(name="Arial", size=10) + hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None - thin = Side(style="thin", color="000000") - border = Border(left=thin, right=thin, top=thin, bottom=thin) - - headers = [cell.value for cell in ws[1]] - new_cols = {"Destroyed", "Basket No."} + headers = [cell.value for cell in ws[1]] for cell in ws[1]: cell.fill = header_fill @@ -155,8 +123,8 @@ def main(): cell.alignment = Alignment(horizontal="center") if col_name in DATE_COLUMNS: cell.number_format = "DD-MMM-YYYY" - if col_name in new_cols: - cell.fill = new_col_fill + if hi_fill and col_name == highlight_col: + cell.fill = hi_fill for cell in ws[1]: width = COLUMN_WIDTHS.get(cell.value, 14) @@ -165,8 +133,125 @@ def main(): ws.auto_filter.ref = ws.dimensions ws.freeze_panes = "A2" + +# ── Build DataFrames ────────────────────────────────────────────────────────── + +def build_main(lookup): + all_rows = [] + for path in sorted(INVENTORY_DIR.glob("onsite_inventory_detail_*.xlsx")): + df, meta = read_inventory(path) + df["DestroyedOn"] = df["Medication ID"].apply( + lambda x: lookup.get(int(x), (None, None))[1] if pd.notna(x) else None) + df["Basket number"] = df["Medication ID"].apply( + lambda x: lookup.get(int(x), (None, None))[0] if pd.notna(x) else None) + df.insert(0, "Site", meta.get("site", path.stem)) + all_rows.append(df) + print(f" {path.name}: {len(df)} kits") + + combined = pd.concat(all_rows, ignore_index=True) + combined.rename(columns=COLUMN_RENAMES, inplace=True) + for col in DATE_COLUMNS: + if col in combined.columns: + combined[col] = pd.to_datetime(combined[col], dayfirst=True, errors="coerce") + combined.sort_values(["Site", "Rcv Date", "Med ID"], inplace=True, ignore_index=True) + return combined + + +def build_expired(df): + today = date.today() + mask = ( + df["Basket No."].isna() & + df["Subject ID"].isna() & + (df["Exp Date"] < pd.Timestamp(today)) + ) + filtered = df[mask].copy().reset_index(drop=True) + sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + print(f" Expired: {len(filtered)}") + return filtered, sheet_name + + +def build_assigned_not_dispensed(df): + mask = df["Subject ID"].notna() & df["Disp Date"].isna() + filtered = df[mask].copy().reset_index(drop=True) + print(f" Assigned not dispensed: {len(filtered)}") + return filtered + + +def build_not_returned(df): + no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].str.upper() != "NOT DISPENSED") + ].copy() + max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") + no_ret = no_ret.join(max_asgn, on="Subject ID") + filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) + filtered = filtered.reset_index(drop=True) + print(f" Not returned: {len(filtered)}") + return filtered + + +def build_kits_for_destruction(df): + mask = ( + df["Basket No."].isna() & + (df["Date Ret"].notna() | (df["Disp Status"].str.upper() == "NOT DISPENSED")) + ) + filtered = df[mask].copy().sort_values(["Site", "Date Ret"], ascending=[True, True]) + filtered = filtered.drop(columns=["Destroyed", "Basket No."]).reset_index(drop=True) + print(f" Kits for destruction: {len(filtered)}") + return filtered + + +# ── Main ────────────────────────────────────────────────────────────────────── + +def main(): + # Prepare output dir, remove any previous overview file + OUTPUT_DIR.mkdir(exist_ok=True) + for old in OUTPUT_DIR.glob("*42847922MDD3003 CZ IWRS overview.xlsx"): + old.unlink() + print(f"Removed old file: {old.name}") + + lookup = read_destruction_lookup() + print(f"Loaded {len(lookup)} kits from destruction reports") + + df = build_main(lookup) + + expired_df, expired_sheet = build_expired(df) + assigned_df = build_assigned_not_dispensed(df) + not_returned_df = build_not_returned(df) + destruction_df = build_kits_for_destruction(df) + + # Write all sheets + with pd.ExcelWriter(OUTPUT_FILE, engine="openpyxl") as writer: + df.to_excel( writer, index=False, sheet_name="CountryMedicationOverview") + expired_df.to_excel( writer, index=False, sheet_name=expired_sheet) + assigned_df.to_excel( writer, index=False, sheet_name="Assigned not dispensed") + not_returned_df.to_excel(writer, index=False, sheet_name="Not returned") + destruction_df.to_excel( writer, index=False, sheet_name="Kits for destruction") + + # Format all sheets + wb = load_workbook(OUTPUT_FILE) + + # Main sheet — dark blue, green highlight for Destroyed/Basket No. + ws_main = wb["CountryMedicationOverview"] + format_sheet(ws_main, header_color="1F4E79") + # Extra: green fill for Destroyed and Basket No. columns + new_col_fill = PatternFill("solid", start_color="E2EFDA") + headers_main = [c.value for c in ws_main[1]] + for row in ws_main.iter_rows(min_row=2, max_row=ws_main.max_row): + for cell in row: + col_name = headers_main[cell.column - 1] if cell.column <= len(headers_main) else None + if col_name in ("Destroyed", "Basket No."): + cell.fill = new_col_fill + + format_sheet(wb[expired_sheet], header_color="C00000", highlight_col="Exp Date", highlight_color="FFE0E0") + format_sheet(wb["Assigned not dispensed"], header_color="833C00", highlight_col="Subject ID", highlight_color="FFF2CC") + format_sheet(wb["Not returned"], header_color="375623", highlight_col="Max Visit Date", highlight_color="E2EFDA") + format_sheet(wb["Kits for destruction"], header_color="595959") + wb.save(OUTPUT_FILE) - print(f"\nSaved: {OUTPUT_FILE} ({len(combined)} rows, sheet '{SHEET_NAME}')") + print(f"\nSaved: {OUTPUT_FILE} ({len(df)} rows on main sheet, {wb.sheetnames})") main() diff --git a/output/2026-04-08 42847922MDD3003 CZ IWRS overview.xlsx b/output/2026-04-08 42847922MDD3003 CZ IWRS overview.xlsx new file mode 100644 index 0000000..4cf16ef Binary files /dev/null and b/output/2026-04-08 42847922MDD3003 CZ IWRS overview.xlsx differ diff --git a/sheet_assigned_not_dispensed.py b/sheet_assigned_not_dispensed.py new file mode 100644 index 0000000..3b97d1a --- /dev/null +++ b/sheet_assigned_not_dispensed.py @@ -0,0 +1,92 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_assigned_not_dispensed.xlsx" +SHEET_NAME = "Assigned not dispensed" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: Subject ID present AND Disp Date missing +mask = df["Subject ID"].notna() & df["Disp Date"].isna() +filtered = df[mask].copy().reset_index(drop=True) + +print(f"Assigned not dispensed: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="833C00") # dark orange +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +subj_fill = PatternFill("solid", start_color="FFF2CC") # light yellow highlight for Subject ID + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Subject ID": + cell.fill = subj_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')") diff --git a/sheet_expired.py b/sheet_expired.py new file mode 100644 index 0000000..b41d353 --- /dev/null +++ b/sheet_expired.py @@ -0,0 +1,97 @@ +import pandas as pd +from datetime import date +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_expired.xlsx" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +today = date.today() +sheet_name = f"Expired as of {today.strftime('%d-%b-%Y')}" + +# Load source +df = pd.read_excel(SOURCE_FILE) + +# Convert date columns +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: not in basket AND not assigned to patient AND Exp Date < today +mask = df["Basket No."].isna() & df["Subject ID"].isna() & (df["Exp Date"] < pd.Timestamp(today)) +filtered = df[mask].copy().reset_index(drop=True) + +print(f"Expired kits not in basket: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=sheet_name) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[sheet_name] + +header_fill = PatternFill("solid", start_color="C00000") # dark red +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +exp_fill = PatternFill("solid", start_color="FFE0E0") # light red highlight for Exp Date + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Exp Date": + cell.fill = exp_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{sheet_name}')") diff --git a/sheet_kits_for_destruction.py b/sheet_kits_for_destruction.py new file mode 100644 index 0000000..a6b6dd8 --- /dev/null +++ b/sheet_kits_for_destruction.py @@ -0,0 +1,99 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_kits_for_destruction.xlsx" +SHEET_NAME = "Kits for destruction" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Date Ret", "Destroyed", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Qty Ret": 10, + "Date Ret": 14, + "Ret User": 18, + "Destroyed": 14, + "Basket No.": 12, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Filter: no basket AND (Date Ret filled OR Disp Status == NOT DISPENSED) +mask = ( + df["Basket No."].isna() & + ( + df["Date Ret"].notna() | + (df["Disp Status"].str.upper() == "NOT DISPENSED") + ) +) +filtered = df[mask].copy().sort_values(["Site", "Date Ret"], ascending=[True, True]) +filtered = filtered.drop(columns=["Destroyed", "Basket No."]).reset_index(drop=True) + +print(f"Kits for destruction: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="595959") # dark grey +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +basket_fill = PatternFill("solid", start_color="FFE0E0") # light red for empty Basket No. + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Basket No.": + cell.fill = basket_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')") diff --git a/sheet_not_returned.py b/sheet_not_returned.py new file mode 100644 index 0000000..79f68e2 --- /dev/null +++ b/sheet_not_returned.py @@ -0,0 +1,102 @@ +import pandas as pd +from openpyxl import load_workbook +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter + +SOURCE_FILE = "accountability_combined.xlsx" +OUTPUT_FILE = "sheet_not_returned.xlsx" +SHEET_NAME = "Not returned" + +DATE_COLUMNS = { + "Orig Exp Date", "Exp Date", "Rcv Date", + "Date Asgn", "Disp Date", "Max Visit Date", +} + +COLUMN_WIDTHS = { + "Site": 14, + "Med ID": 10, + "Lot No.": 12, + "Orig Exp Date": 16, + "Exp Date": 14, + "Rcv Date": 14, + "Rcpt User": 22, + "Subject ID": 14, + "Qty Asgn": 9, + "IRT Tx": 8, + "Date Asgn": 14, + "Asgn User": 20, + "Disp Status": 16, + "Disp Date": 14, + "Qty Disp": 9, + "Disp User": 20, + "Max Visit Date": 16, +} + +df = pd.read_excel(SOURCE_FILE) + +for col in DATE_COLUMNS: + if col in df.columns: + df[col] = pd.to_datetime(df[col], errors="coerce") + +# Kits with no return date, assigned to a patient, and not "NOT DISPENSED" +no_ret = df[ + df["Date Ret"].isna() & + df["Subject ID"].notna() & + (df["Disp Status"].str.upper() != "NOT DISPENSED") +].copy() + +# Max Date Asgn per patient (from full dataset) +max_asgn = df.groupby("Subject ID")["Date Asgn"].max().rename("Max Visit Date") +no_ret = no_ret.join(max_asgn, on="Subject ID") + +# Keep only kits where Date Asgn is NOT the latest for that patient +filtered = no_ret[no_ret["Date Asgn"] < no_ret["Max Visit Date"]].copy() + +# Drop columns Q-U and keep Max Visit Date +filtered = filtered.drop(columns=["Qty Ret", "Date Ret", "Ret User", "Destroyed", "Basket No."]) +filtered = filtered.reset_index(drop=True) + +print(f"Not returned kits: {len(filtered)}") + +filtered.to_excel(OUTPUT_FILE, index=False, sheet_name=SHEET_NAME) + +# Formatting +wb = load_workbook(OUTPUT_FILE) +ws = wb[SHEET_NAME] + +header_fill = PatternFill("solid", start_color="375623") # dark green +header_font = Font(bold=True, color="FFFFFF", name="Arial", size=10) +row_font = Font(name="Arial", size=10) +ret_fill = PatternFill("solid", start_color="E2EFDA") # light green highlight for Date Ret + +thin = Side(style="thin", color="000000") +border = Border(left=thin, right=thin, top=thin, bottom=thin) + +headers = [cell.value for cell in ws[1]] + +for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False) + cell.border = border + +for row in ws.iter_rows(min_row=2, max_row=ws.max_row): + for cell in row: + col_name = headers[cell.column - 1] if cell.column <= len(headers) else None + cell.font = row_font + cell.border = border + cell.alignment = Alignment(horizontal="center") + if col_name in DATE_COLUMNS: + cell.number_format = "DD-MMM-YYYY" + if col_name == "Max Visit Date": + cell.fill = ret_fill + +for cell in ws[1]: + width = COLUMN_WIDTHS.get(cell.value, 14) + ws.column_dimensions[get_column_letter(cell.column)].width = width + +ws.auto_filter.ref = ws.dimensions +ws.freeze_panes = "A2" + +wb.save(OUTPUT_FILE) +print(f"Saved: {OUTPUT_FILE} (sheet: '{SHEET_NAME}')")