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

@@ -84,12 +84,16 @@ output/
2008 Form 1040 Filing Instructions.doc 2008 Form 1040 Filing Instructions.doc
Permanent File/ Permanent File/
Driver's License.pdf Driver's License.pdf
_unparsed/
(files for this client that couldn't be fully parsed, kept for review)
_unparsed/ _unparsed/
(files that couldn't be parsed go here for manual review) (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. 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 ### 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. 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():] return stem[anchors[-1].end():]
def client_name_from_files(files): def client_name_from_filename(filename):
"""Best-effort client name from a drawer's export filenames, or None. """Best-effort client name from a single export filename, or None.
Filenames are '{drawer}_{client}_{folder}_{MM-DD-YYYY}_{doc}.ext', so the Filenames are '{drawer}_{client}_{folder}_{MM-DD-YYYY}_{doc}.ext', so the
client name is the second underscore-delimited token. Client names carry client name is the second underscore-delimited token. Client names carry
commas/spaces but not underscores; the most common value across the drawer's commas/spaces but not underscores, so this token is reliable even when the
files is returned to shrug off any oddball filename. 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 = {} counts = {}
for f in files: for f in files:
parts = f.split("_") name = client_name_from_filename(f)
if len(parts) >= 2 and parts[1].strip(): if name:
name = parts[1].strip()
counts[name] = counts.get(name, 0) + 1 counts[name] = counts.get(name, 0) + 1
if not counts: if not counts:
return None 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 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 structure (recreates FCCS UI nesting):
output/{drawer_id}_{client_name}/Tax Documents/2025/{original_filename} output/{client_name}/Tax Documents/2025/{original_filename}
output/{drawer_id}_{client_name}/UltraTax CS/12-31-2008/{original_filename} output/{client_name}/UltraTax CS/12-31-2008/{original_filename}
output/{drawer_id}_{client_name}/Permanent File/{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 import os
@@ -28,6 +31,7 @@ import sys
from fccs_config import ( from fccs_config import (
parse_args, load_config, make_logger, parse_args, load_config, make_logger,
load_folder_list, build_folder_patterns, decompose_folder_path, 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 # Regex for the creation date field (MM-DD-YYYY) bounded by underscores
@@ -126,8 +130,17 @@ def main():
result = parse_filename(filename, folder_patterns) result = parse_filename(filename, folder_patterns)
if result is None: if result is None:
log(f" UNPARSED: {filename}") # Still recover the client from the leading tokens so the file lands
dest_dir = os.path.join(output_dir, "_unparsed") # 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 failed += 1
else: else:
drawer_id, client_name, folder_parts, date, doc_name = result drawer_id, client_name, folder_parts, date, doc_name = result
@@ -158,7 +171,9 @@ def main():
log("=" * 60) log("=" * 60)
log(f"Reorganization complete: {success} organized, {failed} unparsed") log(f"Reorganization complete: {success} organized, {failed} unparsed")
if failed: 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) log("=" * 60)