diff --git a/README.md b/README.md index eea5737..e8109ae 100644 --- a/README.md +++ b/README.md @@ -84,12 +84,16 @@ output/ 2008 Form 1040 Filing Instructions.doc Permanent File/ Driver's License.pdf + _unparsed/ + (files for this client that couldn't be fully parsed, kept for review) _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. +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. diff --git a/__pycache__/fccs_config.cpython-313.pyc b/__pycache__/fccs_config.cpython-313.pyc index 4c11bb3..e364cb9 100644 Binary files a/__pycache__/fccs_config.cpython-313.pyc and b/__pycache__/fccs_config.cpython-313.pyc differ diff --git a/__pycache__/fccs_reorganize.cpython-313.pyc b/__pycache__/fccs_reorganize.cpython-313.pyc new file mode 100644 index 0000000..dc228e4 Binary files /dev/null and b/__pycache__/fccs_reorganize.cpython-313.pyc differ diff --git a/fccs_config.py b/fccs_config.py index a8921b9..bbfbcc4 100644 --- a/fccs_config.py +++ b/fccs_config.py @@ -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 diff --git a/fccs_reorganize.py b/fccs_reorganize.py index 3fdaf7d..0f01c9b 100644 --- a/fccs_reorganize.py +++ b/fccs_reorganize.py @@ -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}") - dest_dir = os.path.join(output_dir, "_unparsed") + # 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)