castle/account_utils.py
padreug b97e899983 Update default expense accounts to optimized structure
Reorganizes 22 old expense accounts into 31 new accounts with:
- 6 logical categories (Supplies, Materials, Equipment, Utilities, Maintenance, Services)
- Consistent 3-level hierarchy throughout
- Clear groupings that map to virtual parent permission grants

Matches the structure in castle-ledger.beancount for consistency.

Categories:
- Supplies: consumables bought regularly (7 accounts)
- Materials: construction/building materials (2 accounts)
- Equipment: durable goods that last (3 accounts)
- Utilities: ongoing service bills (5 accounts)
- Maintenance: repairs & upkeep (4 accounts)
- Services: professional services & subscriptions (6 accounts)

Benefits:
- Virtual parents auto-generated for each category
- Permission grants more intuitive and efficient
- No conflicting parent/child account names

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-11 03:09:44 +01:00

255 lines
9.3 KiB
Python

"""
Account naming utilities for hierarchical account structure.
Implements Beancount-style account naming conventions.
"""
from typing import Optional
from .models import AccountType
# Mapping from internal account types to Beancount root names
ACCOUNT_TYPE_ROOTS = {
AccountType.ASSET: "Assets",
AccountType.LIABILITY: "Liabilities",
AccountType.EQUITY: "Equity",
AccountType.REVENUE: "Income", # Beancount uses "Income" not "Revenue"
AccountType.EXPENSE: "Expenses",
}
def format_hierarchical_account_name(
account_type: AccountType,
base_name: str,
user_id: Optional[str] = None
) -> str:
"""
Format account name in hierarchical Beancount-style.
Examples:
format_hierarchical_account_name(AccountType.ASSET, "Cash")
"Assets:Cash"
format_hierarchical_account_name(AccountType.ASSET, "Accounts Receivable", "af983632")
"Assets:Receivable:User-af983632"
format_hierarchical_account_name(AccountType.EXPENSE, "Food & Supplies")
"Expenses:Food:Supplies"
Args:
account_type: The type of account (asset, liability, etc.)
base_name: The base name like "Cash", "Accounts Receivable", "Food & Supplies"
user_id: Optional user ID for user-specific accounts
Returns:
Hierarchical account name string
"""
root = ACCOUNT_TYPE_ROOTS[account_type]
# Clean up the base name:
# 1. Remove "Accounts" prefix (e.g., "Accounts Receivable" → "Receivable")
# 2. Replace " & " with ":" for hierarchy (e.g., "Food & Supplies" → "Food:Supplies")
# 3. Remove extra spaces
clean_name = base_name.replace("Accounts ", "").replace(" & ", ":").strip()
# Build hierarchical path
if user_id:
# For user-specific accounts, add user suffix
# "Receivable" + "af983632" → "Receivable:User-af983632"
user_suffix = f"User-{user_id[:8]}"
return f"{root}:{clean_name}:{user_suffix}"
else:
# Regular account
return f"{root}:{clean_name}"
def parse_legacy_account_name(name: str) -> tuple[str, Optional[str]]:
"""
Parse legacy account names like "Accounts Receivable - af983632"
into (base_name, user_id).
Used only for migration from old format to hierarchical.
Args:
name: Legacy account name
Returns:
Tuple of (base_name, user_id or None)
Examples:
parse_legacy_account_name("Accounts Receivable - af983632")
→ ("Accounts Receivable", "af983632")
parse_legacy_account_name("Cash")
→ ("Cash", None)
"""
if " - " in name:
parts = name.split(" - ", 1)
base_name = parts[0].strip()
user_id = parts[1].strip()
return base_name, user_id
else:
return name, None
def format_account_display_name(hierarchical_name: str) -> str:
"""
Convert hierarchical name to human-readable display name.
Examples:
format_account_display_name("Assets:Receivable:User-af983632")
"Accounts Receivable - af983632"
format_account_display_name("Expenses:Food:Supplies")
"Food & Supplies"
Args:
hierarchical_name: Hierarchical account name
Returns:
Human-readable display name
"""
parts = hierarchical_name.split(":")
if len(parts) < 2:
return hierarchical_name
# Skip the root (Assets, Liabilities, etc.)
body_parts = parts[1:]
# Check for user suffix
if len(body_parts) > 1 and body_parts[-1].startswith("User-"):
user_suffix = body_parts[-1].replace("User-", "")
base_parts = body_parts[:-1]
# Reconstruct base name
if base_parts[0] in ["Receivable", "Payable"]:
base_name = f"Accounts {base_parts[0]}"
else:
base_name = " & ".join(base_parts)
return f"{base_name} - {user_suffix}"
else:
# No user suffix, just join with &
return " & ".join(body_parts)
def get_account_type_from_hierarchical(hierarchical_name: str) -> Optional[AccountType]:
"""
Extract account type from hierarchical name.
Examples:
get_account_type_from_hierarchical("Assets:Cash")
→ AccountType.ASSET
get_account_type_from_hierarchical("Income:Accommodation")
→ AccountType.REVENUE
Args:
hierarchical_name: Hierarchical account name
Returns:
AccountType or None if not found
"""
parts = hierarchical_name.split(":")
if not parts:
return None
root = parts[0]
# Reverse lookup in ACCOUNT_TYPE_ROOTS
for account_type, root_name in ACCOUNT_TYPE_ROOTS.items():
if root == root_name:
return account_type
return None
def migrate_account_name(old_name: str, account_type: AccountType) -> str:
"""
Migrate a legacy account name to hierarchical format.
Args:
old_name: Legacy account name like "Accounts Receivable - af983632"
account_type: The account type
Returns:
Hierarchical account name
Examples:
migrate_account_name("Accounts Receivable - af983632", AccountType.ASSET)
"Assets:Receivable:User-af983632"
migrate_account_name("Food & Supplies", AccountType.EXPENSE)
"Expenses:Food:Supplies"
"""
base_name, user_id = parse_legacy_account_name(old_name)
return format_hierarchical_account_name(account_type, base_name, user_id)
# Default chart of accounts with hierarchical names
DEFAULT_HIERARCHICAL_ACCOUNTS = [
# Assets
("Assets:Bank", AccountType.ASSET, "Bank account"),
("Assets:Bitcoin:Lightning", AccountType.ASSET, "Lightning Network balance"),
("Assets:Bitcoin:OnChain", AccountType.ASSET, "On-chain Bitcoin wallet"),
("Assets:Cash", AccountType.ASSET, "Cash on hand"),
("Assets:FixedAssets:Equipment", AccountType.ASSET, "Equipment and machinery"),
("Assets:FixedAssets:FarmEquipment", AccountType.ASSET, "Farm equipment"),
("Assets:FixedAssets:Network", AccountType.ASSET, "Network infrastructure"),
("Assets:FixedAssets:ProductionFacility", AccountType.ASSET, "Production facilities"),
("Assets:Inventory", AccountType.ASSET, "Inventory and stock"),
("Assets:Livestock", AccountType.ASSET, "Livestock and animals"),
("Assets:Receivable", AccountType.ASSET, "Money owed to the Castle"),
("Assets:Tools", AccountType.ASSET, "Tools and hand equipment"),
# Liabilities
("Liabilities:Payable", AccountType.LIABILITY, "Money owed by the Castle"),
# Equity - User equity accounts created dynamically as Equity:User-{user_id}
# No parent "Equity" account needed - hierarchy is implicit in the name
# Revenue (Income in Beancount terminology)
("Income:Accommodation:Guests", AccountType.REVENUE, "Revenue from guest accommodation"),
("Income:Service", AccountType.REVENUE, "Revenue from services"),
("Income:Other", AccountType.REVENUE, "Other revenue"),
# Expenses - SUPPLIES (consumables - things you buy regularly)
("Expenses:Supplies:Food", AccountType.EXPENSE, "Food & groceries"),
("Expenses:Supplies:Kitchen", AccountType.EXPENSE, "Kitchen supplies"),
("Expenses:Supplies:Office", AccountType.EXPENSE, "Office supplies"),
("Expenses:Supplies:Garden", AccountType.EXPENSE, "Garden supplies"),
("Expenses:Supplies:Paint", AccountType.EXPENSE, "Paint & painting supplies"),
("Expenses:Supplies:Cleaning", AccountType.EXPENSE, "Cleaning supplies"),
("Expenses:Supplies:Other", AccountType.EXPENSE, "Other consumables"),
# Expenses - MATERIALS (construction/building materials)
("Expenses:Materials:Construction", AccountType.EXPENSE, "Building materials"),
("Expenses:Materials:Hardware", AccountType.EXPENSE, "Hardware (nails, screws, fasteners)"),
# Expenses - EQUIPMENT (durable goods that last)
("Expenses:Equipment:Tools", AccountType.EXPENSE, "Tools"),
("Expenses:Equipment:Furniture", AccountType.EXPENSE, "Furniture"),
("Expenses:Equipment:Housewares", AccountType.EXPENSE, "Housewares & appliances"),
# Expenses - UTILITIES (ongoing services with bills)
("Expenses:Utilities:Electric", AccountType.EXPENSE, "Electricity"),
("Expenses:Utilities:Internet", AccountType.EXPENSE, "Internet service"),
("Expenses:Utilities:Gas:Kitchen", AccountType.EXPENSE, "Kitchen gas"),
("Expenses:Utilities:Gas:Vehicle", AccountType.EXPENSE, "Vehicle fuel"),
("Expenses:Utilities:Water", AccountType.EXPENSE, "Water"),
# Expenses - MAINTENANCE (repairs & upkeep)
("Expenses:Maintenance:Property", AccountType.EXPENSE, "Building/property repairs"),
("Expenses:Maintenance:Vehicle", AccountType.EXPENSE, "Car maintenance & repairs"),
("Expenses:Maintenance:Garden", AccountType.EXPENSE, "Garden maintenance"),
("Expenses:Maintenance:Equipment", AccountType.EXPENSE, "Equipment repairs"),
# Expenses - SERVICES (professional services & subscriptions)
("Expenses:Services:Insurance", AccountType.EXPENSE, "Insurance premiums"),
("Expenses:Services:Membership", AccountType.EXPENSE, "Membership fees"),
("Expenses:Services:WebHosting:Domain", AccountType.EXPENSE, "Domain registration"),
("Expenses:Services:WebHosting:Wix", AccountType.EXPENSE, "Wix hosting service"),
("Expenses:Services:Administrative", AccountType.EXPENSE, "Administrative services"),
("Expenses:Services:Other", AccountType.EXPENSE, "Other services"),
]