z230
This commit is contained in:
@@ -22,14 +22,19 @@ assert csv_files, "CSV file not found!"
|
|||||||
csv_file = csv_files[0]
|
csv_file = csv_files[0]
|
||||||
print(f"Source csv: {csv_file}")
|
print(f"Source csv: {csv_file}")
|
||||||
|
|
||||||
# Delete old output report if exists
|
kit_csv_files = glob.glob(src_dir + "sponsor-study-36940-kit-inventory-on-hand-expiration.csv")
|
||||||
today = date.today().strftime("%Y-%m-%d")
|
assert kit_csv_files, "Kit inventory CSV not found!"
|
||||||
out_filename = f"{today} 77242113UCO3001 Speciment Inventory report.xlsx"
|
kit_csv_file = kit_csv_files[0]
|
||||||
out_path = out_dir + out_filename
|
print(f"Kit csv: {kit_csv_file}")
|
||||||
|
|
||||||
for old in glob.glob(out_dir + "*77242113UCO3001 Speciment Inventory report*.xlsx"):
|
eq_csv_files = glob.glob(src_dir + "sponsor-study-36940-activity-reports-documents-equery.csv")
|
||||||
os.remove(old)
|
assert eq_csv_files, "eQuery CSV not found!"
|
||||||
print(f"Deleted old: {old}")
|
eq_csv_file = eq_csv_files[0]
|
||||||
|
print(f"eQuery csv: {eq_csv_file}")
|
||||||
|
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
||||||
|
out_filename = f"{timestamp} 77242113UCO3001 CZE Labcorp samples and kit inventory report.xlsx"
|
||||||
|
out_path = out_dir + out_filename
|
||||||
|
|
||||||
# Copy source file to output — preserves all formatting perfectly
|
# Copy source file to output — preserves all formatting perfectly
|
||||||
shutil.copy2(src_file, out_path)
|
shutil.copy2(src_file, out_path)
|
||||||
@@ -52,10 +57,8 @@ def fmt_date(val):
|
|||||||
return datetime.strptime(val, '%d-%b-%Y')
|
return datetime.strptime(val, '%d-%b-%Y')
|
||||||
return pd.to_datetime(val).to_pydatetime()
|
return pd.to_datetime(val).to_pydatetime()
|
||||||
|
|
||||||
# Get Container Receipt Date + Excel row for patient+specimen from given visit dataframe
|
|
||||||
OK_STATUSES = {'Received', 'In Inventory', 'Shipped'}
|
OK_STATUSES = {'Received', 'In Inventory', 'Shipped'}
|
||||||
|
|
||||||
# Excel row = pandas df index + 2 (row 1 header, data from row 2)
|
|
||||||
def get_specimen_info(visit_df, patient, specimen_type=None):
|
def get_specimen_info(visit_df, patient, specimen_type=None):
|
||||||
rows = visit_df[visit_df['Patient No.'] == patient]
|
rows = visit_df[visit_df['Patient No.'] == patient]
|
||||||
if specimen_type:
|
if specimen_type:
|
||||||
@@ -66,7 +69,6 @@ def get_specimen_info(visit_df, patient, specimen_type=None):
|
|||||||
row = rows.iloc[0]
|
row = rows.iloc[0]
|
||||||
return fmt_date(row['Container Receipt Date']), rows.index[0] + 2
|
return fmt_date(row['Container Receipt Date']), rows.index[0] + 2
|
||||||
|
|
||||||
# Get Container Receipt Date + Excel row by Container Label Line 1 code and visit code
|
|
||||||
def get_label_info(patient, label_code, visit_code):
|
def get_label_info(patient, label_code, visit_code):
|
||||||
rows = df[(df['Patient No.'] == patient) &
|
rows = df[(df['Patient No.'] == patient) &
|
||||||
(df['Protocol Visit Code'] == visit_code) &
|
(df['Protocol Visit Code'] == visit_code) &
|
||||||
@@ -87,32 +89,47 @@ for col in src_ws.columns:
|
|||||||
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
|
max_len = max((len(str(cell.value)) if cell.value is not None else 0) for cell in col)
|
||||||
src_ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50)
|
src_ws.column_dimensions[get_column_letter(col[0].column)].width = min(max_len + 2, 50)
|
||||||
|
|
||||||
# Create analysis sheet
|
# ── Styly ────────────────────────────────────────────────────────────────────
|
||||||
|
thin = Side(style='thin')
|
||||||
|
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||||
|
header_fill = PatternFill("solid", fgColor="4472C4")
|
||||||
|
header_font = Font(name='Calibri', bold=True, size=11, color="FFFFFF")
|
||||||
|
data_font = Font(name='Calibri', size=11)
|
||||||
|
date_font_link = Font(name='Calibri', size=11, color="000000", underline='single')
|
||||||
|
yes_fill = PatternFill("solid", fgColor="E2EFDA")
|
||||||
|
no_fill = PatternFill("solid", fgColor="FFE7E7")
|
||||||
|
sum_header_font = Font(name='Calibri', bold=True, size=11, color="000000")
|
||||||
|
sum_total_font = Font(name='Calibri', bold=True, size=11)
|
||||||
|
zero_font = Font(name='Calibri', size=11, color="BFBFBF")
|
||||||
|
zero_red_font = Font(name='Calibri', size=11, color="C00000")
|
||||||
|
dark_blue_fill = PatternFill("solid", fgColor="203764")
|
||||||
|
orange_fill = PatternFill("solid", fgColor="FFF2CC")
|
||||||
|
green_fill = PatternFill("solid", fgColor="E2EFDA")
|
||||||
|
total_fill = PatternFill("solid", fgColor="D9E1F2")
|
||||||
|
exp_fill = PatternFill("solid", fgColor="FFE7E7")
|
||||||
|
ok_fill = PatternFill("solid", fgColor="E2EFDA")
|
||||||
|
|
||||||
|
# ── List: Přehled vzorků ──────────────────────────────────────────────────────
|
||||||
analysis_ws = out_wb.create_sheet("Přehled vzorků")
|
analysis_ws = out_wb.create_sheet("Přehled vzorků")
|
||||||
|
|
||||||
thin = Side(style='thin')
|
|
||||||
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
|
||||||
|
|
||||||
# Column definitions: (header, width)
|
|
||||||
# No "Visit code" column
|
|
||||||
columns = [
|
columns = [
|
||||||
("Investigator Name", 24),
|
("Investigator Name", 24),
|
||||||
("Číslo pacienta", 20),
|
("Číslo pacienta", 20),
|
||||||
("Máme biopsii SM11", 20), # col 3 → SCREENING C:E
|
("Máme biopsii SM11", 20),
|
||||||
("Máme RNA", 16),
|
("Máme RNA", 16),
|
||||||
("Máme Cryostor", 16),
|
("Máme Cryostor", 16),
|
||||||
("DNA", 14), # col 6 → no group
|
("DNA", 14),
|
||||||
("PLASMPK I-0 TROUGH", 18), # col 7 → RANDOMIZACE I-0 G:L
|
("PLASMPK I-0 TROUGH", 18),
|
||||||
("PLASMA PK I-0 PEAK", 18),
|
("PLASMA PK I-0 PEAK", 18),
|
||||||
("SERUM ADA I-0 PRE", 18),
|
("SERUM ADA I-0 PRE", 18),
|
||||||
("SM06/SERUM BIOM", 16),
|
("SM06/SERUM BIOM", 16),
|
||||||
("SM07/WB RNA", 14),
|
("SM07/WB RNA", 14),
|
||||||
("SM10/FECAL", 14),
|
("SM10/FECAL", 14),
|
||||||
("PLASMPK I-2 TROUGH", 18), # col 13 → I-2 M:P
|
("PLASMPK I-2 TROUGH", 18),
|
||||||
("PLASMA PK I-2 PEAK", 18),
|
("PLASMA PK I-2 PEAK", 18),
|
||||||
("SERUM ADA I-2 PRE", 18),
|
("SERUM ADA I-2 PRE", 18),
|
||||||
("STOOL I-2", 12),
|
("STOOL I-2", 12),
|
||||||
("PLASMPK I-4 TROUGH", 18), # col 17 → I-4 Q:V
|
("PLASMPK I-4 TROUGH", 18),
|
||||||
("PLASMA PK I-4 PEAK", 18),
|
("PLASMA PK I-4 PEAK", 18),
|
||||||
("SERUM ADA I-4 PRE", 18),
|
("SERUM ADA I-4 PRE", 18),
|
||||||
("SM06/SERUM BIOM", 16),
|
("SM06/SERUM BIOM", 16),
|
||||||
@@ -120,9 +137,8 @@ columns = [
|
|||||||
("STOOL I-4", 12),
|
("STOOL I-4", 12),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Row 1 — group headers
|
group_font = Font(name='Calibri', bold=True, size=11)
|
||||||
group_font = Font(name='Calibri', bold=True, size=11)
|
group_fill = PatternFill("solid", fgColor="FFFFFF")
|
||||||
group_fill = PatternFill("solid", fgColor="FFFFFF") # white, same as user's theme=0
|
|
||||||
group_border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
group_border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||||
|
|
||||||
groups = [
|
groups = [
|
||||||
@@ -138,18 +154,13 @@ for start_col, end_col, label in groups:
|
|||||||
cell.fill = group_fill
|
cell.fill = group_fill
|
||||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||||
cell.border = group_border
|
cell.border = group_border
|
||||||
# apply border to all merged cells
|
|
||||||
for c in range(start_col, end_col + 1):
|
for c in range(start_col, end_col + 1):
|
||||||
analysis_ws.cell(row=1, column=c).border = group_border
|
analysis_ws.cell(row=1, column=c).border = group_border
|
||||||
|
|
||||||
analysis_ws.row_dimensions[1].height = 20
|
analysis_ws.row_dimensions[1].height = 20
|
||||||
|
|
||||||
# Row 2 — column headers
|
for col_idx, (hdr, width) in enumerate(columns, 1):
|
||||||
header_fill = PatternFill("solid", fgColor="4472C4")
|
cell = analysis_ws.cell(row=2, column=col_idx, value=hdr)
|
||||||
header_font = Font(name='Calibri', bold=True, size=11, color="FFFFFF")
|
|
||||||
|
|
||||||
for col_idx, (header, width) in enumerate(columns, 1):
|
|
||||||
cell = analysis_ws.cell(row=2, column=col_idx, value=header)
|
|
||||||
cell.font = header_font
|
cell.font = header_font
|
||||||
cell.fill = header_fill
|
cell.fill = header_fill
|
||||||
cell.border = border
|
cell.border = border
|
||||||
@@ -157,21 +168,11 @@ for col_idx, (header, width) in enumerate(columns, 1):
|
|||||||
analysis_ws.column_dimensions[get_column_letter(col_idx)].width = width
|
analysis_ws.column_dimensions[get_column_letter(col_idx)].width = width
|
||||||
|
|
||||||
analysis_ws.row_dimensions[2].height = 30
|
analysis_ws.row_dimensions[2].height = 30
|
||||||
|
|
||||||
# Freeze first 2 columns and first 2 rows
|
|
||||||
analysis_ws.freeze_panes = "C3"
|
analysis_ws.freeze_panes = "C3"
|
||||||
|
|
||||||
# Data rows start at row 3
|
|
||||||
date_font_link = Font(name='Calibri', size=11, color="000000", underline='single')
|
|
||||||
yes_fill = PatternFill("solid", fgColor="E2EFDA")
|
|
||||||
no_fill = PatternFill("solid", fgColor="FFE7E7")
|
|
||||||
data_font = Font(name='Calibri', size=11)
|
|
||||||
|
|
||||||
src_sheet_name = out_wb.sheetnames[0]
|
src_sheet_name = out_wb.sheetnames[0]
|
||||||
pat_sheet_name = "Seznam pacientů"
|
pat_sheet_name = "Seznam pacientů"
|
||||||
|
|
||||||
# Build patient → first Excel row in "Seznam pacientů" (header=row1, data from row2)
|
|
||||||
# pat_df is built later, but we need sorted order — pre-sort here too
|
|
||||||
_csv_df_pre = pd.read_csv(csv_file, encoding='utf-8')
|
_csv_df_pre = pd.read_csv(csv_file, encoding='utf-8')
|
||||||
_pat_pre = _csv_df_pre[['SiteNumber', 'Subject', 'Field4Value']].copy()
|
_pat_pre = _csv_df_pre[['SiteNumber', 'Subject', 'Field4Value']].copy()
|
||||||
_pat_pre['Field4Value'] = _pat_pre['Field4Value'].apply(lambda v: datetime.strptime(str(v).strip(), '%d %b %Y') if pd.notna(v) else None)
|
_pat_pre['Field4Value'] = _pat_pre['Field4Value'].apply(lambda v: datetime.strptime(str(v).strip(), '%d %b %Y') if pd.notna(v) else None)
|
||||||
@@ -180,9 +181,8 @@ patient_row_map = {}
|
|||||||
for i, row in _pat_pre.iterrows():
|
for i, row in _pat_pre.iterrows():
|
||||||
pat = row['Subject']
|
pat = row['Subject']
|
||||||
if pat not in patient_row_map:
|
if pat not in patient_row_map:
|
||||||
patient_row_map[pat] = i + 2 # +1 for 1-based, +1 for header row
|
patient_row_map[pat] = i + 2
|
||||||
|
|
||||||
# Only patients with any BXSCR record
|
|
||||||
bxscr_patients = sorted(bxscr['Patient No.'].dropna().unique())
|
bxscr_patients = sorted(bxscr['Patient No.'].dropna().unique())
|
||||||
|
|
||||||
for row_idx, patient in enumerate(bxscr_patients, 3):
|
for row_idx, patient in enumerate(bxscr_patients, 3):
|
||||||
@@ -208,7 +208,6 @@ for row_idx, patient in enumerate(bxscr_patients, 3):
|
|||||||
sm074, sm074_row = get_label_info(patient, 'SM07/WB RNA', 'I-4')
|
sm074, sm074_row = get_label_info(patient, 'SM07/WB RNA', 'I-4')
|
||||||
stool4, stool4_row = get_label_info(patient, 'STOOL I-4', 'I-4')
|
stool4, stool4_row = get_label_info(patient, 'STOOL I-4', 'I-4')
|
||||||
|
|
||||||
# col 1-2: plain text, col 3+: (date, excel_row) tuples
|
|
||||||
row_data = [investigator, patient,
|
row_data = [investigator, patient,
|
||||||
(sm11, sm11_row), (rna, rna_row), (cryo, cryo_row), (dna_date, dna_row),
|
(sm11, sm11_row), (rna, rna_row), (cryo, cryo_row), (dna_date, dna_row),
|
||||||
(trough, trough_row), (peak, peak_row), (ada, ada_row),
|
(trough, trough_row), (peak, peak_row), (ada, ada_row),
|
||||||
@@ -239,9 +238,8 @@ for row_idx, patient in enumerate(bxscr_patients, 3):
|
|||||||
cell.border = border
|
cell.border = border
|
||||||
cell.alignment = Alignment(horizontal='center', vertical='center')
|
cell.alignment = Alignment(horizontal='center', vertical='center')
|
||||||
|
|
||||||
# ── Seznam pacientů sheet ────────────────────────────────────────────────────
|
# ── List: Seznam pacientů ─────────────────────────────────────────────────────
|
||||||
csv_df = pd.read_csv(csv_file, encoding='utf-8')
|
csv_df = pd.read_csv(csv_file, encoding='utf-8')
|
||||||
|
|
||||||
patients_ws = out_wb.create_sheet("Seznam pacientů")
|
patients_ws = out_wb.create_sheet("Seznam pacientů")
|
||||||
|
|
||||||
pat_columns = [
|
pat_columns = [
|
||||||
@@ -252,7 +250,6 @@ pat_columns = [
|
|||||||
("Typ návštěvy", 16),
|
("Typ návštěvy", 16),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Header row
|
|
||||||
for col_idx, (col_name, width) in enumerate(pat_columns, 1):
|
for col_idx, (col_name, width) in enumerate(pat_columns, 1):
|
||||||
cell = patients_ws.cell(row=1, column=col_idx, value=col_name)
|
cell = patients_ws.cell(row=1, column=col_idx, value=col_name)
|
||||||
cell.font = header_font
|
cell.font = header_font
|
||||||
@@ -264,8 +261,7 @@ for col_idx, (col_name, width) in enumerate(pat_columns, 1):
|
|||||||
patients_ws.row_dimensions[1].height = 30
|
patients_ws.row_dimensions[1].height = 30
|
||||||
patients_ws.freeze_panes = "A2"
|
patients_ws.freeze_panes = "A2"
|
||||||
|
|
||||||
# Prepare and sort data
|
def parse_date_edcstd(val):
|
||||||
def parse_date(val):
|
|
||||||
if pd.isna(val) or str(val).strip() == '':
|
if pd.isna(val) or str(val).strip() == '':
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -274,10 +270,9 @@ def parse_date(val):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
pat_df = csv_df[['SiteNumber', 'Subject', 'InstanceName', 'Field4Value', 'Field5Value']].copy()
|
pat_df = csv_df[['SiteNumber', 'Subject', 'InstanceName', 'Field4Value', 'Field5Value']].copy()
|
||||||
pat_df['Field4Value'] = pat_df['Field4Value'].apply(parse_date)
|
pat_df['Field4Value'] = pat_df['Field4Value'].apply(parse_date_edcstd)
|
||||||
pat_df = pat_df.sort_values(['SiteNumber', 'Subject', 'Field4Value']).reset_index(drop=True)
|
pat_df = pat_df.sort_values(['SiteNumber', 'Subject', 'Field4Value']).reset_index(drop=True)
|
||||||
|
|
||||||
# Data rows
|
|
||||||
for row_idx, row in enumerate(pat_df.itertuples(index=False), 2):
|
for row_idx, row in enumerate(pat_df.itertuples(index=False), 2):
|
||||||
for col_idx, value in enumerate(row, 1):
|
for col_idx, value in enumerate(row, 1):
|
||||||
cell = patients_ws.cell(row=row_idx, column=col_idx, value=value)
|
cell = patients_ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
@@ -287,7 +282,244 @@ for row_idx, row in enumerate(pat_df.itertuples(index=False), 2):
|
|||||||
if col_idx == 4 and value is not None:
|
if col_idx == 4 and value is not None:
|
||||||
cell.number_format = 'DD-MMM-YYYY'
|
cell.number_format = 'DD-MMM-YYYY'
|
||||||
|
|
||||||
|
# ── Kit inventory — načtení a příprava dat ────────────────────────────────────
|
||||||
|
kit_df_raw = pd.read_csv(kit_csv_file, encoding="utf-8")
|
||||||
|
cze = kit_df_raw[kit_df_raw["Country"] == "CZE"].copy()
|
||||||
|
|
||||||
|
def parse_kit_date(val):
|
||||||
|
if pd.isna(val):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.strptime(str(val).strip(), "%b %d, %Y")
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cze["Shipped Date"] = cze["Shipped Date"].apply(parse_kit_date)
|
||||||
|
cze["Expiration Date"] = cze["Expiration Date"].apply(parse_kit_date)
|
||||||
|
cze = cze.sort_values(["Site", "Kit Type", "Expiration Date"]).reset_index(drop=True)
|
||||||
|
|
||||||
|
today_dt = datetime.combine(date.today(), datetime.min.time())
|
||||||
|
|
||||||
|
def bucket(exp_date):
|
||||||
|
if exp_date is None:
|
||||||
|
return None
|
||||||
|
return "soon" if (exp_date - today_dt).days <= 30 else "ok"
|
||||||
|
|
||||||
|
cze["_bucket"] = cze["Expiration Date"].apply(bucket)
|
||||||
|
|
||||||
|
kit_order = sorted(cze["Kit Type"].unique(), key=lambda x: (str(x).lstrip("T-").zfill(5), str(x)))
|
||||||
|
kit_desc = cze.drop_duplicates("Kit Type").set_index("Kit Type")["Description"].to_dict()
|
||||||
|
kit_sites = sorted(cze["Site"].unique())
|
||||||
|
|
||||||
|
# ── Pomocná funkce pro souhrnné tabulky ───────────────────────────────────────
|
||||||
|
def write_summary_table(ws, current_row, title, rows_data, col_a_header):
|
||||||
|
for c in range(1, 5):
|
||||||
|
cell = ws.cell(row=current_row, column=c)
|
||||||
|
cell.fill = dark_blue_fill
|
||||||
|
cell.border = border
|
||||||
|
ws.cell(row=current_row, column=1, value=title).font = Font(name='Calibri', bold=True, size=12, color="FFFFFF")
|
||||||
|
ws.cell(row=current_row, column=1).alignment = Alignment(horizontal="left", vertical="center")
|
||||||
|
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=4)
|
||||||
|
ws.row_dimensions[current_row].height = 22
|
||||||
|
current_row += 1
|
||||||
|
|
||||||
|
for col_idx, (h, f) in enumerate(zip(
|
||||||
|
[col_a_header, "Description", "Expiruje do 30 dní", "Expiruje později"],
|
||||||
|
[header_fill, header_fill, orange_fill, green_fill]
|
||||||
|
), 1):
|
||||||
|
cell = ws.cell(row=current_row, column=col_idx, value=h)
|
||||||
|
cell.font = sum_header_font
|
||||||
|
cell.fill = f
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
ws.row_dimensions[current_row].height = 28
|
||||||
|
current_row += 1
|
||||||
|
|
||||||
|
totals = [0, 0]
|
||||||
|
for col_a, col_b, n_soon, n_ok in rows_data:
|
||||||
|
totals[0] += n_soon
|
||||||
|
totals[1] += n_ok
|
||||||
|
all_zero = (n_soon == 0 and n_ok == 0)
|
||||||
|
row_vals = [col_a, col_b, n_soon, n_ok]
|
||||||
|
row_fills = [None, None,
|
||||||
|
orange_fill if n_soon > 0 else None,
|
||||||
|
green_fill if n_ok > 0 else None]
|
||||||
|
for col_idx, (val, rfill) in enumerate(zip(row_vals, row_fills), 1):
|
||||||
|
cell = ws.cell(row=current_row, column=col_idx, value=val)
|
||||||
|
if col_idx >= 3 and val == 0:
|
||||||
|
cell.font = zero_red_font if all_zero else zero_font
|
||||||
|
else:
|
||||||
|
cell.font = data_font
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center" if col_idx >= 2 else "left", vertical="center")
|
||||||
|
if rfill:
|
||||||
|
cell.fill = rfill
|
||||||
|
current_row += 1
|
||||||
|
|
||||||
|
for col_idx, val in enumerate(["CELKEM", "", totals[0], totals[1]], 1):
|
||||||
|
cell = ws.cell(row=current_row, column=col_idx, value=val)
|
||||||
|
cell.font = sum_total_font
|
||||||
|
cell.fill = total_fill
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center" if col_idx >= 2 else "left", vertical="center")
|
||||||
|
current_row += 2
|
||||||
|
return current_row
|
||||||
|
|
||||||
|
# ── List: Kit Inventory CZE ───────────────────────────────────────────────────
|
||||||
|
kit_ws = out_wb.create_sheet("Kit Inventory CZE")
|
||||||
|
|
||||||
|
listing_columns = [
|
||||||
|
("Project No.", 14),
|
||||||
|
("Region", 10),
|
||||||
|
("Country", 10),
|
||||||
|
("Site", 38),
|
||||||
|
("Kit Type", 12),
|
||||||
|
("Description", 22),
|
||||||
|
("Accession", 18),
|
||||||
|
("Shipped Date", 16),
|
||||||
|
("Expiration Date", 16),
|
||||||
|
("Days to Expiration", 20),
|
||||||
|
]
|
||||||
|
|
||||||
|
for col_idx, (hdr, width) in enumerate(listing_columns, 1):
|
||||||
|
cell = kit_ws.cell(row=1, column=col_idx, value=hdr)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
kit_ws.column_dimensions[get_column_letter(col_idx)].width = width
|
||||||
|
|
||||||
|
kit_ws.row_dimensions[1].height = 30
|
||||||
|
kit_ws.freeze_panes = "A2"
|
||||||
|
|
||||||
|
for row_idx, row in enumerate(cze.itertuples(index=False), 2):
|
||||||
|
days = row[9]
|
||||||
|
for col_idx, (col_name, _) in enumerate(listing_columns, 1):
|
||||||
|
value = row[col_idx - 1]
|
||||||
|
cell = kit_ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
|
cell.font = data_font
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||||
|
if col_name in ("Shipped Date", "Expiration Date") and value is not None:
|
||||||
|
cell.number_format = "DD-MMM-YYYY"
|
||||||
|
if col_name == "Days to Expiration":
|
||||||
|
cell.fill = exp_fill if (pd.notna(days) and days <= 60) else ok_fill
|
||||||
|
|
||||||
|
kit_ws.auto_filter.ref = f"A1:{get_column_letter(len(listing_columns))}1"
|
||||||
|
|
||||||
|
# ── List: Přehled po centrech ─────────────────────────────────────────────────
|
||||||
|
ctr_ws = out_wb.create_sheet("Přehled po centrech")
|
||||||
|
ctr_ws.column_dimensions["A"].width = 22
|
||||||
|
ctr_ws.column_dimensions["B"].width = 24
|
||||||
|
ctr_ws.column_dimensions["C"].width = 22
|
||||||
|
ctr_ws.column_dimensions["D"].width = 20
|
||||||
|
|
||||||
|
current_row = 1
|
||||||
|
for site in kit_sites:
|
||||||
|
site_df = cze[cze["Site"] == site]
|
||||||
|
rows_data = []
|
||||||
|
for kit in kit_order:
|
||||||
|
desc = kit_desc.get(kit, "")
|
||||||
|
kit_site_df = site_df[site_df["Kit Type"] == kit]
|
||||||
|
n_soon = (kit_site_df["_bucket"] == "soon").sum()
|
||||||
|
n_ok = (kit_site_df["_bucket"] == "ok").sum()
|
||||||
|
rows_data.append((f"{kit} — {desc}", desc, n_soon, n_ok))
|
||||||
|
current_row = write_summary_table(ctr_ws, current_row, site, rows_data, "Kit Type")
|
||||||
|
|
||||||
|
# ── List: Přehled po typech kitů ──────────────────────────────────────────────
|
||||||
|
sum_ws = out_wb.create_sheet("Přehled po typech")
|
||||||
|
sum_ws.column_dimensions["A"].width = 38
|
||||||
|
sum_ws.column_dimensions["B"].width = 22
|
||||||
|
sum_ws.column_dimensions["C"].width = 22
|
||||||
|
sum_ws.column_dimensions["D"].width = 20
|
||||||
|
|
||||||
|
current_row = 1
|
||||||
|
for kit in kit_order:
|
||||||
|
desc = kit_desc.get(kit, "")
|
||||||
|
kit_df = cze[cze["Kit Type"] == kit]
|
||||||
|
rows_data = []
|
||||||
|
for site in sorted(kit_df["Site"].unique()):
|
||||||
|
site_df = kit_df[kit_df["Site"] == site]
|
||||||
|
n_soon = (site_df["_bucket"] == "soon").sum()
|
||||||
|
n_ok = (site_df["_bucket"] == "ok").sum()
|
||||||
|
rows_data.append((site, desc, n_soon, n_ok))
|
||||||
|
current_row = write_summary_table(sum_ws, current_row, f"Kit Type {kit} — {desc}", rows_data, "Centrum")
|
||||||
|
|
||||||
|
# ── List: eQueries ───────────────────────────────────────────────────────────
|
||||||
|
eq_df = pd.read_csv(eq_csv_file, encoding="utf-8")
|
||||||
|
eq_cze = eq_df[eq_df["Country"] == "CZECH REPUBLIC"].copy()
|
||||||
|
|
||||||
|
status_order = {"Open": 0, "Response Received": 1, "Closed": 2}
|
||||||
|
eq_cze["_status_order"] = eq_cze["Status"].map(status_order).fillna(99)
|
||||||
|
eq_cze = eq_cze.sort_values(["_status_order", "Site"]).reset_index(drop=True)
|
||||||
|
|
||||||
|
def parse_eq_date(val):
|
||||||
|
if pd.isna(val):
|
||||||
|
return None
|
||||||
|
for fmt in ("%b %d, %Y %I:%M %p", "%b %d, %Y %I:%M %p"):
|
||||||
|
try:
|
||||||
|
return datetime.strptime(str(val).strip(), fmt)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return datetime.strptime(str(val).strip().split(" 12:00")[0], "%b %d, %Y")
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
eq_ws = out_wb.create_sheet("eQueries")
|
||||||
|
|
||||||
|
eq_columns = [
|
||||||
|
("Site", 36),
|
||||||
|
("Subject", 14),
|
||||||
|
("Visit", 20),
|
||||||
|
("Visit Collection Date", 20),
|
||||||
|
("Accession", 16),
|
||||||
|
("eQueryId", 12),
|
||||||
|
("Issue Type", 30),
|
||||||
|
("Status", 18),
|
||||||
|
("Create Date", 20),
|
||||||
|
("Response Date Time", 20),
|
||||||
|
("Time Before Response", 20),
|
||||||
|
("User Name", 20),
|
||||||
|
]
|
||||||
|
|
||||||
|
status_fills = {
|
||||||
|
"Open": PatternFill("solid", fgColor="FFE7E7"),
|
||||||
|
"Response Received": PatternFill("solid", fgColor="FFF2CC"),
|
||||||
|
"Closed": PatternFill("solid", fgColor="E2EFDA"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for col_idx, (hdr, width) in enumerate(eq_columns, 1):
|
||||||
|
cell = eq_ws.cell(row=1, column=col_idx, value=hdr)
|
||||||
|
cell.font = header_font
|
||||||
|
cell.fill = header_fill
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||||
|
eq_ws.column_dimensions[get_column_letter(col_idx)].width = width
|
||||||
|
|
||||||
|
eq_ws.row_dimensions[1].height = 30
|
||||||
|
eq_ws.freeze_panes = "A2"
|
||||||
|
|
||||||
|
for row_idx, row in enumerate(eq_cze.itertuples(index=False), 2):
|
||||||
|
status = row[eq_cze.columns.get_loc("Status")]
|
||||||
|
rfill = status_fills.get(status)
|
||||||
|
for col_idx, (col_name, _) in enumerate(eq_columns, 1):
|
||||||
|
value = row[eq_cze.columns.get_loc(col_name)]
|
||||||
|
if col_name in ("Visit Collection Date", "Create Date", "Response Date Time"):
|
||||||
|
value = parse_eq_date(value)
|
||||||
|
cell = eq_ws.cell(row=row_idx, column=col_idx, value=value)
|
||||||
|
cell.font = data_font
|
||||||
|
cell.border = border
|
||||||
|
cell.alignment = Alignment(horizontal="center" if col_idx > 1 else "left", vertical="center")
|
||||||
|
if col_name in ("Visit Collection Date", "Create Date", "Response Date Time") and value:
|
||||||
|
cell.number_format = "DD-MMM-YYYY HH:MM"
|
||||||
|
if rfill:
|
||||||
|
cell.fill = rfill
|
||||||
|
|
||||||
|
eq_ws.auto_filter.ref = f"A1:{get_column_letter(len(eq_columns))}1"
|
||||||
|
|
||||||
out_wb.save(out_path)
|
out_wb.save(out_path)
|
||||||
print(f"Saved: {out_path}")
|
print(f"Saved: {out_path}")
|
||||||
print(f"Patients with BXSCR: {len(bxscr_patients)}")
|
print(f"Patients with BXSCR: {len(bxscr_patients)}, All unique patients: {len(all_patients)}")
|
||||||
print(f"All unique patients: {len(all_patients)}")
|
print(f"CZE kit rows: {len(cze)}, Kit types: {len(kit_order)}, Sites: {len(kit_sites)}")
|
||||||
|
print(f"CZE eQueries: {len(eq_cze)} (Open: {(eq_cze['Status']=='Open').sum()}, Response Received: {(eq_cze['Status']=='Response Received').sum()}, Closed: {(eq_cze['Status']=='Closed').sum()})")
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user