Route account writes for the split Fava ledger layout #32
2 changed files with 63 additions and 0 deletions
Add POST /api/v1/admin/accounts for chart-of-accounts entries
Companion to the fava ledger split (aiolabs/server-deploy#4). Super-user endpoint that adds a new Open directive to accounts/chart.beancount via fava_client.add_account (explicit target_file), then mirrors the account into Libra's DB via sync_single_account_from_beancount so permissions can be granted on it. Validates the account name against the five Beancount top-level prefixes (Assets:/Liabilities:/Equity:/Income:/Expenses:) and returns 400 on a bad prefix. Per-user accounts (matching :User-xxxxxxxx) keep their existing code path via crud.get_or_create_user_account, which inherits the inferred target_file (accounts/users.beancount) from the add_account default. Backend only -- the LNbits admin UI on top is tracked separately as aiolabs/libra#30. Refs: aiolabs/libra#29
commit
34ecb3f249
|
|
@ -48,6 +48,13 @@ class CreateAccount(BaseModel):
|
||||||
is_virtual: bool = False # Set to True to create virtual parent account
|
is_virtual: bool = False # Set to True to create virtual parent account
|
||||||
|
|
||||||
|
|
||||||
|
class CreateChartAccount(BaseModel):
|
||||||
|
"""Admin-created chart-of-accounts entry written to accounts/chart.beancount."""
|
||||||
|
name: str # Full hierarchical account name, e.g. "Expenses:Services:Domain"
|
||||||
|
currencies: list[str] = ["EUR", "SATS", "USD"]
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class EntryLine(BaseModel):
|
class EntryLine(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
journal_entry_id: str
|
journal_entry_id: str
|
||||||
|
|
|
||||||
56
views_api.py
56
views_api.py
|
|
@ -52,6 +52,7 @@ from .models import (
|
||||||
LibraSettings,
|
LibraSettings,
|
||||||
CreateAccount,
|
CreateAccount,
|
||||||
CreateAccountPermission,
|
CreateAccountPermission,
|
||||||
|
CreateChartAccount,
|
||||||
CreateBalanceAssertion,
|
CreateBalanceAssertion,
|
||||||
CreateEntryLine,
|
CreateEntryLine,
|
||||||
CreateJournalEntry,
|
CreateJournalEntry,
|
||||||
|
|
@ -3565,6 +3566,61 @@ async def api_get_account_hierarchy(
|
||||||
# ===== ACCOUNT SYNC ENDPOINTS =====
|
# ===== ACCOUNT SYNC ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
|
_VALID_ACCOUNT_PREFIXES = ("Assets:", "Liabilities:", "Equity:", "Income:", "Expenses:")
|
||||||
|
|
||||||
|
|
||||||
|
@libra_api_router.post("/api/v1/admin/accounts", status_code=HTTPStatus.CREATED)
|
||||||
|
async def api_admin_add_chart_account(
|
||||||
|
payload: CreateChartAccount,
|
||||||
|
auth: AuthContext = Depends(require_super_user),
|
||||||
|
) -> dict:
|
||||||
|
"""
|
||||||
|
Add a chart-of-accounts entry (super-user only).
|
||||||
|
|
||||||
|
Writes an Open directive to accounts/chart.beancount via Fava's /api/source,
|
||||||
|
then syncs the account into Libra's DB so permissions can be granted on it.
|
||||||
|
Per-user accounts (matching :User-xxxxxxxx) take a different code path via
|
||||||
|
crud.get_or_create_user_account and are not created through this endpoint.
|
||||||
|
"""
|
||||||
|
from .fava_client import get_fava_client
|
||||||
|
|
||||||
|
if not payload.name.startswith(_VALID_ACCOUNT_PREFIXES):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=HTTPStatus.BAD_REQUEST,
|
||||||
|
detail=(
|
||||||
|
f"Account name must start with one of "
|
||||||
|
f"{', '.join(_VALID_ACCOUNT_PREFIXES)} (got {payload.name!r})"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Admin {auth.user_id[:8]} adding chart account {payload.name} "
|
||||||
|
f"with currencies {payload.currencies}"
|
||||||
|
)
|
||||||
|
|
||||||
|
fava = get_fava_client()
|
||||||
|
metadata: dict = {"added_by": auth.user_id[:8], "source": "admin-ui"}
|
||||||
|
if payload.description:
|
||||||
|
metadata["description"] = payload.description
|
||||||
|
|
||||||
|
await fava.add_account(
|
||||||
|
account_name=payload.name,
|
||||||
|
currencies=payload.currencies,
|
||||||
|
target_file="accounts/chart.beancount",
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mirror into libra DB so permissions / metadata layer sees it.
|
||||||
|
from .account_sync import sync_single_account_from_beancount
|
||||||
|
synced = await sync_single_account_from_beancount(payload.name)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"account_name": payload.name,
|
||||||
|
"synced_to_libra_db": synced,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@libra_api_router.post("/api/v1/admin/accounts/sync")
|
@libra_api_router.post("/api/v1/admin/accounts/sync")
|
||||||
async def api_sync_all_accounts(
|
async def api_sync_all_accounts(
|
||||||
force_full_sync: bool = False,
|
force_full_sync: bool = False,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue