BudgetX.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. """
  2. BudgetX
  3. 這是一隻用來比對以及檢查預算書的工具程式。
  4. 它主要是設計給需要確認預算書的主管,來快速確認同仁們寫的預算書沒有明顯的問題。
  5. 它主要使用的函式庫為:
  6. Gradio 提供 Web UI
  7. Camelot 提供 PDF 表格萃取
  8. Pandas 提供 分析表格
  9. 版權所有 © 2025 亞新工程顧問股份有限公司
  10. Copyright © 2025 Moh And Associates, All Rights Reserved.
  11. """
  12. import gradio as gr
  13. import camelot
  14. import pandas as pd
  15. import glob
  16. import math
  17. # Global variables to store PDF data, generated by Qwen code?
  18. # May make sense to reinstate if performance becomes a problem in the future.
  19. #pdf1_data = None
  20. #pdf2_data = None
  21. #dropdown_options = []
  22. #table_data = []
  23. # Constant Strings
  24. MSG_PREFIX = "<span style='color: red; font-style: italic'>"
  25. MSG_POSTFIX = "</span>"
  26. GRAND_TOTAL_NAME = "總價"
  27. # DataFrame Column Names
  28. COMP_COLS = ["參考預算書獨有項目 (-)", "共有項目 (∩)", "被檢驗預算書獨有項目 (+)"]
  29. PRICE_COLS = ["項目", "單位", "數量", "單價", "復價", "檢查", "被檢驗預算書價格 %", "參考預算書價格 %"]
  30. CODE_VERIFY_COLS = ["項目", "單位", "編碼", "章碼章名(1~5碼)檢查", "6碼請您比對", "7碼請您比對", "8碼請您比對", "9碼請您比對", "10碼比對"]
  31. # Hard-coded row and column positions for the demo.
  32. # May have to turn these into dynamically-determined values later on if the format is flexible.
  33. ITEMS_COL = 1
  34. UNIT_COL = 2
  35. AMOUNT_COL = 3
  36. UNITPRICE_COL = 4
  37. TOTAL_COL = 5
  38. CODE_COL = 6
  39. HEADER_ROW = 2
  40. # Work Codes
  41. CODE_DIR = "workcodes\\raw\\"
  42. CHAPTER = "章碼"
  43. CHAPTER_NAME = "章名"
  44. CODE_6 = "6碼"
  45. CODE_6_NAME = "6碼名稱"
  46. CODE_7 = "7碼"
  47. CODE_7_NAME = "7碼名稱"
  48. CODE_8 = "8碼"
  49. CODE_8_NAME = "8碼名稱"
  50. CODE_9 = "9碼"
  51. CODE_9_NAME = "9碼名稱"
  52. CODE_10 = "10碼"
  53. CODE_10_NAME = "10碼名稱"
  54. def process_pdf(file_path):
  55. """
  56. Processes a PDF file, given the path.
  57. This method reads in all of the tables found in the given PDF file with camelot, and
  58. builds a pandas DataFrame with its contents, skipping the first few header rows.
  59. The header row indexes are hard-coded for now as HEADER_ROW, but we may choose to
  60. determine this dynamically in the future, if deemed necessary.
  61. Args:
  62. file_path (string): The path to the PDF file, as provided by the gradio.File input box.
  63. Returns:
  64. A pandas DataFrame containing the concatenated table contents.
  65. """
  66. # Extract tables from PDF using Camelot
  67. try:
  68. tables = camelot.read_pdf(file_path, pages='all')
  69. # print(f"{tables.n} Tables read from {file_path}.")
  70. rtn = None
  71. for i, table in enumerate(tables):
  72. df = table.df
  73. if (len(df) > HEADER_ROW):
  74. if rtn is None:
  75. # print(f"Creating DataFrame with Column Names:\n{df.iloc[HEADER_ROW].tolist()}\n")
  76. rtn = pd.DataFrame(columns=df.iloc[HEADER_ROW].tolist())
  77. # print(f"Appending the following rows:\n{df.iloc[HEADER_ROW+1:]}\n")
  78. df.columns = rtn.columns
  79. rtn = pd.concat([rtn, df.iloc[HEADER_ROW+1:]])
  80. else:
  81. print(f"Table {i} has less than or equal to {HEADER_ROW} rows! Skipping Table:\n{df}")
  82. # print(f"Built DataFrame from PDF tables:\n{rtn}")
  83. return rtn
  84. except Exception as e:
  85. return f"Error: {str(e)}"
  86. def comp_items(pdf1_data, pdf2_data):
  87. """
  88. Compares the item columns of two PDFs, building the set differences between them.
  89. Args:
  90. pdf1_data (DataFrame): The combined tables from PDF1, as generated by process_pdf
  91. pdf2_data (DataFrame): The combined tables from PDF2, as generated by process_pdf
  92. Returns:
  93. A pandas DataFrame containing the set left difference, intersection and set right difference
  94. """
  95. set1 = set(pdf1_data.iloc[:, ITEMS_COL])
  96. set2 = set(pdf2_data.iloc[:, ITEMS_COL])
  97. # print(f"Set 1:\n{set1}")
  98. # print(f"Set 2:\n{set2}")
  99. rtn = pd.concat([
  100. pd.DataFrame({COMP_COLS[0]: list(set1 - set2)}),
  101. pd.DataFrame({COMP_COLS[1]: list(set1.intersection(set2))}),
  102. pd.DataFrame({COMP_COLS[2]: list(set2 - set1)})
  103. ])
  104. rtn = rtn.fillna('')
  105. # print(f"Comparison DF:\n{rtn}")
  106. return rtn
  107. def verify_prices(pdf1_data, pdf1_total_price, pdf2_data, pdf2_total_price):
  108. """
  109. Runs a variety of tests against the prices in the second budget PDF.
  110. Args:
  111. pdf2_data (DataFrame): The combined tables from PDF2, as generated by process_pdf
  112. Returns:
  113. A pandas DataFrame containing the price verifications
  114. """
  115. rtn = pd.DataFrame(columns=PRICE_COLS)
  116. grand_total_name = ""
  117. grand_total = 0
  118. running_total = 0
  119. for i in range(len(pdf2_data)):
  120. try:
  121. item = pdf2_data.iloc[i, ITEMS_COL]
  122. unit = pdf2_data.iloc[i, UNIT_COL]
  123. amount_str = pdf2_data.iloc[i, AMOUNT_COL]
  124. unit_price_str = pdf2_data.iloc[i, UNITPRICE_COL]
  125. total_str = pdf2_data.iloc[i, TOTAL_COL]
  126. if GRAND_TOTAL_NAME in item and amount_str == "" and unit_price_str == "" and total_str != "":
  127. if grand_total_name != "":
  128. # Multiple Grand Totals...?
  129. row = pd.DataFrame({
  130. PRICE_COLS[0]: item,
  131. PRICE_COLS[1]: "",
  132. PRICE_COLS[2]: "",
  133. PRICE_COLS[3]: "",
  134. PRICE_COLS[4]: "",
  135. PRICE_COLS[5]: f"有多個 總價 欄位!第一個找到的: {grand_total_name}",
  136. PRICE_COLS[6]: "",
  137. PRICE_COLS[7]: "",
  138. })
  139. rtn = pd.concat([rtn, row], ignore_index=True)
  140. else:
  141. grand_total_name = item
  142. grand_total = float(total_str.replace(",", ""))
  143. else:
  144. amount = float(amount_str.replace(",", ""))
  145. unit_price = float(unit_price_str.replace(",", ""))
  146. total = float(total_str.replace(",", ""))
  147. running_total += total
  148. check = "數量乘單價與復價的差異大於 1 !" if abs(amount*unit_price - total) > 1.0 else "OK"
  149. # pdf2 的復價百分比
  150. pdf2_percent = str(round(total / pdf2_total_price * 100, 2)) + "%"
  151. # pdf1 的復價百分比
  152. pdf1_match = pdf1_data[pdf1_data.iloc[:, ITEMS_COL] == item]
  153. if not pdf1_match.empty:
  154. try:
  155. pdf1_total = float(pdf1_match.iloc[0, TOTAL_COL].replace(",", ""))
  156. pdf1_percent = str(round(pdf1_total / pdf1_total_price * 100, 2)) + "%"
  157. except Exception:
  158. pdf1_percent = ""
  159. else:
  160. pdf1_percent = ""
  161. row = pd.DataFrame({
  162. PRICE_COLS[0]: [item],
  163. PRICE_COLS[1]: [unit],
  164. PRICE_COLS[2]: [round(amount, 2)],
  165. PRICE_COLS[3]: [round(unit_price, 2)],
  166. PRICE_COLS[4]: [round(total, 2)],
  167. PRICE_COLS[5]: [check],
  168. PRICE_COLS[6]: [pdf2_percent],
  169. PRICE_COLS[7]: [pdf1_percent],
  170. })
  171. rtn = pd.concat([rtn, row], ignore_index=True)
  172. except ValueError as v:
  173. # print(f"String to float Error: {v}. Skipping non-value row:\n{pdf2_data.iloc[i]}")
  174. continue
  175. # Append the Grand Total Row
  176. if grand_total_name == "":
  177. row = pd.DataFrame({
  178. PRICE_COLS[0]: "",
  179. PRICE_COLS[1]: "",
  180. PRICE_COLS[2]: "",
  181. PRICE_COLS[3]: "",
  182. PRICE_COLS[4]: "",
  183. PRICE_COLS[5]: "預算書中沒有 總價!",
  184. PRICE_COLS[6]: "",
  185. PRICE_COLS[7]: "",
  186. })
  187. else:
  188. check = f"總價與所有復價加總大於 1 !復價加總:{running_total}" if abs(grand_total - running_total) > 1.0 else "OK"
  189. row = pd.DataFrame({
  190. PRICE_COLS[0]: grand_total_name,
  191. PRICE_COLS[1]: "",
  192. PRICE_COLS[2]: "",
  193. PRICE_COLS[3]: "",
  194. PRICE_COLS[4]: [grand_total],
  195. PRICE_COLS[5]: check,
  196. PRICE_COLS[6]: "",
  197. PRICE_COLS[7]: "",
  198. })
  199. rtn = pd.concat([rtn, row], ignore_index=True)
  200. # print(f"Verify Prices Result:\n{rtn}")
  201. return rtn
  202. def get_total_price(pdf_data):
  203. grand_total = 0
  204. for i in range(len(pdf_data)):
  205. try:
  206. item = pdf_data.iloc[i, ITEMS_COL]
  207. amount_str = pdf_data.iloc[i, AMOUNT_COL]
  208. unit_price_str = pdf_data.iloc[i, UNITPRICE_COL]
  209. total_str = pdf_data.iloc[i, TOTAL_COL]
  210. if GRAND_TOTAL_NAME in item and amount_str == "" and unit_price_str == "" and total_str != "":
  211. grand_total = float(total_str.replace(",", ""))
  212. break
  213. except ValueError as v:
  214. print(f"String to float Error: {v}. Skipping non-value row:\n{pdf_data.iloc[i]}")
  215. continue
  216. print(f"Total Prices :\n{grand_total}")
  217. return grand_total
  218. def read_code_file(chapter):
  219. """
  220. Attempts to find a read in the work code file of the given chapter.
  221. Currently only handles Excel files with pandas' read_excel (xlrd).
  222. TODO: Maybe make the cache global and move it in here?
  223. Args:
  224. chapter (str) The 5-digit work code chapter to look for.
  225. Returns:
  226. The DataFrame representing the Work Code file.
  227. """
  228. code_file = CODE_DIR + chapter + "*"
  229. file_match = glob.glob(code_file)
  230. # print(f"read_code_file: Found matches for {file_name}: {file_match}")
  231. if len(file_match) == 1:
  232. file_lower = file_match[0].lower()
  233. if file_lower.endswith(".xls") or file_lower.endswith(".xlsx"):
  234. return pd.read_excel(file_match[0], header=1, dtype=str)
  235. else:
  236. print(f"Single file match found, but it is not an excel file?! Match: {file_match[0]}")
  237. return None
  238. elif len(file_match) == 0:
  239. return None
  240. else:
  241. print(f"Multiple matches found for: {code_file}! Matches: {file_match}")
  242. return None
  243. return None
  244. def code_69_checks(code_value, code_df, code_col, code_name_col):
  245. """
  246. Verifies one of Code 6~9. Realized that this only works for a particular
  247. work code file format, and we will need to create more of these to handle
  248. more formats.
  249. This version only works for simple work code file formats, where each value
  250. from 6 to 9 are separate and non-conditional.
  251. Example codes that this method works on include:
  252. 02231, 02336, 02751
  253. Args:
  254. code_value (str) The single-digit code entered at this position.
  255. code_df (DataFrame) The DataFrame of the code table, as read from Excel (or cache).
  256. code_col (str) The (constant) string of the code column name.
  257. code_name_col (str) The (constant) string of the code name column name.
  258. Returns:
  259. A string to display in the result table.
  260. """
  261. try:
  262. matches = code_df.loc[code_df[code_col] == code_value]
  263. # print(f"code_69_checks(): Matches for {code_value}:\n{matches}")
  264. if len(matches) == 0:
  265. rtn = f"{code_col} {code_value} 並不合規!"
  266. elif len(matches) > 1:
  267. rtn = f"編碼檔案中對 {code_col} : {code_value} 有重複定義!"
  268. else:
  269. match_val = matches[code_name_col][matches.index[0]]
  270. # print(f"match_val {match_val} for code {code_value}")
  271. if code_value == "0" and (
  272. (isinstance(match_val, float) and math.isnan(match_val)) or
  273. (isinstance(match_val, str) and match_val.strip() == "")):
  274. match_val = "不分類"
  275. rtn = f"{code_df[code_name_col][0]} = {match_val}"
  276. except KeyError as k:
  277. rtn = f"編碼檔案中找不到 {code_col} 或是 {code_name_col} 欄位!"
  278. return rtn
  279. def verify_codes(pdf2_data):
  280. """
  281. Verifies the work codes against government-published work code files.
  282. Args:
  283. pdf2_data (DataFrame): The combined tables from PDF2, as generated by process_pdf
  284. Returns:
  285. A pandas DataFrame containing the code verifications
  286. """
  287. # code_files = os.listdir(CODE_DIR)
  288. # print(f"verify_codes(): Codes Dir Listing: \n{code_files}")
  289. rtn = pd.DataFrame(columns=CODE_VERIFY_COLS)
  290. code_df_cache = {}
  291. for i in range(len(pdf2_data)):
  292. item = pdf2_data.iloc[i, ITEMS_COL]
  293. unit = pdf2_data.iloc[i, UNIT_COL]
  294. code = pdf2_data.iloc[i, CODE_COL]
  295. if len(code) >= 10:
  296. # The files are coded with the first 5 numbers in the code.
  297. chapter = code[0:5]
  298. # print(f"verify_codes: Looking for file: {code_file}")
  299. code_df = None
  300. check = "OK"
  301. check_6 = ""
  302. check_7 = ""
  303. check_8 = ""
  304. check_9 = ""
  305. check_10 = ""
  306. if chapter in code_df_cache:
  307. code_df = code_df_cache[chapter]
  308. else:
  309. code_df = read_code_file(chapter)
  310. if isinstance(code_df, pd.DataFrame):
  311. code_df_cache[chapter] = code_df
  312. # print(f"Read code DataFrame:\n{code_df}")
  313. else:
  314. check = f"找不到這個編碼 (前5碼開頭) 的編碼檔案!請再確認編碼正確,或跟軟體工程師回報編碼檔需要更新!"
  315. if code_df is not None:
  316. if chapter != code_df[CHAPTER][0]:
  317. check = f"檔案名稱與章碼不符合?!請跟開發人員聯繫,更新 {chapter} 章的檔案!"
  318. else:
  319. chapter_name = code_df[CHAPTER_NAME][0]
  320. if not chapter_name in item:
  321. check = f"項目名稱中沒有章節名稱:{chapter_name}"
  322. # Code 6 ~ 9 checks:
  323. check_6 = code_69_checks(code[5:6], code_df, CODE_6, CODE_6_NAME)
  324. check_7 = code_69_checks(code[6:7], code_df, CODE_7, CODE_7_NAME)
  325. check_8 = code_69_checks(code[7:8], code_df, CODE_8, CODE_8_NAME)
  326. check_9 = code_69_checks(code[8:9], code_df, CODE_9, CODE_9_NAME)
  327. try:
  328. code_10 = code[9:10]
  329. matches = code_df.loc[code_df[CODE_10] == code_10]
  330. if len(matches) == 0:
  331. check_10 = f"{CODE_10} {code_10} 並不合規!"
  332. elif len(matches) > 1:
  333. check_10 = f"編碼檔案中對 {CODE_10} : {code_10} 有重複定義!請跟軟體工程師回報編碼檔錯誤!"
  334. else:
  335. match_val = matches[CODE_10_NAME][matches.index[0]]
  336. print(f"match_val {match_val} for code {code_10}")
  337. if (unit == match_val):
  338. check_10 = "OK"
  339. else:
  340. check_10 = f"單位不符!編碼單位應該是:{match_val}"
  341. except KeyError as k:
  342. check_10 = f"編碼檔案中找不到 {CODE_10} 或是 {CODE_10_NAME} 欄位!請跟軟體工程師回報研究檔案格式問題!"
  343. if check == "":
  344. check = "OK"
  345. row = pd.DataFrame({
  346. CODE_VERIFY_COLS[0]: [item],
  347. CODE_VERIFY_COLS[1]: [unit],
  348. CODE_VERIFY_COLS[2]: [code],
  349. CODE_VERIFY_COLS[3]: [check],
  350. CODE_VERIFY_COLS[4]: [check_6],
  351. CODE_VERIFY_COLS[5]: [check_7],
  352. CODE_VERIFY_COLS[6]: [check_8],
  353. CODE_VERIFY_COLS[7]: [check_9],
  354. CODE_VERIFY_COLS[8]: [check_10],
  355. })
  356. rtn = pd.concat([rtn, row], ignore_index=True)
  357. else:
  358. print(f"verify_codes: Skipping row: {item}")
  359. return rtn
  360. def update_table(pdf1, pdf2):
  361. """
  362. Process both PDFs and runs the verification routines.
  363. Args:
  364. pdf1 (gradio.File): Gradio File Input box for PDF1.
  365. pdf2 (gradio.File): Gradio File Input box for PDF2.
  366. Returns:
  367. 3 DataFrames for:
  368. 1. Item Comparison
  369. 2. Price Verification
  370. 3. Code Verification
  371. 1 Status message string
  372. """
  373. # This can be useful if we need to speed up processing by caching the data one day...
  374. # global pdf1_data, pdf2_data
  375. pdf1_data = None
  376. pdf1_total_price = None
  377. pdf2_data = None
  378. pdf2_total_price = None
  379. # Process first PDF
  380. if pdf1:
  381. result = process_pdf(pdf1.name)
  382. if isinstance(result, pd.DataFrame):
  383. pdf1_data = result
  384. pdf1_total_price = get_total_price(pdf1_data)
  385. else:
  386. return None, None, None, MSG_PREFIX+f"參考預算書 PDF 處理錯誤: {result}"+MSG_POSTFIX
  387. # Process second PDF
  388. if pdf2:
  389. result = process_pdf(pdf2.name)
  390. if isinstance(result, pd.DataFrame):
  391. pdf2_data = result
  392. pdf2_total_price = get_total_price(pdf2_data)
  393. else:
  394. return None, None, None, MSG_PREFIX+f"被檢驗預算書 PDF 處理錯誤: {result}"+MSG_POSTFIX
  395. # If only 1 is populated, return update message and that's it.
  396. if pdf1_data is None or pdf2_data is None:
  397. return None, None, None, MSG_PREFIX+f"PDF 檔案讀取 OK"+MSG_POSTFIX
  398. else:
  399. # 1. Compare items in the items column
  400. comp = comp_items(pdf1_data, pdf2_data)
  401. if not isinstance(comp, pd.DataFrame):
  402. return None, None, None, MSG_PREFIX+f"預算書 比對 失敗!錯誤訊息:{comp}"+MSG_POSTFIX
  403. # 2. Verify prices
  404. price = verify_prices(pdf1_data, pdf1_total_price, pdf2_data, pdf2_total_price)
  405. if not isinstance(price, pd.DataFrame):
  406. return comp, None, None, MSG_PREFIX + f"預算書 價格確認 失敗!錯誤訊息:{price}" + MSG_POSTFIX
  407. # 3. Verify codes
  408. code = verify_codes(pdf2_data)
  409. if not isinstance(code, pd.DataFrame):
  410. return comp, price, None, MSG_PREFIX + f"預算書 編碼確認 失敗!錯誤訊息:{code}" + MSG_POSTFIX
  411. return comp, price, code, MSG_PREFIX+f"預算書比對成功"+MSG_POSTFIX
  412. with gr.Blocks(title="亞新 BudgetX") as app:
  413. """
  414. This is the UI layout, Gradio style. UI elements can be added here.
  415. """
  416. # gr.HTML("<img src='/file=MAALogoBlueCropped.png'>")
  417. gr.Markdown("# MAA BudgetX 亞新預算書檢驗系統 (2025-10 Proof of Concept 版)")
  418. gr.Markdown("---")
  419. gr.Markdown("### _請上傳參考預算書與被檢驗預算書_")
  420. with gr.Row():
  421. # Left column for first PDF
  422. with gr.Column():
  423. pdf1_input = gr.File(label="參考預算書 PDF", file_types=[".pdf"])
  424. # pdf1_preview = gr.File(label="First PDF Preview")
  425. # Right column for second PDF
  426. with gr.Column():
  427. pdf2_input = gr.File(label="被檢驗預算書 PDF", file_types=[".pdf"])
  428. # pdf2_preview = gr.File(label="Second PDF Preview")
  429. # Error/warning textbox
  430. # error_output = gr.Textbox(label="_系統訊息_", interactive=False)
  431. error_output = gr.HTML(MSG_PREFIX+"系統訊息"+MSG_POSTFIX)
  432. # 1. 比對兩個預算書中的項目
  433. gr.Markdown("---")
  434. gr.Markdown("### _1. 項目比對_")
  435. gr.Markdown("* _比對參考預算書與被檢驗預算書中的項目欄中的獨有項目。_")
  436. gr.Markdown("* _無視重複項目。_")
  437. gr.Markdown("* _些微差異 (空格、錯題字等) 也會當成不同的項目。_")
  438. # Dropdown for selecting tables
  439. # dropdown = gr.Dropdown(label="選擇比對欄位", choices=["<請先上傳兩本預算書PDF>"])
  440. # Comparison Table
  441. comp_table = gr.Dataframe(label="預算書「項目及說明」列比對", headers=COMP_COLS)
  442. # 2. 價格確認
  443. gr.Markdown("---")
  444. gr.Markdown("### _2. 價格檢查_")
  445. gr.Markdown("* _拿 數量 乘上 單價,確認結果跟 復價 相差 1.0 以內_")
  446. gr.Markdown("* _總價是搜尋有 總價 的欄位,並將它的 復價 欄位跟其他的 復價 的加總比較,要求 相差 1.0 以內_")
  447. # Price validation table
  448. price_table = gr.Dataframe(label="價格確認", headers=PRICE_COLS)
  449. # 3. 編碼確認
  450. gr.Markdown("---")
  451. gr.Markdown("### _3. 編碼檢查_")
  452. gr.Markdown("* _讀取事前下載的編碼列表,但仍有不少問題..._")
  453. # Code validation table
  454. code_table = gr.Dataframe(label="編碼比對", headers=CODE_VERIFY_COLS)
  455. gr.Markdown("---")
  456. # Event handling
  457. pdf1_input.change(
  458. fn=lambda x: x,
  459. inputs=pdf1_input,
  460. # outputs=pdf1_preview
  461. )
  462. pdf2_input.change(
  463. fn=lambda x: x,
  464. inputs=pdf2_input,
  465. # outputs=pdf2_preview
  466. )
  467. # Update table when both files are uploaded
  468. inputs = [pdf1_input, pdf2_input]
  469. outputs = [comp_table, price_table, code_table, error_output]
  470. pdf1_input.change(update_table, inputs=inputs, outputs=outputs)
  471. pdf2_input.change(update_table, inputs=inputs, outputs=outputs)
  472. # Update table when dropdown selection changes
  473. # dropdown.change(update_table, inputs=dropdown, outputs=comp_table)
  474. if __name__ == "__main__":
  475. # Setting server_name to 0.0.0.0 allows us to listen to all interfaces.
  476. app.launch(server_name="0.0.0.0")