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 @@ + + + + + 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, -}