diff --git a/src/main.ts b/src/main.ts
index d632f71..8a0779c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -46,6 +46,7 @@ async function initApp() {
{ path: '/trial-balance', name: 'trial-balance', component: () => import('./reports/TrialBalanceReport.vue') },
// TODO: Generate this list dynamically
{ path: '/austax/cgt-adjustments', name: 'cgt-adjustments', component: () => import('./plugins/austax/CGTAdjustmentsView.vue') },
+ { path: '/austax/cgt-assets', name: 'cgt-assets', component: () => import('./plugins/austax/CGTAssetsView.vue') },
{ path: '/austax/tax-summary', name: 'tax-summary', component: () => import('./plugins/austax/TaxSummaryReport.vue') },
];
const router = createRouter({
diff --git a/src/pages/HomeView.vue b/src/pages/HomeView.vue
index 2ed119b..0ca7b8f 100644
--- a/src/pages/HomeView.vue
+++ b/src/pages/HomeView.vue
@@ -43,6 +43,7 @@
Advanced reports
diff --git a/src/plugins/austax/CGTAdjustmentsView.vue b/src/plugins/austax/CGTAdjustmentsView.vue
index 31d00a2..5c8686c 100644
--- a/src/plugins/austax/CGTAdjustmentsView.vue
+++ b/src/plugins/austax/CGTAdjustmentsView.vue
@@ -68,7 +68,7 @@
import dayjs from 'dayjs';
import { ref } from 'vue';
- import { CGTAdjustment } from './model.ts';
+ import { CGTAdjustment, cgtAssetCommodityName } from './cgt.ts';
import { asCost } from '../../amounts.ts';
import { db } from '../../db.ts';
import { pp, ppBracketed } from '../../display.ts';
@@ -81,13 +81,9 @@
cgtAdjustments.value = await session.select(
`SELECT id, quantity, commodity, account, acquisition_dt, dt, description, cost_adjustment
FROM austax_cgt_cost_adjustments
- ORDER BY dt DESC, account, substr(commodity, 1, instr(commodity, ' {')), acquisition_dt, id DESC`
+ ORDER BY dt DESC, account, substr(commodity, 1, instr(commodity, ' {')), acquisition_dt DESC, id DESC`
);
}
load();
-
- function cgtAssetCommodityName(commodity: string): string {
- return commodity.substring(0, commodity.indexOf(' {'));
- }
diff --git a/src/plugins/austax/CGTAssetsView.vue b/src/plugins/austax/CGTAssetsView.vue
new file mode 100644
index 0000000..6f42bc2
--- /dev/null
+++ b/src/plugins/austax/CGTAssetsView.vue
@@ -0,0 +1,152 @@
+
+
+
+
+ CGT assets
+
+
+
+
+
+ |
+ |
+ |
+ Acquisition |
+ Adjustment |
+ |
+ Disposal |
+ |
+
+
+ Account |
+ Asset |
+ Units |
+ Date |
+ Value |
+ b/f |
+ {{ eofyDate?.format('YYYY') }} |
+ |
+ Date |
+ Value |
+ Gain |
+
+
+
+
+
+ {{ asset.account }}
+ |
+ {{ cgtAssetCommodityName(asset.commodity) }} |
+ {{ pp(asset.quantity) }} |
+ {{ dayjs(asset.acquisition_dt).format('YYYY-MM-DD') }} |
+ {{ pp(asCost(asset.quantity, asset.commodity)) }} |
+ |
+ |
+
+
+ |
+ {{ asset.disposal_dt !== null ? dayjs(asset.disposal_dt).format('YYYY-MM-DD') : '' }} |
+ {{ asset.disposal_value !== null ? pp(asset.disposal_value) : '' }} |
+ |
+
+
+
+
+
+
diff --git a/src/plugins/austax/cgt.ts b/src/plugins/austax/cgt.ts
new file mode 100644
index 0000000..03cc870
--- /dev/null
+++ b/src/plugins/austax/cgt.ts
@@ -0,0 +1,152 @@
+/*
+ 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 .
+*/
+
+import { asCost } from '../../amounts.ts';
+import { db, joinedToTransactions, JoinedTransactionPosting } from '../../db.ts';
+import { ExtendedDatabase } from '../../dbutil.ts';
+import { ppWithCommodity } from '../../display.ts';
+
+export interface CGTAdjustment {
+ id: number | null,
+ quantity: number,
+ commodity: string,
+ account: string,
+ acquisition_dt: string,
+ dt: string,
+ description: string,
+ cost_adjustment: number,
+}
+
+export class CGTAsset {
+ cost_adjustments: CGTAdjustment[];
+ disposal_dt: string | null;
+ disposal_value: number | null;
+
+ constructor(
+ public quantity: number,
+ public commodity: string,
+ public account: string,
+ public acquisition_dt: string
+ ) {
+ this.cost_adjustments = [];
+ this.disposal_dt = null;
+ this.disposal_value = null;
+ }
+}
+
+// Load CGT assets and cost adjustments from database
+export async function getCGTAssets(session: ExtendedDatabase) {
+ // Find all CGT asset accounts
+ const cgtAccounts = (await session.select(
+ `SELECT account FROM account_configurations
+ WHERE kind = 'austax.cgtasset'`
+ ) as {account: string}[]).map((a) => a.account);
+
+ // Find all asset accounts (used to calculate disposal values)
+ const assetAccounts = (await session.select(
+ `SELECT account FROM account_configurations
+ WHERE kind = 'drcr.asset'`
+ ) as {account: string}[]).map((a) => a.account);
+
+ // Get all transactions involving CGT asset accounts
+ const cgtJoinedTransactions = await session.select(
+ `SELECT joined_transactions.*
+ FROM postings
+ JOIN joined_transactions ON postings.transaction_id = joined_transactions.transaction_id
+ JOIN account_configurations ON postings.account = account_configurations.account
+ WHERE account_configurations.kind = 'austax.cgtasset'`
+ ) as JoinedTransactionPosting[];
+ const cgtTransactions = joinedToTransactions(cgtJoinedTransactions);
+
+ // Process postings to determine final balances
+ const assets: CGTAsset[] = [];
+
+ for (const transaction of cgtTransactions) {
+ for (const posting of transaction.postings) {
+ if (cgtAccounts.indexOf(posting.account) < 0) {
+ continue;
+ }
+ if (posting.commodity === db.metadata.reporting_commodity) {
+ continue;
+ }
+
+ // This posting is to a CGT asset account
+
+ if (posting.quantity >= 0) {
+ // Debit CGT asset - create new CGTAsset
+ assets.push(new CGTAsset(posting.quantity, posting.commodity, posting.account, transaction.dt))
+ } else {
+ // Credit CGT asset
+ // Currently only a full disposal of a CGT asset is implemented
+
+ // Find matching CGT asset
+ const asset = assets.find((a) => a.commodity === posting.commodity && a.account === posting.account);
+
+ if (!asset) {
+ throw new Error('Attempted credit of ' + ppWithCommodity(posting.quantity, posting.commodity) + ' without preceding debit balance');
+ }
+ if (asset.quantity + posting.quantity < 0) {
+ throw new Error('Attempted credit of ' + ppWithCommodity(posting.quantity, posting.commodity) + ' which exceeds debit balance of ' + ppWithCommodity(asset.quantity, asset.commodity));
+ }
+ if (asset.quantity + posting.quantity != 0) {
+ throw new Error('Partial disposal of CGT asset not implemented');
+ }
+
+ asset.disposal_dt = transaction.dt;
+
+ // Calculate disposal value for searching for matching asset postings
+ asset.disposal_value = 0;
+ for (const otherPosting of transaction.postings) {
+ if (otherPosting !== posting && assetAccounts.indexOf(otherPosting.account) >= 0) {
+ asset.disposal_value += asCost(otherPosting.quantity, otherPosting.commodity);
+ }
+ }
+ }
+ }
+ }
+
+ // Get all CGT cost adjustments
+ const cgtAdjustments = await session.select(
+ `SELECT id, quantity, commodity, account, acquisition_dt, dt, description, cost_adjustment
+ FROM austax_cgt_cost_adjustments
+ ORDER BY dt DESC, account, substr(commodity, 1, instr(commodity, ' {')), acquisition_dt, id DESC`
+ ) as CGTAdjustment[];
+
+ // Process CGT adjustments
+ for (const cgtAdjustment of cgtAdjustments) {
+ // Get corresponding asset
+ const asset = assets.find((a) => a.quantity === cgtAdjustment.quantity && a.commodity === cgtAdjustment.commodity && a.account === cgtAdjustment.account && a.acquisition_dt === cgtAdjustment.acquisition_dt);
+
+ if (!asset) {
+ throw new Error('No matching CGT asset for cost adjustment ' + ppWithCommodity(cgtAdjustment.quantity, cgtAdjustment.commodity));
+ }
+
+ asset.cost_adjustments.push(cgtAdjustment);
+ }
+
+ // Sort CGT assets
+ assets.sort((a, b) => b.acquisition_dt.localeCompare(a.acquisition_dt));
+ assets.sort((a, b) => cgtAssetCommodityName(a.commodity).localeCompare(cgtAssetCommodityName(b.commodity)));
+ assets.sort((a, b) => a.account.localeCompare(b.account));
+
+ return assets;
+}
+
+export function cgtAssetCommodityName(commodity: string): string {
+ return commodity.substring(0, commodity.indexOf(' {'));
+}
diff --git a/src/plugins/austax/model.ts b/src/plugins/austax/model.ts
deleted file mode 100644
index 2906734..0000000
--- a/src/plugins/austax/model.ts
+++ /dev/null
@@ -1,28 +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 .
-*/
-
-export interface CGTAdjustment {
- id: number | null,
- quantity: number,
- commodity: string,
- account: string,
- acquisition_dt: string,
- dt: string,
- description: string,
- cost_adjustment: number,
-}