From fa922955132c5b0c1b201ea9eaa0da57f6d561b6 Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 11 Nov 2025 02:48:06 +0100 Subject: [PATCH] Auto-generate virtual intermediate parent accounts during sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically creates missing intermediate parent accounts as virtual accounts. Problem: - Beancount has: Expenses:Supplies:Food, Expenses:Supplies:Office - Beancount does NOT have: Expenses:Supplies (intermediate parent) - Admin wants to grant permission on "Expenses:Supplies" to cover all Supplies:* accounts - But Expenses:Supplies doesn't exist in Castle DB Solution: During account sync, for each Beancount account, check if all parent levels exist. If any parent is missing, auto-create it as a virtual account. Example: Beancount accounts: - Expenses:Supplies:Food - Expenses:Supplies:Office - Expenses:Gas:Kitchen Auto-generated virtual parents: - Expenses:Supplies (virtual) - Expenses:Gas (virtual) - (Expenses already exists from migration) Benefits: - No manual creation needed - Always stays in sync with Beancount structure - Enables hierarchical permission grants at any level - Admin can now grant on "Expenses:Supplies" → user gets all Supplies:* children Changes: - Add Step 3 to sync: Auto-generate virtual intermediate parents - Track stats['virtual_parents_created'] - Skip parents that already exist (check all_account_names set) - Infer account type from parent name (e.g., Expenses:* → EXPENSE) - Mark auto-generated accounts with descriptive description 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude --- account_sync.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/account_sync.py b/account_sync.py index af97525..b5d277f 100644 --- a/account_sync.py +++ b/account_sync.py @@ -94,10 +94,17 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict: This ensures Castle DB has metadata entries for all accounts that exist in Beancount, enabling permissions and user associations to work properly. - New behavior (soft delete): + New behavior (soft delete + virtual parents): - Accounts in Beancount but not in Castle DB: Added as active - Accounts in Castle DB but not in Beancount: Marked as inactive (soft delete) - Inactive accounts that return to Beancount: Reactivated + - Missing intermediate parents: Auto-created as virtual accounts + + Virtual parent auto-generation example: + Beancount has: "Expenses:Supplies:Food" + Missing parent: "Expenses:Supplies" (doesn't exist in Beancount) + → Auto-create "Expenses:Supplies" as virtual account + → Enables granting permission on "Expenses:Supplies" to cover all Supplies:* children Args: force_full_sync: If True, re-check all accounts. If False, only add new ones. @@ -112,6 +119,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict: "accounts_skipped": 148, "accounts_deactivated": 5, "accounts_reactivated": 1, + "virtual_parents_created": 3, "errors": [] } """ @@ -150,6 +158,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict: "accounts_skipped": 0, "accounts_deactivated": 0, "accounts_reactivated": 0, + "virtual_parents_created": 0, "errors": [], } @@ -221,11 +230,50 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict: logger.error(error_msg) stats["errors"].append(error_msg) + # Step 3: Auto-generate virtual intermediate parent accounts + # For each account in Beancount, check if all parent levels exist + # If not, create them as virtual accounts + all_account_names = set(castle_accounts_by_name.keys()) + + for bc_account in beancount_accounts: + account_name = bc_account["account"] + parts = account_name.split(":") + + # Check each parent level (e.g., for "Expenses:Supplies:Food", check "Expenses:Supplies") + for i in range(1, len(parts)): + parent_name = ":".join(parts[:i]) + + # Skip if parent already exists + if parent_name in all_account_names: + continue + + # Create virtual parent account + try: + parent_type = infer_account_type_from_name(parent_name) + await create_account( + CreateAccount( + name=parent_name, + account_type=parent_type, + description=f"Auto-generated virtual parent for {parent_name}:* accounts", + is_virtual=True, + ) + ) + + stats["virtual_parents_created"] += 1 + all_account_names.add(parent_name) # Track so we don't create duplicates + logger.info(f"Created virtual parent account: {parent_name}") + + except Exception as e: + error_msg = f"Failed to create virtual parent {parent_name}: {e}" + logger.error(error_msg) + stats["errors"].append(error_msg) + logger.info( f"Account sync complete: " f"{stats['accounts_added']} added, " f"{stats['accounts_reactivated']} reactivated, " f"{stats['accounts_deactivated']} deactivated, " + f"{stats['virtual_parents_created']} virtual parents created, " f"{stats['accounts_skipped']} skipped, " f"{len(stats['errors'])} errors" )