From 9116d1c04a94c301ba350e7d984277a8caa7ba16 Mon Sep 17 00:00:00 2001 From: dat972 Date: Mon, 20 Jul 2026 23:48:21 -0500 Subject: [PATCH] updated the organizer logic so that unparased files at least still get put in the correct client folder --- README.md | 6 +++- __pycache__/fccs_config.cpython-313.pyc | Bin 14862 -> 15182 bytes __pycache__/fccs_reorganize.cpython-313.pyc | Bin 0 -> 7875 bytes fccs_config.py | 25 ++++++++++++----- fccs_reorganize.py | 29 +++++++++++++++----- 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 __pycache__/fccs_reorganize.cpython-313.pyc 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 4c11bb370f624a9a71cb87aacf2a395df3ef879c..e364cb9a6613a76d08e284f92d17bae75e4ea8a3 100644 GIT binary patch delta 927 zcmYk4-%Aux6vywK+1+e(f9SeuYI!N69d})g(zFLD=yzm4wk1JmnRf5GTk~VNGnPc8 zU=K>=4{m`KR75}QsfUdoqKE#1sC+1L}QLKgAfSdeSq>dko zDVuP=2rgL)ci?3&DeZiPkQPHIg_APh+Dt;xl_V|vFId)$20W@)JH&U#H`dq%b3mtr z4izotkX%8hre;unDgJsmPHc-f`GQRx>l`%+IE?BA?TpAj4}lALfRm?6Zcx`LBSnqb zltgouo~5iUt2vYynv==HFI1A#3c8(8T%7(cL02pU=v8HQRJ`mH=mMw`yDt0`Lc_=w ziG(meJlu5|y+BPTE!s=xm}R6_n`Jj({~IiOH(&()3?YAHvUj|9X4_mv)5zY>Ym~8$ z(Th{ZM~7!R?pNOGy4Q27=R-s6T|?|+uzKqF_|W9#@ym0;rnhBHAIpO*0AI;q#NTfU z_dEpuCTd3}PN6V5kU+?k`d9P@Nu^coO*tj6ik*eFkJJjmIgV7rS6W`N(G7mahwnH+OVOS<9Qr0TY zPL>rRO9L}nH}@*5iX;^FpT?LShL3~PLMK-qppGjW+K}pTy`sQ&eHm-ebZpiJ`)m|o zdjR}b>&6yw?G{Dk7yzWLK!MQ6zEA$DG5k3govoY;#>PBf%7bIuxsj;ztwDkCVp-{G z;+f=o02Mx5+O<{j)(aWgV*xRC_C#P-;3>~`s`xIS=xl7Cfq+|KXl5ZtZ6}>AF!o4< ze2m(Iy0cX8mMz~@nuSug^OUy)8Ie-wj!}#o`OB^=A#q41+Ge5wR^kV{*DEi;6;reC c@M8D1+HxG4Z}j6W^P~b-E^LV7s->a-1%oZ@3;+NC delta 770 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 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)