Stub implementations for all steps

This commit is contained in:
RunasSudo 2025-05-21 21:48:57 +10:00
parent 71c3629898
commit 40b0afe492
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 676 additions and 71 deletions

30
Cargo.lock generated
View File

@ -88,6 +88,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" 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]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.63" version = "0.1.63"
@ -112,6 +124,16 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.77" version = "0.3.77"
@ -137,7 +159,7 @@ dependencies = [
"dyn-clone", "dyn-clone",
"dyn-eq", "dyn-eq",
"dyn-hash", "dyn-hash",
"solvent", "indexmap",
] ]
[[package]] [[package]]
@ -191,12 +213,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "solvent"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.101" version = "2.0.101"

View File

@ -9,4 +9,4 @@ downcast-rs = "2.0.1"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"
dyn-eq = "0.1.3" dyn-eq = "0.1.3"
dyn-hash = "0.2.2" dyn-hash = "0.2.2"
solvent = "0.8.3" indexmap = "2.9.0"

View File

@ -20,10 +20,12 @@ use chrono::NaiveDate;
use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::generate_report; use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::{ use libdrcr::reporting::steps::{
register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, register_lookup_fns, AllTransactionsExceptRetainedEarnings,
AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax,
}; };
use libdrcr::reporting::types::{ use libdrcr::reporting::types::{
DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind,
ReportingStep,
}; };
fn main() { fn main() {
@ -31,6 +33,8 @@ fn main() {
register_lookup_fns(&mut context); register_lookup_fns(&mut context);
register_dynamic_builders(&mut context); register_dynamic_builders(&mut context);
// Get income statement
let targets: Vec<Box<dyn ReportingStep>> = vec![ let targets: Vec<Box<dyn ReportingStep>> = vec![
Box::new(CalculateIncomeTax {}), Box::new(CalculateIncomeTax {}),
Box::new(AllTransactionsExceptRetainedEarnings { 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<Box<dyn ReportingStep>> = 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);
} }

View File

@ -16,12 +16,15 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::collections::HashMap;
use std::fmt::Display; 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::executor::ReportingExecutionError;
use super::types::{ use super::types::{
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs,
ReportingStepDynamicBuilder, ReportingStepId, Transactions,
}; };
/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module
@ -40,6 +43,7 @@ pub struct BalancesAtToBalancesBetween {
args: DateStartDateEndArgs, args: DateStartDateEndArgs,
} }
/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates
impl BalancesAtToBalancesBetween { impl BalancesAtToBalancesBetween {
// Implements BalancesAt, BalancesAt -> BalancesBetween // Implements BalancesAt, BalancesAt -> BalancesBetween
@ -137,13 +141,69 @@ impl ReportingStep for BalancesAtToBalancesBetween {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs { args: Box::new(DateArgs {
date: self.args.date_end.clone(), date: self.args.date_end,
}), }),
}, },
] ]
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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::<BalancesAt>()
.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::<BalancesAt>()
.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)] #[derive(Debug)]
pub struct GenerateBalances { pub struct GenerateBalances {
step_name: &'static str, step_name: &'static str,
@ -151,8 +211,6 @@ pub struct GenerateBalances {
} }
impl GenerateBalances { impl GenerateBalances {
// Implements (() -> Transactions) -> BalancesAt
fn register_dynamic_builder(context: &mut ReportingContext) { fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder { context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "GenerateBalances", name: "GenerateBalances",
@ -238,8 +296,58 @@ impl ReportingStep for GenerateBalances {
args: Box::new(self.args.clone()), args: Box::new(self.args.clone()),
}] }]
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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::<Transactions>()
.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)] #[derive(Debug)]
pub struct UpdateBalancesAt { pub struct UpdateBalancesAt {
step_name: &'static str, step_name: &'static str,
@ -374,8 +482,97 @@ impl ReportingStep for UpdateBalancesAt {
}, },
); );
} }
fn execute(
&self,
_context: &ReportingContext,
steps: &Vec<Box<dyn ReportingStep>>,
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::<Transactions>()
.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::<BalancesAt>()
.unwrap();
} else {
// As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available
let date_end = dependency
.args
.downcast_ref::<DateStartDateEndArgs>()
.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)] #[derive(Debug)]
pub struct UpdateBalancesBetween { pub struct UpdateBalancesBetween {
step_name: &'static str, step_name: &'static str,
@ -383,8 +580,6 @@ pub struct UpdateBalancesBetween {
} }
impl UpdateBalancesBetween { impl UpdateBalancesBetween {
// Implements (BalancesBetween -> Transactions) -> BalancesBetween
fn register_dynamic_builder(context: &mut ReportingContext) { fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder { context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "UpdateBalancesBetween", name: "UpdateBalancesBetween",
@ -419,23 +614,6 @@ impl UpdateBalancesBetween {
return true; 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; return false;
} }
@ -493,8 +671,77 @@ impl ReportingStep for UpdateBalancesBetween {
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: parent_step.id().args.clone(), args: parent_step.id().args,
}, },
); );
} }
fn execute(
&self,
_context: &ReportingContext,
steps: &Vec<Box<dyn ReportingStep>>,
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::<Transactions>()
.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::<BalancesBetween>()
.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(())
}
} }

