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

@ -18,8 +18,27 @@
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,
}