Basic implementation of balance sheet report using libdrcr

This commit is contained in:
RunasSudo 2025-05-26 21:36:21 +10:00
parent 2dd967f5a4
commit 25697b501c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 350 additions and 374 deletions

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
hard_tabs = true

101
src-tauri/Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -514,15 +514,17 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"windows-targets 0.52.6",
"wasm-bindgen",
"windows-link",
]
[[package]]
@ -914,6 +916,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "downcast-rs"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf"
[[package]]
name = "dpi"
version = "0.1.1"
@ -927,7 +935,9 @@ dependencies = [
name = "drcr"
version = "0.1.0"
dependencies = [
"indexmap 2.6.0",
"chrono",
"indexmap 2.9.0",
"libdrcr",
"serde",
"serde_json",
"sqlx",
@ -963,9 +973,21 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.17"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "dyn-eq"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388"
[[package]]
name = "dyn-hash"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88"
[[package]]
name = "either"
@ -1950,9 +1972,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.6.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.1",
@ -2149,9 +2171,25 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libdrcr"
version = "0.1.0"
dependencies = [
"chrono",
"downcast-rs 2.0.1",
"dyn-clone",
"dyn-eq",
"dyn-hash",
"indexmap 2.9.0",
"serde",
"serde_json",
"sqlx",
"tokio",
]
[[package]]
name = "libloading"
@ -3022,7 +3060,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
dependencies = [
"base64 0.22.1",
"indexmap 2.6.0",
"indexmap 2.9.0",
"quick-xml 0.32.0",
"serde",
"time",
@ -3492,9 +3530,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
@ -3512,9 +3550,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@ -3534,9 +3572,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.132"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa 1.0.11",
"memchr",
@ -3586,7 +3624,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.6.0",
"indexmap 2.9.0",
"serde",
"serde_derive",
"serde_json",
@ -3846,7 +3884,7 @@ dependencies = [
"hashbrown 0.14.5",
"hashlink",
"hex",
"indexmap 2.6.0",
"indexmap 2.9.0",
"log",
"memchr",
"once_cell",
@ -4390,7 +4428,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7022ccbcd1799dcf7acb97805f1a5fc7f046b4cf67518296a2e8921bab613a"
dependencies = [
"futures-core",
"indexmap 2.6.0",
"indexmap 2.9.0",
"log",
"serde",
"serde_json",
@ -4638,14 +4676,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.41.1"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@ -4656,9 +4695,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
@ -4728,7 +4767,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.6.0",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@ -4741,7 +4780,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.6.0",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@ -5130,7 +5169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
dependencies = [
"cc",
"downcast-rs",
"downcast-rs 1.2.1",
"rustix",
"scoped-tls",
"smallvec",
@ -5382,6 +5421,12 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.2.0"

View File

@ -18,7 +18,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
chrono = "0.4.41"
indexmap = { version = "2", features = ["serde"] }
libdrcr = { path = "../libdrcr" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["json", "time"] }

View File

@ -16,6 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
mod libdrcr_bridge;
mod sql;
use tauri::{AppHandle, Builder, Manager, State};
@ -82,6 +83,7 @@ pub fn run() {
.plugin(tauri_plugin_store::Builder::new().build())
.invoke_handler(tauri::generate_handler![
get_open_filename, set_open_filename,
libdrcr_bridge::get_balance_sheet,
sql::sql_transaction_begin, sql::sql_transaction_execute, sql::sql_transaction_select, sql::sql_transaction_rollback, sql::sql_transaction_commit
])
.run(tauri::generate_context!())

View File

@ -0,0 +1,92 @@
/*
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/>.
*/
use chrono::NaiveDate;
use libdrcr::db::DbConnection;
use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::dynamic_report::DynamicReport;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::register_lookup_fns;
use libdrcr::reporting::types::{
DateArgs, MultipleDateArgs, ReportingContext, ReportingProductId, ReportingProductKind,
VoidArgs,
};
use tauri::State;
use tokio::sync::Mutex;
use tokio::task::spawn_blocking;
use crate::AppState;
#[tauri::command]
pub(crate) async fn get_balance_sheet(state: State<'_, Mutex<AppState>>) -> Result<String, ()> {
let state = state.lock().await;
let db_filename = state.db_filename.clone().unwrap();
spawn_blocking(move || {
// Connect to database
let db_connection =
DbConnection::connect(format!("sqlite:{}", db_filename.as_str()).as_str());
// Initialise ReportingContext
let mut context = ReportingContext::new(
db_connection,
NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
"$".to_string(),
);
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
// Get balance sheet
let targets = vec![
ReportingProductId {
name: "CalculateIncomeTax",
kind: ReportingProductKind::Transactions,
args: Box::new(VoidArgs {}),
},
ReportingProductId {
name: "BalanceSheet",
kind: ReportingProductKind::Generic,
args: Box::new(MultipleDateArgs {
dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
}],
}),
},
];
let products = generate_report(targets, &context).unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "BalanceSheet",
kind: ReportingProductKind::Generic,
args: Box::new(MultipleDateArgs {
dates: vec![DateArgs {
date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
}],
}),
})
.unwrap();
let balance_sheet = result.downcast_ref::<DynamicReport>().unwrap().to_json();
Ok(balance_sheet)
})
.await
.unwrap()
}

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222024 Lee Yingtong Li (RunasSudo)
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
@ -28,22 +28,20 @@
<thead>
<tr class="border-b border-gray-300">
<th></th>
<th class="py-0.5 pl-1 text-gray-900 font-semibold text-end">{{ db.metadata.reporting_commodity }}&nbsp;</th>
<th v-for="column of report.columns" class="py-0.5 pl-1 text-gray-900 font-semibold text-end">{{ column }}&nbsp;</th>
</tr>
</thead>
<tbody>
<DynamicReportEntry :entry="entry" v-for="entry of report.entries" />
<DynamicReportEntryComponent :entry="entry" v-for="entry of report.entries" />
</tbody>
</table>
</template>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { db } from '../db.ts';
import { DynamicReport } from '../reports/base.ts';
import DynamicReportEntry from './DynamicReportEntry.vue';
import DynamicReportEntryComponent from './DynamicReportEntryComponent.vue';
const { report } = defineProps<{ report: DynamicReport | null }>();
</script>

View File

@ -1,49 +0,0 @@
<!--
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>
<template v-if="entry instanceof Entry">
<!-- NB: Subtotal and Calculated are subclasses of Entry -->
<tr :class="entry.bordered ? 'border-y border-gray-300' : null">
<component :is="entry.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': entry.heading }">
<a :href="entry.link" class="hover:text-blue-700 hover:underline" v-if="entry.link !== null">{{ entry.text }}</a>
<template v-if="entry.link === null">{{ entry.text }}</template>
</component>
<component :is="entry.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': entry.heading }" v-html="ppBracketed(entry.quantity, entry.link ?? undefined)" />
</tr>
</template>
<template v-if="entry instanceof Section">
<tr v-if="entry.title !== null">
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">{{ entry.title }}</th>
<th></th>
</tr>
<DynamicReportEntry :entry="child" v-for="child of entry.entries" />
</template>
<template v-if="entry instanceof Spacer">
<tr><td colspan="2" class="py-0.5">&nbsp;</td></tr>
</template>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { ppBracketed } from '../display.ts';
import { DynamicReportNode, Entry, Section, Spacer } from '../reports/base.ts';
const { entry } = defineProps<{ entry: DynamicReportNode }>();
</script>

View File

@ -0,0 +1,46 @@
<!--
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>
<template v-if="entry.LiteralRow">
<tr :class="entry.LiteralRow.bordered ? 'border-y border-gray-300' : null">
<component :is="entry.LiteralRow.heading ? 'th' : 'td'" class="py-0.5 pr-1 text-gray-900 text-start" :class="{ 'font-semibold': entry.LiteralRow.heading }">
<a :href="entry.LiteralRow.link" class="hover:text-blue-700 hover:underline" v-if="entry.LiteralRow.link !== null">{{ entry.LiteralRow.text }}</a>
<template v-if="entry.LiteralRow.link === null">{{ entry.LiteralRow.text }}</template>
</component>
<component :is="entry.LiteralRow.heading ? 'th' : 'td'" class="py-0.5 pl-1 text-gray-900 text-end" :class="{ 'font-semibold': entry.LiteralRow.heading }" v-html="ppBracketed(entry.LiteralRow.quantity, entry.LiteralRow.link ?? undefined)" />
</tr>
</template>
<template v-if="entry.Section">
<tr v-if="entry.Section.text !== null">
<th class="py-0.5 pr-1 text-gray-900 font-semibold text-start">{{ entry.Section.text }}</th>
<th></th><!-- FIXME: Have correct colspan -->
</tr>
<DynamicReportEntryComponent :entry="child" v-for="child of entry.Section.entries" />
</template>
<template v-if="entry == 'Spacer'">
<tr><td colspan="2" class="py-0.5">&nbsp;</td></tr><!-- FIXME: Have correct colspan -->
</template>
</template>
<script setup lang="ts">
import { ppBracketed } from '../display.ts';
import { DynamicReportEntry } from '../reports/base.ts';
const { entry } = defineProps<{ entry: DynamicReportEntry }>();
</script>

View File

@ -1,6 +1,6 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 20222025 Lee Yingtong Li (RunasSudo)
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
@ -16,53 +16,11 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
export class BalanceSheetReport extends DynamicReport {
constructor() {
super('Balance sheet');
}
async generate(balances: Map<string, number>, incomeStatementReport: IncomeStatementReport) {
this.entries = [
new Section(
'Assets',
[
...await DynamicReport.entriesForKind(balances, 'drcr.asset'),
new Subtotal('Total assets', 'total_assets', true /* visible */, true /* bordered */)
]
),
new Spacer(),
new Section(
'Liabilities',
[
...await DynamicReport.entriesForKind(balances, 'drcr.liability', true),
new Subtotal('Total liabilities', 'total_liabilities', true /* visible */, true /* bordered */)
]
),
new Spacer(),
new Section(
'Equity',
[
...await DynamicReport.entriesForKind(balances, 'drcr.equity', true),
new Entry('Current year surplus (deficit)', (incomeStatementReport.byId('net_surplus') as Computed).quantity, null /* id */, true /* visible */, false /* autoHide */, '/income-statement'),
new Entry('Accumulated surplus (deficit)', -(balances.get('Accumulated surplus (deficit)') ?? 0), null /* id */, true /* visible */, false /* autoHide */, '/transactions/Accumulated surplus (deficit)'),
new Subtotal('Total equity', 'total_equity', true /* visible */, true /* bordered */)
]
)
];
this.calculate();
}
}
</script>
<!-- Report display -->
<template>
<ComparativeDynamicReportComponent :reports="reports" :labels="reportLabels">
<div class="my-2 py-2 flex">
<DynamicReportComponent :report="report">
<!-- <div class="my-2 py-2 flex">
<div class="grow flex gap-x-2 items-baseline">
<span class="whitespace-nowrap">As at</span>
<input type="date" class="bordered-field" v-model.lazy="dt">
@ -87,106 +45,120 @@
<p class="text-sm text-red-700">Total assets do not equal total liabilities and equity. This may occur if not all accounts have been classified in the chart of accounts.</p>
</div>
</div>
</div>
</ComparativeDynamicReportComponent>
</div> -->
</DynamicReportComponent>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import dayjs from 'dayjs';
import { invoke } from '@tauri-apps/api/core';
import { ref } from 'vue';
import { ExclamationCircleIcon } from '@heroicons/vue/20/solid';
import DynamicReport from './base.ts';
import DynamicReportComponent from '../components/DynamicReportComponent.vue';
import { Computed, DynamicReport, Entry, Section, Spacer, Subtotal } from './base.ts';
import { IncomeStatementReport} from './IncomeStatementReport.vue';
import { db } from '../db.ts';
import { ExtendedDatabase } from '../dbutil.ts';
import ComparativeDynamicReportComponent from '../components/ComparativeDynamicReportComponent.vue';
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
const reports = ref([] as BalanceSheetReport[]);
const reportLabels = ref([] as string[]);
const dt = ref(null as string | null);
const comparePeriods = ref(1);
const compareUnit = ref('years');
const report = ref(null as DynamicReport | null);
async function load() {
const session = await db.load();
dt.value = db.metadata.eofy_date;
await updateReport(session);
// Update report when dates etc. changed
// We initialise the watcher here only after dt and dtStart are initialised above
watch([dt, comparePeriods, compareUnit], async () => {
const session = await db.load();
await updateReport(session);
});
report.value = JSON.parse(await invoke('get_balance_sheet', {}));
console.log(report.value);
}
load();
async function updateReport(session: ExtendedDatabase) {
const newReportPromises = [];
const newReportLabels = [];
for (let i = 0; i < comparePeriods.value; i++) {
let thisReportDt, thisReportLabel;
// Get period end date
if (compareUnit.value === 'years') {
thisReportDt = dayjs(dt.value!).subtract(i, 'year');
thisReportLabel = dayjs(dt.value!).subtract(i, 'year').format('YYYY');
} else if (compareUnit.value === 'months') {
if (dayjs(dt.value!).add(1, 'day').isSame(dayjs(dt.value!).set('date', 1).add(1, 'month'))) {
// If dt is the end of a calendar month, then fix each prior dt to be the end of the calendar month
thisReportDt = dayjs(dt.value!).subtract(i, 'month').set('date', 1).add(1, 'month').subtract(1, 'day');
} else {
thisReportDt = dayjs(dt.value!).subtract(i, 'month');
}
thisReportLabel = dayjs(dt.value!).subtract(i, 'month').format('YYYY-MM');
} else {
throw new Error('Unexpected compareUnit');
}
// Get start of financial year date
let sofyDayjs = dayjs(db.metadata.eofy_date).subtract(1, 'year').add(1, 'day');
let thisReportDtStart = thisReportDt.set('date', sofyDayjs.get('date')).set('month', sofyDayjs.get('month'));
if (thisReportDtStart.isAfter(thisReportDt)) {
thisReportDtStart = thisReportDtStart.subtract(1, 'year');
}
console.log([thisReportDt, thisReportDtStart]);
// Generate reports asynchronously
newReportPromises.push((async () => {
const reportingWorkflow = new ReportingWorkflow();
await reportingWorkflow.generate(session, thisReportDt.format('YYYY-MM-DD'), thisReportDtStart.format('YYYY-MM-DD'));
return reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, BalanceSheetReport) as BalanceSheetReport;
})());
if (comparePeriods.value === 1) {
// If only 1 report, the heading is simply "$"
newReportLabels.push(db.metadata.reporting_commodity);
} else {
newReportLabels.push(thisReportLabel);
}
}
reports.value = await Promise.all(newReportPromises);
reportLabels.value = newReportLabels;
}
// import { computed, ref, watch } from 'vue';
// import dayjs from 'dayjs';
const doesBalance = computed(function() {
let doesBalance = true;
for (const report of reports.value) {
const totalAssets = (report.byId('total_assets') as Computed).quantity;
const totalLiabilities = (report.byId('total_liabilities') as Computed).quantity;
const totalEquity = (report.byId('total_equity') as Computed).quantity;
if (totalAssets !== totalLiabilities + totalEquity) {
doesBalance = false;
}
}
return doesBalance;
});
// import { ExclamationCircleIcon } from '@heroicons/vue/20/solid';
// import { Computed, DynamicReport, Entry, Section, Spacer, Subtotal } from './base.ts';
// import { IncomeStatementReport} from './IncomeStatementReport.vue';
// import { db } from '../db.ts';
// import { ExtendedDatabase } from '../dbutil.ts';
// import ComparativeDynamicReportComponent from '../components/ComparativeDynamicReportComponent.vue';
// import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
// const reports = ref([] as BalanceSheetReport[]);
// const reportLabels = ref([] as string[]);
// const dt = ref(null as string | null);
// const comparePeriods = ref(1);
// const compareUnit = ref('years');
// async function load() {
// const session = await db.load();
// dt.value = db.metadata.eofy_date;
// await updateReport(session);
// // Update report when dates etc. changed
// // We initialise the watcher here only after dt and dtStart are initialised above
// watch([dt, comparePeriods, compareUnit], async () => {
// const session = await db.load();
// await updateReport(session);
// });
// }
// load();
// async function updateReport(session: ExtendedDatabase) {
// const newReportPromises = [];
// const newReportLabels = [];
// for (let i = 0; i < comparePeriods.value; i++) {
// let thisReportDt, thisReportLabel;
// // Get period end date
// if (compareUnit.value === 'years') {
// thisReportDt = dayjs(dt.value!).subtract(i, 'year');
// thisReportLabel = dayjs(dt.value!).subtract(i, 'year').format('YYYY');
// } else if (compareUnit.value === 'months') {
// if (dayjs(dt.value!).add(1, 'day').isSame(dayjs(dt.value!).set('date', 1).add(1, 'month'))) {
// // If dt is the end of a calendar month, then fix each prior dt to be the end of the calendar month
// thisReportDt = dayjs(dt.value!).subtract(i, 'month').set('date', 1).add(1, 'month').subtract(1, 'day');
// } else {
// thisReportDt = dayjs(dt.value!).subtract(i, 'month');
// }
// thisReportLabel = dayjs(dt.value!).subtract(i, 'month').format('YYYY-MM');
// } else {
// throw new Error('Unexpected compareUnit');
// }
// // Get start of financial year date
// let sofyDayjs = dayjs(db.metadata.eofy_date).subtract(1, 'year').add(1, 'day');
// let thisReportDtStart = thisReportDt.set('date', sofyDayjs.get('date')).set('month', sofyDayjs.get('month'));
// if (thisReportDtStart.isAfter(thisReportDt)) {
// thisReportDtStart = thisReportDtStart.subtract(1, 'year');
// }
// console.log([thisReportDt, thisReportDtStart]);
// // Generate reports asynchronously
// newReportPromises.push((async () => {
// const reportingWorkflow = new ReportingWorkflow();
// await reportingWorkflow.generate(session, thisReportDt.format('YYYY-MM-DD'), thisReportDtStart.format('YYYY-MM-DD'));
// return reportingWorkflow.getReportAtStage(ReportingStage.FINAL_STAGE, BalanceSheetReport) as BalanceSheetReport;
// })());
// if (comparePeriods.value === 1) {
// // If only 1 report, the heading is simply "$"
// newReportLabels.push(db.metadata.reporting_commodity);
// } else {
// newReportLabels.push(thisReportLabel);
// }
// }
// reports.value = await Promise.all(newReportPromises);
// reportLabels.value = newReportLabels;
// }
// const doesBalance = computed(function() {
// let doesBalance = true;
// for (const report of reports.value) {
// const totalAssets = (report.byId('total_assets') as Computed).quantity;
// const totalLiabilities = (report.byId('total_liabilities') as Computed).quantity;
// const totalEquity = (report.byId('total_equity') as Computed).quantity;
// if (totalAssets !== totalLiabilities + totalEquity) {
// doesBalance = false;
// }
// }
// return doesBalance;
// });
</script>

View File

@ -18,166 +18,33 @@
import { db, getAccountsForKind } from '../db.ts';
export interface DrcrReport {
export interface DynamicReport {
title: string;
columns: string[];
entries: DynamicReportEntry[];
}
export interface DynamicReportNode {
// serde_json serialises an enum like this
export type DynamicReportEntry = {Section: Section} | {LiteralRow: LiteralRow} | 'Spacer';
export interface Section {
text: string;
id: string | null;
calculate(parent: DynamicReport | DynamicReportNode): void;
visible: bool;
auto_hide: bool;
entries: DynamicReportEntry[];
}
export class DynamicReport implements DrcrReport {
constructor(
public title: string,
public entries: DynamicReportNode[] = [],
) {}
byId(id: string): DynamicReportNode | null {
// Get the DynamicReportNode with the given ID
for (const entry of this.entries) {
if (entry.id === id) {
return entry;
}
if (entry instanceof Section) {
const result = entry.byId(id);
if (result) {
return result;
}
}
}
return null;
}
calculate() {
// Compute all subtotals
for (const entry of this.entries) {
entry.calculate(this);
}
}
static async entriesForKind(balances: Map<string, number>, kind: string, negate = false) {
// Get accounts associated with this kind
const accountsForKind = await getAccountsForKind(await db.load(), kind);
// Return one entry for each such account
const entries = [];
for (const account of accountsForKind) {
if (balances.has(account)) {
const quantity = balances.get(account)!;
if (quantity === 0) {
continue;
}
entries.push(new Entry(
account,
negate ? -quantity : quantity,
null /* id */,
true /* visible */,
false /* autoHide */,
'/transactions/' + account
));
}
}
return entries;
}
export interface LiteralRow {
text: string;
quantity: number[];
id: string;
visible: bool;
auto_hide: bool;
link: string | null;
heading: bool;
bordered: bool;
}
export class Entry implements DynamicReportNode {
constructor(
public text: string,
public quantity: number,
public id: string | null = null,
public visible = true,
public autoHide = false,
public link: string | null = null,
public heading = false,
public bordered = false,
) {}
calculate(_parent: DynamicReport | DynamicReportNode) {}
}
export class Computed extends Entry {
constructor(
public text: string,
public calc: Function,
public id: string | null = null,
public visible = true,
public autoHide = false,
public link: string | null = null,
public heading = false,
public bordered = false,
) {
super(text, null!, id, visible, autoHide, link, heading, bordered);
}
calculate(_parent: DynamicReport | DynamicReportNode) {
// Calculate the value of this entry
this.quantity = this.calc();
}
}
export class Section implements DynamicReportNode {
constructor(
public title: string | null,
public entries: DynamicReportNode[] = [],
public id: string | null = null,
public visible = true,
public autoHide = false,
) {}
calculate(_parent: DynamicReport | DynamicReportNode) {
for (const entry of this.entries) {
entry.calculate(this);
}
}
byId(id: string): DynamicReportNode | null {
// Get the DynamicReportNode with the given ID
for (const entry of this.entries) {
if (entry.id === id) {
return entry;
}
if (entry instanceof Section) {
const result = entry.byId(id);
if (result) {
return result;
}
}
}
return null;
}
}
export class Spacer implements DynamicReportNode {
id = null;
calculate(_parent: DynamicReport | DynamicReportNode) {}
}
export class Subtotal extends Entry {
constructor(
public text: string,
public id: string | null = null,
public visible = true,
public bordered = false,
public floor = 0,
) {
super(text, null!, id, visible, false /* autoHide */, null /* link */, true /* heading */, bordered);
}
calculate(parent: DynamicReport | DynamicReportNode) {
// Calculate total amount
if (!(parent instanceof Section)) {
throw new Error('Attempt to calculate Subtotal not in Section');
}
this.quantity = 0;
for (const entry of parent.entries) {
if (entry instanceof Entry && entry !== this) {
this.quantity += entry.quantity;
}
}
}
export interface Spacer {
}