diff --git a/helper/import_beancount.py b/helper/import_beancount.py index 4b332b3..30b0236 100755 --- a/helper/import_beancount.py +++ b/helper/import_beancount.py @@ -153,9 +153,10 @@ class AccountLookup: Special handling for user-specific accounts: - "Liabilities:Payable:Pat" -> looks up Pat's user_id and finds their Castle payable account - "Assets:Receivable:Pat" -> looks up Pat's user_id and finds their Castle receivable account + - "Equity:Pat" -> looks up Pat's user_id and finds their Castle equity account Args: - account_name: Beancount account name (e.g., "Expenses:Food:Supplies", "Liabilities:Payable:Pat", "Assets:Receivable:Pat") + account_name: Beancount account name (e.g., "Expenses:Food:Supplies", "Liabilities:Payable:Pat", "Assets:Receivable:Pat", "Equity:Pat") Returns: Castle account UUID or None if not found @@ -204,6 +205,28 @@ class AccountLookup: f"Please configure the wallet for user ID: {user_id}" ) + # Check if this is an Equity: account + # Map Beancount Equity:Pat to Castle Equity:User- + elif account_name.startswith("Equity:"): + user_name = extract_user_from_user_account(account_name) + if user_name: + # Look up user's actual user_id + user_id = USER_MAPPINGS.get(user_name) + if user_id: + # Find this user's equity account + # This is the Equity:User- account in Castle + if user_id in self.accounts_by_user: + equity_account_id = self.accounts_by_user[user_id].get('equity') + if equity_account_id: + return equity_account_id + + # If not found, provide helpful error + raise ValueError( + f"User '{user_name}' (ID: {user_id}) does not have an equity account.\n" + f"Equity eligibility must be enabled for this user in Castle.\n" + f"Please enable equity for user ID: {user_id}" + ) + # Normal account lookup by name return self.accounts.get(account_name) @@ -332,11 +355,12 @@ def parse_beancount_transaction(txn_text: str) -> Optional[Dict]: def extract_user_from_user_account(account_name: str) -> Optional[str]: """ - Extract user name from user-specific accounts (Payable or Receivable). + Extract user name from user-specific accounts (Payable, Receivable, or Equity). Examples: "Liabilities:Payable:Pat" -> "Pat" "Assets:Receivable:Alice" -> "Alice" + "Equity:Pat" -> "Pat" "Expenses:Food" -> None Returns: @@ -350,6 +374,10 @@ def extract_user_from_user_account(account_name: str) -> Optional[str]: parts = account_name.split(":") if len(parts) >= 3: return parts[2] + elif account_name.startswith("Equity:"): + parts = account_name.split(":") + if len(parts) >= 2: + return parts[1] return None def determine_user_id(postings: list) -> Optional[str]: @@ -386,8 +414,11 @@ def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: A if not user_id: raise ValueError( f"Could not determine user ID for transaction.\n" - f"Transactions must have a Liabilities:Payable: or Assets:Receivable: account.\n" - f"Examples: Liabilities:Payable:Pat, Assets:Receivable:Pat" + f"Transactions must have a user-specific account:\n" + f" - Liabilities:Payable: (for payables)\n" + f" - Assets:Receivable: (for receivables)\n" + f" - Equity: (for equity)\n" + f"Examples: Liabilities:Payable:Pat, Assets:Receivable:Pat, Equity:Pat" ) # Build entry lines diff --git a/static/js/index.js b/static/js/index.js index 8e7c577..f58eef0 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -202,7 +202,8 @@ window.app = Vue.createApp({ return [ { label: 'All Types', value: null }, { label: 'Receivable (User owes Castle)', value: 'asset' }, - { label: 'Payable (Castle owes User)', value: 'liability' } + { label: 'Payable (Castle owes User)', value: 'liability' }, + { label: 'Equity (User Balance)', value: 'equity' } ] }, expenseAccounts() { @@ -1551,6 +1552,19 @@ window.app = Vue.createApp({ } } return false + }, + isEquity(entry) { + // Check if this is an equity entry (user capital contribution/balance) + if (!entry.lines || entry.lines.length === 0) return false + + for (const line of entry.lines) { + // Check if the account is an equity account + const account = this.accounts.find(a => a.id === line.account_id) + if (account && account.account_type === 'equity') { + return true + } + } + return false } }, async created() { diff --git a/templates/castle/index.html b/templates/castle/index.html index c9b4f87..bd33114 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -427,7 +427,10 @@
{% raw %}{{ props.row.description }}{% endraw %} - + + Equity + + Receivable diff --git a/views_api.py b/views_api.py index c82b7b2..7932eb9 100644 --- a/views_api.py +++ b/views_api.py @@ -318,18 +318,33 @@ async def api_get_user_entries( enriched_entries = [] for entry in entries: # Find user_id from entry lines (look for user-specific accounts) + # Prioritize equity accounts, then liability/asset accounts entry_user_id = None entry_username = None entry_account_type = None + equity_account = None + other_user_account = None + + # First pass: look for equity and other user accounts for line in entry.lines: account = await get_account(line.account_id) if account and account.user_id: - entry_user_id = account.user_id - entry_account_type = account.account_type.value if hasattr(account.account_type, 'value') else account.account_type - user = await get_user(account.user_id) - entry_username = user.username if user and user.username else account.user_id[:16] + "..." - break + account_type = account.account_type.value if hasattr(account.account_type, 'value') else account.account_type + + if account_type == 'equity': + equity_account = (account.user_id, account_type, account) + break # Prioritize equity, stop searching + elif not other_user_account: + other_user_account = (account.user_id, account_type, account) + + # Use equity account if found, otherwise use other user account + selected_account = equity_account or other_user_account + + if selected_account: + entry_user_id, entry_account_type, account_obj = selected_account + user = await get_user(entry_user_id) + entry_username = user.username if user and user.username else entry_user_id[:16] + "..." enriched_entries.append({ **entry.dict(),