Compare commits

..

No commits in common. "9dd46e818cfeee1cf16b7892e08dec456c5eed7f" and "16ae6c2000c00f22bb57de3a2f6516c91ccf1dd9" have entirely different histories.

3 changed files with 9 additions and 131 deletions

View file

@ -44,22 +44,6 @@ 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.
@ -1560,7 +1544,7 @@ class FavaClient:
async def add_account( async def add_account(
self, self,
account_name: str, account_name: str,
currencies: Optional[list[str]] = None, currencies: list[str],
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,
@ -1658,23 +1642,19 @@ 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 are an optional constraint on an Open currencies_str = ", ".join(currencies)
# directive; when none are given the account accepts open_lines = [
# any commodity. "",
open_directive = f"{opening_date.isoformat()} open {account_name}" f"{opening_date.isoformat()} open {account_name} {currencies_str}"
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( open_lines.append(f' {key}: "{value}"')
f' {key}: "{_escape_beancount_string(value)}"'
)
else: else:
open_lines.append(f' {key}: {value}') open_lines.append(f' {key}: {value}')

View file

@ -69,12 +69,6 @@ window.app = Vue.createApp({
userWalletId: '', userWalletId: '',
loading: false loading: false
}, },
addAccountDialog: {
show: false,
name: '',
description: '',
loading: false
},
receivableDialog: { receivableDialog: {
show: false, show: false,
selectedUser: '', selectedUser: '',
@ -572,45 +566,6 @@ window.app = Vue.createApp({
this.syncingAccounts = false 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() { showSettingsDialog() {
this.settingsDialog.libraWalletId = this.settings?.libra_wallet_id || '' this.settingsDialog.libraWalletId = this.settings?.libra_wallet_id || ''
this.settingsDialog.favaUrl = this.settings?.fava_url || 'http://localhost:3333' this.settingsDialog.favaUrl = this.settings?.fava_url || 'http://localhost:3333'

View file

@ -857,20 +857,7 @@
<!-- Chart of Accounts --> <!-- Chart of Accounts -->
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row items-center q-mb-md"> <h6 class="q-my-none q-mb-md">Chart of Accounts</h6>
<h6 class="q-my-none">Chart of Accounts</h6>
<q-space></q-space>
<q-btn
v-if="isSuperUser"
unelevated
dense
size="sm"
color="primary"
icon="add"
label="Add Account"
@click="showAddAccountDialog"
></q-btn>
</div>
<q-list dense v-if="accounts.length > 0"> <q-list dense v-if="accounts.length > 0">
<q-item v-for="account in accounts" :key="account.id"> <q-item v-for="account in accounts" :key="account.id">
<q-item-section> <q-item-section>
@ -1245,50 +1232,6 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<!-- Add Account Dialog -->
<q-dialog v-model="addAccountDialog.show" position="top">
<q-card v-if="addAccountDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitAddAccount" class="q-gutter-md">
<div class="text-h6 q-mb-md">Add Account</div>
<q-input
filled
dense
v-model.trim="addAccountDialog.name"
label="Account Name *"
placeholder="e.g., Expenses:Services:Domain"
hint="Full hierarchical name. Must start with Assets:, Liabilities:, Equity:, Income: or Expenses:"
></q-input>
<q-input
filled
dense
v-model.trim="addAccountDialog.description"
label="Description"
placeholder="Optional notes about this account"
></q-input>
<div class="text-caption text-grey">
Creates an Open directive in the Beancount ledger and syncs it into Libra
so permissions can be granted. Per-user accounts are managed automatically.
</div>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:loading="addAccountDialog.loading"
:disable="!addAccountDialog.name"
>
Create Account
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
<!-- Receivable Dialog --> <!-- Receivable Dialog -->
<q-dialog v-model="receivableDialog.show" position="top"> <q-dialog v-model="receivableDialog.show" position="top">
<q-card v-if="receivableDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card"> <q-card v-if="receivableDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">