View File

@ -151,7 +151,7 @@ fn would_be_ready_to_execute(
pub fn steps_for_targets( pub fn steps_for_targets(
targets: Vec<Box<dyn ReportingStep>>, targets: Vec<Box<dyn ReportingStep>>,
context: &ReportingContext, context: &ReportingContext,
) -> Result<Vec<Box<dyn ReportingStep>>, ReportingCalculationError> { ) -> Result<(Vec<Box<dyn ReportingStep>>, ReportingGraphDependencies), ReportingCalculationError> {
let mut steps: Vec<Box<dyn ReportingStep>> = Vec::new(); let mut steps: Vec<Box<dyn ReportingStep>> = Vec::new();
let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; let mut dependencies = ReportingGraphDependencies { vec: Vec::new() };
@ -319,5 +319,5 @@ pub fn steps_for_targets(
.map(|(s, _idx)| s) .map(|(s, _idx)| s)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(sorted_steps) Ok((sorted_steps, dependencies))
} }

View File

@ -16,21 +16,22 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::types::{ReportingContext, ReportingProducts, ReportingStep}; use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}};
#[derive(Debug)] #[derive(Debug)]
pub struct ReportingExecutionError { pub enum ReportingExecutionError {
message: String, DependencyNotAvailable { message: String }
} }
pub fn execute_steps( pub fn execute_steps(
steps: Vec<Box<dyn ReportingStep>>, steps: Vec<Box<dyn ReportingStep>>,
dependencies: ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
) -> Result<ReportingProducts, ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let mut products = ReportingProducts::new(); let mut products = ReportingProducts::new();
for step in steps { for step in steps.iter() {
step.execute(context, &mut products)?; step.execute(context, &steps, &dependencies, &mut products)?;
} }
Ok(products) Ok(products)

View File

@ -49,10 +49,10 @@ pub fn generate_report(
context: &ReportingContext, context: &ReportingContext,
) -> Result<ReportingProducts, ReportingError> { ) -> Result<ReportingProducts, ReportingError> {
// Solve dependencies // Solve dependencies
let sorted_steps = steps_for_targets(targets, context)?; let (sorted_steps, dependencies) = steps_for_targets(targets, context)?;
// Execute steps // Execute steps
let products = execute_steps(sorted_steps, context)?; let products = execute_steps(sorted_steps, dependencies, context)?;
Ok(products) Ok(products)
} }

View File

@ -16,21 +16,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
//! This module contains concrete [ReportingStep] implementations
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use chrono::Datelike; use chrono::Datelike;
use crate::reporting::types::{ use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions};
BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId,
};
use crate::util::sofy_from_eofy; use crate::util::sofy_from_eofy;
use super::calculator::ReportingGraphDependencies; use super::calculator::ReportingGraphDependencies;
use super::executor::ReportingExecutionError; use super::executor::ReportingExecutionError;
use super::types::{ use super::types::{
DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind,
ReportingStepArgs, ReportingStepId, VoidArgs, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs,
}; };
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
@ -96,6 +96,64 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings {
args: self.args.clone(), args: self.args.clone(),
} }
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
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<dyn ReportingProduct> = 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)] #[derive(Debug)]
@ -155,6 +213,62 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings {
}, },
] ]
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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::<BalancesAt>()
.unwrap();
// Get RetainedEarningsToEquity transactions
let transactions = products
.get_or_err(&ReportingProductId {
name: "RetainedEarningsToEquity",
kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()),
})?
.downcast_ref::<Transactions>()
.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)] #[derive(Debug)]
@ -226,6 +340,31 @@ impl ReportingStep for CalculateIncomeTax {
} }
} }
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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)] #[derive(Debug)]
@ -285,6 +424,44 @@ impl ReportingStep for CombineOrdinaryTransactions {
}, },
] ]
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
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::<BalancesAt>()
.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)] #[derive(Debug)]
@ -331,6 +508,8 @@ impl ReportingStep for DBBalances {
fn execute( fn execute(
&self, &self,
_context: &ReportingContext, _context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts, products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> { ) -> Result<(), ReportingExecutionError> {
eprintln!("Stub: DBBalances.execute"); eprintln!("Stub: DBBalances.execute");
@ -345,7 +524,7 @@ impl ReportingStep for DBBalances {
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()), args: Box::new(self.args.clone()),
}, },
ReportingProduct::BalancesAt(balances), Box::new(balances),
); );
Ok(()) Ok(())
@ -392,6 +571,31 @@ impl ReportingStep for PostUnreconciledStatementLines {
args: Box::new(self.args.clone()), args: Box::new(self.args.clone()),
} }
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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)] #[derive(Debug)]
@ -448,4 +652,29 @@ impl ReportingStep for RetainedEarningsToEquity {
}), }),
}] }]
} }
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_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(())
}
} }

