Compare commits
No commits in common. "65d6a9bf05802bbb80ca2e7ca9dc01791df5a586" and "54ef0de03b89176ca7987f1cb12394908b4f1784" have entirely different histories.
65d6a9bf05
...
54ef0de03b
@ -446,35 +446,33 @@ function reporting.CalculateIncomeTax.execute(args, context, kinds_for_account,
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Charge monthly tax
|
-- Charge monthly tax
|
||||||
if this_month_tax ~= 0 then
|
table.insert(transactions, {
|
||||||
table.insert(transactions, {
|
id = nil,
|
||||||
id = nil,
|
dt = libdrcr.date_to_dt(libdrcr.format_date(this_year, month, this_day)),
|
||||||
dt = libdrcr.date_to_dt(libdrcr.format_date(this_year, month, this_day)),
|
description = 'Estimated income tax',
|
||||||
description = 'Estimated income tax',
|
postings = {
|
||||||
postings = {
|
{
|
||||||
{
|
id = nil,
|
||||||
id = nil,
|
transaction_id = nil,
|
||||||
transaction_id = nil,
|
description = nil,
|
||||||
description = nil,
|
account = INCOME_TAX,
|
||||||
account = INCOME_TAX,
|
quantity = this_month_tax,
|
||||||
quantity = this_month_tax,
|
commodity = context.reporting_commodity,
|
||||||
commodity = context.reporting_commodity,
|
quantity_ascost = this_month_tax,
|
||||||
quantity_ascost = this_month_tax,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id = nil,
|
|
||||||
transaction_id = nil,
|
|
||||||
description = nil,
|
|
||||||
account = INCOME_TAX_CONTROL,
|
|
||||||
quantity = -this_month_tax,
|
|
||||||
commodity = context.reporting_commodity,
|
|
||||||
quantity_ascost = -this_month_tax,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
end
|
id = nil,
|
||||||
|
transaction_id = nil,
|
||||||
|
description = nil,
|
||||||
|
account = INCOME_TAX_CONTROL,
|
||||||
|
quantity = -this_month_tax,
|
||||||
|
commodity = context.reporting_commodity,
|
||||||
|
quantity_ascost = -this_month_tax,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
end
|
end
|
||||||
elseif (tax_total - total_offset) ~= 0 then
|
else
|
||||||
-- Charge income tax expense in one transaction at EOFY
|
-- Charge income tax expense in one transaction at EOFY
|
||||||
table.insert(transactions, {
|
table.insert(transactions, {
|
||||||
id = nil,
|
id = nil,
|
||||||
@ -504,33 +502,31 @@ function reporting.CalculateIncomeTax.execute(args, context, kinds_for_account,
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Mandatory study loan repayment
|
-- Mandatory study loan repayment
|
||||||
if study_loan_repayment ~= 0 then
|
table.insert(transactions, {
|
||||||
table.insert(transactions, {
|
id = nil,
|
||||||
id = nil,
|
dt = libdrcr.date_to_dt(context.eofy_date),
|
||||||
dt = libdrcr.date_to_dt(context.eofy_date),
|
description = 'Mandatory study loan repayment payable',
|
||||||
description = 'Mandatory study loan repayment payable',
|
postings = {
|
||||||
postings = {
|
{
|
||||||
{
|
id = nil,
|
||||||
id = nil,
|
transaction_id = nil,
|
||||||
transaction_id = nil,
|
description = nil,
|
||||||
description = nil,
|
account = HELP,
|
||||||
account = HELP,
|
quantity = study_loan_repayment,
|
||||||
quantity = study_loan_repayment,
|
commodity = context.reporting_commodity,
|
||||||
commodity = context.reporting_commodity,
|
quantity_ascost = study_loan_repayment,
|
||||||
quantity_ascost = study_loan_repayment,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id = nil,
|
|
||||||
transaction_id = nil,
|
|
||||||
description = nil,
|
|
||||||
account = INCOME_TAX_CONTROL,
|
|
||||||
quantity = -study_loan_repayment,
|
|
||||||
commodity = context.reporting_commodity,
|
|
||||||
quantity_ascost = -study_loan_repayment,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
{
|
||||||
end
|
id = nil,
|
||||||
|
transaction_id = nil,
|
||||||
|
description = nil,
|
||||||
|
account = INCOME_TAX_CONTROL,
|
||||||
|
quantity = -study_loan_repayment,
|
||||||
|
commodity = context.reporting_commodity,
|
||||||
|
quantity_ascost = -study_loan_repayment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
-- Transfer PAYGW balances to Income Tax Control
|
-- Transfer PAYGW balances to Income Tax Control
|
||||||
for account, kinds in pairs(kinds_for_account) do
|
for account, kinds in pairs(kinds_for_account) do
|
||||||
|
14
src/App.vue
14
src/App.vue
@ -22,13 +22,8 @@
|
|||||||
<div class="py-8">
|
<div class="py-8">
|
||||||
<main>
|
<main>
|
||||||
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl px-6 lg:px-8">
|
||||||
<template v-if="error === null">
|
<NoFileView v-if="!(db.filename !== null || route.name === 'new-file')" />
|
||||||
<NoFileView v-if="!(db.filename !== null || route.name === 'new-file')" />
|
<RouterView v-if="db.filename !== null || route.name === 'new-file'" />
|
||||||
<RouterView v-if="db.filename !== null || route.name === 'new-file'" />
|
|
||||||
</template>
|
|
||||||
<template v-if="error !== null">
|
|
||||||
<CriticalErrorView />
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@ -36,16 +31,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onErrorCaptured } from 'vue';
|
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import HeaderBar from './components/HeaderBar.vue';
|
import HeaderBar from './components/HeaderBar.vue';
|
||||||
import { db } from './db.js';
|
import { db } from './db.js';
|
||||||
import { error, handleCriticalError } from './error.js';
|
|
||||||
import CriticalErrorView from './pages/CriticalErrorView.vue';
|
|
||||||
import NoFileView from './pages/NoFileView.vue';
|
import NoFileView from './pages/NoFileView.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
onErrorCaptured((err) => handleCriticalError(err));
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -47,27 +47,14 @@
|
|||||||
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAssertion" v-if="assertion.id !== null">Delete</button>
|
<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>
|
<button class="btn-primary" @click="saveAssertion">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm text-red-700">{{ error }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { DeserialiseAmountError, DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
|
||||||
|
import { DT_FORMAT, db, deserialiseAmount } from '../db.ts';
|
||||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
||||||
|
|
||||||
export interface EditingAssertion {
|
export interface EditingAssertion {
|
||||||
@ -81,34 +68,19 @@
|
|||||||
|
|
||||||
const { assertion } = defineProps<{ assertion: EditingAssertion }>();
|
const { assertion } = defineProps<{ assertion: EditingAssertion }>();
|
||||||
|
|
||||||
const error = ref(null as string | null);
|
|
||||||
|
|
||||||
async function saveAssertion() {
|
async function saveAssertion() {
|
||||||
// Save changes to the assertion
|
// Save changes to the assertion
|
||||||
error.value = null;
|
const amount_abs = deserialiseAmount('' + assertion.amount_abs);
|
||||||
|
|
||||||
let amount_abs;
|
|
||||||
try {
|
|
||||||
amount_abs = deserialiseAmount('' + assertion.amount_abs);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof DeserialiseAmountError) {
|
|
||||||
error.value = err.message;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const quantity = assertion.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity;
|
const quantity = assertion.sign === 'dr' ? amount_abs.quantity : -amount_abs.quantity;
|
||||||
|
|
||||||
const session = await db.load();
|
const session = await db.load();
|
||||||
|
|
||||||
if (assertion.id === null) {
|
if (assertion.id === null) {
|
||||||
const result = await session.execute(
|
await session.execute(
|
||||||
`INSERT INTO balance_assertions (dt, description, account, quantity, commodity)
|
`INSERT INTO balance_assertions (dt, description, account, quantity, commodity)
|
||||||
VALUES ($1, $2, $3, $4, $5)`,
|
VALUES ($1, $2, $3, $4, $5)`,
|
||||||
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity]
|
[dayjs(assertion.dt).format(DT_FORMAT), assertion.description, assertion.account, quantity, amount_abs.commodity]
|
||||||
);
|
);
|
||||||
assertion.id = result.lastInsertId!;
|
|
||||||
} else {
|
} else {
|
||||||
await session.execute(
|
await session.execute(
|
||||||
`UPDATE balance_assertions
|
`UPDATE balance_assertions
|
||||||
@ -118,7 +90,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await emit('balance-assertion-updated', {id: assertion.id});
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,7 +107,6 @@
|
|||||||
[assertion.id]
|
[assertion.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
await emit('balance-assertion-updated', {id: assertion.id});
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -109,7 +109,7 @@
|
|||||||
|
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { DT_FORMAT, DeserialiseAmountError, Posting, Transaction, db, deserialiseAmount } from '../db.ts';
|
import { DT_FORMAT, Posting, Transaction, db, deserialiseAmount } from '../db.ts';
|
||||||
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
import ComboBoxAccounts from './ComboBoxAccounts.vue';
|
||||||
|
|
||||||
interface EditingPosting {
|
interface EditingPosting {
|
||||||
@ -155,17 +155,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const posting of transaction.postings) {
|
for (const posting of transaction.postings) {
|
||||||
let amount_abs;
|
const amount_abs = deserialiseAmount(posting.amount_abs);
|
||||||
try {
|
|
||||||
amount_abs = deserialiseAmount(posting.amount_abs);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof DeserialiseAmountError) {
|
|
||||||
error.value = err.message;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newTransaction.postings.push({
|
newTransaction.postings.push({
|
||||||
id: posting.id,
|
id: posting.id,
|
||||||
@ -320,7 +310,6 @@
|
|||||||
|
|
||||||
await dbTransaction.commit();
|
await dbTransaction.commit();
|
||||||
|
|
||||||
await emit('transaction-updated', {id: transaction.id});
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
28
src/db.ts
28
src/db.ts
@ -25,7 +25,6 @@ import { reactive } from 'vue';
|
|||||||
|
|
||||||
import { Balance } from './amounts.ts';
|
import { Balance } from './amounts.ts';
|
||||||
import { ExtendedDatabase } from './dbutil.ts';
|
import { ExtendedDatabase } from './dbutil.ts';
|
||||||
import { CriticalError } from './error.ts';
|
|
||||||
|
|
||||||
export const DB_VERSION = 3; // Should match schema.sql
|
export const DB_VERSION = 3; // Should match schema.sql
|
||||||
export const DT_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS000';
|
export const DT_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS000';
|
||||||
@ -54,23 +53,8 @@ export const db = reactive({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (filename !== null) {
|
if (filename !== null) {
|
||||||
const session = await this.load();
|
|
||||||
|
|
||||||
// Validate database version
|
|
||||||
let dbVersion: {value: string}[];
|
|
||||||
try {
|
|
||||||
dbVersion = await session.select("SELECT value FROM metadata WHERE key = 'version'");
|
|
||||||
} catch (err) {
|
|
||||||
throw new CriticalError('Unable to parse database (SQL error getting metadata.version)', err);
|
|
||||||
}
|
|
||||||
if (dbVersion.length === 0) {
|
|
||||||
throw new CriticalError('Unable to parse database (no metadata.version)');
|
|
||||||
}
|
|
||||||
if (dbVersion[0].value !== DB_VERSION.toString()) {
|
|
||||||
throw new CriticalError('Unsupported database version ' + dbVersion[0].value + ' (expected ' + DB_VERSION + ')');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialise cached data
|
// Initialise cached data
|
||||||
|
const session = await this.load();
|
||||||
const metadataRaw: {key: string, value: string}[] = await session.select("SELECT * FROM metadata");
|
const metadataRaw: {key: string, value: string}[] = await session.select("SELECT * FROM metadata");
|
||||||
const metadataObject = Object.fromEntries(metadataRaw.map((x) => [x.key, x.value]));
|
const metadataObject = Object.fromEntries(metadataRaw.map((x) => [x.key, x.value]));
|
||||||
this.metadata.version = parseInt(metadataObject.version);
|
this.metadata.version = parseInt(metadataObject.version);
|
||||||
@ -78,6 +62,8 @@ export const db = reactive({
|
|||||||
this.metadata.reporting_commodity = metadataObject.reporting_commodity;
|
this.metadata.reporting_commodity = metadataObject.reporting_commodity;
|
||||||
this.metadata.dps = parseInt(metadataObject.amount_dps);
|
this.metadata.dps = parseInt(metadataObject.amount_dps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Validate database version
|
||||||
},
|
},
|
||||||
|
|
||||||
load: async function(): Promise<ExtendedDatabase> {
|
load: async function(): Promise<ExtendedDatabase> {
|
||||||
@ -176,8 +162,7 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
|||||||
// Default commodity
|
// Default commodity
|
||||||
const quantity = Math.round(parseFloat(amount) * factor)
|
const quantity = Math.round(parseFloat(amount) * factor)
|
||||||
|
|
||||||
if (Number.isNaN(quantity)) { throw new DeserialiseAmountError('Invalid quantity: ' + amount); }
|
if (!Number.isSafeInteger(quantity)) { throw new Error('Quantity not representable by safe integer'); }
|
||||||
if (!Number.isSafeInteger(quantity)) { throw new DeserialiseAmountError('Quantity not representable by safe integer: ' + amount); }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
@ -190,8 +175,7 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
|||||||
const quantityStr = amount.substring(0, amount.indexOf(' '));
|
const quantityStr = amount.substring(0, amount.indexOf(' '));
|
||||||
const quantity = Math.round(parseFloat(quantityStr) * factor)
|
const quantity = Math.round(parseFloat(quantityStr) * factor)
|
||||||
|
|
||||||
if (Number.isNaN(quantity)) { throw new DeserialiseAmountError('Invalid quantity: ' + amount); }
|
if (!Number.isSafeInteger(quantity)) { throw new Error('Quantity not representable by safe integer'); }
|
||||||
if (!Number.isSafeInteger(quantity)) { throw new DeserialiseAmountError('Quantity not representable by safe integer: ' + amount); }
|
|
||||||
|
|
||||||
const commodity = amount.substring(amount.indexOf(' ') + 1);
|
const commodity = amount.substring(amount.indexOf(' ') + 1);
|
||||||
|
|
||||||
@ -201,8 +185,6 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeserialiseAmountError extends Error {}
|
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
|
|
||||||
export class Transaction {
|
export class Transaction {
|
||||||
|
39
src/error.ts
39
src/error.ts
@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
DrCr: Web-based double-entry bookkeeping framework
|
|
||||||
Copyright (C) 2022-2025 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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
// Global error state
|
|
||||||
export const error = ref(null as CriticalError | null);
|
|
||||||
|
|
||||||
export class CriticalError extends Error {
|
|
||||||
public error?: any;
|
|
||||||
|
|
||||||
constructor(message: string, error?: any) {
|
|
||||||
super(message);
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function handleCriticalError(err: any) {
|
|
||||||
if (err instanceof CriticalError) {
|
|
||||||
error.value = err;
|
|
||||||
} else {
|
|
||||||
error.value = new CriticalError('An unexpected error occurred', err);
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,7 +25,6 @@ import { createRouter, createWebHistory } from 'vue-router';
|
|||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
import { db } from './db.ts';
|
import { db } from './db.ts';
|
||||||
import { handleCriticalError } from './error.ts';
|
|
||||||
|
|
||||||
async function initApp() {
|
async function initApp() {
|
||||||
// Init router
|
// Init router
|
||||||
@ -62,11 +61,7 @@ async function initApp() {
|
|||||||
// Init state
|
// Init state
|
||||||
const dbFilename: string = await invoke('get_open_filename');
|
const dbFilename: string = await invoke('get_open_filename');
|
||||||
if (dbFilename !== null) {
|
if (dbFilename !== null) {
|
||||||
try {
|
await db.init(dbFilename); // Ensure all metadata cached before loading Vue
|
||||||
await db.init(dbFilename); // Ensure all metadata cached before loading Vue
|
|
||||||
} catch (err) {
|
|
||||||
handleCriticalError(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Vue app
|
// Create Vue app
|
||||||
|
@ -66,8 +66,7 @@
|
|||||||
import { CheckIcon, PencilIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
import { CheckIcon, PencilIcon, XMarkIcon } from '@heroicons/vue/24/outline';
|
||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
import { ref } from 'vue';
|
||||||
import { onUnmounted, ref } from 'vue';
|
|
||||||
|
|
||||||
import { db } from '../db.ts';
|
import { db } from '../db.ts';
|
||||||
import { pp } from '../display.ts';
|
import { pp } from '../display.ts';
|
||||||
@ -98,17 +97,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
||||||
// Refresh balance assertions list when assertion updated
|
|
||||||
let unlistenAssertionUpdated: UnlistenFn | null = null;
|
|
||||||
(async () => {
|
|
||||||
// Cannot await at top level without <Suspense> therefore do this in an async function
|
|
||||||
unlistenAssertionUpdated = await listen('balance-assertion-updated', async (_event) => { await load(); });
|
|
||||||
})();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (unlistenAssertionUpdated !== null) {
|
|
||||||
unlistenAssertionUpdated();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
<!--
|
|
||||||
DrCr: Web-based double-entry bookkeeping framework
|
|
||||||
Copyright (C) 2022-2025 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="rounded-md bg-red-50 mt-4 p-4 col-span-2">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm text-red-700">{{ error.message }}</p>
|
|
||||||
<p class="text-sm text-red-700 mt-1" v-if="error.error">{{ error.error }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup type="ts">
|
|
||||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
|
||||||
|
|
||||||
import { error } from '../error.ts';
|
|
||||||
|
|
||||||
console.error(error.value);
|
|
||||||
</script>
|
|
@ -60,28 +60,14 @@
|
|||||||
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAdjustment" v-if="adjustment.id !== null">Delete</button>
|
<button class="btn-secondary text-red-600 ring-red-500" @click="deleteAdjustment" v-if="adjustment.id !== null">Delete</button>
|
||||||
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm text-red-700">{{ error }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
||||||
import { DT_FORMAT, DeserialiseAmountError, db, deserialiseAmount } from '../../db.ts';
|
import { DT_FORMAT, db, deserialiseAmount } from '../../db.ts';
|
||||||
|
|
||||||
export interface EditingCGTAdjustment {
|
export interface EditingCGTAdjustment {
|
||||||
id: number | null,
|
id: number | null,
|
||||||
@ -96,36 +82,10 @@
|
|||||||
|
|
||||||
const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>();
|
const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>();
|
||||||
|
|
||||||
const error = ref(null as string | null);
|
|
||||||
|
|
||||||
async function saveAdjustment() {
|
async function saveAdjustment() {
|
||||||
// Save changes to the CGT adjustment
|
// Save changes to the CGT adjustment
|
||||||
error.value = null;
|
const asset = deserialiseAmount('' + adjustment.asset);
|
||||||
|
const cost_adjustment_abs = deserialiseAmount('' + adjustment.cost_adjustment_abs);
|
||||||
let asset;
|
|
||||||
try {
|
|
||||||
asset = deserialiseAmount('' + adjustment.asset);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof DeserialiseAmountError) {
|
|
||||||
error.value = err.message;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cost_adjustment_abs;
|
|
||||||
try {
|
|
||||||
cost_adjustment_abs = deserialiseAmount('' + adjustment.cost_adjustment_abs);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof DeserialiseAmountError) {
|
|
||||||
error.value = err.message;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cost_adjustment = adjustment.sign === 'dr' ? cost_adjustment_abs.quantity : -cost_adjustment_abs.quantity;
|
const cost_adjustment = adjustment.sign === 'dr' ? cost_adjustment_abs.quantity : -cost_adjustment_abs.quantity;
|
||||||
|
|
||||||
const session = await db.load();
|
const session = await db.load();
|
||||||
@ -145,7 +105,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await emit('cgt-adjustment-updated');
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +122,6 @@
|
|||||||
[adjustment.id]
|
[adjustment.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
await emit('cgt-adjustment-updated');
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -70,8 +70,7 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { PencilIcon } from '@heroicons/vue/24/outline';
|
import { PencilIcon } from '@heroicons/vue/24/outline';
|
||||||
import { PlusIcon } from '@heroicons/vue/16/solid';
|
import { PlusIcon } from '@heroicons/vue/16/solid';
|
||||||
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
import { ref } from 'vue';
|
||||||
import { onUnmounted, ref } from 'vue';
|
|
||||||
|
|
||||||
import { CGTAdjustment, cgtAssetCommodityName } from './cgt.ts';
|
import { CGTAdjustment, cgtAssetCommodityName } from './cgt.ts';
|
||||||
import { asCost } from '../../amounts.ts';
|
import { asCost } from '../../amounts.ts';
|
||||||
@ -91,17 +90,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
|
||||||
// Refresh CGT adjustments list when CGT adjustment updated
|
|
||||||
let unlistenAdjustmentUpdated: UnlistenFn | null = null;
|
|
||||||
(async () => {
|
|
||||||
// Cannot await at top level without <Suspense> therefore do this in an async function
|
|
||||||
unlistenAdjustmentUpdated = await listen('cgt-adjustment-updated', async (_event) => { await load(); });
|
|
||||||
})();
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (unlistenAdjustmentUpdated !== null) {
|
|
||||||
unlistenAdjustmentUpdated();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -70,32 +70,18 @@
|
|||||||
<div class="flex justify-end mt-4 space-x-2">
|
<div class="flex justify-end mt-4 space-x-2">
|
||||||
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
<button class="btn-primary" @click="saveAdjustment">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2" v-if="error !== null">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<XCircleIcon class="h-5 w-5 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm text-red-700">{{ error }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { XCircleIcon } from '@heroicons/vue/24/solid';
|
|
||||||
import { InformationCircleIcon } from '@heroicons/vue/20/solid';
|
import { InformationCircleIcon } from '@heroicons/vue/20/solid';
|
||||||
import { emit } from '@tauri-apps/api/event';
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { CGTAsset } from './cgt.ts';
|
import { CGTAsset } from './cgt.ts';
|
||||||
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
import ComboBoxAccounts from '../../components/ComboBoxAccounts.vue';
|
||||||
import { DT_FORMAT, DeserialiseAmountError, JoinedTransactionPosting, db, deserialiseAmount } from '../../db.ts';
|
import { DT_FORMAT, JoinedTransactionPosting, db, deserialiseAmount } from '../../db.ts';
|
||||||
import { ppWithCommodity } from '../../display.ts';
|
import { ppWithCommodity } from '../../display.ts';
|
||||||
import { CriticalError } from '../../error.ts';
|
|
||||||
|
|
||||||
const account = ref('');
|
const account = ref('');
|
||||||
const commodity = ref('');
|
const commodity = ref('');
|
||||||
@ -104,25 +90,10 @@
|
|||||||
const cost_adjustment_abs = ref(null! as number);
|
const cost_adjustment_abs = ref(null! as number);
|
||||||
const sign = ref('dr');
|
const sign = ref('dr');
|
||||||
|
|
||||||
const error = ref(null as string | null);
|
|
||||||
|
|
||||||
async function saveAdjustment() {
|
async function saveAdjustment() {
|
||||||
// TODO: Preview mode?
|
// TODO: Preview mode?
|
||||||
|
|
||||||
error.value = null;
|
const totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
|
||||||
|
|
||||||
let totalAdjustmentAbs;
|
|
||||||
try {
|
|
||||||
totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof DeserialiseAmountError) {
|
|
||||||
error.value = err.message;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalAdjustment = sign.value === 'dr' ? totalAdjustmentAbs.quantity : -totalAdjustmentAbs.quantity;
|
const totalAdjustment = sign.value === 'dr' ? totalAdjustmentAbs.quantity : -totalAdjustmentAbs.quantity;
|
||||||
|
|
||||||
// Get all postings to the CGT asset account
|
// Get all postings to the CGT asset account
|
||||||
@ -178,8 +149,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (assets.length === 0) {
|
if (assets.length === 0) {
|
||||||
error.value = 'No matching CGT assets';
|
throw new Error('No matching CGT assets');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Distribute total adjustment across matching assets
|
// Distribute total adjustment across matching assets
|
||||||
@ -222,7 +192,7 @@
|
|||||||
// Sanity check
|
// Sanity check
|
||||||
const totalRoundedAdjustment = cgtAdjustments.reduce((acc, adj) => acc + adj, 0);
|
const totalRoundedAdjustment = cgtAdjustments.reduce((acc, adj) => acc + adj, 0);
|
||||||
if (totalRoundedAdjustment !== totalAdjustment) {
|
if (totalRoundedAdjustment !== totalAdjustment) {
|
||||||
throw new CriticalError('Rounding unexpectedly changed total CGT adjustment amount');
|
throw new Error('Rounding unexpectedly changed total CGT adjustment amount');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add adjustments to database atomically
|
// Add adjustments to database atomically
|
||||||
@ -239,7 +209,6 @@
|
|||||||
}
|
}
|
||||||
await dbTransaction.commit();
|
await dbTransaction.commit();
|
||||||
|
|
||||||
await emit('cgt-adjustment-updated');
|
|
||||||
await getCurrentWindow().close();
|
await getCurrentWindow().close();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user