Basic implementation of balance sheet report using libdrcr
This commit is contained in:
parent
2dd967f5a4
commit
25697b501c
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
hard_tabs = true
|
101
src-tauri/Cargo.lock
generated
101
src-tauri/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"] }
|
||||
|
@ -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!())
|
||||
|
92
src-tauri/src/libdrcr_bridge.rs
Normal file
92
src-tauri/src/libdrcr_bridge.rs
Normal 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()
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 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 }} </th>
|
||||
<th v-for="column of report.columns" class="py-0.5 pl-1 text-gray-900 font-semibold text-end">{{ column }} </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>
|
||||
|
@ -1,49 +0,0 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2024 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"> </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>
|
46
src/components/DynamicReportEntryComponent.vue
Normal file
46
src/components/DynamicReportEntryComponent.vue
Normal 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"> </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>
|
@ -1,6 +1,6 @@
|
||||
<!--
|
||||
DrCr: Web-based double-entry bookkeeping framework
|
||||
Copyright (C) 2022–2025 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>
|
||||
|
@ -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 {
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user