""" 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"), ]