Compare commits
2 commits
16ae6c2000
...
9dd46e818c
| Author | SHA1 | Date | |
|---|---|---|---|
| 9dd46e818c | |||
| 788a9998f6 |
3 changed files with 131 additions and 9 deletions
|
|
@ -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}')
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -857,7 +857,20 @@
|
|||
<!-- Chart of Accounts -->
|
||||
<q-card>
|
||||
<q-card-section>
|
||||
<h6 class="q-my-none q-mb-md">Chart of Accounts</h6>
|
||||
<div class="row items-center q-mb-md">
|
||||
<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-item v-for="account in accounts" :key="account.id">
|
||||
<q-item-section>
|
||||
|
|
@ -1232,6 +1245,50 @@
|
|||
</q-card>
|
||||
</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 -->
|
||||
<q-dialog v-model="receivableDialog.show" position="top">
|
||||
<q-card v-if="receivableDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue