diff --git a/.dockerignore b/.dockerignore index d31f92e..cf0c894 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,4 @@ __pycache__/ .ruff_cache/ .git/ .claude/ +.venv/ diff --git a/.gitignore b/.gitignore index c7d137b..29c07d4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ logs/ __pycache__/ .ruff_cache/ .claude/ +.venv/ diff --git a/Dockerfile b/Dockerfile index 13cfd13..7e63096 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,11 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +# uv from PyPI rather than its ghcr.io image, for the same registry-reset reason as above +RUN pip install --no-cache-dir uv==0.11.28 +ENV UV_PYTHON_DOWNLOADS=never UV_NO_CACHE=1 PATH="/app/.venv/bin:$PATH" +COPY pyproject.toml uv.lock ./ +RUN uv sync --frozen COPY . . # headless, system Chromium (Playwright's own browser download is not used) diff --git a/app.py b/app.py index 86120c6..c207cb5 100644 --- a/app.py +++ b/app.py @@ -2,7 +2,7 @@ Env: APP_PASSWORD (sign-in password), SECRET_KEY (session signing; random if unset), COOKIE_SECURE=1 behind HTTPS, plus the Waystar/Aquilaehr logins mek_recon.py reads. -Run locally: python app.py -> http://127.0.0.1:8000 +Run locally: uv run app.py -> http://127.0.0.1:8000 """ import hmac import io @@ -107,6 +107,7 @@ def status(): log = LOG.read_text(encoding="utf-8", errors="replace") if LOG.exists() else "" return jsonify( running=running, started=run["started"], finished=run["finished"], code=run["code"], + locations=len(mek_recon.LOCATIONS), log="\n".join(log.splitlines()[-300:]), files=[{"name": f.name, "size": f.stat().st_size, "mtime": f.stat().st_mtime} for f in reports()], ) diff --git a/consolidate.py b/consolidate.py index d6ac2d8..5e6e72f 100644 --- a/consolidate.py +++ b/consolidate.py @@ -5,7 +5,7 @@ Aquilaehr: Patient ("Mary Lopez Garcia") + Visit Date Waystar: Patient Name ("LOPEZ, MARY") + From Date Match = same date, same first name, and at least one last-name word in common. -Run: python consolidate.py +Run: uv run consolidate.py """ from pathlib import Path @@ -15,6 +15,7 @@ ROOT = Path(__file__).parent WAYSTAR_DIR = ROOT / "processing" / "Waystar" AQUILAEHR_DIR = ROOT / "processing" / "Aquilaehr" OUT_DIR = ROOT / "output" +WATERMARK = str(ROOT / "static" / "report-watermark.png") SUFFIXES = {"JR", "SR", "II", "III", "IV"} @@ -130,20 +131,18 @@ def write_report(df, location, out): def write_xlsx(out, summary, df): - with pd.ExcelWriter(out, engine="openpyxl") as xw: - summary.to_excel(xw, sheet_name="Summary", index=False) - df.to_excel(xw, sheet_name="Reconciliation", index=False) - for ws in xw.book.worksheets: - ws.freeze_panes = "A2" - ws.auto_filter.ref = ws.dimensions - for col in ws.columns: - width = max(len(str(c.value or "")) for c in col[:200]) - ws.column_dimensions[col[0].column_letter].width = min(max(width + 2, 10), 60) - ws = xw.book["Reconciliation"] - for col in ws.iter_cols(min_row=1, max_row=ws.max_row): - fmt = {"DOS": "mm/dd/yyyy", "Charges": '"$"#,##0.00'}.get(col[0].value) - for c in col[1:] if fmt else (): - c.number_format = fmt + # xlsxwriter, not openpyxl: only it can set a sheet background (the eCare/Waystar/Aquilaehr watermark). + with pd.ExcelWriter(out, engine="xlsxwriter", date_format="mm/dd/yyyy") as xw: + money = xw.book.add_format({"num_format": '"$"#,##0.00'}) + for name, data in (("Summary", summary), ("Reconciliation", df)): + data.to_excel(xw, sheet_name=name, index=False) + ws = xw.sheets[name] + ws.set_background(WATERMARK) + ws.freeze_panes(1, 0) + ws.autofilter(0, 0, len(data), len(data.columns) - 1) + for i, col in enumerate(data.columns): + width = max([len(col)] + [len(str(v)) for v in data[col].head(199) if pd.notna(v)]) + ws.set_column(i, i, min(max(width + 2, 10), 60), money if col == "Charges" else None) ALL_FILE = "All Locations.xlsx" diff --git a/mek_recon.py b/mek_recon.py index f91eb5f..b1e166e 100644 --- a/mek_recon.py +++ b/mek_recon.py @@ -6,9 +6,9 @@ Aquilaehr: Reports -> Encounter & Claim Monitor -> pick practice -> Export CSV -> processing/Aquilaehr/.csv (same file name as Waystar so the pair lines up) Then consolidate.py matches Patient Name + DOS per location -> output/.xlsx -Run: python mek_recon.py both sites at the same time, all locations - python mek_recon.py Aquilaehr only Aquilaehr - python mek_recon.py Waystar "Healing Bridge" only Waystar, only that location +Run: uv run mek_recon.py both sites at the same time, all locations + uv run mek_recon.py Aquilaehr only Aquilaehr + uv run mek_recon.py Waystar "Healing Bridge" only Waystar, only that location Login is automatic from .env (WAYSTAR_USERNAME, WAYSTAR_PASSWORD, WAYSTAR_ANSWER_CAR, WAYSTAR_ANSWER_JOB, AQUILAEHR_USERNAME, AQUILAEHR_PASSWORD). If something is missing/unexpected, finish the login by hand in Chrome; the script waits. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b0677f1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "mek-charge-reconciliation" +version = "0.1.0" +description = "Waystar vs Aquilaehr charge reconciliation, per location, with a small web front end" +requires-python = ">=3.12" +dependencies = [ + "flask==3.1.0", + "gunicorn==23.0.0", + "pandas==3.0.3", + "playwright==1.49.0", + "xlsxwriter==3.3.0", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 24fc9bc..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -playwright==1.49.0 -pandas==3.0.3 -openpyxl==3.1.5 -flask==3.1.0 -gunicorn==23.0.0 diff --git a/static/aquilaehr-logo-light.png b/static/aquilaehr-logo-light.png new file mode 100644 index 0000000..6e8ed33 Binary files /dev/null and b/static/aquilaehr-logo-light.png differ diff --git a/static/aquilaehr-logo.png b/static/aquilaehr-logo.png new file mode 100644 index 0000000..cd0cbc8 Binary files /dev/null and b/static/aquilaehr-logo.png differ diff --git a/static/ecare-logo.png b/static/ecare-logo.png new file mode 100644 index 0000000..842c5e6 Binary files /dev/null and b/static/ecare-logo.png differ diff --git a/static/report-watermark.png b/static/report-watermark.png new file mode 100644 index 0000000..815fc6f Binary files /dev/null and b/static/report-watermark.png differ diff --git a/static/waystar-logo-light.png b/static/waystar-logo-light.png new file mode 100644 index 0000000..cfba634 Binary files /dev/null and b/static/waystar-logo-light.png differ diff --git a/static/waystar-logo.png b/static/waystar-logo.png new file mode 100644 index 0000000..6a6df00 Binary files /dev/null and b/static/waystar-logo.png differ diff --git a/templates/index.html b/templates/index.html index 0213beb..0a07be3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@ MEK Charge Reconciliation +