diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9d130d7..13114da 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 49e3241..2ecbb33 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 44aa338..e120918 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ along with this program. If not, see . */ +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!()) diff --git a/src-tauri/src/libdrcr_bridge.rs b/src-tauri/src/libdrcr_bridge.rs new file mode 100644 index 0000000..dc9deb9 --- /dev/null +++ b/src-tauri/src/libdrcr_bridge.rs @@ -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 . +*/ + +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>) -> Result { + 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::().unwrap().to_json(); + + Ok(balance_sheet) + }) + .await + .unwrap() +} diff --git a/src/components/DynamicReportComponent.vue b/src/components/DynamicReportComponent.vue index b71d18d..3dd1cbc 100644 --- a/src/components/DynamicReportComponent.vue +++ b/src/components/DynamicReportComponent.vue @@ -1,6 +1,6 @@ - - - - diff --git a/src/components/DynamicReportEntryComponent.vue b/src/components/DynamicReportEntryComponent.vue new file mode 100644 index 0000000..e8c0804 --- /dev/null +++ b/src/components/DynamicReportEntryComponent.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/reports/BalanceSheetReport.vue b/src/reports/BalanceSheetReport.vue index bfb135e..2ea50a4 100644 --- a/src/reports/BalanceSheetReport.vue +++ b/src/reports/BalanceSheetReport.vue @@ -1,6 +1,6 @@ - - diff --git a/src/reports/base.ts b/src/reports/base.ts index a472750..6644b51 100644 --- a/src/reports/base.ts +++ b/src/reports/base.ts @@ -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, 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 { }