From b270ca91865be79f80b03d57b1c010f32243e546 Mon Sep 17 00:00:00 2001 From: dat972 Date: Sun, 19 Jul 2026 21:47:31 -0500 Subject: [PATCH] added fccs_report.py this generates a report of all the errors in a clean html format --- README.md | 11 +- __pycache__/fccs_config.cpython-313.pyc | Bin 13922 -> 14862 bytes __pycache__/fccs_report.cpython-313.pyc | Bin 0 -> 9016 bytes config.ini | 1 + fccs_config.py | 19 +++ fccs_report.py | 209 ++++++++++++++++++++++++ 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 __pycache__/fccs_report.cpython-313.pyc create mode 100644 fccs_report.py diff --git a/README.md b/README.md index bc8c673..aacbf0e 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Extracts and organizes documents from FileCabinet CS (Thomson Reuters) using GUI | `fccs_reorganize.py` | Step 3: Parse filenames and rebuild folder structure | | `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files | | `fccs_check.py` | Utility: Interactively check if specific drawers exported completely | +| `fccs_report.py` | Utility: Generate a clean client-facing HTML progress report (issues only) | | `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog | ## Setup Per Engagement @@ -115,10 +116,18 @@ Drawer ID(s): 08097 18430 > Note: because page-splitting means the number of files can't be mapped one-to-one to documents, completeness is judged by document *presence* (is each manifest document represented by at least one exported file), not by exact file counts. +**Client-facing progress report:** For a clean summary to share with the client, generate an HTML report of outstanding work only: + +``` +python fccs_report.py +``` + +This runs the same document-level check as `fccs_verify.py` but writes a self-contained, print-friendly HTML file (`report_html`, default `C:\Migration\progress_report.html`) that lists **only** the drawers with missing documents, each with the client name and the specific documents still outstanding. Fully-migrated drawers are omitted (a headline shows how many are done). Drawers with no manifest or in `crashed.txt` are excluded since they reflect internal tooling state, not client-facing progress. Open it in any browser and print to PDF to send. + ## Config Reference All scripts read from `config.ini` (or specify `--config path\to\config.ini`). -- **`[paths]`** -- backup_dir, export_dir, output_dir, drawer_id_file, completed_file, ignore_file, crashed_file, log_file, verify_report, screenshot_dir, manifest_dir, folder_list +- **`[paths]`** -- backup_dir, export_dir, output_dir, drawer_id_file, completed_file, ignore_file, crashed_file, log_file, verify_report, report_html, screenshot_dir, manifest_dir, folder_list - **`[timeouts]`** -- nav_timeout, dialog_timeout, progress_appear, progress_finish, settle, confirm_timeout - **`[controls]`** -- FCCS window class names and button titles (rarely need changing) diff --git a/__pycache__/fccs_config.cpython-313.pyc b/__pycache__/fccs_config.cpython-313.pyc index c2e53a723fd14fa08f860229a201e12ea8fe870b..4c11bb370f624a9a71cb87aacf2a395df3ef879c 100644 GIT binary patch delta 1040 zcmY*YO=uHA6rRa;({`K0r17UVpyOZDl_t@YLaox`4@J;cYz!(zk+=}3q&)-BU9&QrgdIF8%#sGBss}(?T4Yn6u9pDl9sv3_2tFmr?zFVRO)RT(=U4gb zdj2Ps18F!=yO`$hnyyLJbnqm?Qc{fLp`}rdeWw6u00^XEs|az^c{xM!V14zA>p}xw zQE0}y#j_r$wc24W*nycH!?_%xHp&`#Y}v@tOpJ1rm`Fo9)y`lVWeAT93rQ|-U=e3+ zh)~o|EF4#q-bE5=6r-qnRBanO8^=Rh|H+7x~3Nf4|mW~`t$CPCW#W5Wl zc{6Wg9ogg*w&G~#-+~y*YLt#DSz?+ROR!-riy1oZ@N$X$PZGAZl6=f0j5ox^#6qVv z!@)?)Qo^c7FLtGfb{OIXQEWTZs+^423F-_XlFK2@8bw6ck86hUr}MZ{hMAAz0%%Zx z@cLCq_wZGf={~{rf-#Pr+$~E(*e*+^HloU9xpG|DnkrNv`i$L4MZ|C^+L? zrO?%$k~6t~V&v|*iF0pOCuUbC=E6;-P_gq?cd>gmysofoF0$rs*F@K({$%@XB>pTk z6G;|&$M=4%2Z8W+1^620mV`yR(?T`V&@x@qGWBbt&m+_8ya?AHyDU#~=f~9TScBYr zkGu4NfXaX#3h$AC$L_173h%g|l9A2yCTGe%Q?v3p{sc36p3?h*@>0wk!>%MK*Hxb# z*7@Gf_JiK%T%o|J8|@b$jJQo5my%)}bW(tb(RKj=0hJ-vZ8M4P7`U6h;GFn9*-iI# V$Cbt)jDBnJ!O+60HrQC%@EemW9|-^e delta 217 zcmeAxd6dKVnU|M~0SIf3nr(SX);co+-D|je2XL``pt4(RMXAN5IVE5tZgHem6yz6`q^1CM-!cnlTMbn4onf=N wc@ra})Z|?jEo>m0i&jtew3Ovq2V^n=aq+~-1(rR$JdA>$<#ZT@iuizH00|yK0{{R3 diff --git a/__pycache__/fccs_report.cpython-313.pyc b/__pycache__/fccs_report.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97fd8a970a2beda7eebfe7b6f60a6287046878bc GIT binary patch literal 9016 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 QKlcRw>?I)D)1|NWzh)o_y#N3J literal 0 HcmV?d00001 diff --git a/config.ini b/config.ini index d2e3ecf..6adb962 100644 --- a/config.ini +++ b/config.ini @@ -9,6 +9,7 @@ ignore_file = C:\Migration\ignore.txt crashed_file = C:\Migration\crashed.txt log_file = C:\Migration\run_log.txt verify_report = C:\Migration\verify_report.txt +report_html = C:\Migration\progress_report.html screenshot_dir = C:\Migration\screenshots manifest_dir = C:\Migration\manifests folder_list = fccs_folders.txt diff --git a/fccs_config.py b/fccs_config.py index 1a29108..a8921b9 100644 --- a/fccs_config.py +++ b/fccs_config.py @@ -229,6 +229,25 @@ def exported_doc_name(filename): return stem[anchors[-1].end():] +def client_name_from_files(files): + """Best-effort client name from a drawer's export filenames, or None. + + Filenames are '{drawer}_{client}_{folder}_{MM-DD-YYYY}_{doc}.ext', so the + client name is the second underscore-delimited token. Client names carry + commas/spaces but not underscores; the most common value across the drawer's + files is returned to shrug off any oddball filename. + """ + counts = {} + for f in files: + parts = f.split("_") + if len(parts) >= 2 and parts[1].strip(): + name = parts[1].strip() + counts[name] = counts.get(name, 0) + 1 + if not counts: + return None + return max(counts, key=counts.get) + + def index_files_by_drawer(export_dir): """Map each export file to its leading drawer-ID token (before first '_'). diff --git a/fccs_report.py b/fccs_report.py new file mode 100644 index 0000000..121430c --- /dev/null +++ b/fccs_report.py @@ -0,0 +1,209 @@ +""" +Utility: Generate a client-facing progress report (HTML). + +Runs the same document-level completeness check as fccs_verify.py, but produces +a clean, self-contained HTML file listing ONLY the drawers with outstanding +items and, for each, the specific documents still missing from the export. It's +meant as a "check-in progress" document you can send to the client. + +Drawers that are fully exported, that have no manifest, or that only appear in +crashed.txt are intentionally left out — the report shows outstanding work, not +internal tooling state. A headline shows how many drawers are fully migrated so +the overall progress is clear. + +Output goes to `report_html` (default C:\\Migration\\progress_report.html). + +USAGE +----- + python fccs_report.py +""" + +import os +import sys +import html +from datetime import datetime + +from fccs_config import ( + parse_args, load_config, + evaluate_drawer, index_files_by_drawer, client_name_from_files, +) + + +PAGE_TEMPLATE = """ + + + + +Migration Progress Report + + + +
+
+

