Initial commit — clean history (removed large test files, browser profiles, Medidata/Clario downloads)
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import mysql.connector
|
||||
import db_config
|
||||
|
||||
conn = mysql.connector.connect(
|
||||
host=db_config.DB_HOST, port=db_config.DB_PORT,
|
||||
user=db_config.DB_USER, password=db_config.DB_PASSWORD,
|
||||
database=db_config.DB_NAME
|
||||
)
|
||||
c = conn.cursor()
|
||||
|
||||
# Přidat report_type do iwrs_import (pokud ještě neexistuje)
|
||||
try:
|
||||
c.execute("""ALTER TABLE iwrs_import
|
||||
ADD COLUMN report_type VARCHAR(20) NOT NULL DEFAULT 'patients'
|
||||
AFTER source_file""")
|
||||
print("ALTER TABLE iwrs_import OK — report_type přidán")
|
||||
except mysql.connector.errors.DatabaseError as e:
|
||||
if "Duplicate column" in str(e):
|
||||
print("report_type již existuje — přeskočeno")
|
||||
else:
|
||||
raise
|
||||
|
||||
stmts = [
|
||||
(
|
||||
"iwrs_shipments",
|
||||
"""CREATE TABLE IF NOT EXISTS iwrs_shipments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
import_id INT NOT NULL,
|
||||
study VARCHAR(20) NOT NULL,
|
||||
shipment_id VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(50),
|
||||
type VARCHAR(30),
|
||||
ship_from VARCHAR(50),
|
||||
ship_to_site VARCHAR(50),
|
||||
location VARCHAR(50),
|
||||
request_date DATE,
|
||||
shipped_date DATE,
|
||||
received_date DATE,
|
||||
received_by VARCHAR(100),
|
||||
delivered_date_utc DATE,
|
||||
delivery_recipient VARCHAR(100),
|
||||
delivery_details VARCHAR(200),
|
||||
cancelled_date DATE,
|
||||
total_medication_ids SMALLINT,
|
||||
tracking_no VARCHAR(100),
|
||||
shipping_category VARCHAR(50),
|
||||
expected_arrival DATE,
|
||||
FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id),
|
||||
INDEX idx_import (import_id),
|
||||
INDEX idx_study_shipment (study, shipment_id)
|
||||
)"""
|
||||
),
|
||||
(
|
||||
"iwrs_shipment_items",
|
||||
"""CREATE TABLE IF NOT EXISTS iwrs_shipment_items (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
import_id INT NOT NULL,
|
||||
study VARCHAR(20) NOT NULL,
|
||||
shipment_id VARCHAR(20) NOT NULL,
|
||||
destination_location VARCHAR(50),
|
||||
shipment_status VARCHAR(50),
|
||||
shipment_type VARCHAR(30),
|
||||
destination_site VARCHAR(50),
|
||||
investigator VARCHAR(100),
|
||||
medication_description VARCHAR(200),
|
||||
medication_type VARCHAR(50),
|
||||
medication_id VARCHAR(20),
|
||||
packaged_lot_no VARCHAR(50),
|
||||
packaged_lot_description VARCHAR(100),
|
||||
container_id VARCHAR(50),
|
||||
quantity SMALLINT,
|
||||
expiration_date DATE,
|
||||
item_status VARCHAR(50),
|
||||
FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id),
|
||||
INDEX idx_import (import_id),
|
||||
INDEX idx_med_id (medication_id)
|
||||
)"""
|
||||
),
|
||||
(
|
||||
"iwrs_inventory",
|
||||
"""CREATE TABLE IF NOT EXISTS iwrs_inventory (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
import_id INT NOT NULL,
|
||||
study VARCHAR(20) NOT NULL,
|
||||
site VARCHAR(50),
|
||||
investigator VARCHAR(100),
|
||||
location VARCHAR(50),
|
||||
medication_id VARCHAR(20),
|
||||
packaged_lot_no VARCHAR(50),
|
||||
original_expiration_date DATE,
|
||||
expiration_date DATE,
|
||||
received_date DATE,
|
||||
receipt_user VARCHAR(100),
|
||||
subject_identifier VARCHAR(20),
|
||||
quantity_assigned SMALLINT,
|
||||
irt_transaction VARCHAR(100),
|
||||
date_assigned DATE,
|
||||
assignment_user VARCHAR(100),
|
||||
dispensation_status VARCHAR(50),
|
||||
dispensing_date DATE,
|
||||
quantity_dispensed SMALLINT,
|
||||
dispensing_user VARCHAR(100),
|
||||
quantity_returned SMALLINT,
|
||||
date_returned DATE,
|
||||
return_user VARCHAR(100),
|
||||
FOREIGN KEY (import_id) REFERENCES iwrs_import(import_id),
|
||||
INDEX idx_import (import_id),
|
||||
INDEX idx_site (study, site)
|
||||
)"""
|
||||
),
|
||||
(
|
||||
"iwrs_destruction",
|
||||
"""CREATE TABLE IF NOT EXISTS iwrs_destruction (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
study VARCHAR(20) NOT NULL,
|
||||
site_id VARCHAR(50),
|
||||
investigator VARCHAR(100),
|
||||
location VARCHAR(50),
|
||||
basket_id VARCHAR(20) NOT NULL,
|
||||
destruction_date DATE,
|
||||
medication_description VARCHAR(200),
|
||||
medication_id VARCHAR(20),
|
||||
packaged_lot_description VARCHAR(100),
|
||||
comments VARCHAR(500),
|
||||
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uq_destruction (study, basket_id, medication_id),
|
||||
INDEX idx_study_basket (study, basket_id)
|
||||
)"""
|
||||
),
|
||||
]
|
||||
|
||||
for name, sql in stmts:
|
||||
c.execute(sql)
|
||||
print(f"OK: {name}")
|
||||
|
||||
conn.commit()
|
||||
c.close()
|
||||
conn.close()
|
||||
print("\nVšechny tabulky připraveny.")
|
||||
@@ -0,0 +1,364 @@
|
||||
import sys
|
||||
import os
|
||||
import mysql.connector
|
||||
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
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
import db_config
|
||||
|
||||
STUDY = "42847922MDD3003"
|
||||
# STUDY = "77242113UCO3001"
|
||||
|
||||
BASE_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUTPUT_DIR = BASE_DIR / "output"
|
||||
OUTPUT_FILE = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} {STUDY} CZ IWRS overview.xlsx"
|
||||
|
||||
DATE_COLUMNS = {
|
||||
"Orig Exp Date", "Exp Date", "Rcv Date",
|
||||
"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,
|
||||
"Max Visit Date": 16,
|
||||
}
|
||||
|
||||
# shipments sheet: kolík kde začínají detail sloupce (1-based, pro format_shipment_sheet)
|
||||
N_SHIP_COLS = 9
|
||||
|
||||
|
||||
# ── DB ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_conn():
|
||||
return mysql.connector.connect(
|
||||
host=db_config.DB_HOST, port=db_config.DB_PORT,
|
||||
user=db_config.DB_USER, password=db_config.DB_PASSWORD,
|
||||
database=db_config.DB_NAME,
|
||||
)
|
||||
|
||||
|
||||
def get_latest_import_id(cursor, study):
|
||||
cursor.execute(
|
||||
"SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'",
|
||||
(study,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
mid = row["mid"]
|
||||
if mid is None:
|
||||
raise RuntimeError(f"Žádná data v MySQL pro studii {study}")
|
||||
return mid
|
||||
|
||||
|
||||
# ── Načítání dat z MySQL ──────────────────────────────────────────────────────
|
||||
|
||||
def load_inventory(cursor, study, import_id):
|
||||
"""
|
||||
Vrátí DataFrame s inventory + destruction join.
|
||||
Sloupce jsou rovnou přejmenované pro downstream funkce.
|
||||
"""
|
||||
sql = """
|
||||
SELECT
|
||||
i.site AS Site,
|
||||
i.medication_id AS `Med ID`,
|
||||
i.packaged_lot_no AS `Lot No.`,
|
||||
i.original_expiration_date AS `Orig Exp Date`,
|
||||
i.expiration_date AS `Exp Date`,
|
||||
i.received_date AS `Rcv Date`,
|
||||
i.receipt_user AS `Rcpt User`,
|
||||
i.subject_identifier AS `Subject ID`,
|
||||
i.quantity_assigned AS `Qty Asgn`,
|
||||
i.irt_transaction AS `IRT Tx`,
|
||||
i.date_assigned AS `Date Asgn`,
|
||||
i.assignment_user AS `Asgn User`,
|
||||
i.dispensation_status AS `Disp Status`,
|
||||
i.dispensing_date AS `Disp Date`,
|
||||
i.quantity_dispensed AS `Qty Disp`,
|
||||
i.dispensing_user AS `Disp User`,
|
||||
i.quantity_returned AS `Qty Ret`,
|
||||
i.date_returned AS `Date Ret`,
|
||||
i.return_user AS `Ret User`,
|
||||
d.destruction_date AS Destroyed,
|
||||
d.basket_id AS `Basket No.`
|
||||
FROM iwrs_inventory i
|
||||
LEFT JOIN (
|
||||
SELECT medication_id,
|
||||
ANY_VALUE(basket_id) AS basket_id,
|
||||
ANY_VALUE(destruction_date) AS destruction_date
|
||||
FROM iwrs_destruction
|
||||
WHERE study = %s
|
||||
GROUP BY medication_id
|
||||
) d ON d.medication_id = i.medication_id
|
||||
WHERE i.import_id = %s
|
||||
AND i.study = %s
|
||||
ORDER BY i.site, i.received_date, i.medication_id
|
||||
"""
|
||||
cursor.execute(sql, (study, import_id, study))
|
||||
rows = cursor.fetchall()
|
||||
df = pd.DataFrame(rows)
|
||||
for col in DATE_COLUMNS:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_datetime(df[col], errors="coerce")
|
||||
print(f" Inventory: {len(df)} kitu")
|
||||
return df
|
||||
|
||||
|
||||
def load_shipments(cursor, study, import_id):
|
||||
"""
|
||||
Vrátí DataFrame se spojenými shipments + items.
|
||||
"""
|
||||
sql = """
|
||||
SELECT
|
||||
s.shipment_id AS `Shipment ID`,
|
||||
s.status AS `IRT Shipment Status`,
|
||||
s.type AS Type,
|
||||
s.ship_from AS `Shipment From`,
|
||||
s.ship_to_site AS `Ship To:`,
|
||||
s.request_date AS `Request Date`,
|
||||
s.received_date AS `Received Date`,
|
||||
s.received_by AS `Received by`,
|
||||
s.expected_arrival AS `Expected Arrival`,
|
||||
i.investigator AS Investigator,
|
||||
i.medication_description AS `Medication Description`,
|
||||
i.medication_id AS `Medication ID`,
|
||||
i.packaged_lot_no AS `Packaged Lot number`,
|
||||
i.expiration_date AS `Expiration Date`,
|
||||
i.item_status AS Status
|
||||
FROM iwrs_shipments s
|
||||
JOIN iwrs_shipment_items i
|
||||
ON i.study = s.study
|
||||
AND i.shipment_id = s.shipment_id
|
||||
AND i.import_id = %s
|
||||
WHERE s.import_id = %s
|
||||
AND s.study = %s
|
||||
ORDER BY s.ship_to_site, s.shipment_id, i.medication_id
|
||||
"""
|
||||
cursor.execute(sql, (import_id, import_id, study))
|
||||
rows = cursor.fetchall()
|
||||
df = pd.DataFrame(rows)
|
||||
for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"):
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_datetime(df[col], errors="coerce")
|
||||
print(f" Shipments: {df['Shipment ID'].nunique() if len(df) else 0} zásilek, {len(df)} kitu")
|
||||
return df
|
||||
|
||||
|
||||
# ── Odvozené sheety ───────────────────────────────────────────────────────────
|
||||
|
||||
def build_site_summary(shipments_df):
|
||||
STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"]
|
||||
pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0)
|
||||
for s in STATUS_COLS:
|
||||
if s not in pivot.columns:
|
||||
pivot[s] = 0
|
||||
pivot = (
|
||||
pivot[STATUS_COLS]
|
||||
.reset_index()
|
||||
.rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"})
|
||||
.sort_values("Site")
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1)
|
||||
print(f" Site Summary: {len(pivot)} center")
|
||||
return pivot
|
||||
|
||||
|
||||
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"].fillna("").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"].fillna("").str.upper() == "NOT DISPENSED"))
|
||||
)
|
||||
filtered = (
|
||||
df[mask]
|
||||
.copy()
|
||||
.sort_values(["Site", "Date Ret"], ascending=[True, True])
|
||||
.drop(columns=["Destroyed", "Basket No."])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
print(f" Kits for destruction: {len(filtered)}")
|
||||
return filtered
|
||||
|
||||
|
||||
# ── Formátování ───────────────────────────────────────────────────────────────
|
||||
|
||||
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)
|
||||
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]]
|
||||
|
||||
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 hi_fill and col_name == highlight_col:
|
||||
cell.fill = hi_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"
|
||||
|
||||
|
||||
def format_shipment_sheet(ws, header_color_ship, header_color_detail, n_ship_cols):
|
||||
thin = Side(style="thin", color="000000")
|
||||
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10)
|
||||
dfont = Font(name="Arial", size=10)
|
||||
fill_ship = PatternFill("solid", start_color=header_color_ship)
|
||||
fill_detail = PatternFill("solid", start_color=header_color_detail)
|
||||
|
||||
for cell in ws[1]:
|
||||
cell.fill = fill_ship if cell.column <= n_ship_cols else fill_detail
|
||||
cell.font = hfont
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
cell.border = border
|
||||
ws.column_dimensions[get_column_letter(cell.column)].width = min(
|
||||
len(str(cell.value or "")) + 4, 35
|
||||
)
|
||||
ws.row_dimensions[1].height = 30
|
||||
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
|
||||
for cell in row:
|
||||
cell.font = dfont
|
||||
cell.border = border
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"):
|
||||
cell.number_format = "DD-MMM-YYYY"
|
||||
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
print(f"\nNačítám data z MySQL pro {STUDY}...")
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
import_id = get_latest_import_id(cursor, STUDY)
|
||||
print(f" import_id = {import_id}")
|
||||
|
||||
df = load_inventory(cursor, STUDY, import_id)
|
||||
shipments_df = load_shipments(cursor, STUDY, import_id)
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
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)
|
||||
site_summary_df = build_site_summary(shipments_df)
|
||||
|
||||
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")
|
||||
shipments_df.to_excel( writer, index=False, sheet_name="Shipments")
|
||||
site_summary_df.to_excel( writer, index=False, sheet_name="Site Summary")
|
||||
|
||||
wb = load_workbook(OUTPUT_FILE)
|
||||
|
||||
ws_main = wb["CountryMedicationOverview"]
|
||||
format_sheet(ws_main, header_color="1F4E79")
|
||||
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")
|
||||
format_shipment_sheet(wb["Shipments"], "1F4E79", "375623", N_SHIP_COLS)
|
||||
format_sheet(wb["Site Summary"], header_color="1F4E79")
|
||||
|
||||
wb.save(OUTPUT_FILE)
|
||||
print(f"\nUloženo: {OUTPUT_FILE} ({len(df)} řádků, sheety: {wb.sheetnames})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,205 @@
|
||||
import sys
|
||||
import os
|
||||
import mysql.connector
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
from datetime import date
|
||||
import pandas as pd
|
||||
|
||||
# db_config.py je v nadřazeném adresáři (Drugs/)
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
import db_config
|
||||
|
||||
STUDY = "77242113UCO3001"
|
||||
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def get_conn():
|
||||
return mysql.connector.connect(
|
||||
host=db_config.DB_HOST, port=db_config.DB_PORT,
|
||||
user=db_config.DB_USER, password=db_config.DB_PASSWORD,
|
||||
database=db_config.DB_NAME,
|
||||
)
|
||||
|
||||
|
||||
def load_data(study):
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# nejnovější import_id pro danou studii
|
||||
cursor.execute(
|
||||
"SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'",
|
||||
(study,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
import_id = row["mid"]
|
||||
if import_id is None:
|
||||
raise RuntimeError(f"Žádná data v MySQL pro studii {study}")
|
||||
print(f" import_id = {import_id}")
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
s.shipment_id,
|
||||
s.status AS irt_shipment_status,
|
||||
s.type,
|
||||
s.ship_from AS shipment_from,
|
||||
s.ship_to_site AS ship_to,
|
||||
s.request_date,
|
||||
s.received_date,
|
||||
s.received_by,
|
||||
s.expected_arrival,
|
||||
i.investigator,
|
||||
i.medication_description,
|
||||
i.medication_id,
|
||||
i.packaged_lot_no,
|
||||
i.expiration_date,
|
||||
i.item_status AS status
|
||||
FROM iwrs_shipments s
|
||||
JOIN iwrs_shipment_items i
|
||||
ON i.study = s.study
|
||||
AND i.shipment_id = s.shipment_id
|
||||
AND i.import_id = %s
|
||||
WHERE s.import_id = %s
|
||||
AND s.study = %s
|
||||
ORDER BY s.ship_to_site, s.shipment_id, i.medication_id
|
||||
"""
|
||||
cursor.execute(sql, (import_id, import_id, study))
|
||||
rows = cursor.fetchall()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print(f" Načteno řádků: {len(rows)}")
|
||||
return rows
|
||||
|
||||
|
||||
# shipment sloupce (modrý header) / detail sloupce (zelený header)
|
||||
SHIP_COLS = [
|
||||
("shipment_id", "Shipment ID"),
|
||||
("irt_shipment_status","IRT Shipment Status"),
|
||||
("type", "Type"),
|
||||
("shipment_from", "Shipment From"),
|
||||
("ship_to", "Ship To:"),
|
||||
("request_date", "Request Date"),
|
||||
("received_date", "Received Date"),
|
||||
("received_by", "Received by"),
|
||||
("expected_arrival", "Expected Arrival"),
|
||||
]
|
||||
|
||||
DETAIL_COLS = [
|
||||
("investigator", "Investigator"),
|
||||
("medication_description", "Medication Description"),
|
||||
("medication_id", "Medication ID"),
|
||||
("packaged_lot_no", "Packaged Lot number"),
|
||||
("expiration_date", "Expiration Date"),
|
||||
("status", "Status"),
|
||||
]
|
||||
|
||||
ALL_COLS = SHIP_COLS + DETAIL_COLS
|
||||
N_SHIP_COLS = len(SHIP_COLS)
|
||||
|
||||
HEADER_FILL_SHIP = PatternFill("solid", fgColor="1F4E79")
|
||||
HEADER_FILL_DETAIL = PatternFill("solid", fgColor="375623")
|
||||
HEADER_FONT = Font(name="Arial", bold=True, color="FFFFFF", size=10)
|
||||
DATA_FONT = Font(name="Arial", size=10)
|
||||
THIN_BORDER = Border(
|
||||
left=Side(style="thin", color="BFBFBF"),
|
||||
right=Side(style="thin", color="BFBFBF"),
|
||||
bottom=Side(style="thin", color="BFBFBF"),
|
||||
)
|
||||
|
||||
|
||||
def write_shipments_sheet(wb, rows):
|
||||
ws = wb.active
|
||||
ws.title = "Shipments"
|
||||
|
||||
# záhlaví
|
||||
for ci, (_, label) in enumerate(ALL_COLS, 1):
|
||||
cell = ws.cell(row=1, column=ci, value=label)
|
||||
cell.font = HEADER_FONT
|
||||
cell.fill = HEADER_FILL_SHIP if ci <= N_SHIP_COLS else HEADER_FILL_DETAIL
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
cell.border = THIN_BORDER
|
||||
ws.row_dimensions[1].height = 30
|
||||
|
||||
# data
|
||||
for ri, row in enumerate(rows, 2):
|
||||
for ci, (key, _) in enumerate(ALL_COLS, 1):
|
||||
val = row[key]
|
||||
cell = ws.cell(row=ri, column=ci, value=val)
|
||||
cell.font = DATA_FONT
|
||||
cell.border = THIN_BORDER
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
if isinstance(val, date):
|
||||
cell.number_format = "DD-MMM-YYYY"
|
||||
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
# šířky sloupců
|
||||
for ci, (key, label) in enumerate(ALL_COLS, 1):
|
||||
vals = [label] + [str(r[key]) for r in rows if r[key] is not None]
|
||||
ws.column_dimensions[get_column_letter(ci)].width = min(
|
||||
max((len(v) for v in vals), default=10) + 2, 35
|
||||
)
|
||||
|
||||
|
||||
def write_summary_sheet(wb, rows):
|
||||
STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"]
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
pivot = df.groupby("ship_to")["status"].value_counts().unstack(fill_value=0)
|
||||
for s in STATUS_COLS:
|
||||
if s not in pivot.columns:
|
||||
pivot[s] = 0
|
||||
pivot = (
|
||||
pivot[STATUS_COLS]
|
||||
.reset_index()
|
||||
.rename(columns={"ship_to": "Site", "Returned by Subject": "Returned"})
|
||||
.sort_values("Site")
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1)
|
||||
|
||||
ws = wb.create_sheet("Site Summary")
|
||||
s_cols = ["Site", "Available", "Assigned", "Dispensed", "Returned", "Total"]
|
||||
|
||||
for ci, col in enumerate(s_cols, 1):
|
||||
cell = ws.cell(row=1, column=ci, value=col)
|
||||
cell.font = HEADER_FONT
|
||||
cell.fill = PatternFill("solid", fgColor="1F4E79")
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
cell.border = THIN_BORDER
|
||||
ws.row_dimensions[1].height = 25
|
||||
|
||||
for ri, (_, row) in enumerate(pivot.iterrows(), 2):
|
||||
for ci, col in enumerate(s_cols, 1):
|
||||
cell = ws.cell(row=ri, column=ci, value=row[col])
|
||||
cell.font = DATA_FONT
|
||||
cell.border = THIN_BORDER
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
|
||||
for ci, col in enumerate(s_cols, 1):
|
||||
vals = [col] + [str(pivot.iloc[r][col]) for r in range(len(pivot))]
|
||||
ws.column_dimensions[get_column_letter(ci)].width = min(
|
||||
max(len(v) for v in vals) + 4, 35
|
||||
)
|
||||
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
|
||||
def build_report():
|
||||
print(f"\nNačítám data z MySQL pro {STUDY}...")
|
||||
rows = load_data(STUDY)
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
write_shipments_sheet(wb, rows)
|
||||
write_summary_sheet(wb, rows)
|
||||
|
||||
outfile = os.path.join(OUTPUT_DIR, f"{date.today()} {STUDY} CZ Shipments.xlsx")
|
||||
wb.save(outfile)
|
||||
print(f"\nUloženo -> {outfile}")
|
||||
|
||||
|
||||
build_report()
|
||||
@@ -0,0 +1,393 @@
|
||||
import sys
|
||||
import os
|
||||
import mysql.connector
|
||||
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
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
import db_config
|
||||
|
||||
STUDIES = [
|
||||
("77242113UCO3001", "UCO"),
|
||||
("42847922MDD3003", "MDD"),
|
||||
]
|
||||
|
||||
BASE_DIR = Path(os.path.dirname(os.path.abspath(__file__)))
|
||||
OUTPUT_DIR = BASE_DIR / "output"
|
||||
|
||||
DATE_COLUMNS = {
|
||||
"Orig Exp Date", "Exp Date", "Rcv Date",
|
||||
"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,
|
||||
"Max Visit Date": 16,
|
||||
}
|
||||
|
||||
N_SHIP_COLS = 9 # počet shipment sloupců (modrý header v Shipments sheetu)
|
||||
|
||||
|
||||
# ── DB ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_conn():
|
||||
return mysql.connector.connect(
|
||||
host=db_config.DB_HOST, port=db_config.DB_PORT,
|
||||
user=db_config.DB_USER, password=db_config.DB_PASSWORD,
|
||||
database=db_config.DB_NAME,
|
||||
)
|
||||
|
||||
|
||||
def get_latest_import_id(cursor, study):
|
||||
cursor.execute(
|
||||
"SELECT MAX(import_id) AS mid FROM iwrs_import WHERE study=%s AND report_type='drugs'",
|
||||
(study,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
mid = row["mid"]
|
||||
if mid is None:
|
||||
raise RuntimeError(f"Žádná data v MySQL pro studii {study}")
|
||||
return mid
|
||||
|
||||
|
||||
# ── Načítání dat ──────────────────────────────────────────────────────────────
|
||||
|
||||
def load_inventory(cursor, study, import_id):
|
||||
sql = """
|
||||
SELECT
|
||||
i.site AS Site,
|
||||
i.medication_id AS `Med ID`,
|
||||
i.packaged_lot_no AS `Lot No.`,
|
||||
i.original_expiration_date AS `Orig Exp Date`,
|
||||
i.expiration_date AS `Exp Date`,
|
||||
i.received_date AS `Rcv Date`,
|
||||
i.receipt_user AS `Rcpt User`,
|
||||
i.subject_identifier AS `Subject ID`,
|
||||
i.quantity_assigned AS `Qty Asgn`,
|
||||
i.irt_transaction AS `IRT Tx`,
|
||||
i.date_assigned AS `Date Asgn`,
|
||||
i.assignment_user AS `Asgn User`,
|
||||
i.dispensation_status AS `Disp Status`,
|
||||
i.dispensing_date AS `Disp Date`,
|
||||
i.quantity_dispensed AS `Qty Disp`,
|
||||
i.dispensing_user AS `Disp User`,
|
||||
i.quantity_returned AS `Qty Ret`,
|
||||
i.date_returned AS `Date Ret`,
|
||||
i.return_user AS `Ret User`,
|
||||
d.destruction_date AS Destroyed,
|
||||
d.basket_id AS `Basket No.`
|
||||
FROM iwrs_inventory i
|
||||
LEFT JOIN (
|
||||
SELECT medication_id,
|
||||
ANY_VALUE(basket_id) AS basket_id,
|
||||
ANY_VALUE(destruction_date) AS destruction_date
|
||||
FROM iwrs_destruction
|
||||
WHERE study = %s
|
||||
GROUP BY medication_id
|
||||
) d ON d.medication_id = i.medication_id
|
||||
WHERE i.import_id = %s
|
||||
AND i.study = %s
|
||||
ORDER BY i.site, i.received_date, i.medication_id
|
||||
"""
|
||||
cursor.execute(sql, (study, import_id, study))
|
||||
rows = cursor.fetchall()
|
||||
df = pd.DataFrame(rows)
|
||||
for col in DATE_COLUMNS:
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_datetime(df[col], errors="coerce")
|
||||
print(f" Inventory: {len(df)} kitu")
|
||||
return df
|
||||
|
||||
|
||||
def load_shipments(cursor, study, import_id):
|
||||
sql = """
|
||||
SELECT
|
||||
s.shipment_id AS `Shipment ID`,
|
||||
s.status AS `IRT Shipment Status`,
|
||||
s.type AS Type,
|
||||
s.ship_from AS `Shipment From`,
|
||||
s.ship_to_site AS `Ship To:`,
|
||||
s.request_date AS `Request Date`,
|
||||
s.received_date AS `Received Date`,
|
||||
s.received_by AS `Received by`,
|
||||
s.expected_arrival AS `Expected Arrival`,
|
||||
i.investigator AS Investigator,
|
||||
i.medication_description AS `Medication Description`,
|
||||
i.medication_id AS `Medication ID`,
|
||||
i.packaged_lot_no AS `Packaged Lot number`,
|
||||
i.expiration_date AS `Expiration Date`,
|
||||
i.item_status AS Status
|
||||
FROM iwrs_shipments s
|
||||
JOIN iwrs_shipment_items i
|
||||
ON i.study = s.study
|
||||
AND i.shipment_id = s.shipment_id
|
||||
AND i.import_id = %s
|
||||
WHERE s.import_id = %s
|
||||
AND s.study = %s
|
||||
ORDER BY s.ship_to_site, s.shipment_id, i.medication_id
|
||||
"""
|
||||
cursor.execute(sql, (import_id, import_id, study))
|
||||
rows = cursor.fetchall()
|
||||
df = pd.DataFrame(rows)
|
||||
for col in ("Request Date", "Received Date", "Expiration Date", "Expected Arrival"):
|
||||
if col in df.columns:
|
||||
df[col] = pd.to_datetime(df[col], errors="coerce")
|
||||
n_ship = df["Shipment ID"].nunique() if len(df) else 0
|
||||
print(f" Shipments: {n_ship} zásilek, {len(df)} kitu")
|
||||
return df
|
||||
|
||||
|
||||
# ── Odvozené sheety ───────────────────────────────────────────────────────────
|
||||
|
||||
def build_site_summary(shipments_df):
|
||||
STATUS_COLS = ["Available", "Assigned", "Dispensed", "Returned by Subject"]
|
||||
pivot = shipments_df.groupby("Ship To:")["Status"].value_counts().unstack(fill_value=0)
|
||||
for s in STATUS_COLS:
|
||||
if s not in pivot.columns:
|
||||
pivot[s] = 0
|
||||
pivot = (
|
||||
pivot[STATUS_COLS]
|
||||
.reset_index()
|
||||
.rename(columns={"Ship To:": "Site", "Returned by Subject": "Returned"})
|
||||
.sort_values("Site")
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
pivot["Total"] = pivot[["Available", "Assigned", "Dispensed", "Returned"]].sum(axis=1)
|
||||
print(f" Site Summary: {len(pivot)} center")
|
||||
return pivot
|
||||
|
||||
|
||||
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)
|
||||
print(f" Expired: {len(filtered)}")
|
||||
return filtered
|
||||
|
||||
|
||||
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"].fillna("").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"].fillna("").str.upper() == "NOT DISPENSED"))
|
||||
)
|
||||
filtered = (
|
||||
df[mask]
|
||||
.copy()
|
||||
.sort_values(["Site", "Date Ret"], ascending=[True, True])
|
||||
.drop(columns=["Destroyed", "Basket No."])
|
||||
.reset_index(drop=True)
|
||||
)
|
||||
print(f" Kits for destruction: {len(filtered)}")
|
||||
return filtered
|
||||
|
||||
|
||||
# ── Formátování ───────────────────────────────────────────────────────────────
|
||||
|
||||
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)
|
||||
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]]
|
||||
|
||||
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 hi_fill and col_name == highlight_col:
|
||||
cell.fill = hi_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"
|
||||
|
||||
|
||||
def format_overview_sheet(ws):
|
||||
format_sheet(ws, header_color="1F4E79")
|
||||
new_col_fill = PatternFill("solid", start_color="E2EFDA")
|
||||
headers = [c.value for c in ws[1]]
|
||||
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
|
||||
if col_name in ("Destroyed", "Basket No."):
|
||||
cell.fill = new_col_fill
|
||||
|
||||
|
||||
def format_shipment_sheet(ws):
|
||||
thin = Side(style="thin", color="000000")
|
||||
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
hfont = Font(bold=True, color="FFFFFF", name="Arial", size=10)
|
||||
dfont = Font(name="Arial", size=10)
|
||||
fill_ship = PatternFill("solid", start_color="1F4E79")
|
||||
fill_detail = PatternFill("solid", start_color="375623")
|
||||
|
||||
for cell in ws[1]:
|
||||
cell.fill = fill_ship if cell.column <= N_SHIP_COLS else fill_detail
|
||||
cell.font = hfont
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
cell.border = border
|
||||
ws.column_dimensions[get_column_letter(cell.column)].width = min(
|
||||
len(str(cell.value or "")) + 4, 35
|
||||
)
|
||||
ws.row_dimensions[1].height = 30
|
||||
|
||||
for row in ws.iter_rows(min_row=2, max_row=ws.max_row):
|
||||
for cell in row:
|
||||
cell.font = dfont
|
||||
cell.border = border
|
||||
cell.alignment = Alignment(horizontal="center", vertical="center")
|
||||
if cell.value.__class__.__name__ in ("datetime", "date", "Timestamp"):
|
||||
cell.number_format = "DD-MMM-YYYY"
|
||||
|
||||
ws.auto_filter.ref = ws.dimensions
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
SHEETS_DEF = [
|
||||
("CountryMedicationOverview", "overview"),
|
||||
("Expired", "expired"),
|
||||
("Assigned not dispensed", "assigned"),
|
||||
("Not returned", "not_returned"),
|
||||
("Kits for destruction", "destruction"),
|
||||
("Shipments", "shipments"),
|
||||
("Site Summary", "site_summary"),
|
||||
]
|
||||
|
||||
FORMAT_MAP = {
|
||||
"overview": lambda ws: format_overview_sheet(ws),
|
||||
"expired": lambda ws: format_sheet(ws, "C00000", "Exp Date", "FFE0E0"),
|
||||
"assigned": lambda ws: format_sheet(ws, "833C00", "Subject ID", "FFF2CC"),
|
||||
"not_returned": lambda ws: format_sheet(ws, "375623", "Max Visit Date", "E2EFDA"),
|
||||
"destruction": lambda ws: format_sheet(ws, "595959"),
|
||||
"shipments": lambda ws: format_shipment_sheet(ws),
|
||||
"site_summary": lambda ws: format_sheet(ws, "1F4E79"),
|
||||
}
|
||||
|
||||
|
||||
def process_study(cursor, study):
|
||||
today = date.today().strftime("%d-%b-%Y")
|
||||
import_id = get_latest_import_id(cursor, study)
|
||||
print(f" import_id = {import_id}")
|
||||
|
||||
df = load_inventory(cursor, study, import_id)
|
||||
shipments_df = load_shipments(cursor, study, import_id)
|
||||
|
||||
expired_df = build_expired(df)
|
||||
assigned_df = build_assigned_not_dispensed(df)
|
||||
not_returned_df = build_not_returned(df)
|
||||
destruction_df = build_kits_for_destruction(df)
|
||||
site_summ_df = build_site_summary(shipments_df)
|
||||
|
||||
return [
|
||||
df, expired_df, assigned_df, not_returned_df,
|
||||
destruction_df, shipments_df, site_summ_df,
|
||||
]
|
||||
|
||||
|
||||
def save_study_report(study, data_frames):
|
||||
output_file = OUTPUT_DIR / f"{date.today().strftime('%Y-%m-%d')} {study} report.xlsx"
|
||||
|
||||
with pd.ExcelWriter(output_file, engine="openpyxl") as writer:
|
||||
for (sheet_name, _), df_sheet in zip(SHEETS_DEF, data_frames):
|
||||
df_sheet.to_excel(writer, index=False, sheet_name=sheet_name)
|
||||
|
||||
wb = load_workbook(output_file)
|
||||
for (sheet_name, fmt_key) in SHEETS_DEF:
|
||||
FORMAT_MAP[fmt_key](wb[sheet_name])
|
||||
wb.save(output_file)
|
||||
print(f" Uloženo: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
||||
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
for study, _ in STUDIES:
|
||||
print(f"\n{'='*55}")
|
||||
print(f"[{study}]")
|
||||
print(f"{'='*55}")
|
||||
try:
|
||||
data_frames = process_study(cursor, study)
|
||||
save_study_report(study, data_frames)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f" CHYBA: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print(f"\nHotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,76 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import os
|
||||
|
||||
# ── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://janssen.4gclinical.com"
|
||||
|
||||
EMAIL = "vbuzalka@its.jnj.com"
|
||||
PASSWORD = "Vlado123++-+"
|
||||
|
||||
# STUDY = "42847922MDD3003"
|
||||
STUDY = "77242113UCO3001"
|
||||
|
||||
OUTPUT_DIR = f"xls_ip_destruction_{STUDY}"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run(page, study):
|
||||
output_dir = f"xls_ip_destruction_{study}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
page.goto(f"{BASE_URL}/report/ip_destruction_form")
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
page.locator('input[placeholder="search"], input[type="text"]').first.click()
|
||||
page.wait_for_timeout(1000)
|
||||
baskets = [b.strip() for b in page.locator('mat-option').all_inner_texts()
|
||||
if b.strip() and b.strip() != "No results found"]
|
||||
print(f" Nalezeno {len(baskets)} kosiku: {baskets}")
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
if not baskets:
|
||||
print(" Zadne destruction kosite — preskakuji.")
|
||||
return
|
||||
|
||||
for basket in baskets:
|
||||
filename = os.path.join(output_dir, f"ip_destruction_basket_{basket}.xlsx")
|
||||
if os.path.exists(filename):
|
||||
print(f" [{basket}] Preskakuji — existuje.")
|
||||
continue
|
||||
print(f" [{basket}] Stahuji...")
|
||||
input_field = page.locator('input[placeholder="search"], input[type="text"]').first
|
||||
input_field.click()
|
||||
input_field.fill(basket)
|
||||
page.wait_for_timeout(500)
|
||||
page.locator('mat-option').first.dispatch_event('click')
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
with page.expect_download(timeout=120000) as dl:
|
||||
page.get_by_role("button", name="Download XLS").click()
|
||||
dl.value.save_as(filename)
|
||||
print(f" [{basket}] OK")
|
||||
|
||||
page.get_by_role("button", name="Clear").click()
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
print(" Destruction hotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(accept_downloads=True)
|
||||
page = context.new_page()
|
||||
page.goto(BASE_URL)
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Email *").fill(EMAIL)
|
||||
page.get_by_label("Password *").fill(PASSWORD)
|
||||
page.locator('#login__submit').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Study *").click()
|
||||
page.get_by_role("option", name=STUDY).click()
|
||||
page.get_by_role("button", name="SELECT").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
run(page, STUDY)
|
||||
browser.close()
|
||||
@@ -0,0 +1,83 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import os
|
||||
|
||||
# ── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://janssen.4gclinical.com"
|
||||
|
||||
EMAIL = "vbuzalka@its.jnj.com"
|
||||
PASSWORD = "Vlado123++-+"
|
||||
|
||||
# STUDY = "42847922MDD3003"
|
||||
STUDY = "77242113UCO3001"
|
||||
|
||||
SITES = {
|
||||
"42847922MDD3003": [
|
||||
"S10-CZ10002",
|
||||
"S10-CZ10004",
|
||||
"S10-CZ10005",
|
||||
"S10-CZ10008",
|
||||
"S10-CZ10011",
|
||||
"S10-CZ10012",
|
||||
],
|
||||
"77242113UCO3001": [
|
||||
"DD5-CZ10001",
|
||||
"DD5-CZ10003",
|
||||
"DD5-CZ10006",
|
||||
"DD5-CZ10009",
|
||||
"DD5-CZ10010",
|
||||
"DD5-CZ10012",
|
||||
"DD5-CZ10013",
|
||||
"DD5-CZ10015",
|
||||
"DD5-CZ10016",
|
||||
"DD5-CZ10020",
|
||||
"DD5-CZ10021",
|
||||
"DD5-CZ10022",
|
||||
],
|
||||
}
|
||||
|
||||
OUTPUT_DIR = f"xls_reports_{STUDY}"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run(page, study):
|
||||
output_dir = f"xls_reports_{study}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
page.goto(f"{BASE_URL}/report/onsite_inventory_detail")
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
for site_id in SITES[study]:
|
||||
print(f" [{site_id}] Stahuji...")
|
||||
page.locator('input[placeholder="search"], input[type="text"]').first.click()
|
||||
page.get_by_role("option", name=site_id).click()
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
with page.expect_download(timeout=120000) as dl:
|
||||
page.get_by_role("button", name="Download XLS").click()
|
||||
|
||||
dl.value.save_as(os.path.join(output_dir, f"onsite_inventory_detail_{site_id}.xlsx"))
|
||||
print(f" [{site_id}] OK")
|
||||
|
||||
page.get_by_role("button", name="Clear").click()
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
print(" Inventory hotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(accept_downloads=True)
|
||||
page = context.new_page()
|
||||
page.goto(BASE_URL)
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Email *").fill(EMAIL)
|
||||
page.get_by_label("Password *").fill(PASSWORD)
|
||||
page.locator('#login__submit').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Study *").click()
|
||||
page.get_by_role("option", name=STUDY).click()
|
||||
page.get_by_role("button", name="SELECT").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
run(page, STUDY)
|
||||
browser.close()
|
||||
@@ -0,0 +1,95 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import os
|
||||
import pandas as pd
|
||||
|
||||
# ── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://janssen.4gclinical.com"
|
||||
|
||||
EMAIL = "vbuzalka@its.jnj.com"
|
||||
PASSWORD = "Vlado123++-+"
|
||||
|
||||
STUDY = "42847922MDD3003"
|
||||
#STUDY = "77242113UCO3001"
|
||||
|
||||
OUTPUT_DIR = f"xls_shipment_details_{STUDY}"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def get_cz_shipment_ids(study):
|
||||
path = f"xls_shipments_{study}/shipments_report_{study}.xlsx"
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
df = pd.read_excel(path, header=5)
|
||||
df.columns = df.columns.str.strip()
|
||||
df = df.dropna(how="all")
|
||||
df["Shipment ID"] = df["Shipment ID"].astype(str).str.strip()
|
||||
cz = df[df["Location"].str.contains("Czech", na=False, case=False)]
|
||||
return cz["Shipment ID"].tolist()
|
||||
|
||||
|
||||
def run(page, study):
|
||||
output_dir = f"xls_shipment_details_{study}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
page.goto(f"{BASE_URL}/report/shipment_details_report")
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
cz_ids = get_cz_shipment_ids(study)
|
||||
if cz_ids is not None:
|
||||
shipments = cz_ids
|
||||
print(f" Filtrovano ze shipments reportu: {len(shipments)} CZ shipmentu")
|
||||
else:
|
||||
page.locator('input[placeholder="search"], input[type="text"]').first.click()
|
||||
page.wait_for_timeout(1000)
|
||||
shipments = [s.strip() for s in page.locator('mat-option').all_inner_texts()
|
||||
if s.strip() and s.strip() != "No results found"]
|
||||
print(f" Nalezeno {len(shipments)} shipmentu z dropdownu")
|
||||
page.keyboard.press("Escape")
|
||||
page.wait_for_timeout(500)
|
||||
|
||||
if not shipments:
|
||||
print(" Zadne shipments — preskakuji.")
|
||||
return
|
||||
|
||||
for shipment in shipments:
|
||||
filename = os.path.join(output_dir, f"shipment_details_{shipment}.xlsx")
|
||||
if os.path.exists(filename):
|
||||
print(f" [{shipment}] Preskakuji — existuje.")
|
||||
continue
|
||||
print(f" [{shipment}] Stahuji...")
|
||||
|
||||
input_field = page.locator('input[placeholder="search"], input[type="text"]').first
|
||||
input_field.click()
|
||||
input_field.fill(shipment)
|
||||
page.wait_for_timeout(500)
|
||||
page.locator('mat-option').first.dispatch_event('click')
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
with page.expect_download(timeout=120000) as dl:
|
||||
page.get_by_role("button", name="Download XLS").click()
|
||||
dl.value.save_as(filename)
|
||||
print(f" [{shipment}] OK")
|
||||
|
||||
page.get_by_role("button", name="Clear").click()
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
print(" Shipment details hotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(accept_downloads=True)
|
||||
page = context.new_page()
|
||||
page.goto(BASE_URL)
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Email *").fill(EMAIL)
|
||||
page.get_by_label("Password *").fill(PASSWORD)
|
||||
page.locator('#login__submit').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Study *").click()
|
||||
page.get_by_role("option", name=STUDY).click()
|
||||
page.get_by_role("button", name="SELECT").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
run(page, STUDY)
|
||||
browser.close()
|
||||
@@ -0,0 +1,47 @@
|
||||
from playwright.sync_api import sync_playwright
|
||||
import os
|
||||
|
||||
# ── CONFIG ──────────────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://janssen.4gclinical.com"
|
||||
|
||||
EMAIL = "vbuzalka@its.jnj.com"
|
||||
PASSWORD = "Vlado123++-+"
|
||||
|
||||
# STUDY = "42847922MDD3003"
|
||||
STUDY = "77242113UCO3001"
|
||||
|
||||
OUTPUT_DIR = f"xls_shipments_{STUDY}"
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run(page, study):
|
||||
output_dir = f"xls_shipments_{study}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
page.goto(f"{BASE_URL}/report/shipments_report")
|
||||
page.wait_for_load_state("networkidle", timeout=120000)
|
||||
|
||||
filename = os.path.join(output_dir, f"shipments_report_{study}.xlsx")
|
||||
with page.expect_download(timeout=120000) as dl:
|
||||
page.get_by_role("button", name="Download XLS").click()
|
||||
dl.value.save_as(filename)
|
||||
print(f" Shipments report OK -> {filename}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from playwright.sync_api import sync_playwright
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(accept_downloads=True)
|
||||
page = context.new_page()
|
||||
page.goto(BASE_URL)
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Email *").fill(EMAIL)
|
||||
page.get_by_label("Password *").fill(PASSWORD)
|
||||
page.locator('#login__submit').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Study *").click()
|
||||
page.get_by_role("option", name=STUDY).click()
|
||||
page.get_by_role("button", name="SELECT").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
run(page, STUDY)
|
||||
browser.close()
|
||||
@@ -0,0 +1,441 @@
|
||||
"""
|
||||
Importuje drugs data z IWRS Excel reportů do MySQL.
|
||||
|
||||
Tabulky:
|
||||
iwrs_shipments — zásilky (jen CZ, verzováno import_id)
|
||||
iwrs_shipment_items — obsah zásilek (verzováno import_id)
|
||||
iwrs_inventory — lékový sklad na centrech (verzováno import_id)
|
||||
iwrs_destruction — destrukce (bez verzování, přeskočí již importované košíky)
|
||||
|
||||
Spustit po stažení souborů (nebo přes run_all.py).
|
||||
"""
|
||||
|
||||
import os
|
||||
import glob
|
||||
import re
|
||||
import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import mysql.connector
|
||||
|
||||
import db_config
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
STUDIES = ["77242113UCO3001", "42847922MDD3003"]
|
||||
|
||||
SITES = {
|
||||
"77242113UCO3001": [
|
||||
"DD5-CZ10001", "DD5-CZ10003", "DD5-CZ10006", "DD5-CZ10009",
|
||||
"DD5-CZ10010", "DD5-CZ10012", "DD5-CZ10013", "DD5-CZ10015",
|
||||
"DD5-CZ10016", "DD5-CZ10020", "DD5-CZ10021", "DD5-CZ10022",
|
||||
],
|
||||
"42847922MDD3003": [
|
||||
"S10-CZ10002", "S10-CZ10004", "S10-CZ10005",
|
||||
"S10-CZ10008", "S10-CZ10011", "S10-CZ10012",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# ── type converters ──────────────────────────────────────────────────────────
|
||||
|
||||
def _py(val):
|
||||
if isinstance(val, np.generic):
|
||||
return val.item()
|
||||
return val
|
||||
|
||||
def to_date(val):
|
||||
val = _py(val)
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, float) and (val != val):
|
||||
return None
|
||||
try:
|
||||
if pd.isna(val):
|
||||
return None
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if isinstance(val, pd.Timestamp):
|
||||
return None if pd.isna(val) else val.date()
|
||||
if isinstance(val, datetime.datetime):
|
||||
return val.date()
|
||||
if isinstance(val, datetime.date):
|
||||
return val
|
||||
s = str(val).strip()
|
||||
if not s or s.lower() in ("nat", "nan", "none", ""):
|
||||
return None
|
||||
for fmt in ("%Y-%m-%d", "%d-%b-%Y", "%d-%m-%Y", "%Y-%m-%d %H:%M:%S"):
|
||||
try:
|
||||
return datetime.datetime.strptime(s, fmt).date()
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def to_int(val):
|
||||
val = _py(val)
|
||||
try:
|
||||
v = float(val)
|
||||
return None if (v != v) else int(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def to_str(val):
|
||||
val = _py(val)
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, float) and (val != val):
|
||||
return None
|
||||
s = str(val).strip()
|
||||
return None if s.lower() in ("nan", "nat", "none", "") else s
|
||||
|
||||
|
||||
# ── DB helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
def get_conn():
|
||||
return mysql.connector.connect(
|
||||
host=db_config.DB_HOST, port=db_config.DB_PORT,
|
||||
user=db_config.DB_USER, password=db_config.DB_PASSWORD,
|
||||
database=db_config.DB_NAME,
|
||||
)
|
||||
|
||||
def insert_import(cursor, study, source_label):
|
||||
cursor.execute(
|
||||
"INSERT INTO iwrs_import (study, imported_at, source_file, report_type) VALUES (%s, %s, %s, %s)",
|
||||
(study, datetime.datetime.now(), source_label, "drugs"),
|
||||
)
|
||||
return cursor.lastrowid
|
||||
|
||||
def basket_already_imported(cursor, study, basket_id):
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM iwrs_destruction WHERE study=%s AND basket_id=%s LIMIT 1",
|
||||
(study, str(basket_id)),
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
|
||||
# ── parsers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def parse_shipments_report(study):
|
||||
path = os.path.join(BASE_DIR, f"xls_shipments_{study}", f"shipments_report_{study}.xlsx")
|
||||
if not os.path.exists(path):
|
||||
print(f" CHYBÍ: {path}")
|
||||
return []
|
||||
|
||||
raw = pd.read_excel(path, header=None)
|
||||
header_row = None
|
||||
for i, row in raw.iterrows():
|
||||
if "Shipment ID" in [str(v).strip() for v in row]:
|
||||
header_row = i
|
||||
break
|
||||
if header_row is None:
|
||||
return []
|
||||
|
||||
df = pd.read_excel(path, header=header_row)
|
||||
df = df.dropna(how="all")
|
||||
# pouze CZ zásilky
|
||||
df = df[df["Location"].astype(str).str.contains("Czech", na=False, case=False)]
|
||||
col = df.columns.tolist()
|
||||
|
||||
rows = []
|
||||
for _, r in df.iterrows():
|
||||
rows.append({
|
||||
"shipment_id": to_str(r["Shipment ID"]),
|
||||
"status": to_str(r["IRT Shipment Status"]),
|
||||
"type": to_str(r["Type"]),
|
||||
"ship_from": to_str(r["Shipment From"]),
|
||||
"ship_to_site": to_str(r["Ship To:"]),
|
||||
"location": to_str(r["Location"]),
|
||||
"request_date": to_date(r["Request Date"]),
|
||||
"shipped_date": to_date(r["Shipped Date"]),
|
||||
"received_date": to_date(r["Received Date"]) if "Received Date" in col else None,
|
||||
"received_by": to_str(r["Received by"]) if "Received by" in col else None,
|
||||
"delivered_date_utc": to_date(r["Delivered Date [UTC]"]) if "Delivered Date [UTC]" in col else None,
|
||||
"delivery_recipient": to_str(r["Delivery Recipient"]) if "Delivery Recipient" in col else None,
|
||||
"delivery_details": to_str(r["Delivery Details"]) if "Delivery Details" in col else None,
|
||||
"cancelled_date": to_date(r["Cancelled Date"]) if "Cancelled Date" in col else None,
|
||||
"total_medication_ids": to_int(r["Total Medication IDs"]) if "Total Medication IDs" in col else None,
|
||||
"tracking_no": to_str(r["Tracking #"]) if "Tracking #" in col else None,
|
||||
"shipping_category": to_str(r["Shipping Category"]) if "Shipping Category" in col else None,
|
||||
"expected_arrival": to_date(r["Expected Arrival"]) if "Expected Arrival" in col else None,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def parse_shipment_details(study):
|
||||
detail_dir = os.path.join(BASE_DIR, f"xls_shipment_details_{study}")
|
||||
files = sorted(glob.glob(os.path.join(detail_dir, "shipment_details_*.xlsx")))
|
||||
rows = []
|
||||
for path in files:
|
||||
# shipment ID z názvu souboru
|
||||
m = re.search(r"shipment_details_(.+)\.xlsx", os.path.basename(path))
|
||||
shipment_id = m.group(1) if m else "UNKNOWN"
|
||||
|
||||
raw = pd.read_excel(path, header=None)
|
||||
header_row = None
|
||||
for i, row in raw.iterrows():
|
||||
if "Medication ID" in [str(v).strip() for v in row]:
|
||||
header_row = i
|
||||
break
|
||||
if header_row is None:
|
||||
continue
|
||||
|
||||
df = pd.read_excel(path, header=header_row)
|
||||
df = df.dropna(how="all")
|
||||
col = df.columns.tolist()
|
||||
|
||||
for _, r in df.iterrows():
|
||||
# normalizace názvů sloupců lišících se mezi studiemi
|
||||
med_desc = (to_str(r.get("Medication Description"))
|
||||
or to_str(r.get("Medication ID Description")))
|
||||
med_type = (to_str(r.get("Medication type"))
|
||||
or to_str(r.get("Medication ID type")))
|
||||
rows.append({
|
||||
"shipment_id": shipment_id,
|
||||
"destination_location": to_str(r.get("Destination Location")),
|
||||
"shipment_status": to_str(r.get("IRT Shipment Status")),
|
||||
"shipment_type": to_str(r.get("Type")),
|
||||
"destination_site": to_str(r.get("Destination Site")),
|
||||
"investigator": to_str(r.get("Investigator")),
|
||||
"medication_description": med_desc,
|
||||
"medication_type": med_type,
|
||||
"medication_id": to_str(r.get("Medication ID")),
|
||||
"packaged_lot_no": to_str(r.get("Packaged Lot number")),
|
||||
"packaged_lot_description": to_str(r.get("Packaged Lot description")),
|
||||
"container_id": to_str(r.get("Container ID")),
|
||||
"quantity": to_int(r.get("Quantity of Medication IDs")),
|
||||
"expiration_date": to_date(r.get("Expiration Date")),
|
||||
"item_status": to_str(r.get("Status")),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def parse_inventory(study):
|
||||
inv_dir = os.path.join(BASE_DIR, f"xls_reports_{study}")
|
||||
files = sorted(glob.glob(os.path.join(inv_dir, "onsite_inventory_detail_*.xlsx")))
|
||||
rows = []
|
||||
for path in files:
|
||||
raw = pd.read_excel(path, header=None)
|
||||
|
||||
# extrahuj metadata ze záhlaví
|
||||
site = investigator = location = None
|
||||
header_row = None
|
||||
for i, row in raw.iterrows():
|
||||
first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else ""
|
||||
if first.startswith("Site:"):
|
||||
site = first.replace("Site:", "").strip()
|
||||
elif first.startswith("Investigator:"):
|
||||
investigator = first.replace("Investigator:", "").strip()
|
||||
elif first.startswith("Location:"):
|
||||
location = first.replace("Location:", "").strip()
|
||||
# hlavička dat — první sloupec je "Medication" nebo "Medication ID"
|
||||
if first in ("Medication", "Medication ID") and header_row is None:
|
||||
header_row = i
|
||||
if header_row is None:
|
||||
continue
|
||||
|
||||
df = pd.read_excel(path, header=header_row)
|
||||
df = df.dropna(how="all")
|
||||
# normalizuj první sloupec na "medication_id"
|
||||
df = df.rename(columns={df.columns[0]: "medication_id"})
|
||||
col = df.columns.tolist()
|
||||
|
||||
for _, r in df.iterrows():
|
||||
rows.append({
|
||||
"site": site,
|
||||
"investigator": investigator,
|
||||
"location": location,
|
||||
"medication_id": to_str(r["medication_id"]),
|
||||
"packaged_lot_no": to_str(r.get("Packaged Lot number")),
|
||||
"original_expiration_date": to_date(r.get("Original Expiration Date when Packaged Lot was Added")),
|
||||
"expiration_date": to_date(r.get("Expiration date")),
|
||||
"received_date": to_date(r.get("Received Date")),
|
||||
"receipt_user": to_str(r.get("Shipment Receipt User")),
|
||||
"subject_identifier": to_str(r.get("Subject Identifier")),
|
||||
"quantity_assigned": to_int(r.get("Quantity Assigned")),
|
||||
"irt_transaction": to_str(r.get("IRT Transaction")),
|
||||
"date_assigned": to_date(r.get("Date Assigned")),
|
||||
"assignment_user": to_str(r.get("Assignment User")),
|
||||
"dispensation_status": to_str(r.get("Dispensation Status")),
|
||||
"dispensing_date": to_date(r.get("Dispensing date") or r.get("Dispensing Date")),
|
||||
"quantity_dispensed": to_int(r.get("Quantity Dispensed")),
|
||||
"dispensing_user": to_str(r.get("Dispensing User")),
|
||||
"quantity_returned": to_int(r.get("Quantity Returned")),
|
||||
"date_returned": to_date(r.get("Date Returned")),
|
||||
"return_user": to_str(r.get("Return User")),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def parse_destruction_files(study):
|
||||
dest_dir = os.path.join(BASE_DIR, f"xls_ip_destruction_{study}")
|
||||
files = sorted(glob.glob(os.path.join(dest_dir, "ip_destruction_basket_*.xlsx")))
|
||||
baskets = []
|
||||
for path in files:
|
||||
raw = pd.read_excel(path, header=None)
|
||||
|
||||
# metadata z záhlaví
|
||||
meta = {}
|
||||
header_row = None
|
||||
for i, row in raw.iterrows():
|
||||
first = str(row.iloc[0]).strip() if pd.notna(row.iloc[0]) else ""
|
||||
for key, attr in [
|
||||
("Investigator Name:", "investigator"),
|
||||
("Site ID:", "site_id"),
|
||||
("Location:", "location"),
|
||||
("Basket ID:", "basket_id"),
|
||||
("Drug Destruction Created Date:", "destruction_date"),
|
||||
]:
|
||||
if first.startswith(key):
|
||||
meta[attr] = first.replace(key, "").strip()
|
||||
if first == "Medication ID Description" and header_row is None:
|
||||
header_row = i
|
||||
|
||||
if header_row is None:
|
||||
continue
|
||||
|
||||
df = pd.read_excel(path, header=header_row)
|
||||
df = df.dropna(how="all")
|
||||
|
||||
items = []
|
||||
for _, r in df.iterrows():
|
||||
items.append({
|
||||
"medication_description": to_str(r.get("Medication ID Description")),
|
||||
"medication_id": to_str(r.get("Medication ID")),
|
||||
"packaged_lot_description": to_str(r.get("Packaged Lot description")),
|
||||
"comments": to_str(r.get("Comments")),
|
||||
})
|
||||
|
||||
baskets.append({
|
||||
"site_id": meta.get("site_id"),
|
||||
"investigator": meta.get("investigator"),
|
||||
"location": meta.get("location"),
|
||||
"basket_id": meta.get("basket_id"),
|
||||
"destruction_date": to_date(meta.get("destruction_date")),
|
||||
"items": items,
|
||||
})
|
||||
return baskets
|
||||
|
||||
|
||||
# ── inserters ────────────────────────────────────────────────────────────────
|
||||
|
||||
def insert_shipments(cursor, import_id, study, rows):
|
||||
sql = """INSERT INTO iwrs_shipments
|
||||
(import_id, study, shipment_id, status, type, ship_from, ship_to_site,
|
||||
location, request_date, shipped_date, received_date, received_by,
|
||||
delivered_date_utc, delivery_recipient, delivery_details, cancelled_date,
|
||||
total_medication_ids, tracking_no, shipping_category, expected_arrival)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
|
||||
for r in rows:
|
||||
cursor.execute(sql, (
|
||||
import_id, study, r["shipment_id"], r["status"], r["type"],
|
||||
r["ship_from"], r["ship_to_site"], r["location"],
|
||||
r["request_date"], r["shipped_date"], r["received_date"],
|
||||
r["received_by"], r["delivered_date_utc"], r["delivery_recipient"],
|
||||
r["delivery_details"], r["cancelled_date"], r["total_medication_ids"],
|
||||
r["tracking_no"], r["shipping_category"], r["expected_arrival"],
|
||||
))
|
||||
|
||||
|
||||
def insert_shipment_items(cursor, import_id, study, rows):
|
||||
sql = """INSERT INTO iwrs_shipment_items
|
||||
(import_id, study, shipment_id, destination_location, shipment_status,
|
||||
shipment_type, destination_site, investigator, medication_description,
|
||||
medication_type, medication_id, packaged_lot_no, packaged_lot_description,
|
||||
container_id, quantity, expiration_date, item_status)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
|
||||
for r in rows:
|
||||
cursor.execute(sql, (
|
||||
import_id, study, r["shipment_id"], r["destination_location"],
|
||||
r["shipment_status"], r["shipment_type"], r["destination_site"],
|
||||
r["investigator"], r["medication_description"], r["medication_type"],
|
||||
r["medication_id"], r["packaged_lot_no"], r["packaged_lot_description"],
|
||||
r["container_id"], r["quantity"], r["expiration_date"], r["item_status"],
|
||||
))
|
||||
|
||||
|
||||
def insert_inventory(cursor, import_id, study, rows):
|
||||
sql = """INSERT INTO iwrs_inventory
|
||||
(import_id, study, site, investigator, location, medication_id,
|
||||
packaged_lot_no, original_expiration_date, expiration_date, received_date,
|
||||
receipt_user, subject_identifier, quantity_assigned, irt_transaction,
|
||||
date_assigned, assignment_user, dispensation_status, dispensing_date,
|
||||
quantity_dispensed, dispensing_user, quantity_returned, date_returned, return_user)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
|
||||
for r in rows:
|
||||
cursor.execute(sql, (
|
||||
import_id, study, r["site"], r["investigator"], r["location"],
|
||||
r["medication_id"], r["packaged_lot_no"], r["original_expiration_date"],
|
||||
r["expiration_date"], r["received_date"], r["receipt_user"],
|
||||
r["subject_identifier"], r["quantity_assigned"], r["irt_transaction"],
|
||||
r["date_assigned"], r["assignment_user"], r["dispensation_status"],
|
||||
r["dispensing_date"], r["quantity_dispensed"], r["dispensing_user"],
|
||||
r["quantity_returned"], r["date_returned"], r["return_user"],
|
||||
))
|
||||
|
||||
|
||||
def insert_destruction(cursor, study, baskets):
|
||||
sql = """INSERT IGNORE INTO iwrs_destruction
|
||||
(study, site_id, investigator, location, basket_id, destruction_date,
|
||||
medication_description, medication_id, packaged_lot_description, comments)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)"""
|
||||
skipped = 0
|
||||
imported = 0
|
||||
for b in baskets:
|
||||
if basket_already_imported(cursor, study, b["basket_id"]):
|
||||
skipped += 1
|
||||
continue
|
||||
for item in b["items"]:
|
||||
cursor.execute(sql, (
|
||||
study, b["site_id"], b["investigator"], b["location"],
|
||||
b["basket_id"], b["destruction_date"],
|
||||
item["medication_description"], item["medication_id"],
|
||||
item["packaged_lot_description"], item["comments"],
|
||||
))
|
||||
imported += 1
|
||||
return imported, skipped
|
||||
|
||||
|
||||
# ── main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def import_study(study):
|
||||
print(f"\n Parsování dat pro {study}...")
|
||||
shipments = parse_shipments_report(study)
|
||||
items = parse_shipment_details(study)
|
||||
inventory = parse_inventory(study)
|
||||
baskets = parse_destruction_files(study)
|
||||
|
||||
print(f" Zásilky: {len(shipments)} | Položky zásilek: {len(items)} | Sklad: {len(inventory)} | Destrukční košíky: {len(baskets)}")
|
||||
|
||||
conn = get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
import_id = insert_import(cursor, study, f"drugs_{study}")
|
||||
print(f" import_id = {import_id}")
|
||||
|
||||
insert_shipments(cursor, import_id, study, shipments)
|
||||
insert_shipment_items(cursor, import_id, study, items)
|
||||
insert_inventory(cursor, import_id, study, inventory)
|
||||
dest_imported, dest_skipped = insert_destruction(cursor, study, baskets)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print(f" Destrukce: {dest_imported} nových | {dest_skipped} košíků přeskočeno (již importováno)")
|
||||
|
||||
|
||||
def main():
|
||||
for study in STUDIES:
|
||||
print(f"\n{'='*60}")
|
||||
print(f"[{study}]")
|
||||
print(f"{'='*60}")
|
||||
try:
|
||||
import_study(study)
|
||||
print(f" OK")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f" CHYBA: {e}")
|
||||
traceback.print_exc()
|
||||
print("\nHotovo.")
|
||||
|
||||
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import os
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
import download_reports
|
||||
import download_ip_destruction
|
||||
import download_shipments_report
|
||||
import download_shipment_details
|
||||
import create_accountability_report
|
||||
|
||||
BASE_URL = "https://janssen.4gclinical.com"
|
||||
EMAIL = "vbuzalka@its.jnj.com"
|
||||
PASSWORD = "Vlado123++-+"
|
||||
|
||||
STUDIES = {
|
||||
"1": "77242113UCO3001",
|
||||
"2": "42847922MDD3003",
|
||||
}
|
||||
|
||||
|
||||
def pick_study():
|
||||
print("Vyber studii:")
|
||||
for k, v in STUDIES.items():
|
||||
print(f" {k}) {v}")
|
||||
while True:
|
||||
choice = input("Volba (1/2): ").strip()
|
||||
if choice in STUDIES:
|
||||
return STUDIES[choice]
|
||||
print(" Neplatna volba, zkus znovu.")
|
||||
|
||||
|
||||
def login_and_select_study(page, study):
|
||||
print(f"\n[1/5] Prihlaseni a vyber studie {study}...")
|
||||
page.goto(BASE_URL)
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Email *").fill(EMAIL)
|
||||
page.get_by_label("Password *").fill(PASSWORD)
|
||||
page.locator('#login__submit').click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
page.get_by_label("Study *").click()
|
||||
page.get_by_role("option", name=study).click()
|
||||
page.get_by_role("button", name="SELECT").click()
|
||||
page.wait_for_load_state("networkidle")
|
||||
print(" OK")
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
study = pick_study()
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=False)
|
||||
context = browser.new_context(accept_downloads=True)
|
||||
page = context.new_page()
|
||||
|
||||
login_and_select_study(page, study)
|
||||
|
||||
print(f"\n[2/5] Stahuji inventory reporty...")
|
||||
download_reports.run(page, study)
|
||||
|
||||
print(f"\n[3/5] Stahuji IP destruction reporty...")
|
||||
download_ip_destruction.run(page, study)
|
||||
|
||||
print(f"\n[4/5] Stahuji shipments report...")
|
||||
download_shipments_report.run(page, study)
|
||||
|
||||
print(f"\n[5/5] Stahuji shipment details...")
|
||||
download_shipment_details.run(page, study)
|
||||
|
||||
browser.close()
|
||||
|
||||
print(f"\n[6/6] Generuji accountability report...")
|
||||
create_accountability_report.STUDY = study
|
||||
create_accountability_report.INVENTORY_DIR = __import__("pathlib").Path(f"xls_reports_{study}")
|
||||
create_accountability_report.DESTRUCTION_DIR= __import__("pathlib").Path(f"xls_ip_destruction_{study}")
|
||||
create_accountability_report.SHIPMENTS_FILE = __import__("pathlib").Path(f"xls_shipments_{study}/shipments_report_{study}.xlsx")
|
||||
create_accountability_report.DETAILS_DIR = __import__("pathlib").Path(f"xls_shipment_details_{study}")
|
||||
create_accountability_report.OUTPUT_FILE = create_accountability_report.OUTPUT_DIR / f"{__import__('datetime').date.today().strftime('%Y-%m-%d')} {study} CZ IWRS overview.xlsx"
|
||||
create_accountability_report.main()
|
||||
|
||||
print("\nVse hotovo!")
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user