updated the reorganize script to handle nested folders inside of template files like 'YYYY' Income Documents > 2016 > Joe Shmo 2016

This commit is contained in:
2026-08-24 21:43:43 -05:00
parent 253e4fe419
commit 5e7630bf5c
3 changed files with 32 additions and 7 deletions

View File

@@ -97,6 +97,8 @@ output/
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.
FCCS drawers can (rarely) contain **nested subfolders** under a template folder; the export encodes these in square brackets appended to the folder field, e.g. `2016 Income Documents[4201 N Beach Street, LLC]`. The reorganizer recreates them as deeper nesting — that example becomes `Income Documents/2016/4201 N Beach Street, LLC/` — as long as the parent (`YYYY Income Documents`) is a listed template; no bracket entries are needed in `fccs_folders.txt`.
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/`. 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/`.
Folder names are sanitized for Windows before use — trailing spaces and periods are stripped from each path component, since Windows can't create a directory ending in a space or dot (e.g. a client named `FARR GROUP, P.L.` becomes `FARR GROUP, P.L`). Reorganization is also resilient per file: if one file can't be placed for any reason, the error is logged and counted (reported as `errored` in the summary) and the run continues with the rest rather than aborting. Folder names are sanitized for Windows before use — trailing spaces and periods are stripped from each path component, since Windows can't create a directory ending in a space or dot (e.g. a client named `FARR GROUP, P.L.` becomes `FARR GROUP, P.L`). Reorganization is also resilient per file: if one file can't be placed for any reason, the error is logged and counted (reported as `errored` in the summary) and the run continues with the rest rather than aborting.

View File

@@ -36,6 +36,10 @@ from fccs_config import (
# Regex for the creation date field (MM-DD-YYYY) bounded by underscores # Regex for the creation date field (MM-DD-YYYY) bounded by underscores
_DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_") _DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_")
# Trailing "[Subfolder Name]" on the folder field — FCCS encodes nested
# subfolders (e.g. Income Documents > 2016 > "4201 N Beach Street, LLC") as
# "2016 Income Documents[4201 N Beach Street, LLC]" in the export filename.
_SUBFOLDER_RE = re.compile(r"\[([^\[\]]+)\]$")
def parse_filename(filename, folder_patterns): def parse_filename(filename, folder_patterns):
@@ -81,17 +85,29 @@ def parse_filename(filename, folder_patterns):
after_date_pos = m.start() - 1 + len("_") + len(date_str) + len("_") after_date_pos = m.start() - 1 + len("_") + len(date_str) + len("_")
doc_name = rest[after_date_pos:] doc_name = rest[after_date_pos:]
# Try each folder pattern against the end of before_date # Peel any trailing [Subfolder] groups off the folder field so the
# remainder can match a template; each becomes one more nested path
# component (in order, so "a[b][c]" nests as a/b/c).
folder_text = before_date
sub_parts = []
while True:
sm = _SUBFOLDER_RE.search(folder_text)
if not sm:
break
sub_parts.insert(0, sm.group(1))
folder_text = folder_text[: sm.start()]
# Try each folder pattern against the end of the folder text
for pattern, template_name, folder_type in folder_patterns: for pattern, template_name, folder_type in folder_patterns:
folder_match = pattern.search(before_date) folder_match = pattern.search(folder_text)
if folder_match and folder_match.end() == len(before_date): if folder_match and folder_match.end() == len(folder_text):
# Folder matched at the end — check for underscore separator before it # Folder matched at the end — check for underscore separator before it
folder_start = folder_match.start() folder_start = folder_match.start()
if folder_start > 0 and before_date[folder_start - 1] == "_": if folder_start > 0 and folder_text[folder_start - 1] == "_":
client_name = before_date[: folder_start - 1] client_name = folder_text[: folder_start - 1]
folder_parts = decompose_folder_path( folder_parts = decompose_folder_path(
folder_match, folder_type, template_name folder_match, folder_type, template_name
) ) + tuple(sub_parts)
return (drawer_id, client_name, folder_parts, date_str, doc_name + ext) return (drawer_id, client_name, folder_parts, date_str, doc_name + ext)
return None return None

View File

@@ -26,6 +26,8 @@ from fccs_config import parse_args, load_config
_DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_") _DATE_RE = re.compile(r"_(\d{2}-\d{2}-\d{4})_")
# A standalone year inside a folder name, e.g. "2013 Tax Documents". # A standalone year inside a folder name, e.g. "2013 Tax Documents".
_YEAR_RE = re.compile(r"\b(19|20)\d{2}\b") _YEAR_RE = re.compile(r"\b(19|20)\d{2}\b")
# Trailing "[Subfolder]" group(s) — FCCS's encoding for nested subfolders.
_SUBFOLDER_RE = re.compile(r"(?:\[[^\[\]]+\])+$")
UNPARSED_DIRNAME = "_unparsed" UNPARSED_DIRNAME = "_unparsed"
@@ -55,8 +57,13 @@ def suggest_template(folder_field):
"""Generalize a concrete folder field into an fccs_folders.txt line. """Generalize a concrete folder field into an fccs_folders.txt line.
e.g. "2013 Tax Documents" -> "YYYY Tax Documents". Fields without a year e.g. "2013 Tax Documents" -> "YYYY Tax Documents". Fields without a year
are suggested as-is (static folders). are suggested as-is (static folders). Trailing [Subfolder] groups (FCCS's
nested-subfolder encoding) are dropped so the suggestion is the parent
template line, which is what fccs_folders.txt actually takes.
""" """
stripped = _SUBFOLDER_RE.sub("", folder_field).strip()
if stripped:
folder_field = stripped
return _YEAR_RE.sub("YYYY", folder_field) return _YEAR_RE.sub("YYYY", folder_field)