From f15d19011255ea6755742bf8deb6617473572d1b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 00:33:00 +1000 Subject: [PATCH 01/45] Basic dependency resolution code --- .gitignore | 1 + Cargo.lock | 311 ++++++++++++++++++++++++++++++++++ Cargo.toml | 10 ++ rustfmt.toml | 1 + src/lib.rs | 3 + src/main.rs | 43 +++++ src/reporting/builders.rs | 245 +++++++++++++++++++++++++++ src/reporting/calculator.rs | 322 ++++++++++++++++++++++++++++++++++++ src/reporting/mod.rs | 161 ++++++++++++++++++ src/reporting/steps.rs | 198 ++++++++++++++++++++++ src/transaction.rs | 25 +++ src/util.rs | 24 +++ 12 files changed, 1344 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rustfmt.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/reporting/builders.rs create mode 100644 src/reporting/calculator.rs create mode 100644 src/reporting/mod.rs create mode 100644 src/reporting/steps.rs create mode 100644 src/transaction.rs create mode 100644 src/util.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bf1c400 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "downcast-rs" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libdrcr" +version = "0.1.0" +dependencies = [ + "chrono", + "downcast-rs", + "solvent", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "solvent" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c925807 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "libdrcr" +version = "0.1.0" +edition = "2021" + +[dependencies] +chrono = "0.4.41" +downcast-rs = "2.0.1" +#dyn-clone = "1.0.19" +solvent = "0.8.3" 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/lib.rs b/src/lib.rs new file mode 100644 index 0000000..24e0b08 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,3 @@ +pub mod reporting; +pub mod transaction; +pub mod util; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c728461 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,43 @@ +/* + 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::reporting::{ + builders::register_dynamic_builders, + calculator::solve_for, + steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, + ReportingContext, ReportingStep, +}; + +fn main() { + let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); + register_dynamic_builders(&mut context); + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + Box::new(AllTransactionsExceptRetainedEarnings { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + ]; + + println!("{:?}", solve_for(targets, context)); +} diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs new file mode 100644 index 0000000..3987725 --- /dev/null +++ b/src/reporting/builders.rs @@ -0,0 +1,245 @@ +/* + 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 super::{ + calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, + ReportingStepDynamicBuilder, ReportingStepId, +}; + +pub fn register_dynamic_builders(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: BalancesAtToBalancesBetween::can_build, + build: BalancesAtToBalancesBetween::build, + }); + + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesBetween", + can_build: UpdateBalancesBetween::can_build, + build: UpdateBalancesBetween::build, + }); +} + +#[derive(Debug)] +pub struct BalancesAtToBalancesBetween { + step_name: &'static str, + date_start: NaiveDate, + date_end: NaiveDate, +} + +impl BalancesAtToBalancesBetween { + // Implements BalancesAt, BalancesAt -> BalancesBetween + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for BalancesAt, BalancesAt -> BalancesBetween + if kind == ReportingProductKind::BalancesBetween { + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::BalancesAt, + args: vec![args[1].clone()], + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + return true; + } + HasStepOrCanBuild::None => {} + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Vec, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(BalancesAtToBalancesBetween { + step_name: name, + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for BalancesAtToBalancesBetween { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: vec![self.date_start.format("%Y-%m-%d").to_string()], + }, + ); + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: vec![self.date_end.format("%Y-%m-%d").to_string()], + }, + ); + } +} + +#[derive(Debug)] +pub struct UpdateBalancesBetween { + step_name: &'static str, + date_start: NaiveDate, + date_end: NaiveDate, +} + +impl UpdateBalancesBetween { + // Implements (BalancesBetween -> Transactions) -> BalancesBetween + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + _args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesBetween + if kind == ReportingProductKind::BalancesBetween { + // Initially no need to check args + if let Some(step) = steps.iter().find(|s| { + s.id().name == name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) { + // Check for BalancesBetween -> Transactions + let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind + == ReportingProductKind::BalancesBetween + { + return true; + } + } + + // Check lookup or builder - with args + /*match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: args.clone(), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(step) => unreachable!(), + HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) + | HasStepOrCanBuild::None => {} + }*/ + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Vec, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(UpdateBalancesBetween { + step_name: name, + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for UpdateBalancesBetween { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } + + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args.clone(), + }, + ); + } +} diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs new file mode 100644 index 0000000..e8fb151 --- /dev/null +++ b/src/reporting/calculator.rs @@ -0,0 +1,322 @@ +/* + 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 super::{ + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, + ReportingStepDynamicBuilder, ReportingStepId, ReportingStepLookupFn, +}; + +#[derive(Debug)] +pub struct ReportingGraphDependencies { + vec: Vec, +} + +impl ReportingGraphDependencies { + pub fn vec(&self) -> &Vec { + &self.vec + } + + pub fn add_dependency(&mut self, step: ReportingStepId, dependency: ReportingProductId) { + if !self + .vec + .iter() + .any(|d| d.step == step && d.dependency == dependency) + { + self.vec.push(Dependency { step, dependency }); + } + } + + pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { + for kind in target.product_kinds { + match kind { + ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { + self.add_dependency( + target.clone(), + ReportingProductId { + name: dependency.name, + kind: *kind, + args: target.args.clone(), + }, + ); + } + ReportingProductKind::BalancesAt => todo!(), + ReportingProductKind::Generic => todo!(), + } + } + } + + pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { + return self.vec.iter().filter(|d| d.step == *step).collect(); + } +} + +#[derive(Debug)] +pub struct Dependency { + pub step: ReportingStepId, + pub dependency: ReportingProductId, +} + +#[derive(Debug)] +pub enum ReportingCalculationError { + UnknownStep { message: String }, + NoStepForProduct { message: String }, + CircularDependencies, +} + +pub enum HasStepOrCanBuild<'a, 'b> { + HasStep(&'a Box), + CanLookup(ReportingStepLookupFn), + CanBuild(&'b ReportingStepDynamicBuilder), + None, +} + +pub fn has_step_or_can_build<'a, 'b>( + product: &ReportingProductId, + steps: &'a Vec>, + dependencies: &ReportingGraphDependencies, + context: &'b ReportingContext, +) -> HasStepOrCanBuild<'a, 'b> { + if let Some(step) = steps.iter().find(|s| { + s.id().name == product.name + && s.id().args == product.args + && s.id().product_kinds.contains(&product.kind) + }) { + return HasStepOrCanBuild::HasStep(step); + } + + // Try lookup function + if let Some(lookup_key) = context + .step_lookup_fn + .keys() + .find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind)) + { + return HasStepOrCanBuild::CanLookup(*context.step_lookup_fn.get(lookup_key).unwrap()); + } + + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + product.name, + product.kind, + product.args.clone(), + steps, + dependencies, + context, + ) { + return HasStepOrCanBuild::CanBuild(builder); + } + } + + return HasStepOrCanBuild::None; +} + +fn would_be_ready_to_execute( + step: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + previous_steps: &Vec, +) -> bool { + //println!( + // "- would_be_ready_to_execute: {}, {:?}", + // step.id(), + // previous_steps + //); + + // Check whether the step would be ready to execute, if the previous steps have already completed + 'check_each_dependency: for dependency in dependencies.vec.iter() { + if dependency.step == step.id() { + //println!("-- {}", dependency.dependency); + + // Check if the dependency has been produced by a previous step + for previous_step in previous_steps { + if steps[*previous_step].id().name == dependency.dependency.name + && steps[*previous_step].id().args == dependency.dependency.args + && steps[*previous_step] + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + continue 'check_each_dependency; + } + } + + // Dependency is not met + return false; + } + } + true +} + +pub fn solve_for( + targets: Vec>, + context: ReportingContext, +) -> Result>, ReportingCalculationError> { + let mut steps: Vec> = Vec::new(); + let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; + + // Initialise targets + for target in targets { + steps.push(target); + let target = steps.last().unwrap(); + target.as_ref().init_graph(&steps, &mut dependencies); + } + + // Call after_init_graph on targets + for step in steps.iter() { + step.as_ref().after_init_graph(&steps, &mut dependencies); + } + + // Process dependencies + loop { + let mut new_steps = Vec::new(); + + for dependency in dependencies.vec.iter() { + if !steps.iter().any(|s| s.id() == dependency.step) { + // FIXME: Call the lookup function + todo!(); + } + if !steps.iter().any(|s| { + s.id().name == dependency.dependency.name + && s.id().args == dependency.dependency.args + && s.id().product_kinds.contains(&dependency.dependency.kind) + }) { + // Try lookup function + if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { + *name == dependency.dependency.name + && kinds.contains(&dependency.dependency.kind) + }) { + let lookup_fn = context.step_lookup_fn.get(lookup_key).unwrap(); + let new_step = lookup_fn(dependency.dependency.args.clone()); + + // Check new step meets the dependency + if new_step.id().name != dependency.dependency.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + } + if new_step.id().args != dependency.dependency.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + } + + new_steps.push(new_step); + } else { + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + dependency.dependency.name, + dependency.dependency.kind, + dependency.dependency.args.clone(), + &steps, + &dependencies, + &context, + ) { + new_steps.push((builder.build)( + dependency.dependency.name, + dependency.dependency.kind, + dependency.dependency.args.clone(), + &steps, + &dependencies, + &context, + )); + break; + } + } + } + } + } + + if new_steps.len() == 0 { + break; + } + + // Initialise new steps + let mut new_step_indexes = Vec::new(); + for new_step in new_steps { + new_step_indexes.push(steps.len()); + steps.push(new_step); + let new_step = steps.last().unwrap(); + new_step.as_ref().init_graph(&steps, &mut dependencies); + } + + // Call after_init_graph on new steps + for new_step_index in new_step_indexes { + steps[new_step_index].after_init_graph(&steps, &mut dependencies); + } + } + + // Check all dependencies satisfied + for dependency in dependencies.vec.iter() { + if !steps.iter().any(|s| s.id() == dependency.step) { + return Err(ReportingCalculationError::UnknownStep { + message: format!( + "No implementation for step {} which {} is a dependency of", + dependency.step, dependency.dependency + ), + }); + } + if !steps.iter().any(|s| { + s.id().name == dependency.dependency.name + && s.id().args == dependency.dependency.args + && s.id().product_kinds.contains(&dependency.dependency.kind) + }) { + return Err(ReportingCalculationError::NoStepForProduct { + message: format!( + "No step builds product {} wanted by {}", + dependency.dependency, dependency.step + ), + }); + } + } + + // Sort + let mut sorted_step_indexes = Vec::new(); + let mut steps_remaining = steps.iter().enumerate().collect::>(); + + 'loop_until_all_sorted: while !steps_remaining.is_empty() { + for (cur_index, (orig_index, step)) in steps_remaining.iter().enumerate() { + if would_be_ready_to_execute(step, &steps, &dependencies, &sorted_step_indexes) { + sorted_step_indexes.push(*orig_index); + steps_remaining.remove(cur_index); + continue 'loop_until_all_sorted; + } + } + + // No steps to execute - must be circular dependency + return Err(ReportingCalculationError::CircularDependencies); + } + + let mut sort_mapping = vec![0_usize; sorted_step_indexes.len()]; + for i in 0..sorted_step_indexes.len() { + sort_mapping[sorted_step_indexes[i]] = i; + } + + // TODO: This can be done in place + let mut sorted_steps = steps.into_iter().zip(sort_mapping).collect::>(); + sorted_steps.sort_unstable_by_key(|(_s, order)| *order); + let sorted_steps = sorted_steps + .into_iter() + .map(|(s, _idx)| s) + .collect::>(); + + Ok(sorted_steps) +} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs new file mode 100644 index 0000000..8bd4012 --- /dev/null +++ b/src/reporting/mod.rs @@ -0,0 +1,161 @@ +/* + 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 std::fmt::Debug; +use std::{collections::HashMap, fmt::Display}; + +use calculator::{ReportingGraphDependencies}; +use chrono::NaiveDate; +use downcast_rs::Downcast; + +pub mod builders; +pub mod calculator; +pub mod steps; + +pub struct ReportingContext { + _eofy_date: NaiveDate, + step_lookup_fn: HashMap<(&'static str, &'static [ReportingProductKind]), ReportingStepLookupFn>, + step_dynamic_builders: Vec, +} + +impl ReportingContext { + pub fn new(eofy_date: NaiveDate) -> Self { + Self { + _eofy_date: eofy_date, + step_lookup_fn: HashMap::new(), + step_dynamic_builders: Vec::new(), + } + } + + fn register_lookup_fn( + &mut self, + name: &'static str, + product_kinds: &'static [ReportingProductKind], + builder: ReportingStepLookupFn, + ) { + self.step_lookup_fn.insert((name, product_kinds), builder); + } + + fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { + if !self + .step_dynamic_builders + .iter() + .any(|b| b.name == builder.name) + { + self.step_dynamic_builders.push(builder); + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub struct ReportingProductId { + name: &'static str, + kind: ReportingProductKind, + args: Vec, +} + +impl Display for ReportingProductId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}.{:?}{:?}", self.name, self.kind, self.args)) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ReportingProductKind { + Transactions, + BalancesAt, + BalancesBetween, + Generic, +} + +//enum ReportingProduct { +// Transactions(Transactions), +// BalancesAt(BalancesAt), +// BalancesBetween(BalancesBetween), +// Generic(Box), +//} + +//struct Transactions {} +//struct BalancesAt {} +//struct BalancesBetween {} + +//trait GenericReportingProduct {} + +//type ReportingProducts = HashMap; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReportingStepId { + pub name: &'static str, + pub product_kinds: &'static [ReportingProductKind], + pub args: Vec, +} + +impl Display for ReportingStepId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{:?}{:?}", + self.name, self.product_kinds, self.args + )) + } +} + +pub trait ReportingStep: Debug + Downcast { + // Info + fn id(&self) -> ReportingStepId; + + // Methods + fn init_graph( + &self, + _steps: &Vec>, + _dependencies: &mut ReportingGraphDependencies, + ) { + } + fn after_init_graph( + &self, + _steps: &Vec>, + _dependencies: &mut ReportingGraphDependencies, + ) { + } + //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { + // todo!(); + //} +} + +downcast_rs::impl_downcast!(ReportingStep); + +pub type ReportingStepLookupFn = fn(args: Vec) -> Box; + +pub struct ReportingStepDynamicBuilder { + name: &'static str, + can_build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool, + build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Vec, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> Box, +} diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs new file mode 100644 index 0000000..575b3e7 --- /dev/null +++ b/src/reporting/steps.rs @@ -0,0 +1,198 @@ +/* + 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 crate::util::sofy_from_eofy; + +use super::{ + calculator::ReportingGraphDependencies, ReportingContext, ReportingProductId, + ReportingProductKind, ReportingStep, ReportingStepId, +}; + +pub fn register_lookup_fns(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + AllTransactionsExceptRetainedEarnings::from_args, + ); + + context.register_lookup_fn( + "CalculateIncomeTax", + &[ReportingProductKind::Transactions], + CalculateIncomeTax::from_args, + ); + + context.register_lookup_fn( + "CombineOrdinaryTransactions", + &[ReportingProductKind::BalancesAt], + CombineOrdinaryTransactions::from_args, + ); + + context.register_lookup_fn( + "DBBalances", + &[ReportingProductKind::BalancesAt], + DBBalances::from_args, + ); +} + +#[derive(Debug)] +pub struct AllTransactionsExceptRetainedEarnings { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl AllTransactionsExceptRetainedEarnings { + fn from_args(args: Vec) -> Box { + Box::new(AllTransactionsExceptRetainedEarnings { + date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for AllTransactionsExceptRetainedEarnings { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsExceptRetainedEarnings", + product_kinds: &[ReportingProductKind::BalancesBetween], + args: vec![ + self.date_start.format("%Y-%m-%d").to_string(), + self.date_end.format("%Y-%m-%d").to_string(), + ], + } + } +} + +#[derive(Debug)] +pub struct CalculateIncomeTax { + pub date_eofy: NaiveDate, +} + +impl CalculateIncomeTax { + fn from_args(args: Vec) -> Box { + Box::new(CalculateIncomeTax { + date_eofy: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for CalculateIncomeTax { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CalculateIncomeTax", + product_kinds: &[ReportingProductKind::Transactions], + args: vec![self.date_eofy.format("%Y-%m-%d").to_string()], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: vec![ + sofy_from_eofy(self.date_eofy) + .format("%Y-%m-%d") + .to_string(), + self.date_eofy.format("%Y-%m-%d").to_string(), + ], + }, + ); + } + + fn after_init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + for other in steps { + if let Some(other) = other.downcast_ref::() { + if other.date_start <= self.date_eofy && other.date_end >= self.date_eofy { + dependencies.add_target_dependency(other.id(), self.id()); + } + } + } + } +} + +#[derive(Debug)] +pub struct CombineOrdinaryTransactions { + pub date: NaiveDate, +} + +impl CombineOrdinaryTransactions { + fn from_args(args: Vec) -> Box { + Box::new(CombineOrdinaryTransactions { + date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for CombineOrdinaryTransactions { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CombineOrdinaryTransactions", + product_kinds: &[ReportingProductKind::BalancesAt], + args: vec![self.date.format("%Y-%m-%d").to_string()], + } + } + + fn init_graph( + &self, + _steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: "DBBalances", + kind: ReportingProductKind::BalancesAt, + args: vec![self.date.format("%Y-%m-%d").to_string()], + }, + ); + } +} + +#[derive(Debug)] +pub struct DBBalances { + pub date: NaiveDate, +} + +impl DBBalances { + fn from_args(args: Vec) -> Box { + Box::new(DBBalances { + date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + }) + } +} + +impl ReportingStep for DBBalances { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "DBBalances", + product_kinds: &[ReportingProductKind::BalancesAt], + args: vec![self.date.format("%Y-%m-%d").to_string()], + } + } +} diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000..a17ebbc --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,25 @@ +/* + 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::NaiveDateTime; + +pub struct Transaction { + pub id: Option, + pub dt: NaiveDateTime, + pub description: String, +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..c8129c4 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,24 @@ +/* + 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::{Datelike, NaiveDate}; + +pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { + // Return the start date of the financial year, given the end date of the financial year + return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); +} From a7e9c74dd1f71e4c6ec9f59f8a3d42c4d6f211b0 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 16:39:18 +1000 Subject: [PATCH 02/45] Refactor representation of ReportingStep args --- Cargo.lock | 14 ++++++++ Cargo.toml | 3 +- src/main.rs | 21 +++++++++--- src/reporting/builders.rs | 47 ++++++++++++--------------- src/reporting/calculator.rs | 4 +-- src/reporting/mod.rs | 64 ++++++++++++++++++++++++++++++++----- src/reporting/steps.rs | 63 +++++++++++++++++------------------- 7 files changed, 140 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf1c400..fee6985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,6 +70,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea8a8b81cacc08888170eef4d13b775126db426d0b348bee9d18c2c1eaf123cf" +[[package]] +name = "dyn-clone" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" + +[[package]] +name = "dyn-eq" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d035d21af5cde1a6f5c7b444a5bf963520a9f142e5d06931178433d7d5388" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -116,6 +128,8 @@ version = "0.1.0" dependencies = [ "chrono", "downcast-rs", + "dyn-clone", + "dyn-eq", "solvent", ] diff --git a/Cargo.toml b/Cargo.toml index c925807..b4bac49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" [dependencies] chrono = "0.4.41" downcast-rs = "2.0.1" -#dyn-clone = "1.0.19" +dyn-clone = "1.0.19" +dyn-eq = "0.1.3" solvent = "0.8.3" diff --git a/src/main.rs b/src/main.rs index c728461..5870164 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use libdrcr::reporting::{ builders::register_dynamic_builders, calculator::solve_for, steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, - ReportingContext, ReportingStep, + DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingStep, }; fn main() { @@ -31,13 +31,24 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + args: DateEofyArgs { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, }), Box::new(AllTransactionsExceptRetainedEarnings { - date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), - date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + args: DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, }), ]; - println!("{:?}", solve_for(targets, context)); + match solve_for(targets, context) { + Ok(steps) => { + for step in steps { + println!("- {}", step.id()); + } + } + Err(err) => panic!("Error: {:?}", err), + } } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 3987725..6d91e61 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,12 +16,10 @@ along with this program. If not, see . */ -use chrono::NaiveDate; - use super::{ calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepId, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, + ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; pub fn register_dynamic_builders(context: &mut ReportingContext) { @@ -41,8 +39,7 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { #[derive(Debug)] pub struct BalancesAtToBalancesBetween { step_name: &'static str, - date_start: NaiveDate, - date_end: NaiveDate, + args: DateStartDateEndArgs, } impl BalancesAtToBalancesBetween { @@ -51,7 +48,7 @@ impl BalancesAtToBalancesBetween { fn can_build( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, @@ -62,7 +59,7 @@ impl BalancesAtToBalancesBetween { &ReportingProductId { name, kind: ReportingProductKind::BalancesAt, - args: vec![args[1].clone()], + args: args.clone(), }, steps, dependencies, @@ -82,15 +79,14 @@ impl BalancesAtToBalancesBetween { fn build( name: &'static str, _kind: ReportingProductKind, - args: Vec, + args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(BalancesAtToBalancesBetween { step_name: name, - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -100,10 +96,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } @@ -112,12 +105,15 @@ impl ReportingStep for BalancesAtToBalancesBetween { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // BalancesAtToBalancesBetween depends on BalancesAt at both time points dependencies.add_dependency( self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, - args: vec![self.date_start.format("%Y-%m-%d").to_string()], + args: Box::new(DateArgs { + date: self.args.date_start.clone(), + }), }, ); dependencies.add_dependency( @@ -125,7 +121,9 @@ impl ReportingStep for BalancesAtToBalancesBetween { ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, - args: vec![self.date_end.format("%Y-%m-%d").to_string()], + args: Box::new(DateArgs { + date: self.args.date_end.clone(), + }), }, ); } @@ -134,8 +132,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, - date_start: NaiveDate, - date_end: NaiveDate, + args: DateStartDateEndArgs, } impl UpdateBalancesBetween { @@ -144,7 +141,7 @@ impl UpdateBalancesBetween { fn can_build( name: &'static str, kind: ReportingProductKind, - _args: Vec, + _args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, _context: &ReportingContext, @@ -191,15 +188,14 @@ impl UpdateBalancesBetween { fn build( name: &'static str, _kind: ReportingProductKind, - args: Vec, + args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(UpdateBalancesBetween { step_name: name, - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -209,10 +205,7 @@ impl ReportingStep for UpdateBalancesBetween { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index e8fb151..07a1ddc 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -113,7 +113,7 @@ pub fn has_step_or_can_build<'a, 'b>( if (builder.can_build)( product.name, product.kind, - product.args.clone(), + &product.args, steps, dependencies, context, @@ -225,7 +225,7 @@ pub fn solve_for( if (builder.can_build)( dependency.dependency.name, dependency.dependency.kind, - dependency.dependency.args.clone(), + &dependency.dependency.args, &steps, &dependencies, &context, diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 8bd4012..5e27a52 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -19,9 +19,11 @@ use std::fmt::Debug; use std::{collections::HashMap, fmt::Display}; -use calculator::{ReportingGraphDependencies}; +use calculator::ReportingGraphDependencies; use chrono::NaiveDate; use downcast_rs::Downcast; +use dyn_clone::DynClone; +use dyn_eq::DynEq; pub mod builders; pub mod calculator; @@ -66,12 +68,12 @@ impl ReportingContext { pub struct ReportingProductId { name: &'static str, kind: ReportingProductKind, - args: Vec, + args: Box, } impl Display for ReportingProductId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}.{:?}{:?}", self.name, self.kind, self.args)) + f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) } } @@ -102,13 +104,13 @@ pub enum ReportingProductKind { pub struct ReportingStepId { pub name: &'static str, pub product_kinds: &'static [ReportingProductKind], - pub args: Vec, + pub args: Box, } impl Display for ReportingStepId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( - "{}{:?}{:?}", + "{}{:?}({})", self.name, self.product_kinds, self.args )) } @@ -138,14 +140,60 @@ pub trait ReportingStep: Debug + Downcast { downcast_rs::impl_downcast!(ReportingStep); -pub type ReportingStepLookupFn = fn(args: Vec) -> Box; +pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq {} + +downcast_rs::impl_downcast!(ReportingStepArgs); +dyn_clone::clone_trait_object!(ReportingStepArgs); +dyn_eq::eq_trait_object!(ReportingStepArgs); + +pub type ReportingStepLookupFn = fn(args: Box) -> Box; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateArgs { + pub date: NaiveDate, +} + +impl ReportingStepArgs for DateArgs {} + +impl Display for DateArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateEofyArgs { + pub date_eofy: NaiveDate, +} + +impl ReportingStepArgs for DateEofyArgs {} + +impl Display for DateEofyArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date_eofy)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DateStartDateEndArgs { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl ReportingStepArgs for DateStartDateEndArgs {} + +impl Display for DateStartDateEndArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) + } +} pub struct ReportingStepDynamicBuilder { name: &'static str, can_build: fn( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, @@ -153,7 +201,7 @@ pub struct ReportingStepDynamicBuilder { build: fn( name: &'static str, kind: ReportingProductKind, - args: Vec, + args: Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 575b3e7..42a90b8 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,13 +16,12 @@ along with this program. If not, see . */ -use chrono::NaiveDate; - use crate::util::sofy_from_eofy; use super::{ - calculator::ReportingGraphDependencies, ReportingContext, ReportingProductId, - ReportingProductKind, ReportingStep, ReportingStepId, + calculator::ReportingGraphDependencies, DateArgs, DateEofyArgs, DateStartDateEndArgs, + ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, + ReportingStepId, }; pub fn register_lookup_fns(context: &mut ReportingContext) { @@ -53,15 +52,13 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub date_start: NaiveDate, - pub date_end: NaiveDate, + pub args: DateStartDateEndArgs, } impl AllTransactionsExceptRetainedEarnings { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { - date_start: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), - date_end: NaiveDate::parse_from_str(&args[1], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -71,23 +68,20 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { ReportingStepId { name: "AllTransactionsExceptRetainedEarnings", product_kinds: &[ReportingProductKind::BalancesBetween], - args: vec![ - self.date_start.format("%Y-%m-%d").to_string(), - self.date_end.format("%Y-%m-%d").to_string(), - ], + args: Box::new(self.args.clone()), } } } #[derive(Debug)] pub struct CalculateIncomeTax { - pub date_eofy: NaiveDate, + pub args: DateEofyArgs, } impl CalculateIncomeTax { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { - date_eofy: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -97,7 +91,7 @@ impl ReportingStep for CalculateIncomeTax { ReportingStepId { name: "CalculateIncomeTax", product_kinds: &[ReportingProductKind::Transactions], - args: vec![self.date_eofy.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } @@ -106,17 +100,16 @@ impl ReportingStep for CalculateIncomeTax { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // CalculateIncomeTax depends on CombineOrdinaryTransactions dependencies.add_dependency( self.id(), ReportingProductId { name: "CombineOrdinaryTransactions", kind: ReportingProductKind::BalancesBetween, - args: vec![ - sofy_from_eofy(self.date_eofy) - .format("%Y-%m-%d") - .to_string(), - self.date_eofy.format("%Y-%m-%d").to_string(), - ], + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(self.args.date_eofy), + date_end: self.args.date_eofy.clone(), + }), }, ); } @@ -128,7 +121,10 @@ impl ReportingStep for CalculateIncomeTax { ) { for other in steps { if let Some(other) = other.downcast_ref::() { - if other.date_start <= self.date_eofy && other.date_end >= self.date_eofy { + if other.args.date_start <= self.args.date_eofy + && other.args.date_end >= self.args.date_eofy + { + // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax dependencies.add_target_dependency(other.id(), self.id()); } } @@ -138,13 +134,13 @@ impl ReportingStep for CalculateIncomeTax { #[derive(Debug)] pub struct CombineOrdinaryTransactions { - pub date: NaiveDate, + pub args: DateArgs, } impl CombineOrdinaryTransactions { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { - date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -154,7 +150,7 @@ impl ReportingStep for CombineOrdinaryTransactions { ReportingStepId { name: "CombineOrdinaryTransactions", product_kinds: &[ReportingProductKind::BalancesAt], - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } @@ -163,12 +159,13 @@ impl ReportingStep for CombineOrdinaryTransactions { _steps: &Vec>, dependencies: &mut ReportingGraphDependencies, ) { + // CombineOrdinaryTransactions depends on DBBalances dependencies.add_dependency( self.id(), ReportingProductId { name: "DBBalances", kind: ReportingProductKind::BalancesAt, - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), }, ); } @@ -176,13 +173,13 @@ impl ReportingStep for CombineOrdinaryTransactions { #[derive(Debug)] pub struct DBBalances { - pub date: NaiveDate, + pub args: DateArgs, } impl DBBalances { - fn from_args(args: Vec) -> Box { + fn from_args(args: Box) -> Box { Box::new(DBBalances { - date: NaiveDate::parse_from_str(&args[0], "%Y-%m-%d").unwrap(), + args: *args.downcast().unwrap(), }) } } @@ -192,7 +189,7 @@ impl ReportingStep for DBBalances { ReportingStepId { name: "DBBalances", product_kinds: &[ReportingProductKind::BalancesAt], - args: vec![self.date.format("%Y-%m-%d").to_string()], + args: Box::new(self.args.clone()), } } } From 4add6292a1b2260ad5573c95b4355a7bb9acdca4 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:11:20 +1000 Subject: [PATCH 03/45] Implement GenerateBalances dynamic builder --- src/reporting/builders.rs | 108 ++++++++++++++++++++++++++++++++---- src/reporting/calculator.rs | 17 ++++-- src/reporting/mod.rs | 20 +++++-- src/reporting/steps.rs | 98 +++++++++++++++++++++++--------- 4 files changed, 198 insertions(+), 45 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 6d91e61..af5e455 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -29,6 +29,12 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { build: BalancesAtToBalancesBetween::build, }); + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "GenerateBalances", + can_build: GenerateBalances::can_build, + build: GenerateBalances::build, + }); + context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", can_build: UpdateBalancesBetween::can_build, @@ -55,11 +61,15 @@ impl BalancesAtToBalancesBetween { ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { + let args = args.downcast_ref::().unwrap(); + match has_step_or_can_build( &ReportingProductId { name, kind: ReportingProductKind::BalancesAt, - args: args.clone(), + args: Box::new(DateArgs { + date: args.date_start.clone(), + }), }, steps, dependencies, @@ -100,14 +110,9 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { + fn requires(&self) -> Vec { // BalancesAtToBalancesBetween depends on BalancesAt at both time points - dependencies.add_dependency( - self.id(), + vec![ ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -115,9 +120,6 @@ impl ReportingStep for BalancesAtToBalancesBetween { date: self.args.date_start.clone(), }), }, - ); - dependencies.add_dependency( - self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -125,7 +127,89 @@ impl ReportingStep for BalancesAtToBalancesBetween { date: self.args.date_end.clone(), }), }, - ); + ] + } +} + +#[derive(Debug)] +pub struct GenerateBalances { + step_name: &'static str, + args: DateArgs, +} + +impl GenerateBalances { + // Implements (() -> Transactions) -> BalancesAt + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesAt + if kind == ReportingProductKind::BalancesAt { + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: args.clone(), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(step) => { + // Check for () -> Transactions + if dependencies.dependencies_for_step(&step.id()).len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanLookup(lookup_fn) => { + // Check for () -> Transactions + let step = lookup_fn(args.clone()); + if step.requires().len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Box, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(GenerateBalances { + step_name: name, + args: *args.downcast().unwrap(), + }) + } +} + +impl ReportingStep for GenerateBalances { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self) -> Vec { + // GenerateBalances depends on Transactions + vec![ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }] } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 07a1ddc..e5c2771 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -18,7 +18,7 @@ use super::{ ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepId, ReportingStepLookupFn, + ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, }; #[derive(Debug)] @@ -80,7 +80,7 @@ pub enum ReportingCalculationError { pub enum HasStepOrCanBuild<'a, 'b> { HasStep(&'a Box), - CanLookup(ReportingStepLookupFn), + CanLookup(ReportingStepFromArgsFn), CanBuild(&'b ReportingStepDynamicBuilder), None, } @@ -105,7 +105,10 @@ pub fn has_step_or_can_build<'a, 'b>( .keys() .find(|(name, kinds)| *name == product.name && kinds.contains(&product.kind)) { - return HasStepOrCanBuild::CanLookup(*context.step_lookup_fn.get(lookup_key).unwrap()); + let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); + if takes_args_fn(&product.args) { + return HasStepOrCanBuild::CanLookup(*from_args_fn); + } } // No explicit step for product - try builders @@ -173,6 +176,9 @@ pub fn solve_for( for target in targets { steps.push(target); let target = steps.last().unwrap(); + for dependency in target.requires() { + dependencies.add_dependency(target.id(), dependency); + } target.as_ref().init_graph(&steps, &mut dependencies); } @@ -200,7 +206,7 @@ pub fn solve_for( *name == dependency.dependency.name && kinds.contains(&dependency.dependency.kind) }) { - let lookup_fn = context.step_lookup_fn.get(lookup_key).unwrap(); + let (_, lookup_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); let new_step = lookup_fn(dependency.dependency.args.clone()); // Check new step meets the dependency @@ -255,6 +261,9 @@ pub fn solve_for( new_step_indexes.push(steps.len()); steps.push(new_step); let new_step = steps.last().unwrap(); + for dependency in new_step.requires() { + dependencies.add_dependency(new_step.id(), dependency); + } new_step.as_ref().init_graph(&steps, &mut dependencies); } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 5e27a52..dd0fe23 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -31,7 +31,10 @@ pub mod steps; pub struct ReportingContext { _eofy_date: NaiveDate, - step_lookup_fn: HashMap<(&'static str, &'static [ReportingProductKind]), ReportingStepLookupFn>, + step_lookup_fn: HashMap< + (&'static str, &'static [ReportingProductKind]), + (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), + >, step_dynamic_builders: Vec, } @@ -48,9 +51,11 @@ impl ReportingContext { &mut self, name: &'static str, product_kinds: &'static [ReportingProductKind], - builder: ReportingStepLookupFn, + takes_args_fn: ReportingStepTakesArgsFn, + from_args_fn: ReportingStepFromArgsFn, ) { - self.step_lookup_fn.insert((name, product_kinds), builder); + self.step_lookup_fn + .insert((name, product_kinds), (takes_args_fn, from_args_fn)); } fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { @@ -121,18 +126,24 @@ pub trait ReportingStep: Debug + Downcast { fn id(&self) -> ReportingStepId; // Methods + fn requires(&self) -> Vec { + vec![] + } + fn init_graph( &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, ) { } + fn after_init_graph( &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, ) { } + //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { // todo!(); //} @@ -146,7 +157,8 @@ downcast_rs::impl_downcast!(ReportingStepArgs); dyn_clone::clone_trait_object!(ReportingStepArgs); dyn_eq::eq_trait_object!(ReportingStepArgs); -pub type ReportingStepLookupFn = fn(args: Box) -> Box; +pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; +pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateArgs { diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 42a90b8..3c94461 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -28,26 +28,37 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( "AllTransactionsExceptRetainedEarnings", &[ReportingProductKind::BalancesBetween], + AllTransactionsExceptRetainedEarnings::takes_args, AllTransactionsExceptRetainedEarnings::from_args, ); context.register_lookup_fn( "CalculateIncomeTax", &[ReportingProductKind::Transactions], + CalculateIncomeTax::takes_args, CalculateIncomeTax::from_args, ); context.register_lookup_fn( "CombineOrdinaryTransactions", &[ReportingProductKind::BalancesAt], + CombineOrdinaryTransactions::takes_args, CombineOrdinaryTransactions::from_args, ); context.register_lookup_fn( "DBBalances", &[ReportingProductKind::BalancesAt], + DBBalances::takes_args, DBBalances::from_args, ); + + context.register_lookup_fn( + "PostUnreconciledStatementLines", + &[ReportingProductKind::Transactions], + PostUnreconciledStatementLines::takes_args, + PostUnreconciledStatementLines::from_args, + ); } #[derive(Debug)] @@ -56,6 +67,10 @@ pub struct AllTransactionsExceptRetainedEarnings { } impl AllTransactionsExceptRetainedEarnings { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { args: *args.downcast().unwrap(), @@ -79,6 +94,10 @@ pub struct CalculateIncomeTax { } impl CalculateIncomeTax { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { args: *args.downcast().unwrap(), @@ -95,23 +114,16 @@ impl ReportingStep for CalculateIncomeTax { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { + fn requires(&self) -> Vec { // CalculateIncomeTax depends on CombineOrdinaryTransactions - dependencies.add_dependency( - self.id(), - ReportingProductId { - name: "CombineOrdinaryTransactions", - kind: ReportingProductKind::BalancesBetween, - args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(self.args.date_eofy), - date_end: self.args.date_eofy.clone(), - }), - }, - ); + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(self.args.date_eofy), + date_end: self.args.date_eofy.clone(), + }), + }] } fn after_init_graph( @@ -138,6 +150,10 @@ pub struct CombineOrdinaryTransactions { } impl CombineOrdinaryTransactions { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { args: *args.downcast().unwrap(), @@ -154,20 +170,21 @@ impl ReportingStep for CombineOrdinaryTransactions { } } - fn init_graph( - &self, - _steps: &Vec>, - dependencies: &mut ReportingGraphDependencies, - ) { - // CombineOrdinaryTransactions depends on DBBalances - dependencies.add_dependency( - self.id(), + fn requires(&self) -> Vec { + vec![ + // CombineOrdinaryTransactions depends on DBBalances ReportingProductId { name: "DBBalances", kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - ); + // CombineOrdinaryTransactions depends on PostUnreconciledStatementLines + ReportingProductId { + name: "PostUnreconciledStatementLines", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + ] } } @@ -177,6 +194,10 @@ pub struct DBBalances { } impl DBBalances { + fn takes_args(args: &Box) -> bool { + args.is::() + } + fn from_args(args: Box) -> Box { Box::new(DBBalances { args: *args.downcast().unwrap(), @@ -193,3 +214,30 @@ impl ReportingStep for DBBalances { } } } + +#[derive(Debug)] +pub struct PostUnreconciledStatementLines { + pub args: DateArgs, +} + +impl PostUnreconciledStatementLines { + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(PostUnreconciledStatementLines { + args: *args.downcast().unwrap(), + }) + } +} + +impl ReportingStep for PostUnreconciledStatementLines { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "PostUnreconciledStatementLines", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } +} From b28c75c00f49462b262b1e0ae94b04b1fa5a5389 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:16:25 +1000 Subject: [PATCH 04/45] Implement Display for ReportingStep --- src/main.rs | 2 +- src/reporting/builders.rs | 20 ++++++++++++++++++++ src/reporting/mod.rs | 2 +- src/reporting/steps.rs | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5870164..30bd029 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ fn main() { match solve_for(targets, context) { Ok(steps) => { for step in steps { - println!("- {}", step.id()); + println!("- {}", step); } } Err(err) => panic!("Error: {:?}", err), diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index af5e455..7905298 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::fmt::Display; + use super::{ calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, @@ -101,6 +103,12 @@ impl BalancesAtToBalancesBetween { } } +impl Display for BalancesAtToBalancesBetween { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{BalancesAtToBalancesBetween}}", self.id())) + } +} + impl ReportingStep for BalancesAtToBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -194,6 +202,12 @@ impl GenerateBalances { } } +impl Display for GenerateBalances { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{GenerateBalances}}", self.id())) + } +} + impl ReportingStep for GenerateBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -284,6 +298,12 @@ impl UpdateBalancesBetween { } } +impl Display for UpdateBalancesBetween { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{UpdateBalancesBetween}}", self.id())) + } +} + impl ReportingStep for UpdateBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index dd0fe23..b0155bd 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -121,7 +121,7 @@ impl Display for ReportingStepId { } } -pub trait ReportingStep: Debug + Downcast { +pub trait ReportingStep: Debug + Display + Downcast { // Info fn id(&self) -> ReportingStepId; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 3c94461..c3f8f19 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::fmt::Display; + use crate::util::sofy_from_eofy; use super::{ @@ -78,6 +80,12 @@ impl AllTransactionsExceptRetainedEarnings { } } +impl Display for AllTransactionsExceptRetainedEarnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for AllTransactionsExceptRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -105,6 +113,12 @@ impl CalculateIncomeTax { } } +impl Display for CalculateIncomeTax { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for CalculateIncomeTax { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -161,6 +175,12 @@ impl CombineOrdinaryTransactions { } } +impl Display for CombineOrdinaryTransactions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for CombineOrdinaryTransactions { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -205,6 +225,12 @@ impl DBBalances { } } +impl Display for DBBalances { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for DBBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -232,6 +258,12 @@ impl PostUnreconciledStatementLines { } } +impl Display for PostUnreconciledStatementLines { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + impl ReportingStep for PostUnreconciledStatementLines { fn id(&self) -> ReportingStepId { ReportingStepId { From d78d6ed1fea64f61d66025045e20ccbc92652335 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 17:58:42 +1000 Subject: [PATCH 05/45] Implement UpdateBalancesAt --- src/main.rs | 42 +++++++++- src/reporting/builders.rs | 155 ++++++++++++++++++++++++++++++++++-- src/reporting/calculator.rs | 84 +++++++++---------- src/reporting/steps.rs | 149 +++++++++++++++++++++++++++++----- 4 files changed, 361 insertions(+), 69 deletions(-) diff --git a/src/main.rs b/src/main.rs index 30bd029..02ea9f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::{ builders::register_dynamic_builders, calculator::solve_for, - steps::{register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax}, - DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingStep, + steps::{ + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + }, + DateArgs, DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, + ReportingStep, }; fn main() { @@ -36,13 +40,43 @@ fn main() { }, }), Box::new(AllTransactionsExceptRetainedEarnings { - args: DateStartDateEndArgs { + product_kinds: &[ReportingProductKind::BalancesBetween], + args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, + }), }), ]; + println!("For income statement:"); + match solve_for(targets, context) { + Ok(steps) => { + for step in steps { + println!("- {}", step); + } + } + Err(err) => panic!("Error: {:?}", err), + } + + let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); + register_dynamic_builders(&mut context); + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax { + args: DateEofyArgs { + date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }), + ]; + + println!("For balance sheet:"); match solve_for(targets, context) { Ok(steps) => { for step in steps { diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 7905298..369df9c 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -25,12 +25,6 @@ use super::{ }; pub fn register_dynamic_builders(context: &mut ReportingContext) { - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "BalancesAtToBalancesBetween", - can_build: BalancesAtToBalancesBetween::can_build, - build: BalancesAtToBalancesBetween::build, - }); - context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", can_build: GenerateBalances::can_build, @@ -42,6 +36,19 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { can_build: UpdateBalancesBetween::can_build, build: UpdateBalancesBetween::build, }); + + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesAt", + can_build: UpdateBalancesAt::can_build, + build: UpdateBalancesAt::build, + }); + + // This is the least efficient way of generating BalancesBetween + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: BalancesAtToBalancesBetween::can_build, + build: BalancesAtToBalancesBetween::build, + }); } #[derive(Debug)] @@ -63,6 +70,10 @@ impl BalancesAtToBalancesBetween { ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { + if !args.is::() { + return false; + } + let args = args.downcast_ref::().unwrap(); match has_step_or_can_build( @@ -105,7 +116,10 @@ impl BalancesAtToBalancesBetween { impl Display for BalancesAtToBalancesBetween { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{} {{BalancesAtToBalancesBetween}}", self.id())) + f.write_fmt(format_args!( + "{} {{BalancesAtToBalancesBetween}}", + self.id() + )) } } @@ -227,6 +241,133 @@ impl ReportingStep for GenerateBalances { } } +#[derive(Debug)] +pub struct UpdateBalancesAt { + step_name: &'static str, + args: DateArgs, +} + +impl UpdateBalancesAt { + // Implements (BalancesAt -> Transactions) -> BalancesAt + + fn can_build( + name: &'static str, + kind: ReportingProductKind, + _args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool { + // Check for Transactions -> BalancesAt + if kind == ReportingProductKind::BalancesAt { + // Initially no need to check args + if let Some(step) = steps.iter().find(|s| { + s.id().name == name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) { + // Check for BalancesAt -> Transactions + let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt + { + return true; + } + + // Check if BalancesBetween -> Transactions and BalancesAt is available + if dependencies_for_step.len() == 1 + && dependencies_for_step[0].dependency.kind + == ReportingProductKind::BalancesBetween + { + let date_end = dependencies_for_step[0] + .dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + match has_step_or_can_build( + &ReportingProductId { + name: dependencies_for_step[0].dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + return true; + } + HasStepOrCanBuild::None => {} + } + } + } + } + return false; + } + + fn build( + name: &'static str, + _kind: ReportingProductKind, + args: Box, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _context: &ReportingContext, + ) -> Box { + Box::new(UpdateBalancesAt { + step_name: name, + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for UpdateBalancesAt { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{} {{UpdateBalancesAt}}", self.id())) + } +} + +impl ReportingStep for UpdateBalancesAt { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: self.step_name, + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), + } + } + + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args.clone(), + }, + ); + } +} + #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index e5c2771..2073a43 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -17,8 +17,8 @@ */ use super::{ - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, - ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, + ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder, + ReportingStepFromArgsFn, ReportingStepId, }; #[derive(Debug)] @@ -41,7 +41,7 @@ impl ReportingGraphDependencies { } } - pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { + /*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { for kind in target.product_kinds { match kind { ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { @@ -58,7 +58,7 @@ impl ReportingGraphDependencies { ReportingProductKind::Generic => todo!(), } } - } + }*/ pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { return self.vec.iter().filter(|d| d.step == *step).collect(); @@ -206,46 +206,50 @@ pub fn solve_for( *name == dependency.dependency.name && kinds.contains(&dependency.dependency.kind) }) { - let (_, lookup_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); - let new_step = lookup_fn(dependency.dependency.args.clone()); + let (takes_args_fn, from_args_fn) = + context.step_lookup_fn.get(lookup_key).unwrap(); + if takes_args_fn(&dependency.dependency.args) { + let new_step = from_args_fn(dependency.dependency.args.clone()); - // Check new step meets the dependency - if new_step.id().name != dependency.dependency.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); - } - if new_step.id().args != dependency.dependency.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); - } - if !new_step - .id() - .product_kinds - .contains(&dependency.dependency.kind) - { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); - } + // Check new step meets the dependency + if new_step.id().name != dependency.dependency.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + } + if new_step.id().args != dependency.dependency.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.dependency.kind) + { + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + } - new_steps.push(new_step); - } else { - // No explicit step for product - try builders - for builder in context.step_dynamic_builders.iter() { - if (builder.can_build)( + new_steps.push(new_step); + continue; + } + } + + // No explicit step for product - try builders + for builder in context.step_dynamic_builders.iter() { + if (builder.can_build)( + dependency.dependency.name, + dependency.dependency.kind, + &dependency.dependency.args, + &steps, + &dependencies, + &context, + ) { + new_steps.push((builder.build)( dependency.dependency.name, dependency.dependency.kind, - &dependency.dependency.args, + dependency.dependency.args.clone(), &steps, &dependencies, &context, - ) { - new_steps.push((builder.build)( - dependency.dependency.name, - dependency.dependency.kind, - dependency.dependency.args.clone(), - &steps, - &dependencies, - &context, - )); - break; - } + )); + break; } } } @@ -267,9 +271,9 @@ pub fn solve_for( new_step.as_ref().init_graph(&steps, &mut dependencies); } - // Call after_init_graph on new steps - for new_step_index in new_step_indexes { - steps[new_step_index].after_init_graph(&steps, &mut dependencies); + // Call after_init_graph on all steps + for step in steps.iter() { + step.as_ref().after_init_graph(&steps, &mut dependencies); } } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index c3f8f19..bf512ed 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -27,11 +27,49 @@ use super::{ }; pub fn register_lookup_fns(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsExceptRetainedEarnings::takes_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args(&[ReportingProductKind::BalancesAt], a) + }, + ); + context.register_lookup_fn( "AllTransactionsExceptRetainedEarnings", &[ReportingProductKind::BalancesBetween], AllTransactionsExceptRetainedEarnings::takes_args, - AllTransactionsExceptRetainedEarnings::from_args, + |a| { + AllTransactionsExceptRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesAt], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesAt], + a, + ) + }, + ); + + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + AllTransactionsIncludingRetainedEarnings::takes_args, + |a| { + AllTransactionsIncludingRetainedEarnings::from_args( + &[ReportingProductKind::BalancesBetween], + a, + ) + }, ); context.register_lookup_fn( @@ -65,17 +103,22 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub args: DateStartDateEndArgs, + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, } impl AllTransactionsExceptRetainedEarnings { - fn takes_args(args: &Box) -> bool { - args.is::() + fn takes_args(_args: &Box) -> bool { + true } - - fn from_args(args: Box) -> Box { + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { - args: *args.downcast().unwrap(), + product_kinds, + args, }) } } @@ -90,12 +133,58 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsExceptRetainedEarnings", - product_kinds: &[ReportingProductKind::BalancesBetween], - args: Box::new(self.args.clone()), + product_kinds: self.product_kinds, + args: self.args.clone(), } } } +#[derive(Debug)] +pub struct AllTransactionsIncludingRetainedEarnings { + pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub args: Box, +} + +impl AllTransactionsIncludingRetainedEarnings { + fn takes_args(_args: &Box) -> bool { + true + } + + fn from_args( + product_kinds: &'static [ReportingProductKind], + args: Box, + ) -> Box { + Box::new(AllTransactionsIncludingRetainedEarnings { + product_kinds, + args, + }) + } +} + +impl Display for AllTransactionsIncludingRetainedEarnings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for AllTransactionsIncludingRetainedEarnings { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsIncludingRetainedEarnings", + product_kinds: self.product_kinds, + args: self.args.clone(), + } + } + + fn requires(&self) -> Vec { + vec![ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: self.product_kinds[0], + args: self.args.clone(), + }] + } +} + #[derive(Debug)] pub struct CalculateIncomeTax { pub args: DateEofyArgs, @@ -105,7 +194,7 @@ impl CalculateIncomeTax { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CalculateIncomeTax { args: *args.downcast().unwrap(), @@ -147,11 +236,35 @@ impl ReportingStep for CalculateIncomeTax { ) { for other in steps { if let Some(other) = other.downcast_ref::() { - if other.args.date_start <= self.args.date_eofy - && other.args.date_end >= self.args.date_eofy - { - // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax - dependencies.add_target_dependency(other.id(), self.id()); + // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax + if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date >= self.args.date_eofy { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else if other.args.is::() { + let other_args = other.args.downcast_ref::().unwrap(); + if other_args.date_start <= self.args.date_eofy + && other_args.date_end >= self.args.date_eofy + { + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); + } + } else { + unreachable!(); } } } @@ -167,7 +280,7 @@ impl CombineOrdinaryTransactions { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(CombineOrdinaryTransactions { args: *args.downcast().unwrap(), @@ -217,7 +330,7 @@ impl DBBalances { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(DBBalances { args: *args.downcast().unwrap(), @@ -250,7 +363,7 @@ impl PostUnreconciledStatementLines { fn takes_args(args: &Box) -> bool { args.is::() } - + fn from_args(args: Box) -> Box { Box::new(PostUnreconciledStatementLines { args: *args.downcast().unwrap(), From e47ad229eb92d9adba4236ef474ba8d418253f41 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:10:08 +1000 Subject: [PATCH 06/45] Refactor CalculateIncomeTax --- src/main.rs | 15 ++------ src/reporting/builders.rs | 8 +++-- src/reporting/calculator.rs | 12 +++---- src/reporting/mod.rs | 32 ++++++++--------- src/reporting/steps.rs | 72 +++++++++++++------------------------ 5 files changed, 54 insertions(+), 85 deletions(-) diff --git a/src/main.rs b/src/main.rs index 02ea9f7..a80fcae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,8 +24,7 @@ use libdrcr::reporting::{ register_lookup_fns, AllTransactionsExceptRetainedEarnings, AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, }, - DateArgs, DateEofyArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, - ReportingStep, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; fn main() { @@ -34,11 +33,7 @@ fn main() { register_dynamic_builders(&mut context); let targets: Vec> = vec![ - Box::new(CalculateIncomeTax { - args: DateEofyArgs { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsExceptRetainedEarnings { product_kinds: &[ReportingProductKind::BalancesBetween], args: Box::new(DateStartDateEndArgs { @@ -63,11 +58,7 @@ fn main() { register_dynamic_builders(&mut context); let targets: Vec> = vec![ - Box::new(CalculateIncomeTax { - args: DateEofyArgs { - date_eofy: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsIncludingRetainedEarnings { product_kinds: &[ReportingProductKind::BalancesAt], args: Box::new(DateArgs { diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 369df9c..1f89da1 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -132,7 +132,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { // BalancesAtToBalancesBetween depends on BalancesAt at both time points vec![ ReportingProductId { @@ -191,7 +191,7 @@ impl GenerateBalances { HasStepOrCanBuild::CanLookup(lookup_fn) => { // Check for () -> Transactions let step = lookup_fn(args.clone()); - if step.requires().len() == 0 { + if step.requires(context).len() == 0 { return true; } } @@ -231,7 +231,7 @@ impl ReportingStep for GenerateBalances { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { // GenerateBalances depends on Transactions vec![ReportingProductId { name: self.step_name, @@ -344,6 +344,7 @@ impl ReportingStep for UpdateBalancesAt { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args @@ -458,6 +459,7 @@ impl ReportingStep for UpdateBalancesBetween { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 2073a43..dc7d11e 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -176,15 +176,15 @@ pub fn solve_for( for target in targets { steps.push(target); let target = steps.last().unwrap(); - for dependency in target.requires() { + for dependency in target.requires(&context) { dependencies.add_dependency(target.id(), dependency); } - target.as_ref().init_graph(&steps, &mut dependencies); + target.as_ref().init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on targets for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies); + step.as_ref().after_init_graph(&steps, &mut dependencies, &context); } // Process dependencies @@ -265,15 +265,15 @@ pub fn solve_for( new_step_indexes.push(steps.len()); steps.push(new_step); let new_step = steps.last().unwrap(); - for dependency in new_step.requires() { + for dependency in new_step.requires(&context) { dependencies.add_dependency(new_step.id(), dependency); } - new_step.as_ref().init_graph(&steps, &mut dependencies); + new_step.as_ref().init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on all steps for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies); + step.as_ref().after_init_graph(&steps, &mut dependencies, &context); } } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index b0155bd..5a1ae40 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -30,7 +30,7 @@ pub mod calculator; pub mod steps; pub struct ReportingContext { - _eofy_date: NaiveDate, + eofy_date: NaiveDate, step_lookup_fn: HashMap< (&'static str, &'static [ReportingProductKind]), (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), @@ -41,7 +41,7 @@ pub struct ReportingContext { impl ReportingContext { pub fn new(eofy_date: NaiveDate) -> Self { Self { - _eofy_date: eofy_date, + eofy_date: eofy_date, step_lookup_fn: HashMap::new(), step_dynamic_builders: Vec::new(), } @@ -126,7 +126,7 @@ pub trait ReportingStep: Debug + Display + Downcast { fn id(&self) -> ReportingStepId; // Methods - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![] } @@ -134,6 +134,7 @@ pub trait ReportingStep: Debug + Display + Downcast { &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { } @@ -141,6 +142,7 @@ pub trait ReportingStep: Debug + Display + Downcast { &self, _steps: &Vec>, _dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { } @@ -160,6 +162,17 @@ dyn_eq::eq_trait_object!(ReportingStepArgs); pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VoidArgs {} + +impl ReportingStepArgs for VoidArgs {} + +impl Display for VoidArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("")) + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateArgs { pub date: NaiveDate, @@ -173,19 +186,6 @@ impl Display for DateArgs { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateEofyArgs { - pub date_eofy: NaiveDate, -} - -impl ReportingStepArgs for DateEofyArgs {} - -impl Display for DateEofyArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.date_eofy)) - } -} - #[derive(Clone, Debug, Eq, PartialEq)] pub struct DateStartDateEndArgs { pub date_start: NaiveDate, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index bf512ed..60b3c3e 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -21,9 +21,9 @@ use std::fmt::Display; use crate::util::sofy_from_eofy; use super::{ - calculator::ReportingGraphDependencies, DateArgs, DateEofyArgs, DateStartDateEndArgs, - ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, - ReportingStepId, + calculator::ReportingGraphDependencies, DateArgs, DateStartDateEndArgs, ReportingContext, + ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepId, + VoidArgs, }; pub fn register_lookup_fns(context: &mut ReportingContext) { @@ -176,7 +176,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![ReportingProductId { name: "AllTransactionsExceptRetainedEarnings", kind: self.product_kinds[0], @@ -186,19 +186,15 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } #[derive(Debug)] -pub struct CalculateIncomeTax { - pub args: DateEofyArgs, -} +pub struct CalculateIncomeTax {} impl CalculateIncomeTax { - fn takes_args(args: &Box) -> bool { - args.is::() + fn takes_args(_args: &Box) -> bool { + true } - fn from_args(args: Box) -> Box { - Box::new(CalculateIncomeTax { - args: *args.downcast().unwrap(), - }) + fn from_args(_args: Box) -> Box { + Box::new(CalculateIncomeTax {}) } } @@ -213,18 +209,18 @@ impl ReportingStep for CalculateIncomeTax { ReportingStepId { name: "CalculateIncomeTax", product_kinds: &[ReportingProductKind::Transactions], - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), } } - fn requires(&self) -> Vec { + fn requires(&self, context: &ReportingContext) -> Vec { // CalculateIncomeTax depends on CombineOrdinaryTransactions vec![ReportingProductId { name: "CombineOrdinaryTransactions", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(self.args.date_eofy), - date_end: self.args.date_eofy.clone(), + date_start: sofy_from_eofy(context.eofy_date), + date_end: context.eofy_date.clone(), }), }] } @@ -233,39 +229,19 @@ impl ReportingStep for CalculateIncomeTax { &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, + _context: &ReportingContext, ) { for other in steps { if let Some(other) = other.downcast_ref::() { - // AllTransactionsExceptRetainedEarnings (in applicable periods) depends on CalculateIncomeTax - if other.args.is::() { - let other_args = other.args.downcast_ref::().unwrap(); - if other_args.date >= self.args.date_eofy { - dependencies.add_dependency( - other.id(), - ReportingProductId { - name: self.id().name, - kind: other.product_kinds[0], - args: other.id().args, - }, - ); - } - } else if other.args.is::() { - let other_args = other.args.downcast_ref::().unwrap(); - if other_args.date_start <= self.args.date_eofy - && other_args.date_end >= self.args.date_eofy - { - dependencies.add_dependency( - other.id(), - ReportingProductId { - name: self.id().name, - kind: other.product_kinds[0], - args: other.id().args, - }, - ); - } - } else { - unreachable!(); - } + // AllTransactionsExceptRetainedEarnings depends on CalculateIncomeTax + dependencies.add_dependency( + other.id(), + ReportingProductId { + name: self.id().name, + kind: other.product_kinds[0], + args: other.id().args, + }, + ); } } } @@ -303,7 +279,7 @@ impl ReportingStep for CombineOrdinaryTransactions { } } - fn requires(&self) -> Vec { + fn requires(&self, _context: &ReportingContext) -> Vec { vec![ // CombineOrdinaryTransactions depends on DBBalances ReportingProductId { From ed0fbfb5ddd1190e04da55bc73801d8eab72398b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:20:19 +1000 Subject: [PATCH 07/45] Refactor AllTransactionsIncludingRetainedEarnings --- src/main.rs | 5 ++-- src/reporting/steps.rs | 57 ++++++++++++++++-------------------------- 2 files changed, 24 insertions(+), 38 deletions(-) diff --git a/src/main.rs b/src/main.rs index a80fcae..b112c11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,10 +60,9 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsIncludingRetainedEarnings { - product_kinds: &[ReportingProductKind::BalancesAt], - args: Box::new(DateArgs { + args: DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }), + }, }), ]; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 60b3c3e..e98c162 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -52,24 +52,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { "AllTransactionsIncludingRetainedEarnings", &[ReportingProductKind::BalancesAt], AllTransactionsIncludingRetainedEarnings::takes_args, - |a| { - AllTransactionsIncludingRetainedEarnings::from_args( - &[ReportingProductKind::BalancesAt], - a, - ) - }, - ); - - context.register_lookup_fn( - "AllTransactionsIncludingRetainedEarnings", - &[ReportingProductKind::BalancesBetween], - AllTransactionsIncludingRetainedEarnings::takes_args, - |a| { - AllTransactionsIncludingRetainedEarnings::from_args( - &[ReportingProductKind::BalancesBetween], - a, - ) - }, + AllTransactionsIncludingRetainedEarnings::from_args, ); context.register_lookup_fn( @@ -141,22 +124,17 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { #[derive(Debug)] pub struct AllTransactionsIncludingRetainedEarnings { - pub product_kinds: &'static [ReportingProductKind], // Must have single member - pub args: Box, + pub args: DateArgs, } impl AllTransactionsIncludingRetainedEarnings { - fn takes_args(_args: &Box) -> bool { - true + fn takes_args(args: &Box) -> bool { + args.is::() } - fn from_args( - product_kinds: &'static [ReportingProductKind], - args: Box, - ) -> Box { + fn from_args(args: Box) -> Box { Box::new(AllTransactionsIncludingRetainedEarnings { - product_kinds, - args, + args: *args.downcast().unwrap(), }) } } @@ -171,17 +149,26 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsIncludingRetainedEarnings", - product_kinds: self.product_kinds, - args: self.args.clone(), + product_kinds: &[ReportingProductKind::BalancesAt], + args: Box::new(self.args.clone()), } } fn requires(&self, _context: &ReportingContext) -> Vec { - vec![ReportingProductId { - name: "AllTransactionsExceptRetainedEarnings", - kind: self.product_kinds[0], - args: self.args.clone(), - }] + vec![ + // AllTransactionsIncludingRetainedEarnings requires AllTransactionsExceptRetainedEarnings + ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + // AllTransactionsIncludingRetainedEarnings requires RetainedEarningsToEquity + ReportingProductId { + name: "RetainedEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + ] } } From 959a8d8fb2d62a9688f586e11c50320a4d744634 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:24:29 +1000 Subject: [PATCH 08/45] Fix off by one error in BalancesAtToBalancesBetween --- src/reporting/builders.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 1f89da1..9779e69 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -139,7 +139,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: self.args.date_start.clone(), + date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day }), }, ReportingProductId { From 2d7250e9430b3e8fa16ef3a619b356400f88d2c5 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 18:24:59 +1000 Subject: [PATCH 09/45] Implement RetainedEarningsToEquity --- src/reporting/steps.rs | 56 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index e98c162..85ab6a7 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -18,6 +18,8 @@ use std::fmt::Display; +use chrono::Datelike; + use crate::util::sofy_from_eofy; use super::{ @@ -82,6 +84,13 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { PostUnreconciledStatementLines::takes_args, PostUnreconciledStatementLines::from_args, ); + + context.register_lookup_fn( + "RetainedEarningsToEquity", + &[ReportingProductKind::Transactions], + RetainedEarningsToEquity::takes_args, + RetainedEarningsToEquity::from_args, + ); } #[derive(Debug)] @@ -349,3 +358,50 @@ impl ReportingStep for PostUnreconciledStatementLines { } } } + +#[derive(Debug)] +pub struct RetainedEarningsToEquity { + pub args: DateArgs, +} + +impl RetainedEarningsToEquity { + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(RetainedEarningsToEquity { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for RetainedEarningsToEquity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for RetainedEarningsToEquity { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "RetainedEarningsToEquity", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, context: &ReportingContext) -> Vec { + // RetainedEarningsToEquity depends on CombineOrdinaryTransactions for last financial year + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: context + .eofy_date + .with_year(context.eofy_date.year() - 1) + .unwrap(), + }), + }] + } +} From d422e832753fffc25e88ff0428f8c19c7f368bd2 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 19:18:14 +1000 Subject: [PATCH 10/45] Rename Dependency.dependency to Dependency.product --- src/reporting/builders.rs | 10 +++--- src/reporting/calculator.rs | 61 +++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 9779e69..614f749 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -270,18 +270,18 @@ impl UpdateBalancesAt { // Check for BalancesAt -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesAt { return true; } // Check if BalancesBetween -> Transactions and BalancesAt is available if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesBetween { let date_end = dependencies_for_step[0] - .dependency + .product .args .downcast_ref::() .unwrap() @@ -289,7 +289,7 @@ impl UpdateBalancesAt { match has_step_or_can_build( &ReportingProductId { - name: dependencies_for_step[0].dependency.name, + name: dependencies_for_step[0].product.name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: date_end }), }, @@ -398,7 +398,7 @@ impl UpdateBalancesBetween { // Check for BalancesBetween -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 - && dependencies_for_step[0].dependency.kind + && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesBetween { return true; diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index dc7d11e..a06369b 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -35,9 +35,9 @@ impl ReportingGraphDependencies { if !self .vec .iter() - .any(|d| d.step == step && d.dependency == dependency) + .any(|d| d.step == step && d.product == dependency) { - self.vec.push(Dependency { step, dependency }); + self.vec.push(Dependency { step, product: dependency }); } } @@ -65,10 +65,11 @@ impl ReportingGraphDependencies { } } +/// Represents that a [ReportingStep] depends on a [ReportingProduct] #[derive(Debug)] pub struct Dependency { pub step: ReportingStepId, - pub dependency: ReportingProductId, + pub product: ReportingProductId, } #[derive(Debug)] @@ -147,12 +148,12 @@ fn would_be_ready_to_execute( // Check if the dependency has been produced by a previous step for previous_step in previous_steps { - if steps[*previous_step].id().name == dependency.dependency.name - && steps[*previous_step].id().args == dependency.dependency.args + if steps[*previous_step].id().name == dependency.product.name + && steps[*previous_step].id().args == dependency.product.args && steps[*previous_step] .id() .product_kinds - .contains(&dependency.dependency.kind) + .contains(&dependency.product.kind) { continue 'check_each_dependency; } @@ -197,33 +198,33 @@ pub fn solve_for( todo!(); } if !steps.iter().any(|s| { - s.id().name == dependency.dependency.name - && s.id().args == dependency.dependency.args - && s.id().product_kinds.contains(&dependency.dependency.kind) + s.id().name == dependency.product.name + && s.id().args == dependency.product.args + && s.id().product_kinds.contains(&dependency.product.kind) }) { // Try lookup function if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { - *name == dependency.dependency.name - && kinds.contains(&dependency.dependency.kind) + *name == dependency.product.name + && kinds.contains(&dependency.product.kind) }) { let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); - if takes_args_fn(&dependency.dependency.args) { - let new_step = from_args_fn(dependency.dependency.args.clone()); + if takes_args_fn(&dependency.product.args) { + let new_step = from_args_fn(dependency.product.args.clone()); // Check new step meets the dependency - if new_step.id().name != dependency.dependency.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.dependency.name, new_step.id().name); + if new_step.id().name != dependency.product.name { + panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.product.name, new_step.id().name); } - if new_step.id().args != dependency.dependency.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.dependency.name, dependency.dependency.args, new_step.id().args); + if new_step.id().args != dependency.product.args { + panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.product.name, dependency.product.args, new_step.id().args); } if !new_step .id() .product_kinds - .contains(&dependency.dependency.kind) + .contains(&dependency.product.kind) { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.dependency.name, dependency.dependency.kind, new_step.id().product_kinds); + panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.product.name, dependency.product.kind, new_step.id().product_kinds); } new_steps.push(new_step); @@ -234,17 +235,17 @@ pub fn solve_for( // No explicit step for product - try builders for builder in context.step_dynamic_builders.iter() { if (builder.can_build)( - dependency.dependency.name, - dependency.dependency.kind, - &dependency.dependency.args, + dependency.product.name, + dependency.product.kind, + &dependency.product.args, &steps, &dependencies, &context, ) { new_steps.push((builder.build)( - dependency.dependency.name, - dependency.dependency.kind, - dependency.dependency.args.clone(), + dependency.product.name, + dependency.product.kind, + dependency.product.args.clone(), &steps, &dependencies, &context, @@ -283,19 +284,19 @@ pub fn solve_for( return Err(ReportingCalculationError::UnknownStep { message: format!( "No implementation for step {} which {} is a dependency of", - dependency.step, dependency.dependency + dependency.step, dependency.product ), }); } if !steps.iter().any(|s| { - s.id().name == dependency.dependency.name - && s.id().args == dependency.dependency.args - && s.id().product_kinds.contains(&dependency.dependency.kind) + s.id().name == dependency.product.name + && s.id().args == dependency.product.args + && s.id().product_kinds.contains(&dependency.product.kind) }) { return Err(ReportingCalculationError::NoStepForProduct { message: format!( "No step builds product {} wanted by {}", - dependency.dependency, dependency.step + dependency.product, dependency.step ), }); } From 9e34360abf0e947303adce8420c8cc283dd45a1b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 19:59:57 +1000 Subject: [PATCH 11/45] Refactoring and documentation --- src/main.rs | 18 +-- src/reporting/builders.rs | 4 +- src/reporting/calculator.rs | 65 ++++----- src/reporting/mod.rs | 201 +------------------------ src/reporting/steps.rs | 8 +- src/reporting/types.rs | 282 ++++++++++++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 255 deletions(-) create mode 100644 src/reporting/types.rs diff --git a/src/main.rs b/src/main.rs index b112c11..16f2c3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,13 +17,13 @@ */ use chrono::NaiveDate; -use libdrcr::reporting::{ - builders::register_dynamic_builders, - calculator::solve_for, - steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, - AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, - }, +use libdrcr::reporting::builders::register_dynamic_builders; +use libdrcr::reporting::calculator::steps_for_targets; +use libdrcr::reporting::steps::{ + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, +}; +use libdrcr::reporting::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; @@ -44,7 +44,7 @@ fn main() { ]; println!("For income statement:"); - match solve_for(targets, context) { + match steps_for_targets(targets, context) { Ok(steps) => { for step in steps { println!("- {}", step); @@ -67,7 +67,7 @@ fn main() { ]; println!("For balance sheet:"); - match solve_for(targets, context) { + match steps_for_targets(targets, context) { Ok(steps) => { for step in steps { println!("- {}", step); diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 614f749..15bb630 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -18,8 +18,8 @@ use std::fmt::Display; -use super::{ - calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, +use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; +use super::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index a06369b..47f9c0e 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -16,50 +16,37 @@ along with this program. If not, see . */ -use super::{ +//! This module implements the dependency resolution for [ReportingStep]s + +use super::types::{ ReportingContext, ReportingProductId, ReportingStep, ReportingStepDynamicBuilder, ReportingStepFromArgsFn, ReportingStepId, }; +/// List of dependencies between [ReportingStep]s and [ReportingProduct]s #[derive(Debug)] pub struct ReportingGraphDependencies { vec: Vec, } impl ReportingGraphDependencies { + /// Get the list of [Dependency]s pub fn vec(&self) -> &Vec { &self.vec } - pub fn add_dependency(&mut self, step: ReportingStepId, dependency: ReportingProductId) { + /// Record that the [ReportingStep] depends on the [ReportingProduct] + pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) { if !self .vec .iter() - .any(|d| d.step == step && d.product == dependency) + .any(|d| d.step == step && d.product == product) { - self.vec.push(Dependency { step, product: dependency }); + self.vec.push(Dependency { step, product }); } } - /*pub fn add_target_dependency(&mut self, target: ReportingStepId, dependency: ReportingStepId) { - for kind in target.product_kinds { - match kind { - ReportingProductKind::Transactions | ReportingProductKind::BalancesBetween => { - self.add_dependency( - target.clone(), - ReportingProductId { - name: dependency.name, - kind: *kind, - args: target.args.clone(), - }, - ); - } - ReportingProductKind::BalancesAt => todo!(), - ReportingProductKind::Generic => todo!(), - } - } - }*/ - + /// Get the [Dependency]s for the given [ReportingStep] pub fn dependencies_for_step(&self, step: &ReportingStepId) -> Vec<&Dependency> { return self.vec.iter().filter(|d| d.step == *step).collect(); } @@ -72,6 +59,7 @@ pub struct Dependency { pub product: ReportingProductId, } +/// Indicates an error during dependency resolution in [steps_for_targets] #[derive(Debug)] pub enum ReportingCalculationError { UnknownStep { message: String }, @@ -86,6 +74,7 @@ pub enum HasStepOrCanBuild<'a, 'b> { None, } +/// Determines whether the [ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder pub fn has_step_or_can_build<'a, 'b>( product: &ReportingProductId, steps: &'a Vec>, @@ -129,23 +118,15 @@ pub fn has_step_or_can_build<'a, 'b>( return HasStepOrCanBuild::None; } +/// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed fn would_be_ready_to_execute( step: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, previous_steps: &Vec, ) -> bool { - //println!( - // "- would_be_ready_to_execute: {}, {:?}", - // step.id(), - // previous_steps - //); - - // Check whether the step would be ready to execute, if the previous steps have already completed 'check_each_dependency: for dependency in dependencies.vec.iter() { if dependency.step == step.id() { - //println!("-- {}", dependency.dependency); - // Check if the dependency has been produced by a previous step for previous_step in previous_steps { if steps[*previous_step].id().name == dependency.product.name @@ -166,7 +147,8 @@ fn would_be_ready_to_execute( true } -pub fn solve_for( +/// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s +pub fn steps_for_targets( targets: Vec>, context: ReportingContext, ) -> Result>, ReportingCalculationError> { @@ -180,12 +162,15 @@ pub fn solve_for( for dependency in target.requires(&context) { dependencies.add_dependency(target.id(), dependency); } - target.as_ref().init_graph(&steps, &mut dependencies, &context); + target + .as_ref() + .init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on targets for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies, &context); + step.as_ref() + .after_init_graph(&steps, &mut dependencies, &context); } // Process dependencies @@ -204,8 +189,7 @@ pub fn solve_for( }) { // Try lookup function if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { - *name == dependency.product.name - && kinds.contains(&dependency.product.kind) + *name == dependency.product.name && kinds.contains(&dependency.product.kind) }) { let (takes_args_fn, from_args_fn) = context.step_lookup_fn.get(lookup_key).unwrap(); @@ -269,12 +253,15 @@ pub fn solve_for( for dependency in new_step.requires(&context) { dependencies.add_dependency(new_step.id(), dependency); } - new_step.as_ref().init_graph(&steps, &mut dependencies, &context); + new_step + .as_ref() + .init_graph(&steps, &mut dependencies, &context); } // Call after_init_graph on all steps for step in steps.iter() { - step.as_ref().after_init_graph(&steps, &mut dependencies, &context); + step.as_ref() + .after_init_graph(&steps, &mut dependencies, &context); } } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 5a1ae40..bc34ccb 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -16,206 +16,7 @@ along with this program. If not, see . */ -use std::fmt::Debug; -use std::{collections::HashMap, fmt::Display}; - -use calculator::ReportingGraphDependencies; -use chrono::NaiveDate; -use downcast_rs::Downcast; -use dyn_clone::DynClone; -use dyn_eq::DynEq; - pub mod builders; pub mod calculator; pub mod steps; - -pub struct ReportingContext { - eofy_date: NaiveDate, - step_lookup_fn: HashMap< - (&'static str, &'static [ReportingProductKind]), - (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), - >, - step_dynamic_builders: Vec, -} - -impl ReportingContext { - pub fn new(eofy_date: NaiveDate) -> Self { - Self { - eofy_date: eofy_date, - step_lookup_fn: HashMap::new(), - step_dynamic_builders: Vec::new(), - } - } - - fn register_lookup_fn( - &mut self, - name: &'static str, - product_kinds: &'static [ReportingProductKind], - takes_args_fn: ReportingStepTakesArgsFn, - from_args_fn: ReportingStepFromArgsFn, - ) { - self.step_lookup_fn - .insert((name, product_kinds), (takes_args_fn, from_args_fn)); - } - - fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { - if !self - .step_dynamic_builders - .iter() - .any(|b| b.name == builder.name) - { - self.step_dynamic_builders.push(builder); - } - } -} - -#[derive(Debug, Eq, PartialEq)] -pub struct ReportingProductId { - name: &'static str, - kind: ReportingProductKind, - args: Box, -} - -impl Display for ReportingProductId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) - } -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum ReportingProductKind { - Transactions, - BalancesAt, - BalancesBetween, - Generic, -} - -//enum ReportingProduct { -// Transactions(Transactions), -// BalancesAt(BalancesAt), -// BalancesBetween(BalancesBetween), -// Generic(Box), -//} - -//struct Transactions {} -//struct BalancesAt {} -//struct BalancesBetween {} - -//trait GenericReportingProduct {} - -//type ReportingProducts = HashMap; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ReportingStepId { - pub name: &'static str, - pub product_kinds: &'static [ReportingProductKind], - pub args: Box, -} - -impl Display for ReportingStepId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "{}{:?}({})", - self.name, self.product_kinds, self.args - )) - } -} - -pub trait ReportingStep: Debug + Display + Downcast { - // Info - fn id(&self) -> ReportingStepId; - - // Methods - fn requires(&self, _context: &ReportingContext) -> Vec { - vec![] - } - - fn init_graph( - &self, - _steps: &Vec>, - _dependencies: &mut ReportingGraphDependencies, - _context: &ReportingContext, - ) { - } - - fn after_init_graph( - &self, - _steps: &Vec>, - _dependencies: &mut ReportingGraphDependencies, - _context: &ReportingContext, - ) { - } - - //fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) { - // todo!(); - //} -} - -downcast_rs::impl_downcast!(ReportingStep); - -pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq {} - -downcast_rs::impl_downcast!(ReportingStepArgs); -dyn_clone::clone_trait_object!(ReportingStepArgs); -dyn_eq::eq_trait_object!(ReportingStepArgs); - -pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; -pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct VoidArgs {} - -impl ReportingStepArgs for VoidArgs {} - -impl Display for VoidArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("")) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateArgs { - pub date: NaiveDate, -} - -impl ReportingStepArgs for DateArgs {} - -impl Display for DateArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}", self.date)) - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct DateStartDateEndArgs { - pub date_start: NaiveDate, - pub date_end: NaiveDate, -} - -impl ReportingStepArgs for DateStartDateEndArgs {} - -impl Display for DateStartDateEndArgs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) - } -} - -pub struct ReportingStepDynamicBuilder { - name: &'static str, - can_build: fn( - name: &'static str, - kind: ReportingProductKind, - args: &Box, - steps: &Vec>, - dependencies: &ReportingGraphDependencies, - context: &ReportingContext, - ) -> bool, - build: fn( - name: &'static str, - kind: ReportingProductKind, - args: Box, - steps: &Vec>, - dependencies: &ReportingGraphDependencies, - context: &ReportingContext, - ) -> Box, -} +pub mod types; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 85ab6a7..15482cd 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -20,13 +20,11 @@ use std::fmt::Display; use chrono::Datelike; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId}; use crate::util::sofy_from_eofy; -use super::{ - calculator::ReportingGraphDependencies, DateArgs, DateStartDateEndArgs, ReportingContext, - ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepId, - VoidArgs, -}; +use super:: calculator::ReportingGraphDependencies; +use super::types::{DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs}; pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( diff --git a/src/reporting/types.rs b/src/reporting/types.rs new file mode 100644 index 0000000..e6a25bd --- /dev/null +++ b/src/reporting/types.rs @@ -0,0 +1,282 @@ +/* + 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 std::collections::HashMap; +use std::fmt::{Debug, Display}; + +use chrono::NaiveDate; +use downcast_rs::Downcast; +use dyn_clone::DynClone; +use dyn_eq::DynEq; +use dyn_hash::DynHash; + +use crate::QuantityInt; + +use super::calculator::ReportingGraphDependencies; + +// ----------------- +// REPORTING CONTEXT + +/// Records the context for a single reporting job +pub struct ReportingContext { + pub eofy_date: NaiveDate, + pub(crate) step_lookup_fn: HashMap< + (&'static str, &'static [ReportingProductKind]), + (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), + >, + pub(crate) step_dynamic_builders: Vec, +} + +impl ReportingContext { + /// Initialise a new [ReportingContext] + pub fn new(eofy_date: NaiveDate) -> Self { + Self { + eofy_date: eofy_date, + step_lookup_fn: HashMap::new(), + step_dynamic_builders: Vec::new(), + } + } + + /// Register a lookup function + /// + /// A lookup function generates concrete [ReportingStep]s from a [ReportingStepId]. + pub fn register_lookup_fn( + &mut self, + name: &'static str, + product_kinds: &'static [ReportingProductKind], + takes_args_fn: ReportingStepTakesArgsFn, + from_args_fn: ReportingStepFromArgsFn, + ) { + self.step_lookup_fn + .insert((name, product_kinds), (takes_args_fn, from_args_fn)); + } + + /// Register a dynamic builder + /// + /// Dynamic builders are called when no concrete [ReportingStep] is implemented, and can dynamically generate a [ReportingStep]. Dynamic builders are implemented in [super::builders]. + pub fn register_dynamic_builder(&mut self, builder: ReportingStepDynamicBuilder) { + if !self + .step_dynamic_builders + .iter() + .any(|b| b.name == builder.name) + { + self.step_dynamic_builders.push(builder); + } + } +} + +/// Function which determines whether the [ReportingStepArgs] are valid arguments for a given [ReportingStep] +/// +/// See [ReportingContext::register_lookup_fn]. +pub type ReportingStepTakesArgsFn = fn(args: &Box) -> bool; + +/// Function which builds a concrete [ReportingStep] from the given [ReportingStepArgs] +/// +/// See [ReportingContext::register_lookup_fn]. +pub type ReportingStepFromArgsFn = fn(args: Box) -> Box; + +// ------------------------------- +// REPORTING STEP DYNAMIC BUILDERS + +/// Represents a reporting step dynamic builder +/// +/// See [ReportingContext::register_dynamic_builder]. +pub struct ReportingStepDynamicBuilder { + pub name: &'static str, + pub can_build: fn( + name: &'static str, + kind: ReportingProductKind, + args: &Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> bool, + pub build: fn( + name: &'static str, + kind: ReportingProductKind, + args: Box, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, + ) -> Box, +} + +// ------------------ +// REPORTING PRODUCTS + +/// Identifies a [ReportingProduct] +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct ReportingProductId { + pub name: &'static str, + pub kind: ReportingProductKind, + pub args: Box, +} + +impl Display for ReportingProductId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}.{:?}({})", self.name, self.kind, self.args)) + } +} + +/// Identifies a type of [ReportingProduct] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ReportingProductKind { + Transactions, + BalancesAt, + BalancesBetween, + Generic, +} + +/// Represents the result of a [ReportingStep] +pub enum ReportingProduct { + Transactions(Transactions), + BalancesAt(BalancesAt), + BalancesBetween(BalancesBetween), + Generic(Box), +} + +/// Records a list of transactions generated by a [ReportingStep] +pub struct Transactions {} + +/// Records cumulative account balances at a particular point in time +pub struct BalancesAt { + pub balances: HashMap, +} + +/// Records the total value of transactions in each account between two points in time +pub struct BalancesBetween {} + +/// Represents a custom [ReportingProduct] generated by a [ReportingStep] +pub trait GenericReportingProduct {} + +/// Convenience type mapping [ReportingProductId] to [ReportingProduct] +pub type ReportingProducts = HashMap; + +// --------------- +// REPORTING STEPS + +/// Identifies a [ReportingStep] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReportingStepId { + pub name: &'static str, + pub product_kinds: &'static [ReportingProductKind], + pub args: Box, +} + +impl Display for ReportingStepId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{:?}({})", + self.name, self.product_kinds, self.args + )) + } +} + +/// Represents a step in a reporting job +pub trait ReportingStep: Debug + Display + Downcast { + /// Get the [ReportingStepId] for this [ReportingStep] + fn id(&self) -> ReportingStepId; + + /// Return a list of statically defined dependencies for this [ReportingStep] + #[allow(unused_variables)] + fn requires(&self, context: &ReportingContext) -> Vec { + vec![] + } + + /// Called when the [ReportingStep] is initialised in [super::calculator::steps_for_targets] + #[allow(unused_variables)] + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + } + + /// Called when new [ReportingStep]s are initialised in [super::calculator::steps_for_targets] + /// + /// This callback can be used to dynamically declare dependencies between [ReportingStep]s that are not known at initialisation. + #[allow(unused_variables)] + fn after_init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + } + + /// Called to generate the [ReportingProduct] for this [ReportingStep] + #[allow(unused_variables)] + fn execute(&self, context: &ReportingContext, products: &mut ReportingProducts) { + todo!(); + } +} + +downcast_rs::impl_downcast!(ReportingStep); + +// ------------------------ +// REPORTING STEP ARGUMENTS + +/// Represents arguments to a [ReportingStep] +pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {} + +downcast_rs::impl_downcast!(ReportingStepArgs); +dyn_clone::clone_trait_object!(ReportingStepArgs); +dyn_eq::eq_trait_object!(ReportingStepArgs); +dyn_hash::hash_trait_object!(ReportingStepArgs); + +/// [ReportingStepArgs] implementation which takes no arguments +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct VoidArgs {} + +impl ReportingStepArgs for VoidArgs {} + +impl Display for VoidArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("")) + } +} + +/// [ReportingStepArgs] implementation which takes a single date +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DateArgs { + pub date: NaiveDate, +} + +impl ReportingStepArgs for DateArgs {} + +impl Display for DateArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.date)) + } +} + +/// [ReportingStepArgs] implementation which takes a date range +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DateStartDateEndArgs { + pub date_start: NaiveDate, + pub date_end: NaiveDate, +} + +impl ReportingStepArgs for DateStartDateEndArgs {} + +impl Display for DateStartDateEndArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) + } +} From db89814498158fd84112f250beecdae1b9962c1b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 20:15:18 +1000 Subject: [PATCH 12/45] Basic framework for executing reports --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/lib.rs | 2 ++ src/main.rs | 40 +++++-------------------------------- src/reporting/calculator.rs | 2 +- src/reporting/executor.rs | 37 ++++++++++++++++++++++++++++++++++ src/reporting/mod.rs | 36 +++++++++++++++++++++++++++++++++ src/reporting/steps.rs | 36 ++++++++++++++++++++++++++++++--- src/reporting/types.rs | 15 +++++++++++--- 9 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 src/reporting/executor.rs diff --git a/Cargo.lock b/Cargo.lock index fee6985..14f6134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ 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 = "iana-time-zone" version = "0.1.63" @@ -130,6 +136,7 @@ dependencies = [ "downcast-rs", "dyn-clone", "dyn-eq", + "dyn-hash", "solvent", ] diff --git a/Cargo.toml b/Cargo.toml index b4bac49..9cd9b5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ chrono = "0.4.41" downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" +dyn-hash = "0.2.2" solvent = "0.8.3" diff --git a/src/lib.rs b/src/lib.rs index 24e0b08..6130654 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ pub mod reporting; pub mod transaction; pub mod util; + +pub type QuantityInt = u64; diff --git a/src/main.rs b/src/main.rs index 16f2c3a..65cc9e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,13 +18,12 @@ use chrono::NaiveDate; use libdrcr::reporting::builders::register_dynamic_builders; -use libdrcr::reporting::calculator::steps_for_targets; +use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, - AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, }; use libdrcr::reporting::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, + DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, }; fn main() { @@ -43,36 +42,7 @@ fn main() { }), ]; - println!("For income statement:"); - match steps_for_targets(targets, context) { - Ok(steps) => { - for step in steps { - println!("- {}", step); - } - } - Err(err) => panic!("Error: {:?}", err), - } + let products = generate_report(targets, &context); - let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); - register_lookup_fns(&mut context); - register_dynamic_builders(&mut context); - - let targets: Vec> = vec![ - Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsIncludingRetainedEarnings { - args: DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), - ]; - - println!("For balance sheet:"); - match steps_for_targets(targets, context) { - Ok(steps) => { - for step in steps { - println!("- {}", step); - } - } - Err(err) => panic!("Error: {:?}", err), - } + println!("{:?}", products); } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 47f9c0e..68cbbfb 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -150,7 +150,7 @@ fn would_be_ready_to_execute( /// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s pub fn steps_for_targets( targets: Vec>, - context: ReportingContext, + context: &ReportingContext, ) -> Result>, ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs new file mode 100644 index 0000000..bcb9976 --- /dev/null +++ b/src/reporting/executor.rs @@ -0,0 +1,37 @@ +/* + 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 super::types::{ReportingContext, ReportingProducts, ReportingStep}; + +#[derive(Debug)] +pub struct ReportingExecutionError { + message: String, +} + +pub fn execute_steps( + steps: Vec>, + context: &ReportingContext, +) -> Result { + let mut products = ReportingProducts::new(); + + for step in steps { + step.execute(context, &mut products)?; + } + + Ok(products) +} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index bc34ccb..83a672f 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -16,7 +16,43 @@ along with this program. If not, see . */ +use calculator::{steps_for_targets, ReportingCalculationError}; +use executor::{execute_steps, ReportingExecutionError}; +use types::{ReportingContext, ReportingProducts, ReportingStep}; + pub mod builders; pub mod calculator; +pub mod executor; pub mod steps; pub mod types; + +#[derive(Debug)] +pub enum ReportingError { + ReportingCalculationError(ReportingCalculationError), + ReportingExecutionError(ReportingExecutionError), +} + +impl From for ReportingError { + fn from(err: ReportingCalculationError) -> Self { + ReportingError::ReportingCalculationError(err) + } +} + +impl From for ReportingError { + fn from(err: ReportingExecutionError) -> Self { + ReportingError::ReportingExecutionError(err) + } +} + +pub fn generate_report( + targets: Vec>, + context: &ReportingContext, +) -> Result { + // Solve dependencies + let sorted_steps = steps_for_targets(targets, context)?; + + // Execute steps + let products = execute_steps(sorted_steps, context)?; + + Ok(products) +} diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 15482cd..9984f69 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,15 +16,22 @@ along with this program. If not, see . */ +use std::collections::HashMap; use std::fmt::Display; use chrono::Datelike; -use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId}; +use crate::reporting::types::{ + BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId, +}; use crate::util::sofy_from_eofy; -use super:: calculator::ReportingGraphDependencies; -use super::types::{DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs}; +use super::calculator::ReportingGraphDependencies; +use super::executor::ReportingExecutionError; +use super::types::{ + DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, + ReportingStepArgs, ReportingStepId, VoidArgs, +}; pub fn register_lookup_fns(context: &mut ReportingContext) { context.register_lookup_fn( @@ -322,6 +329,29 @@ impl ReportingStep for DBBalances { args: Box::new(self.args.clone()), } } + + fn execute( + &self, + _context: &ReportingContext, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: DBBalances.execute"); + + let balances = BalancesAt { + balances: HashMap::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + ReportingProduct::BalancesAt(balances), + ); + + Ok(()) + } } #[derive(Debug)] diff --git a/src/reporting/types.rs b/src/reporting/types.rs index e6a25bd..7272eb3 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -28,6 +28,7 @@ use dyn_hash::DynHash; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; +use super::executor::ReportingExecutionError; // ----------------- // REPORTING CONTEXT @@ -143,6 +144,7 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] +#[derive(Debug)] pub enum ReportingProduct { Transactions(Transactions), BalancesAt(BalancesAt), @@ -151,18 +153,21 @@ pub enum ReportingProduct { } /// Records a list of transactions generated by a [ReportingStep] +#[derive(Debug)] pub struct Transactions {} /// Records cumulative account balances at a particular point in time +#[derive(Debug)] pub struct BalancesAt { pub balances: HashMap, } /// Records the total value of transactions in each account between two points in time +#[derive(Debug)] pub struct BalancesBetween {} /// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct {} +pub trait GenericReportingProduct: Debug {} /// Convenience type mapping [ReportingProductId] to [ReportingProduct] pub type ReportingProducts = HashMap; @@ -222,8 +227,12 @@ pub trait ReportingStep: Debug + Display + Downcast { /// Called to generate the [ReportingProduct] for this [ReportingStep] #[allow(unused_variables)] - fn execute(&self, context: &ReportingContext, products: &mut ReportingProducts) { - todo!(); + fn execute( + &self, + context: &ReportingContext, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + todo!("{}", self); } } From 71c3629898c3426b0a156ef0af18f49161cdc5c8 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 20:22:06 +1000 Subject: [PATCH 13/45] Refactor register_lookup_fns and register_dynamic_builders for readability --- src/reporting/builders.rs | 61 ++++++++++------- src/reporting/steps.rs | 140 +++++++++++++++++++++----------------- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 15bb630..0eaa902 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -24,31 +24,14 @@ use super::types::{ ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; +/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module pub fn register_dynamic_builders(context: &mut ReportingContext) { - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "GenerateBalances", - can_build: GenerateBalances::can_build, - build: GenerateBalances::build, - }); + GenerateBalances::register_dynamic_builder(context); + UpdateBalancesBetween::register_dynamic_builder(context); + UpdateBalancesAt::register_dynamic_builder(context); - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "UpdateBalancesBetween", - can_build: UpdateBalancesBetween::can_build, - build: UpdateBalancesBetween::build, - }); - - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "UpdateBalancesAt", - can_build: UpdateBalancesAt::can_build, - build: UpdateBalancesAt::build, - }); - - // This is the least efficient way of generating BalancesBetween - context.register_dynamic_builder(ReportingStepDynamicBuilder { - name: "BalancesAtToBalancesBetween", - can_build: BalancesAtToBalancesBetween::can_build, - build: BalancesAtToBalancesBetween::build, - }); + // This is the least efficient way of generating BalancesBetween so put at the end + BalancesAtToBalancesBetween::register_dynamic_builder(context); } #[derive(Debug)] @@ -60,6 +43,14 @@ pub struct BalancesAtToBalancesBetween { impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "BalancesAtToBalancesBetween", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -162,6 +153,14 @@ pub struct GenerateBalances { impl GenerateBalances { // Implements (() -> Transactions) -> BalancesAt + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "GenerateBalances", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -250,6 +249,14 @@ pub struct UpdateBalancesAt { impl UpdateBalancesAt { // Implements (BalancesAt -> Transactions) -> BalancesAt + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesAt", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, @@ -378,6 +385,14 @@ pub struct UpdateBalancesBetween { impl UpdateBalancesBetween { // Implements (BalancesBetween -> Transactions) -> BalancesBetween + fn register_dynamic_builder(context: &mut ReportingContext) { + context.register_dynamic_builder(ReportingStepDynamicBuilder { + name: "UpdateBalancesBetween", + can_build: Self::can_build, + build: Self::build, + }); + } + fn can_build( name: &'static str, kind: ReportingProductKind, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 9984f69..e021277 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -33,69 +33,15 @@ use super::types::{ ReportingStepArgs, ReportingStepId, VoidArgs, }; +/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module pub fn register_lookup_fns(context: &mut ReportingContext) { - context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", - &[ReportingProductKind::BalancesAt], - AllTransactionsExceptRetainedEarnings::takes_args, - |a| { - AllTransactionsExceptRetainedEarnings::from_args(&[ReportingProductKind::BalancesAt], a) - }, - ); - - context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", - &[ReportingProductKind::BalancesBetween], - AllTransactionsExceptRetainedEarnings::takes_args, - |a| { - AllTransactionsExceptRetainedEarnings::from_args( - &[ReportingProductKind::BalancesBetween], - a, - ) - }, - ); - - context.register_lookup_fn( - "AllTransactionsIncludingRetainedEarnings", - &[ReportingProductKind::BalancesAt], - AllTransactionsIncludingRetainedEarnings::takes_args, - AllTransactionsIncludingRetainedEarnings::from_args, - ); - - context.register_lookup_fn( - "CalculateIncomeTax", - &[ReportingProductKind::Transactions], - CalculateIncomeTax::takes_args, - CalculateIncomeTax::from_args, - ); - - context.register_lookup_fn( - "CombineOrdinaryTransactions", - &[ReportingProductKind::BalancesAt], - CombineOrdinaryTransactions::takes_args, - CombineOrdinaryTransactions::from_args, - ); - - context.register_lookup_fn( - "DBBalances", - &[ReportingProductKind::BalancesAt], - DBBalances::takes_args, - DBBalances::from_args, - ); - - context.register_lookup_fn( - "PostUnreconciledStatementLines", - &[ReportingProductKind::Transactions], - PostUnreconciledStatementLines::takes_args, - PostUnreconciledStatementLines::from_args, - ); - - context.register_lookup_fn( - "RetainedEarningsToEquity", - &[ReportingProductKind::Transactions], - RetainedEarningsToEquity::takes_args, - RetainedEarningsToEquity::from_args, - ); + AllTransactionsExceptRetainedEarnings::register_lookup_fn(context); + AllTransactionsIncludingRetainedEarnings::register_lookup_fn(context); + CalculateIncomeTax::register_lookup_fn(context); + CombineOrdinaryTransactions::register_lookup_fn(context); + DBBalances::register_lookup_fn(context); + PostUnreconciledStatementLines::register_lookup_fn(context); + RetainedEarningsToEquity::register_lookup_fn(context); } #[derive(Debug)] @@ -105,6 +51,22 @@ pub struct AllTransactionsExceptRetainedEarnings { } impl AllTransactionsExceptRetainedEarnings { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + |a| Self::from_args(&[ReportingProductKind::BalancesAt], a), + ); + + context.register_lookup_fn( + "AllTransactionsExceptRetainedEarnings", + &[ReportingProductKind::BalancesBetween], + Self::takes_args, + |a| Self::from_args(&[ReportingProductKind::BalancesBetween], a), + ); + } + fn takes_args(_args: &Box) -> bool { true } @@ -142,6 +104,15 @@ pub struct AllTransactionsIncludingRetainedEarnings { } impl AllTransactionsIncludingRetainedEarnings { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsIncludingRetainedEarnings", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -190,6 +161,15 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { pub struct CalculateIncomeTax {} impl CalculateIncomeTax { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CalculateIncomeTax", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(_args: &Box) -> bool { true } @@ -254,6 +234,15 @@ pub struct CombineOrdinaryTransactions { } impl CombineOrdinaryTransactions { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CombineOrdinaryTransactions", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -304,6 +293,15 @@ pub struct DBBalances { } impl DBBalances { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "DBBalances", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -360,6 +358,15 @@ pub struct PostUnreconciledStatementLines { } impl PostUnreconciledStatementLines { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "PostUnreconciledStatementLines", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } @@ -393,6 +400,15 @@ pub struct RetainedEarningsToEquity { } impl RetainedEarningsToEquity { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "RetainedEarningsToEquity", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + fn takes_args(args: &Box) -> bool { args.is::() } From 40b0afe4922057badb31ae29cf9f8ad68c5a0952 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 21:48:57 +1000 Subject: [PATCH 14/45] Stub implementations for all steps --- Cargo.lock | 30 +++- Cargo.toml | 2 +- src/main.rs | 46 +++++- src/reporting/builders.rs | 297 +++++++++++++++++++++++++++++++++--- src/reporting/calculator.rs | 4 +- src/reporting/executor.rs | 11 +- src/reporting/mod.rs | 4 +- src/reporting/steps.rs | 241 ++++++++++++++++++++++++++++- src/reporting/types.rs | 87 +++++++++-- src/transaction.rs | 25 ++- 10 files changed, 676 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 14f6134..7a70601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -112,6 +124,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -137,7 +159,7 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", - "solvent", + "indexmap", ] [[package]] @@ -191,12 +213,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "solvent" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" - [[package]] name = "syn" version = "2.0.101" diff --git a/Cargo.toml b/Cargo.toml index 9cd9b5d..f6046dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" -solvent = "0.8.3" +indexmap = "2.9.0" diff --git a/src/main.rs b/src/main.rs index 65cc9e2..b7e2bc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, }; use libdrcr::reporting::types::{ - DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, + ReportingStep, }; fn main() { @@ -31,6 +33,8 @@ fn main() { register_lookup_fns(&mut context); register_dynamic_builders(&mut context); + // Get income statement + let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsExceptRetainedEarnings { @@ -42,7 +46,41 @@ fn main() { }), ]; - let products = generate_report(targets, &context); + let products = generate_report(targets, &context).unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); - println!("{:?}", products); + println!("{:?}", result); + + // Get balance sheet + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax {}), + Box::new(AllTransactionsIncludingRetainedEarnings { + args: 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: "AllTransactionsIncludingRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("{:?}", result); } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 0eaa902..8cd3c11 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,12 +16,15 @@ along with this program. If not, see . */ +use std::collections::HashMap; use std::fmt::Display; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; +use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, - ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, + BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, + ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, + ReportingStepDynamicBuilder, ReportingStepId, Transactions, }; /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module @@ -40,6 +43,7 @@ pub struct BalancesAtToBalancesBetween { args: DateStartDateEndArgs, } +/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween @@ -137,13 +141,69 @@ impl ReportingStep for BalancesAtToBalancesBetween { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: self.args.date_end.clone(), + date: self.args.date_end, }), }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances at dates + let balances_start = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + let balances_end = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_end, + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + // Compute balances_end - balances_start + let mut balances = BalancesBetween { + balances: balances_end.clone(), + }; + + for (account, balance) in balances_start.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) - balance; + balances.balances.insert(account.clone(), running_balance); + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [super::steps::PostUnreconciledStatementLines]) #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, @@ -151,8 +211,6 @@ pub struct GenerateBalances { } impl GenerateBalances { - // Implements (() -> Transactions) -> BalancesAt - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", @@ -238,8 +296,58 @@ impl ReportingStep for GenerateBalances { args: Box::new(self.args.clone()), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get the transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Sum balances + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from: +/// - a step which generates [Transactions] from [BalancesAt], or +/// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available #[derive(Debug)] pub struct UpdateBalancesAt { step_name: &'static str, @@ -374,8 +482,97 @@ impl ReportingStep for UpdateBalancesAt { }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesAt step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build + + let opening_balances_at; + + if dependency.kind == ReportingProductKind::BalancesAt { + // Directly depends on BalancesAt -> Transaction + opening_balances_at = products + .get_or_err(&dependency)? + .downcast_ref::() + .unwrap(); + } else { + // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available + let date_end = dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + opening_balances_at = products + .get_or_err(&ReportingProductId { + name: dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + })? + .downcast_ref() + .unwrap(); + } + + // Sum balances + let mut balances = BalancesAt { + balances: opening_balances_at.balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween] #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, @@ -383,8 +580,6 @@ pub struct UpdateBalancesBetween { } impl UpdateBalancesBetween { - // Implements (BalancesBetween -> Transactions) -> BalancesBetween - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", @@ -419,23 +614,6 @@ impl UpdateBalancesBetween { return true; } } - - // Check lookup or builder - with args - /*match has_step_or_can_build( - &ReportingProductId { - name, - kind: ReportingProductKind::Transactions, - args: args.clone(), - }, - steps, - dependencies, - context, - ) { - HasStepOrCanBuild::HasStep(step) => unreachable!(), - HasStepOrCanBuild::CanLookup(_) - | HasStepOrCanBuild::CanBuild(_) - | HasStepOrCanBuild::None => {} - }*/ } return false; } @@ -493,8 +671,77 @@ impl ReportingStep for UpdateBalancesBetween { ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, - args: parent_step.id().args.clone(), + args: parent_step.id().args, }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesBetween step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness is checked in can_build + + // Get opening balances + let opening_balances = &products + .get_or_err(balances_between_product)? + .downcast_ref::() + .unwrap() + .balances; + + // Sum balances + let mut balances = BalancesBetween { + balances: opening_balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 68cbbfb..862df18 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -151,7 +151,7 @@ fn would_be_ready_to_execute( pub fn steps_for_targets( targets: Vec>, context: &ReportingContext, -) -> Result>, ReportingCalculationError> { +) -> Result<(Vec>, ReportingGraphDependencies), ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; @@ -319,5 +319,5 @@ pub fn steps_for_targets( .map(|(s, _idx)| s) .collect::>(); - Ok(sorted_steps) + Ok((sorted_steps, dependencies)) } diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index bcb9976..5c9dd4d 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,21 +16,22 @@ along with this program. If not, see . */ -use super::types::{ReportingContext, ReportingProducts, ReportingStep}; +use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; #[derive(Debug)] -pub struct ReportingExecutionError { - message: String, +pub enum ReportingExecutionError { + DependencyNotAvailable { message: String } } pub fn execute_steps( steps: Vec>, + dependencies: ReportingGraphDependencies, context: &ReportingContext, ) -> Result { let mut products = ReportingProducts::new(); - for step in steps { - step.execute(context, &mut products)?; + for step in steps.iter() { + step.execute(context, &steps, &dependencies, &mut products)?; } Ok(products) diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 83a672f..411a8ef 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -49,10 +49,10 @@ pub fn generate_report( context: &ReportingContext, ) -> Result { // Solve dependencies - let sorted_steps = steps_for_targets(targets, context)?; + let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; // Execute steps - let products = execute_steps(sorted_steps, context)?; + let products = execute_steps(sorted_steps, dependencies, context)?; Ok(products) } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index e021277..b541f4a 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,21 +16,21 @@ along with this program. If not, see . */ +//! This module contains concrete [ReportingStep] implementations + use std::collections::HashMap; use std::fmt::Display; use chrono::Datelike; -use crate::reporting::types::{ - BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId, -}; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, - ReportingStepArgs, ReportingStepId, VoidArgs, + BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind, + ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module @@ -96,6 +96,64 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { args: self.args.clone(), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get all dependencies + let step_dependencies = dependencies.dependencies_for_step(&self.id()); + + // Identify the product_kinds dependency most recently generated + if self.product_kinds.len() != 1 { + panic!("AllTransactionsExceptRetainedEarnings.product_kinds.len() != 1"); + } + let product_kind = self.product_kinds[0]; + + for (product_id, product) in products.map().iter().rev() { + if step_dependencies.iter().any(|d| d.product == *product_id) { + // Store the result + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product.clone(), + ); + + return Ok(()); + } + } + + // No dependencies?! - store empty result + let product: Box = match self.product_kinds[0] { + ReportingProductKind::Transactions => Box::new(Transactions { + transactions: Vec::new(), + }), + ReportingProductKind::BalancesAt => Box::new(BalancesAt { + balances: HashMap::new(), + }), + ReportingProductKind::BalancesBetween => Box::new(BalancesBetween { + balances: HashMap::new(), + }), + ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptRetainedEarnings.Generic but no available dependencies to provide it"), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product, + ); + + Ok(()) + } } #[derive(Debug)] @@ -155,6 +213,62 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get opening balances from AllTransactionsExceptRetainedEarnings + let opening_balances = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Get RetainedEarningsToEquity transactions + let transactions = products + .get_or_err(&ReportingProductId { + name: "RetainedEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Update balances + let mut balances = BalancesAt { + balances: opening_balances.balances.clone(), + }; + + for transaction in transactions.transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -226,6 +340,31 @@ impl ReportingStep for CalculateIncomeTax { } } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: CalculateIncomeTax.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -285,6 +424,44 @@ impl ReportingStep for CombineOrdinaryTransactions { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Sum balances of all dependencies + + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for dependency in dependencies.dependencies_for_step(&self.id()) { + let dependency_balances = &products + .get_or_err(&dependency.product)? + .downcast_ref::() + .unwrap() + .balances; + for (account, balance) in dependency_balances.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) + balance; + balances.balances.insert(account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -331,6 +508,8 @@ impl ReportingStep for DBBalances { fn execute( &self, _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { eprintln!("Stub: DBBalances.execute"); @@ -345,7 +524,7 @@ impl ReportingStep for DBBalances { kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - ReportingProduct::BalancesAt(balances), + Box::new(balances), ); Ok(()) @@ -392,6 +571,31 @@ impl ReportingStep for PostUnreconciledStatementLines { args: Box::new(self.args.clone()), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: PostUnreconciledStatementLines.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -448,4 +652,29 @@ impl ReportingStep for RetainedEarningsToEquity { }), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: RetainedEarningsToEquity.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 7272eb3..99a3494 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -24,7 +24,9 @@ use downcast_rs::Downcast; use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; +use indexmap::IndexMap; +use crate::transaction::TransactionWithPostings; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -121,7 +123,7 @@ pub struct ReportingStepDynamicBuilder { // REPORTING PRODUCTS /// Identifies a [ReportingProduct] -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct ReportingProductId { pub name: &'static str, pub kind: ReportingProductKind, @@ -144,33 +146,84 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] -#[derive(Debug)] -pub enum ReportingProduct { - Transactions(Transactions), - BalancesAt(BalancesAt), - BalancesBetween(BalancesBetween), - Generic(Box), -} +pub trait ReportingProduct: Debug + Downcast + DynClone {} + +downcast_rs::impl_downcast!(ReportingProduct); +dyn_clone::clone_trait_object!(ReportingProduct); /// Records a list of transactions generated by a [ReportingStep] -#[derive(Debug)] -pub struct Transactions {} +#[derive(Clone, Debug)] +pub struct Transactions { + pub transactions: Vec, +} + +impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct BalancesAt { pub balances: HashMap, } +impl ReportingProduct for BalancesAt {} + /// Records the total value of transactions in each account between two points in time -#[derive(Debug)] -pub struct BalancesBetween {} +#[derive(Clone, Debug)] +pub struct BalancesBetween { + pub balances: HashMap, +} + +impl ReportingProduct for BalancesBetween {} /// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct: Debug {} +pub trait GenericReportingProduct: Debug + ReportingProduct {} -/// Convenience type mapping [ReportingProductId] to [ReportingProduct] -pub type ReportingProducts = HashMap; +/// Map from [ReportingProductId] to [ReportingProduct] +#[derive(Clone, Debug)] +pub struct ReportingProducts { + map: IndexMap>, +} + +impl ReportingProducts { + pub fn new() -> Self { + Self { + map: IndexMap::new(), + } + } + + pub fn map(&self) -> &IndexMap> { + &self.map + } + + pub fn insert(&mut self, key: ReportingProductId, value: Box) { + self.map.insert(key, value); + } + + pub fn get_or_err( + &self, + key: &ReportingProductId, + ) -> Result<&Box, ReportingExecutionError> { + match self.map.get(key) { + Some(value) => Ok(value), + None => Err(ReportingExecutionError::DependencyNotAvailable { + message: format!("Product {} not available when expected", key), + }), + } + } +} + +impl Display for ReportingProducts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "ReportingProducts {{\n{}\n}}", + self.map + .iter() + .map(|(k, v)| format!(" {}: {:?}", k, v)) + .collect::>() + .join(",\n") + )) + } +} // --------------- // REPORTING STEPS @@ -230,6 +283,8 @@ pub trait ReportingStep: Debug + Display + Downcast { fn execute( &self, context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { todo!("{}", self); diff --git a/src/transaction.rs b/src/transaction.rs index a17ebbc..5a4c1aa 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,25 +1,44 @@ /* 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::NaiveDateTime; +use crate::QuantityInt; + +#[derive(Clone, Debug)] pub struct Transaction { pub id: Option, pub dt: NaiveDateTime, pub description: String, } + +#[derive(Clone, Debug)] +pub struct TransactionWithPostings { + pub transaction: Transaction, + pub postings: Vec, +} + +#[derive(Clone, Debug)] +pub struct Posting { + pub id: Option, + pub transaction_id: Option, + pub description: String, + pub account: String, + pub quantity: QuantityInt, + pub commodity: String, +} From 98018bb4bc9194b5eca2fc4aa9a9dbf993d83acc Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 21:53:35 +1000 Subject: [PATCH 15/45] Refactor update_balances_from_transactions --- src/reporting/builders.rs | 38 +++++--------------------------------- src/reporting/steps.rs | 13 ++----------- src/transaction.rs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 8cd3c11..54717ae 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -19,6 +19,8 @@ use std::collections::HashMap; use std::fmt::Display; +use crate::transaction::update_balances_from_transactions; + use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; use super::executor::ReportingExecutionError; use super::types::{ @@ -319,17 +321,7 @@ impl ReportingStep for GenerateBalances { let mut balances = BalancesAt { balances: HashMap::new(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( @@ -546,17 +538,7 @@ impl ReportingStep for UpdateBalancesAt { let mut balances = BalancesAt { balances: opening_balances_at.balances.clone(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( @@ -720,17 +702,7 @@ impl ReportingStep for UpdateBalancesBetween { let mut balances = BalancesBetween { balances: opening_balances.clone(), }; - - for transaction in transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result products.insert( diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index b541f4a..ed87930 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -24,6 +24,7 @@ use std::fmt::Display; use chrono::Datelike; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; +use crate::transaction::update_balances_from_transactions; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; @@ -245,17 +246,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { let mut balances = BalancesAt { balances: opening_balances.balances.clone(), }; - - for transaction in transactions.transactions.iter() { - for posting in transaction.postings.iter() { - // FIXME: Do currency conversion - let running_balance = - balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; - balances - .balances - .insert(posting.account.clone(), running_balance); - } - } + update_balances_from_transactions(&mut balances.balances, transactions.transactions.iter()); // Store result products.insert( diff --git a/src/transaction.rs b/src/transaction.rs index 5a4c1aa..1220c95 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::collections::HashMap; + use chrono::NaiveDateTime; use crate::QuantityInt; @@ -42,3 +44,16 @@ pub struct Posting { pub quantity: QuantityInt, pub commodity: String, } + +pub(crate) fn update_balances_from_transactions<'a, I: Iterator>( + balances: &mut HashMap, + transactions: I, +) { + for transaction in transactions { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances.insert(posting.account.clone(), running_balance); + } + } +} From 1bed0f269e8ba0d8dafa88ded2672a3aba329a09 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 22:26:40 +1000 Subject: [PATCH 16/45] Update documentation --- src/reporting/builders.rs | 8 ++++++-- src/reporting/calculator.rs | 8 ++++---- src/reporting/steps.rs | 17 +++++++++++++++++ src/util.rs | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 54717ae..a8f0027 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,6 +16,10 @@ along with this program. If not, see . */ +//! This module contains implementations of dynamic step builders +//! +//! See [ReportingContext::register_dynamic_builder][super::types::ReportingContext::register_dynamic_builder]. + use std::collections::HashMap; use std::fmt::Display; @@ -39,13 +43,13 @@ pub fn register_dynamic_builders(context: &mut ReportingContext) { BalancesAtToBalancesBetween::register_dynamic_builder(context); } +/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates #[derive(Debug)] pub struct BalancesAtToBalancesBetween { step_name: &'static str, args: DateStartDateEndArgs, } -/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween @@ -205,7 +209,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { } } -/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [super::steps::PostUnreconciledStatementLines]) +/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [PostUnreconciledStatementLines][super::steps::PostUnreconciledStatementLines]) #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 862df18..0c70a98 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -23,7 +23,7 @@ use super::types::{ ReportingStepFromArgsFn, ReportingStepId, }; -/// List of dependencies between [ReportingStep]s and [ReportingProduct]s +/// List of dependencies between [ReportingStep]s and [ReportingProduct][super::types::ReportingProduct]s #[derive(Debug)] pub struct ReportingGraphDependencies { vec: Vec, @@ -35,7 +35,7 @@ impl ReportingGraphDependencies { &self.vec } - /// Record that the [ReportingStep] depends on the [ReportingProduct] + /// Record that the [ReportingStep] depends on the [ReportingProduct][super::types::ReportingProduct] pub fn add_dependency(&mut self, step: ReportingStepId, product: ReportingProductId) { if !self .vec @@ -52,7 +52,7 @@ impl ReportingGraphDependencies { } } -/// Represents that a [ReportingStep] depends on a [ReportingProduct] +/// Represents that a [ReportingStep] depends on a [ReportingProduct][super::types::ReportingProduct] #[derive(Debug)] pub struct Dependency { pub step: ReportingStepId, @@ -74,7 +74,7 @@ pub enum HasStepOrCanBuild<'a, 'b> { None, } -/// Determines whether the [ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder +/// Determines whether the [ReportingProduct][super::types::ReportingProduct] is generated by a known step, or can be generated by a lookup function or dynamic builder pub fn has_step_or_can_build<'a, 'b>( product: &ReportingProductId, steps: &'a Vec>, diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index ed87930..4d0d5f4 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -45,6 +45,11 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { RetainedEarningsToEquity::register_lookup_fn(context); } +/// Target representing all transactions except charging retained earnings to equity +/// +/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. +/// +/// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { pub product_kinds: &'static [ReportingProductKind], // Must have single member @@ -157,6 +162,11 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { } } +/// Target representing all transactions including charging retained earnings to equity +/// +/// In other words, this is [AllTransactionsExceptRetainedEarnings] and [RetainedEarningsToEquity]. +/// +/// Used as the basis for the balance sheet. #[derive(Debug)] pub struct AllTransactionsIncludingRetainedEarnings { pub args: DateArgs, @@ -262,6 +272,7 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { } } +/// Calculates income tax #[derive(Debug)] pub struct CalculateIncomeTax {} @@ -358,6 +369,9 @@ impl ReportingStep for CalculateIncomeTax { } } +/// Combines all steps producing ordinary transactions +/// +/// By default, these are [DBBalances] and [PostUnreconciledStatementLines] #[derive(Debug)] pub struct CombineOrdinaryTransactions { pub args: DateArgs, @@ -455,6 +469,7 @@ impl ReportingStep for CombineOrdinaryTransactions { } } +/// Look up account balances from the database #[derive(Debug)] pub struct DBBalances { pub args: DateArgs, @@ -522,6 +537,7 @@ impl ReportingStep for DBBalances { } } +/// Generate transactions for unreconciled statement lines #[derive(Debug)] pub struct PostUnreconciledStatementLines { pub args: DateArgs, @@ -589,6 +605,7 @@ impl ReportingStep for PostUnreconciledStatementLines { } } +/// Transfer historical balances in income and expense accounts to the retained earnings equity account #[derive(Debug)] pub struct RetainedEarningsToEquity { pub args: DateArgs, diff --git a/src/util.rs b/src/util.rs index c8129c4..73bd8f9 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,7 +18,7 @@ use chrono::{Datelike, NaiveDate}; +/// Return the start date of the financial year, given the end date of the financial year pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { - // Return the start date of the financial year, given the end date of the financial year return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); } From 0eb583d0286a6de428ca1c2c2fc2008b63f12adf Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 21 May 2025 22:29:18 +1000 Subject: [PATCH 17/45] Statically require single member for AllTransactionsExceptRetainedEarnings.product_kinds --- src/reporting/steps.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 4d0d5f4..29850bc 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -52,7 +52,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { /// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { - pub product_kinds: &'static [ReportingProductKind], // Must have single member + pub product_kinds: &'static [ReportingProductKind; 1], // Must have single member - represented as static array for compatibility with ReportingStepId pub args: Box, } @@ -78,7 +78,7 @@ impl AllTransactionsExceptRetainedEarnings { } fn from_args( - product_kinds: &'static [ReportingProductKind], + product_kinds: &'static [ReportingProductKind; 1], args: Box, ) -> Box { Box::new(AllTransactionsExceptRetainedEarnings { @@ -113,10 +113,7 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { // Get all dependencies let step_dependencies = dependencies.dependencies_for_step(&self.id()); - // Identify the product_kinds dependency most recently generated - if self.product_kinds.len() != 1 { - panic!("AllTransactionsExceptRetainedEarnings.product_kinds.len() != 1"); - } + // Identify the product_kind dependency most recently generated let product_kind = self.product_kinds[0]; for (product_id, product) in products.map().iter().rev() { From ec470f8ced8f51bb90823c6273b7f8b87228b165 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Thu, 22 May 2025 00:25:51 +1000 Subject: [PATCH 18/45] Basic implementation of DBBalances --- Cargo.lock | 1654 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/db.rs | 85 ++ src/lib.rs | 3 +- src/main.rs | 10 +- src/reporting/builders.rs | 4 +- src/reporting/steps.rs | 17 +- src/reporting/types.rs | 8 +- src/util.rs | 7 +- 9 files changed, 1776 insertions(+), 15 deletions(-) create mode 100644 src/db.rs diff --git a/Cargo.lock b/Cargo.lock index 7a70601..202ed01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -17,18 +38,84 @@ dependencies = [ "libc", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + [[package]] name = "cc" version = "1.2.23" @@ -58,12 +145,116 @@ dependencies = [ "windows-link", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "2.0.1" @@ -88,17 +279,254 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] [[package]] name = "iana-time-zone" @@ -124,6 +552,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -134,6 +669,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -144,6 +685,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.172" @@ -159,7 +709,43 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", + "futures", "indexmap", + "sqlx", + "tokio", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", ] [[package]] @@ -168,6 +754,79 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -175,6 +834,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", ] [[package]] @@ -183,6 +852,107 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -201,18 +971,438 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustversion" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.101" @@ -224,12 +1414,208 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -288,6 +1674,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "whoami" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" +dependencies = [ + "redox_syscall", + "wasite", +] + [[package]] name = "windows-core" version = "0.61.1" @@ -346,3 +1742,261 @@ checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a" dependencies = [ "windows-link", ] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index f6046dc..ff5cf66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,7 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" +futures = "0.3.31" indexmap = "2.9.0" +sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } +tokio = { version = "1.45.0", features = ["full"] } diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..b7f0307 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,85 @@ +/* + 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 std::collections::HashMap; +use std::ops::DerefMut; +use std::{cell::RefCell, future::Future}; + +use chrono::NaiveDate; +use sqlx::{Connection, Row, SqliteConnection}; +use tokio::runtime::Runtime; + +use crate::{util::format_date, QuantityInt}; + +pub struct DbConnection { + sqlx_connection: RefCell, +} + +fn run_blocking(future: F) -> F::Output { + let rt = Runtime::new().unwrap(); + rt.block_on(future) +} + +impl DbConnection { + /// Connect to the given Sqlite database + pub fn connect(url: &str) -> Self { + Self { + sqlx_connection: RefCell::new(run_blocking(Self::connect_async(url))), + } + } + + async fn connect_async(url: &str) -> SqliteConnection { + SqliteConnection::connect(url).await.expect("SQL error") + } + + /// Get account balances from the database + pub fn get_balances(&self, date: NaiveDate) -> HashMap { + run_blocking(self.get_balances_async(date)) + } + + async fn get_balances_async(&self, date: NaiveDate) -> HashMap { + let mut connection = self.sqlx_connection.borrow_mut(); + + let rows = sqlx::query( + "-- Get last transaction for each account + WITH max_dt_by_account AS ( + SELECT account, max(dt) AS max_dt + FROM joined_transactions + WHERE DATE(dt) <= DATE($1) + GROUP BY account + ), + max_tid_by_account AS ( + SELECT max_dt_by_account.account, max(transaction_id) AS max_tid + FROM max_dt_by_account + JOIN joined_transactions ON max_dt_by_account.account = joined_transactions.account AND max_dt_by_account.max_dt = joined_transactions.dt + GROUP BY max_dt_by_account.account + ) + -- Get running balance at last transaction for each account + SELECT max_tid_by_account.account, running_balance AS quantity + FROM max_tid_by_account + JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account" + ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.unwrap(); + + let mut balances = HashMap::new(); + for row in rows { + balances.insert(row.get("account"), row.get("quantity")); + } + + balances + } +} diff --git a/src/lib.rs b/src/lib.rs index 6130654..8f1052b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ +pub mod db; pub mod reporting; pub mod transaction; pub mod util; -pub type QuantityInt = u64; +pub type QuantityInt = i64; diff --git a/src/main.rs b/src/main.rs index b7e2bc2..7f81e00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ */ use chrono::NaiveDate; +use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ @@ -29,7 +30,13 @@ use libdrcr::reporting::types::{ }; fn main() { - let mut context = ReportingContext::new(NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + // Connect to database + let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); + + // Initialise ReportingContext + let mut context = + ReportingContext::new(db_connection, NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + register_lookup_fns(&mut context); register_dynamic_builders(&mut context); @@ -82,5 +89,6 @@ fn main() { }) .unwrap(); + //println!("{}", products); println!("{:?}", result); } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index a8f0027..5411118 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -17,7 +17,7 @@ */ //! This module contains implementations of dynamic step builders -//! +//! //! See [ReportingContext::register_dynamic_builder][super::types::ReportingContext::register_dynamic_builder]. use std::collections::HashMap; @@ -548,7 +548,7 @@ impl ReportingStep for UpdateBalancesAt { products.insert( ReportingProductId { name: self.step_name, - kind: ReportingProductKind::BalancesBetween, + kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, Box::new(balances), diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 29850bc..39174ce 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -46,9 +46,9 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { } /// Target representing all transactions except charging retained earnings to equity -/// +/// /// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. -/// +/// /// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptRetainedEarnings { @@ -160,9 +160,9 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { } /// Target representing all transactions including charging retained earnings to equity -/// +/// /// In other words, this is [AllTransactionsExceptRetainedEarnings] and [RetainedEarningsToEquity]. -/// +/// /// Used as the basis for the balance sheet. #[derive(Debug)] pub struct AllTransactionsIncludingRetainedEarnings { @@ -367,7 +367,7 @@ impl ReportingStep for CalculateIncomeTax { } /// Combines all steps producing ordinary transactions -/// +/// /// By default, these are [DBBalances] and [PostUnreconciledStatementLines] #[derive(Debug)] pub struct CombineOrdinaryTransactions { @@ -510,15 +510,14 @@ impl ReportingStep for DBBalances { fn execute( &self, - _context: &ReportingContext, + context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { - eprintln!("Stub: DBBalances.execute"); - + // Get balances from DB let balances = BalancesAt { - balances: HashMap::new(), + balances: context.db_connection.get_balances(self.args.date), }; products.insert( diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 99a3494..3c7cb6a 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -26,6 +26,7 @@ use dyn_eq::DynEq; use dyn_hash::DynHash; use indexmap::IndexMap; +use crate::db::DbConnection; use crate::transaction::TransactionWithPostings; use crate::QuantityInt; @@ -37,7 +38,11 @@ use super::executor::ReportingExecutionError; /// Records the context for a single reporting job pub struct ReportingContext { + // Configuration + pub db_connection: DbConnection, pub eofy_date: NaiveDate, + + // State pub(crate) step_lookup_fn: HashMap< (&'static str, &'static [ReportingProductKind]), (ReportingStepTakesArgsFn, ReportingStepFromArgsFn), @@ -47,8 +52,9 @@ pub struct ReportingContext { impl ReportingContext { /// Initialise a new [ReportingContext] - pub fn new(eofy_date: NaiveDate) -> Self { + pub fn new(db_connection: DbConnection, eofy_date: NaiveDate) -> Self { Self { + db_connection: db_connection, eofy_date: eofy_date, step_lookup_fn: HashMap::new(), step_dynamic_builders: Vec::new(), diff --git a/src/util.rs b/src/util.rs index 73bd8f9..73f25a5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -20,5 +20,10 @@ use chrono::{Datelike, NaiveDate}; /// Return the start date of the financial year, given the end date of the financial year pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { - return date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap(); + date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap() +} + +/// Format the [NaiveDate] as a string +pub fn format_date(date: NaiveDate) -> String { + date.format("%Y-%m-%d 00:00:00.000000").to_string() } From 3add701d3cf9169786a9e31fcad027bc4922b5bc Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 23 May 2025 23:27:00 +1000 Subject: [PATCH 19/45] Implement RetainedEarningsToEquity --- src/account_config.rs | 43 +++++++++++++++++++++++++++ src/db.rs | 27 ++++++++++++++++- src/lib.rs | 1 + src/main.rs | 10 +++++-- src/reporting/steps.rs | 66 +++++++++++++++++++++++++++++++++++++++--- src/reporting/types.rs | 12 ++++++-- src/transaction.rs | 2 +- 7 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/account_config.rs diff --git a/src/account_config.rs b/src/account_config.rs new file mode 100644 index 0000000..c17e5d6 --- /dev/null +++ b/src/account_config.rs @@ -0,0 +1,43 @@ +/* + 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 std::collections::HashMap; + +pub struct AccountConfiguration { + pub id: Option, + pub account: String, + pub kind: String, + pub data: Option, +} + +/// Convert [`Vec`] into a [HashMap] mapping account names to account kinds +pub fn kinds_for_account( + account_configurations: Vec, +) -> HashMap> { + let mut result = HashMap::new(); + + for account_configuration in account_configurations { + // Record the account kind + result + .entry(account_configuration.account) + .or_insert_with(|| Vec::new()) + .push(account_configuration.kind); + } + + result +} diff --git a/src/db.rs b/src/db.rs index b7f0307..6c39107 100644 --- a/src/db.rs +++ b/src/db.rs @@ -21,9 +21,11 @@ use std::ops::DerefMut; use std::{cell::RefCell, future::Future}; use chrono::NaiveDate; +use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; use tokio::runtime::Runtime; +use crate::account_config::AccountConfiguration; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { @@ -73,7 +75,7 @@ impl DbConnection { SELECT max_tid_by_account.account, running_balance AS quantity FROM max_tid_by_account JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account" - ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.unwrap(); + ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error"); let mut balances = HashMap::new(); for row in rows { @@ -82,4 +84,27 @@ impl DbConnection { balances } + + /// Get account configurations from the database + pub fn get_account_configurations(&self) -> Vec { + run_blocking(self.get_account_configurations_async()) + } + + async fn get_account_configurations_async(&self) -> Vec { + let mut connection = self.sqlx_connection.borrow_mut(); + + let account_configurations = + sqlx::query("SELECT id, account, kind, data FROM account_configurations") + .map(|r: SqliteRow| AccountConfiguration { + id: r.get("id"), + account: r.get("account"), + kind: r.get("kind"), + data: r.get("data"), + }) + .fetch_all(connection.deref_mut()) + .await + .expect("SQL error"); + + account_configurations + } } diff --git a/src/lib.rs b/src/lib.rs index 8f1052b..4400e00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod account_config; pub mod db; pub mod reporting; pub mod transaction; diff --git a/src/main.rs b/src/main.rs index 7f81e00..e402984 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,8 +34,11 @@ fn main() { let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); // Initialise ReportingContext - let mut context = - ReportingContext::new(db_connection, NaiveDate::from_ymd_opt(2025, 6, 30).unwrap()); + 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); @@ -65,6 +68,7 @@ fn main() { }) .unwrap(); + println!("Income statement:"); println!("{:?}", result); // Get balance sheet @@ -89,6 +93,6 @@ fn main() { }) .unwrap(); - //println!("{}", products); + println!("Balance sheet:"); println!("{:?}", result); } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 39174ce..4d6d8ec 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -23,8 +23,11 @@ use std::fmt::Display; use chrono::Datelike; +use crate::account_config::kinds_for_account; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; -use crate::transaction::update_balances_from_transactions; +use crate::transaction::{ + update_balances_from_transactions, Posting, Transaction, TransactionWithPostings, +}; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; @@ -659,17 +662,72 @@ impl ReportingStep for RetainedEarningsToEquity { fn execute( &self, - _context: &ReportingContext, + context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { - eprintln!("Stub: RetainedEarningsToEquity.execute"); + // Get balances at end of last financial year + let last_eofy_date = context + .eofy_date + .with_year(context.eofy_date.year() - 1) + .unwrap(); - let transactions = Transactions { + let balances_last_eofy = products + .get_or_err(&ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: last_eofy_date.clone(), + }), + })? + .downcast_ref::() + .unwrap(); + + // Get income and expense accounts + let kinds_for_account = + kinds_for_account(context.db_connection.get_account_configurations()); + + // Transfer income and expense balances to retained earnings + let mut transactions = Transactions { transactions: Vec::new(), }; + for (account, balance) in balances_last_eofy.balances.iter() { + if let Some(kinds) = kinds_for_account.get(account) { + if kinds + .iter() + .any(|k| k == "drcr.income" || k == "drcr.expense") + { + transactions.transactions.push(TransactionWithPostings { + transaction: Transaction { + id: None, + dt: last_eofy_date.and_hms_opt(0, 0, 0).unwrap(), + description: "Retained earnings".to_string(), + }, + postings: vec![ + Posting { + id: None, + transaction_id: None, + description: None, + account: account.clone(), + quantity: -balance, + commodity: context.reporting_commodity.clone(), + }, + Posting { + id: None, + transaction_id: None, + description: None, + account: "Retained Earnings".to_string(), + quantity: *balance, + commodity: context.reporting_commodity.clone(), + }, + ], + }) + } + } + } + products.insert( ReportingProductId { name: self.id().name, diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 3c7cb6a..c78547b 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -41,6 +41,7 @@ pub struct ReportingContext { // Configuration pub db_connection: DbConnection, pub eofy_date: NaiveDate, + pub reporting_commodity: String, // State pub(crate) step_lookup_fn: HashMap< @@ -52,10 +53,15 @@ pub struct ReportingContext { impl ReportingContext { /// Initialise a new [ReportingContext] - pub fn new(db_connection: DbConnection, eofy_date: NaiveDate) -> Self { + pub fn new( + db_connection: DbConnection, + eofy_date: NaiveDate, + reporting_commodity: String, + ) -> Self { Self { - db_connection: db_connection, - eofy_date: eofy_date, + db_connection, + eofy_date, + reporting_commodity, step_lookup_fn: HashMap::new(), step_dynamic_builders: Vec::new(), } diff --git a/src/transaction.rs b/src/transaction.rs index 1220c95..f887cbc 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -39,7 +39,7 @@ pub struct TransactionWithPostings { pub struct Posting { pub id: Option, pub transaction_id: Option, - pub description: String, + pub description: Option, pub account: String, pub quantity: QuantityInt, pub commodity: String, From fa764619264260e59dae8bccf7d6dae22384407a Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 00:10:37 +1000 Subject: [PATCH 20/45] Implement CurrentYearEarningsToEquity --- src/main.rs | 12 +-- src/reporting/steps.rs | 217 +++++++++++++++++++++++++++++++++++------ 2 files changed, 193 insertions(+), 36 deletions(-) diff --git a/src/main.rs b/src/main.rs index e402984..fcae284 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,8 @@ use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, - AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptEarningsToEquity, + AllTransactionsIncludingEarningsToEquity, CalculateIncomeTax, }; use libdrcr::reporting::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, @@ -47,7 +47,7 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsExceptRetainedEarnings { + Box::new(AllTransactionsExceptEarningsToEquity { product_kinds: &[ReportingProductKind::BalancesBetween], args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), @@ -59,7 +59,7 @@ fn main() { let products = generate_report(targets, &context).unwrap(); let result = products .get_or_err(&ReportingProductId { - name: "AllTransactionsExceptRetainedEarnings", + name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), @@ -75,7 +75,7 @@ fn main() { let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsIncludingRetainedEarnings { + Box::new(AllTransactionsIncludingEarningsToEquity { args: DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), }, @@ -85,7 +85,7 @@ fn main() { let products = generate_report(targets, &context).unwrap(); let result = products .get_or_err(&ReportingProductId { - name: "AllTransactionsIncludingRetainedEarnings", + name: "AllTransactionsIncludingEarningsToEquity", kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 4d6d8ec..3403494 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -39,37 +39,38 @@ use super::types::{ /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module pub fn register_lookup_fns(context: &mut ReportingContext) { - AllTransactionsExceptRetainedEarnings::register_lookup_fn(context); - AllTransactionsIncludingRetainedEarnings::register_lookup_fn(context); + AllTransactionsExceptEarningsToEquity::register_lookup_fn(context); + AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context); CalculateIncomeTax::register_lookup_fn(context); CombineOrdinaryTransactions::register_lookup_fn(context); + CurrentYearEarningsToEquity::register_lookup_fn(context); DBBalances::register_lookup_fn(context); PostUnreconciledStatementLines::register_lookup_fn(context); RetainedEarningsToEquity::register_lookup_fn(context); } -/// Target representing all transactions except charging retained earnings to equity +/// Target representing all transactions except charging current year and retained earnings to equity /// /// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. /// /// Used as the basis for the income statement. #[derive(Debug)] -pub struct AllTransactionsExceptRetainedEarnings { +pub struct AllTransactionsExceptEarningsToEquity { pub product_kinds: &'static [ReportingProductKind; 1], // Must have single member - represented as static array for compatibility with ReportingStepId pub args: Box, } -impl AllTransactionsExceptRetainedEarnings { +impl AllTransactionsExceptEarningsToEquity { fn register_lookup_fn(context: &mut ReportingContext) { context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", + "AllTransactionsExceptEarningsToEquity", &[ReportingProductKind::BalancesAt], Self::takes_args, |a| Self::from_args(&[ReportingProductKind::BalancesAt], a), ); context.register_lookup_fn( - "AllTransactionsExceptRetainedEarnings", + "AllTransactionsExceptEarningsToEquity", &[ReportingProductKind::BalancesBetween], Self::takes_args, |a| Self::from_args(&[ReportingProductKind::BalancesBetween], a), @@ -84,23 +85,23 @@ impl AllTransactionsExceptRetainedEarnings { product_kinds: &'static [ReportingProductKind; 1], args: Box, ) -> Box { - Box::new(AllTransactionsExceptRetainedEarnings { + Box::new(AllTransactionsExceptEarningsToEquity { product_kinds, args, }) } } -impl Display for AllTransactionsExceptRetainedEarnings { +impl Display for AllTransactionsExceptEarningsToEquity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}", self.id())) } } -impl ReportingStep for AllTransactionsExceptRetainedEarnings { +impl ReportingStep for AllTransactionsExceptEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { - name: "AllTransactionsExceptRetainedEarnings", + name: "AllTransactionsExceptEarningsToEquity", product_kinds: self.product_kinds, args: self.args.clone(), } @@ -146,7 +147,7 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { ReportingProductKind::BalancesBetween => Box::new(BalancesBetween { balances: HashMap::new(), }), - ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptRetainedEarnings.Generic but no available dependencies to provide it"), + ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptEarningsToEquity.Generic but no available dependencies to provide it"), }; products.insert( @@ -162,20 +163,20 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { } } -/// Target representing all transactions including charging retained earnings to equity +/// Target representing all transactions including charging current year and retained earnings to equity /// -/// In other words, this is [AllTransactionsExceptRetainedEarnings] and [RetainedEarningsToEquity]. +/// In other words, this is [AllTransactionsExceptEarningsToEquity], [CurrentYearEarningsToEquity] and [RetainedEarningsToEquity]. /// /// Used as the basis for the balance sheet. #[derive(Debug)] -pub struct AllTransactionsIncludingRetainedEarnings { +pub struct AllTransactionsIncludingEarningsToEquity { pub args: DateArgs, } -impl AllTransactionsIncludingRetainedEarnings { +impl AllTransactionsIncludingEarningsToEquity { fn register_lookup_fn(context: &mut ReportingContext) { context.register_lookup_fn( - "AllTransactionsIncludingRetainedEarnings", + "AllTransactionsIncludingEarningsToEquity", &[ReportingProductKind::BalancesAt], Self::takes_args, Self::from_args, @@ -187,22 +188,22 @@ impl AllTransactionsIncludingRetainedEarnings { } fn from_args(args: Box) -> Box { - Box::new(AllTransactionsIncludingRetainedEarnings { + Box::new(AllTransactionsIncludingEarningsToEquity { args: *args.downcast().unwrap(), }) } } -impl Display for AllTransactionsIncludingRetainedEarnings { +impl Display for AllTransactionsIncludingEarningsToEquity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}", self.id())) } } -impl ReportingStep for AllTransactionsIncludingRetainedEarnings { +impl ReportingStep for AllTransactionsIncludingEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { - name: "AllTransactionsIncludingRetainedEarnings", + name: "AllTransactionsIncludingEarningsToEquity", product_kinds: &[ReportingProductKind::BalancesAt], args: Box::new(self.args.clone()), } @@ -210,13 +211,19 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { fn requires(&self, _context: &ReportingContext) -> Vec { vec![ - // AllTransactionsIncludingRetainedEarnings requires AllTransactionsExceptRetainedEarnings + // AllTransactionsIncludingEarningsToEquity requires AllTransactionsExceptEarningsToEquity ReportingProductId { - name: "AllTransactionsExceptRetainedEarnings", + name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - // AllTransactionsIncludingRetainedEarnings requires RetainedEarningsToEquity + // AllTransactionsIncludingEarningsToEquity requires CurrentYearEarningsToEquity + ReportingProductId { + name: "CurrentYearEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + // AllTransactionsIncludingEarningsToEquity requires RetainedEarningsToEquity ReportingProductId { name: "RetainedEarningsToEquity", kind: ReportingProductKind::Transactions, @@ -232,18 +239,28 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { - // Get opening balances from AllTransactionsExceptRetainedEarnings + // Get opening balances from AllTransactionsExceptEarningsToEquity let opening_balances = products .get_or_err(&ReportingProductId { - name: "AllTransactionsExceptRetainedEarnings", + name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), })? .downcast_ref::() .unwrap(); + // Get CurrentYearEarningsToEquity transactions + let transactions_current = products + .get_or_err(&ReportingProductId { + name: "CurrentYearEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + // Get RetainedEarningsToEquity transactions - let transactions = products + let transactions_retained = products .get_or_err(&ReportingProductId { name: "RetainedEarningsToEquity", kind: ReportingProductKind::Transactions, @@ -256,7 +273,14 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { let mut balances = BalancesAt { balances: opening_balances.balances.clone(), }; - update_balances_from_transactions(&mut balances.balances, transactions.transactions.iter()); + update_balances_from_transactions( + &mut balances.balances, + transactions_current.transactions.iter(), + ); + update_balances_from_transactions( + &mut balances.balances, + transactions_retained.transactions.iter(), + ); // Store result products.insert( @@ -329,8 +353,8 @@ impl ReportingStep for CalculateIncomeTax { _context: &ReportingContext, ) { for other in steps { - if let Some(other) = other.downcast_ref::() { - // AllTransactionsExceptRetainedEarnings depends on CalculateIncomeTax + if let Some(other) = other.downcast_ref::() { + // AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax dependencies.add_dependency( other.id(), ReportingProductId { @@ -469,6 +493,138 @@ impl ReportingStep for CombineOrdinaryTransactions { } } +/// Transfer current year balances in income and expense accounts to the current year earnings equity account +#[derive(Debug)] +pub struct CurrentYearEarningsToEquity { + pub args: DateArgs, +} + +impl CurrentYearEarningsToEquity { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CurrentYearEarningsToEquity", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(CurrentYearEarningsToEquity { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for CurrentYearEarningsToEquity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for CurrentYearEarningsToEquity { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CurrentYearEarningsToEquity", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, context: &ReportingContext) -> Vec { + // CurrentYearEarningsToEquity depends on CombineOrdinaryTransactions + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(context.eofy_date), + date_end: context.eofy_date.clone(), + }), + }] + } + + fn execute( + &self, + context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances for this financial year + let balances = products + .get_or_err(&ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: sofy_from_eofy(context.eofy_date), + date_end: context.eofy_date.clone(), + }), + })? + .downcast_ref::() + .unwrap(); + + // Get income and expense accounts + let kinds_for_account = + kinds_for_account(context.db_connection.get_account_configurations()); + + // Transfer income and expense balances to current year earnings + let mut transactions = Transactions { + transactions: Vec::new(), + }; + + for (account, balance) in balances.balances.iter() { + if let Some(kinds) = kinds_for_account.get(account) { + if kinds + .iter() + .any(|k| k == "drcr.income" || k == "drcr.expense") + { + transactions.transactions.push(TransactionWithPostings { + transaction: Transaction { + id: None, + dt: context.eofy_date.and_hms_opt(0, 0, 0).unwrap(), + description: "Current year earnings".to_string(), + }, + postings: vec![ + Posting { + id: None, + transaction_id: None, + description: None, + account: account.clone(), + quantity: -balance, + commodity: context.reporting_commodity.clone(), + }, + Posting { + id: None, + transaction_id: None, + description: None, + account: "Current Year Earnings".to_string(), + quantity: *balance, + commodity: context.reporting_commodity.clone(), + }, + ], + }) + } + } + } + + // Store product + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } +} + /// Look up account balances from the database #[derive(Debug)] pub struct DBBalances { @@ -728,6 +884,7 @@ impl ReportingStep for RetainedEarningsToEquity { } } + // Store product products.insert( ReportingProductId { name: self.id().name, From fa7381fce548d8e471c11b34efdaadc7d1aa9268 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 00:40:27 +1000 Subject: [PATCH 21/45] Add function to visualise dependency tree via graphviz --- src/main.rs | 16 ++++++++++++ src/reporting/calculator.rs | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/main.rs b/src/main.rs index fcae284..1d03348 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ use chrono::NaiveDate; use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; +use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets}; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ register_lookup_fns, AllTransactionsExceptEarningsToEquity, @@ -43,6 +44,21 @@ fn main() { register_lookup_fns(&mut context); register_dynamic_builders(&mut context); + // Print Graphviz + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax {}), + Box::new(AllTransactionsIncludingEarningsToEquity { + args: DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + ]; + let (sorted_steps, dependencies) = steps_for_targets(targets, &context).unwrap(); + + println!("Graphviz:"); + println!("{}", steps_as_graphviz(&sorted_steps, &dependencies)); + // Get income statement let targets: Vec> = vec![ diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 0c70a98..501430f 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -321,3 +321,52 @@ pub fn steps_for_targets( Ok((sorted_steps, dependencies)) } + +/// Generate graphviz code representing the dependency tree +/// +/// Useful for debugging or visualisation. Can be compiled using e.g. `dot -Tpdf -O output.gv`. +pub fn steps_as_graphviz( + steps: &Vec>, + dependencies: &ReportingGraphDependencies, +) -> String { + let mut result = String::from("strict digraph drcr {\n"); + + // Output all steps + for step in steps.iter() { + let step_display_name = step.to_string(); + if step_display_name.contains("{") { + // Bodge: Detect dynamic step builders + result.push_str(&format!( + "\"{}\" [shape=box, style=dashed, label=\"{}\"];\n", + step.id(), + step_display_name + )); + } else { + result.push_str(&format!("\"{}\" [shape=box];\n", step.id())); + } + + // Output the products of the step + for product_kind in step.id().product_kinds.iter() { + result.push_str(&format!( + "\"{}\" -> \"{}\";\n", + step.id(), + ReportingProductId { + name: step.id().name, + kind: *product_kind, + args: step.id().args + } + )); + } + } + + // Output all dependencies + for dependency in dependencies.vec().iter() { + result.push_str(&format!( + "\"{}\" -> \"{}\";\n", + dependency.product, dependency.step + )); + } + + result.push_str("}"); + result +} From a0c4aedb2d375344bd03fd3848762205958dd902 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 00:43:40 +1000 Subject: [PATCH 22/45] Validate dynamic builder outputs --- src/reporting/calculator.rs | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 501430f..b10f4a7 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -226,14 +226,31 @@ pub fn steps_for_targets( &dependencies, &context, ) { - new_steps.push((builder.build)( + let new_step = (builder.build)( dependency.product.name, dependency.product.kind, dependency.product.args.clone(), &steps, &dependencies, &context, - )); + ); + + // Check new step meets the dependency + if new_step.id().name != dependency.product.name { + panic!("Unexpected step returned from dynamic builder (expected name {}, got {})", dependency.product.name, new_step.id().name); + } + if new_step.id().args != dependency.product.args { + panic!("Unexpected step returned from dynamic builder {} (expected args {:?}, got {:?})", dependency.product.name, dependency.product.args, new_step.id().args); + } + if !new_step + .id() + .product_kinds + .contains(&dependency.product.kind) + { + panic!("Unexpected step returned from dynamic builder {} (expected kind {:?}, got {:?})", dependency.product.name, dependency.product.kind, new_step.id().product_kinds); + } + + new_steps.push(new_step); break; } } From fd761934e0bb8af1ee62cc5ac0ab8f3f3d9c03f5 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 00:51:24 +1000 Subject: [PATCH 23/45] Refactor build_step_for_product into standalone function Prepare for changing steps_for_targets to take list of products. --- src/reporting/calculator.rs | 132 ++++++++++++++++++------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index b10f4a7..65fdb2c 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -118,6 +118,66 @@ pub fn has_step_or_can_build<'a, 'b>( return HasStepOrCanBuild::None; } +/// Generates a new step which generates the requested [ReportingProduct][super::types::ReportingProduct], using a lookup function or dynamic builder +/// +/// Panics if a known step already generates the requested [ReportingProduct][super::types::ReportingProduct]. +fn build_step_for_product( + product: &ReportingProductId, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + context: &ReportingContext, +) -> Option> { + let new_step; + match has_step_or_can_build(product, steps, dependencies, context) { + HasStepOrCanBuild::HasStep(_) => { + panic!("Attempted to call build_step_for_product for already existing step") + } + HasStepOrCanBuild::CanLookup(from_args_fn) => { + new_step = from_args_fn(product.args.clone()); + } + HasStepOrCanBuild::CanBuild(builder) => { + new_step = (builder.build)( + product.name, + product.kind, + product.args.clone(), + &steps, + &dependencies, + &context, + ); + } + HasStepOrCanBuild::None => { + return None; + } + } + + // Check new step meets the dependency + if new_step.id().name != product.name { + panic!( + "Unexpected step returned from lookup function (expected name {}, got {})", + product.name, + new_step.id().name + ); + } + if new_step.id().args != product.args { + panic!( + "Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", + product.name, + product.args, + new_step.id().args + ); + } + if !new_step.id().product_kinds.contains(&product.kind) { + panic!( + "Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", + product.name, + product.kind, + new_step.id().product_kinds + ); + } + + Some(new_step) +} + /// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed fn would_be_ready_to_execute( step: &Box, @@ -179,6 +239,7 @@ pub fn steps_for_targets( for dependency in dependencies.vec.iter() { if !steps.iter().any(|s| s.id() == dependency.step) { + // Unknown step for which a dependency has been declared // FIXME: Call the lookup function todo!(); } @@ -187,72 +248,11 @@ pub fn steps_for_targets( && s.id().args == dependency.product.args && s.id().product_kinds.contains(&dependency.product.kind) }) { - // Try lookup function - if let Some(lookup_key) = context.step_lookup_fn.keys().find(|(name, kinds)| { - *name == dependency.product.name && kinds.contains(&dependency.product.kind) - }) { - let (takes_args_fn, from_args_fn) = - context.step_lookup_fn.get(lookup_key).unwrap(); - if takes_args_fn(&dependency.product.args) { - let new_step = from_args_fn(dependency.product.args.clone()); - - // Check new step meets the dependency - if new_step.id().name != dependency.product.name { - panic!("Unexpected step returned from lookup function (expected name {}, got {})", dependency.product.name, new_step.id().name); - } - if new_step.id().args != dependency.product.args { - panic!("Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", dependency.product.name, dependency.product.args, new_step.id().args); - } - if !new_step - .id() - .product_kinds - .contains(&dependency.product.kind) - { - panic!("Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", dependency.product.name, dependency.product.kind, new_step.id().product_kinds); - } - - new_steps.push(new_step); - continue; - } - } - - // No explicit step for product - try builders - for builder in context.step_dynamic_builders.iter() { - if (builder.can_build)( - dependency.product.name, - dependency.product.kind, - &dependency.product.args, - &steps, - &dependencies, - &context, - ) { - let new_step = (builder.build)( - dependency.product.name, - dependency.product.kind, - dependency.product.args.clone(), - &steps, - &dependencies, - &context, - ); - - // Check new step meets the dependency - if new_step.id().name != dependency.product.name { - panic!("Unexpected step returned from dynamic builder (expected name {}, got {})", dependency.product.name, new_step.id().name); - } - if new_step.id().args != dependency.product.args { - panic!("Unexpected step returned from dynamic builder {} (expected args {:?}, got {:?})", dependency.product.name, dependency.product.args, new_step.id().args); - } - if !new_step - .id() - .product_kinds - .contains(&dependency.product.kind) - { - panic!("Unexpected step returned from dynamic builder {} (expected kind {:?}, got {:?})", dependency.product.name, dependency.product.kind, new_step.id().product_kinds); - } - - new_steps.push(new_step); - break; - } + // No current step generates the product - try to lookup or build + if let Some(new_step) = + build_step_for_product(&dependency.product, &steps, &dependencies, context) + { + new_steps.push(new_step); } } } From 004f749c331e13e69953b5cf0fd0a2308fef191d Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 01:01:03 +1000 Subject: [PATCH 24/45] Refactor steps_for_targets to accept Vec --- src/main.rs | 58 +++++++++++++++++++++++-------------- src/reporting/calculator.rs | 34 +++++++++++++--------- src/reporting/mod.rs | 7 +++-- 3 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1d03348..d169050 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,13 +21,10 @@ use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets}; use libdrcr::reporting::generate_report; -use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptEarningsToEquity, - AllTransactionsIncludingEarningsToEquity, CalculateIncomeTax, -}; +use libdrcr::reporting::steps::register_lookup_fns; use libdrcr::reporting::types::{ DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, - ReportingStep, + VoidArgs, }; fn main() { @@ -46,13 +43,19 @@ fn main() { // Print Graphviz - let targets: Vec> = vec![ - Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsIncludingEarningsToEquity { - args: DateArgs { + let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "AllTransactionsIncludingEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + }), + }, ]; let (sorted_steps, dependencies) = steps_for_targets(targets, &context).unwrap(); @@ -61,15 +64,20 @@ fn main() { // Get income statement - let targets: Vec> = vec![ - Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsExceptEarningsToEquity { - product_kinds: &[ReportingProductKind::BalancesBetween], + let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), }), - }), + }, ]; let products = generate_report(targets, &context).unwrap(); @@ -89,13 +97,19 @@ fn main() { // Get balance sheet - let targets: Vec> = vec![ - Box::new(CalculateIncomeTax {}), - Box::new(AllTransactionsIncludingEarningsToEquity { - args: DateArgs { + let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "AllTransactionsIncludingEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), - }, - }), + }), + }, ]; let products = generate_report(targets, &context).unwrap(); diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 65fdb2c..35de398 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -207,33 +207,41 @@ fn would_be_ready_to_execute( true } -/// Recursively resolve the dependencies of the target [ReportingStep]s and return a sorted [Vec] of [ReportingStep]s +/// Recursively resolve the dependencies of the target [ReportingProductId]s and return a sorted [Vec] of [ReportingStep]s pub fn steps_for_targets( - targets: Vec>, + targets: Vec, context: &ReportingContext, ) -> Result<(Vec>, ReportingGraphDependencies), ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; - // Initialise targets - for target in targets { - steps.push(target); - let target = steps.last().unwrap(); - for dependency in target.requires(&context) { - dependencies.add_dependency(target.id(), dependency); + // Process initial targets + for target in targets.iter() { + if !steps.iter().any(|s| { + s.id().name == target.name + && s.id().args == target.args + && s.id().product_kinds.contains(&target.kind) + }) { + // No current step generates the product - try to lookup or build + if let Some(new_step) = build_step_for_product(&target, &steps, &dependencies, context) + { + steps.push(new_step); + let new_step = steps.last().unwrap(); + for dependency in new_step.requires(&context) { + dependencies.add_dependency(new_step.id(), dependency); + } + new_step.init_graph(&steps, &mut dependencies, &context); + } } - target - .as_ref() - .init_graph(&steps, &mut dependencies, &context); } - // Call after_init_graph on targets + // Call after_init_graph for step in steps.iter() { step.as_ref() .after_init_graph(&steps, &mut dependencies, &context); } - // Process dependencies + // Recursively process dependencies loop { let mut new_steps = Vec::new(); diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 411a8ef..aabe51c 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -18,7 +18,7 @@ use calculator::{steps_for_targets, ReportingCalculationError}; use executor::{execute_steps, ReportingExecutionError}; -use types::{ReportingContext, ReportingProducts, ReportingStep}; +use types::{ReportingContext, ReportingProductId, ReportingProducts}; pub mod builders; pub mod calculator; @@ -44,8 +44,11 @@ impl From for ReportingError { } } +/// Calculate the steps required to generate the requested [ReportingProductId]s and then execute them +/// +/// Helper function to call [steps_for_targets] followed by [execute_steps]. pub fn generate_report( - targets: Vec>, + targets: Vec, context: &ReportingContext, ) -> Result { // Solve dependencies From eb13bd5a87da0d1b08d48cb7f755725fdeeea3a9 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 14:32:01 +1000 Subject: [PATCH 25/45] CurrentYearEarningsToEquity depends on AllTransactionsExceptEarningsToEquity --- src/reporting/steps.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 3403494..7a19a44 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -536,9 +536,9 @@ impl ReportingStep for CurrentYearEarningsToEquity { } fn requires(&self, context: &ReportingContext) -> Vec { - // CurrentYearEarningsToEquity depends on CombineOrdinaryTransactions + // CurrentYearEarningsToEquity depends on AllTransactionsExceptEarningsToEquity vec![ReportingProductId { - name: "CombineOrdinaryTransactions", + name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { date_start: sofy_from_eofy(context.eofy_date), @@ -557,7 +557,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { // Get balances for this financial year let balances = products .get_or_err(&ReportingProductId { - name: "CombineOrdinaryTransactions", + name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { date_start: sofy_from_eofy(context.eofy_date), From 737ed5bfb2388a54de42baf32bbc4022f4c19c6e Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 24 May 2025 21:07:18 +1000 Subject: [PATCH 26/45] Implement formal BalanceSheet report --- src/db.rs | 16 +- src/main.rs | 34 ++-- src/reporting/dynamic_report.rs | 311 ++++++++++++++++++++++++++++++++ src/reporting/mod.rs | 1 + src/reporting/steps.rs | 186 ++++++++++++++++++- src/reporting/types.rs | 43 +++++ 6 files changed, 574 insertions(+), 17 deletions(-) create mode 100644 src/reporting/dynamic_report.rs diff --git a/src/db.rs b/src/db.rs index 6c39107..0e533fa 100644 --- a/src/db.rs +++ b/src/db.rs @@ -93,7 +93,7 @@ impl DbConnection { async fn get_account_configurations_async(&self) -> Vec { let mut connection = self.sqlx_connection.borrow_mut(); - let account_configurations = + let mut account_configurations = sqlx::query("SELECT id, account, kind, data FROM account_configurations") .map(|r: SqliteRow| AccountConfiguration { id: r.get("id"), @@ -105,6 +105,20 @@ impl DbConnection { .await .expect("SQL error"); + // System accounts + account_configurations.push(AccountConfiguration { + id: None, + account: "Current Year Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + account_configurations.push(AccountConfiguration { + id: None, + account: "Retained Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + account_configurations } } diff --git a/src/main.rs b/src/main.rs index d169050..9e011d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,8 +23,8 @@ use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets}; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::register_lookup_fns; use libdrcr::reporting::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, - VoidArgs, + DateArgs, DateStartDateEndArgs, MultipleDateArgs, ReportingContext, ReportingProductId, + ReportingProductKind, VoidArgs, }; fn main() { @@ -50,10 +50,12 @@ fn main() { args: Box::new(VoidArgs {}), }, ReportingProductId { - name: "AllTransactionsIncludingEarningsToEquity", - kind: ReportingProductKind::BalancesAt, - args: Box::new(DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + name: "BalanceSheet", + kind: ReportingProductKind::Generic, + args: Box::new(MultipleDateArgs { + dates: vec![DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }], }), }, ]; @@ -104,10 +106,12 @@ fn main() { args: Box::new(VoidArgs {}), }, ReportingProductId { - name: "AllTransactionsIncludingEarningsToEquity", - kind: ReportingProductKind::BalancesAt, - args: Box::new(DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + name: "BalanceSheet", + kind: ReportingProductKind::Generic, + args: Box::new(MultipleDateArgs { + dates: vec![DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }], }), }, ]; @@ -115,10 +119,12 @@ fn main() { let products = generate_report(targets, &context).unwrap(); let result = products .get_or_err(&ReportingProductId { - name: "AllTransactionsIncludingEarningsToEquity", - kind: ReportingProductKind::BalancesAt, - args: Box::new(DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + name: "BalanceSheet", + kind: ReportingProductKind::Generic, + args: Box::new(MultipleDateArgs { + dates: vec![DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }], }), }) .unwrap(); diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs new file mode 100644 index 0000000..5ba208a --- /dev/null +++ b/src/reporting/dynamic_report.rs @@ -0,0 +1,311 @@ +/* + 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 std::collections::HashMap; + +use crate::QuantityInt; + +use super::types::{GenericReportingProduct, ReportingProduct}; + +/// Represents a dynamically generated report composed of [DynamicReportEntry] +#[derive(Clone, Debug)] +pub struct DynamicReport { + pub title: String, + pub columns: Vec, + pub entries: Vec, +} + +impl DynamicReport { + /// Remove all entries from the report where auto_hide is enabled and quantity is zero + pub fn auto_hide(&mut self) { + self.entries.retain_mut(|e| match e { + DynamicReportEntry::Section(section) => { + section.auto_hide_children(); + if section.can_auto_hide_self() { + false + } else { + true + } + } + DynamicReportEntry::LiteralRow(row) => { + if row.can_auto_hide() { + false + } else { + true + } + } + DynamicReportEntry::CalculatedRow(_) => true, + DynamicReportEntry::Spacer => true, + }); + } + + /// Recursively calculate all [CalculatedRow] entries + pub fn calculate(&mut self) { + // FIXME: This is for the borrow checker - can it be avoided? + let report_cloned = self.clone(); + + for entry in self.entries.iter_mut() { + match entry { + DynamicReportEntry::Section(section) => section.calculate(&report_cloned), + DynamicReportEntry::LiteralRow(_) => (), + DynamicReportEntry::CalculatedRow(row) => { + *entry = DynamicReportEntry::LiteralRow(row.calculate(&report_cloned)); + } + DynamicReportEntry::Spacer => (), + } + } + } + + /// Look up [DynamicReportEntry] by id + pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { + for entry in self.entries.iter() { + match entry { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); + } + } + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { + return Some(entry); + } + } + } + DynamicReportEntry::CalculatedRow(_) => (), + DynamicReportEntry::Spacer => (), + } + } + + None + } + + /// Calculate the subtotals for the [Section] with the given id + pub fn subtotal_for_id(&self, id: &str) -> Vec { + let entry = self.by_id(id).expect("Invalid id"); + if let DynamicReportEntry::Section(section) = entry { + section.subtotal(&self) + } else { + panic!("Called subtotal_for_id on non-Section"); + } + } +} + +impl GenericReportingProduct for DynamicReport {} +impl ReportingProduct for DynamicReport {} + +#[derive(Clone, Debug)] +pub enum DynamicReportEntry { + Section(Section), + LiteralRow(LiteralRow), + CalculatedRow(CalculatedRow), + Spacer, +} + +#[derive(Clone, Debug)] +pub struct Section { + pub text: String, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub entries: Vec, +} + +impl Section { + fn auto_hide_children(&mut self) { + self.entries.retain_mut(|e| match e { + DynamicReportEntry::Section(section) => { + section.auto_hide_children(); + if section.can_auto_hide_self() { + false + } else { + true + } + } + DynamicReportEntry::LiteralRow(row) => { + if row.can_auto_hide() { + false + } else { + true + } + } + DynamicReportEntry::CalculatedRow(_) => true, + DynamicReportEntry::Spacer => true, + }); + } + + fn can_auto_hide_self(&self) -> bool { + self.auto_hide + && self.entries.iter().all(|e| match e { + DynamicReportEntry::Section(section) => section.can_auto_hide_self(), + DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), + DynamicReportEntry::CalculatedRow(_) => false, + DynamicReportEntry::Spacer => true, + }) + } + + /// Recursively calculate all [CalculatedRow] entries + pub fn calculate(&mut self, report: &DynamicReport) { + for entry in self.entries.iter_mut() { + match entry { + DynamicReportEntry::Section(section) => section.calculate(report), + DynamicReportEntry::LiteralRow(_) => (), + DynamicReportEntry::CalculatedRow(row) => { + *entry = DynamicReportEntry::LiteralRow(row.calculate(report)) + } + DynamicReportEntry::Spacer => (), + } + } + } + + /// Look up [DynamicReportEntry] by id + pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { + for entry in self.entries.iter() { + match entry { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); + } + } + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { + return Some(entry); + } + } + } + DynamicReportEntry::CalculatedRow(_) => (), + DynamicReportEntry::Spacer => (), + } + } + + None + } + + /// Calculate the subtotals for this [Section] + pub fn subtotal(&self, report: &DynamicReport) -> Vec { + let mut subtotals = vec![0; report.columns.len()]; + for entry in self.entries.iter() { + match entry { + DynamicReportEntry::Section(section) => { + for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + DynamicReportEntry::LiteralRow(row) => { + for (col_idx, subtotal) in row.quantity.iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + DynamicReportEntry::CalculatedRow(_) => (), + DynamicReportEntry::Spacer => (), + } + } + subtotals + } +} + +#[derive(Clone, Debug)] +pub struct LiteralRow { + pub text: String, + pub quantity: Vec, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub link: Option, + pub heading: bool, + pub bordered: bool, +} + +impl LiteralRow { + /// Returns whether the row has auto_hide enabled and all quantities are zero + fn can_auto_hide(&self) -> bool { + self.auto_hide && self.quantity.iter().all(|q| *q == 0) + } +} + +#[derive(Clone, Debug)] +pub struct CalculatedRow { + //pub text: String, + pub calculate_fn: fn(report: &DynamicReport) -> LiteralRow, + //pub id: Option, + //pub visible: bool, + //pub auto_hide: bool, + //pub link: Option, + //pub heading: bool, + //pub bordered: bool, +} + +impl CalculatedRow { + fn calculate(&self, report: &DynamicReport) -> LiteralRow { + (self.calculate_fn)(report) + } +} + +pub fn entries_for_kind( + kind: &str, + invert: bool, + balances: &Vec<&HashMap>, + kinds_for_account: &HashMap>, +) -> Vec { + // Get accounts of specified kind + let mut accounts = kinds_for_account + .iter() + .filter_map(|(a, k)| { + if k.iter().any(|k| k == kind) { + Some(a) + } else { + None + } + }) + .collect::>(); + + accounts.sort(); + + let mut entries = Vec::new(); + for account in accounts { + let quantities = balances + .iter() + .map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 }) + .collect::>(); + + let entry = LiteralRow { + text: account.to_string(), + quantity: quantities, + id: None, + visible: true, + auto_hide: true, + link: None, + heading: false, + bordered: false, + }; + entries.push(DynamicReportEntry::LiteralRow(entry)); + } + + entries +} diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index aabe51c..1ea2e13 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -22,6 +22,7 @@ use types::{ReportingContext, ReportingProductId, ReportingProducts}; pub mod builders; pub mod calculator; +pub mod dynamic_report; pub mod executor; pub mod steps; pub mod types; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 7a19a44..0c2977d 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -29,18 +29,24 @@ use crate::transaction::{ update_balances_from_transactions, Posting, Transaction, TransactionWithPostings, }; use crate::util::sofy_from_eofy; +use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; +use super::dynamic_report::{ + entries_for_kind, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, Section, +}; use super::executor::ReportingExecutionError; use super::types::{ - BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind, - ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, + BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProduct, + ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, + VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module pub fn register_lookup_fns(context: &mut ReportingContext) { AllTransactionsExceptEarningsToEquity::register_lookup_fn(context); AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context); + BalanceSheet::register_lookup_fn(context); CalculateIncomeTax::register_lookup_fn(context); CombineOrdinaryTransactions::register_lookup_fn(context); CurrentYearEarningsToEquity::register_lookup_fn(context); @@ -296,6 +302,182 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { } } +/// Generates a balance sheet [DynamicReport] +#[derive(Debug)] +pub struct BalanceSheet { + pub args: MultipleDateArgs, +} + +impl BalanceSheet { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "BalanceSheet", + &[ReportingProductKind::Generic], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(BalanceSheet { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for BalanceSheet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for BalanceSheet { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "BalanceSheet", + product_kinds: &[ReportingProductKind::Generic], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + let mut result = Vec::new(); + + // BalanceSheet depends on AllTransactionsIncludingEarningsToEquity in each requested period + for date_args in self.args.dates.iter() { + result.push(ReportingProductId { + name: "AllTransactionsIncludingEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(date_args.clone()), + }); + } + + result + } + + fn execute( + &self, + context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances for each period + let mut balances: Vec<&HashMap> = Vec::new(); + for date_args in self.args.dates.iter() { + let product = products.get_or_err(&ReportingProductId { + name: "AllTransactionsIncludingEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(date_args.clone()), + })?; + + balances.push(&product.downcast_ref::().unwrap().balances); + } + + // Get names of all balance sheet accounts + let kinds_for_account = + kinds_for_account(context.db_connection.get_account_configurations()); + + // Init report + let mut report = DynamicReport { + title: "Balance sheet".to_string(), + columns: self.args.dates.iter().map(|d| d.date.to_string()).collect(), + entries: vec![ + DynamicReportEntry::Section(Section { + text: "Assets".to_string(), + id: Some("assets".to_string()), + visible: true, + auto_hide: false, + entries: { + let mut entries = + entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); + entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total assets".to_string(), + quantity: report.subtotal_for_id("assets"), + id: None, + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + })); + entries + }, + }), + DynamicReportEntry::Spacer, + DynamicReportEntry::Section(Section { + text: "Liabilities".to_string(), + id: Some("liabilities".to_string()), + visible: true, + auto_hide: false, + entries: { + let mut entries = + entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); + entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total liabilities".to_string(), + quantity: report.subtotal_for_id("liabilities"), + id: None, + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + })); + entries + }, + }), + DynamicReportEntry::Spacer, + DynamicReportEntry::Section(Section { + text: "Equity".to_string(), + id: Some("equity".to_string()), + visible: true, + auto_hide: false, + entries: { + let mut entries = + entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); + entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total equity".to_string(), + quantity: report.subtotal_for_id("equity"), + id: None, + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + })); + entries + }, + }), + ], + }; + + report.calculate(); + report.auto_hide(); + + // Store the result + products.insert( + ReportingProductId { + name: "BalanceSheet", + kind: ReportingProductKind::Generic, + args: Box::new(self.args.clone()), + }, + Box::new(report), + ); + + Ok(()) + } +} + /// Calculates income tax #[derive(Debug)] pub struct CalculateIncomeTax {} diff --git a/src/reporting/types.rs b/src/reporting/types.rs index c78547b..27e89f2 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -18,6 +18,7 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; +use std::hash::Hash; use chrono::NaiveDate; use downcast_rs::Downcast; @@ -356,3 +357,45 @@ impl Display for DateStartDateEndArgs { f.write_fmt(format_args!("{}, {}", self.date_start, self.date_end)) } } + +/// [ReportingStepArgs] implementation which takes multiple [DateArgs] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct MultipleDateArgs { + pub dates: Vec, +} + +impl ReportingStepArgs for MultipleDateArgs {} + +impl Display for MultipleDateArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}", + self.dates + .iter() + .map(|a| a.to_string()) + .collect::>() + .join(", ") + )) + } +} + +/// [ReportingStepArgs] implementation which takes multiple [DateStartDateEndArgs] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct MultipleDateStartDateEndArgs { + pub dates: Vec, +} + +impl ReportingStepArgs for MultipleDateStartDateEndArgs {} + +impl Display for MultipleDateStartDateEndArgs { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}", + self.dates + .iter() + .map(|a| format!("({})", a)) + .collect::>() + .join(", ") + )) + } +} From 3913558410953d2ffd6846a404f6d1d4a0ab94ef Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 25 May 2025 01:20:30 +1000 Subject: [PATCH 27/45] Remove unused dependency from Cargo.toml --- Cargo.lock | 29 ----------------------------- Cargo.toml | 1 - 2 files changed, 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 202ed01..267c59a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,21 +342,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -401,17 +386,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -430,10 +404,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -709,7 +681,6 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", - "futures", "indexmap", "sqlx", "tokio", diff --git a/Cargo.toml b/Cargo.toml index ff5cf66..b15bbe8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" -futures = "0.3.31" indexmap = "2.9.0" sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } tokio = { version = "1.45.0", features = ["full"] } From 33252739d9b2f3b01611a419bc285bf6b80c8e15 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 25 May 2025 01:20:37 +1000 Subject: [PATCH 28/45] Update documentation --- src/reporting/types.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 27e89f2..1cbfb95 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -194,6 +194,7 @@ pub trait GenericReportingProduct: Debug + ReportingProduct {} /// Map from [ReportingProductId] to [ReportingProduct] #[derive(Clone, Debug)] pub struct ReportingProducts { + // This needs to be an IndexMap not HashMap, because sometimes we query which product is more up to date map: IndexMap>, } From 3cfcdf67785c750b8fb3895e813f7ac35ca09896 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 25 May 2025 01:23:35 +1000 Subject: [PATCH 29/45] Implement JSON serialisation for DynamicReport --- Cargo.lock | 2 ++ Cargo.toml | 2 ++ src/main.rs | 6 +++++- src/reporting/dynamic_report.rs | 16 ++++++++++++---- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 267c59a..a9e4885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -682,6 +682,8 @@ dependencies = [ "dyn-eq", "dyn-hash", "indexmap", + "serde", + "serde_json", "sqlx", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index b15bbe8..d5bbea0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,7 @@ dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" indexmap = "2.9.0" +serde = "1.0.219" +serde_json = "1.0.140" sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite" ] } tokio = { version = "1.45.0", features = ["full"] } diff --git a/src/main.rs b/src/main.rs index 9e011d3..a8430e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use chrono::NaiveDate; use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::calculator::{steps_as_graphviz, steps_for_targets}; +use libdrcr::reporting::dynamic_report::DynamicReport; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::register_lookup_fns; use libdrcr::reporting::types::{ @@ -130,5 +131,8 @@ fn main() { .unwrap(); println!("Balance sheet:"); - println!("{:?}", result); + println!( + "{}", + result.downcast_ref::().unwrap().to_json() + ); } diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index 5ba208a..f08276a 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -18,12 +18,14 @@ use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + use crate::QuantityInt; use super::types::{GenericReportingProduct, ReportingProduct}; /// Represents a dynamically generated report composed of [DynamicReportEntry] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct DynamicReport { pub title: String, pub columns: Vec, @@ -109,20 +111,26 @@ impl DynamicReport { panic!("Called subtotal_for_id on non-Section"); } } + + /// Serialise the report (as JSON) using serde + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } } impl GenericReportingProduct for DynamicReport {} impl ReportingProduct for DynamicReport {} -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum DynamicReportEntry { Section(Section), LiteralRow(LiteralRow), + #[serde(skip)] CalculatedRow(CalculatedRow), Spacer, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Section { pub text: String, pub id: Option, @@ -230,7 +238,7 @@ impl Section { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct LiteralRow { pub text: String, pub quantity: Vec, From f4c232ae35b59f26874862e71a2a06feb806af7a Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 26 May 2025 22:43:59 +1000 Subject: [PATCH 30/45] Fix multiple logic errors when reporting for not current year --- src/main.rs | 16 +++++---- src/reporting/builders.rs | 63 ++++++++++++++++++++++---------- src/reporting/steps.rs | 75 +++++++++++++++++---------------------- src/util.rs | 18 ++++++++-- 4 files changed, 103 insertions(+), 69 deletions(-) diff --git a/src/main.rs b/src/main.rs index a8430e3..27da957 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,8 @@ use libdrcr::reporting::types::{ }; fn main() { + const YEAR: i32 = 2023; + // Connect to database let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); @@ -55,7 +57,7 @@ fn main() { kind: ReportingProductKind::Generic, args: Box::new(MultipleDateArgs { dates: vec![DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), }], }), }, @@ -77,8 +79,8 @@ fn main() { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), - date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), }), }, ]; @@ -89,8 +91,8 @@ fn main() { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), - date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), }), }) .unwrap(); @@ -111,7 +113,7 @@ fn main() { kind: ReportingProductKind::Generic, args: Box::new(MultipleDateArgs { dates: vec![DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), }], }), }, @@ -124,7 +126,7 @@ fn main() { kind: ReportingProductKind::Generic, args: Box::new(MultipleDateArgs { dates: vec![DateArgs { - date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), }], }), }) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 5411118..d6a5e8b 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -364,11 +364,15 @@ impl UpdateBalancesAt { fn can_build( name: &'static str, kind: ReportingProductKind, - _args: &Box, + args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, ) -> bool { + if !args.is::() { + return false; + } + // Check for Transactions -> BalancesAt if kind == ReportingProductKind::BalancesAt { // Initially no need to check args @@ -391,18 +395,13 @@ impl UpdateBalancesAt { && dependencies_for_step[0].product.kind == ReportingProductKind::BalancesBetween { - let date_end = dependencies_for_step[0] - .product - .args - .downcast_ref::() - .unwrap() - .date_end; - match has_step_or_can_build( &ReportingProductId { name: dependencies_for_step[0].product.name, kind: ReportingProductKind::BalancesAt, - args: Box::new(DateArgs { date: date_end }), + args: Box::new(DateArgs { + date: args.downcast_ref::().unwrap().date, + }), }, steps, dependencies, @@ -477,6 +476,27 @@ impl ReportingStep for UpdateBalancesAt { args: parent_step.id().args.clone(), }, ); + + // Look up the BalancesAt step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build + + if dependency.kind == ReportingProductKind::BalancesAt { + // Directly depends on BalancesAt -> Transaction + // Do not need to add extra dependencies + } else { + // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date, + }), + }, + ); + } } fn execute( @@ -522,17 +542,13 @@ impl ReportingStep for UpdateBalancesAt { .unwrap(); } else { // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available - let date_end = dependency - .args - .downcast_ref::() - .unwrap() - .date_end; - opening_balances_at = products .get_or_err(&ReportingProductId { name: dependency.name, kind: ReportingProductKind::BalancesAt, - args: Box::new(DateArgs { date: date_end }), + args: Box::new(DateArgs { + date: self.args.date, + }), })? .downcast_ref() .unwrap(); @@ -542,7 +558,12 @@ impl ReportingStep for UpdateBalancesAt { let mut balances = BalancesAt { balances: opening_balances_at.balances.clone(), }; - update_balances_from_transactions(&mut balances.balances, transactions.iter()); + update_balances_from_transactions( + &mut balances.balances, + transactions + .iter() + .filter(|t| t.transaction.dt.date() <= self.args.date), + ); // Store result products.insert( @@ -706,7 +727,13 @@ impl ReportingStep for UpdateBalancesBetween { let mut balances = BalancesBetween { balances: opening_balances.clone(), }; - update_balances_from_transactions(&mut balances.balances, transactions.iter()); + update_balances_from_transactions( + &mut balances.balances, + transactions.iter().filter(|t| { + t.transaction.dt.date() >= self.args.date_start + && t.transaction.dt.date() <= self.args.date_end + }), + ); // Store result products.insert( diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 0c2977d..a152bbe 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -28,7 +28,7 @@ use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProduct use crate::transaction::{ update_balances_from_transactions, Posting, Transaction, TransactionWithPostings, }; -use crate::util::sofy_from_eofy; +use crate::util::{get_eofy, sofy_from_eofy}; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -37,9 +37,8 @@ use super::dynamic_report::{ }; use super::executor::ReportingExecutionError; use super::types::{ - BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProduct, - ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, - VoidArgs, + BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProductKind, + ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module @@ -113,6 +112,15 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { } } + fn requires(&self, _context: &ReportingContext) -> Vec { + // AllTransactionsExceptEarningsToEquity always depends on CombineOrdinaryTransactions at least + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: self.product_kinds[0], + args: self.args.clone(), + }] + } + fn execute( &self, _context: &ReportingContext, @@ -142,30 +150,11 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { } } - // No dependencies?! - store empty result - let product: Box = match self.product_kinds[0] { - ReportingProductKind::Transactions => Box::new(Transactions { - transactions: Vec::new(), - }), - ReportingProductKind::BalancesAt => Box::new(BalancesAt { - balances: HashMap::new(), - }), - ReportingProductKind::BalancesBetween => Box::new(BalancesBetween { - balances: HashMap::new(), - }), - ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptEarningsToEquity.Generic but no available dependencies to provide it"), - }; - - products.insert( - ReportingProductId { - name: self.id().name, - kind: product_kind, - args: self.args.clone(), - }, - product, + // No dependencies?! - this is likely a mistake + panic!( + "Requested {:?} but no available dependencies to provide it", + self.product_kinds[0] ); - - Ok(()) } } @@ -675,7 +664,7 @@ impl ReportingStep for CombineOrdinaryTransactions { } } -/// Transfer current year balances in income and expense accounts to the current year earnings equity account +/// Transfer year-to-date balances in income and expense accounts (as at the requested date) to the current year earnings equity account #[derive(Debug)] pub struct CurrentYearEarningsToEquity { pub args: DateArgs, @@ -718,13 +707,15 @@ impl ReportingStep for CurrentYearEarningsToEquity { } fn requires(&self, context: &ReportingContext) -> Vec { + let eofy_date = get_eofy(&self.args.date, &context.eofy_date); + // CurrentYearEarningsToEquity depends on AllTransactionsExceptEarningsToEquity vec![ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(context.eofy_date), - date_end: context.eofy_date.clone(), + date_start: sofy_from_eofy(eofy_date), + date_end: eofy_date, }), }] } @@ -736,14 +727,16 @@ impl ReportingStep for CurrentYearEarningsToEquity { _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { + let eofy_date = get_eofy(&self.args.date, &context.eofy_date); + // Get balances for this financial year let balances = products .get_or_err(&ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(context.eofy_date), - date_end: context.eofy_date.clone(), + date_start: sofy_from_eofy(eofy_date), + date_end: eofy_date, }), })? .downcast_ref::() @@ -767,7 +760,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { transactions.transactions.push(TransactionWithPostings { transaction: Transaction { id: None, - dt: context.eofy_date.and_hms_opt(0, 0, 0).unwrap(), + dt: eofy_date.and_hms_opt(0, 0, 0).unwrap(), description: "Current year earnings".to_string(), }, postings: vec![ @@ -985,15 +978,15 @@ impl ReportingStep for RetainedEarningsToEquity { } fn requires(&self, context: &ReportingContext) -> Vec { + let eofy_date = get_eofy(&self.args.date, &context.eofy_date); + let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap(); + // RetainedEarningsToEquity depends on CombineOrdinaryTransactions for last financial year vec![ReportingProductId { name: "CombineOrdinaryTransactions", kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: context - .eofy_date - .with_year(context.eofy_date.year() - 1) - .unwrap(), + date: last_eofy_date, }), }] } @@ -1005,12 +998,10 @@ impl ReportingStep for RetainedEarningsToEquity { _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { - // Get balances at end of last financial year - let last_eofy_date = context - .eofy_date - .with_year(context.eofy_date.year() - 1) - .unwrap(); + let eofy_date = get_eofy(&self.args.date, &context.eofy_date); + let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap(); + // Get balances at end of last financial year let balances_last_eofy = products .get_or_err(&ReportingProductId { name: "CombineOrdinaryTransactions", diff --git a/src/util.rs b/src/util.rs index 73f25a5..1d6102b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,9 +18,23 @@ use chrono::{Datelike, NaiveDate}; +/// Return the end date of the current financial year for the given date +pub fn get_eofy(date: &NaiveDate, eofy_date: &NaiveDate) -> NaiveDate { + let date_eofy = eofy_date.with_year(date.year()).unwrap(); + if date_eofy >= *date { + date_eofy + } else { + date_eofy.with_year(date_eofy.year() + 1).unwrap() + } +} + /// Return the start date of the financial year, given the end date of the financial year -pub fn sofy_from_eofy(date_eofy: NaiveDate) -> NaiveDate { - date_eofy.with_year(date_eofy.year() - 1).unwrap().succ_opt().unwrap() +pub fn sofy_from_eofy(eofy_date: NaiveDate) -> NaiveDate { + eofy_date + .with_year(eofy_date.year() - 1) + .unwrap() + .succ_opt() + .unwrap() } /// Format the [NaiveDate] as a string From eb3fbccc85f1492793a4b80ae27468d194628c5f Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 26 May 2025 23:07:18 +1000 Subject: [PATCH 31/45] Add ids for total rows in balance sheet --- src/reporting/steps.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index a152bbe..db9a3fb 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -388,7 +388,7 @@ impl ReportingStep for BalanceSheet { calculate_fn: |report| LiteralRow { text: "Total assets".to_string(), quantity: report.subtotal_for_id("assets"), - id: None, + id: Some("total_assets".to_string()), visible: true, auto_hide: false, link: None, @@ -412,7 +412,7 @@ impl ReportingStep for BalanceSheet { calculate_fn: |report| LiteralRow { text: "Total liabilities".to_string(), quantity: report.subtotal_for_id("liabilities"), - id: None, + id: Some("total_liabilities".to_string()), visible: true, auto_hide: false, link: None, @@ -436,7 +436,7 @@ impl ReportingStep for BalanceSheet { calculate_fn: |report| LiteralRow { text: "Total equity".to_string(), quantity: report.subtotal_for_id("equity"), - id: None, + id: Some("total_equity".to_string()), visible: true, auto_hide: false, link: None, From b8b2547aab22d50464cf6addc9de06147f47273b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 00:21:17 +1000 Subject: [PATCH 32/45] Refactor DynamicReport to use RefCell Allows calculations to refer to the results of previous calculations Rather than the same cloned DynamicReport being passed to all calculations --- src/reporting/dynamic_report.rs | 119 +++++++++++++++++++++++++------- src/reporting/steps.rs | 52 +++++++------- 2 files changed, 121 insertions(+), 50 deletions(-) diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index f08276a..7b036d8 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -16,6 +16,7 @@ along with this program. If not, see . */ +use std::cell::RefCell; use std::collections::HashMap; use serde::{Deserialize, Serialize}; @@ -29,13 +30,22 @@ use super::types::{GenericReportingProduct, ReportingProduct}; pub struct DynamicReport { pub title: String, pub columns: Vec, - pub entries: Vec, + // This must use RefCell as, during calculation, we iterate while mutating the report + pub entries: Vec>, } impl DynamicReport { + pub fn new(title: String, columns: Vec, entries: Vec) -> Self { + Self { + title, + columns, + entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), + } + } + /// Remove all entries from the report where auto_hide is enabled and quantity is zero pub fn auto_hide(&mut self) { - self.entries.retain_mut(|e| match e { + self.entries.retain(|e| match &mut *e.borrow_mut() { DynamicReportEntry::Section(section) => { section.auto_hide_children(); if section.can_auto_hide_self() { @@ -58,15 +68,26 @@ impl DynamicReport { /// Recursively calculate all [CalculatedRow] entries pub fn calculate(&mut self) { - // FIXME: This is for the borrow checker - can it be avoided? - let report_cloned = self.clone(); + for (entry_idx, entry) in self.entries.iter().enumerate() { + let entry_ref = entry.borrow(); - for entry in self.entries.iter_mut() { - match entry { - DynamicReportEntry::Section(section) => section.calculate(&report_cloned), + match &*entry_ref { + DynamicReportEntry::Section(section) => { + // Clone first, in case calculation needs to take reference to the section + let mut updated_section = section.clone(); + updated_section.calculate(&self); + + drop(entry_ref); // Drop entry_ref so we can borrow mutably + let mut entry_mut = self.entries[entry_idx].borrow_mut(); + *entry_mut = DynamicReportEntry::Section(updated_section); + } DynamicReportEntry::LiteralRow(_) => (), DynamicReportEntry::CalculatedRow(row) => { - *entry = DynamicReportEntry::LiteralRow(row.calculate(&report_cloned)); + let updated_row = row.calculate(&self); + + drop(entry_ref); // Drop entry_ref so we can borrow mutably + let mut entry_mut = self.entries[entry_idx].borrow_mut(); + *entry_mut = DynamicReportEntry::LiteralRow(updated_row); } DynamicReportEntry::Spacer => (), } @@ -74,13 +95,18 @@ impl DynamicReport { } /// Look up [DynamicReportEntry] by id - pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { + /// + /// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. + pub fn by_id(&self, id: &str) -> Option { + // Manually iterate over self.entries rather than self.entries() + // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { - match entry { + match entry.try_borrow() { + Ok(entry) => match &*entry { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { - return Some(entry); + return Some(entry.clone()); } } if let Some(e) = section.by_id(id) { @@ -90,12 +116,17 @@ impl DynamicReport { DynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { - return Some(entry); + return Some(entry.clone()); } } } DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), + }, + Err(err) => panic!( + "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", + err + ), } } @@ -136,12 +167,28 @@ pub struct Section { pub id: Option, pub visible: bool, pub auto_hide: bool, - pub entries: Vec, + pub entries: Vec>, } impl Section { + pub fn new( + text: String, + id: Option, + visible: bool, + auto_hide: bool, + entries: Vec, + ) -> Self { + Self { + text, + id, + visible, + auto_hide, + entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), + } + } + fn auto_hide_children(&mut self) { - self.entries.retain_mut(|e| match e { + self.entries.retain(|e| match &mut *e.borrow_mut() { DynamicReportEntry::Section(section) => { section.auto_hide_children(); if section.can_auto_hide_self() { @@ -164,7 +211,7 @@ impl Section { fn can_auto_hide_self(&self) -> bool { self.auto_hide - && self.entries.iter().all(|e| match e { + && self.entries.iter().all(|e| match &*e.borrow() { DynamicReportEntry::Section(section) => section.can_auto_hide_self(), DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), DynamicReportEntry::CalculatedRow(_) => false, @@ -174,12 +221,26 @@ impl Section { /// Recursively calculate all [CalculatedRow] entries pub fn calculate(&mut self, report: &DynamicReport) { - for entry in self.entries.iter_mut() { - match entry { - DynamicReportEntry::Section(section) => section.calculate(report), + for (entry_idx, entry) in self.entries.iter().enumerate() { + let entry_ref = entry.borrow(); + + match &*entry_ref { + DynamicReportEntry::Section(section) => { + // Clone first, in case calculation needs to take reference to the section + let mut updated_section = section.clone(); + updated_section.calculate(&report); + + drop(entry_ref); // Drop entry_ref so we can borrow mutably + let mut entry_mut = self.entries[entry_idx].borrow_mut(); + *entry_mut = DynamicReportEntry::Section(updated_section); + } DynamicReportEntry::LiteralRow(_) => (), DynamicReportEntry::CalculatedRow(row) => { - *entry = DynamicReportEntry::LiteralRow(row.calculate(report)) + let updated_row = row.calculate(&report); + + drop(entry_ref); // Drop entry_ref so we can borrow mutably + let mut entry_mut = self.entries[entry_idx].borrow_mut(); + *entry_mut = DynamicReportEntry::LiteralRow(updated_row); } DynamicReportEntry::Spacer => (), } @@ -187,13 +248,18 @@ impl Section { } /// Look up [DynamicReportEntry] by id - pub fn by_id(&self, id: &str) -> Option<&DynamicReportEntry> { + /// + /// Returns a cloned copy of the [DynamicReportEntry]. + pub fn by_id(&self, id: &str) -> Option { + // Manually iterate over self.entries rather than self.entries() + // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { - match entry { + match entry.try_borrow() { + Ok(entry) => match &*entry { DynamicReportEntry::Section(section) => { if let Some(i) = §ion.id { if i == id { - return Some(entry); + return Some(entry.clone()); } } if let Some(e) = section.by_id(id) { @@ -203,12 +269,17 @@ impl Section { DynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { - return Some(entry); + return Some(entry.clone()); } } } DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), + }, + Err(err) => panic!( + "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", + err + ), } } @@ -219,7 +290,7 @@ impl Section { pub fn subtotal(&self, report: &DynamicReport) -> Vec { let mut subtotals = vec![0; report.columns.len()]; for entry in self.entries.iter() { - match entry { + match &*entry.borrow() { DynamicReportEntry::Section(section) => { for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { subtotals[col_idx] += subtotal; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index db9a3fb..b5a2f70 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -372,16 +372,16 @@ impl ReportingStep for BalanceSheet { kinds_for_account(context.db_connection.get_account_configurations()); // Init report - let mut report = DynamicReport { - title: "Balance sheet".to_string(), - columns: self.args.dates.iter().map(|d| d.date.to_string()).collect(), - entries: vec![ - DynamicReportEntry::Section(Section { - text: "Assets".to_string(), - id: Some("assets".to_string()), - visible: true, - auto_hide: false, - entries: { + let mut report = DynamicReport::new( + "Balance sheet".to_string(), + self.args.dates.iter().map(|d| d.date.to_string()).collect(), + vec![ + DynamicReportEntry::Section(Section::new( + "Assets".to_string(), + Some("assets".to_string()), + true, + false, + { let mut entries = entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { @@ -398,14 +398,14 @@ impl ReportingStep for BalanceSheet { })); entries }, - }), + )), DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section { - text: "Liabilities".to_string(), - id: Some("liabilities".to_string()), - visible: true, - auto_hide: false, - entries: { + DynamicReportEntry::Section(Section::new( + "Liabilities".to_string(), + Some("liabilities".to_string()), + true, + false, + { let mut entries = entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { @@ -422,14 +422,14 @@ impl ReportingStep for BalanceSheet { })); entries }, - }), + )), DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section { - text: "Equity".to_string(), - id: Some("equity".to_string()), - visible: true, - auto_hide: false, - entries: { + DynamicReportEntry::Section(Section::new( + "Equity".to_string(), + Some("equity".to_string()), + true, + false, + { let mut entries = entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { @@ -446,9 +446,9 @@ impl ReportingStep for BalanceSheet { })); entries }, - }), + )), ], - }; + ); report.calculate(); report.auto_hide(); From 541467a2dac4949dc78633cd3bd50ca1353acf4f Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 00:21:30 +1000 Subject: [PATCH 33/45] Implement IncomeStatement step --- src/reporting/dynamic_report.rs | 62 ++++++----- src/reporting/steps.rs | 180 +++++++++++++++++++++++++++++++- 2 files changed, 214 insertions(+), 28 deletions(-) diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index 7b036d8..d1b25a6 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -103,25 +103,25 @@ impl DynamicReport { for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { - if let Some(i) = §ion.id { - if i == id { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); } } - if let Some(e) = section.by_id(id) { - return Some(e); - } - } - DynamicReportEntry::LiteralRow(row) => { - if let Some(i) = &row.id { - if i == id { + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { return Some(entry.clone()); + } } } - } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + DynamicReportEntry::CalculatedRow(_) => (), + DynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", @@ -143,6 +143,16 @@ impl DynamicReport { } } + // Return the quantities for the [LiteralRow] with the given id + pub fn quantity_for_id(&self, id: &str) -> Vec { + let entry = self.by_id(id).expect("Invalid id"); + if let DynamicReportEntry::LiteralRow(row) = entry { + row.quantity + } else { + panic!("Called quantity_for_id on non-LiteralRow"); + } + } + /// Serialise the report (as JSON) using serde pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap() @@ -256,25 +266,25 @@ impl Section { for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { - if let Some(i) = §ion.id { - if i == id { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); } } - if let Some(e) = section.by_id(id) { - return Some(e); - } - } - DynamicReportEntry::LiteralRow(row) => { - if let Some(i) = &row.id { - if i == id { + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { return Some(entry.clone()); + } } } - } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + DynamicReportEntry::CalculatedRow(_) => (), + DynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index b5a2f70..7febc34 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -37,8 +37,9 @@ use super::dynamic_report::{ }; use super::executor::ReportingExecutionError; use super::types::{ - BalancesBetween, DateArgs, MultipleDateArgs, ReportingContext, ReportingProductKind, - ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, + BalancesBetween, DateArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, ReportingContext, + ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, + VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module @@ -50,6 +51,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { CombineOrdinaryTransactions::register_lookup_fn(context); CurrentYearEarningsToEquity::register_lookup_fn(context); DBBalances::register_lookup_fn(context); + IncomeStatement::register_lookup_fn(context); PostUnreconciledStatementLines::register_lookup_fn(context); RetainedEarningsToEquity::register_lookup_fn(context); } @@ -867,6 +869,180 @@ impl ReportingStep for DBBalances { } } +/// Generates an income statement [DynamicReport] +#[derive(Debug)] +pub struct IncomeStatement { + pub args: MultipleDateStartDateEndArgs, +} + +impl IncomeStatement { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "IncomeStatement", + &[ReportingProductKind::Generic], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(IncomeStatement { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for IncomeStatement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +impl ReportingStep for IncomeStatement { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "IncomeStatement", + product_kinds: &[ReportingProductKind::Generic], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + let mut result = Vec::new(); + + // IncomeStatement depends on AllTransactionsExceptEarningsToEquity in each requested period + for date_args in self.args.dates.iter() { + result.push(ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(date_args.clone()), + }); + } + + result + } + + fn execute( + &self, + context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances for each period + let mut balances: Vec<&HashMap> = Vec::new(); + for date_args in self.args.dates.iter() { + let product = products.get_or_err(&ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(date_args.clone()), + })?; + + balances.push(&product.downcast_ref::().unwrap().balances); + } + + // Get names of all income statement accounts + let kinds_for_account = + kinds_for_account(context.db_connection.get_account_configurations()); + + // Init report + let mut report = DynamicReport::new( + "Income statement".to_string(), + self.args + .dates + .iter() + .map(|d| d.date_end.to_string()) + .collect(), + vec![ + DynamicReportEntry::Section(Section::new( + "Income".to_string(), + Some("income".to_string()), + true, + false, + { + let mut entries = + entries_for_kind("drcr.income", true, &balances, &kinds_for_account); + entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total income".to_string(), + quantity: report.subtotal_for_id("income"), + id: Some("total_income".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + })); + entries + }, + )), + DynamicReportEntry::Spacer, + DynamicReportEntry::Section(Section::new( + "Expenses".to_string(), + Some("expenses".to_string()), + true, + false, + { + let mut entries = + entries_for_kind("drcr.expense", false, &balances, &kinds_for_account); + entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total expenses".to_string(), + quantity: report.subtotal_for_id("expenses"), + id: Some("total_expenses".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + })); + entries + }, + )), + DynamicReportEntry::Spacer, + DynamicReportEntry::CalculatedRow(CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Net surplus (deficit)".to_string(), + quantity: report + .quantity_for_id("total_income") // Get total income row + .iter() + .zip(report.quantity_for_id("total_expenses").iter()) // Zip with total expenses row + .map(|(i, e)| i - e) // Compute net surplus + .collect(), + id: Some("net_surplus".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, + }), + ], + ); + + report.calculate(); + report.auto_hide(); + + // Store the result + products.insert( + ReportingProductId { + name: "IncomeStatement", + kind: ReportingProductKind::Generic, + args: Box::new(self.args.clone()), + }, + Box::new(report), + ); + + Ok(()) + } +} + /// Generate transactions for unreconciled statement lines #[derive(Debug)] pub struct PostUnreconciledStatementLines { From b111e7023c22bdf5383eae5b8e1d48cc77d6d499 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 00:53:26 +1000 Subject: [PATCH 34/45] Cache database metadata --- src/db.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/db.rs b/src/db.rs index 0e533fa..c977132 100644 --- a/src/db.rs +++ b/src/db.rs @@ -30,6 +30,7 @@ use crate::{util::format_date, QuantityInt}; pub struct DbConnection { sqlx_connection: RefCell, + metadata: DbMetadata, } fn run_blocking(future: F) -> F::Output { @@ -40,13 +41,21 @@ fn run_blocking(future: F) -> F::Output { impl DbConnection { /// Connect to the given Sqlite database pub fn connect(url: &str) -> Self { + run_blocking(DbConnection::connect_async(url)) + } + + async fn connect_async(url: &str) -> Self { + let mut connection = SqliteConnection::connect(url).await.expect("SQL error"); + let metadata = DbMetadata::from_database(&mut connection).await; + Self { - sqlx_connection: RefCell::new(run_blocking(Self::connect_async(url))), + sqlx_connection: RefCell::new(connection), + metadata, } } - async fn connect_async(url: &str) -> SqliteConnection { - SqliteConnection::connect(url).await.expect("SQL error") + pub fn metadata(&self) -> &DbMetadata { + &self.metadata } /// Get account balances from the database @@ -122,3 +131,58 @@ impl DbConnection { account_configurations } } + +/// Container for cached database-related metadata +pub struct DbMetadata { + pub version: u32, + pub eofy_date: NaiveDate, + pub reporting_commodity: String, + pub dps: u32, +} + +impl DbMetadata { + /// Initialise [DbMetadata] with values from the metadata database table + async fn from_database(connection: &mut SqliteConnection) -> Self { + let version = sqlx::query("SELECT value FROM metadata WHERE key = 'version'") + .map(|r: SqliteRow| { + r.get::(0) + .parse() + .expect("Invalid metadata.version") + }) + .fetch_one(&mut *connection) + .await + .expect("SQL error"); + + let eofy_date = sqlx::query("SELECT value FROM metadata WHERE key ='eofy_date'") + .map(|r: SqliteRow| { + NaiveDate::parse_from_str(r.get(0), "%Y-%m-%d").expect("Invalid metadata.eofy_date") + }) + .fetch_one(&mut *connection) + .await + .expect("SQL error"); + + let reporting_commodity = + sqlx::query("SELECT value FROM metadata WHERE key = 'reporting_commodity'") + .map(|r: SqliteRow| r.get(0)) + .fetch_one(&mut *connection) + .await + .expect("SQL error"); + + let dps = sqlx::query("SELECT value FROM metadata WHERE key = 'amount_dps'") + .map(|r: SqliteRow| { + r.get::(0) + .parse() + .expect("Invalid metadata.amount_dps") + }) + .fetch_one(&mut *connection) + .await + .expect("SQL error"); + + DbMetadata { + version, + eofy_date, + reporting_commodity, + dps, + } + } +} From 148390f030d802d7e5b854e3f463d2f37dc41e1b Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 14:29:27 +1000 Subject: [PATCH 35/45] Make reporting API async --- Cargo.lock | 12 + Cargo.toml | 1 + src/db.rs | 43 ++-- src/main.rs | 9 +- src/reporting/builders.rs | 63 ++++-- src/reporting/dynamic_report.rs | 387 ++++++++++++++++++++++---------- src/reporting/executor.rs | 24 +- src/reporting/mod.rs | 4 +- src/reporting/steps.rs | 307 ++++++++++++++----------- src/reporting/types.rs | 31 ++- 10 files changed, 553 insertions(+), 328 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9e4885..f81a1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -676,6 +687,7 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" name = "libdrcr" version = "0.1.0" dependencies = [ + "async-trait", "chrono", "downcast-rs", "dyn-clone", diff --git a/Cargo.toml b/Cargo.toml index d5bbea0..1546094 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +async-trait = "0.1.88" chrono = "0.4.41" downcast-rs = "2.0.1" dyn-clone = "1.0.19" diff --git a/src/db.rs b/src/db.rs index c977132..05d1595 100644 --- a/src/db.rs +++ b/src/db.rs @@ -17,39 +17,26 @@ */ use std::collections::HashMap; -use std::ops::DerefMut; -use std::{cell::RefCell, future::Future}; use chrono::NaiveDate; use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; -use tokio::runtime::Runtime; use crate::account_config::AccountConfiguration; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { - sqlx_connection: RefCell, + url: String, metadata: DbMetadata, } -fn run_blocking(future: F) -> F::Output { - let rt = Runtime::new().unwrap(); - rt.block_on(future) -} - impl DbConnection { - /// Connect to the given Sqlite database - pub fn connect(url: &str) -> Self { - run_blocking(DbConnection::connect_async(url)) - } - - async fn connect_async(url: &str) -> Self { + pub async fn new(url: &str) -> Self { let mut connection = SqliteConnection::connect(url).await.expect("SQL error"); let metadata = DbMetadata::from_database(&mut connection).await; Self { - sqlx_connection: RefCell::new(connection), + url: url.to_string(), metadata, } } @@ -58,13 +45,15 @@ impl DbConnection { &self.metadata } - /// Get account balances from the database - pub fn get_balances(&self, date: NaiveDate) -> HashMap { - run_blocking(self.get_balances_async(date)) + pub async fn connect(&self) -> SqliteConnection { + SqliteConnection::connect(&self.url) + .await + .expect("SQL error") } - async fn get_balances_async(&self, date: NaiveDate) -> HashMap { - let mut connection = self.sqlx_connection.borrow_mut(); + /// Get account balances from the database + pub async fn get_balances(&self, date: NaiveDate) -> HashMap { + let mut connection = self.connect().await; let rows = sqlx::query( "-- Get last transaction for each account @@ -84,7 +73,7 @@ impl DbConnection { SELECT max_tid_by_account.account, running_balance AS quantity FROM max_tid_by_account JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account" - ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error"); + ).bind(format_date(date)).fetch_all(&mut connection).await.expect("SQL error"); let mut balances = HashMap::new(); for row in rows { @@ -95,12 +84,8 @@ impl DbConnection { } /// Get account configurations from the database - pub fn get_account_configurations(&self) -> Vec { - run_blocking(self.get_account_configurations_async()) - } - - async fn get_account_configurations_async(&self) -> Vec { - let mut connection = self.sqlx_connection.borrow_mut(); + pub async fn get_account_configurations(&self) -> Vec { + let mut connection = self.connect().await; let mut account_configurations = sqlx::query("SELECT id, account, kind, data FROM account_configurations") @@ -110,7 +95,7 @@ impl DbConnection { kind: r.get("kind"), data: r.get("data"), }) - .fetch_all(connection.deref_mut()) + .fetch_all(&mut connection) .await .expect("SQL error"); diff --git a/src/main.rs b/src/main.rs index 27da957..2037235 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,11 +28,12 @@ use libdrcr::reporting::types::{ ReportingProductKind, VoidArgs, }; -fn main() { +#[tokio::main] +async fn main() { const YEAR: i32 = 2023; // Connect to database - let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); + let db_connection = DbConnection::new("sqlite:drcr_testing.db").await; // Initialise ReportingContext let mut context = ReportingContext::new( @@ -85,7 +86,7 @@ fn main() { }, ]; - let products = generate_report(targets, &context).unwrap(); + let products = generate_report(targets, &context).await.unwrap(); let result = products .get_or_err(&ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", @@ -119,7 +120,7 @@ fn main() { }, ]; - let products = generate_report(targets, &context).unwrap(); + let products = generate_report(targets, &context).await.unwrap(); let result = products .get_or_err(&ReportingProductId { name: "BalanceSheet", diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index d6a5e8b..1f1e860 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -23,6 +23,9 @@ use std::collections::HashMap; use std::fmt::Display; +use async_trait::async_trait; +use tokio::sync::RwLock; + use crate::transaction::update_balances_from_transactions; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; @@ -124,6 +127,7 @@ impl Display for BalancesAtToBalancesBetween { } } +#[async_trait] impl ReportingStep for BalancesAtToBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -153,13 +157,15 @@ impl ReportingStep for BalancesAtToBalancesBetween { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances at dates let balances_start = &products .get_or_err(&ReportingProductId { @@ -196,7 +202,8 @@ impl ReportingStep for BalancesAtToBalancesBetween { } // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesBetween, @@ -204,8 +211,7 @@ impl ReportingStep for BalancesAtToBalancesBetween { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -285,6 +291,7 @@ impl Display for GenerateBalances { } } +#[async_trait] impl ReportingStep for GenerateBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -303,13 +310,15 @@ impl ReportingStep for GenerateBalances { }] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get the transactions let transactions = &products .get_or_err(&ReportingProductId { @@ -328,7 +337,8 @@ impl ReportingStep for GenerateBalances { update_balances_from_transactions(&mut balances.balances, transactions.iter()); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -336,8 +346,7 @@ impl ReportingStep for GenerateBalances { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -441,6 +450,7 @@ impl Display for UpdateBalancesAt { } } +#[async_trait] impl ReportingStep for UpdateBalancesAt { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -499,13 +509,15 @@ impl ReportingStep for UpdateBalancesAt { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Look up the parent step, so we can extract the appropriate args let parent_step = steps .iter() @@ -566,7 +578,8 @@ impl ReportingStep for UpdateBalancesAt { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, @@ -574,8 +587,7 @@ impl ReportingStep for UpdateBalancesAt { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -646,6 +658,7 @@ impl Display for UpdateBalancesBetween { } } +#[async_trait] impl ReportingStep for UpdateBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -683,13 +696,15 @@ impl ReportingStep for UpdateBalancesBetween { ); } - fn execute( + async fn execute( &self, _context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Look up the parent step, so we can extract the appropriate args let parent_step = steps .iter() @@ -736,7 +751,8 @@ impl ReportingStep for UpdateBalancesBetween { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesBetween, @@ -744,7 +760,6 @@ impl ReportingStep for UpdateBalancesBetween { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index d1b25a6..82ef0ca 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +// FIXME: Tidy up this file + use std::cell::RefCell; use std::collections::HashMap; @@ -25,17 +27,21 @@ use crate::QuantityInt; use super::types::{GenericReportingProduct, ReportingProduct}; -/// Represents a dynamically generated report composed of [DynamicReportEntry] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct DynamicReport { +/// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] +#[derive(Clone, Debug)] +pub struct CalculatableDynamicReport { pub title: String, pub columns: Vec, // This must use RefCell as, during calculation, we iterate while mutating the report - pub entries: Vec>, + pub entries: Vec>, } -impl DynamicReport { - pub fn new(title: String, columns: Vec, entries: Vec) -> Self { +impl CalculatableDynamicReport { + pub fn new( + title: String, + columns: Vec, + entries: Vec, + ) -> Self { Self { title, columns, @@ -43,67 +49,60 @@ impl DynamicReport { } } - /// Remove all entries from the report where auto_hide is enabled and quantity is zero - pub fn auto_hide(&mut self) { - self.entries.retain(|e| match &mut *e.borrow_mut() { - DynamicReportEntry::Section(section) => { - section.auto_hide_children(); - if section.can_auto_hide_self() { - false - } else { - true - } - } - DynamicReportEntry::LiteralRow(row) => { - if row.can_auto_hide() { - false - } else { - true - } - } - DynamicReportEntry::CalculatedRow(_) => true, - DynamicReportEntry::Spacer => true, - }); - } - /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(&mut self) { + pub fn calculate(self) -> DynamicReport { + let mut calculated_entries = Vec::new(); + for (entry_idx, entry) in self.entries.iter().enumerate() { let entry_ref = entry.borrow(); match &*entry_ref { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { // Clone first, in case calculation needs to take reference to the section - let mut updated_section = section.clone(); - updated_section.calculate(&self); + let updated_section = section.clone().calculate(&self); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::Section(updated_section); + *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); + + calculated_entries.push(DynamicReportEntry::Section(updated_section)); } - DynamicReportEntry::LiteralRow(_) => (), - DynamicReportEntry::CalculatedRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + calculated_entries.push(DynamicReportEntry::Section(section.clone())); + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); + } + CalculatableDynamicReportEntry::CalculatedRow(row) => { let updated_row = row.calculate(&self); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::LiteralRow(updated_row); + *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); + + calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); } - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::Spacer => (), } } + + DynamicReport { + title: self.title, + columns: self.columns, + entries: calculated_entries, + } } - /// Look up [DynamicReportEntry] by id + /// Look up [CalculatableDynamicReportEntry] by id /// - /// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. - pub fn by_id(&self, id: &str) -> Option { + /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. + pub fn by_id(&self, id: &str) -> Option { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry.clone()); @@ -113,15 +112,35 @@ impl DynamicReport { return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(match e { + DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::Section(section.clone()) + } + DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::LiteralRow(row.clone()) + } + DynamicReportEntry::Spacer => { + CalculatableDynamicReportEntry::Spacer + } + }); + } + } + CalculatableDynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry.clone()); } } } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", @@ -136,7 +155,7 @@ impl DynamicReport { /// Calculate the subtotals for the [Section] with the given id pub fn subtotal_for_id(&self, id: &str) -> Vec { let entry = self.by_id(id).expect("Invalid id"); - if let DynamicReportEntry::Section(section) = entry { + if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry { section.subtotal(&self) } else { panic!("Called subtotal_for_id on non-Section"); @@ -146,59 +165,26 @@ impl DynamicReport { // Return the quantities for the [LiteralRow] with the given id pub fn quantity_for_id(&self, id: &str) -> Vec { let entry = self.by_id(id).expect("Invalid id"); - if let DynamicReportEntry::LiteralRow(row) = entry { + if let CalculatableDynamicReportEntry::LiteralRow(row) = entry { row.quantity } else { panic!("Called quantity_for_id on non-LiteralRow"); } } - - /// Serialise the report (as JSON) using serde - pub fn to_json(&self) -> String { - serde_json::to_string(self).unwrap() - } } -impl GenericReportingProduct for DynamicReport {} -impl ReportingProduct for DynamicReport {} - +/// Represents a dynamically generated report composed of [DynamicReportEntry], with no [CalculatedRow]s #[derive(Clone, Debug, Deserialize, Serialize)] -pub enum DynamicReportEntry { - Section(Section), - LiteralRow(LiteralRow), - #[serde(skip)] - CalculatedRow(CalculatedRow), - Spacer, +pub struct DynamicReport { + pub title: String, + pub columns: Vec, + pub entries: Vec, } -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Section { - pub text: String, - pub id: Option, - pub visible: bool, - pub auto_hide: bool, - pub entries: Vec>, -} - -impl Section { - pub fn new( - text: String, - id: Option, - visible: bool, - auto_hide: bool, - entries: Vec, - ) -> Self { - Self { - text, - id, - visible, - auto_hide, - entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), - } - } - - fn auto_hide_children(&mut self) { - self.entries.retain(|e| match &mut *e.borrow_mut() { +impl DynamicReport { + /// Remove all entries from the report where auto_hide is enabled and quantity is zero + pub fn auto_hide(&mut self) { + self.entries.retain_mut(|e| match e { DynamicReportEntry::Section(section) => { section.auto_hide_children(); if section.can_auto_hide_self() { @@ -214,59 +200,116 @@ impl Section { true } } - DynamicReportEntry::CalculatedRow(_) => true, DynamicReportEntry::Spacer => true, }); } - fn can_auto_hide_self(&self) -> bool { - self.auto_hide - && self.entries.iter().all(|e| match &*e.borrow() { - DynamicReportEntry::Section(section) => section.can_auto_hide_self(), - DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), - DynamicReportEntry::CalculatedRow(_) => false, - DynamicReportEntry::Spacer => true, - }) + /// Serialise the report (as JSON) using serde + pub fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl GenericReportingProduct for DynamicReport {} +impl ReportingProduct for DynamicReport {} + +#[derive(Clone, Debug)] +pub enum CalculatableDynamicReportEntry { + CalculatableSection(CalculatableSection), + Section(Section), + LiteralRow(LiteralRow), + CalculatedRow(CalculatedRow), + Spacer, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub enum DynamicReportEntry { + Section(Section), + LiteralRow(LiteralRow), + Spacer, +} + +#[derive(Clone, Debug)] +pub struct CalculatableSection { + pub text: String, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub entries: Vec>, +} + +impl CalculatableSection { + pub fn new( + text: String, + id: Option, + visible: bool, + auto_hide: bool, + entries: Vec, + ) -> Self { + Self { + text, + id, + visible, + auto_hide, + entries: entries.into_iter().map(|e| RefCell::new(e)).collect(), + } } /// Recursively calculate all [CalculatedRow] entries - pub fn calculate(&mut self, report: &DynamicReport) { + pub fn calculate(&mut self, report: &CalculatableDynamicReport) -> Section { + let mut calculated_entries = Vec::new(); + for (entry_idx, entry) in self.entries.iter().enumerate() { let entry_ref = entry.borrow(); match &*entry_ref { - DynamicReportEntry::Section(section) => { - // Clone first, in case calculation needs to take reference to the section - let mut updated_section = section.clone(); - updated_section.calculate(&report); + CalculatableDynamicReportEntry::CalculatableSection(section) => { + let updated_section = section.clone().calculate(&report); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::Section(updated_section); + *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone()); + + calculated_entries.push(DynamicReportEntry::Section(updated_section)); } - DynamicReportEntry::LiteralRow(_) => (), - DynamicReportEntry::CalculatedRow(row) => { + CalculatableDynamicReportEntry::Section(section) => { + calculated_entries.push(DynamicReportEntry::Section(section.clone())); + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone())); + } + CalculatableDynamicReportEntry::CalculatedRow(row) => { let updated_row = row.calculate(&report); drop(entry_ref); // Drop entry_ref so we can borrow mutably let mut entry_mut = self.entries[entry_idx].borrow_mut(); - *entry_mut = DynamicReportEntry::LiteralRow(updated_row); + *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone()); + + calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); } - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::Spacer => (), } } + + Section { + text: self.text.clone(), + id: self.id.clone(), + visible: self.visible, + auto_hide: self.auto_hide, + entries: calculated_entries, + } } - /// Look up [DynamicReportEntry] by id + /// Look up [CalculatableDynamicReportEntry] by id /// - /// Returns a cloned copy of the [DynamicReportEntry]. - pub fn by_id(&self, id: &str) -> Option { + /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. + pub fn by_id(&self, id: &str) -> Option { // Manually iterate over self.entries rather than self.entries() // To catch the situation where entry is already mutably borrowed for entry in self.entries.iter() { match entry.try_borrow() { Ok(entry) => match &*entry { - DynamicReportEntry::Section(section) => { + CalculatableDynamicReportEntry::CalculatableSection(section) => { if let Some(i) = §ion.id { if i == id { return Some(entry.clone()); @@ -276,15 +319,16 @@ impl Section { return Some(e); } } - DynamicReportEntry::LiteralRow(row) => { + CalculatableDynamicReportEntry::Section(_) => todo!(), + CalculatableDynamicReportEntry::LiteralRow(row) => { if let Some(i) = &row.id { if i == id { return Some(entry.clone()); } } } - DynamicReportEntry::CalculatedRow(_) => (), - DynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), }, Err(err) => panic!( "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", @@ -296,11 +340,111 @@ impl Section { None } - /// Calculate the subtotals for this [Section] - pub fn subtotal(&self, report: &DynamicReport) -> Vec { + /// Calculate the subtotals for this [CalculatableSection] + pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { let mut subtotals = vec![0; report.columns.len()]; for entry in self.entries.iter() { match &*entry.borrow() { + CalculatableDynamicReportEntry::CalculatableSection(section) => { + for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::Section(section) => { + for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::LiteralRow(row) => { + for (col_idx, subtotal) in row.quantity.iter().enumerate() { + subtotals[col_idx] += subtotal; + } + } + CalculatableDynamicReportEntry::CalculatedRow(_) => (), + CalculatableDynamicReportEntry::Spacer => (), + } + } + subtotals + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Section { + pub text: String, + pub id: Option, + pub visible: bool, + pub auto_hide: bool, + pub entries: Vec, +} + +impl Section { + fn auto_hide_children(&mut self) { + self.entries.retain_mut(|e| match e { + DynamicReportEntry::Section(section) => { + section.auto_hide_children(); + if section.can_auto_hide_self() { + false + } else { + true + } + } + DynamicReportEntry::LiteralRow(row) => { + if row.can_auto_hide() { + false + } else { + true + } + } + DynamicReportEntry::Spacer => true, + }); + } + + fn can_auto_hide_self(&self) -> bool { + self.auto_hide + && self.entries.iter().all(|e| match e { + DynamicReportEntry::Section(section) => section.can_auto_hide_self(), + DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), + DynamicReportEntry::Spacer => true, + }) + } + + /// Look up [DynamicReportEntry] by id + /// + /// Returns a cloned copy of the [DynamicReportEntry]. + pub fn by_id(&self, id: &str) -> Option { + // Manually iterate over self.entries rather than self.entries() + // To catch the situation where entry is already mutably borrowed + for entry in self.entries.iter() { + match entry { + DynamicReportEntry::Section(section) => { + if let Some(i) = §ion.id { + if i == id { + return Some(entry.clone()); + } + } + if let Some(e) = section.by_id(id) { + return Some(e); + } + } + DynamicReportEntry::LiteralRow(row) => { + if let Some(i) = &row.id { + if i == id { + return Some(entry.clone()); + } + } + } + DynamicReportEntry::Spacer => (), + } + } + + None + } + + /// Calculate the subtotals for this [Section] + pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec { + let mut subtotals = vec![0; report.columns.len()]; + for entry in self.entries.iter() { + match entry { DynamicReportEntry::Section(section) => { for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { subtotals[col_idx] += subtotal; @@ -311,7 +455,6 @@ impl Section { subtotals[col_idx] += subtotal; } } - DynamicReportEntry::CalculatedRow(_) => (), DynamicReportEntry::Spacer => (), } } @@ -341,7 +484,7 @@ impl LiteralRow { #[derive(Clone, Debug)] pub struct CalculatedRow { //pub text: String, - pub calculate_fn: fn(report: &DynamicReport) -> LiteralRow, + pub calculate_fn: fn(report: &CalculatableDynamicReport) -> LiteralRow, //pub id: Option, //pub visible: bool, //pub auto_hide: bool, @@ -351,7 +494,7 @@ pub struct CalculatedRow { } impl CalculatedRow { - fn calculate(&self, report: &DynamicReport) -> LiteralRow { + fn calculate(&self, report: &CalculatableDynamicReport) -> LiteralRow { (self.calculate_fn)(report) } } @@ -361,7 +504,7 @@ pub fn entries_for_kind( invert: bool, balances: &Vec<&HashMap>, kinds_for_account: &HashMap>, -) -> Vec { +) -> Vec { // Get accounts of specified kind let mut accounts = kinds_for_account .iter() @@ -393,7 +536,7 @@ pub fn entries_for_kind( heading: false, bordered: false, }; - entries.push(DynamicReportEntry::LiteralRow(entry)); + entries.push(CalculatableDynamicReportEntry::LiteralRow(entry)); } entries diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index 5c9dd4d..decc709 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,23 +16,35 @@ along with this program. If not, see . */ -use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; +use tokio::sync::RwLock; + +use super::{ + calculator::ReportingGraphDependencies, + types::{ReportingContext, ReportingProducts, ReportingStep}, +}; #[derive(Debug)] pub enum ReportingExecutionError { - DependencyNotAvailable { message: String } + DependencyNotAvailable { message: String }, } -pub fn execute_steps( +pub async fn execute_steps( steps: Vec>, dependencies: ReportingGraphDependencies, context: &ReportingContext, ) -> Result { - let mut products = ReportingProducts::new(); + let products = RwLock::new(ReportingProducts::new()); for step in steps.iter() { - step.execute(context, &steps, &dependencies, &mut products)?; + // Execute the step + // TODO: Do this in parallel + let mut new_products = step + .execute(context, &steps, &dependencies, &products) + .await?; + + // Insert the new products + products.write().await.append(&mut new_products); } - Ok(products) + Ok(products.into_inner()) } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 1ea2e13..9214bd3 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -48,7 +48,7 @@ impl From for ReportingError { /// Calculate the steps required to generate the requested [ReportingProductId]s and then execute them /// /// Helper function to call [steps_for_targets] followed by [execute_steps]. -pub fn generate_report( +pub async fn generate_report( targets: Vec, context: &ReportingContext, ) -> Result { @@ -56,7 +56,7 @@ pub fn generate_report( let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; // Execute steps - let products = execute_steps(sorted_steps, dependencies, context)?; + let products = execute_steps(sorted_steps, dependencies, context).await?; Ok(products) } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 7febc34..d836b9c 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -21,7 +21,9 @@ use std::collections::HashMap; use std::fmt::Display; +use async_trait::async_trait; use chrono::Datelike; +use tokio::sync::RwLock; use crate::account_config::kinds_for_account; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; @@ -33,7 +35,8 @@ use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; use super::dynamic_report::{ - entries_for_kind, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, Section, + entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, + CalculatableSection, CalculatedRow, LiteralRow, }; use super::executor::ReportingExecutionError; use super::types::{ @@ -105,6 +108,7 @@ impl Display for AllTransactionsExceptEarningsToEquity { } } +#[async_trait] impl ReportingStep for AllTransactionsExceptEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -123,13 +127,15 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { }] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get all dependencies let step_dependencies = dependencies.dependencies_for_step(&self.id()); @@ -139,7 +145,8 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { for (product_id, product) in products.map().iter().rev() { if step_dependencies.iter().any(|d| d.product == *product_id) { // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: product_kind, @@ -147,8 +154,7 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { }, product.clone(), ); - - return Ok(()); + return Ok(result); } } @@ -197,6 +203,7 @@ impl Display for AllTransactionsIncludingEarningsToEquity { } } +#[async_trait] impl ReportingStep for AllTransactionsIncludingEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -229,13 +236,15 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get opening balances from AllTransactionsExceptEarningsToEquity let opening_balances = products .get_or_err(&ReportingProductId { @@ -280,7 +289,8 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { ); // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -288,8 +298,7 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -326,6 +335,7 @@ impl Display for BalanceSheet { } } +#[async_trait] impl ReportingStep for BalanceSheet { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -350,13 +360,15 @@ impl ReportingStep for BalanceSheet { result } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances for each period let mut balances: Vec<&HashMap> = Vec::new(); for date_args in self.args.dates.iter() { @@ -371,14 +383,14 @@ impl ReportingStep for BalanceSheet { // Get names of all balance sheet accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let mut report = DynamicReport::new( + let report = CalculatableDynamicReport::new( "Balance sheet".to_string(), self.args.dates.iter().map(|d| d.date.to_string()).collect(), vec![ - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Assets".to_string(), Some("assets".to_string()), true, @@ -386,23 +398,25 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total assets".to_string(), - quantity: report.subtotal_for_id("assets"), - id: Some("total_assets".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total assets".to_string(), + quantity: report.subtotal_for_id("assets"), + id: Some("total_assets".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Liabilities".to_string(), Some("liabilities".to_string()), true, @@ -410,23 +424,25 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total liabilities".to_string(), - quantity: report.subtotal_for_id("liabilities"), - id: Some("total_liabilities".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total liabilities".to_string(), + quantity: report.subtotal_for_id("liabilities"), + id: Some("total_liabilities".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Equity".to_string(), Some("equity".to_string()), true, @@ -434,29 +450,32 @@ impl ReportingStep for BalanceSheet { { let mut entries = entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total equity".to_string(), - quantity: report.subtotal_for_id("equity"), - id: Some("total_equity".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total equity".to_string(), + quantity: report.subtotal_for_id("equity"), + id: Some("total_equity".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), ], ); - report.calculate(); + let mut report = report.calculate(); report.auto_hide(); // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: "BalanceSheet", kind: ReportingProductKind::Generic, @@ -464,8 +483,7 @@ impl ReportingStep for BalanceSheet { }, Box::new(report), ); - - Ok(()) + Ok(result) } } @@ -498,6 +516,7 @@ impl Display for CalculateIncomeTax { } } +#[async_trait] impl ReportingStep for CalculateIncomeTax { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -540,20 +559,21 @@ impl ReportingStep for CalculateIncomeTax { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { eprintln!("Stub: CalculateIncomeTax.execute"); let transactions = Transactions { transactions: Vec::new(), }; - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -561,8 +581,7 @@ impl ReportingStep for CalculateIncomeTax { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -601,6 +620,7 @@ impl Display for CombineOrdinaryTransactions { } } +#[async_trait] impl ReportingStep for CombineOrdinaryTransactions { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -627,13 +647,15 @@ impl ReportingStep for CombineOrdinaryTransactions { ] } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Sum balances of all dependencies let mut balances = BalancesAt { @@ -653,7 +675,8 @@ impl ReportingStep for CombineOrdinaryTransactions { } // Store result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -661,8 +684,7 @@ impl ReportingStep for CombineOrdinaryTransactions { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -699,6 +721,7 @@ impl Display for CurrentYearEarningsToEquity { } } +#[async_trait] impl ReportingStep for CurrentYearEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -722,13 +745,14 @@ impl ReportingStep for CurrentYearEarningsToEquity { }] } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; let eofy_date = get_eofy(&self.args.date, &context.eofy_date); // Get balances for this financial year @@ -746,7 +770,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { // Get income and expense accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Transfer income and expense balances to current year earnings let mut transactions = Transactions { @@ -789,7 +813,8 @@ impl ReportingStep for CurrentYearEarningsToEquity { } // Store product - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -797,8 +822,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -835,6 +859,7 @@ impl Display for DBBalances { } } +#[async_trait] impl ReportingStep for DBBalances { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -844,19 +869,21 @@ impl ReportingStep for DBBalances { } } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { // Get balances from DB let balances = BalancesAt { - balances: context.db_connection.get_balances(self.args.date), + balances: context.db_connection.get_balances(self.args.date).await, }; - products.insert( + // Store result + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::BalancesAt, @@ -864,8 +891,7 @@ impl ReportingStep for DBBalances { }, Box::new(balances), ); - - Ok(()) + Ok(result) } } @@ -902,6 +928,7 @@ impl Display for IncomeStatement { } } +#[async_trait] impl ReportingStep for IncomeStatement { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -926,13 +953,15 @@ impl ReportingStep for IncomeStatement { result } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; + // Get balances for each period let mut balances: Vec<&HashMap> = Vec::new(); for date_args in self.args.dates.iter() { @@ -947,10 +976,10 @@ impl ReportingStep for IncomeStatement { // Get names of all income statement accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Init report - let mut report = DynamicReport::new( + let report = CalculatableDynamicReport::new( "Income statement".to_string(), self.args .dates @@ -958,7 +987,7 @@ impl ReportingStep for IncomeStatement { .map(|d| d.date_end.to_string()) .collect(), vec![ - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Income".to_string(), Some("income".to_string()), true, @@ -966,23 +995,25 @@ impl ReportingStep for IncomeStatement { { let mut entries = entries_for_kind("drcr.income", true, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total income".to_string(), - quantity: report.subtotal_for_id("income"), - id: Some("total_income".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total income".to_string(), + quantity: report.subtotal_for_id("income"), + id: Some("total_income".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::Section(Section::new( + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new( "Expenses".to_string(), Some("expenses".to_string()), true, @@ -990,23 +1021,25 @@ impl ReportingStep for IncomeStatement { { let mut entries = entries_for_kind("drcr.expense", false, &balances, &kinds_for_account); - entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { - calculate_fn: |report| LiteralRow { - text: "Total expenses".to_string(), - quantity: report.subtotal_for_id("expenses"), - id: Some("total_expenses".to_string()), - visible: true, - auto_hide: false, - link: None, - heading: true, - bordered: true, + entries.push(CalculatableDynamicReportEntry::CalculatedRow( + CalculatedRow { + calculate_fn: |report| LiteralRow { + text: "Total expenses".to_string(), + quantity: report.subtotal_for_id("expenses"), + id: Some("total_expenses".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + }, }, - })); + )); entries }, )), - DynamicReportEntry::Spacer, - DynamicReportEntry::CalculatedRow(CalculatedRow { + CalculatableDynamicReportEntry::Spacer, + CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow { calculate_fn: |report| LiteralRow { text: "Net surplus (deficit)".to_string(), quantity: report @@ -1026,11 +1059,12 @@ impl ReportingStep for IncomeStatement { ], ); - report.calculate(); + let mut report = report.calculate(); report.auto_hide(); // Store the result - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: "IncomeStatement", kind: ReportingProductKind::Generic, @@ -1038,8 +1072,7 @@ impl ReportingStep for IncomeStatement { }, Box::new(report), ); - - Ok(()) + Ok(result) } } @@ -1076,6 +1109,7 @@ impl Display for PostUnreconciledStatementLines { } } +#[async_trait] impl ReportingStep for PostUnreconciledStatementLines { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -1085,20 +1119,22 @@ impl ReportingStep for PostUnreconciledStatementLines { } } - fn execute( + async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + _products: &RwLock, + ) -> Result { eprintln!("Stub: PostUnreconciledStatementLines.execute"); let transactions = Transactions { transactions: Vec::new(), }; - products.insert( + // Store result + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -1106,8 +1142,7 @@ impl ReportingStep for PostUnreconciledStatementLines { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } @@ -1144,6 +1179,7 @@ impl Display for RetainedEarningsToEquity { } } +#[async_trait] impl ReportingStep for RetainedEarningsToEquity { fn id(&self) -> ReportingStepId { ReportingStepId { @@ -1167,13 +1203,14 @@ impl ReportingStep for RetainedEarningsToEquity { }] } - fn execute( + async fn execute( &self, context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { + let products = products.read().await; let eofy_date = get_eofy(&self.args.date, &context.eofy_date); let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap(); @@ -1191,7 +1228,7 @@ impl ReportingStep for RetainedEarningsToEquity { // Get income and expense accounts let kinds_for_account = - kinds_for_account(context.db_connection.get_account_configurations()); + kinds_for_account(context.db_connection.get_account_configurations().await); // Transfer income and expense balances to retained earnings let mut transactions = Transactions { @@ -1234,7 +1271,8 @@ impl ReportingStep for RetainedEarningsToEquity { } // Store product - products.insert( + let mut result = ReportingProducts::new(); + result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, @@ -1242,7 +1280,6 @@ impl ReportingStep for RetainedEarningsToEquity { }, Box::new(transactions), ); - - Ok(()) + Ok(result) } } diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 1cbfb95..d1e6428 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -20,12 +20,14 @@ use std::collections::HashMap; use std::fmt::{Debug, Display}; use std::hash::Hash; +use async_trait::async_trait; use chrono::NaiveDate; use downcast_rs::Downcast; use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; use indexmap::IndexMap; +use tokio::sync::RwLock; use crate::db::DbConnection; use crate::transaction::TransactionWithPostings; @@ -159,7 +161,7 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] -pub trait ReportingProduct: Debug + Downcast + DynClone {} +pub trait ReportingProduct: Debug + Downcast + DynClone + Send + Sync {} downcast_rs::impl_downcast!(ReportingProduct); dyn_clone::clone_trait_object!(ReportingProduct); @@ -205,14 +207,25 @@ impl ReportingProducts { } } + /// Returns a reference to the underlying [IndexMap] pub fn map(&self) -> &IndexMap> { &self.map } + /// Insert a key-value pair in the map + /// + /// See [IndexMap::insert]. pub fn insert(&mut self, key: ReportingProductId, value: Box) { self.map.insert(key, value); } + /// Moves all key-value pairs from `other` into `self`, leaving `other` empty + /// + /// See [IndexMap::append]. + pub fn append(&mut self, other: &mut ReportingProducts) { + self.map.append(&mut other.map); + } + pub fn get_or_err( &self, key: &ReportingProductId, @@ -260,7 +273,8 @@ impl Display for ReportingStepId { } /// Represents a step in a reporting job -pub trait ReportingStep: Debug + Display + Downcast { +#[async_trait] +pub trait ReportingStep: Debug + Display + Downcast + Send + Sync { /// Get the [ReportingStepId] for this [ReportingStep] fn id(&self) -> ReportingStepId; @@ -293,14 +307,16 @@ pub trait ReportingStep: Debug + Display + Downcast { } /// Called to generate the [ReportingProduct] for this [ReportingStep] + /// + /// Returns a [ReportingProducts] containing (only) the new [ReportingProduct]s. #[allow(unused_variables)] - fn execute( + async fn execute( &self, context: &ReportingContext, steps: &Vec>, dependencies: &ReportingGraphDependencies, - products: &mut ReportingProducts, - ) -> Result<(), ReportingExecutionError> { + products: &RwLock, + ) -> Result { todo!("{}", self); } } @@ -311,7 +327,10 @@ downcast_rs::impl_downcast!(ReportingStep); // REPORTING STEP ARGUMENTS /// Represents arguments to a [ReportingStep] -pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {} +pub trait ReportingStepArgs: + Debug + Display + Downcast + DynClone + DynEq + DynHash + Send + Sync +{ +} downcast_rs::impl_downcast!(ReportingStepArgs); dyn_clone::clone_trait_object!(ReportingStepArgs); From 0518163c3efe35872a4b8c5831fcbdbd45f38ba3 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 16:16:15 +1000 Subject: [PATCH 36/45] Sanity check reporting products --- src/reporting/executor.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index decc709..c8830e8 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -42,6 +42,31 @@ pub async fn execute_steps( .execute(context, &steps, &dependencies, &products) .await?; + // Sanity check the new products + for (product_id, _product) in new_products.map().iter() { + if product_id.name != step.id().name { + panic!( + "Unexpected product name {} from step {}", + product_id, + step.id() + ); + } + if !step.id().product_kinds.contains(&product_id.kind) { + panic!( + "Unexpected product kind {} from step {}", + product_id, + step.id() + ); + } + if product_id.args != step.id().args { + panic!( + "Unexpected product args {} from step {}", + product_id, + step.id() + ); + } + } + // Insert the new products products.write().await.append(&mut new_products); } From aa5238917e69ef0a14f4690f80c098ced219e8a9 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 17:28:22 +1000 Subject: [PATCH 37/45] Fix UpdateBalancesBetween using incorrect period --- src/reporting/builders.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 1f1e860..1fa5012 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -694,6 +694,29 @@ impl ReportingStep for UpdateBalancesBetween { args: parent_step.id().args, }, ); + + // Look up the BalancesBetween step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build + + if *balances_between_product + .args + .downcast_ref::() + .unwrap() == self.args + { + // Directly depends on BalanceBetween -> Transaction with appropriate date + // Do not need to add extra dependencies + } else { + // Depends on BalanceBetween with appropriate date + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: balances_between_product.name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + ); + } } async fn execute( @@ -733,7 +756,11 @@ impl ReportingStep for UpdateBalancesBetween { // Get opening balances let opening_balances = &products - .get_or_err(balances_between_product)? + .get_or_err(&ReportingProductId { + name: balances_between_product.name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + })? .downcast_ref::() .unwrap() .balances; From 280a2090d9d52cf300031e9e8cc7ae2a31ca549e Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 17:28:34 +1000 Subject: [PATCH 38/45] Execute reporting steps in parallel --- src/main.rs | 13 +++++-- src/reporting/calculator.rs | 2 +- src/reporting/executor.rs | 67 +++++++++++++++++++++++++++++++------ src/reporting/mod.rs | 6 ++-- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2037235..7ab29a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::sync::Arc; + use chrono::NaiveDate; use libdrcr::db::DbConnection; use libdrcr::reporting::builders::register_dynamic_builders; @@ -41,10 +43,11 @@ async fn main() { NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), "$".to_string(), ); - register_lookup_fns(&mut context); register_dynamic_builders(&mut context); + let context = Arc::new(context); + // Print Graphviz let targets = vec![ @@ -86,7 +89,9 @@ async fn main() { }, ]; - let products = generate_report(targets, &context).await.unwrap(); + let products = generate_report(targets, Arc::clone(&context)) + .await + .unwrap(); let result = products .get_or_err(&ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", @@ -120,7 +125,9 @@ async fn main() { }, ]; - let products = generate_report(targets, &context).await.unwrap(); + let products = generate_report(targets, Arc::clone(&context)) + .await + .unwrap(); let result = products .get_or_err(&ReportingProductId { name: "BalanceSheet", diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 35de398..410aff2 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -179,7 +179,7 @@ fn build_step_for_product( } /// Check whether the [ReportingStep] would be ready to execute, if the given previous steps have already completed -fn would_be_ready_to_execute( +pub(crate) fn would_be_ready_to_execute( step: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index c8830e8..dd42c55 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,10 +16,12 @@ along with this program. If not, see . */ -use tokio::sync::RwLock; +use std::sync::Arc; + +use tokio::{sync::RwLock, task::JoinSet}; use super::{ - calculator::ReportingGraphDependencies, + calculator::{would_be_ready_to_execute, ReportingGraphDependencies}, types::{ReportingContext, ReportingProducts, ReportingStep}, }; @@ -28,19 +30,62 @@ pub enum ReportingExecutionError { DependencyNotAvailable { message: String }, } +async fn execute_step( + step_idx: usize, + steps: Arc>>, + dependencies: Arc, + context: Arc, + products: Arc>, +) -> (usize, Result) { + let step = &steps[step_idx]; + let result = step + .execute(&*context, &*steps, &*dependencies, &*products) + .await; + + (step_idx, result) +} + pub async fn execute_steps( steps: Vec>, dependencies: ReportingGraphDependencies, - context: &ReportingContext, + context: Arc, ) -> Result { - let products = RwLock::new(ReportingProducts::new()); + let products = Arc::new(RwLock::new(ReportingProducts::new())); - for step in steps.iter() { - // Execute the step - // TODO: Do this in parallel - let mut new_products = step - .execute(context, &steps, &dependencies, &products) - .await?; + // Prepare for async + let steps = Arc::new(steps); + let dependencies = Arc::new(dependencies); + + // Execute steps asynchronously + let mut handles = JoinSet::new(); + let mut steps_done = Vec::new(); + let mut steps_remaining = (0..steps.len()).collect::>(); + + while steps_done.len() != steps.len() { + // Execute each step which is ready to run + for step_idx in steps_remaining.iter().copied().collect::>() { + // Check if ready to run + if would_be_ready_to_execute(&steps[step_idx], &steps, &dependencies, &steps_done) { + // Spawn new task + // Unfortunately the compiler cannot guarantee lifetimes are correct, so we must pass Arc across thread boundaries + handles.spawn(execute_step( + step_idx, + Arc::clone(&steps), + Arc::clone(&dependencies), + Arc::clone(&context), + Arc::clone(&products), + )); + steps_remaining + .remove(steps_remaining.iter().position(|i| *i == step_idx).unwrap()); + } + } + + // Join next result + let (step_idx, result) = handles.join_next().await.unwrap().unwrap(); + let step = &steps[step_idx]; + steps_done.push(step_idx); + + let mut new_products = result?; // Sanity check the new products for (product_id, _product) in new_products.map().iter() { @@ -71,5 +116,5 @@ pub async fn execute_steps( products.write().await.append(&mut new_products); } - Ok(products.into_inner()) + Ok(Arc::into_inner(products).unwrap().into_inner()) } diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 9214bd3..310e076 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -16,6 +16,8 @@ along with this program. If not, see . */ +use std::sync::Arc; + use calculator::{steps_for_targets, ReportingCalculationError}; use executor::{execute_steps, ReportingExecutionError}; use types::{ReportingContext, ReportingProductId, ReportingProducts}; @@ -50,10 +52,10 @@ impl From for ReportingError { /// Helper function to call [steps_for_targets] followed by [execute_steps]. pub async fn generate_report( targets: Vec, - context: &ReportingContext, + context: Arc, ) -> Result { // Solve dependencies - let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; + let (sorted_steps, dependencies) = steps_for_targets(targets, &*context)?; // Execute steps let products = execute_steps(sorted_steps, dependencies, context).await?; From e02b927b693a7ae54d15c4aa3ef84bfe71e19b3f Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 17:31:11 +1000 Subject: [PATCH 39/45] Fix Spacer being dropped when DynamicReport is calculated --- src/reporting/dynamic_report.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index 82ef0ca..aae7fa1 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -82,7 +82,9 @@ impl CalculatableDynamicReport { calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row)); } - CalculatableDynamicReportEntry::Spacer => (), + CalculatableDynamicReportEntry::Spacer => { + calculated_entries.push(DynamicReportEntry::Spacer); + } } } From bb8383b2222a3b4f624bb3df02ee15cbc62f21ac Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 17:36:39 +1000 Subject: [PATCH 40/45] Only calculate YTD figures in CurrentEarningsToEquity --- src/reporting/steps.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index d836b9c..076ae80 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -732,15 +732,13 @@ impl ReportingStep for CurrentYearEarningsToEquity { } fn requires(&self, context: &ReportingContext) -> Vec { - let eofy_date = get_eofy(&self.args.date, &context.eofy_date); - // CurrentYearEarningsToEquity depends on AllTransactionsExceptEarningsToEquity vec![ReportingProductId { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(eofy_date), - date_end: eofy_date, + date_start: sofy_from_eofy(get_eofy(&self.args.date, &context.eofy_date)), + date_end: self.args.date, }), }] } @@ -753,7 +751,6 @@ impl ReportingStep for CurrentYearEarningsToEquity { products: &RwLock, ) -> Result { let products = products.read().await; - let eofy_date = get_eofy(&self.args.date, &context.eofy_date); // Get balances for this financial year let balances = products @@ -761,8 +758,8 @@ impl ReportingStep for CurrentYearEarningsToEquity { name: "AllTransactionsExceptEarningsToEquity", kind: ReportingProductKind::BalancesBetween, args: Box::new(DateStartDateEndArgs { - date_start: sofy_from_eofy(eofy_date), - date_end: eofy_date, + date_start: sofy_from_eofy(get_eofy(&self.args.date, &context.eofy_date)), + date_end: self.args.date, }), })? .downcast_ref::() @@ -786,7 +783,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { transactions.transactions.push(TransactionWithPostings { transaction: Transaction { id: None, - dt: eofy_date.and_hms_opt(0, 0, 0).unwrap(), + dt: self.args.date.and_hms_opt(0, 0, 0).unwrap(), description: "Current year earnings".to_string(), }, postings: vec![ From 2a2fb5764c3e40407bcfdecaceb542b1067fbdeb Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 17:54:48 +1000 Subject: [PATCH 41/45] Implement PostUnreconciledStatementLines --- src/db.rs | 81 +++++++++++++++++++++------------- src/lib.rs | 1 + src/reporting/builders.rs | 91 +++++++++++++++++++++++++++++++++------ src/reporting/steps.rs | 59 +++++++++++++++++++------ src/statements.rs | 31 +++++++++++++ 5 files changed, 207 insertions(+), 56 deletions(-) create mode 100644 src/statements.rs diff --git a/src/db.rs b/src/db.rs index 05d1595..684d1f1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -18,11 +18,12 @@ use std::collections::HashMap; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; use crate::account_config::AccountConfiguration; +use crate::statements::StatementLine; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { @@ -51,6 +52,39 @@ impl DbConnection { .expect("SQL error") } + /// Get account configurations from the database + pub async fn get_account_configurations(&self) -> Vec { + let mut connection = self.connect().await; + + let mut account_configurations = + sqlx::query("SELECT id, account, kind, data FROM account_configurations") + .map(|r: SqliteRow| AccountConfiguration { + id: r.get("id"), + account: r.get("account"), + kind: r.get("kind"), + data: r.get("data"), + }) + .fetch_all(&mut connection) + .await + .expect("SQL error"); + + // System accounts + account_configurations.push(AccountConfiguration { + id: None, + account: "Current Year Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + account_configurations.push(AccountConfiguration { + id: None, + account: "Retained Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + + account_configurations + } + /// Get account balances from the database pub async fn get_balances(&self, date: NaiveDate) -> HashMap { let mut connection = self.connect().await; @@ -83,37 +117,26 @@ impl DbConnection { balances } - /// Get account configurations from the database - pub async fn get_account_configurations(&self) -> Vec { + /// Get unreconciled statement lines from the database + pub async fn get_unreconciled_statement_lines(&self) -> Vec { let mut connection = self.connect().await; - let mut account_configurations = - sqlx::query("SELECT id, account, kind, data FROM account_configurations") - .map(|r: SqliteRow| AccountConfiguration { - id: r.get("id"), - account: r.get("account"), - kind: r.get("kind"), - data: r.get("data"), - }) - .fetch_all(&mut connection) - .await - .expect("SQL error"); + let rows = sqlx::query( + // On testing, JOIN is much faster than WHERE NOT EXISTS + "SELECT statement_lines.* FROM statement_lines + LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id + WHERE statement_line_reconciliations.id IS NULL" + ).map(|r: SqliteRow| StatementLine { + id: Some(r.get("id")), + source_account: r.get("source_account"), + dt: NaiveDateTime::parse_from_str(r.get("dt"), "%Y-%m-%d").expect("Invalid statement_lines.dt"), + description: r.get("description"), + quantity: r.get("quantity"), + balance: r.get("balance"), + commodity: r.get("commodity"), + }).fetch_all(&mut connection).await.expect("SQL error"); - // System accounts - account_configurations.push(AccountConfiguration { - id: None, - account: "Current Year Earnings".to_string(), - kind: "drcr.equity".to_string(), - data: None, - }); - account_configurations.push(AccountConfiguration { - id: None, - account: "Retained Earnings".to_string(), - kind: "drcr.equity".to_string(), - data: None, - }); - - account_configurations + rows } } diff --git a/src/lib.rs b/src/lib.rs index 4400e00..a5404eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod account_config; pub mod db; pub mod reporting; pub mod transaction; +pub mod statements; pub mod util; pub type QuantityInt = i64; diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 1fa5012..b5ced36 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -33,7 +33,7 @@ use super::executor::ReportingExecutionError; use super::types::{ BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, - ReportingStepDynamicBuilder, ReportingStepId, Transactions, + ReportingStepDynamicBuilder, ReportingStepId, Transactions, VoidArgs, }; /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module @@ -241,6 +241,7 @@ impl GenerateBalances { ) -> bool { // Check for Transactions -> BalancesAt if kind == ReportingProductKind::BalancesAt { + // Try DateArgs match has_step_or_can_build( &ReportingProductId { name, @@ -266,6 +267,33 @@ impl GenerateBalances { } HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} } + + // Try VoidArgs + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(step) => { + // Check for () -> Transactions + if dependencies.dependencies_for_step(&step.id()).len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanLookup(lookup_fn) => { + // Check for () -> Transactions + let step = lookup_fn(args.clone()); + if step.requires(context).len() == 0 { + return true; + } + } + HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} + } } return false; } @@ -301,31 +329,66 @@ impl ReportingStep for GenerateBalances { } } - fn requires(&self, _context: &ReportingContext) -> Vec { - // GenerateBalances depends on Transactions - vec![ReportingProductId { - name: self.step_name, - kind: ReportingProductKind::Transactions, - args: Box::new(self.args.clone()), - }] + fn init_graph( + &self, + steps: &Vec>, + dependencies: &mut ReportingGraphDependencies, + context: &ReportingContext, + ) { + // Add a dependency on the Transactions result + // Look up that step, so we can extract the appropriate args + + // Try DateArgs + match has_step_or_can_build( + &ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + ); + return; + } + HasStepOrCanBuild::None => (), + } + + // Must be VoidArgs (as checked in can_build) + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ); } async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, - _dependencies: &ReportingGraphDependencies, + dependencies: &ReportingGraphDependencies, products: &RwLock, ) -> Result { let products = products.read().await; // Get the transactions + let transactions_product = &dependencies.dependencies_for_step(&self.id())[0].product; let transactions = &products - .get_or_err(&ReportingProductId { - name: self.step_name, - kind: ReportingProductKind::Transactions, - args: Box::new(self.args.clone()), - })? + .get_or_err(transactions_product)? .downcast_ref::() .unwrap() .transactions; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 076ae80..2e3a1de 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -1075,9 +1075,7 @@ impl ReportingStep for IncomeStatement { /// Generate transactions for unreconciled statement lines #[derive(Debug)] -pub struct PostUnreconciledStatementLines { - pub args: DateArgs, -} +pub struct PostUnreconciledStatementLines {} impl PostUnreconciledStatementLines { fn register_lookup_fn(context: &mut ReportingContext) { @@ -1090,13 +1088,11 @@ impl PostUnreconciledStatementLines { } fn takes_args(args: &Box) -> bool { - args.is::() + args.is::() } - fn from_args(args: Box) -> Box { - Box::new(PostUnreconciledStatementLines { - args: *args.downcast().unwrap(), - }) + fn from_args(_args: Box) -> Box { + Box::new(PostUnreconciledStatementLines {}) } } @@ -1112,30 +1108,67 @@ impl ReportingStep for PostUnreconciledStatementLines { ReportingStepId { name: "PostUnreconciledStatementLines", product_kinds: &[ReportingProductKind::Transactions], - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), } } async fn execute( &self, - _context: &ReportingContext, + context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _products: &RwLock, ) -> Result { - eprintln!("Stub: PostUnreconciledStatementLines.execute"); + let unreconciled_statement_lines = context + .db_connection + .get_unreconciled_statement_lines() + .await; - let transactions = Transactions { + // Post unreconciled statement lines + let mut transactions = Transactions { transactions: Vec::new(), }; + for line in unreconciled_statement_lines { + let unclassified_account = if line.quantity >= 0 { + "Unclassified Statement Line Debits" + } else { + "Unclassified Statement Line Credits" + }; + transactions.transactions.push(TransactionWithPostings { + transaction: Transaction { + id: None, + dt: line.dt, + description: line.description.clone(), + }, + postings: vec![ + Posting { + id: None, + transaction_id: None, + description: None, + account: line.source_account.clone(), + quantity: line.quantity, + commodity: line.commodity.clone(), + }, + Posting { + id: None, + transaction_id: None, + description: None, + account: unclassified_account.to_string(), + quantity: -line.quantity, + commodity: line.commodity.clone(), + }, + ], + }); + } + // Store result let mut result = ReportingProducts::new(); result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), }, Box::new(transactions), ); diff --git a/src/statements.rs b/src/statements.rs new file mode 100644 index 0000000..87a4123 --- /dev/null +++ b/src/statements.rs @@ -0,0 +1,31 @@ +/* + 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::NaiveDateTime; + +use crate::QuantityInt; + +pub struct StatementLine { + pub id: Option, + pub source_account: String, + pub dt: NaiveDateTime, + pub description: String, + pub quantity: QuantityInt, + pub balance: QuantityInt, + pub commodity: String, +} From 00c7833706c2fe1eb4ca25093405dc78431eca48 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 22:19:36 +1000 Subject: [PATCH 42/45] Implement trial balance report --- src/main.rs | 36 ++++++++ src/reporting/dynamic_report.rs | 11 ++- src/reporting/steps.rs | 153 +++++++++++++++++++++++++++++++- src/reporting/types.rs | 3 - 4 files changed, 197 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7ab29a9..0dfcdc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,4 +145,40 @@ async fn main() { "{}", result.downcast_ref::().unwrap().to_json() ); + + // Get trial balance + + let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }, + ]; + + let products = generate_report(targets, Arc::clone(&context)) + .await + .unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("Trial balance:"); + println!( + "{}", + result.downcast_ref::().unwrap().to_json() + ); } diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index aae7fa1..131cb07 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use crate::QuantityInt; -use super::types::{GenericReportingProduct, ReportingProduct}; +use super::types::ReportingProduct; /// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] #[derive(Clone, Debug)] @@ -184,6 +184,14 @@ pub struct DynamicReport { } impl DynamicReport { + pub fn new(title: String, columns: Vec, entries: Vec) -> Self { + Self { + title, + columns, + entries, + } + } + /// Remove all entries from the report where auto_hide is enabled and quantity is zero pub fn auto_hide(&mut self) { self.entries.retain_mut(|e| match e { @@ -212,7 +220,6 @@ impl DynamicReport { } } -impl GenericReportingProduct for DynamicReport {} impl ReportingProduct for DynamicReport {} #[derive(Clone, Debug)] diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 2e3a1de..893026a 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -36,7 +36,7 @@ use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; use super::dynamic_report::{ entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, - CalculatableSection, CalculatedRow, LiteralRow, + CalculatableSection, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, }; use super::executor::ReportingExecutionError; use super::types::{ @@ -57,6 +57,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { IncomeStatement::register_lookup_fn(context); PostUnreconciledStatementLines::register_lookup_fn(context); RetainedEarningsToEquity::register_lookup_fn(context); + TrialBalance::register_lookup_fn(context); } /// Target representing all transactions except charging current year and retained earnings to equity @@ -1313,3 +1314,153 @@ impl ReportingStep for RetainedEarningsToEquity { Ok(result) } } + +/// Generates a trial balance [DynamicReport] +#[derive(Debug)] +pub struct TrialBalance { + pub args: DateArgs, +} + +impl TrialBalance { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "TrialBalance", + &[ReportingProductKind::Generic], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(TrialBalance { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for TrialBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for TrialBalance { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "TrialBalance", + product_kinds: &[ReportingProductKind::Generic], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + let mut result = Vec::new(); + + // TrialBalance depends on AllTransactionsExceptEarningsToEquity at the requested date + result.push(ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }); + + result + } + + async fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &RwLock, + ) -> Result { + let products = products.read().await; + + // Get balances for each period + let balances = &products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap() + .balances; + + // Get sorted list of accounts + let mut accounts = balances.keys().collect::>(); + accounts.sort(); + + // Get total debits and credits + let total_dr = balances.values().filter(|b| **b >= 0).sum::(); + let total_cr = -balances.values().filter(|b| **b < 0).sum::(); + + // Init report + let mut report = DynamicReport::new( + "Trial balance".to_string(), + vec!["Dr".to_string(), "Cr".to_string()], + { + let mut entries = Vec::new(); + + // Entry for each account + for account in accounts { + entries.push(DynamicReportEntry::LiteralRow(LiteralRow { + text: account.clone(), + quantity: vec![ + // Dr cell + if balances[account] >= 0 { + balances[account] + } else { + 0 + }, + // Cr cell + if balances[account] < 0 { + -balances[account] + } else { + 0 + }, + ], + id: None, + visible: true, + auto_hide: true, + link: None, + heading: false, + bordered: false, + })); + } + + // Total row + entries.push(DynamicReportEntry::LiteralRow(LiteralRow { + text: "Totals".to_string(), + quantity: vec![total_dr, total_cr], + id: Some("totals".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + })); + + entries + }, + ); + + report.auto_hide(); + + // Store result + let mut result = ReportingProducts::new(); + result.insert( + ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(self.args.clone()), + }, + Box::new(report), + ); + Ok(result) + } +} diff --git a/src/reporting/types.rs b/src/reporting/types.rs index d1e6428..253f25b 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -190,9 +190,6 @@ pub struct BalancesBetween { impl ReportingProduct for BalancesBetween {} -/// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct: Debug + ReportingProduct {} - /// Map from [ReportingProductId] to [ReportingProduct] #[derive(Clone, Debug)] pub struct ReportingProducts { From 4f1db1268822c3b2d676395436e839131988ff11 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 23:48:40 +1000 Subject: [PATCH 43/45] Implement getting transactions from database --- src/db.rs | 48 +++++- src/lib.rs | 1 + src/main.rs | 59 +++++++- src/reporting/calculator.rs | 82 +++++++---- src/reporting/steps.rs | 287 ++++++++++++++++++++++++++++++++++-- src/reporting/types.rs | 22 ++- src/serde.rs | 62 ++++++++ src/transaction.rs | 16 +- 8 files changed, 530 insertions(+), 47 deletions(-) create mode 100644 src/serde.rs diff --git a/src/db.rs b/src/db.rs index 684d1f1..c51a1dc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -24,6 +24,7 @@ use sqlx::{Connection, Row, SqliteConnection}; use crate::account_config::AccountConfiguration; use crate::statements::StatementLine; +use crate::transaction::{Posting, Transaction, TransactionWithPostings}; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { @@ -90,7 +91,7 @@ impl DbConnection { let mut connection = self.connect().await; let rows = sqlx::query( - "-- Get last transaction for each account + "-- Get last transaction for each account WITH max_dt_by_account AS ( SELECT account, max(dt) AS max_dt FROM joined_transactions @@ -117,13 +118,56 @@ impl DbConnection { balances } + /// Get transactions from the database + pub async fn get_transactions(&self) -> Vec { + let mut connection = self.connect().await; + + let rows = sqlx::query( + "SELECT transaction_id, dt, transaction_description, id, description, account, quantity, commodity, quantity_ascost + FROM transactions_with_quantity_ascost + ORDER BY dt, transaction_id, id" + ).fetch_all(&mut connection).await.expect("SQL error"); + + // Un-flatten transaction list + let mut transactions: Vec = Vec::new(); + + for row in rows { + if transactions.is_empty() + || transactions.last().unwrap().transaction.id != row.get("transaction_id") + { + // New transaction + transactions.push(TransactionWithPostings { + transaction: Transaction { + id: row.get("transaction_id"), + dt: NaiveDateTime::parse_from_str(row.get("dt"), "%Y-%m-%d %H:%M:%S.%6f") + .expect("Invalid transactions.dt"), + description: row.get("transaction_description"), + }, + postings: Vec::new(), + }); + } + + transactions.last_mut().unwrap().postings.push(Posting { + id: row.get("id"), + transaction_id: row.get("transaction_id"), + description: row.get("description"), + account: row.get("account"), + quantity: row.get("quantity"), + commodity: row.get("commodity"), + quantity_ascost: row.get("quantity_ascost"), + }); + } + + transactions + } + /// Get unreconciled statement lines from the database pub async fn get_unreconciled_statement_lines(&self) -> Vec { let mut connection = self.connect().await; let rows = sqlx::query( // On testing, JOIN is much faster than WHERE NOT EXISTS - "SELECT statement_lines.* FROM statement_lines + "SELECT statement_lines.* FROM statement_lines LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id WHERE statement_line_reconciliations.id IS NULL" ).map(|r: SqliteRow| StatementLine { diff --git a/src/lib.rs b/src/lib.rs index a5404eb..3a658cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod account_config; pub mod db; pub mod reporting; pub mod transaction; +pub mod serde; pub mod statements; pub mod util; diff --git a/src/main.rs b/src/main.rs index 0dfcdc4..79acabb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,13 +26,13 @@ use libdrcr::reporting::dynamic_report::DynamicReport; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::register_lookup_fns; use libdrcr::reporting::types::{ - DateArgs, DateStartDateEndArgs, MultipleDateArgs, ReportingContext, ReportingProductId, - ReportingProductKind, VoidArgs, + DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, + ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs, }; #[tokio::main] async fn main() { - const YEAR: i32 = 2023; + const YEAR: i32 = 2025; // Connect to database let db_connection = DbConnection::new("sqlite:drcr_testing.db").await; @@ -56,6 +56,13 @@ async fn main() { kind: ReportingProductKind::Transactions, args: Box::new(VoidArgs {}), }, + // ReportingProductId { + // name: "AllTransactionsExceptEarningsToEquity", + // kind: ReportingProductKind::Transactions, + // args: Box::new(DateArgs { + // date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + // }), + // }, ReportingProductId { name: "BalanceSheet", kind: ReportingProductKind::Generic, @@ -65,6 +72,16 @@ async fn main() { }], }), }, + ReportingProductId { + name: "IncomeStatement", + kind: ReportingProductKind::Generic, + args: Box::new(MultipleDateStartDateEndArgs { + dates: vec![DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }], + }), + }, ]; let (sorted_steps, dependencies) = steps_for_targets(targets, &context).unwrap(); @@ -181,4 +198,40 @@ async fn main() { "{}", result.downcast_ref::().unwrap().to_json() ); + + // Get all transactions + + /*let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }, + ]; + + let products = generate_report(targets, Arc::clone(&context)) + .await + .unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("All transactions:"); + println!( + "{}", + result.downcast_ref::().unwrap().to_json() + );*/ } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 410aff2..06549f7 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -134,6 +134,31 @@ fn build_step_for_product( } HasStepOrCanBuild::CanLookup(from_args_fn) => { new_step = from_args_fn(product.args.clone()); + + // Check new step meets the dependency + if new_step.id().name != product.name { + panic!( + "Unexpected step returned from lookup function (expected name {}, got {})", + product.name, + new_step.id().name + ); + } + if new_step.id().args != product.args { + panic!( + "Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", + product.name, + product.args, + new_step.id().args + ); + } + if !new_step.id().product_kinds.contains(&product.kind) { + panic!( + "Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", + product.name, + product.kind, + new_step.id().product_kinds + ); + } } HasStepOrCanBuild::CanBuild(builder) => { new_step = (builder.build)( @@ -144,37 +169,40 @@ fn build_step_for_product( &dependencies, &context, ); + + // Check new step meets the dependency + if new_step.id().name != product.name { + panic!( + "Unexpected step returned from builder {} (expected name {}, got {})", + builder.name, + product.name, + new_step.id().name + ); + } + if new_step.id().args != product.args { + panic!( + "Unexpected step returned from builder {} for {} (expected args {:?}, got {:?})", + builder.name, + product.name, + product.args, + new_step.id().args + ); + } + if !new_step.id().product_kinds.contains(&product.kind) { + panic!( + "Unexpected step returned from builder {} for {} (expected kind {:?}, got {:?})", + builder.name, + product.name, + product.kind, + new_step.id().product_kinds + ); + } } HasStepOrCanBuild::None => { return None; } } - // Check new step meets the dependency - if new_step.id().name != product.name { - panic!( - "Unexpected step returned from lookup function (expected name {}, got {})", - product.name, - new_step.id().name - ); - } - if new_step.id().args != product.args { - panic!( - "Unexpected step returned from lookup function {} (expected args {:?}, got {:?})", - product.name, - product.args, - new_step.id().args - ); - } - if !new_step.id().product_kinds.contains(&product.kind) { - panic!( - "Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})", - product.name, - product.kind, - new_step.id().product_kinds - ); - } - Some(new_step) } @@ -231,6 +259,10 @@ pub fn steps_for_targets( dependencies.add_dependency(new_step.id(), dependency); } new_step.init_graph(&steps, &mut dependencies, &context); + } else { + return Err(ReportingCalculationError::NoStepForProduct { + message: format!("No step builds target product {}", target), + }); } } } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 893026a..c28a70f 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -48,30 +48,100 @@ use super::types::{ /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module pub fn register_lookup_fns(context: &mut ReportingContext) { AllTransactionsExceptEarningsToEquity::register_lookup_fn(context); + AllTransactionsExceptEarningsToEquityBalances::register_lookup_fn(context); AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context); BalanceSheet::register_lookup_fn(context); CalculateIncomeTax::register_lookup_fn(context); CombineOrdinaryTransactions::register_lookup_fn(context); + CombineOrdinaryTransactionsBalances::register_lookup_fn(context); CurrentYearEarningsToEquity::register_lookup_fn(context); DBBalances::register_lookup_fn(context); + DBTransactions::register_lookup_fn(context); IncomeStatement::register_lookup_fn(context); PostUnreconciledStatementLines::register_lookup_fn(context); RetainedEarningsToEquity::register_lookup_fn(context); TrialBalance::register_lookup_fn(context); } -/// Target representing all transactions except charging current year and retained earnings to equity +/// Target representing all transactions except charging current year and retained earnings to equity (returns transaction list) /// /// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. /// /// Used as the basis for the income statement. #[derive(Debug)] pub struct AllTransactionsExceptEarningsToEquity { + pub args: DateArgs, +} + +impl AllTransactionsExceptEarningsToEquity { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "AllTransactionsExceptEarningsToEquity", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(AllTransactionsExceptEarningsToEquity { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for AllTransactionsExceptEarningsToEquity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for AllTransactionsExceptEarningsToEquity { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "AllTransactionsExceptEarningsToEquity", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + // AllTransactionsExceptEarningsToEquity always depends on CombineOrdinaryTransactions at least + vec![ReportingProductId { + name: "CombineOrdinaryTransactions", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }] + } + + async fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &RwLock, + ) -> Result { + combine_transactions_of_all_dependencies(self.id(), dependencies, products).await + } +} + +/// Target representing all transactions except charging current year and retained earnings to equity (returns balances) +/// +/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax]. +/// +/// Used as the basis for the income statement. +#[derive(Debug)] +pub struct AllTransactionsExceptEarningsToEquityBalances { pub product_kinds: &'static [ReportingProductKind; 1], // Must have single member - represented as static array for compatibility with ReportingStepId pub args: Box, } -impl AllTransactionsExceptEarningsToEquity { +impl AllTransactionsExceptEarningsToEquityBalances { fn register_lookup_fn(context: &mut ReportingContext) { context.register_lookup_fn( "AllTransactionsExceptEarningsToEquity", @@ -96,21 +166,21 @@ impl AllTransactionsExceptEarningsToEquity { product_kinds: &'static [ReportingProductKind; 1], args: Box, ) -> Box { - Box::new(AllTransactionsExceptEarningsToEquity { + Box::new(AllTransactionsExceptEarningsToEquityBalances { product_kinds, args, }) } } -impl Display for AllTransactionsExceptEarningsToEquity { +impl Display for AllTransactionsExceptEarningsToEquityBalances { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{}", self.id())) } } #[async_trait] -impl ReportingStep for AllTransactionsExceptEarningsToEquity { +impl ReportingStep for AllTransactionsExceptEarningsToEquityBalances { fn id(&self) -> ReportingStepId { ReportingStepId { name: "AllTransactionsExceptEarningsToEquity", @@ -141,6 +211,7 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity { let step_dependencies = dependencies.dependencies_for_step(&self.id()); // Identify the product_kind dependency most recently generated + // TODO: Make this deterministic - parallel execution may cause the order to vary let product_kind = self.product_kinds[0]; for (product_id, product) in products.map().iter().rev() { @@ -502,8 +573,8 @@ impl CalculateIncomeTax { ); } - fn takes_args(_args: &Box) -> bool { - true + fn takes_args(args: &Box) -> bool { + args.is::() } fn from_args(_args: Box) -> Box { @@ -546,14 +617,20 @@ impl ReportingStep for CalculateIncomeTax { _context: &ReportingContext, ) { for other in steps { - if let Some(other) = other.downcast_ref::() { + if let Some(other) = + other.downcast_ref::() + { // AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax dependencies.add_dependency( other.id(), ReportingProductId { name: self.id().name, kind: other.product_kinds[0], - args: other.id().args, + args: if other.product_kinds[0] == ReportingProductKind::Transactions { + Box::new(VoidArgs {}) + } else { + other.id().args + }, }, ); } @@ -586,9 +663,9 @@ impl ReportingStep for CalculateIncomeTax { } } -/// Combines all steps producing ordinary transactions +/// Combines all steps producing ordinary transactions (returns transaction list) /// -/// By default, these are [DBBalances] and [PostUnreconciledStatementLines] +/// By default, these are [DBTransactions] and [PostUnreconciledStatementLines]. #[derive(Debug)] pub struct CombineOrdinaryTransactions { pub args: DateArgs, @@ -598,7 +675,7 @@ impl CombineOrdinaryTransactions { fn register_lookup_fn(context: &mut ReportingContext) { context.register_lookup_fn( "CombineOrdinaryTransactions", - &[ReportingProductKind::BalancesAt], + &[ReportingProductKind::Transactions], Self::takes_args, Self::from_args, ); @@ -623,6 +700,79 @@ impl Display for CombineOrdinaryTransactions { #[async_trait] impl ReportingStep for CombineOrdinaryTransactions { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "CombineOrdinaryTransactions", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + vec![ + // CombineOrdinaryTransactions depends on DBTransactions + ReportingProductId { + name: "DBTransactions", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + // CombineOrdinaryTransactions depends on PostUnreconciledStatementLines + ReportingProductId { + name: "PostUnreconciledStatementLines", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ] + } + + async fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &RwLock, + ) -> Result { + combine_transactions_of_all_dependencies(self.id(), dependencies, products).await + } +} + +/// Combines all steps producing ordinary transactions (returns balances) +/// +/// By default, these are [DBBalances] and [PostUnreconciledStatementLines]. +#[derive(Debug)] +pub struct CombineOrdinaryTransactionsBalances { + pub args: DateArgs, +} + +impl CombineOrdinaryTransactionsBalances { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "CombineOrdinaryTransactions", + &[ReportingProductKind::BalancesAt], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(CombineOrdinaryTransactionsBalances { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for CombineOrdinaryTransactionsBalances { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for CombineOrdinaryTransactionsBalances { fn id(&self) -> ReportingStepId { ReportingStepId { name: "CombineOrdinaryTransactions", @@ -795,6 +945,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { account: account.clone(), quantity: -balance, commodity: context.reporting_commodity.clone(), + quantity_ascost: None, }, Posting { id: None, @@ -803,6 +954,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { account: "Current Year Earnings".to_string(), quantity: *balance, commodity: context.reporting_commodity.clone(), + quantity_ascost: None, }, ], }) @@ -893,6 +1045,71 @@ impl ReportingStep for DBBalances { } } +/// Look up transactions from the database +#[derive(Debug)] +pub struct DBTransactions {} + +impl DBTransactions { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "DBTransactions", + &[ReportingProductKind::Transactions], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(_args: Box) -> Box { + Box::new(DBTransactions {}) + } +} + +impl Display for DBTransactions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for DBTransactions { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "DBTransactions", + product_kinds: &[ReportingProductKind::Transactions], + args: Box::new(VoidArgs {}), + } + } + + async fn execute( + &self, + context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + _products: &RwLock, + ) -> Result { + // Get transactions from DB + let transactions = Transactions { + transactions: context.db_connection.get_transactions().await, + }; + + // Store result + let mut result = ReportingProducts::new(); + result.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + Box::new(transactions), + ); + Ok(result) + } +} + /// Generates an income statement [DynamicReport] #[derive(Debug)] pub struct IncomeStatement { @@ -1150,6 +1367,7 @@ impl ReportingStep for PostUnreconciledStatementLines { account: line.source_account.clone(), quantity: line.quantity, commodity: line.commodity.clone(), + quantity_ascost: None, }, Posting { id: None, @@ -1158,6 +1376,7 @@ impl ReportingStep for PostUnreconciledStatementLines { account: unclassified_account.to_string(), quantity: -line.quantity, commodity: line.commodity.clone(), + quantity_ascost: None, }, ], }); @@ -1286,6 +1505,7 @@ impl ReportingStep for RetainedEarningsToEquity { account: account.clone(), quantity: -balance, commodity: context.reporting_commodity.clone(), + quantity_ascost: None, }, Posting { id: None, @@ -1294,6 +1514,7 @@ impl ReportingStep for RetainedEarningsToEquity { account: "Retained Earnings".to_string(), quantity: *balance, commodity: context.reporting_commodity.clone(), + quantity_ascost: None, }, ], }) @@ -1464,3 +1685,45 @@ impl ReportingStep for TrialBalance { Ok(result) } } + +/// Combines the transactions of all dependencies and returns [Transactions] as [ReportingProducts] for the given step +/// +/// Used to implement [CombineOrdinaryTransactions] and [AllTransactionsExceptEarningsToEquity]. +async fn combine_transactions_of_all_dependencies( + step_id: ReportingStepId, + dependencies: &ReportingGraphDependencies, + products: &RwLock, +) -> Result { + let products = products.read().await; + + // Combine transactions of all dependencies + + let mut transactions = Transactions { + transactions: Vec::new(), + }; + + for dependency in dependencies.dependencies_for_step(&step_id) { + let dependency_transactions = &products + .get_or_err(&dependency.product)? + .downcast_ref::() + .unwrap() + .transactions; + + for transaction in dependency_transactions.iter() { + transactions.transactions.push(transaction.clone()); + } + } + + // Store result + let mut result = ReportingProducts::new(); + result.insert( + ReportingProductId { + name: step_id.name, + kind: ReportingProductKind::Transactions, + args: step_id.args, + }, + Box::new(transactions), + ); + + Ok(result) +} diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 253f25b..06d648e 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -27,6 +27,7 @@ use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; use indexmap::IndexMap; +use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::db::DbConnection; @@ -167,11 +168,18 @@ downcast_rs::impl_downcast!(ReportingProduct); dyn_clone::clone_trait_object!(ReportingProduct); /// Records a list of transactions generated by a [ReportingStep] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Transactions { pub transactions: Vec, } +impl Transactions { + /// Serialise the product (as JSON) using serde + pub fn to_json(&self) -> String { + serde_json::to_string(&self.transactions).unwrap() + } +} + impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time @@ -234,6 +242,18 @@ impl ReportingProducts { }), } } + + pub fn get_owned_or_err( + mut self, + key: &ReportingProductId, + ) -> Result, ReportingExecutionError> { + match self.map.swap_remove(key) { + Some(value) => Ok(value), + None => Err(ReportingExecutionError::DependencyNotAvailable { + message: format!("Product {} not available when expected", key), + }), + } + } } impl Display for ReportingProducts { diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..bbeaafb --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,62 @@ +/* + 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 . +*/ + +/// Serialises [chrono::NaiveDateTime] in database format +/// +/// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc. +pub mod naivedatetime_to_js { + use std::fmt; + + use chrono::NaiveDateTime; + use serde::{ + de::{self, Unexpected, Visitor}, + Deserializer, Serializer, + }; + + pub(crate) fn serialize( + dt: &NaiveDateTime, + serializer: S, + ) -> Result { + serializer.serialize_str(&dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string()) + } + + struct DateVisitor; + impl<'de> Visitor<'de> for DateVisitor { + type Value = NaiveDateTime; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "a date string") + } + + fn visit_str(self, s: &str) -> Result + where + E: de::Error, + { + match NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.6f") { + Ok(dt) => Ok(dt), + Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)), + } + } + } + + pub(crate) fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + deserializer.deserialize_str(DateVisitor) + } +} diff --git a/src/transaction.rs b/src/transaction.rs index f887cbc..4386fbc 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -19,23 +19,26 @@ use std::collections::HashMap; use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; use crate::QuantityInt; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Transaction { pub id: Option, + #[serde(with = "crate::serde::naivedatetime_to_js")] pub dt: NaiveDateTime, pub description: String, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct TransactionWithPostings { + #[serde(flatten)] pub transaction: Transaction, pub postings: Vec, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Posting { pub id: Option, pub transaction_id: Option, @@ -43,9 +46,14 @@ pub struct Posting { pub account: String, pub quantity: QuantityInt, pub commodity: String, + pub quantity_ascost: Option, + //pub running_balance: Option, } -pub(crate) fn update_balances_from_transactions<'a, I: Iterator>( +pub(crate) fn update_balances_from_transactions< + 'a, + I: Iterator, +>( balances: &mut HashMap, transactions: I, ) { From d326b3ae432ec413c76ca57323053a40030221f1 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 28 May 2025 00:15:52 +1000 Subject: [PATCH 44/45] Implement getting balance assertions from database --- src/db.rs | 30 ++++++++++++++++++++++++++++-- src/lib.rs | 3 +-- src/model/assertions.rs | 33 +++++++++++++++++++++++++++++++++ src/model/mod.rs | 21 +++++++++++++++++++++ src/{ => model}/statements.rs | 0 src/{ => model}/transaction.rs | 0 src/reporting/builders.rs | 2 +- src/reporting/steps.rs | 4 ++-- src/reporting/types.rs | 9 +-------- 9 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/model/assertions.rs create mode 100644 src/model/mod.rs rename src/{ => model}/statements.rs (100%) rename src/{ => model}/transaction.rs (100%) diff --git a/src/db.rs b/src/db.rs index c51a1dc..908c027 100644 --- a/src/db.rs +++ b/src/db.rs @@ -23,8 +23,9 @@ use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; use crate::account_config::AccountConfiguration; -use crate::statements::StatementLine; -use crate::transaction::{Posting, Transaction, TransactionWithPostings}; +use crate::model::assertions::BalanceAssertion; +use crate::model::statements::StatementLine; +use crate::model::transaction::{Posting, Transaction, TransactionWithPostings}; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { @@ -86,6 +87,31 @@ impl DbConnection { account_configurations } + /// Get balance assertions from the database + pub async fn get_balance_assertions(&self) -> Vec { + let mut connection = self.connect().await; + + let balance_assertions = sqlx::query( + "SELECT id, dt, description, account, quantity, commodity + FROM balance_assertions + ORDER BY dt DESC, id DESC", + ) + .map(|r: SqliteRow| BalanceAssertion { + id: r.get("id"), + dt: NaiveDateTime::parse_from_str(r.get("dt"), "%Y-%m-%d %H:%M:%S.%6f") + .expect("Invalid balance_assertions.dt"), + description: r.get("description"), + account: r.get("account"), + quantity: r.get("quantity"), + commodity: r.get("commodity"), + }) + .fetch_all(&mut connection) + .await + .expect("SQL error"); + + balance_assertions + } + /// Get account balances from the database pub async fn get_balances(&self, date: NaiveDate) -> HashMap { let mut connection = self.connect().await; diff --git a/src/lib.rs b/src/lib.rs index 3a658cd..4cdbcc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,8 @@ pub mod account_config; pub mod db; +pub mod model; pub mod reporting; -pub mod transaction; pub mod serde; -pub mod statements; pub mod util; pub type QuantityInt = i64; diff --git a/src/model/assertions.rs b/src/model/assertions.rs new file mode 100644 index 0000000..59e8eb4 --- /dev/null +++ b/src/model/assertions.rs @@ -0,0 +1,33 @@ +/* + 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::NaiveDateTime; +use serde::{Deserialize, Serialize}; + +use crate::QuantityInt; + +#[derive(Deserialize, Serialize)] +pub struct BalanceAssertion { + pub id: Option, + #[serde(with = "crate::serde::naivedatetime_to_js")] + pub dt: NaiveDateTime, + pub description: String, + pub account: String, + pub quantity: QuantityInt, + pub commodity: String, +} diff --git a/src/model/mod.rs b/src/model/mod.rs new file mode 100644 index 0000000..fc27c3d --- /dev/null +++ b/src/model/mod.rs @@ -0,0 +1,21 @@ +/* + 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 . +*/ + +pub mod assertions; +pub mod statements; +pub mod transaction; diff --git a/src/statements.rs b/src/model/statements.rs similarity index 100% rename from src/statements.rs rename to src/model/statements.rs diff --git a/src/transaction.rs b/src/model/transaction.rs similarity index 100% rename from src/transaction.rs rename to src/model/transaction.rs diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index b5ced36..6620e62 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -26,7 +26,7 @@ use std::fmt::Display; use async_trait::async_trait; use tokio::sync::RwLock; -use crate::transaction::update_balances_from_transactions; +use crate::model::transaction::update_balances_from_transactions; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; use super::executor::ReportingExecutionError; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index c28a70f..bfd76f0 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -26,10 +26,10 @@ use chrono::Datelike; use tokio::sync::RwLock; use crate::account_config::kinds_for_account; -use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; -use crate::transaction::{ +use crate::model::transaction::{ update_balances_from_transactions, Posting, Transaction, TransactionWithPostings, }; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; use crate::util::{get_eofy, sofy_from_eofy}; use crate::QuantityInt; diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 06d648e..13891c2 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -31,7 +31,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; use crate::db::DbConnection; -use crate::transaction::TransactionWithPostings; +use crate::model::transaction::TransactionWithPostings; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -173,13 +173,6 @@ pub struct Transactions { pub transactions: Vec, } -impl Transactions { - /// Serialise the product (as JSON) using serde - pub fn to_json(&self) -> String { - serde_json::to_string(&self.transactions).unwrap() - } -} - impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time From 0d680275df56300ff9059928f5611b9da2313b74 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Wed, 28 May 2025 00:33:54 +1000 Subject: [PATCH 45/45] Restore account links to dynamic reports --- src/db.rs | 4 ++-- src/lib.rs | 6 ++++++ src/reporting/dynamic_report.rs | 12 +++++++++++- src/reporting/steps.rs | 6 +++--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/db.rs b/src/db.rs index 908c027..bfee6ec 100644 --- a/src/db.rs +++ b/src/db.rs @@ -73,13 +73,13 @@ impl DbConnection { // System accounts account_configurations.push(AccountConfiguration { id: None, - account: "Current Year Earnings".to_string(), + account: crate::CURRENT_YEAR_EARNINGS.to_string(), kind: "drcr.equity".to_string(), data: None, }); account_configurations.push(AccountConfiguration { id: None, - account: "Retained Earnings".to_string(), + account: crate::RETAINED_EARNINGS.to_string(), kind: "drcr.equity".to_string(), data: None, }); diff --git a/src/lib.rs b/src/lib.rs index 4cdbcc4..300942e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,4 +5,10 @@ pub mod reporting; pub mod serde; pub mod util; +/// Data type used to represent transaction and account quantities pub type QuantityInt = i64; + +// Magic strings +// TODO: Make this configurable +pub const CURRENT_YEAR_EARNINGS: &'static str = "Current Year Earnings"; +pub const RETAINED_EARNINGS: &'static str = "Retained Earnings"; diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index 131cb07..d2149c8 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -535,13 +535,23 @@ pub fn entries_for_kind( .map(|b| b.get(account).unwrap_or(&0) * if invert { -1 } else { 1 }) .collect::>(); + // Some exceptions for the link + let link; + if account == crate::CURRENT_YEAR_EARNINGS { + link = Some("/income-statement".to_string()); + } else if account == crate::RETAINED_EARNINGS { + link = None + } else { + link = Some(format!("/transactions/{}", account)); + } + let entry = LiteralRow { text: account.to_string(), quantity: quantities, id: None, visible: true, auto_hide: true, - link: None, + link, heading: false, bordered: false, }; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index bfd76f0..37a9b86 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -951,7 +951,7 @@ impl ReportingStep for CurrentYearEarningsToEquity { id: None, transaction_id: None, description: None, - account: "Current Year Earnings".to_string(), + account: crate::CURRENT_YEAR_EARNINGS.to_string(), quantity: *balance, commodity: context.reporting_commodity.clone(), quantity_ascost: None, @@ -1511,7 +1511,7 @@ impl ReportingStep for RetainedEarningsToEquity { id: None, transaction_id: None, description: None, - account: "Retained Earnings".to_string(), + account: crate::RETAINED_EARNINGS.to_string(), quantity: *balance, commodity: context.reporting_commodity.clone(), quantity_ascost: None, @@ -1648,7 +1648,7 @@ impl ReportingStep for TrialBalance { id: None, visible: true, auto_hide: true, - link: None, + link: Some(format!("/transactions/{}", account)), heading: false, bordered: false, }));