diff --git a/Cargo.lock b/Cargo.lock index 14f6134..7a70601 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15401da73a9ed8c80e3b2d4dc05fe10e7b72d7243b9f614e516a44fa99986e88" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + [[package]] name = "iana-time-zone" version = "0.1.63" @@ -112,6 +124,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -137,7 +159,7 @@ dependencies = [ "dyn-clone", "dyn-eq", "dyn-hash", - "solvent", + "indexmap", ] [[package]] @@ -191,12 +213,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "solvent" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14a50198e546f29eb0a4f977763c8277ec2184b801923c3be71eeaec05471f16" - [[package]] name = "syn" version = "2.0.101" diff --git a/Cargo.toml b/Cargo.toml index 9cd9b5d..f6046dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ downcast-rs = "2.0.1" dyn-clone = "1.0.19" dyn-eq = "0.1.3" dyn-hash = "0.2.2" -solvent = "0.8.3" +indexmap = "2.9.0" diff --git a/src/main.rs b/src/main.rs index 65cc9e2..b7e2bc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,10 +20,12 @@ use chrono::NaiveDate; use libdrcr::reporting::builders::register_dynamic_builders; use libdrcr::reporting::generate_report; use libdrcr::reporting::steps::{ - register_lookup_fns, AllTransactionsExceptRetainedEarnings, CalculateIncomeTax, + register_lookup_fns, AllTransactionsExceptRetainedEarnings, + AllTransactionsIncludingRetainedEarnings, CalculateIncomeTax, }; use libdrcr::reporting::types::{ - DateStartDateEndArgs, ReportingContext, ReportingProductKind, ReportingStep, + DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, + ReportingStep, }; fn main() { @@ -31,6 +33,8 @@ fn main() { register_lookup_fns(&mut context); register_dynamic_builders(&mut context); + // Get income statement + let targets: Vec> = vec![ Box::new(CalculateIncomeTax {}), Box::new(AllTransactionsExceptRetainedEarnings { @@ -42,7 +46,41 @@ fn main() { }), ]; - let products = generate_report(targets, &context); + let products = generate_report(targets, &context).unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesBetween, + args: Box::new(DateStartDateEndArgs { + date_start: NaiveDate::from_ymd_opt(2024, 7, 1).unwrap(), + date_end: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); - println!("{:?}", products); + println!("{:?}", result); + + // Get balance sheet + + let targets: Vec> = vec![ + Box::new(CalculateIncomeTax {}), + Box::new(AllTransactionsIncludingRetainedEarnings { + args: DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }, + }), + ]; + + let products = generate_report(targets, &context).unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsIncludingRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("{:?}", result); } diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 0eaa902..8cd3c11 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -16,12 +16,15 @@ along with this program. If not, see . */ +use std::collections::HashMap; use std::fmt::Display; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; +use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, - ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, + BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, + ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, + ReportingStepDynamicBuilder, ReportingStepId, Transactions, }; /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module @@ -40,6 +43,7 @@ pub struct BalancesAtToBalancesBetween { args: DateStartDateEndArgs, } +/// This dynamic builder automatically generates a [BalancesBetween] by subtracting [BalancesAt] between two dates impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween @@ -137,13 +141,69 @@ impl ReportingStep for BalancesAtToBalancesBetween { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { - date: self.args.date_end.clone(), + date: self.args.date_end, }), }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get balances at dates + let balances_start = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_start.pred_opt().unwrap(), // Opening balance is the closing balance of the preceding day + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + let balances_end = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { + date: self.args.date_end, + }), + })? + .downcast_ref::() + .unwrap() + .balances; + + // Compute balances_end - balances_start + let mut balances = BalancesBetween { + balances: balances_end.clone(), + }; + + for (account, balance) in balances_start.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) - balance; + balances.balances.insert(account.clone(), running_balance); + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from a step which has no dependencies and generates [Transactions] (e.g. [super::steps::PostUnreconciledStatementLines]) #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, @@ -151,8 +211,6 @@ pub struct GenerateBalances { } impl GenerateBalances { - // Implements (() -> Transactions) -> BalancesAt - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", @@ -238,8 +296,58 @@ impl ReportingStep for GenerateBalances { args: Box::new(self.args.clone()), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get the transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Sum balances + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesAt] from: +/// - a step which generates [Transactions] from [BalancesAt], or +/// - a step which generates [Transactions] from [BalancesBetween], and for which a [BalancesAt] is also available #[derive(Debug)] pub struct UpdateBalancesAt { step_name: &'static str, @@ -374,8 +482,97 @@ impl ReportingStep for UpdateBalancesAt { }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesAt step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let dependency = &dependencies_for_step[0].product; // Existence and uniqueness checked in can_build + + let opening_balances_at; + + if dependency.kind == ReportingProductKind::BalancesAt { + // Directly depends on BalancesAt -> Transaction + opening_balances_at = products + .get_or_err(&dependency)? + .downcast_ref::() + .unwrap(); + } else { + // As checked in can_build, must depend on BalancesBetween -> Transaction with a BalancesAt available + let date_end = dependency + .args + .downcast_ref::() + .unwrap() + .date_end; + + opening_balances_at = products + .get_or_err(&ReportingProductId { + name: dependency.name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(DateArgs { date: date_end }), + })? + .downcast_ref() + .unwrap(); + } + + // Sum balances + let mut balances = BalancesAt { + balances: opening_balances_at.balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } +/// This dynamic builder automatically generates a [BalancesBetween] from a step which generates [Transactions] from [BalancesBetween] #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, @@ -383,8 +580,6 @@ pub struct UpdateBalancesBetween { } impl UpdateBalancesBetween { - // Implements (BalancesBetween -> Transactions) -> BalancesBetween - fn register_dynamic_builder(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", @@ -419,23 +614,6 @@ impl UpdateBalancesBetween { return true; } } - - // Check lookup or builder - with args - /*match has_step_or_can_build( - &ReportingProductId { - name, - kind: ReportingProductKind::Transactions, - args: args.clone(), - }, - steps, - dependencies, - context, - ) { - HasStepOrCanBuild::HasStep(step) => unreachable!(), - HasStepOrCanBuild::CanLookup(_) - | HasStepOrCanBuild::CanBuild(_) - | HasStepOrCanBuild::None => {} - }*/ } return false; } @@ -493,8 +671,77 @@ impl ReportingStep for UpdateBalancesBetween { ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, - args: parent_step.id().args.clone(), + args: parent_step.id().args, }, ); } + + fn execute( + &self, + _context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Look up the parent step, so we can extract the appropriate args + let parent_step = steps + .iter() + .find(|s| { + s.id().name == self.step_name + && s.id() + .product_kinds + .contains(&ReportingProductKind::Transactions) + }) + .unwrap(); // Existence is checked in can_build + + // Get transactions + let transactions = &products + .get_or_err(&ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: parent_step.id().args, + })? + .downcast_ref::() + .unwrap() + .transactions; + + // Look up the BalancesBetween step + let dependencies_for_step = dependencies.dependencies_for_step(&parent_step.id()); + let balances_between_product = &dependencies_for_step[0].product; // Existence and uniqueness is checked in can_build + + // Get opening balances + let opening_balances = &products + .get_or_err(balances_between_product)? + .downcast_ref::() + .unwrap() + .balances; + + // Sum balances + let mut balances = BalancesBetween { + balances: opening_balances.clone(), + }; + + for transaction in transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::BalancesBetween, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } diff --git a/src/reporting/calculator.rs b/src/reporting/calculator.rs index 68cbbfb..862df18 100644 --- a/src/reporting/calculator.rs +++ b/src/reporting/calculator.rs @@ -151,7 +151,7 @@ fn would_be_ready_to_execute( pub fn steps_for_targets( targets: Vec>, context: &ReportingContext, -) -> Result>, ReportingCalculationError> { +) -> Result<(Vec>, ReportingGraphDependencies), ReportingCalculationError> { let mut steps: Vec> = Vec::new(); let mut dependencies = ReportingGraphDependencies { vec: Vec::new() }; @@ -319,5 +319,5 @@ pub fn steps_for_targets( .map(|(s, _idx)| s) .collect::>(); - Ok(sorted_steps) + Ok((sorted_steps, dependencies)) } diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs index bcb9976..5c9dd4d 100644 --- a/src/reporting/executor.rs +++ b/src/reporting/executor.rs @@ -16,21 +16,22 @@ along with this program. If not, see . */ -use super::types::{ReportingContext, ReportingProducts, ReportingStep}; +use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; #[derive(Debug)] -pub struct ReportingExecutionError { - message: String, +pub enum ReportingExecutionError { + DependencyNotAvailable { message: String } } pub fn execute_steps( steps: Vec>, + dependencies: ReportingGraphDependencies, context: &ReportingContext, ) -> Result { let mut products = ReportingProducts::new(); - for step in steps { - step.execute(context, &mut products)?; + for step in steps.iter() { + step.execute(context, &steps, &dependencies, &mut products)?; } Ok(products) diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs index 83a672f..411a8ef 100644 --- a/src/reporting/mod.rs +++ b/src/reporting/mod.rs @@ -49,10 +49,10 @@ pub fn generate_report( context: &ReportingContext, ) -> Result { // Solve dependencies - let sorted_steps = steps_for_targets(targets, context)?; + let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; // Execute steps - let products = execute_steps(sorted_steps, context)?; + let products = execute_steps(sorted_steps, dependencies, context)?; Ok(products) } diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index e021277..b541f4a 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -16,21 +16,21 @@ along with this program. If not, see . */ +//! This module contains concrete [ReportingStep] implementations + use std::collections::HashMap; use std::fmt::Display; use chrono::Datelike; -use crate::reporting::types::{ - BalancesAt, DateStartDateEndArgs, ReportingProduct, ReportingProductId, -}; +use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; use crate::util::sofy_from_eofy; use super::calculator::ReportingGraphDependencies; use super::executor::ReportingExecutionError; use super::types::{ - DateArgs, ReportingContext, ReportingProductKind, ReportingProducts, ReportingStep, - ReportingStepArgs, ReportingStepId, VoidArgs, + BalancesBetween, DateArgs, ReportingContext, ReportingProduct, ReportingProductKind, + ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId, VoidArgs, }; /// Call [ReportingContext::register_lookup_fn] for all steps provided by this module @@ -96,6 +96,64 @@ impl ReportingStep for AllTransactionsExceptRetainedEarnings { args: self.args.clone(), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get all dependencies + let step_dependencies = dependencies.dependencies_for_step(&self.id()); + + // Identify the product_kinds dependency most recently generated + if self.product_kinds.len() != 1 { + panic!("AllTransactionsExceptRetainedEarnings.product_kinds.len() != 1"); + } + let product_kind = self.product_kinds[0]; + + for (product_id, product) in products.map().iter().rev() { + if step_dependencies.iter().any(|d| d.product == *product_id) { + // Store the result + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product.clone(), + ); + + return Ok(()); + } + } + + // No dependencies?! - store empty result + let product: Box = match self.product_kinds[0] { + ReportingProductKind::Transactions => Box::new(Transactions { + transactions: Vec::new(), + }), + ReportingProductKind::BalancesAt => Box::new(BalancesAt { + balances: HashMap::new(), + }), + ReportingProductKind::BalancesBetween => Box::new(BalancesBetween { + balances: HashMap::new(), + }), + ReportingProductKind::Generic => panic!("Requested AllTransactionsExceptRetainedEarnings.Generic but no available dependencies to provide it"), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: product_kind, + args: self.args.clone(), + }, + product, + ); + + Ok(()) + } } #[derive(Debug)] @@ -155,6 +213,62 @@ impl ReportingStep for AllTransactionsIncludingRetainedEarnings { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Get opening balances from AllTransactionsExceptRetainedEarnings + let opening_balances = products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptRetainedEarnings", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Get RetainedEarningsToEquity transactions + let transactions = products + .get_or_err(&ReportingProductId { + name: "RetainedEarningsToEquity", + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap(); + + // Update balances + let mut balances = BalancesAt { + balances: opening_balances.balances.clone(), + }; + + for transaction in transactions.transactions.iter() { + for posting in transaction.postings.iter() { + // FIXME: Do currency conversion + let running_balance = + balances.balances.get(&posting.account).unwrap_or(&0) + posting.quantity; + balances + .balances + .insert(posting.account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -226,6 +340,31 @@ impl ReportingStep for CalculateIncomeTax { } } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: CalculateIncomeTax.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -285,6 +424,44 @@ impl ReportingStep for CombineOrdinaryTransactions { }, ] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + // Sum balances of all dependencies + + let mut balances = BalancesAt { + balances: HashMap::new(), + }; + + for dependency in dependencies.dependencies_for_step(&self.id()) { + let dependency_balances = &products + .get_or_err(&dependency.product)? + .downcast_ref::() + .unwrap() + .balances; + for (account, balance) in dependency_balances.iter() { + let running_balance = balances.balances.get(account).unwrap_or(&0) + balance; + balances.balances.insert(account.clone(), running_balance); + } + } + + // Store result + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }, + Box::new(balances), + ); + + Ok(()) + } } #[derive(Debug)] @@ -331,6 +508,8 @@ impl ReportingStep for DBBalances { fn execute( &self, _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { eprintln!("Stub: DBBalances.execute"); @@ -345,7 +524,7 @@ impl ReportingStep for DBBalances { kind: ReportingProductKind::BalancesAt, args: Box::new(self.args.clone()), }, - ReportingProduct::BalancesAt(balances), + Box::new(balances), ); Ok(()) @@ -392,6 +571,31 @@ impl ReportingStep for PostUnreconciledStatementLines { args: Box::new(self.args.clone()), } } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: PostUnreconciledStatementLines.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } #[derive(Debug)] @@ -448,4 +652,29 @@ impl ReportingStep for RetainedEarningsToEquity { }), }] } + + fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &mut ReportingProducts, + ) -> Result<(), ReportingExecutionError> { + eprintln!("Stub: RetainedEarningsToEquity.execute"); + + let transactions = Transactions { + transactions: Vec::new(), + }; + + products.insert( + ReportingProductId { + name: self.id().name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + Box::new(transactions), + ); + + Ok(()) + } } diff --git a/src/reporting/types.rs b/src/reporting/types.rs index 7272eb3..99a3494 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -24,7 +24,9 @@ use downcast_rs::Downcast; use dyn_clone::DynClone; use dyn_eq::DynEq; use dyn_hash::DynHash; +use indexmap::IndexMap; +use crate::transaction::TransactionWithPostings; use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; @@ -121,7 +123,7 @@ pub struct ReportingStepDynamicBuilder { // REPORTING PRODUCTS /// Identifies a [ReportingProduct] -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct ReportingProductId { pub name: &'static str, pub kind: ReportingProductKind, @@ -144,33 +146,84 @@ pub enum ReportingProductKind { } /// Represents the result of a [ReportingStep] -#[derive(Debug)] -pub enum ReportingProduct { - Transactions(Transactions), - BalancesAt(BalancesAt), - BalancesBetween(BalancesBetween), - Generic(Box), -} +pub trait ReportingProduct: Debug + Downcast + DynClone {} + +downcast_rs::impl_downcast!(ReportingProduct); +dyn_clone::clone_trait_object!(ReportingProduct); /// Records a list of transactions generated by a [ReportingStep] -#[derive(Debug)] -pub struct Transactions {} +#[derive(Clone, Debug)] +pub struct Transactions { + pub transactions: Vec, +} + +impl ReportingProduct for Transactions {} /// Records cumulative account balances at a particular point in time -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct BalancesAt { pub balances: HashMap, } +impl ReportingProduct for BalancesAt {} + /// Records the total value of transactions in each account between two points in time -#[derive(Debug)] -pub struct BalancesBetween {} +#[derive(Clone, Debug)] +pub struct BalancesBetween { + pub balances: HashMap, +} + +impl ReportingProduct for BalancesBetween {} /// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct: Debug {} +pub trait GenericReportingProduct: Debug + ReportingProduct {} -/// Convenience type mapping [ReportingProductId] to [ReportingProduct] -pub type ReportingProducts = HashMap; +/// Map from [ReportingProductId] to [ReportingProduct] +#[derive(Clone, Debug)] +pub struct ReportingProducts { + map: IndexMap>, +} + +impl ReportingProducts { + pub fn new() -> Self { + Self { + map: IndexMap::new(), + } + } + + pub fn map(&self) -> &IndexMap> { + &self.map + } + + pub fn insert(&mut self, key: ReportingProductId, value: Box) { + self.map.insert(key, value); + } + + pub fn get_or_err( + &self, + key: &ReportingProductId, + ) -> Result<&Box, ReportingExecutionError> { + match self.map.get(key) { + Some(value) => Ok(value), + None => Err(ReportingExecutionError::DependencyNotAvailable { + message: format!("Product {} not available when expected", key), + }), + } + } +} + +impl Display for ReportingProducts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "ReportingProducts {{\n{}\n}}", + self.map + .iter() + .map(|(k, v)| format!(" {}: {:?}", k, v)) + .collect::>() + .join(",\n") + )) + } +} // --------------- // REPORTING STEPS @@ -230,6 +283,8 @@ pub trait ReportingStep: Debug + Display + Downcast { fn execute( &self, context: &ReportingContext, + steps: &Vec>, + dependencies: &ReportingGraphDependencies, products: &mut ReportingProducts, ) -> Result<(), ReportingExecutionError> { todo!("{}", self); diff --git a/src/transaction.rs b/src/transaction.rs index a17ebbc..5a4c1aa 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,25 +1,44 @@ /* DrCr: Web-based double-entry bookkeeping framework Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) - + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. - + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use chrono::NaiveDateTime; +use crate::QuantityInt; + +#[derive(Clone, Debug)] pub struct Transaction { pub id: Option, pub dt: NaiveDateTime, pub description: String, } + +#[derive(Clone, Debug)] +pub struct TransactionWithPostings { + pub transaction: Transaction, + pub postings: Vec, +} + +#[derive(Clone, Debug)] +pub struct Posting { + pub id: Option, + pub transaction_id: Option, + pub description: String, + pub account: String, + pub quantity: QuantityInt, + pub commodity: String, +}