Removes parent accounts from the database to simplify account management. Since the application exports to Beancount and doesn't directly interface with it, parent accounts for organizational hierarchy aren't necessary. The hierarchy is implicitly derived from the colon-separated account names. This change cleans the database and prevents accidental postings to parent accounts. Specifically removes "Assets:Bitcoin" and "Equity" accounts.
239 lines
8.4 KiB
Python
239 lines
8.4 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
|
|
("Expenses:Administrative", AccountType.EXPENSE, "Administrative expenses"),
|
|
("Expenses:Construction:Materials", AccountType.EXPENSE, "Construction materials"),
|
|
("Expenses:Furniture", AccountType.EXPENSE, "Furniture and furnishings"),
|
|
("Expenses:Garden", AccountType.EXPENSE, "Garden supplies and materials"),
|
|
("Expenses:Gas:Kitchen", AccountType.EXPENSE, "Kitchen gas"),
|
|
("Expenses:Gas:Vehicle", AccountType.EXPENSE, "Vehicle gas and fuel"),
|
|
("Expenses:Groceries", AccountType.EXPENSE, "Groceries and food"),
|
|
("Expenses:Hardware", AccountType.EXPENSE, "Hardware and tools"),
|
|
("Expenses:Housewares", AccountType.EXPENSE, "Housewares and household items"),
|
|
("Expenses:Insurance", AccountType.EXPENSE, "Insurance premiums"),
|
|
("Expenses:Kitchen", AccountType.EXPENSE, "Kitchen supplies and equipment"),
|
|
("Expenses:Maintenance:Car", AccountType.EXPENSE, "Car maintenance and repairs"),
|
|
("Expenses:Maintenance:Garden", AccountType.EXPENSE, "Garden maintenance"),
|
|
("Expenses:Maintenance:Property", AccountType.EXPENSE, "Property maintenance and repairs"),
|
|
("Expenses:Membership", AccountType.EXPENSE, "Membership fees"),
|
|
("Expenses:Supplies", AccountType.EXPENSE, "General supplies"),
|
|
("Expenses:Tools", AccountType.EXPENSE, "Tools and equipment"),
|
|
("Expenses:Utilities:Electric", AccountType.EXPENSE, "Electricity"),
|
|
("Expenses:Utilities:Internet", AccountType.EXPENSE, "Internet service"),
|
|
("Expenses:WebHosting:Domain", AccountType.EXPENSE, "Domain registration"),
|
|
("Expenses:WebHosting:Wix", AccountType.EXPENSE, "Wix hosting service"),
|
|
]
|