Implement editing and deleting balance assertions

This commit is contained in:
RunasSudo 2024-11-22 18:54:28 +11:00
parent e791fb2a8a
commit c5c3d1133a
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
6 changed files with 242 additions and 16 deletions

View File

@ -0,0 +1,117 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="grid grid-cols-[max-content_1fr] space-y-2 mb-4 items-baseline">
<label for="dt" class="block text-gray-900 pr-4">Date</label>
<div>
<input type="date" class="bordered-field" id="dt" v-model="assertion.dt">
</div>
<label for="description" class="block text-gray-900 pr-4">Description</label>
<div>
<input type="text" class="bordered-field" id="description" v-model="assertion.description" placeholder=" ">
</div>
<label for="account" class="block text-gray-900 pr-4">Account</label>
<div class="relative combobox">
<input type="text" class="bordered-field peer" id="account" v-model="assertion.account" placeholder=" " autocomplete="off">
<!--{% include 'components/accounts_combobox_inner.html' %}-->
</div>
<label for="amount" class="block text-gray-900 pr-4">Balance</label>
<div class="relative shadow-sm">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span>
</div>
<!-- TODO: Display existing credit assertion as credit, not as negative debit -->
<input type="number" class="bordered-field pl-7 pr-16" step="0.01" v-model="assertion.amount_abs" placeholder="0.00">
<div class="absolute inset-y-0 right-0 flex items-center">
<select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" v-model="assertion.sign">
<option value="dr">Dr</option>
<option value="cr">Cr</option>
</select>
</div>
</div>
</div>
<div class="flex justify-end mt-4 space-x-2">
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAssertion" v-if="assertion.id !== null">Delete</button>
<button class="btn-primary" @click="saveAssertion">Save</button>
</div>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { defineProps } from 'vue';
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
export interface EditingAssertion {
id: number | null,
dt: string,
description: string,
account: string,
sign: string,
amount_abs: string,
}
const { assertion } = defineProps<{ assertion: EditingAssertion }>();
async function saveAssertion() {
// Save changes to the assertion
const amount_abs = deserialiseAmount('' + assertion.amount_abs);
const quantity = assertion.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity;
const session = await db.load();
if (assertion.id === null) {
await session.execute(
`INSERT INTO balance_assertions (dt, description, account, quantity, commodity)
VALUES ($1, $2, $3, $4, $5)`,
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity]
);
} else {
await session.execute(
`UPDATE balance_assertions
SET dt = $1, description = $2, account = $3, quantity = $4, commodity = $5
WHERE id = $6`,
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity, assertion.id]
);
}
await getCurrentWindow().close();
}
async function deleteAssertion() {
// Delete the current assertion
if (!await confirm('Are you sure you want to delete this balance assertion? This operation is irreversible.')) {
return;
}
const session = await db.load();
await session.execute(
`DELETE FROM balance_assertions
WHERE id = $1`,
[assertion.id]
);
await getCurrentWindow().close();
}
</script>

View File

