update streamlit app
This commit is contained in:
parent
ba29105bd2
commit
09601981ba
4 changed files with 384 additions and 0 deletions
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
60
streamlit_app.py
Normal file
60
streamlit_app.py
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import streamlit as st
|
||||||
|
from trial_balance_parser import parse_trial_balance_to_excel
|
||||||
|
|
||||||
|
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="MEDITECH Detail Trial Balance Parser",
|
||||||
|
page_icon="📄",
|
||||||
|
layout="wide",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
st.title("MEDITECH Detail Trial Balance Parser")
|
||||||
|
|
||||||
|
st.write(
|
||||||
|
"Upload a MEDITECH Detail Trial Balance PDF and export the parsed transactions "
|
||||||
|
"and account summary to Excel."
|
||||||
|
)
|
||||||
|
|
||||||
|
uploaded_file = st.file_uploader(
|
||||||
|
"Upload Trial Balance PDF",
|
||||||
|
type=["pdf"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if uploaded_file is not None:
|
||||||
|
st.info(f"Uploaded file: {uploaded_file.name}")
|
||||||
|
|
||||||
|
if st.button("Parse PDF"):
|
||||||
|
try:
|
||||||
|
with st.spinner("Parsing PDF..."):
|
||||||
|
excel_bytes, df_txn, df_acct, title = parse_trial_balance_to_excel(uploaded_file)
|
||||||
|
|
||||||
|
st.success("Parsed successfully.")
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.metric("Transactions", len(df_txn))
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.metric("Account Summary Rows", len(df_acct))
|
||||||
|
|
||||||
|
if title:
|
||||||
|
st.write(f"**Report Title:** {title}")
|
||||||
|
|
||||||
|
st.subheader("Transactions Preview")
|
||||||
|
st.dataframe(df_txn, use_container_width=True)
|
||||||
|
|
||||||
|
st.subheader("Account Summary Preview")
|
||||||
|
st.dataframe(df_acct, use_container_width=True)
|
||||||
|
|
||||||
|
st.download_button(
|
||||||
|
label="Download Excel File",
|
||||||
|
data=excel_bytes,
|
||||||
|
file_name="meditech_trial_balance_parsed.xlsx",
|
||||||
|
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error("Something went wrong while parsing the PDF.")
|
||||||
|
st.exception(e)
|
||||||
314
trial_balance_parser.py
Normal file
314
trial_balance_parser.py
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
import re
|
||||||
|
import io
|
||||||
|
import pandas as pd
|
||||||
|
import pdfplumber
|
||||||
|
import openpyxl
|
||||||
|
|
||||||
|
from openpyxl.styles import Font
|
||||||
|
from openpyxl.worksheet.table import Table, TableStyleInfo
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
|
|
||||||
|
|
||||||
|
money_re = re.compile(r"-?\d{1,3}(?:,\d{3})*(?:\.\d{2})")
|
||||||
|
acct_re = re.compile(r"^(?P<account>\d{2}\.\d{5}\.\d{5})\s+-\s+(?P<fund>\d{2})\s+(?P<name>.+)$")
|
||||||
|
journal_re = re.compile(
|
||||||
|
r"^(?P<journal>[A-Z][A-Z0-9/]*)\s+"
|
||||||
|
r"(?P<date>\d{2}/\d{2}/\d{2})(?P<bch>\d{1,2})?\s+"
|
||||||
|
r"(?P<entry>\d+(?:-\d+)?)\b"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_money(s):
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
|
||||||
|
m = money_re.search(s)
|
||||||
|
return float(m.group(0).replace(",", "")) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def is_page_header_noise(s):
|
||||||
|
s = " ".join(s.split()).upper()
|
||||||
|
return (
|
||||||
|
("FISCAL CALENDAR" in s or "FIRST CALENDAR" in s)
|
||||||
|
and ("ROUND MONEY" in s or "TRIAL" in s)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def clean_description(desc):
|
||||||
|
if not desc or is_page_header_noise(desc):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
r"FISCAL\s+CALENDAR\s+[A-Z]{3,6}\s*,?\s*[A-Z]{3}\s+20\d{2}",
|
||||||
|
r"FIRST\s+CALENDAR\s+[A-Z]{3,6}\s*,?\s*[A-Z]{3}\s+20\d{2}",
|
||||||
|
r"ROUND\s+MONEY\s*:\s*\d+\.\d+",
|
||||||
|
r"\bTRIAL\b",
|
||||||
|
]
|
||||||
|
|
||||||
|
for p in patterns:
|
||||||
|
desc = re.sub(p, "", desc, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
return " ".join(desc.split())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trial_balance_pdf(pdf_file):
|
||||||
|
"""
|
||||||
|
Parses a MEDITECH detail trial balance PDF.
|
||||||
|
|
||||||
|
Accepts either:
|
||||||
|
- a file path string
|
||||||
|
- a Streamlit uploaded file
|
||||||
|
- a file-like object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- df_txn
|
||||||
|
- df_acct
|
||||||
|
- title
|
||||||
|
"""
|
||||||
|
|
||||||
|
transactions = []
|
||||||
|
acct_summaries = []
|
||||||
|
|
||||||
|
report_title_parts = {
|
||||||
|
"fiscal": None,
|
||||||
|
"period": None,
|
||||||
|
"trial": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
with pdfplumber.open(pdf_file) as pdf:
|
||||||
|
for page in pdf.pages:
|
||||||
|
text = page.extract_text(layout=True) or ""
|
||||||
|
lines = text.splitlines()
|
||||||
|
|
||||||
|
for ln in lines:
|
||||||
|
s = ln.strip()
|
||||||
|
|
||||||
|
if report_title_parts["fiscal"] is None and "Fiscal Calendar" in s:
|
||||||
|
report_title_parts["fiscal"] = " ".join(s.split())
|
||||||
|
|
||||||
|
if report_title_parts["period"] is None and re.search(r"\b[A-Z]{3}\s+20\d{2}\b", s):
|
||||||
|
report_title_parts["period"] = " ".join(s.split())
|
||||||
|
|
||||||
|
if report_title_parts["trial"] is None and s.upper() == "TRIAL":
|
||||||
|
report_title_parts["trial"] = "TRIAL"
|
||||||
|
|
||||||
|
txn_header = None
|
||||||
|
acct_header = None
|
||||||
|
|
||||||
|
for i, ln in enumerate(lines):
|
||||||
|
if "JOURNAL" in ln and "DESCRIPTION" in ln and "DEBITS" in ln:
|
||||||
|
txn_header = i
|
||||||
|
|
||||||
|
if "ACCOUNT" in ln and "OPEN" in ln and "CLOSE" in ln:
|
||||||
|
acct_header = i
|
||||||
|
|
||||||
|
deb_pos = cred_pos = desc_pos = None
|
||||||
|
open_pos = adeb_pos = acredit_pos = change_pos = close_pos = None
|
||||||
|
|
||||||
|
if txn_header is not None:
|
||||||
|
h = lines[txn_header]
|
||||||
|
deb_pos = h.find("DEBITS")
|
||||||
|
cred_pos = h.find("CREDITS", deb_pos)
|
||||||
|
desc_pos = h.find("DESCRIPTION", cred_pos)
|
||||||
|
|
||||||
|
if acct_header is not None:
|
||||||
|
h = lines[acct_header]
|
||||||
|
open_pos = h.find("OPEN")
|
||||||
|
adeb_pos = h.find("DEBITS", open_pos)
|
||||||
|
acredit_pos = h.find("CREDITS", adeb_pos)
|
||||||
|
change_pos = h.find("CHANGE", acredit_pos)
|
||||||
|
close_pos = h.find("CLOSE", change_pos)
|
||||||
|
|
||||||
|
current_account = None
|
||||||
|
current_account_name = None
|
||||||
|
pending_totals = False
|
||||||
|
last_txn = None
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(lines):
|
||||||
|
ln = lines[i]
|
||||||
|
s = ln.strip()
|
||||||
|
|
||||||
|
if not s or is_page_header_noise(s):
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = acct_re.match(s)
|
||||||
|
|
||||||
|
if m:
|
||||||
|
current_account = m.group("account")
|
||||||
|
current_account_name = m.group("name").strip()
|
||||||
|
pending_totals = True
|
||||||
|
last_txn = None
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if pending_totals and money_re.search(s):
|
||||||
|
nums = [float(x.replace(",", "")) for x in money_re.findall(s)]
|
||||||
|
nums += [None] * 5
|
||||||
|
|
||||||
|
acct_summaries.append(
|
||||||
|
{
|
||||||
|
"ACCOUNT_NAME": current_account_name,
|
||||||
|
"ACCOUNT": current_account,
|
||||||
|
"OPEN": nums[0],
|
||||||
|
"TOTAL_DEBITS": nums[1],
|
||||||
|
"TOTAL_CREDITS": nums[2],
|
||||||
|
"NET_CHANGE": nums[3],
|
||||||
|
"CLOSE": nums[4],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
pending_totals = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = journal_re.match(s)
|
||||||
|
|
||||||
|
if m and current_account:
|
||||||
|
journal = m.group("journal")
|
||||||
|
date = m.group("date")
|
||||||
|
bch = int(m.group("bch")) if m.group("bch") else None
|
||||||
|
entry = m.group("entry")
|
||||||
|
|
||||||
|
debit = None
|
||||||
|
credit = None
|
||||||
|
desc = ""
|
||||||
|
|
||||||
|
if deb_pos is not None and cred_pos is not None:
|
||||||
|
debit = parse_money(ln[deb_pos:cred_pos])
|
||||||
|
credit = parse_money(ln[cred_pos:desc_pos])
|
||||||
|
desc = ln[desc_pos:].strip()
|
||||||
|
else:
|
||||||
|
after = s[m.end():].strip()
|
||||||
|
nums = money_re.findall(after)
|
||||||
|
|
||||||
|
if nums:
|
||||||
|
debit = float(nums[0].replace(",", ""))
|
||||||
|
|
||||||
|
desc = money_re.sub("", after).strip()
|
||||||
|
|
||||||
|
row = {
|
||||||
|
"ACCOUNT_NAME": current_account_name,
|
||||||
|
"ACCOUNT": current_account,
|
||||||
|
"JOURNAL": journal,
|
||||||
|
"DATE": date,
|
||||||
|
"BCH": bch,
|
||||||
|
"ENTRY": entry,
|
||||||
|
"DEBITS": debit,
|
||||||
|
"CREDITS": credit,
|
||||||
|
"DESCRIPTION": clean_description(desc),
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions.append(row)
|
||||||
|
last_txn = row
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if last_txn:
|
||||||
|
add = clean_description(s)
|
||||||
|
|
||||||
|
if add:
|
||||||
|
last_txn["DESCRIPTION"] += " " + add
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
title = ", ".join(v for v in report_title_parts.values() if v)
|
||||||
|
|
||||||
|
df_txn = pd.DataFrame(transactions)
|
||||||
|
df_acct = pd.DataFrame(acct_summaries)
|
||||||
|
|
||||||
|
return df_txn, df_acct, title
|
||||||
|
|
||||||
|
|
||||||
|
def build_excel_file(df_txn, df_acct, title):
|
||||||
|
"""
|
||||||
|
Builds a formatted Excel workbook in memory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- bytes suitable for Streamlit download_button
|
||||||
|
"""
|
||||||
|
|
||||||
|
initial_buffer = io.BytesIO()
|
||||||
|
|
||||||
|
with pd.ExcelWriter(initial_buffer, engine="openpyxl") as writer:
|
||||||
|
df_txn.to_excel(writer, "Transactions", index=False, startrow=2)
|
||||||
|
df_acct.to_excel(writer, "Account Summary", index=False, startrow=2)
|
||||||
|
|
||||||
|
initial_buffer.seek(0)
|
||||||
|
|
||||||
|
wb = openpyxl.load_workbook(initial_buffer)
|
||||||
|
|
||||||
|
for sheet_name in ["Transactions", "Account Summary"]:
|
||||||
|
ws = wb[sheet_name]
|
||||||
|
|
||||||
|
ws["A1"] = title
|
||||||
|
ws.merge_cells(
|
||||||
|
start_row=1,
|
||||||
|
start_column=1,
|
||||||
|
end_row=1,
|
||||||
|
end_column=ws.max_column,
|
||||||
|
)
|
||||||
|
ws["A1"].font = Font(bold=True)
|
||||||
|
ws.freeze_panes = "A4"
|
||||||
|
|
||||||
|
header_row = 3
|
||||||
|
|
||||||
|
if ws.max_row >= header_row and ws.max_column >= 1:
|
||||||
|
end_col = get_column_letter(ws.max_column)
|
||||||
|
table_ref = f"A{header_row}:{end_col}{ws.max_row}"
|
||||||
|
|
||||||
|
table_name = sheet_name.replace(" ", "") + "Table"
|
||||||
|
|
||||||
|
tab = Table(displayName=table_name, ref=table_ref)
|
||||||
|
|
||||||
|
style = TableStyleInfo(
|
||||||
|
name="TableStyleMedium9",
|
||||||
|
showFirstColumn=False,
|
||||||
|
showLastColumn=False,
|
||||||
|
showRowStripes=True,
|
||||||
|
showColumnStripes=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
tab.tableStyleInfo = style
|
||||||
|
ws.add_table(tab)
|
||||||
|
|
||||||
|
for col in ws.columns:
|
||||||
|
max_length = 0
|
||||||
|
col_letter = get_column_letter(col[0].column)
|
||||||
|
|
||||||
|
for cell in col:
|
||||||
|
try:
|
||||||
|
if cell.value:
|
||||||
|
max_length = max(max_length, len(str(cell.value)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ws.column_dimensions[col_letter].width = min(max_length + 2, 60)
|
||||||
|
|
||||||
|
final_buffer = io.BytesIO()
|
||||||
|
wb.save(final_buffer)
|
||||||
|
final_buffer.seek(0)
|
||||||
|
|
||||||
|
return final_buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_trial_balance_to_excel(pdf_file):
|
||||||
|
"""
|
||||||
|
Main function for Streamlit or command-line use.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- excel_bytes
|
||||||
|
- df_txn
|
||||||
|
- df_acct
|
||||||
|
- title
|
||||||
|
"""
|
||||||
|
|
||||||
|
df_txn, df_acct, title = parse_trial_balance_pdf(pdf_file)
|
||||||
|
excel_bytes = build_excel_file(df_txn, df_acct, title)
|
||||||
|
|
||||||
|
return excel_bytes, df_txn, df_acct, title
|
||||||
Loading…
Add table
Reference in a new issue