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,