@ -31,6 +31,8 @@ async function initApp() {
const routes = [ const routes = [
{ path: '/', name: 'index', component: () => import('./pages/HomeView.vue') }, { path: '/', name: 'index', component: () => import('./pages/HomeView.vue') },
{ path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') }, { path: '/balance-assertions', name: 'balance-assertions', component: () => import('./pages/BalanceAssertionsView.vue') },
{ path: '/balance-assertions/edit/:id', name: 'balance-assertions-edit', component: () => import('./pages/EditBalanceAssertionView.vue') },
{ path: '/balance-assertions/new', name: 'balance-assertions-new', component: () => import('./pages/NewBalanceAssertionView.vue') },
{ path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') }, { path: '/chart-of-accounts', name: 'chart-of-accounts', component: () => import('./pages/ChartOfAccountsView.vue') },
{ path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') }, { path: '/general-ledger', name: 'general-ledger', component: () => import('./pages/GeneralLedgerView.vue') },
{ path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') }, { path: '/journal', name: 'journal', component: () => import('./pages/JournalView.vue') },

View File

@ -22,12 +22,10 @@
</h1> </h1>
<div class="my-4 flex gap-x-2"> <div class="my-4 flex gap-x-2">
<!--<a href="{{ url_for('balance_assertions_new') }}" class="btn-primary pl-2"> <a href="/balance-assertions/new" class="btn-primary pl-2" onclick="return openLinkInNewWindow(this);">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <PlusIcon class="w-4 h-4" />
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
New assertion New assertion
</a>--> </a>
</div> </div>
<table class="min-w-full"> <table class="min-w-full">
@ -54,10 +52,8 @@
<XMarkIcon class="w-4 h-4 text-red-500" v-if="!assertion.isValid" /> <XMarkIcon class="w-4 h-4 text-red-500" v-if="!assertion.isValid" />
</td> </td>
<td class="py-0.5 pl-1 text-gray-900 text-end"> <td class="py-0.5 pl-1 text-gray-900 text-end">
<a href="#" class="text-gray-500 hover:text-gray-700"> <a :href="'/balance-assertions/edit/' + assertion.id" class="text-gray-500 hover:text-gray-700" onclick="return openLinkInNewWindow(this);">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 inline align-middle -mt-0.5"> <PencilIcon class="w-4 h-4" />
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>
</a> </a>
</td> </td>
</tr> </tr>
@ -72,7 +68,8 @@
import { db, totalBalancesAtDate } from '../db.ts'; import { db, totalBalancesAtDate } from '../db.ts';
import { pp } from '../display.ts'; import { pp } from '../display.ts';
import { CheckIcon, XMarkIcon } from '@heroicons/vue/24/outline'; import { CheckIcon, PencilIcon, XMarkIcon } from '@heroicons/vue/24/outline';
import { PlusIcon } from '@heroicons/vue/16/solid';
const balanceAssertions = ref([] as ValidatedBalanceAssertion[]); const balanceAssertions = ref([] as ValidatedBalanceAssertion[]);

View File

@ -0,0 +1,67 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<h1 class="page-heading mb-4">
Edit balance assertion
</h1>
<BalanceAssertionEditor :assertion="assertion" />
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { db, serialiseAmount } from '../db.ts';
import BalanceAssertionEditor, { EditingAssertion } from '../components/BalanceAssertionEditor.vue';
const route = useRoute();
const assertion = ref({
id: null,
dt: null!,
description: null!,
account: null!,
sign: null!,
amount_abs: null!,
} as EditingAssertion);
async function load() {
const session = await db.load();
const rawAssertions: any[] = await session.select(
`SELECT *
FROM balance_assertions
WHERE id = $1`,
[route.params.id]
);
const rawAssertion = rawAssertions[0];
// Format parameters for display
rawAssertion.dt = dayjs(rawAssertion.dt).format('YYYY-MM-DD');
rawAssertion.sign = rawAssertion.quantity >= 0 ? 'dr' : 'cr';
rawAssertion.amount_abs = serialiseAmount(Math.abs(rawAssertion.quantity), rawAssertion.commodity);
assertion.value = rawAssertion as EditingAssertion;
}
load();
</script>

View File

@ -30,7 +30,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { JoinedTransactionPosting, Posting, db, joinedToTransactions, serialiseAmount } from '../db.ts'; import { JoinedTransactionPosting, db, joinedToTransactions, serialiseAmount } from '../db.ts';
import TransactionEditor, { EditingTransaction } from '../components/TransactionEditor.vue'; import TransactionEditor, { EditingTransaction } from '../components/TransactionEditor.vue';
const route = useRoute(); const route = useRoute();
@ -56,16 +56,18 @@
const transactions = joinedToTransactions(joinedTransactionPostings); const transactions = joinedToTransactions(joinedTransactionPostings);
if (transactions.length !== 1) { throw new Error('Unexpected number of transactions returned from SQL'); } if (transactions.length !== 1) { throw new Error('Unexpected number of transactions returned from SQL'); }
transaction.value = transactions[0] as unknown as EditingTransaction; const rawTransaction = transactions[0] as any;
// Format dt // Format dt
transaction.value.dt = dayjs(transaction.value.dt).format('YYYY-MM-DD') rawTransaction.dt = dayjs(rawTransaction.dt).format('YYYY-MM-DD');
// Initialise sign and amount_abs // Initialise sign and amount_abs
for (const posting of transaction.value.postings) { for (const posting of rawTransaction.postings) {
posting.sign = (posting as unknown as Posting).quantity! >= 0 ? 'dr' : 'cr'; posting.sign = posting.quantity >= 0 ? 'dr' : 'cr';
posting.amount_abs = serialiseAmount(Math.abs((posting as unknown as Posting).quantity), (posting as unknown as Posting).commodity); posting.amount_abs = serialiseAmount(Math.abs(posting.quantity), posting.commodity);
} }
transaction.value = rawTransaction as EditingTransaction;
} }
load(); load();
</script> </script>

View File

@ -0,0 +1,41 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<h1 class="page-heading mb-4">
New balance assertion
</h1>
<BalanceAssertionEditor :assertion="assertion" />
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import { ref } from 'vue';
import BalanceAssertionEditor, { EditingAssertion } from '../components/BalanceAssertionEditor.vue';
const assertion = ref({
id: null,
dt: dayjs().format('YYYY-MM-DD'),
description: '',
account: '',
sign: 'dr',
amount_abs: '',
} as EditingAssertion);
</script>