updated script for reorganizing folders to handle the weird nesting that FCCS does

This commit is contained in:
2026-07-06 16:12:55 -05:00
parent 1ae9ba27a6
commit a664a00f4d
3 changed files with 74 additions and 29 deletions

View File

@@ -12,8 +12,10 @@ Examples:
01069_ABRAHAM, REBEKAH L._Permanent File_02-24-2018_Driver's License.pdf
01069_ABRAHAM, REBEKAH L._UltraTax CS 12-31-2008_02-12-2009_2008 Form 1040 Filing Instructions.doc
Output structure:
output/{drawer_id}_{client_name}/{folder_name}/{original_filename}
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}
Files that cannot be parsed go to output/_unparsed/ for manual review.
"""
@@ -25,7 +27,7 @@ import sys
from fccs_config import (
parse_args, load_config, make_logger,
load_folder_list, build_folder_patterns,
load_folder_list, build_folder_patterns, decompose_folder_path,
)
# Regex for the creation date field (MM-DD-YYYY) bounded by underscores
@@ -34,18 +36,22 @@ _DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_")
def parse_filename(filename, folder_patterns):
"""
Parse an FCCS export filename into (drawer_id, client_name, folder_name,
date, doc_name_with_ext) or return None if it cannot be parsed.
Parse an FCCS export filename into its components or return None.
folder_patterns is a list of (compiled_regex, template_name) from
build_folder_patterns(), sorted longest-first.
Returns (drawer_id, client_name, folder_parts, date, doc_name_with_ext)
where folder_parts is a tuple of nested path components, e.g.:
("Tax Documents", "2025") or ("UltraTax CS", "12-31-2008") or ("Permanent File",)
folder_patterns is a list of (compiled_regex, template_name, folder_type)
from build_folder_patterns(), sorted longest-first.
Strategy:
1. Drawer ID: first token before first underscore
2. Creation date: find MM-DD-YYYY pattern as anchor
3. Folder name: regex-match a known folder template at the end of
the text between client_name and creation_date
4. Client name: whatever is left between drawer_id and folder_name
4. Decompose the matched folder into nested path components
5. Client name: whatever is left between drawer_id and folder_name
"""
stem, ext = os.path.splitext(filename)
@@ -67,22 +73,22 @@ def parse_filename(filename, folder_patterns):
for m in date_matches:
date_str = m.group(1)
# Calculate positions relative to `rest` (adjust for prepended _)
# m.start() is the position of the leading _ in search_str
# In `rest`, the content before the date ends at (m.start() - 1)
before_date = rest[: m.start() - 1]
after_date_pos = m.start() - 1 + len("_") + len(date_str) + len("_")
doc_name = rest[after_date_pos:]
# Try each folder pattern against the end of before_date
for pattern, template_name in folder_patterns:
match = pattern.search(before_date)
if match and match.end() == len(before_date):
for pattern, template_name, folder_type in folder_patterns:
folder_match = pattern.search(before_date)
if folder_match and folder_match.end() == len(before_date):
# Folder matched at the end — check for underscore separator before it
folder_start = match.start()
folder_start = folder_match.start()
if folder_start > 0 and before_date[folder_start - 1] == "_":
client_name = before_date[: folder_start - 1]
folder_name = match.group() # the actual expanded name
return (drawer_id, client_name, folder_name, date_str, doc_name + ext)
folder_parts = decompose_folder_path(
folder_match, folder_type, template_name
)
return (drawer_id, client_name, folder_parts, date_str, doc_name + ext)
return None
@@ -124,11 +130,11 @@ def main():
dest_dir = os.path.join(output_dir, "_unparsed")
failed += 1
else:
drawer_id, client_name, folder_name, date, doc_name = result
drawer_id, client_name, folder_parts, date, doc_name = result
dest_dir = os.path.join(
output_dir,
f"{drawer_id}_{client_name}",
folder_name,
*folder_parts,
)
success += 1