Sanitizes reference links for Beancount

Ensures that user-provided reference strings for expense,
receivable, and revenue entries are sanitized before being
included as Beancount links. This prevents issues caused by
invalid characters in the links, improving compatibility with
Beancount's link format. A new utility function is introduced
to handle the sanitization process.
This commit is contained in:
padreug 2025-11-10 15:04:27 +01:00
parent a6b67b7416
commit 51ae2e8e47
2 changed files with 34 additions and 9 deletions

View file

@ -14,6 +14,31 @@ Key concepts:
from datetime import date, datetime from datetime import date, datetime
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import re
def sanitize_link(text: str) -> str:
"""
Sanitize a string to make it valid for Beancount links.
Beancount links can only contain: A-Z, a-z, 0-9, -, _, /, .
All other characters are replaced with hyphens.
Examples:
>>> sanitize_link("Test (pending)")
'Test-pending'
>>> sanitize_link("Invoice #123")
'Invoice-123'
>>> sanitize_link("castle-abc123")
'castle-abc123'
"""
# Replace any character that's not alphanumeric, dash, underscore, slash, or period with a hyphen
sanitized = re.sub(r'[^A-Za-z0-9\-_/.]', '-', text)
# Remove consecutive hyphens
sanitized = re.sub(r'-+', '-', sanitized)
# Remove leading/trailing hyphens
sanitized = sanitized.strip('-')
return sanitized
def format_transaction( def format_transaction(

View file

@ -802,7 +802,7 @@ async def api_create_expense_entry(
# Format as Beancount entry and submit to Fava # Format as Beancount entry and submit to Fava
from .fava_client import get_fava_client from .fava_client import get_fava_client
from .beancount_format import format_expense_entry from .beancount_format import format_expense_entry, sanitize_link
fava = get_fava_client() fava = get_fava_client()
@ -814,10 +814,10 @@ async def api_create_expense_entry(
import uuid import uuid
entry_id = str(uuid.uuid4()).replace("-", "")[:16] entry_id = str(uuid.uuid4()).replace("-", "")[:16]
# Add castle ID as reference/link # Add castle ID as reference/link (sanitized for Beancount)
castle_reference = f"castle-{entry_id}" castle_reference = f"castle-{entry_id}"
if data.reference: if data.reference:
castle_reference = f"{data.reference}-{entry_id}" castle_reference = f"{sanitize_link(data.reference)}-{entry_id}"
# Format Beancount entry # Format Beancount entry
entry = format_expense_entry( entry = format_expense_entry(
@ -930,7 +930,7 @@ async def api_create_receivable_entry(
# Format as Beancount entry and submit to Fava # Format as Beancount entry and submit to Fava
from .fava_client import get_fava_client from .fava_client import get_fava_client
from .beancount_format import format_receivable_entry from .beancount_format import format_receivable_entry, sanitize_link
fava = get_fava_client() fava = get_fava_client()
@ -942,10 +942,10 @@ async def api_create_receivable_entry(
import uuid import uuid
entry_id = str(uuid.uuid4()).replace("-", "")[:16] entry_id = str(uuid.uuid4()).replace("-", "")[:16]
# Add castle ID as reference/link # Add castle ID as reference/link (sanitized for Beancount)
castle_reference = f"castle-{entry_id}" castle_reference = f"castle-{entry_id}"
if data.reference: if data.reference:
castle_reference = f"{data.reference}-{entry_id}" castle_reference = f"{sanitize_link(data.reference)}-{entry_id}"
# Format Beancount entry # Format Beancount entry
entry = format_receivable_entry( entry = format_receivable_entry(
@ -1007,7 +1007,7 @@ async def api_create_revenue_entry(
Submits entry to Fava/Beancount. Submits entry to Fava/Beancount.
""" """
from .fava_client import get_fava_client from .fava_client import get_fava_client
from .beancount_format import format_revenue_entry from .beancount_format import format_revenue_entry, sanitize_link
# Get revenue account # Get revenue account
revenue_account = await get_account_by_name(data.revenue_account) revenue_account = await get_account_by_name(data.revenue_account)
@ -1057,10 +1057,10 @@ async def api_create_revenue_entry(
import uuid import uuid
entry_id = str(uuid.uuid4()).replace("-", "")[:16] entry_id = str(uuid.uuid4()).replace("-", "")[:16]
# Add castle ID as reference/link # Add castle ID as reference/link (sanitized for Beancount)
castle_reference = f"castle-{entry_id}" castle_reference = f"castle-{entry_id}"
if data.reference: if data.reference:
castle_reference = f"{data.reference}-{entry_id}" castle_reference = f"{sanitize_link(data.reference)}-{entry_id}"
entry = format_revenue_entry( entry = format_revenue_entry(
payment_account=payment_account.name, payment_account=payment_account.name,