Creates accounts in Fava if they don't exist

This change ensures that user-specific accounts are automatically created
in the Fava/Beancount ledger when they are first requested. It checks for
the existence of the account via a Fava query and creates it via an Open
directive if it's missing.  This simplifies account management and
ensures that all necessary accounts are available for transactions.

This implementation adds a new `add_account` method to the `FavaClient`
class which makes use of the /add_entries endpoint to create an account
using an Open Directive.
This commit is contained in:
padreug 2025-11-10 15:56:22 +01:00
parent 51ae2e8e47
commit b6886793ee
2 changed files with 113 additions and 2 deletions

View file

@ -6,9 +6,11 @@ All accounting logic is delegated to Fava/Beancount.
Fava provides a REST API for:
- Adding transactions (PUT /api/add_entries)
- Adding accounts via Open directives (PUT /api/add_entries)
- Querying balances (GET /api/query)
- Balance sheets (GET /api/balance_sheet)
- Account reports (GET /api/account_report)
- Updating/deleting entries (PUT/DELETE /api/source_slice)
See: https://github.com/beancount/fava/blob/main/src/fava/json_api.py
"""
@ -679,6 +681,73 @@ class FavaClient:
logger.error(f"Fava connection error: {e}")
raise
async def add_account(
self,
account_name: str,
currencies: list[str],
opening_date: Optional[date] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Add an account to the Beancount ledger via an Open directive.
Args:
account_name: Full account name (e.g., "Assets:Receivable:User-abc123")
currencies: List of currencies for this account (e.g., ["EUR", "SATS"])
opening_date: Date to open the account (defaults to today)
metadata: Optional metadata for the account
Returns:
Response from Fava ({"data": "Stored 1 entries.", "mtime": "..."})
Example:
# Add a user's receivable account
result = await fava.add_account(
account_name="Assets:Receivable:User-abc123",
currencies=["EUR", "SATS", "USD"],
metadata={"user_id": "abc123", "description": "User receivables"}
)
# Add a user's payable account
result = await fava.add_account(
account_name="Liabilities:Payable:User-abc123",
currencies=["EUR", "SATS"]
)
"""
from datetime import date as date_type
if opening_date is None:
opening_date = date_type.today()
# Format Open directive for Fava
open_directive = {
"t": "Open",
"date": opening_date.isoformat(),
"account": account_name,
"currencies": currencies,
"meta": metadata or {}
}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.put(
f"{self.base_url}/add_entries",
json={"entries": [open_directive]},
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
result = response.json()
logger.info(f"Added account {account_name} to Fava with currencies {currencies}")
return result
except httpx.HTTPStatusError as e:
logger.error(f"Fava HTTP error adding account: {e.response.status_code} - {e.response.text}")
raise
except httpx.RequestError as e:
logger.error(f"Fava connection error: {e}")
raise
# Singleton instance (configured from settings)
_fava_client: Optional[FavaClient] = None