This commit is contained in:
2026-04-08 16:29:35 +02:00
parent 855cef678f
commit 1b00c10f42
8 changed files with 547 additions and 72 deletions
Binary file not shown.
Binary file not shown.
+133 -48
View File
@@ -1,4 +1,5 @@
import pandas as pd import pandas as pd
from datetime import date
from pathlib import Path from pathlib import Path
from openpyxl import load_workbook from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
@@ -6,8 +7,10 @@ from openpyxl.utils import get_column_letter
INVENTORY_DIR = Path("xls_reports") INVENTORY_DIR = Path("xls_reports")
DESTRUCTION_DIR = Path("xls_ip_destruction") DESTRUCTION_DIR = Path("xls_ip_destruction")
OUTPUT_FILE = "accountability_combined.xlsx" OUTPUT_DIR = Path("output")
SHEET_NAME = "CountryMedicationOverview" OUTPUT_FILE = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} 42847922MDD3003 CZ IWRS overview.xlsx"
# ── Shared constants ──────────────────────────────────────────────────────────
COLUMN_RENAMES = { COLUMN_RENAMES = {
"Site": "Site", "Site": "Site",
@@ -35,7 +38,7 @@ COLUMN_RENAMES = {
DATE_COLUMNS = { DATE_COLUMNS = {
"Orig Exp Date", "Exp Date", "Rcv Date", "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 = { COLUMN_WIDTHS = {
@@ -60,8 +63,10 @@ COLUMN_WIDTHS = {
"Ret User": 18, "Ret User": 18,
"Destroyed": 14, "Destroyed": 14,
"Basket No.": 12, "Basket No.": 12,
"Max Visit Date": 16,
} }
# ── Helpers ───────────────────────────────────────────────────────────────────
def read_inventory(path): def read_inventory(path):
df = pd.read_excel(path, header=None) df = pd.read_excel(path, header=None)
@@ -94,52 +99,15 @@ def read_destruction_lookup():
return lookup return lookup
def main(): def format_sheet(ws, header_color, highlight_col=None, highlight_color=None):
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")
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)
thin = Side(style="thin", color="000000") thin = Side(style="thin", color="000000")
border = Border(left=thin, right=thin, top=thin, bottom=thin) 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)
row_font = Font(name="Arial", size=10)
hi_fill = PatternFill("solid", start_color=highlight_color) if highlight_color else None
headers = [cell.value for cell in ws[1]] headers = [cell.value for cell in ws[1]]
new_cols = {"Destroyed", "Basket No."}
for cell in ws[1]: for cell in ws[1]:
cell.fill = header_fill cell.fill = header_fill
@@ -155,8 +123,8 @@ def main():
cell.alignment = Alignment(horizontal="center") cell.alignment = Alignment(horizontal="center")
if col_name in DATE_COLUMNS: if col_name in DATE_COLUMNS:
cell.number_format = "DD-MMM-YYYY" cell.number_format = "DD-MMM-YYYY"
if col_name in new_cols: if hi_fill and col_name == highlight_col:
cell.fill = new_col_fill cell.fill = hi_fill
for cell in ws[1]: for cell in ws[1]:
width = COLUMN_WIDTHS.get(cell.value, 14) width = COLUMN_WIDTHS.get(cell.value, 14)
@@ -165,8 +133,125 @@ def main():
ws.auto_filter.ref = ws.dimensions ws.auto_filter.ref = ws.dimensions
ws.freeze_panes = "A2" 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) 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() main()
+92
View File
@@ -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}')")
+97
View File
@@ -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}')")
+99
View File
@@ -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}')")
+102
View File
@@ -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}')")