z230
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
import pandas as pd
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side, numbers
|
||||
from openpyxl.utils import get_column_letter
|
||||
from datetime import date
|
||||
import os
|
||||
|
||||
CSV_FILE = "filename.csv"
|
||||
SVR_FILE = "Site Visit Report (2).xlsx"
|
||||
OUTPUT_DIR = os.path.join("..", "..", "CTMS", "output")
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
today_str = date.today().strftime("%Y-%m-%d")
|
||||
OUTPUT_FILE = os.path.join(OUTPUT_DIR, f"{today_str} UCO3001 CZ CTMS Visits.xlsx")
|
||||
|
||||
# --- Load & filter ---
|
||||
df = pd.read_csv(CSV_FILE, sep=";", encoding="utf-8-sig")
|
||||
df["Country"] = df["Study Site Number"].str.extract(r"DD5-([A-Z]+)\d+")
|
||||
cz = df[df["Country"] == "CZ"].copy()
|
||||
|
||||
date_cols = ["Original Due Date", "Due Date", "Window Start Date", "Cutoff Date", "Completed Date"]
|
||||
for col in date_cols:
|
||||
cz[col] = pd.to_datetime(cz[col], errors="coerce")
|
||||
|
||||
SITES = [
|
||||
"DD5-CZ10001", "DD5-CZ10003", "DD5-CZ10006", "DD5-CZ10009",
|
||||
"DD5-CZ10010", "DD5-CZ10012", "DD5-CZ10013", "DD5-CZ10015",
|
||||
"DD5-CZ10016", "DD5-CZ10020", "DD5-CZ10021", "DD5-CZ10022",
|
||||
]
|
||||
cz = cz[cz["Study Site Number"].isin(SITES) & cz["Status"].isin(["Completed", "Scheduled", "Planned"])].copy()
|
||||
|
||||
cz["CRA"] = cz["Assigned To Last Name"].fillna("")
|
||||
|
||||
# --- Merge Site Visit Report (2) ---
|
||||
import re as _re
|
||||
def _svid_to_ref(svid):
|
||||
svid = str(svid).replace("MCTMS|", "")
|
||||
if svid == "Qualification Visit": return "SQV"
|
||||
if svid == "Site Initiation": return "SIV"
|
||||
if svid == "Closure Visit": return "COV"
|
||||
m = _re.match(r"Monitoring Visit (\d+)", svid)
|
||||
return f"IMV{m.group(1)}" if m else svid
|
||||
|
||||
svr = pd.read_excel(SVR_FILE, header=5)
|
||||
svr = svr[svr["Site ID"].isin(SITES)].copy()
|
||||
svr["Reference"] = svr["Site Visit ID"].apply(_svid_to_ref)
|
||||
svr = svr[["Site ID", "Reference", "Site Visit Type", "Submitter Name", "Approver Name"]].rename(columns={"Site ID": "Study Site Number"})
|
||||
|
||||
cz = cz.merge(svr, on=["Study Site Number", "Reference"], how="left")
|
||||
|
||||
# --- Styles ---
|
||||
FONT_NAME = "Arial"
|
||||
COL_HEADER = "1F5C99" # dark blue
|
||||
COL_COMPL = "E2EFDA" # light green
|
||||
COL_SCHED = "FFF2CC" # light yellow
|
||||
COL_PLAN = "FCE4D6" # light orange
|
||||
COL_NA = "F2F2F2" # grey
|
||||
WHITE = "FFFFFF"
|
||||
DARK_TEXT = "000000"
|
||||
|
||||
STATUS_COLORS = {
|
||||
"Completed": COL_COMPL,
|
||||
"Scheduled": COL_SCHED,
|
||||
"Planned": COL_PLAN,
|
||||
"Not applicable": COL_NA,
|
||||
}
|
||||
|
||||
thin = Side(style="thin", color="BFBFBF")
|
||||
med = Side(style="medium", color="808080")
|
||||
|
||||
def border(left=thin, right=thin, top=thin, bottom=thin):
|
||||
return Border(left=left, right=right, top=top, bottom=bottom)
|
||||
|
||||
def header_cell(ws, row, col, value, width=None):
|
||||
c = ws.cell(row=row, column=col, value=value)
|
||||
c.font = Font(name=FONT_NAME, bold=True, color=WHITE, size=10)
|
||||
c.fill = PatternFill("solid", fgColor=COL_HEADER)
|
||||
c.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
c.border = Border(left=Side(style="medium", color=WHITE),
|
||||
right=Side(style="medium", color=WHITE),
|
||||
top=thin, bottom=thin)
|
||||
if width and col <= ws.max_column or width:
|
||||
ws.column_dimensions[get_column_letter(col)].width = width
|
||||
return c
|
||||
|
||||
def data_cell(ws, row, col, value, fill_color=WHITE, align="left", bold=False, num_fmt=None, date_val=False):
|
||||
c = ws.cell(row=row, column=col, value=value)
|
||||
c.font = Font(name=FONT_NAME, size=9, bold=bold, color=DARK_TEXT)
|
||||
if fill_color != WHITE:
|
||||
c.fill = PatternFill("solid", fgColor=fill_color)
|
||||
c.alignment = Alignment(horizontal=align, vertical="center")
|
||||
c.border = border()
|
||||
if num_fmt:
|
||||
c.number_format = num_fmt
|
||||
elif date_val and isinstance(value, (pd.Timestamp, type(None))):
|
||||
c.number_format = "DD-MMM-YYYY"
|
||||
return c
|
||||
|
||||
# =========================================================
|
||||
# SHEET 1: Přehled per site
|
||||
# =========================================================
|
||||
wb = openpyxl.Workbook()
|
||||
ws1 = wb.active
|
||||
ws1.title = "Přehled CZ"
|
||||
ws1.freeze_panes = "A3"
|
||||
|
||||
# Title
|
||||
ws1.merge_cells("A1:M1")
|
||||
title = ws1["A1"]
|
||||
title.value = f"UCO3001 — CZ CTMS Visits Overview | {today_str}"
|
||||
title.font = Font(name=FONT_NAME, bold=True, size=12, color=WHITE)
|
||||
title.fill = PatternFill("solid", fgColor="2E4057")
|
||||
title.alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws1.row_dimensions[1].height = 22
|
||||
|
||||
# Headers
|
||||
headers = [
|
||||
("Site", 14), ("Investigátor", 22),
|
||||
("SQV", 11), ("SIV", 11),
|
||||
("IMV\nCompleted", 11), ("IMV\nScheduled", 11), ("IMV\nPlanned", 11),
|
||||
("COV", 11),
|
||||
("Poslední vizita\nDatum", 14), ("Poslední vizita\nTyp", 16),
|
||||
("Příští vizita\nDatum", 14), ("Příští vizita\nTyp", 16),
|
||||
("Celkem\nvizit", 10),
|
||||
]
|
||||
for ci, (h, w) in enumerate(headers, 1):
|
||||
header_cell(ws1, 2, ci, h, width=w)
|
||||
ws1.row_dimensions[2].height = 30
|
||||
|
||||
# Data per site
|
||||
sites = sorted(cz["Study Site Number"].unique())
|
||||
for ri, site in enumerate(sites, 3):
|
||||
s = cz[cz["Study Site Number"] == site]
|
||||
inv_row = s.iloc[0]
|
||||
inv = f"{inv_row['INV_FIRST_NAME']} {inv_row['INV_LAST_NAME']}"
|
||||
cra = s["CRA"].replace("", pd.NA).dropna().iloc[0] if not s["CRA"].replace("", pd.NA).dropna().empty else ""
|
||||
|
||||
sqv = s[s["Reference"] == "SQV"]
|
||||
siv = s[s["Reference"] == "SIV"]
|
||||
cov = s[s["Reference"] == "COV"]
|
||||
imv = s[s["Category"] == "Monitoring Visit"]
|
||||
|
||||
def visit_status(sub):
|
||||
if sub.empty:
|
||||
return ("—", COL_NA)
|
||||
st = sub.iloc[0]["Status"]
|
||||
return (st, STATUS_COLORS.get(st, WHITE))
|
||||
|
||||
sqv_st, sqv_c = visit_status(sqv)
|
||||
siv_st, siv_c = visit_status(siv)
|
||||
cov_st, cov_c = visit_status(cov)
|
||||
|
||||
imv_comp = int((imv["Status"] == "Completed").sum())
|
||||
imv_sch = int((imv["Status"] == "Scheduled").sum())
|
||||
imv_plan = int((imv["Status"] == "Planned").sum())
|
||||
|
||||
# Last completed
|
||||
comp = s[s["Status"] == "Completed"].dropna(subset=["Completed Date"])
|
||||
last_comp = comp.sort_values("Completed Date").iloc[-1] if not comp.empty else None
|
||||
last_date = last_comp["Completed Date"] if last_comp is not None else None
|
||||
last_type = last_comp["Reference"] if last_comp is not None else "—"
|
||||
|
||||
# Next upcoming — pouze vizity s Due Date po poslední Completed
|
||||
upcoming = s[s["Status"].isin(["Scheduled", "Planned"])].dropna(subset=["Due Date"])
|
||||
if last_date is not None:
|
||||
upcoming = upcoming[upcoming["Due Date"] > last_date]
|
||||
next_vis = upcoming.sort_values("Due Date").iloc[0] if not upcoming.empty else None
|
||||
next_date = next_vis["Due Date"] if next_vis is not None else None
|
||||
next_type = next_vis["Reference"] if next_vis is not None else "—"
|
||||
|
||||
total = len(s)
|
||||
bg = WHITE if ri % 2 == 0 else "F7F9FC"
|
||||
|
||||
row_data = [
|
||||
(site, "left", True, None, None),
|
||||
(inv, "left", False, None, None),
|
||||
(sqv_st, "center", False, None, sqv_c),
|
||||
(siv_st, "center", False, None, siv_c),
|
||||
(imv_comp, "center", False, "#,##0", None),
|
||||
(imv_sch, "center", False, "#,##0", None),
|
||||
(imv_plan, "center", False, "#,##0", None),
|
||||
(cov_st, "center", False, None, cov_c),
|
||||
(last_date, "center", False, "DD-MMM-YY",None),
|
||||
(last_type, "center", False, None, None),
|
||||
(next_date, "center", False, "DD-MMM-YY",None),
|
||||
(next_type, "center", False, None, None),
|
||||
(total, "center", True, "#,##0", None),
|
||||
]
|
||||
for ci, (val, align, bold, fmt, fill) in enumerate(row_data, 1):
|
||||
fc = fill if fill else bg
|
||||
c = data_cell(ws1, ri, ci, val, fill_color=fc, align=align, bold=bold)
|
||||
if fmt:
|
||||
c.number_format = fmt
|
||||
ws1.row_dimensions[ri].height = 16
|
||||
|
||||
# Autofilter
|
||||
ws1.auto_filter.ref = f"A2:{get_column_letter(len(headers))}2"
|
||||
|
||||
# =========================================================
|
||||
# SHEET 2: Detail všech CZ vizit
|
||||
# =========================================================
|
||||
ws2 = wb.create_sheet("Detail CZ")
|
||||
ws2.freeze_panes = "A3"
|
||||
|
||||
ws2.merge_cells("A1:N1")
|
||||
t2 = ws2["A1"]
|
||||
t2.value = f"UCO3001 — CZ CTMS Visits — Detail | {today_str}"
|
||||
t2.font = Font(name=FONT_NAME, bold=True, size=12, color=WHITE)
|
||||
t2.fill = PatternFill("solid", fgColor="2E4057")
|
||||
t2.alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws2.row_dimensions[1].height = 22
|
||||
|
||||
det_headers = [
|
||||
("Site", 14), ("Investigátor", 22), ("CRA (Submitter)", 24),
|
||||
("Ref", 9), ("Název vizity", 24), ("Category", 20), ("Sub Category", 16),
|
||||
("Status", 14),
|
||||
("Due Date", 13), ("Window Start", 13), ("Cutoff Date", 13), ("Completed Date", 13),
|
||||
("Typ vizity", 12),
|
||||
]
|
||||
for ci, (h, w) in enumerate(det_headers, 1):
|
||||
header_cell(ws2, 2, ci, h, width=w)
|
||||
ws2.row_dimensions[2].height = 26
|
||||
|
||||
# Sort: site → SQV → SIV → IMV1 → IMV2 … → COV
|
||||
ref_order = {"SQV": 0, "SIV": 1, "COV": 9999}
|
||||
def ref_sort_key(ref):
|
||||
if ref in ref_order:
|
||||
return ref_order[ref]
|
||||
import re
|
||||
m = re.match(r"IMV(\d+)$", str(ref))
|
||||
return int(m.group(1)) + 1 if m else 5000
|
||||
cz["_ref_ord"] = cz["Reference"].apply(ref_sort_key)
|
||||
detail = cz.sort_values(["Study Site Number", "_ref_ord"]).reset_index(drop=True)
|
||||
|
||||
for ri, row in detail.iterrows():
|
||||
r = ri + 3
|
||||
st = row["Status"]
|
||||
bg = STATUS_COLORS.get(st, WHITE)
|
||||
|
||||
inv = f"{row['INV_FIRST_NAME']} {row['INV_LAST_NAME']}"
|
||||
submitter = row["Submitter Name"] if pd.notna(row.get("Submitter Name")) else ""
|
||||
visit_type = row["Site Visit Type"] if pd.notna(row.get("Site Visit Type")) else ""
|
||||
vals = [
|
||||
(row["Study Site Number"], "left", True),
|
||||
(inv, "left", False),
|
||||
(submitter, "left", False),
|
||||
(row["Reference"], "center", True),
|
||||
(row["Visit Name"], "left", False),
|
||||
(row["Category"], "left", False),
|
||||
(row["Sub Category"], "left", False),
|
||||
(st, "center", False),
|
||||
(row["Due Date"], "center", False),
|
||||
(row["Window Start Date"], "center", False),
|
||||
(row["Cutoff Date"], "center", False),
|
||||
(row["Completed Date"], "center", False),
|
||||
(visit_type, "center", False),
|
||||
]
|
||||
for ci, (val, align, bold) in enumerate(vals, 1):
|
||||
c = data_cell(ws2, r, ci, val, fill_color=bg, align=align, bold=bold)
|
||||
if isinstance(val, pd.Timestamp) and not pd.isna(val):
|
||||
c.value = val.to_pydatetime()
|
||||
c.number_format = "DD-MMM-YY"
|
||||
ws2.row_dimensions[r].height = 14
|
||||
|
||||
ws2.auto_filter.ref = f"A2:{get_column_letter(len(det_headers))}2"
|
||||
|
||||
# =========================================================
|
||||
# SHEET 3: Nadcházející / Scheduled+Planned
|
||||
# =========================================================
|
||||
ws3 = wb.create_sheet("Nadcházející vizity")
|
||||
ws3.freeze_panes = "A3"
|
||||
|
||||
ws3.merge_cells("A1:J1")
|
||||
t3 = ws3["A1"]
|
||||
t3.value = f"UCO3001 — CZ — Nadcházející vizity (Scheduled + Planned) | {today_str}"
|
||||
t3.font = Font(name=FONT_NAME, bold=True, size=12, color=WHITE)
|
||||
t3.fill = PatternFill("solid", fgColor="2E4057")
|
||||
t3.alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws3.row_dimensions[1].height = 22
|
||||
|
||||
upc_headers = [
|
||||
("Due Date", 13), ("Site", 14), ("Investigátor", 22), ("CRA", 14),
|
||||
("Ref", 9), ("Název vizity", 24), ("Category", 20),
|
||||
("Status", 12), ("Window Start", 13), ("Cutoff Date", 13),
|
||||
]
|
||||
for ci, (h, w) in enumerate(upc_headers, 1):
|
||||
header_cell(ws3, 2, ci, h, width=w)
|
||||
ws3.row_dimensions[2].height = 26
|
||||
|
||||
upcoming = cz[cz["Status"].isin(["Scheduled", "Planned"])].sort_values(["Due Date", "Study Site Number"]).reset_index(drop=True)
|
||||
|
||||
for ri, row in upcoming.iterrows():
|
||||
r = ri + 3
|
||||
bg = STATUS_COLORS.get(row["Status"], WHITE)
|
||||
inv = f"{row['INV_FIRST_NAME']} {row['INV_LAST_NAME']}"
|
||||
vals = [
|
||||
(row["Due Date"], "center", True),
|
||||
(row["Study Site Number"], "left", False),
|
||||
(inv, "left", False),
|
||||
(row["CRA"], "center", False),
|
||||
(row["Reference"], "center", True),
|
||||
(row["Visit Name"], "left", False),
|
||||
(row["Category"], "left", False),
|
||||
(row["Status"], "center", False),
|
||||
(row["Window Start Date"], "center", False),
|
||||
(row["Cutoff Date"], "center", False),
|
||||
]
|
||||
for ci, (val, align, bold) in enumerate(vals, 1):
|
||||
c = data_cell(ws3, r, ci, val, fill_color=bg, align=align, bold=bold)
|
||||
if isinstance(val, pd.Timestamp) and not pd.isna(val):
|
||||
c.value = val.to_pydatetime()
|
||||
c.number_format = "DD-MMM-YY"
|
||||
ws3.row_dimensions[r].height = 14
|
||||
|
||||
ws3.auto_filter.ref = f"A2:{get_column_letter(len(upc_headers))}2"
|
||||
|
||||
# =========================================================
|
||||
# SHEET 4: Problémy — datové nesoulady
|
||||
# =========================================================
|
||||
ws4 = wb.create_sheet("Problémy")
|
||||
ws4.freeze_panes = "A3"
|
||||
|
||||
# Načteme původní data bez statusového filtru pro detekci problémů
|
||||
df_raw = pd.read_csv(CSV_FILE, sep=";", encoding="utf-8-sig")
|
||||
df_raw["Country"] = df_raw["Study Site Number"].str.extract(r"DD5-([A-Z]+)\d+")
|
||||
cz_raw = df_raw[df_raw["Study Site Number"].isin(SITES)].copy()
|
||||
for col in date_cols:
|
||||
cz_raw[col] = pd.to_datetime(cz_raw[col], errors="coerce")
|
||||
cz_raw["CRA"] = cz_raw["Assigned To Last Name"].fillna("")
|
||||
cz_raw = cz_raw.merge(svr, on=["Study Site Number", "Reference"], how="left")
|
||||
cz_raw["Submitter Name"] = cz_raw["Submitter Name"].fillna("")
|
||||
|
||||
problems = []
|
||||
|
||||
# Pravidlo 1: Completed Date vyplněno ale Status ≠ Completed
|
||||
mask1 = cz_raw["Completed Date"].notna() & (cz_raw["Status"] != "Completed")
|
||||
for _, row in cz_raw[mask1].iterrows():
|
||||
problems.append((row, "Completed Date je vyplněno, ale Status není Completed"))
|
||||
|
||||
# Seřadit podle site a reference
|
||||
import re as _re
|
||||
def _ref_key(ref):
|
||||
if ref == "SQV": return 0
|
||||
if ref == "SIV": return 1
|
||||
if ref == "COV": return 9999
|
||||
m = _re.match(r"IMV(\d+)$", str(ref))
|
||||
return int(m.group(1)) + 1 if m else 5000
|
||||
|
||||
problems.sort(key=lambda x: (x[0]["Study Site Number"], _ref_key(x[0]["Reference"])))
|
||||
|
||||
COL_PROBLEM = "FFC7CE" # světle červená
|
||||
|
||||
ws4.merge_cells("A1:M1")
|
||||
t4 = ws4["A1"]
|
||||
t4.value = f"UCO3001 — CZ — Datové problémy k opravě v OneCTMS | {today_str}"
|
||||
t4.font = Font(name=FONT_NAME, bold=True, size=12, color=WHITE)
|
||||
t4.fill = PatternFill("solid", fgColor="C00000")
|
||||
t4.alignment = Alignment(horizontal="center", vertical="center")
|
||||
ws4.row_dimensions[1].height = 22
|
||||
|
||||
prob_headers = [
|
||||
("Site", 14), ("Investigátor", 22), ("CRA (Submitter)", 24),
|
||||
("Ref", 9), ("Název vizity", 24), ("Category", 18),
|
||||
("Status", 14),
|
||||
("Due Date", 13), ("Completed Date", 13),
|
||||
("", 2),
|
||||
("Důvod — co je potřeba opravit v OneCTMS", 50),
|
||||
]
|
||||
for ci, (h, w) in enumerate(prob_headers, 1):
|
||||
header_cell(ws4, 2, ci, h, width=w)
|
||||
ws4.row_dimensions[2].height = 26
|
||||
|
||||
for ri, (row, reason) in enumerate(problems, 3):
|
||||
inv = f"{row['INV_FIRST_NAME']} {row['INV_LAST_NAME']}"
|
||||
vals = [
|
||||
(row["Study Site Number"], "left", True, None),
|
||||
(inv, "left", False, None),
|
||||
(row["Submitter Name"], "left", False, None),
|
||||
(row["Reference"], "center", True, None),
|
||||
(row["Visit Name"], "left", False, None),
|
||||
(row["Category"], "left", False, None),
|
||||
(row["Status"], "center", False, None),
|
||||
(row["Due Date"], "center", False, "DD-MMM-YY"),
|
||||
(row["Completed Date"], "center", False, "DD-MMM-YY"),
|
||||
("", "center", False, None),
|
||||
(reason, "left", True, None),
|
||||
]
|
||||
for ci, (val, align, bold, fmt) in enumerate(vals, 1):
|
||||
c = data_cell(ws4, ri, ci, val, fill_color=COL_PROBLEM, align=align, bold=bold)
|
||||
if fmt and isinstance(val, pd.Timestamp) and not pd.isna(val):
|
||||
c.value = val.to_pydatetime()
|
||||
c.number_format = fmt
|
||||
ws4.row_dimensions[ri].height = 16
|
||||
|
||||
ws4.auto_filter.ref = f"A2:{get_column_letter(len(prob_headers))}2"
|
||||
|
||||
wb.save(OUTPUT_FILE)
|
||||
print(f"Report uložen: {OUTPUT_FILE}")
|
||||
print(f" Sheet 'Přehled CZ' : {len(sites)} sites")
|
||||
print(f" Sheet 'Detail CZ' : {len(detail)} řádků")
|
||||
print(f" Sheet 'Nadcházející vizity': {len(upcoming)} vizit")
|
||||
print(f" Sheet 'Problémy' : {len(problems)} záznamů")
|
||||
Reference in New Issue
Block a user