Files
FCCS/fccs_reorganize.py

159 lines
5.5 KiB
Python

"""
Step 3: Reorganize flat exported files into a proper folder structure.
Parses FCCS export filenames using known folder templates and date anchors
to reconstruct the original drawer/folder hierarchy.
Filename format:
{drawer_id}_{client_name}_{folder_name}_{creation_date MM-DD-YYYY}_{document_name}.ext
Examples:
01069_ABRAHAM, REBEKAH L._2025 Tax Documents_03-03-2026_030126 E-mail re Tax Info.pdf
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}
Files that cannot be parsed go to output/_unparsed/ for manual review.
"""
import os
import re
import shutil
import sys
from fccs_config import (
parse_args, load_config, make_logger,
load_folder_list, build_folder_patterns,
)
# Regex for the creation date field (MM-DD-YYYY) bounded by underscores
_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.
folder_patterns is a list of (compiled_regex, template_name) 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
"""
stem, ext = os.path.splitext(filename)
# 1. Drawer ID — first underscore-delimited token
sep = stem.find("_")
if sep == -1:
return None
drawer_id = stem[:sep]
rest = stem[sep + 1:]
# 2. Find all creation date candidates (MM-DD-YYYY bounded by underscores)
# Prepend underscore so a date right at the start of `rest` is also found
search_str = "_" + rest
date_matches = list(_DATE_RE.finditer(search_str))
if not date_matches:
return None
# 3. Try each date match; for each, try to match a known folder template
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):
# Folder matched at the end — check for underscore separator before it
folder_start = 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)
return None
def main():
args = parse_args()
cfg = load_config(args.config)
log = make_logger(cfg.get("paths", "log_file"))
export_dir = cfg.get("paths", "export_dir")
output_dir = cfg.get("paths", "output_dir")
folder_list_path = cfg.get("paths", "folder_list")
if not os.path.isdir(export_dir):
log(f"ERROR: export directory not found: {export_dir}")
sys.exit(1)
folder_templates = load_folder_list(folder_list_path)
if not folder_templates:
log("WARNING: folder list is empty. Only built-in patterns (UltraTax CS) will match.")
folder_patterns = build_folder_patterns(folder_templates)
log(f"Loaded {len(folder_patterns)} folder patterns")
files = [
f for f in os.listdir(export_dir)
if os.path.isfile(os.path.join(export_dir, f))
]
log(f"Found {len(files)} files to reorganize")
success = 0
failed = 0
for filename in files:
result = parse_filename(filename, folder_patterns)
if result is None:
log(f" UNPARSED: {filename}")
dest_dir = os.path.join(output_dir, "_unparsed")
failed += 1
else:
drawer_id, client_name, folder_name, date, doc_name = result
dest_dir = os.path.join(
output_dir,
f"{drawer_id}_{client_name}",
folder_name,
)
success += 1
os.makedirs(dest_dir, exist_ok=True)
src = os.path.join(export_dir, filename)
dst = os.path.join(dest_dir, filename)
# Handle duplicate filenames
if os.path.exists(dst):
base, fext = os.path.splitext(filename)
counter = 1
while os.path.exists(dst):
dst = os.path.join(dest_dir, f"{base}_{counter}{fext}")
counter += 1
log(f" DUPLICATE renamed: {filename} -> {os.path.basename(dst)}")
shutil.copy2(src, dst)
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("=" * 60)
if __name__ == "__main__":
main()