Migration Progress Report

+
{date}
+
+
+
{complete}
+
Drawers migrated
+
{incomplete}
+
Drawers outstanding
+
{total}
+
Total drawers
+
+
+{content} +
+
Generated {date} · Lists documents not yet present in the + export. Drawers not shown are fully migrated.
+
+ + +""" + + +def build_drawer_block(drawer_id, client, missing): + """Return the HTML for one outstanding drawer.""" + head = f'{html.escape(drawer_id)}' + if client: + head += f' — {html.escape(client)}' + n = len(missing) + docs = "\n".join( + f"
  • {html.escape(m)}
  • " for m in missing + ) + return ( + '
    \n' + f'

    {head}

    \n' + f'
    {n} document{"s" if n != 1 else ""} ' + 'outstanding
    \n' + f'
      \n{docs}\n
    \n' + '
    ' + ) + + +def main(): + args = parse_args() + cfg = load_config(args.config) + + export_dir = cfg.get("paths", "export_dir") + manifest_dir = cfg.get("paths", "manifest_dir") + report_html = cfg.get("paths", "report_html") + + if not os.path.isdir(manifest_dir): + print(f"ERROR: manifest directory not found: {manifest_dir}") + print("Run fccs_export.py first to generate manifests.") + sys.exit(1) + if not os.path.isdir(export_dir): + print(f"ERROR: export directory not found: {export_dir}") + sys.exit(1) + + drawer_ids = sorted( + os.path.splitext(f)[0] + for f in os.listdir(manifest_dir) + if f.endswith(".txt") + ) + if not drawer_ids: + print("No manifest files found.") + sys.exit(1) + + files_by_drawer = index_files_by_drawer(export_dir) + + complete = 0 + outstanding = [] # (drawer_id, client, missing_list) + for drawer_id in drawer_ids: + files = files_by_drawer.get(drawer_id, []) + r = evaluate_drawer(drawer_id, files, manifest_dir) + if r["missing"]: + client = client_name_from_files(files) + outstanding.append((drawer_id, client, r["missing"])) + else: + complete += 1 + + total = len(drawer_ids) + date_str = f"{datetime.now():%B %d, %Y}" + + if outstanding: + content = "\n".join( + build_drawer_block(d, c, m) for d, c, m in outstanding + ) + else: + content = ('
    All drawers are fully ' + "migrated — no outstanding documents.
    ") + + page = PAGE_TEMPLATE.format( + date=html.escape(date_str), + complete=complete, + incomplete=len(outstanding), + total=total, + content=content, + ) + + with open(report_html, "w", encoding="utf-8") as f: + f.write(page) + + print(f"Progress report written to {report_html}") + print(f" {complete}/{total} drawers fully migrated, " + f"{len(outstanding)} outstanding.") + if outstanding: + print(" Outstanding: " + ", ".join(d for d, _, _ in outstanding)) + + +if __name__ == "__main__": + main()