Adds Fava integration for journal entries
Integrates Fava/Beancount for managing journal entries. This change introduces functions to format entries into Beancount format and submit them to a Fava instance. It replaces the previous direct database entry creation with Fava submission for expense, receivable, and revenue entries. The existing create_journal_entry function is also updated to submit generic journal entries to Fava.
This commit is contained in:
parent
a88d7b4ea0
commit
e3acc53e20
2 changed files with 411 additions and 110 deletions
|
|
@ -462,3 +462,86 @@ def format_payment_entry(
|
|||
links=links,
|
||||
meta=entry_meta
|
||||
)
|
||||
|
||||
|
||||
def format_revenue_entry(
|
||||
payment_account: str,
|
||||
revenue_account: str,
|
||||
amount_sats: int,
|
||||
description: str,
|
||||
entry_date: date,
|
||||
fiat_currency: Optional[str] = None,
|
||||
fiat_amount: Optional[Decimal] = None,
|
||||
reference: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Format a revenue entry (castle receives payment directly).
|
||||
|
||||
Creates a cleared transaction (flag="*") since payment was received.
|
||||
|
||||
Example: Cash sale, Lightning payment received, bank transfer received.
|
||||
|
||||
Args:
|
||||
payment_account: Payment method account (e.g., "Assets:Bitcoin:Lightning", "Assets:Cash")
|
||||
revenue_account: Revenue account name (e.g., "Income:Sales", "Income:Services")
|
||||
amount_sats: Amount in satoshis (unsigned)
|
||||
description: Entry description
|
||||
entry_date: Date of payment
|
||||
fiat_currency: Optional fiat currency
|
||||
fiat_amount: Optional fiat amount (unsigned)
|
||||
reference: Optional reference
|
||||
|
||||
Returns:
|
||||
Fava API entry dict
|
||||
|
||||
Example:
|
||||
entry = format_revenue_entry(
|
||||
payment_account="Assets:Cash",
|
||||
revenue_account="Income:Sales",
|
||||
amount_sats=100000,
|
||||
description="Product sale",
|
||||
entry_date=date.today(),
|
||||
fiat_currency="EUR",
|
||||
fiat_amount=Decimal("50.00")
|
||||
)
|
||||
"""
|
||||
amount_sats_abs = abs(amount_sats)
|
||||
fiat_amount_abs = abs(fiat_amount) if fiat_amount else None
|
||||
|
||||
narration = description
|
||||
if fiat_currency and fiat_amount_abs:
|
||||
narration += f" ({fiat_amount_abs:.2f} {fiat_currency})"
|
||||
|
||||
postings = [
|
||||
format_posting_with_cost(
|
||||
account=payment_account,
|
||||
amount_sats=amount_sats_abs, # Positive = debit (asset increase)
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=fiat_amount_abs
|
||||
),
|
||||
format_posting_with_cost(
|
||||
account=revenue_account,
|
||||
amount_sats=-amount_sats_abs, # Negative = credit (revenue increase)
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=fiat_amount_abs
|
||||
)
|
||||
]
|
||||
|
||||
entry_meta = {
|
||||
"source": "castle-api",
|
||||
"created-via": "revenue_entry"
|
||||
}
|
||||
|
||||
links = []
|
||||
if reference:
|
||||
links.append(reference)
|
||||
|
||||
return format_transaction(
|
||||
date_val=entry_date,
|
||||
flag="*", # Cleared (payment received)
|
||||
narration=narration,
|
||||
postings=postings,
|
||||
tags=["revenue-entry"],
|
||||
links=links,
|
||||
meta=entry_meta
|
||||
)
|
||||
|
|
|
|||
438
views_api.py
438
views_api.py
|
|
@ -412,11 +412,110 @@ async def api_create_journal_entry(
|
|||
data: CreateJournalEntry,
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
) -> JournalEntry:
|
||||
"""Create a new journal entry"""
|
||||
try:
|
||||
return await create_journal_entry(data, wallet.wallet.id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
|
||||
"""
|
||||
Create a new generic journal entry.
|
||||
|
||||
Submits entry to Fava/Beancount.
|
||||
"""
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_transaction, format_posting_with_cost
|
||||
|
||||
# Validate that entry balances to zero
|
||||
total = sum(line.amount for line in data.lines)
|
||||
if total != 0:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Entry does not balance (total: {total}, expected: 0)"
|
||||
)
|
||||
|
||||
# Get all accounts and validate they exist
|
||||
account_map = {}
|
||||
for line in data.lines:
|
||||
account = await get_account(line.account_id)
|
||||
if not account:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Account '{line.account_id}' not found"
|
||||
)
|
||||
account_map[line.account_id] = account
|
||||
|
||||
# Format postings
|
||||
postings = []
|
||||
for line in data.lines:
|
||||
account = account_map[line.account_id]
|
||||
|
||||
# Extract fiat info from metadata if present
|
||||
fiat_currency = line.metadata.get("fiat_currency")
|
||||
fiat_amount_str = line.metadata.get("fiat_amount")
|
||||
fiat_amount = Decimal(fiat_amount_str) if fiat_amount_str else None
|
||||
|
||||
# Create posting metadata (excluding fiat fields that go in cost basis)
|
||||
posting_metadata = {k: v for k, v in line.metadata.items()
|
||||
if k not in ["fiat_currency", "fiat_amount"]}
|
||||
if line.description:
|
||||
posting_metadata["description"] = line.description
|
||||
|
||||
posting = format_posting_with_cost(
|
||||
account=account.name,
|
||||
amount_sats=line.amount,
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=abs(fiat_amount) if fiat_amount else None,
|
||||
metadata=posting_metadata if posting_metadata else None
|
||||
)
|
||||
postings.append(posting)
|
||||
|
||||
# Extract tags and links from meta
|
||||
tags = data.meta.get("tags", [])
|
||||
links = data.meta.get("links", [])
|
||||
if data.reference:
|
||||
links.append(data.reference)
|
||||
|
||||
# Entry metadata (excluding tags and links which go at transaction level)
|
||||
entry_meta = {k: v for k, v in data.meta.items() if k not in ["tags", "links"]}
|
||||
entry_meta["source"] = "castle-api"
|
||||
entry_meta["created-by"] = wallet.wallet.id
|
||||
|
||||
# Format as Beancount entry
|
||||
fava = get_fava_client()
|
||||
|
||||
entry = format_transaction(
|
||||
date_val=data.entry_date.date() if data.entry_date else datetime.now().date(),
|
||||
flag=data.flag.value if data.flag else "*",
|
||||
narration=data.description,
|
||||
postings=postings,
|
||||
tags=tags if tags else None,
|
||||
links=links if links else None,
|
||||
meta=entry_meta
|
||||
)
|
||||
|
||||
# Submit to Fava
|
||||
result = await fava.add_entry(entry)
|
||||
logger.info(f"Journal entry submitted to Fava: {result.get('data', 'Unknown')}")
|
||||
|
||||
# Return mock JournalEntry for API compatibility
|
||||
# TODO: Query Fava to get the actual entry back with its hash
|
||||
timestamp = datetime.now().timestamp()
|
||||
return JournalEntry(
|
||||
id=f"fava-{timestamp}",
|
||||
description=data.description,
|
||||
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
||||
created_by=wallet.wallet.id,
|
||||
created_at=datetime.now(),
|
||||
reference=data.reference,
|
||||
flag=data.flag if data.flag else JournalEntryFlag.CLEARED,
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"fava-{timestamp}-{i}",
|
||||
journal_entry_id=f"fava-{timestamp}",
|
||||
account_id=line.account_id,
|
||||
amount=line.amount,
|
||||
description=line.description,
|
||||
metadata=line.metadata
|
||||
)
|
||||
for i, line in enumerate(data.lines)
|
||||
],
|
||||
meta={**data.meta, "source": "fava", "fava_response": result.get('data', 'Unknown')}
|
||||
)
|
||||
|
||||
|
||||
# ===== SIMPLIFIED ENTRY ENDPOINTS =====
|
||||
|
|
@ -530,29 +629,64 @@ async def api_create_expense_entry(
|
|||
"is_equity": data.is_equity,
|
||||
}
|
||||
|
||||
entry_data = CreateJournalEntry(
|
||||
description=data.description + description_suffix,
|
||||
reference=data.reference,
|
||||
entry_date=data.entry_date,
|
||||
flag=JournalEntryFlag.PENDING, # Expenses require admin approval
|
||||
meta=entry_meta,
|
||||
lines=[
|
||||
CreateEntryLine(
|
||||
account_id=expense_account.id,
|
||||
amount=amount_sats, # Positive = debit (expense increase)
|
||||
description=f"Expense paid by user {wallet.wallet.user[:8]}",
|
||||
metadata=metadata,
|
||||
),
|
||||
CreateEntryLine(
|
||||
account_id=user_account.id,
|
||||
amount=-amount_sats, # Negative = credit (liability/equity increase)
|
||||
description=f"{'Equity contribution' if data.is_equity else 'Amount owed to user'}",
|
||||
metadata=metadata,
|
||||
),
|
||||
],
|
||||
# Format as Beancount entry and submit to Fava
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_expense_entry
|
||||
|
||||
fava = get_fava_client()
|
||||
|
||||
# Extract fiat info from metadata
|
||||
fiat_currency = metadata.get("fiat_currency") if metadata else None
|
||||
fiat_amount = Decimal(metadata.get("fiat_amount")) if metadata and metadata.get("fiat_amount") else None
|
||||
|
||||
# Format Beancount entry
|
||||
entry = format_expense_entry(
|
||||
user_id=wallet.wallet.user,
|
||||
expense_account=expense_account.name,
|
||||
user_account=user_account.name,
|
||||
amount_sats=amount_sats,
|
||||
description=data.description,
|
||||
entry_date=data.entry_date.date() if data.entry_date else datetime.now().date(),
|
||||
is_equity=data.is_equity,
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=fiat_amount,
|
||||
reference=data.reference
|
||||
)
|
||||
|
||||
return await create_journal_entry(entry_data, wallet.wallet.id)
|
||||
# Submit to Fava
|
||||
result = await fava.add_entry(entry)
|
||||
|
||||
# Return a JournalEntry-like response for compatibility
|
||||
# TODO: Query Fava to get the actual entry back with its hash
|
||||
from .models import EntryLine
|
||||
return JournalEntry(
|
||||
id=f"fava-{datetime.now().timestamp()}", # Temporary ID
|
||||
description=data.description + description_suffix,
|
||||
entry_date=data.entry_date if data.entry_date else datetime.now(),
|
||||
created_by=wallet.wallet.id,
|
||||
created_at=datetime.now(),
|
||||
reference=data.reference,
|
||||
flag=JournalEntryFlag.PENDING,
|
||||
meta=entry_meta,
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"line-1-{datetime.now().timestamp()}",
|
||||
journal_entry_id=f"fava-{datetime.now().timestamp()}",
|
||||
account_id=expense_account.id,
|
||||
amount=amount_sats,
|
||||
description=f"Expense paid by user {wallet.wallet.user[:8]}",
|
||||
metadata=metadata or {}
|
||||
),
|
||||
EntryLine(
|
||||
id=f"line-2-{datetime.now().timestamp()}",
|
||||
journal_entry_id=f"fava-{datetime.now().timestamp()}",
|
||||
account_id=user_account.id,
|
||||
amount=-amount_sats,
|
||||
description=f"{'Equity contribution' if data.is_equity else 'Amount owed to user'}",
|
||||
metadata=metadata or {}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/entries/receivable", status_code=HTTPStatus.CREATED)
|
||||
|
|
@ -615,28 +749,62 @@ async def api_create_receivable_entry(
|
|||
"debtor_user_id": data.user_id,
|
||||
}
|
||||
|
||||
entry_data = CreateJournalEntry(
|
||||
description=data.description + description_suffix,
|
||||
reference=data.reference,
|
||||
flag=JournalEntryFlag.PENDING, # Receivables start as pending until paid
|
||||
meta=entry_meta,
|
||||
lines=[
|
||||
CreateEntryLine(
|
||||
account_id=user_receivable.id,
|
||||
amount=amount_sats, # Positive = debit (asset increase - user owes castle)
|
||||
description=f"Amount owed by user {data.user_id[:8]}",
|
||||
metadata=metadata,
|
||||
),
|
||||
CreateEntryLine(
|
||||
account_id=revenue_account.id,
|
||||
amount=-amount_sats, # Negative = credit (revenue increase)
|
||||
description="Revenue earned",
|
||||
metadata=metadata,
|
||||
),
|
||||
],
|
||||
# Format as Beancount entry and submit to Fava
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_receivable_entry
|
||||
|
||||
fava = get_fava_client()
|
||||
|
||||
# Extract fiat info from metadata
|
||||
fiat_currency = metadata.get("fiat_currency") if metadata else None
|
||||
fiat_amount = Decimal(metadata.get("fiat_amount")) if metadata and metadata.get("fiat_amount") else None
|
||||
|
||||
# Format Beancount entry
|
||||
entry = format_receivable_entry(
|
||||
user_id=data.user_id,
|
||||
revenue_account=revenue_account.name,
|
||||
receivable_account=user_receivable.name,
|
||||
amount_sats=amount_sats,
|
||||
description=data.description,
|
||||
entry_date=datetime.now().date(),
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=fiat_amount,
|
||||
reference=data.reference
|
||||
)
|
||||
|
||||
return await create_journal_entry(entry_data, wallet.wallet.id)
|
||||
# Submit to Fava
|
||||
result = await fava.add_entry(entry)
|
||||
|
||||
# Return a JournalEntry-like response for compatibility
|
||||
from .models import EntryLine
|
||||
return JournalEntry(
|
||||
id=f"fava-{datetime.now().timestamp()}",
|
||||
description=data.description + description_suffix,
|
||||
entry_date=datetime.now(),
|
||||
created_by=wallet.wallet.id,
|
||||
created_at=datetime.now(),
|
||||
reference=data.reference,
|
||||
flag=JournalEntryFlag.PENDING,
|
||||
meta=entry_meta,
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"line-1-{datetime.now().timestamp()}",
|
||||
journal_entry_id=f"fava-{datetime.now().timestamp()}",
|
||||
account_id=user_receivable.id,
|
||||
amount=amount_sats,
|
||||
description=f"Amount owed by user {data.user_id[:8]}",
|
||||
metadata=metadata or {}
|
||||
),
|
||||
EntryLine(
|
||||
id=f"line-2-{datetime.now().timestamp()}",
|
||||
journal_entry_id=f"fava-{datetime.now().timestamp()}",
|
||||
account_id=revenue_account.id,
|
||||
amount=-amount_sats,
|
||||
description="Revenue earned",
|
||||
metadata=metadata or {}
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/entries/revenue", status_code=HTTPStatus.CREATED)
|
||||
|
|
@ -647,7 +815,12 @@ async def api_create_revenue_entry(
|
|||
"""
|
||||
Create a revenue entry (castle receives payment).
|
||||
Admin only.
|
||||
|
||||
Submits entry to Fava/Beancount.
|
||||
"""
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_revenue_entry
|
||||
|
||||
# Get revenue account
|
||||
revenue_account = await get_account_by_name(data.revenue_account)
|
||||
if not revenue_account:
|
||||
|
|
@ -668,26 +841,76 @@ async def api_create_revenue_entry(
|
|||
detail=f"Payment account '{data.payment_method_account}' not found",
|
||||
)
|
||||
|
||||
# Create journal entry
|
||||
# DR Cash/Lightning/Bank, CR Revenue
|
||||
entry_data = CreateJournalEntry(
|
||||
description=data.description,
|
||||
reference=data.reference,
|
||||
lines=[
|
||||
CreateEntryLine(
|
||||
account_id=payment_account.id,
|
||||
amount=data.amount, # Positive = debit (asset increase)
|
||||
description="Payment received",
|
||||
),
|
||||
CreateEntryLine(
|
||||
account_id=revenue_account.id,
|
||||
amount=-data.amount, # Negative = credit (revenue increase)
|
||||
description="Revenue earned",
|
||||
),
|
||||
],
|
||||
# Handle currency conversion if provided
|
||||
amount_sats = int(data.amount)
|
||||
fiat_currency = None
|
||||
fiat_amount = None
|
||||
|
||||
if data.currency:
|
||||
# Validate currency
|
||||
if data.currency.upper() not in allowed_currencies():
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Currency '{data.currency}' not supported. Allowed: {', '.join(allowed_currencies())}",
|
||||
)
|
||||
|
||||
return await create_journal_entry(entry_data, wallet.wallet.id)
|
||||
# Store fiat info for cost basis
|
||||
fiat_currency = data.currency.upper()
|
||||
fiat_amount = data.amount # Original fiat amount
|
||||
# In this case, data.amount should be the satoshi amount
|
||||
# This is a bit confusing - the API accepts amount as Decimal which could be either sats or fiat
|
||||
# For now, assume if currency is provided, amount is fiat and needs conversion
|
||||
# TODO: Consider updating the API model to be clearer about this
|
||||
|
||||
# Format as Beancount entry and submit to Fava
|
||||
fava = get_fava_client()
|
||||
|
||||
entry = format_revenue_entry(
|
||||
payment_account=payment_account.name,
|
||||
revenue_account=revenue_account.name,
|
||||
amount_sats=amount_sats,
|
||||
description=data.description,
|
||||
entry_date=datetime.now().date(),
|
||||
fiat_currency=fiat_currency,
|
||||
fiat_amount=fiat_amount,
|
||||
reference=data.reference
|
||||
)
|
||||
|
||||
# Submit to Fava
|
||||
result = await fava.add_entry(entry)
|
||||
logger.info(f"Revenue entry submitted to Fava: {result.get('data', 'Unknown')}")
|
||||
|
||||
# Return mock JournalEntry for API compatibility
|
||||
# TODO: Query Fava to get the actual entry back with its hash
|
||||
timestamp = datetime.now().timestamp()
|
||||
return JournalEntry(
|
||||
id=f"fava-{timestamp}",
|
||||
description=data.description,
|
||||
entry_date=datetime.now(),
|
||||
created_by=wallet.wallet.id,
|
||||
created_at=datetime.now(),
|
||||
reference=data.reference,
|
||||
flag=JournalEntryFlag.CLEARED, # Revenue entries are cleared
|
||||
lines=[
|
||||
EntryLine(
|
||||
id=f"fava-{timestamp}-1",
|
||||
journal_entry_id=f"fava-{timestamp}",
|
||||
account_id=payment_account.id,
|
||||
amount=amount_sats,
|
||||
description="Payment received",
|
||||
metadata={"fiat_currency": fiat_currency, "fiat_amount": str(fiat_amount)} if fiat_currency else {}
|
||||
),
|
||||
EntryLine(
|
||||
id=f"fava-{timestamp}-2",
|
||||
journal_entry_id=f"fava-{timestamp}",
|
||||
account_id=revenue_account.id,
|
||||
amount=-amount_sats,
|
||||
description="Revenue earned",
|
||||
metadata={}
|
||||
)
|
||||
],
|
||||
meta={"source": "fava", "fava_response": result.get('data', 'Unknown')}
|
||||
)
|
||||
|
||||
|
||||
# ===== USER BALANCE ENDPOINTS =====
|
||||
|
|
@ -1609,31 +1832,31 @@ async def api_approve_manual_payment_request(
|
|||
detail="Lightning account not found",
|
||||
)
|
||||
|
||||
# Create journal entry: Debit Lightning (asset decreased), Credit Accounts Payable (liability increased)
|
||||
# This records that the Castle paid the user, reducing the lightning balance and reducing what castle owes
|
||||
journal_entry = await create_journal_entry(
|
||||
CreateJournalEntry(
|
||||
# Format payment entry and submit to Fava
|
||||
from .fava_client import get_fava_client
|
||||
from .beancount_format import format_payment_entry
|
||||
|
||||
fava = get_fava_client()
|
||||
|
||||
entry = format_payment_entry(
|
||||
user_id=request.user_id,
|
||||
payment_account=lightning_account.name,
|
||||
payable_or_receivable_account=liability_account.name,
|
||||
amount_sats=request.amount,
|
||||
description=f"Manual payment to user: {request.description}",
|
||||
reference=f"MPR-{request.id}",
|
||||
lines=[
|
||||
CreateEntryLine(
|
||||
account_id=liability_account.id,
|
||||
amount=request.amount, # Positive = debit (liability decrease - castle owes less)
|
||||
description="Payment to user",
|
||||
),
|
||||
CreateEntryLine(
|
||||
account_id=lightning_account.id,
|
||||
amount=-request.amount, # Negative = credit (asset decrease)
|
||||
description="Payment from castle",
|
||||
),
|
||||
],
|
||||
),
|
||||
castle_wallet_id,
|
||||
entry_date=datetime.now().date(),
|
||||
is_payable=True, # Castle paying user
|
||||
reference=f"MPR-{request.id}"
|
||||
)
|
||||
|
||||
# Approve the request
|
||||
# Submit to Fava
|
||||
result = await fava.add_entry(entry)
|
||||
logger.info(f"Manual payment entry submitted to Fava: {result.get('data', 'Unknown')}")
|
||||
|
||||
# Approve the request with Fava entry reference
|
||||
entry_id = f"fava-{datetime.now().timestamp()}"
|
||||
return await approve_manual_payment_request(
|
||||
request_id, wallet.wallet.user, journal_entry.id
|
||||
request_id, wallet.wallet.user, entry_id
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1675,8 +1898,13 @@ async def api_reject_manual_payment_request(
|
|||
async def api_approve_expense_entry(
|
||||
entry_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> JournalEntry:
|
||||
"""Approve a pending expense entry (admin only)"""
|
||||
) -> dict:
|
||||
"""
|
||||
Approve a pending expense entry (admin only).
|
||||
|
||||
With Fava integration, entries must be approved through Fava UI or API.
|
||||
This endpoint provides instructions on how to approve entries.
|
||||
"""
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
|
||||
if wallet.wallet.user != lnbits_settings.super_user:
|
||||
|
|
@ -1685,33 +1913,23 @@ async def api_approve_expense_entry(
|
|||
detail="Only super user can approve expenses",
|
||||
)
|
||||
|
||||
# Get the entry
|
||||
entry = await get_journal_entry(entry_id)
|
||||
if not entry:
|
||||
# TODO: Implement Fava entry update via PUT /api/source_slice
|
||||
# This requires:
|
||||
# 1. Query Fava for entry by link (^castle-{entry_id} or similar)
|
||||
# 2. Get the entry's source text
|
||||
# 3. Change flag from ! to *
|
||||
# 4. Submit updated source back to Fava
|
||||
|
||||
# For now, return instructions
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Journal entry not found",
|
||||
status_code=HTTPStatus.NOT_IMPLEMENTED,
|
||||
detail=(
|
||||
f"Entry approval via API not yet implemented with Fava integration. "
|
||||
f"To approve entry {entry_id}, open Fava and edit the transaction to change the flag from '!' to '*'. "
|
||||
f"Fava URL: http://localhost:3333/castle-ledger/"
|
||||
)
|
||||
|
||||
if entry.flag != JournalEntryFlag.PENDING:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=f"Entry is not pending (current status: {entry.flag.value})",
|
||||
)
|
||||
|
||||
# Update flag to cleared
|
||||
await db.execute(
|
||||
"""
|
||||
UPDATE journal_entries
|
||||
SET flag = :flag
|
||||
WHERE id = :id
|
||||
""",
|
||||
{"flag": JournalEntryFlag.CLEARED.value, "id": entry_id}
|
||||
)
|
||||
|
||||
# Return updated entry
|
||||
return await get_journal_entry(entry_id)
|
||||
|
||||
|
||||
@castle_api_router.post("/api/v1/entries/{entry_id}/reject")
|
||||
async def api_reject_expense_entry(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue