Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,37 @@ from openpyxl import Workbook
|
|
| 6 |
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
| 7 |
from openpyxl.utils import get_column_letter
|
| 8 |
|
| 9 |
-
# --- 1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
def parse_dosage_to_numeric(dose_str):
|
| 11 |
if not isinstance(dose_str, str):
|
| 12 |
return 0.0
|
|
@@ -18,87 +48,57 @@ def parse_dosage_to_numeric(dose_str):
|
|
| 18 |
return val
|
| 19 |
return 0.0
|
| 20 |
|
| 21 |
-
# ---
|
| 22 |
-
def
|
| 23 |
-
if
|
| 24 |
-
return pd.DataFrame(), gr.update(choices=[]), "請上傳健保申報量資料。"
|
| 25 |
-
|
| 26 |
-
try:
|
| 27 |
-
# 支援讀取 CSV 或 Excel
|
| 28 |
-
if file_info.name.endswith('.csv'):
|
| 29 |
-
df_raw = pd.read_csv(file_info.name)
|
| 30 |
-
else:
|
| 31 |
-
df_raw = pd.read_excel(file_info.name)
|
| 32 |
-
|
| 33 |
-
df_raw.columns = df_raw.columns.str.strip()
|
| 34 |
-
|
| 35 |
-
# 確保必要欄位存在並去除空白
|
| 36 |
-
for col in ['成分', '劑型', '劑量', '廠商', '年度']:
|
| 37 |
-
if col in df_raw.columns:
|
| 38 |
-
df_raw[col] = df_raw[col].astype(str).str.strip()
|
| 39 |
-
|
| 40 |
-
# 抓取所有成分
|
| 41 |
-
if '成分' not in df_raw.columns:
|
| 42 |
-
return pd.DataFrame(), gr.update(choices=[]), "❌ 檔案格式錯誤,找不到「成分」欄位。"
|
| 43 |
-
|
| 44 |
-
all_comps = sorted(df_raw['成分'].dropna().unique())
|
| 45 |
-
return df_raw, gr.update(choices=all_comps), f"✅ 資料載入成功!共 {len(df_raw)} 筆資料,發現 {len(all_comps)} 種成分。"
|
| 46 |
-
|
| 47 |
-
except Exception as e:
|
| 48 |
-
return pd.DataFrame(), gr.update(choices=[]), f"❌ 載入失敗:{str(e)}"
|
| 49 |
-
|
| 50 |
-
# --- 3. 動態搜尋成分 ---
|
| 51 |
-
def update_component_choices(search_text, df_raw):
|
| 52 |
-
# 若還沒上傳資料,回傳空選項
|
| 53 |
-
if df_raw is None or df_raw.empty:
|
| 54 |
return gr.update(choices=[])
|
| 55 |
-
|
| 56 |
-
all_comps = sorted(df_raw['成分'].dropna().unique())
|
| 57 |
if not search_text or search_text.strip() == "":
|
| 58 |
-
return gr.update(choices=
|
| 59 |
|
| 60 |
search_text = search_text.strip().lower()
|
| 61 |
-
filtered = [c for c in
|
| 62 |
return gr.update(choices=filtered)
|
| 63 |
|
| 64 |
-
# --- 4. 產出
|
| 65 |
-
def generate_standard_excel(selected_components
|
| 66 |
-
if
|
| 67 |
-
return None, "❌
|
| 68 |
if not selected_components:
|
| 69 |
return None, "❌ 請至少勾選一個成分品項!"
|
| 70 |
|
| 71 |
-
# 篩選資料
|
| 72 |
-
df_filtered =
|
| 73 |
if df_filtered.empty:
|
| 74 |
return None, "❌ 找不到相關資料!"
|
| 75 |
|
| 76 |
-
#
|
| 77 |
qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
|
| 78 |
|
| 79 |
-
# 樞紐分析
|
| 80 |
pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
|
| 81 |
|
|
|
|
| 82 |
for year_col in ['2022年', '2023年', '2024年']:
|
| 83 |
if year_col not in pivot_df.columns:
|
| 84 |
pivot_df[year_col] = 0
|
| 85 |
|
| 86 |
pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
|
| 87 |
|
| 88 |
-
# 排序:劑量由小到大 -> 2024年數量由大到小
|
| 89 |
pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
|
| 90 |
pivot_df = pivot_df.sort_values(by=['dose_numeric', '2024年'], ascending=[True, False]).drop(columns=['dose_numeric'])
|
| 91 |
|
| 92 |
-
# 建立 Excel
|
| 93 |
wb = Workbook()
|
| 94 |
ws = wb.active
|
| 95 |
ws.title = "廠商排名報表"
|
| 96 |
ws.views.sheetView[0].showGridLines = True
|
| 97 |
|
|
|
|
| 98 |
font_family = "微軟正黑體"
|
| 99 |
-
header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid")
|
| 100 |
-
subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid")
|
| 101 |
-
total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
|
| 102 |
|
| 103 |
header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
|
| 104 |
data_font = Font(name=font_family, size=12)
|
|
@@ -111,7 +111,7 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 111 |
thin_side = Side(border_style="thin", color="D9D9D9")
|
| 112 |
cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
|
| 113 |
|
| 114 |
-
#
|
| 115 |
headers = ["成分", "劑型", "劑量", "廠商", "2022年\n數量", "2023年\n數量", "2024年\n數量", "2024年\n占比(%)"]
|
| 116 |
ws.append(headers)
|
| 117 |
ws.row_dimensions[1].height = 30
|
|
@@ -130,6 +130,7 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 130 |
|
| 131 |
current_row = 2
|
| 132 |
|
|
|
|
| 133 |
for dose in unique_doses:
|
| 134 |
dose_group = pivot_df[pivot_df['劑量'] == dose]
|
| 135 |
dose_2024_sum = dose_group['2024年'].sum()
|
|
@@ -138,7 +139,7 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 138 |
dose_subtotal_2023 = 0
|
| 139 |
dose_subtotal_2024 = 0
|
| 140 |
|
| 141 |
-
is_first_row = True
|
| 142 |
|
| 143 |
for _, row in dose_group.iterrows():
|
| 144 |
qty_2022 = row['2022年']
|
|
@@ -146,13 +147,14 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 146 |
qty_2024 = row['2024年']
|
| 147 |
ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
|
| 148 |
|
| 149 |
-
#
|
| 150 |
c_val = row['成分'] if is_first_row else ""
|
| 151 |
f_val = row['劑型'] if is_first_row else ""
|
| 152 |
d_val = row['劑量'] if is_first_row else ""
|
| 153 |
|
| 154 |
ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
|
| 155 |
|
|
|
|
| 156 |
for col_idx in range(1, 9):
|
| 157 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 158 |
cell.font = data_font
|
|
@@ -163,18 +165,18 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 163 |
cell.alignment = left_align
|
| 164 |
elif col_idx in [5, 6, 7]:
|
| 165 |
cell.alignment = right_align
|
| 166 |
-
cell.number_format = '#,##0'
|
| 167 |
elif col_idx == 8:
|
| 168 |
cell.alignment = right_align
|
| 169 |
-
cell.number_format = '0.0%'
|
| 170 |
|
| 171 |
dose_subtotal_2022 += qty_2022
|
| 172 |
dose_subtotal_2023 += qty_2023
|
| 173 |
dose_subtotal_2024 += qty_2024
|
| 174 |
current_row += 1
|
| 175 |
-
is_first_row = False
|
| 176 |
|
| 177 |
-
# 寫入合計列
|
| 178 |
ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
|
| 179 |
for col_idx in range(1, 9):
|
| 180 |
cell = ws.cell(row=current_row, column=col_idx)
|
|
@@ -195,7 +197,7 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 195 |
grand_total_2024 += dose_subtotal_2024
|
| 196 |
current_row += 1
|
| 197 |
|
| 198 |
-
# 寫入總計列
|
| 199 |
ws.append(["", "", "總計", "", grand_total_2022, grand_total_2023, grand_total_2024, 1.0])
|
| 200 |
for col_idx in range(1, 9):
|
| 201 |
cell = ws.cell(row=current_row, column=col_idx)
|
|
@@ -224,10 +226,10 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 224 |
max_len = line_len
|
| 225 |
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
|
| 226 |
|
| 227 |
-
#
|
| 228 |
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
| 229 |
-
ws.page_setup.fitToWidth = 1
|
| 230 |
-
ws.page_setup.fitToHeight = 0
|
| 231 |
ws.page_margins.top = 2.7 / 2.54
|
| 232 |
ws.page_margins.bottom = 2.5 / 2.54
|
| 233 |
ws.page_margins.left = 1.5 / 2.54
|
|
@@ -235,66 +237,66 @@ def generate_standard_excel(selected_components, df_raw):
|
|
| 235 |
ws.page_margins.header = 1.5 / 2.54
|
| 236 |
ws.page_margins.footer = 1.0 / 2.54
|
| 237 |
|
|
|
|
| 238 |
comp_names = "、".join(df_filtered['成分'].unique())
|
| 239 |
form_mapping = {"注射劑": "Inj.", "一般錠劑膠囊劑": "Tab./Cap.", "膜衣錠": "F.C. Tab.", "膠囊劑": "Cap.", "錠劑": "Tab."}
|
| 240 |
unique_forms = df_filtered['劑型'].unique()
|
| 241 |
form_abbr = f" {form_mapping.get(unique_forms[0], unique_forms[0])}" if len(unique_forms) == 1 else ""
|
| 242 |
doses_str = "、".join(unique_doses)
|
| 243 |
|
|
|
|
| 244 |
header_string = f"&16&B{comp_names}{form_abbr} {doses_str}廠商申報量排名"
|
| 245 |
ws.oddHeader.center.text = header_string
|
| 246 |
ws.oddFooter.left.text = "&12中央健康保險署 政府資料開放平台 2024年資料"
|
| 247 |
ws.oddFooter.right.text = "&12https://data.gov.tw/dataset/22131"
|
|
|
|
| 248 |
ws.page_setup.scaleWithDoc = True
|
| 249 |
ws.page_setup.alignWithMargins = True
|
| 250 |
|
|
|
|
| 251 |
output_filename = "Standardized_Report.xlsx"
|
| 252 |
wb.save(output_filename)
|
| 253 |
|
| 254 |
-
return output_filename, f"🎉 成功!已匯出報表。"
|
| 255 |
|
| 256 |
-
# --- 5.
|
| 257 |
with gr.Blocks(title="健保申報量標準化 Excel 產出工具") as demo:
|
| 258 |
gr.Markdown("# 💊 健保申報量標準化 Excel 自動化產出工具")
|
|
|
|
| 259 |
|
| 260 |
-
# 建立一個隱藏的狀態儲存區,用來暫存您上傳的資料表
|
| 261 |
-
df_state = gr.State()
|
| 262 |
-
|
| 263 |
with gr.Row():
|
| 264 |
with gr.Column(scale=1):
|
| 265 |
-
gr.Markdown("###
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
-
gr.
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
|
| 273 |
-
submit_btn = gr.Button("🚀
|
| 274 |
|
| 275 |
with gr.Column(scale=1):
|
| 276 |
-
gr.Markdown("###
|
| 277 |
-
status_output = gr.Textbox(label="系統
|
| 278 |
-
file_output = gr.File(label="
|
| 279 |
|
| 280 |
-
#
|
| 281 |
-
file_input.upload(
|
| 282 |
-
fn=process_uploaded_file,
|
| 283 |
-
inputs=file_input,
|
| 284 |
-
outputs=[df_state, component_choices, load_status]
|
| 285 |
-
)
|
| 286 |
-
|
| 287 |
-
# 事件綁定:搜尋框過濾選項
|
| 288 |
search_input.change(
|
| 289 |
fn=update_component_choices,
|
| 290 |
-
inputs=
|
| 291 |
outputs=component_choices
|
| 292 |
)
|
| 293 |
|
| 294 |
-
#
|
| 295 |
submit_btn.click(
|
| 296 |
fn=generate_standard_excel,
|
| 297 |
-
inputs=
|
| 298 |
outputs=[file_output, status_output]
|
| 299 |
)
|
| 300 |
|
|
|
|
| 6 |
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
|
| 7 |
from openpyxl.utils import get_column_letter
|
| 8 |
|
| 9 |
+
# --- 1. 固定讀取同目錄下的資料庫檔案 ---
|
| 10 |
+
DATA_FILE = "data.csv"
|
| 11 |
+
|
| 12 |
+
def load_fixed_database():
|
| 13 |
+
if os.path.exists(DATA_FILE):
|
| 14 |
+
try:
|
| 15 |
+
# 讀取固定的健保資料庫
|
| 16 |
+
df = pd.read_csv(DATA_FILE)
|
| 17 |
+
df.columns = df.columns.str.strip()
|
| 18 |
+
# 清理欄位前後空白
|
| 19 |
+
for col in ['成分', '劑型', '劑量', '廠商', '年度']:
|
| 20 |
+
if col in df.columns:
|
| 21 |
+
df[col] = df[col].astype(str).str.strip()
|
| 22 |
+
return df
|
| 23 |
+
except Exception as e:
|
| 24 |
+
print(f"讀取本地資料庫失敗: {e}")
|
| 25 |
+
return pd.DataFrame()
|
| 26 |
+
else:
|
| 27 |
+
print(f"錯誤:找不到固定資料庫檔案 {DATA_FILE},請確保檔案已上傳至同目錄。")
|
| 28 |
+
return pd.DataFrame()
|
| 29 |
+
|
| 30 |
+
# 程式啟動時載入全域資料
|
| 31 |
+
df_global = load_fixed_database()
|
| 32 |
+
if not df_global.empty:
|
| 33 |
+
ALL_COMPONENTS = sorted(df_global['成分'].dropna().unique())
|
| 34 |
+
INIT_MESSAGE = f"✅ 成功自動載入固定資料庫!共 {len(df_global)} 筆數據,包含 {len(ALL_COMPONENTS)} 種成分。"
|
| 35 |
+
else:
|
| 36 |
+
ALL_COMPONENTS = []
|
| 37 |
+
INIT_MESSAGE = "❌ 警告:未在目錄下找到 data.csv 檔案,請先將資料庫檔案上傳至 Space 檔案庫中。"
|
| 38 |
+
|
| 39 |
+
# --- 2. 輔助函式:劑量排序邏輯 ---
|
| 40 |
def parse_dosage_to_numeric(dose_str):
|
| 41 |
if not isinstance(dose_str, str):
|
| 42 |
return 0.0
|
|
|
|
| 48 |
return val
|
| 49 |
return 0.0
|
| 50 |
|
| 51 |
+
# --- 3. 動態搜尋成分(如同 Excel 篩選功能) ---
|
| 52 |
+
def update_component_choices(search_text):
|
| 53 |
+
if not ALL_COMPONENTS:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return gr.update(choices=[])
|
|
|
|
|
|
|
| 55 |
if not search_text or search_text.strip() == "":
|
| 56 |
+
return gr.update(choices=ALL_COMPONENTS)
|
| 57 |
|
| 58 |
search_text = search_text.strip().lower()
|
| 59 |
+
filtered = [c for c in ALL_COMPONENTS if search_text in c.lower()]
|
| 60 |
return gr.update(choices=filtered)
|
| 61 |
|
| 62 |
+
# --- 4. 產出標準化 Excel 報表 ---
|
| 63 |
+
def generate_standard_excel(selected_components):
|
| 64 |
+
if df_global.empty:
|
| 65 |
+
return None, "❌ 系統未成功載入 data.csv 資料庫,請檢查 Space 中的檔案是否存在。"
|
| 66 |
if not selected_components:
|
| 67 |
return None, "❌ 請至少勾選一個成分品項!"
|
| 68 |
|
| 69 |
+
# 篩選選定的成分資料
|
| 70 |
+
df_filtered = df_global[df_global['成分'].isin(selected_components)].copy()
|
| 71 |
if df_filtered.empty:
|
| 72 |
return None, "❌ 找不到相關資料!"
|
| 73 |
|
| 74 |
+
# 自動判斷數量欄位名稱
|
| 75 |
qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
|
| 76 |
|
| 77 |
+
# 進行樞紐分析 (Pivot Table)
|
| 78 |
pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
|
| 79 |
|
| 80 |
+
# 確保三年欄位皆完整
|
| 81 |
for year_col in ['2022年', '2023年', '2024年']:
|
| 82 |
if year_col not in pivot_df.columns:
|
| 83 |
pivot_df[year_col] = 0
|
| 84 |
|
| 85 |
pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
|
| 86 |
|
| 87 |
+
# 高階排序:劑量由小到大 -> 2024年數量由大到小
|
| 88 |
pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
|
| 89 |
pivot_df = pivot_df.sort_values(by=['dose_numeric', '2024年'], ascending=[True, False]).drop(columns=['dose_numeric'])
|
| 90 |
|
| 91 |
+
# 建立全新 Excel 工作簿
|
| 92 |
wb = Workbook()
|
| 93 |
ws = wb.active
|
| 94 |
ws.title = "廠商排名報表"
|
| 95 |
ws.views.sheetView[0].showGridLines = True
|
| 96 |
|
| 97 |
+
# 樣式與底色定義
|
| 98 |
font_family = "微軟正黑體"
|
| 99 |
+
header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid") # 標題深藍
|
| 100 |
+
subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid") # 合計淡藍
|
| 101 |
+
total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid") # 總計中藍
|
| 102 |
|
| 103 |
header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
|
| 104 |
data_font = Font(name=font_family, size=12)
|
|
|
|
| 111 |
thin_side = Side(border_style="thin", color="D9D9D9")
|
| 112 |
cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
|
| 113 |
|
| 114 |
+
# 寫入標題列 (欄位依序排列,包含換行)
|
| 115 |
headers = ["成分", "劑型", "劑量", "廠商", "2022年\n數量", "2023年\n數量", "2024年\n數量", "2024年\n占比(%)"]
|
| 116 |
ws.append(headers)
|
| 117 |
ws.row_dimensions[1].height = 30
|
|
|
|
| 130 |
|
| 131 |
current_row = 2
|
| 132 |
|
| 133 |
+
# 按劑量拆分群組寫入
|
| 134 |
for dose in unique_doses:
|
| 135 |
dose_group = pivot_df[pivot_df['劑量'] == dose]
|
| 136 |
dose_2024_sum = dose_group['2024年'].sum()
|
|
|
|
| 139 |
dose_subtotal_2023 = 0
|
| 140 |
dose_subtotal_2024 = 0
|
| 141 |
|
| 142 |
+
is_first_row = True
|
| 143 |
|
| 144 |
for _, row in dose_group.iterrows():
|
| 145 |
qty_2022 = row['2022年']
|
|
|
|
| 147 |
qty_2024 = row['2024年']
|
| 148 |
ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
|
| 149 |
|
| 150 |
+
# 同劑量重複廠商:前三欄留白不重複顯示
|
| 151 |
c_val = row['成分'] if is_first_row else ""
|
| 152 |
f_val = row['劑型'] if is_first_row else ""
|
| 153 |
d_val = row['劑量'] if is_first_row else ""
|
| 154 |
|
| 155 |
ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
|
| 156 |
|
| 157 |
+
# 設定一般資料列格式
|
| 158 |
for col_idx in range(1, 9):
|
| 159 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 160 |
cell.font = data_font
|
|
|
|
| 165 |
cell.alignment = left_align
|
| 166 |
elif col_idx in [5, 6, 7]:
|
| 167 |
cell.alignment = right_align
|
| 168 |
+
cell.number_format = '#,##0' # 千分位、至個位數
|
| 169 |
elif col_idx == 8:
|
| 170 |
cell.alignment = right_align
|
| 171 |
+
cell.number_format = '0.0%' # 占比至小數點第一位
|
| 172 |
|
| 173 |
dose_subtotal_2022 += qty_2022
|
| 174 |
dose_subtotal_2023 += qty_2023
|
| 175 |
dose_subtotal_2024 += qty_2024
|
| 176 |
current_row += 1
|
| 177 |
+
is_first_row = False
|
| 178 |
|
| 179 |
+
# 寫入「劑量合計」列
|
| 180 |
ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
|
| 181 |
for col_idx in range(1, 9):
|
| 182 |
cell = ws.cell(row=current_row, column=col_idx)
|
|
|
|
| 197 |
grand_total_2024 += dose_subtotal_2024
|
| 198 |
current_row += 1
|
| 199 |
|
| 200 |
+
# 寫入「總計」列
|
| 201 |
ws.append(["", "", "總計", "", grand_total_2022, grand_total_2023, grand_total_2024, 1.0])
|
| 202 |
for col_idx in range(1, 9):
|
| 203 |
cell = ws.cell(row=current_row, column=col_idx)
|
|
|
|
| 226 |
max_len = line_len
|
| 227 |
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
|
| 228 |
|
| 229 |
+
# 專業列印與邊界設定 (公分轉英吋)
|
| 230 |
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
| 231 |
+
ws.page_setup.fitToWidth = 1 # 欄位強迫縮放放入單頁
|
| 232 |
+
ws.page_setup.fitToHeight = 0 # 高度自動延伸
|
| 233 |
ws.page_margins.top = 2.7 / 2.54
|
| 234 |
ws.page_margins.bottom = 2.5 / 2.54
|
| 235 |
ws.page_margins.left = 1.5 / 2.54
|
|
|
|
| 237 |
ws.page_margins.header = 1.5 / 2.54
|
| 238 |
ws.page_margins.footer = 1.0 / 2.54
|
| 239 |
|
| 240 |
+
# 動態變數計算 (用於頁首)
|
| 241 |
comp_names = "、".join(df_filtered['成分'].unique())
|
| 242 |
form_mapping = {"注射劑": "Inj.", "一般錠劑膠囊劑": "Tab./Cap.", "膜衣錠": "F.C. Tab.", "膠囊劑": "Cap.", "錠劑": "Tab."}
|
| 243 |
unique_forms = df_filtered['劑型'].unique()
|
| 244 |
form_abbr = f" {form_mapping.get(unique_forms[0], unique_forms[0])}" if len(unique_forms) == 1 else ""
|
| 245 |
doses_str = "、".join(unique_doses)
|
| 246 |
|
| 247 |
+
# 頁首頁尾設定 (&16&B 為 Excel 內建 16級字+粗體 語法)
|
| 248 |
header_string = f"&16&B{comp_names}{form_abbr} {doses_str}廠商申報量排名"
|
| 249 |
ws.oddHeader.center.text = header_string
|
| 250 |
ws.oddFooter.left.text = "&12中央健康保險署 政府資料開放平台 2024年資料"
|
| 251 |
ws.oddFooter.right.text = "&12https://data.gov.tw/dataset/22131"
|
| 252 |
+
|
| 253 |
ws.page_setup.scaleWithDoc = True
|
| 254 |
ws.page_setup.alignWithMargins = True
|
| 255 |
|
| 256 |
+
# 儲存 Excel
|
| 257 |
output_filename = "Standardized_Report.xlsx"
|
| 258 |
wb.save(output_filename)
|
| 259 |
|
| 260 |
+
return output_filename, f"🎉 成功!已為您匯出完整的廠商排名 Excel 報表。"
|
| 261 |
|
| 262 |
+
# --- 5. 簡潔的 Gradio 使用者介面 (移除上傳區塊) ---
|
| 263 |
with gr.Blocks(title="健保申報量標準化 Excel 產出工具") as demo:
|
| 264 |
gr.Markdown("# 💊 健保申報量標準化 Excel 自動化產出工具")
|
| 265 |
+
gr.Markdown(f"**系統狀態:** {INIT_MESSAGE}")
|
| 266 |
|
|
|
|
|
|
|
|
|
|
| 267 |
with gr.Row():
|
| 268 |
with gr.Column(scale=1):
|
| 269 |
+
gr.Markdown("### 🔍 第一步:輸入成分篩選(支援單方、複方模糊比對)")
|
| 270 |
+
search_input = gr.Textbox(
|
| 271 |
+
label="輸入成分關鍵字",
|
| 272 |
+
placeholder="例如:Levofloxacin 或 Cilostazol",
|
| 273 |
+
value=""
|
| 274 |
+
)
|
| 275 |
|
| 276 |
+
component_choices = gr.CheckboxGroup(
|
| 277 |
+
label="📋 第二步:勾選欲納入報表的成分品項 (可多選)",
|
| 278 |
+
choices=ALL_COMPONENTS,
|
| 279 |
+
value=[]
|
| 280 |
+
)
|
| 281 |
|
| 282 |
+
submit_btn = gr.Button("🚀 產生標準化 Excel 報表", variant="primary")
|
| 283 |
|
| 284 |
with gr.Column(scale=1):
|
| 285 |
+
gr.Markdown("### 📥 第三步:下載產出檔案")
|
| 286 |
+
status_output = gr.Textbox(label="系統處理結果", interactive=False)
|
| 287 |
+
file_output = gr.File(label="點擊下載產出的 Excel 報表")
|
| 288 |
|
| 289 |
+
# 監聽搜尋輸入框變更,即時過濾全域資料庫的成分
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
search_input.change(
|
| 291 |
fn=update_component_choices,
|
| 292 |
+
inputs=search_input,
|
| 293 |
outputs=component_choices
|
| 294 |
)
|
| 295 |
|
| 296 |
+
# 點擊按鈕一鍵生成報表
|
| 297 |
submit_btn.click(
|
| 298 |
fn=generate_standard_excel,
|
| 299 |
+
inputs=component_choices,
|
| 300 |
outputs=[file_output, status_output]
|
| 301 |
)
|
| 302 |
|