forked from aiolabs/libra
Fix get_all_accounts to discover accounts from open directives
The previous BQL query (SELECT DISTINCT account) only returned accounts with postings, missing all accounts that were opened but had no transactions yet. On a fresh ledger this returned 0 accounts, causing the account sync to deactivate everything. Now uses Fava's balance_sheet and income_statement API endpoints which return the full account tree including zero-balance accounts. Falls back to BQL if the tree endpoints fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b06c53c40f
commit
f2f9183106
1 changed files with 50 additions and 5 deletions
|
|
@ -1132,9 +1132,27 @@ class FavaClient:
|
||||||
limit=limit
|
limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _extract_accounts_from_tree(self, tree: Any) -> List[str]:
|
||||||
|
"""Recursively extract account names from a Fava tree structure."""
|
||||||
|
accounts = []
|
||||||
|
if isinstance(tree, dict):
|
||||||
|
for key, val in tree.items():
|
||||||
|
if key == "account":
|
||||||
|
accounts.append(val)
|
||||||
|
elif isinstance(val, (dict, list)):
|
||||||
|
accounts.extend(self._extract_accounts_from_tree(val))
|
||||||
|
elif isinstance(tree, list):
|
||||||
|
for item in tree:
|
||||||
|
accounts.extend(self._extract_accounts_from_tree(item))
|
||||||
|
return accounts
|
||||||
|
|
||||||
async def get_all_accounts(self) -> List[Dict[str, Any]]:
|
async def get_all_accounts(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get all accounts from Beancount/Fava using BQL query.
|
Get all accounts from Beancount/Fava.
|
||||||
|
|
||||||
|
Uses Fava's balance_sheet and income_statement API endpoints to
|
||||||
|
discover all opened accounts, including those with zero balances.
|
||||||
|
Falls back to BQL query if the tree endpoints fail.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of account dictionaries:
|
List of account dictionaries:
|
||||||
|
|
@ -1150,25 +1168,52 @@ class FavaClient:
|
||||||
print(acc["account"]) # "Assets:Cash"
|
print(acc["account"]) # "Assets:Cash"
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Use BQL to get all unique accounts
|
# Use balance_sheet + income_statement to get ALL opened accounts
|
||||||
|
# (BQL's SELECT DISTINCT account only returns accounts with postings)
|
||||||
|
account_names: set[str] = set()
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
|
for endpoint in ("balance_sheet", "income_statement"):
|
||||||
|
try:
|
||||||
|
response = await client.get(f"{self.base_url}/{endpoint}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json().get("data", {})
|
||||||
|
trees = data.get("trees", {})
|
||||||
|
names = self._extract_accounts_from_tree(trees)
|
||||||
|
account_names.update(names)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to fetch {endpoint}: {e}")
|
||||||
|
|
||||||
|
# Filter out synthetic entries like "Net Profit"
|
||||||
|
account_names = {
|
||||||
|
name for name in account_names
|
||||||
|
if ":" in name or name in ("Assets", "Liabilities", "Equity", "Income", "Expenses")
|
||||||
|
}
|
||||||
|
|
||||||
|
if account_names:
|
||||||
|
accounts = [{"account": name, "meta": {}} for name in sorted(account_names)]
|
||||||
|
logger.debug(f"Fava returned {len(accounts)} accounts via tree endpoints")
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
# Fallback: BQL query (only finds accounts with postings)
|
||||||
|
logger.info("Tree endpoints returned no accounts, falling back to BQL")
|
||||||
query = "SELECT DISTINCT account"
|
query = "SELECT DISTINCT account"
|
||||||
result = await self.query_bql(query)
|
result = await self.query_bql(query)
|
||||||
|
|
||||||
# Convert BQL result to expected format
|
|
||||||
accounts = []
|
accounts = []
|
||||||
for row in result["rows"]:
|
for row in result["rows"]:
|
||||||
account_name = row[0] if isinstance(row, list) else row.get("account")
|
account_name = row[0] if isinstance(row, list) else row.get("account")
|
||||||
if account_name:
|
if account_name:
|
||||||
accounts.append({
|
accounts.append({
|
||||||
"account": account_name,
|
"account": account_name,
|
||||||
"meta": {} # BQL doesn't return metadata easily
|
"meta": {}
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.debug(f"Fava returned {len(accounts)} accounts via BQL")
|
logger.debug(f"Fava returned {len(accounts)} accounts via BQL")
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to fetch accounts via BQL: {e}")
|
logger.error(f"Failed to fetch accounts: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def get_journal_entries(
|
async def get_journal_entries(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue