Spaces:
Runtime error
Runtime error
Download app.py from Bubbles30/RemittanceVerifier: direct link, hf CLI and curl.
- Browser
- Download file 8.85 kB
-
https://huggingface.co/spaces/Bubbles30/RemittanceVerifier/resolve/main/app.py
- Command line
-
hf download hf://spaces/Bubbles30/RemittanceVerifier/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Bubbles30/RemittanceVerifier/resolve/main/app.py
8.85 kB
| """Hugging Face Spaces / local Gradio entrypoint.""" | |
| from __future__ import annotations | |
| import sys | |
| import tempfile | |
| from decimal import Decimal | |
| from io import BytesIO | |
| from pathlib import Path | |
| from typing import Any, Optional | |
| import gradio as gr | |
| import pandas as pd | |
| ROOT = Path(__file__).resolve().parent | |
| SRC = ROOT / "src" | |
| if str(SRC) not in sys.path: | |
| sys.path.insert(0, str(SRC)) | |
| from invoice_reconciler.mapping import FieldMapping, records_from_table, suggest_mapping | |
| from invoice_reconciler.models import MatchStatus, ReconciliationResult | |
| from invoice_reconciler.parsers import parse_file | |
| from invoice_reconciler.parsers.base import ParsedTable | |
| from invoice_reconciler.reconciliation import reconcile | |
| from invoice_reconciler.reporting import export_excel_report | |
| def _save_upload(file_obj) -> tuple[Path, str]: | |
| """Persist a Gradio upload to a temp path; return path and original name.""" | |
| if file_obj is None: | |
| raise ValueError("No file uploaded") | |
| # gradio File: .name is temp path; .orig_name may exist on NamedString | |
| if isinstance(file_obj, str): | |
| path = Path(file_obj) | |
| return path, path.name | |
| name = getattr(file_obj, "orig_name", None) or Path(str(file_obj)).name | |
| src = Path(str(file_obj)) | |
| suffix = Path(name).suffix or src.suffix | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) | |
| tmp.write(src.read_bytes()) | |
| tmp.close() | |
| return Path(tmp.name), name | |
| def _fmt_money(v: Decimal | None) -> str: | |
| if v is None: | |
| return "" | |
| return f"{v:.2f}" | |
| def _preview_df(table: ParsedTable | None, limit: int = 15) -> pd.DataFrame: | |
| if table is None or not table.rows: | |
| return pd.DataFrame() | |
| return pd.DataFrame(table.preview(limit)) | |
| def _exceptions_df(result: ReconciliationResult) -> pd.DataFrame: | |
| rows = [] | |
| for e in result.exceptions: | |
| rows.append( | |
| { | |
| "Invoice Number": e.invoice_number, | |
| "Status": e.status.value, | |
| "Source Amount": _fmt_money(e.source_amount), | |
| "QuickBooks Amount": _fmt_money(e.quickbooks_amount), | |
| "Difference": _fmt_money(e.difference), | |
| "Source Page/Row": e.source_location, | |
| "QuickBooks Page/Row": e.quickbooks_location, | |
| "Reason": e.reason, | |
| } | |
| ) | |
| return pd.DataFrame(rows) | |
| def _summary_md(result: ReconciliationResult) -> str: | |
| s = result.summary | |
| return f"""### Summary Dashboard | |
| | Metric | Count | | |
| |---|---:| | |
| | Total Source Invoices | {s.total_source} | | |
| | Total QuickBooks Invoices | {s.total_quickbooks} | | |
| | Matched | {s.matched} | | |
| | Amount Mismatch | {s.amount_mismatch} | | |
| | Missing from QuickBooks | {s.missing_from_quickbooks} | | |
| | Extra in QuickBooks | {s.extra_in_quickbooks} | | |
| | Duplicates | {s.duplicates} | | |
| | Needs Review | {s.needs_review} | | |
| """ | |
| def parse_uploads( | |
| source_file, qb_file | |
| ) -> tuple[ | |
| Optional[ParsedTable], | |
| Optional[ParsedTable], | |
| gr.Dropdown, | |
| gr.Dropdown, | |
| gr.Dropdown, | |
| gr.Dropdown, | |
| pd.DataFrame, | |
| pd.DataFrame, | |
| str, | |
| ]: | |
| if source_file is None or qb_file is None: | |
| empty = gr.Dropdown(choices=[], value=None) | |
| return ( | |
| None, | |
| None, | |
| empty, | |
| empty, | |
| empty, | |
| empty, | |
| pd.DataFrame(), | |
| pd.DataFrame(), | |
| "Upload **both** Source of Truth and QuickBooks files, then click **Parse & detect fields**.", | |
| ) | |
| src_path, src_name = _save_upload(source_file) | |
| qb_path, qb_name = _save_upload(qb_file) | |
| source_table = parse_file(src_path, original_name=src_name) | |
| qb_table = parse_file(qb_path, original_name=qb_name) | |
| src_sug = suggest_mapping(source_table.columns) | |
| qb_sug = suggest_mapping(qb_table.columns) | |
| notes = [ | |
| f"Parsed **{src_name}**: {len(source_table.rows)} rows, columns={source_table.columns}", | |
| f"Parsed **{qb_name}**: {len(qb_table.rows)} rows, columns={qb_table.columns}", | |
| ] | |
| if source_table.ocr_required or source_table.ocr_used: | |
| notes.append("Source PDF may need / used OCR β confirm fields carefully.") | |
| if qb_table.ocr_required or qb_table.ocr_used: | |
| notes.append("QuickBooks PDF may need / used OCR β confirm fields carefully.") | |
| notes.append("Confirm Invoice Number and Gross Amount β do **not** assume the first money column.") | |
| return ( | |
| source_table, | |
| qb_table, | |
| gr.Dropdown(choices=source_table.columns, value=src_sug.invoice_column, interactive=True), | |
| gr.Dropdown(choices=source_table.columns, value=src_sug.amount_column, interactive=True), | |
| gr.Dropdown(choices=qb_table.columns, value=qb_sug.invoice_column, interactive=True), | |
| gr.Dropdown(choices=qb_table.columns, value=qb_sug.amount_column, interactive=True), | |
| _preview_df(source_table), | |
| _preview_df(qb_table), | |
| "\n\n".join(notes), | |
| ) | |
| def run_reconciliation( | |
| source_table: ParsedTable | None, | |
| qb_table: ParsedTable | None, | |
| src_inv: str | None, | |
| src_amt: str | None, | |
| qb_inv: str | None, | |
| qb_amt: str | None, | |
| ) -> tuple[str, pd.DataFrame, Optional[str], Optional[ReconciliationResult]]: | |
| if source_table is None or qb_table is None: | |
| return "Parse both files first.", pd.DataFrame(), None, None | |
| if not all([src_inv, src_amt, qb_inv, qb_amt]): | |
| return "Select Invoice Number and Gross Amount columns for both files.", pd.DataFrame(), None, None | |
| source_records = records_from_table( | |
| source_table, FieldMapping(invoice_column=src_inv, amount_column=src_amt) | |
| ) | |
| qb_records = records_from_table( | |
| qb_table, FieldMapping(invoice_column=qb_inv, amount_column=qb_amt) | |
| ) | |
| result = reconcile(source_records, qb_records) | |
| buf = BytesIO() | |
| export_excel_report(result, buf) | |
| out_path = Path(tempfile.gettempdir()) / "reconciliation_report.xlsx" | |
| out_path.write_bytes(buf.getvalue()) | |
| return _summary_md(result), _exceptions_df(result), str(out_path), result | |
| def build_demo() -> gr.Blocks: | |
| with gr.Blocks(title="Invoice Reconciler") as demo: | |
| gr.Markdown( | |
| """ | |
| # Invoice Reconciler | |
| Source of truth β QuickBooks Β· **parsers Β· engine Β· UI** stay separate | |
| """ | |
| ) | |
| source_state = gr.State(None) | |
| qb_state = gr.State(None) | |
| result_state = gr.State(None) | |
| with gr.Row(): | |
| source_file = gr.File( | |
| label="1. Upload Source of Truth (PDF / XLSX / XLS / CSV)", | |
| file_types=[".pdf", ".xlsx", ".xls", ".csv"], | |
| ) | |
| qb_file = gr.File( | |
| label="2. Upload QuickBooks (PDF / XLSX / XLS / CSV)", | |
| file_types=[".pdf", ".xlsx", ".xls", ".csv"], | |
| ) | |
| parse_btn = gr.Button("Parse & detect fields", variant="secondary") | |
| status = gr.Markdown() | |
| gr.Markdown("### 3. Review detected fields") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("**Source of Truth mapping**") | |
| src_inv = gr.Dropdown(label="Invoice Number β", choices=[], interactive=True) | |
| src_amt = gr.Dropdown( | |
| label="Gross Amount β", | |
| choices=[], | |
| interactive=True, | |
| info="Do not assume the first monetary column is the amount.", | |
| ) | |
| src_preview = gr.Dataframe(label="Source preview", interactive=False) | |
| with gr.Column(): | |
| gr.Markdown("**QuickBooks mapping**") | |
| qb_inv = gr.Dropdown(label="Invoice Number β", choices=[], interactive=True) | |
| qb_amt = gr.Dropdown(label="Gross Amount β", choices=[], interactive=True) | |
| qb_preview = gr.Dataframe(label="QuickBooks preview", interactive=False) | |
| run_btn = gr.Button("4. Run Reconciliation", variant="primary") | |
| summary = gr.Markdown("### 5. Summary Dashboard\n_Run reconciliation to see counts._") | |
| exceptions = gr.Dataframe(label="6. Detailed Exceptions", interactive=False) | |
| report = gr.File(label="7. Export Report") | |
| parse_btn.click( | |
| fn=parse_uploads, | |
| inputs=[source_file, qb_file], | |
| outputs=[ | |
| source_state, | |
| qb_state, | |
| src_inv, | |
| src_amt, | |
| qb_inv, | |
| qb_amt, | |
| src_preview, | |
| qb_preview, | |
| status, | |
| ], | |
| ) | |
| run_btn.click( | |
| fn=run_reconciliation, | |
| inputs=[source_state, qb_state, src_inv, src_amt, qb_inv, qb_amt], | |
| outputs=[summary, exceptions, report, result_state], | |
| ) | |
| return demo | |
| demo = build_demo() | |
| if __name__ == "__main__": | |
| demo.launch() | |