Wire admin add-account endpoint into the UI #46
1 changed files with 28 additions and 8 deletions
fix(fava): escape string metadata + make Open currencies optional
add_account wrote free-text metadata values straight into the ledger source via /api/source with no escaping — an unescaped quote or newline in an admin-supplied description would corrupt the Beancount file (or forge extra metadata lines). Escape backslash/quote/newline per the tokenizer's cunescape rules (verified round-trip through beancount's parser). Also make the currency constraint list optional so an Open directive can be written unconstrained (currencies are an optional part of the directive, not required). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
commit
788a9998f6
|
|
@ -44,6 +44,22 @@ def _infer_target_file(account_name: str) -> str:
|
||||||
return "accounts/chart.beancount"
|
return "accounts/chart.beancount"
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_beancount_string(value: str) -> str:
|
||||||
|
"""Escape a value for safe inclusion in a Beancount string literal.
|
||||||
|
|
||||||
|
Beancount's tokenizer unescapes \\", \\n, \\t, \\r, \\\\ etc. (tokens.c
|
||||||
|
cunescape). Unescaped quotes or newlines in free-text metadata written
|
||||||
|
straight into the ledger source would corrupt the file, so escape the
|
||||||
|
backslash first (to keep it round-tripping) then quotes and newlines.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
value.replace("\\", "\\\\")
|
||||||
|
.replace('"', '\\"')
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FavaClient:
|
class FavaClient:
|
||||||
"""
|
"""
|
||||||
Async client for Fava REST API.
|
Async client for Fava REST API.
|
||||||
|
|
@ -1544,7 +1560,7 @@ class FavaClient:
|
||||||
async def add_account(
|
async def add_account(
|
||||||
self,
|
self,
|
||||||
account_name: str,
|
account_name: str,
|
||||||
currencies: list[str],
|
currencies: Optional[list[str]] = None,
|
||||||
opening_date: Optional[date] = None,
|
opening_date: Optional[date] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
target_file: Optional[str] = None,
|
target_file: Optional[str] = None,
|
||||||
|
|
@ -1642,19 +1658,23 @@ class FavaClient:
|
||||||
lines = source.split('\n')
|
lines = source.split('\n')
|
||||||
insert_index = len(lines)
|
insert_index = len(lines)
|
||||||
|
|
||||||
# Step 4: Format Open directive as Beancount text
|
# Step 4: Format Open directive as Beancount text.
|
||||||
currencies_str = ", ".join(currencies)
|
# Currencies are an optional constraint on an Open
|
||||||
open_lines = [
|
# directive; when none are given the account accepts
|
||||||
"",
|
# any commodity.
|
||||||
f"{opening_date.isoformat()} open {account_name} {currencies_str}"
|
open_directive = f"{opening_date.isoformat()} open {account_name}"
|
||||||
]
|
if currencies:
|
||||||
|
open_directive += f" {', '.join(currencies)}"
|
||||||
|
open_lines = ["", open_directive]
|
||||||
|
|
||||||
# Add metadata if provided
|
# Add metadata if provided
|
||||||
if metadata:
|
if metadata:
|
||||||
for key, value in metadata.items():
|
for key, value in metadata.items():
|
||||||
# Format metadata with proper indentation
|
# Format metadata with proper indentation
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
open_lines.append(f' {key}: "{value}"')
|
open_lines.append(
|
||||||
|
f' {key}: "{_escape_beancount_string(value)}"'
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
open_lines.append(f' {key}: {value}')
|
open_lines.append(f' {key}: {value}')
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue