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

View File

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

View File

@ -20,10 +20,12 @@ use chrono::NaiveDate;
use libdrcr::reporting::builders::register_dynamic_builders;
use libdrcr::reporting::generate_report;
use libdrcr::reporting::steps::{
register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax,
register_lookup_fns, AllTransactionsExceptRetainedEarnings,
AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax,
};
use libdrcr::reporting::types::{
DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep,
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind,
ReportingStep,
};
fn main() {
@ -31,6 +33,8 @@ fn main() {
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
// Get income statement
let targets: Vec<Box<dyn ReportingStep>> = vec![
Box::new(CalculateIncomeTax {}),
Box::new(AllTransactionsExceptRetainedEarnings {
@ -42,7 +46,41 @@ fn main() {
}),
];
let products = generate_report(targets, &context);
let products = generate_report(targets, &context).unwrap();
let result = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptRetainedEarnings",
kind: ReportingProductKind::BalancesBetween,
args: Box::new(DateStartDateEndArgs {
date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(),
date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
}),
})
.unwrap();
println!("{:?}", products);
println!("{:?}", result);
// Get balance sheet
let targets: Vec<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/>.
*/
use std::collections::HashMap;
use std::fmt::Display;
use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies};
use super::executor::ReportingExecutionError;
use super::types::{
DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind,
ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId,
BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs,
ReportingStepDynamicBuilder, ReportingStepId, Transactions,
};
/// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module
@ -40,6 +43,7 @@ pub struct BalancesAtToBalancesBetween {
args: DateStartDateEndArgs,
}
/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates
impl BalancesAtToBalancesBetween {
// Implements BalancesAt, BalancesAt -> BalancesBetween
@ -137,13 +141,69 @@ impl ReportingStep for BalancesAtToBalancesBetween {
name: self.step_name,
kind: ReportingProductKind::BalancesAt,
args: Box::new(DateArgs {
date: self.args.date_end.clone(),
date: self.args.date_end,
}),
},
]
}
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<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)]
pub struct GenerateBalances {
step_name: &'static str,
@ -151,8 +211,6 @@ pub struct GenerateBalances {
}
impl GenerateBalances {
// Implements (() -> Transactions) -> BalancesAt
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "GenerateBalances",
@ -238,8 +296,58 @@ impl ReportingStep for GenerateBalances {
args: Box::new(self.args.clone()),
}]
}
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<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)]
pub struct UpdateBalancesAt {
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)]
pub struct UpdateBalancesBetween {
step_name: &'static str,
@ -383,8 +580,6 @@ pub struct UpdateBalancesBetween {
}
impl UpdateBalancesBetween {
// Implements (BalancesBetween -> Transactions) -> BalancesBetween
fn register_dynamic_builder(context: &mut ReportingContext) {
context.register_dynamic_builder(ReportingStepDynamicBuilder {
name: "UpdateBalancesBetween",
@ -419,23 +614,6 @@ impl UpdateBalancesBetween {
return true;
}
}
// Check lookup or builder - with args
/*match has_step_or_can_build(
&ReportingProductId {
name,
kind: ReportingProductKind::Transactions,
args: args.clone(),
},
steps,
dependencies,
context,
) {
HasStepOrCanBuild::HasStep(step) => unreachable!(),
HasStepOrCanBuild::CanLookup(_)
| HasStepOrCanBuild::CanBuild(_)
| HasStepOrCanBuild::None => {}
}*/
}
return false;
}
@ -493,8 +671,77 @@ impl ReportingStep for UpdateBalancesBetween {
ReportingProductId {
name: self.step_name,
kind: ReportingProductKind::Transactions,
args: parent_step.id().args.clone(),
args: parent_step.id().args,
},
);
}
fn execute(
&self,
_context: &ReportingContext,
steps: &Vec<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(
targets: Vec<Box<dyn ReportingStep>>,
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 dependencies = ReportingGraphDependencies { vec: Vec::new() };
@ -319,5 +319,5 @@ pub fn steps_for_targets(
.map(|(s, _idx)| s)
.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/>.
*/
use super::types::{ReportingContext, ReportingProducts, ReportingStep};
use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}};
#[derive(Debug)]
pub struct ReportingExecutionError {
message: String,
pub enum ReportingExecutionError {
DependencyNotAvailable { message: String }
}
pub fn execute_steps(
steps: Vec<Box<dyn ReportingStep>>,
dependencies: ReportingGraphDependencies,
context: &ReportingContext,
) -> Result<ReportingProducts, ReportingExecutionError> {
let mut products = ReportingProducts::new();
for step in steps {
step.execute(context, &mut products)?;
for step in steps.iter() {
step.execute(context, &steps, &dependencies, &mut products)?;
}
Ok(products)

View File

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

View File

@ -16,21 +16,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! This module contains concrete [ReportingStep] implementations
use std::collections::HashMap;
use std::fmt::Display;
use chrono::Datelike;
use crate::reporting::types::{
BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId,
};
use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions};
use crate::util::sofy_from_eofy;
use super::calculator::ReportingGraphDependencies;
use super::executor::ReportingExecutionError;
use super::types::{
DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep,
ReportingStepArgs, ReportingStepId, VoidArgs,
BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind,
ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs,
};
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
@ -96,6 +96,64 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings {
args: self.args.clone(),
}
}
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<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)]
@ -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)]
@ -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)]
@ -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)]
@ -331,6 +508,8 @@ impl ReportingStep for DBBalances {
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> {
eprintln!("Stub: DBBalances.execute");
@ -345,7 +524,7 @@ impl ReportingStep for DBBalances {
kind: ReportingProductKind::BalancesAt,
args: Box::new(self.args.clone()),
},
ReportingProduct::BalancesAt(balances),
Box::new(balances),
);
Ok(())
@ -392,6 +571,31 @@ impl ReportingStep for PostUnreconciledStatementLines {
args: Box::new(self.args.clone()),
}
}
fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec<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)]
@ -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_eq::DynEq;
use dyn_hash::DynHash;
use indexmap::IndexMap;
use crate::transaction::TransactionWithPostings;
use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies;
@ -121,7 +123,7 @@ pub struct ReportingStepDynamicBuilder {
// REPORTING PRODUCTS
/// Identifies a [ReportingProduct]
#[derive(Debug, Eq, Hash, PartialEq)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct ReportingProductId {
pub name: &'static str,
pub kind: ReportingProductKind,
@ -144,33 +146,84 @@ pub enum ReportingProductKind {
}
/// Represents the result of a [ReportingStep]
#[derive(Debug)]
pub enum ReportingProduct {
Transactions(Transactions),
BalancesAt(BalancesAt),
BalancesBetween(BalancesBetween),
Generic(Box<dyn GenericReportingProduct>),
}
pub trait ReportingProduct: Debug + Downcast + DynClone {}
downcast_rs::impl_downcast!(ReportingProduct);
dyn_clone::clone_trait_object!(ReportingProduct);
/// Records a list of transactions generated by a [ReportingStep]
#[derive(Debug)]
pub struct Transactions {}
#[derive(Clone, Debug)]
pub struct Transactions {
pub transactions: Vec<TransactionWithPostings>,
}
impl ReportingProduct for Transactions {}
/// Records cumulative account balances at a particular point in time
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct BalancesAt {
pub balances: HashMap<String, QuantityInt>,
}
impl ReportingProduct for BalancesAt {}
/// Records the total value of transactions in each account between two points in time
#[derive(Debug)]
pub struct BalancesBetween {}
#[derive(Clone, Debug)]
pub struct BalancesBetween {
pub balances: HashMap<String, QuantityInt>,
}
impl ReportingProduct for BalancesBetween {}
/// Represents a custom [ReportingProduct] generated by a [ReportingStep]
pub trait GenericReportingProduct: Debug {}
pub trait GenericReportingProduct: Debug + ReportingProduct {}
/// Convenience type mapping [ReportingProductId] to [ReportingProduct]
pub type ReportingProducts = HashMap<ReportingProductId, ReportingProduct>;
/// Map from [ReportingProductId] to [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
@ -230,6 +283,8 @@ pub trait ReportingStep: Debug + Display + Downcast {
fn execute(
&self,
context: &ReportingContext,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> {
todo!("{}", self);

View File

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