updated the organizer logic so that unparased files at least still get put in the correct client folder
This commit is contained in:
@@ -85,11 +85,15 @@ output/
|
|||||||
Permanent File/
|
Permanent File/
|
||||||
Driver's License.pdf
|
Driver's License.pdf
|
||||||
_unparsed/
|
_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.
|
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.
BIN
__pycache__/fccs_reorganize.cpython-313.pyc
Normal file
BIN
__pycache__/fccs_reorganize.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
# 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")
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user