From 51ae2e8e47ed1af9df3a74b2c49ddc53f47d840b Mon Sep 17 00:00:00 2001 From: padreug Date: Mon, 10 Nov 2025 15:04:27 +0100 Subject: [PATCH] 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. --- beancount_format.py | 25 +++++++++++++++++++++++++ views_api.py | 18 +++++++++--------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/beancount_format.py b/beancount_format.py index 06f0afb..2819e67 100644 --- a/beancount_format.py +++ b/beancount_format.py @@ -14,6 +14,31 @@ Key concepts: from datetime import date, datetime from decimal import Decimal 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( diff --git a/views_api.py b/views_api.py index 783309c..b6e95dc 100644 --- a/views_api.py +++ b/views_api.py @@ -802,7 +802,7 @@ async def api_create_expense_entry( # Format as Beancount entry and submit to Fava 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() @@ -814,10 +814,10 @@ async def api_create_expense_entry( import uuid 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}" if data.reference: - castle_reference = f"{data.reference}-{entry_id}" + castle_reference = f"{sanitize_link(data.reference)}-{entry_id}" # Format Beancount entry entry = format_expense_entry( @@ -930,7 +930,7 @@ async def api_create_receivable_entry( # Format as Beancount entry and submit to Fava 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() @@ -942,10 +942,10 @@ async def api_create_receivable_entry( import uuid 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}" if data.reference: - castle_reference = f"{data.reference}-{entry_id}" + castle_reference = f"{sanitize_link(data.reference)}-{entry_id}" # Format Beancount entry entry = format_receivable_entry( @@ -1007,7 +1007,7 @@ async def api_create_revenue_entry( Submits entry to Fava/Beancount. """ 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 revenue_account = await get_account_by_name(data.revenue_account) @@ -1057,10 +1057,10 @@ async def api_create_revenue_entry( import uuid 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}" if data.reference: - castle_reference = f"{data.reference}-{entry_id}" + castle_reference = f"{sanitize_link(data.reference)}-{entry_id}" entry = format_revenue_entry( payment_account=payment_account.name,