View File

@ -24,7 +24,9 @@ use downcast_rs::Downcast;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use dyn_eq::DynEq; use dyn_eq::DynEq;
use dyn_hash::DynHash; use dyn_hash::DynHash;
use indexmap::IndexMap;
use crate::transaction::TransactionWithPostings;
use crate::QuantityInt; use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies; use super::calculator::ReportingGraphDependencies;
@ -121,7 +123,7 @@ pub struct ReportingStepDynamicBuilder {
// REPORTING PRODUCTS // REPORTING PRODUCTS
/// Identifies a [ReportingProduct] /// Identifies a [ReportingProduct]
#[derive(Debug, Eq, Hash, PartialEq)] #[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ReportingProductId { pub struct ReportingProductId {
pub name: &'static str, pub name: &'static str,
pub kind: ReportingProductKind, pub kind: ReportingProductKind,
@ -144,33 +146,84 @@ pub enum ReportingProductKind {
} }
/// Represents the result of a [ReportingStep] /// Represents the result of a [ReportingStep]
#[derive(Debug)] pub trait ReportingProduct: Debug + Downcast + DynClone {}
pub enum ReportingProduct {
Transactions(Transactions), downcast_rs::impl_downcast!(ReportingProduct);
BalancesAt(BalancesAt), dyn_clone::clone_trait_object!(ReportingProduct);
BalancesBetween(BalancesBetween),
Generic(Box<dyn GenericReportingProduct>),
}
/// Records a list of transactions generated by a [ReportingStep] /// Records a list of transactions generated by a [ReportingStep]
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct Transactions {} pub struct Transactions {
pub transactions: Vec<TransactionWithPostings>,
}
impl ReportingProduct for Transactions {}
/// Records cumulative account balances at a particular point in time /// Records cumulative account balances at a particular point in time
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct BalancesAt { pub struct BalancesAt {
pub balances: HashMap<String, QuantityInt>, pub balances: HashMap<String, QuantityInt>,
} }
impl ReportingProduct for BalancesAt {}
/// Records the total value of transactions in each account between two points in time /// Records the total value of transactions in each account between two points in time
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct BalancesBetween {} pub struct BalancesBetween {
pub balances: HashMap<String, QuantityInt>,
}
impl ReportingProduct for BalancesBetween {}
/// Represents a custom [ReportingProduct] generated by a [ReportingStep] /// Represents a custom [ReportingProduct] generated by a [ReportingStep]
pub trait GenericReportingProduct: Debug {} pub trait GenericReportingProduct: Debug + ReportingProduct {}
/// Convenience type mapping [ReportingProductId] to [ReportingProduct] /// Map from [ReportingProductId] to [ReportingProduct]
pub type ReportingProducts = HashMap<ReportingProductId, ReportingProduct>; #[derive(Clone, Debug)]
pub struct ReportingProducts {
map: IndexMap<ReportingProductId, Box<dyn ReportingProduct>>,
}
impl ReportingProducts {
pub fn new() -> Self {
Self {
map: IndexMap::new(),
}
}
pub fn map(&self) -> &IndexMap<ReportingProductId, Box<dyn ReportingProduct>> {
&self.map
}
pub fn insert(&mut self, key: ReportingProductId, value: Box<dyn ReportingProduct>) {
self.map.insert(key, value);
}
pub fn get_or_err(
&self,
key: &ReportingProductId,
) -> Result<&Box<dyn ReportingProduct>, 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::<Vec<_>>()
.join(",\n")
))
}
}
// --------------- // ---------------
// REPORTING STEPS // REPORTING STEPS
@ -230,6 +283,8 @@ pub trait ReportingStep: Debug + Display + Downcast {
fn execute( fn execute(
&self, &self,
context: &ReportingContext, context: &ReportingContext,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts, products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> { ) -> Result<(), ReportingExecutionError> {
todo!("{}", self); todo!("{}", self);

View File

@ -1,25 +1,44 @@
/* /*
DrCr: Web-based double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify 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 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 the Free Software Foundation, either version 3 of the License, or
(at your option) any later version. (at your option) any later version.
This program is distributed in the hope that it will be useful, This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details. GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License 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/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use crate::QuantityInt;
#[derive(Clone, Debug)]
pub struct Transaction { pub struct Transaction {
pub id: Option<u64>, pub id: Option<u64>,
pub dt: NaiveDateTime, pub dt: NaiveDateTime,
pub description: String, pub description: String,
} }
#[derive(Clone, Debug)]
pub struct TransactionWithPostings {
pub transaction: Transaction,
pub postings: Vec<Posting>,
}
#[derive(Clone, Debug)]
pub struct Posting {
pub id: Option<u64>,
pub transaction_id: Option<u64>,
pub description: String,
pub account: String,
pub quantity: QuantityInt,
pub commodity: String,
}