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"
|
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}')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,12 @@ 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: '',
|
||||||
|
|
@ -566,6 +572,45 @@ 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'
|
||||||
|
|
|
||||||
|
|
@ -857,7 +857,20 @@
|
||||||
<!-- Chart of Accounts -->
|
<!-- Chart of Accounts -->
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<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-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>
|
||||||
|
|
@ -1232,6 +1245,50 @@
|
||||||
</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">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue