Auto-generate virtual intermediate parent accounts during sync
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 <noreply@anthropic.com>
This commit is contained in:
parent
2ebc9af798
commit
fa92295513
1 changed files with 49 additions and 1 deletions
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue