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>
255 lines
9.3 KiB
Python
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"),
|
|
]
|