From 4b327a0aab76e0ec1ab1ba9b9850f9ea6908cc3b Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 8 Nov 2025 23:51:07 +0100 Subject: [PATCH] Extends account lookup for user accounts Implements account lookup logic for user-specific accounts, specifically Liabilities:Payable and Assets:Receivable. This allows the system to automatically map Beancount accounts to corresponding accounts in the Castle system based on user ID. Improves error messages when user accounts are not properly configured. --- helper/import_beancount.py | 85 ++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/helper/import_beancount.py b/helper/import_beancount.py index 7ca9d9b..4b332b3 100755 --- a/helper/import_beancount.py +++ b/helper/import_beancount.py @@ -150,32 +150,58 @@ class AccountLookup: """ Get Castle account ID for a Beancount account name. - Special handling for Equity accounts: - - "Equity:Pat" -> looks up Pat's user_id and finds their equity account + 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 Args: - account_name: Beancount account name (e.g., "Expenses:Food:Supplies" or "Equity:Pat") + account_name: Beancount account name (e.g., "Expenses:Food:Supplies", "Liabilities:Payable:Pat", "Assets:Receivable:Pat") Returns: Castle account UUID or None if not found """ - # Check if this is an Equity: account - if account_name.startswith("Equity:"): - user_name = extract_user_from_equity_account(account_name) + # Check if this is a Liabilities:Payable: account + # Map Beancount Liabilities:Payable:Pat to Castle Liabilities:Payable:User- + if account_name.startswith("Liabilities:Payable:"): + 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 + # Find this user's liability (payable) account + # This is the Liabilities:Payable: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 + liability_account_id = self.accounts_by_user[user_id].get('liability') + if liability_account_id: + return liability_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"Please enable equity eligibility for this user in Castle first." + f"User '{user_name}' (ID: {user_id}) does not have a payable account.\n" + f"This should have been created when they configured their wallet.\n" + f"Please configure the wallet for user ID: {user_id}" + ) + + # Check if this is an Assets:Receivable: account + # Map Beancount Assets:Receivable:Pat to Castle Assets:Receivable:User- + elif account_name.startswith("Assets:Receivable:"): + 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 asset (receivable) account + # This is the Assets:Receivable:User- account in Castle + if user_id in self.accounts_by_user: + asset_account_id = self.accounts_by_user[user_id].get('asset') + if asset_account_id: + return asset_account_id + + # If not found, provide helpful error + raise ValueError( + f"User '{user_name}' (ID: {user_id}) does not have a receivable account.\n" + f"This should have been created when they configured their wallet.\n" + f"Please configure the wallet for user ID: {user_id}" ) # Normal account lookup by name @@ -304,36 +330,40 @@ def parse_beancount_transaction(txn_text: str) -> Optional[Dict]: # ===== HELPER FUNCTIONS ===== -def extract_user_from_equity_account(account_name: str) -> Optional[str]: +def extract_user_from_user_account(account_name: str) -> Optional[str]: """ - Extract user name from Equity account. + Extract user name from user-specific accounts (Payable or Receivable). Examples: - "Equity:Pat" -> "Pat" - "Equity:Alice" -> "Alice" + "Liabilities:Payable:Pat" -> "Pat" + "Assets:Receivable:Alice" -> "Alice" "Expenses:Food" -> None Returns: - User name or None if not an Equity account + User name or None if not a user-specific account """ - if account_name.startswith("Equity:"): + if account_name.startswith("Liabilities:Payable:"): parts = account_name.split(":") - if len(parts) >= 2: - return parts[1] + if len(parts) >= 3: + return parts[2] + elif account_name.startswith("Assets:Receivable:"): + parts = account_name.split(":") + if len(parts) >= 3: + return parts[2] return None def determine_user_id(postings: list) -> Optional[str]: """ - Determine which user ID to use for this transaction based on Equity accounts. + Determine which user ID to use for this transaction based on user-specific accounts. Args: postings: List of posting dicts with 'account' key Returns: - User ID (wallet ID) from USER_MAPPINGS, or None if no Equity account found + User ID (wallet ID) from USER_MAPPINGS, or None if no user account found """ for posting in postings: - user_name = extract_user_from_equity_account(posting['account']) + user_name = extract_user_from_user_account(posting['account']) if user_name: user_id = USER_MAPPINGS.get(user_name) if not user_id: @@ -343,7 +373,7 @@ def determine_user_id(postings: list) -> Optional[str]: ) return user_id - # No Equity account found - this shouldn't happen for typical transactions + # No user-specific account found - this shouldn't happen for typical transactions return None # ===== CASTLE CONVERTER ===== @@ -351,12 +381,13 @@ def determine_user_id(postings: list) -> Optional[str]: def convert_to_castle_entry(parsed: dict, btc_eur_rate: float, account_lookup: AccountLookup) -> dict: """Convert parsed Beancount transaction to Castle format""" - # Determine which user this transaction is for (based on Equity accounts) + # Determine which user this transaction is for (based on user-specific accounts) user_id = determine_user_id(parsed['postings']) if not user_id: raise ValueError( f"Could not determine user ID for transaction.\n" - f"Transactions must have an Equity: account (e.g., Equity:Pat)." + f"Transactions must have a Liabilities:Payable: or Assets:Receivable: account.\n" + f"Examples: Liabilities:Payable:Pat, Assets:Receivable:Pat" ) # Build entry lines @@ -502,7 +533,7 @@ def import_beancount_file(beancount_file: str, dry_run: bool = False): # Get user name for display user_name = None for posting in parsed['postings']: - user_name = extract_user_from_equity_account(posting['account']) + user_name = extract_user_from_user_account(posting['account']) if user_name: break