added fccs_report.py this generates a report of all the errors in a clean html format
This commit is contained in:
11
README.md
11
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_reorganize.py` | Step 3: Parse filenames and rebuild folder structure |
|
||||||
| `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files |
|
| `fccs_verify.py` | Step 4 (optional): Compare manifests against exported files |
|
||||||
| `fccs_check.py` | Utility: Interactively check if specific drawers exported completely |
|
| `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 |
|
| `fccs_dump_controls.py` | Utility: Dump control identifiers of an on-screen dialog |
|
||||||
|
|
||||||
## Setup Per Engagement
|
## 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.
|
> 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
|
## Config Reference
|
||||||
|
|
||||||
All scripts read from `config.ini` (or specify `--config path\to\config.ini`).
|
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
|
- **`[timeouts]`** -- nav_timeout, dialog_timeout, progress_appear, progress_finish, settle, confirm_timeout
|
||||||
- **`[controls]`** -- FCCS window class names and button titles (rarely need changing)
|
- **`[controls]`** -- FCCS window class names and button titles (rarely need changing)
|
||||||
|
|||||||
Binary file not shown.
BIN
__pycache__/fccs_report.cpython-313.pyc
Normal file
BIN
__pycache__/fccs_report.cpython-313.pyc
Normal file
Binary file not shown.
@@ -9,6 +9,7 @@ ignore_file = C:\Migration\ignore.txt
|
|||||||
crashed_file = C:\Migration\crashed.txt
|
crashed_file = C:\Migration\crashed.txt
|
||||||
log_file = C:\Migration\run_log.txt
|
log_file = C:\Migration\run_log.txt
|
||||||
verify_report = C:\Migration\verify_report.txt
|
verify_report = C:\Migration\verify_report.txt
|
||||||
|
report_html = C:\Migration\progress_report.html
|
||||||
screenshot_dir = C:\Migration\screenshots
|
screenshot_dir = C:\Migration\screenshots
|
||||||
manifest_dir = C:\Migration\manifests
|
manifest_dir = C:\Migration\manifests
|
||||||
folder_list = fccs_folders.txt
|
folder_list = fccs_folders.txt
|
||||||
|
|||||||
@@ -229,6 +229,25 @@ def exported_doc_name(filename):
|
|||||||
return stem[anchors[-1].end():]
|
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):
|
def index_files_by_drawer(export_dir):
|
||||||
"""Map each export file to its leading drawer-ID token (before first '_').
|
"""Map each export file to its leading drawer-ID token (before first '_').
|
||||||
|
|
||||||
|
|||||||
209
fccs_report.py
Normal file
209
fccs_report.py
Normal file
@@ -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 = """<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Migration Progress Report</title>
|
||||||
|
<style>
|
||||||
|
* {{ box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: #1f2933; background: #f5f7fa; margin: 0; padding: 2.5rem 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}}
|
||||||
|
.sheet {{
|
||||||
|
max-width: 820px; margin: 0 auto; background: #fff; border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(16,24,40,.1), 0 1px 2px rgba(16,24,40,.06);
|
||||||
|
overflow: hidden;
|
||||||
|
}}
|
||||||
|
header {{
|
||||||
|
padding: 2rem 2.25rem 1.5rem; border-bottom: 1px solid #e4e7eb;
|
||||||
|
}}
|
||||||
|
h1 {{ margin: 0 0 .25rem; font-size: 1.5rem; font-weight: 650; }}
|
||||||
|
.date {{ color: #7b8794; font-size: .9rem; }}
|
||||||
|
.summary {{
|
||||||
|
display: flex; gap: 2rem; flex-wrap: wrap;
|
||||||
|
padding: 1.25rem 2.25rem; background: #fafbfc; border-bottom: 1px solid #e4e7eb;
|
||||||
|
}}
|
||||||
|
.stat .num {{ font-size: 1.75rem; font-weight: 680; line-height: 1; }}
|
||||||
|
.stat .lbl {{ color: #7b8794; font-size: .8rem; text-transform: uppercase;
|
||||||
|
letter-spacing: .03em; margin-top: .35rem; }}
|
||||||
|
.stat.good .num {{ color: #2e7d32; }}
|
||||||
|
.stat.flag .num {{ color: #c0392b; }}
|
||||||
|
.body {{ padding: 1.5rem 2.25rem 2.25rem; }}
|
||||||
|
.all-clear {{
|
||||||
|
text-align: center; padding: 2.5rem 1rem; color: #2e7d32; font-size: 1.1rem;
|
||||||
|
}}
|
||||||
|
.drawer {{
|
||||||
|
border: 1px solid #e4e7eb; border-radius: 8px; padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}}
|
||||||
|
.drawer:last-child {{ margin-bottom: 0; }}
|
||||||
|
.drawer h2 {{
|
||||||
|
margin: 0 0 .1rem; font-size: 1.05rem; font-weight: 620;
|
||||||
|
}}
|
||||||
|
.drawer .id {{ color: #486581; font-variant-numeric: tabular-nums; }}
|
||||||
|
.drawer .meta {{ color: #7b8794; font-size: .82rem; margin-bottom: .6rem; }}
|
||||||
|
ul.docs {{ margin: 0; padding: 0; list-style: none; }}
|
||||||
|
ul.docs li {{
|
||||||
|
padding: .3rem 0 .3rem 1.5rem; position: relative; font-size: .93rem;
|
||||||
|
border-top: 1px solid #f0f2f5;
|
||||||
|
}}
|
||||||
|
ul.docs li:first-child {{ border-top: none; }}
|
||||||
|
ul.docs li::before {{
|
||||||
|
content: "\\2717"; color: #c0392b; position: absolute; left: 0; font-weight: 700;
|
||||||
|
}}
|
||||||
|
footer {{
|
||||||
|
padding: 1rem 2.25rem; border-top: 1px solid #e4e7eb; color: #9aa5b1;
|
||||||
|
font-size: .78rem;
|
||||||
|
}}
|
||||||
|
@media print {{
|
||||||
|
body {{ background: #fff; padding: 0; }}
|
||||||
|
.sheet {{ box-shadow: none; max-width: none; }}
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sheet">
|
||||||
|
<header>
|
||||||
|
<h1>Migration Progress Report</h1>
|
||||||
|
<div class="date">{date}</div>
|
||||||
|
</header>
|
||||||
|
<div class="summary">
|
||||||
|
<div class="stat good"><div class="num">{complete}</div>
|
||||||
|
<div class="lbl">Drawers migrated</div></div>
|
||||||
|
<div class="stat flag"><div class="num">{incomplete}</div>
|
||||||
|
<div class="lbl">Drawers outstanding</div></div>
|
||||||
|
<div class="stat"><div class="num">{total}</div>
|
||||||
|
<div class="lbl">Total drawers</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
<footer>Generated {date} · Lists documents not yet present in the
|
||||||
|
export. Drawers not shown are fully migrated.</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_drawer_block(drawer_id, client, missing):
|
||||||
|
"""Return the HTML for one outstanding drawer."""
|
||||||
|
head = f'<span class="id">{html.escape(drawer_id)}</span>'
|
||||||
|
if client:
|
||||||
|
head += f' — {html.escape(client)}'
|
||||||
|
n = len(missing)
|
||||||
|
docs = "\n".join(
|
||||||
|
f" <li>{html.escape(m)}</li>" for m in missing
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
' <div class="drawer">\n'
|
||||||
|
f' <h2>{head}</h2>\n'
|
||||||
|
f' <div class="meta">{n} document{"s" if n != 1 else ""} '
|
||||||
|
'outstanding</div>\n'
|
||||||
|
f' <ul class="docs">\n{docs}\n </ul>\n'
|
||||||
|
' </div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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 = (' <div class="all-clear">All drawers are fully '
|
||||||
|
"migrated — no outstanding documents.</div>")
|
||||||
|
|
||||||
|
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()
|
||||||
Reference in New Issue
Block a user