diff --git a/fava_client.py b/fava_client.py
index c61e1d9..afc964f 100644
--- a/fava_client.py
+++ b/fava_client.py
@@ -44,6 +44,22 @@ def _infer_target_file(account_name: str) -> str:
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:
"""
Async client for Fava REST API.
@@ -1544,7 +1560,7 @@ class FavaClient:
async def add_account(
self,
account_name: str,
- currencies: list[str],
+ currencies: Optional[list[str]] = None,
opening_date: Optional[date] = None,
metadata: Optional[Dict[str, Any]] = None,
target_file: Optional[str] = None,
@@ -1642,19 +1658,23 @@ class FavaClient:
lines = source.split('\n')
insert_index = len(lines)
- # Step 4: Format Open directive as Beancount text
- currencies_str = ", ".join(currencies)
- open_lines = [
- "",
- f"{opening_date.isoformat()} open {account_name} {currencies_str}"
- ]
+ # Step 4: Format Open directive as Beancount text.
+ # Currencies are an optional constraint on an Open
+ # directive; when none are given the account accepts
+ # any commodity.
+ open_directive = f"{opening_date.isoformat()} open {account_name}"
+ if currencies:
+ open_directive += f" {', '.join(currencies)}"
+ open_lines = ["", open_directive]
# Add metadata if provided
if metadata:
for key, value in metadata.items():
# Format metadata with proper indentation
if isinstance(value, str):
- open_lines.append(f' {key}: "{value}"')
+ open_lines.append(
+ f' {key}: "{_escape_beancount_string(value)}"'
+ )
else:
open_lines.append(f' {key}: {value}')
diff --git a/static/js/index.js b/static/js/index.js
index 418ec41..a10d50c 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -69,6 +69,12 @@ window.app = Vue.createApp({
userWalletId: '',
loading: false
},
+ addAccountDialog: {
+ show: false,
+ name: '',
+ description: '',
+ loading: false
+ },
receivableDialog: {
show: false,
selectedUser: '',
@@ -566,6 +572,45 @@ window.app = Vue.createApp({
this.syncingAccounts = false
}
},
+ showAddAccountDialog() {
+ this.addAccountDialog.name = ''
+ this.addAccountDialog.description = ''
+ this.addAccountDialog.show = true
+ },
+ async submitAddAccount() {
+ const name = (this.addAccountDialog.name || '').trim()
+ const validPrefixes = ['Assets:', 'Liabilities:', 'Equity:', 'Income:', 'Expenses:']
+ if (!validPrefixes.some(p => name.startsWith(p))) {
+ this.$q.notify({
+ type: 'warning',
+ message: `Account name must start with one of: ${validPrefixes.join(', ')}`
+ })
+ return
+ }
+ this.addAccountDialog.loading = true
+ try {
+ const {data} = await LNbits.api.request(
+ 'POST',
+ '/libra/api/v1/admin/accounts',
+ this.g.user.wallets[0].adminkey,
+ {
+ name,
+ description: this.addAccountDialog.description || null
+ }
+ )
+ this.$q.notify({
+ type: 'positive',
+ message: `Account ${data.account_name} created` +
+ (data.synced_to_libra_db ? '' : ' (sync pending)')
+ })
+ this.addAccountDialog.show = false
+ await this.loadAccounts()
+ } catch (error) {
+ LNbits.utils.notifyApiError(error)
+ } finally {
+ this.addAccountDialog.loading = false
+ }
+ },
showSettingsDialog() {
this.settingsDialog.libraWalletId = this.settings?.libra_wallet_id || ''
this.settingsDialog.favaUrl = this.settings?.fava_url || 'http://localhost:3333'
diff --git a/templates/libra/index.html b/templates/libra/index.html
index 0de0e71..f36c466 100644
--- a/templates/libra/index.html
+++ b/templates/libra/index.html
@@ -857,7 +857,20 @@
-
Chart of Accounts
+
+
Chart of Accounts
+
+
+
@@ -1232,6 +1245,50 @@
+
+
+
+
+
Add Account
+
+
+
+
+
+
+ Creates an Open directive in the Beancount ledger and syncs it into Libra
+ so permissions can be granted. Per-user accounts are managed automatically.
+