updated the organizer logic so that unparased files at least still get put in the correct client folder

This commit is contained in:
2026-07-20 23:48:21 -05:00
parent 3b950cf886
commit 9116d1c04a
5 changed files with 45 additions and 15 deletions

View File

@@ -85,11 +85,15 @@ output/
Permanent File/
Driver's License.pdf
_unparsed/
(files that couldn't be parsed go here for manual review)
(files for this client that couldn't be fully parsed, kept for review)
_unparsed/
(only files whose client couldn't be recovered from the filename)
```
Exported filenames follow the format `{drawer_id}_{client_name}_{folder_name}_{creation_date}_{document_name}.ext`. The parser uses folder templates from `fccs_folders.txt` (with `YYYY` expanded via regex) and the creation date (`MM-DD-YYYY`) as anchors to reliably split the underscore-delimited fields. Folder names are decomposed into nested paths that match the FCCS UI structure (e.g. `2025 Tax Documents` becomes `Tax Documents/2025/`). UltraTax CS folders are matched by a built-in pattern.
Files that can't be fully parsed still keep their client: the drawer ID and client name are the first two underscore-delimited tokens and stay recoverable even when the folder/date parse fails, so those files are filed under `{client_name}/_unparsed/` (retaining their original filename). Only files whose client can't be recovered at all fall back to the top-level `_unparsed/`.
### Step 4 (optional): Verify Export Completeness
During export, each drawer's document list is captured from the FCCS dialog and saved as a manifest (in `manifest_dir`). These tools compare the manifests against the files actually in the export folder to confirm nothing was missed.

Binary file not shown.

View File

@@ -229,19 +229,30 @@ 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.
def client_name_from_filename(filename):
"""Best-effort client name from a single export filename, 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.
commas/spaces but not underscores, so this token is reliable even when the
fuller parse (folder/date matching) fails.
"""
parts = filename.split("_")
if len(parts) >= 2 and parts[1].strip():
return parts[1].strip()
return None
def client_name_from_files(files):
"""Best-effort client name from a drawer's export filenames, or None.
The most common per-file client name 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()
name = client_name_from_filename(f)
if name:
counts[name] = counts.get(name, 0) + 1
if not counts:
return None

View File

@@ -13,11 +13,14 @@ Examples:
01069_ABRAHAM, REBEKAH L._UltraTax CS 12-31-2008_02-12-2009_2008 Form 1040 Filing Instructions.doc
Output structure (recreates FCCS UI nesting):
output/{drawer_id}_{client_name}/Tax Documents/2025/{original_filename}
output/{drawer_id}_{client_name}/UltraTax CS/12-31-2008/{original_filename}
output/{drawer_id}_{client_name}/Permanent File/{original_filename}
output/{client_name}/Tax Documents/2025/{original_filename}
output/{client_name}/UltraTax CS/12-31-2008/{original_filename}
output/{client_name}/Permanent File/{original_filename}
Files that cannot be parsed go to output/_unparsed/ for manual review.
Files that cannot be parsed are still filed under their client:
output/{client_name}/_unparsed/{original_filename}
Only files whose client can't be recovered from the leading tokens fall back to
the top-level output/_unparsed/ for manual review.
"""
import os
@@ -28,6 +31,7 @@ import sys
from fccs_config import (
parse_args, load_config, make_logger,
load_folder_list, build_folder_patterns, decompose_folder_path,
client_name_from_filename,
)
# Regex for the creation date field (MM-DD-YYYY) bounded by underscores
@@ -126,8 +130,17 @@ def main():
result = parse_filename(filename, folder_patterns)
if result is None:
log(f" UNPARSED: {filename}")
# Still recover the client from the leading tokens so the file lands
# in that client's folder (in an _unparsed subfolder to flag it),
# rather than a single top-level bucket. Fall back to the top-level
# _unparsed only when even the client can't be determined.
client_name = client_name_from_filename(filename)
if client_name:
dest_dir = os.path.join(output_dir, client_name, "_unparsed")
log(f" UNPARSED (filed under {client_name}): {filename}")
else:
dest_dir = os.path.join(output_dir, "_unparsed")
log(f" UNPARSED (no client): {filename}")
failed += 1
else:
drawer_id, client_name, folder_parts, date, doc_name = result
@@ -158,7 +171,9 @@ def main():
log("=" * 60)
log(f"Reorganization complete: {success} organized, {failed} unparsed")
if failed:
log(f"Review unparsed files in: {os.path.join(output_dir, '_unparsed')}")
log("Review unparsed files in each client's _unparsed subfolder "
f"(and {os.path.join(output_dir, '_unparsed')} for any without a "
"detectable client).")
log("=" * 60)