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
|
This ensures Castle DB has metadata entries for all accounts that exist
|
||||||
in Beancount, enabling permissions and user associations to work properly.
|
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 Beancount but not in Castle DB: Added as active
|
||||||
- Accounts in Castle DB but not in Beancount: Marked as inactive (soft delete)
|
- Accounts in Castle DB but not in Beancount: Marked as inactive (soft delete)
|
||||||
- Inactive accounts that return to Beancount: Reactivated
|
- 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:
|
Args:
|
||||||
force_full_sync: If True, re-check all accounts. If False, only add new ones.
|
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_skipped": 148,
|
||||||
"accounts_deactivated": 5,
|
"accounts_deactivated": 5,
|
||||||
"accounts_reactivated": 1,
|
"accounts_reactivated": 1,
|
||||||
|
"virtual_parents_created": 3,
|
||||||
"errors": []
|
"errors": []
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
@ -150,6 +158,7 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
||||||
"accounts_skipped": 0,
|
"accounts_skipped": 0,
|
||||||
"accounts_deactivated": 0,
|
"accounts_deactivated": 0,
|
||||||
"accounts_reactivated": 0,
|
"accounts_reactivated": 0,
|
||||||
|
"virtual_parents_created": 0,
|
||||||
"errors": [],
|
"errors": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -221,11 +230,50 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
stats["errors"].append(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(
|
logger.info(
|
||||||
f"Account sync complete: "
|
f"Account sync complete: "
|
||||||
f"{stats['accounts_added']} added, "
|
f"{stats['accounts_added']} added, "
|
||||||
f"{stats['accounts_reactivated']} reactivated, "
|
f"{stats['accounts_reactivated']} reactivated, "
|
||||||
f"{stats['accounts_deactivated']} deactivated, "
|
f"{stats['accounts_deactivated']} deactivated, "
|
||||||
|
f"{stats['virtual_parents_created']} virtual parents created, "
|
||||||
f"{stats['accounts_skipped']} skipped, "
|
f"{stats['accounts_skipped']} skipped, "
|
||||||
f"{len(stats['errors'])} errors"
|
f"{len(stats['errors'])} errors"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue