Basic dependency resolution code
This commit is contained in:
commit
f15d190112
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
311
Cargo.lock
generated
Normal file
311
Cargo.lock
generated
Normal file
@ -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",
|
||||||
|
]
|
10
Cargo.toml
Normal file
10
Cargo.toml
Normal file
@ -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"
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
|||||||
|
hard_tabs = true
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod reporting;
|
||||||
|
pub mod transaction;
|
||||||
|
pub mod util;
|
43
src/main.rs
Normal file
43
src/main.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Box<dyn ReportingStep>> = 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));
|
||||||
|
}
|
245
src/reporting/builders.rs
Normal file
245
src/reporting/builders.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
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<String>,
|
||||||
|
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
_dependencies: &ReportingGraphDependencies,
|
||||||
|
_context: &ReportingContext,
|
||||||
|
) -> Box<dyn ReportingStep> {
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
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<String>,
|
||||||
|
steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
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<String>,
|
||||||
|
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
_dependencies: &ReportingGraphDependencies,
|
||||||
|
_context: &ReportingContext,
|
||||||
|
) -> Box<dyn ReportingStep> {
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
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(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
322
src/reporting/calculator.rs
Normal file
322
src/reporting/calculator.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep,
|
||||||
|
ReportingStepDynamicBuilder, ReportingStepId, ReportingStepLookupFn,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ReportingGraphDependencies {
|
||||||
|
vec: Vec<Dependency>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportingGraphDependencies {
|
||||||
|
pub fn vec(&self) -> &Vec<Dependency> {
|
||||||
|
&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<dyn ReportingStep>),
|
||||||
|
CanLookup(ReportingStepLookupFn),
|
||||||
|
CanBuild(&'b ReportingStepDynamicBuilder),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_step_or_can_build<'a, 'b>(
|
||||||
|
product: &ReportingProductId,
|
||||||
|
steps: &'a Vec<Box<dyn ReportingStep>>,
|
||||||
|
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<dyn ReportingStep>,
|
||||||
|
steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
dependencies: &ReportingGraphDependencies,
|
||||||
|
previous_steps: &Vec<usize>,
|
||||||
|
) -> 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<Box<dyn ReportingStep>>,
|
||||||
|
context: ReportingContext,
|
||||||
|
) -> Result<Vec<Box<dyn ReportingStep>>, ReportingCalculationError> {
|
||||||
|
let mut steps: Vec<Box<dyn ReportingStep>> = 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::<Vec<_>>();
|
||||||
|
|
||||||
|
'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::<Vec<_>>();
|
||||||
|
sorted_steps.sort_unstable_by_key(|(_s, order)| *order);
|
||||||
|
let sorted_steps = sorted_steps
|
||||||
|
.into_iter()
|
||||||
|
.map(|(s, _idx)| s)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(sorted_steps)
|
||||||
|
}
|
161
src/reporting/mod.rs
Normal file
161
src/reporting/mod.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<ReportingStepDynamicBuilder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<dyn GenericReportingProduct>),
|
||||||
|
//}
|
||||||
|
|
||||||
|
//struct Transactions {}
|
||||||
|
//struct BalancesAt {}
|
||||||
|
//struct BalancesBetween {}
|
||||||
|
|
||||||
|
//trait GenericReportingProduct {}
|
||||||
|
|
||||||
|
//type ReportingProducts = HashMap<ReportingProductId, ReportingProduct>;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ReportingStepId {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub product_kinds: &'static [ReportingProductKind],
|
||||||
|
pub args: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
_dependencies: &mut ReportingGraphDependencies,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
fn after_init_graph(
|
||||||
|
&self,
|
||||||
|
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
_dependencies: &mut ReportingGraphDependencies,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
//fn execute(&self, _context: &ReportingContext, _products: &mut ReportingProducts) {
|
||||||
|
// todo!();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
downcast_rs::impl_downcast!(ReportingStep);
|
||||||
|
|
||||||
|
pub type ReportingStepLookupFn = fn(args: Vec<String>) -> Box<dyn ReportingStep>;
|
||||||
|
|
||||||
|
pub struct ReportingStepDynamicBuilder {
|
||||||
|
name: &'static str,
|
||||||
|
can_build: fn(
|
||||||
|
name: &'static str,
|
||||||
|
kind: ReportingProductKind,
|
||||||
|
args: Vec<String>,
|
||||||
|
steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
dependencies: &ReportingGraphDependencies,
|
||||||
|
context: &ReportingContext,
|
||||||
|
) -> bool,
|
||||||
|
build: fn(
|
||||||
|
name: &'static str,
|
||||||
|
kind: ReportingProductKind,
|
||||||
|
args: Vec<String>,
|
||||||
|
steps: &Vec<Box<dyn ReportingStep>>,
|
||||||
|
dependencies: &ReportingGraphDependencies,
|
||||||
|
context: &ReportingContext,
|
||||||
|
) -> Box<dyn ReportingStep>,
|
||||||
|
}
|
198
src/reporting/steps.rs
Normal file
198
src/reporting/steps.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String>) -> Box<dyn ReportingStep> {
|
||||||
|
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<String>) -> Box<dyn ReportingStep> {
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
dependencies: &mut ReportingGraphDependencies,
|
||||||
|
) {
|
||||||
|
for other in steps {
|
||||||
|
if let Some(other) = other.downcast_ref::<AllTransactionsExceptRetainedEarnings>() {
|
||||||
|
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<String>) -> Box<dyn ReportingStep> {
|
||||||
|
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<Box<dyn ReportingStep>>,
|
||||||
|
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<String>) -> Box<dyn ReportingStep> {
|
||||||
|
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()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
src/transaction.rs
Normal file
25
src/transaction.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
|
||||||
|
pub struct Transaction {
|
||||||
|
pub id: Option<u64>,
|
||||||
|
pub dt: NaiveDateTime,
|
||||||
|
pub description: String,
|
||||||
|
}
|
24
src/util.rs
Normal file
24
src/util.rs
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user