/* 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 std::fmt::Display; use super::{ calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingStep, ReportingStepArgs, ReportingStepDynamicBuilder, ReportingStepId, }; pub fn register_dynamic_builders(context: &mut ReportingContext) { context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "GenerateBalances", can_build: GenerateBalances::can_build, build: GenerateBalances::build, }); context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesBetween", can_build: UpdateBalancesBetween::can_build, build: UpdateBalancesBetween::build, }); context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "UpdateBalancesAt", can_build: UpdateBalancesAt::can_build, build: UpdateBalancesAt::build, }); // This is the least efficient way of generating BalancesBetween context.register_dynamic_builder(ReportingStepDynamicBuilder { name: "BalancesAtToBalancesBetween", can_build: BalancesAtToBalancesBetween::can_build, build: BalancesAtToBalancesBetween::build, }); } #[derive(Debug)] pub struct BalancesAtToBalancesBetween { step_name: &'static str, args: DateStartDateEndArgs, } impl BalancesAtToBalancesBetween { // Implements BalancesAt, BalancesAt -> BalancesBetween fn can_build( name: &'static str, kind: ReportingProductKind, args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, ) -> bool { // Check for BalancesAt, BalancesAt -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { if !args.is::() { return false; } let args = args.downcast_ref::().unwrap(); match has_step_or_can_build( &ReportingProductId { name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: args.date_start.clone(), }), }, steps, dependencies, context, ) { HasStepOrCanBuild::HasStep(_) | HasStepOrCanBuild::CanLookup(_) | HasStepOrCanBuild::CanBuild(_) => { return true; } HasStepOrCanBuild::None => {} } } return false; } fn build( name: &'static str, _kind: ReportingProductKind, args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(BalancesAtToBalancesBetween { step_name: name, args: *args.downcast().unwrap(), }) } } impl Display for BalancesAtToBalancesBetween { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!( "{} {{BalancesAtToBalancesBetween}}", self.id() )) } } impl ReportingStep for BalancesAtToBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], args: Box::new(self.args.clone()), } } fn requires(&self, _context: &ReportingContext) -> Vec { // BalancesAtToBalancesBetween depends on BalancesAt at both time points vec![ 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 }), }, ReportingProductId { name: self.step_name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: self.args.date_end.clone(), }), }, ] } } #[derive(Debug)] pub struct GenerateBalances { step_name: &'static str, args: DateArgs, } impl GenerateBalances { // Implements (() -> Transactions) -> BalancesAt fn can_build( name: &'static str, kind: ReportingProductKind, args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, ) -> bool { // Check for Transactions -> BalancesAt if kind == ReportingProductKind::BalancesAt { match has_step_or_can_build( &ReportingProductId { name, kind: ReportingProductKind::Transactions, args: args.clone(), }, steps, dependencies, context, ) { HasStepOrCanBuild::HasStep(step) => { // Check for () -> Transactions if dependencies.dependencies_for_step(&step.id()).len() == 0 { return true; } } HasStepOrCanBuild::CanLookup(lookup_fn) => { // Check for () -> Transactions let step = lookup_fn(args.clone()); if step.requires(context).len() == 0 { return true; } } HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} } } return false; } fn build( name: &'static str, _kind: ReportingProductKind, args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(GenerateBalances { step_name: name, args: *args.downcast().unwrap(), }) } } impl Display for GenerateBalances { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{} {{GenerateBalances}}", self.id())) } } impl ReportingStep for GenerateBalances { fn id(&self) -> ReportingStepId { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesAt], args: Box::new(self.args.clone()), } } fn requires(&self, _context: &ReportingContext) -> Vec { // GenerateBalances depends on Transactions vec![ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, args: Box::new(self.args.clone()), }] } } #[derive(Debug)] pub struct UpdateBalancesAt { step_name: &'static str, args: DateArgs, } impl UpdateBalancesAt { // Implements (BalancesAt -> Transactions) -> BalancesAt fn can_build( name: &'static str, kind: ReportingProductKind, _args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, context: &ReportingContext, ) -> bool { // Check for Transactions -> BalancesAt if kind == ReportingProductKind::BalancesAt { // Initially no need to check args if let Some(step) = steps.iter().find(|s| { s.id().name == name && s.id() .product_kinds .contains(&ReportingProductKind::Transactions) }) { // Check for BalancesAt -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesAt { return true; } // Check if BalancesBetween -> Transactions and BalancesAt is available if dependencies_for_step.len() == 1 && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesBetween { let date_end = dependencies_for_step[0] .dependency .args .downcast_ref::() .unwrap() .date_end; match has_step_or_can_build( &ReportingProductId { name: dependencies_for_step[0].dependency.name, kind: ReportingProductKind::BalancesAt, args: Box::new(DateArgs { date: date_end }), }, steps, dependencies, context, ) { HasStepOrCanBuild::HasStep(_) | HasStepOrCanBuild::CanLookup(_) | HasStepOrCanBuild::CanBuild(_) => { return true; } HasStepOrCanBuild::None => {} } } } } return false; } fn build( name: &'static str, _kind: ReportingProductKind, args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(UpdateBalancesAt { step_name: name, args: *args.downcast().unwrap(), }) } } impl Display for UpdateBalancesAt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{} {{UpdateBalancesAt}}", self.id())) } } impl ReportingStep for UpdateBalancesAt { fn id(&self) -> ReportingStepId { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesAt], args: Box::new(self.args.clone()), } } fn init_graph( &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args let parent_step = steps .iter() .find(|s| { s.id().name == self.step_name && s.id() .product_kinds .contains(&ReportingProductKind::Transactions) }) .unwrap(); // Existence is checked in can_build dependencies.add_dependency( self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, args: parent_step.id().args.clone(), }, ); } } #[derive(Debug)] pub struct UpdateBalancesBetween { step_name: &'static str, args: DateStartDateEndArgs, } impl UpdateBalancesBetween { // Implements (BalancesBetween -> Transactions) -> BalancesBetween fn can_build( name: &'static str, kind: ReportingProductKind, _args: &Box, steps: &Vec>, dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> bool { // Check for Transactions -> BalancesBetween if kind == ReportingProductKind::BalancesBetween { // Initially no need to check args if let Some(step) = steps.iter().find(|s| { s.id().name == name && s.id() .product_kinds .contains(&ReportingProductKind::Transactions) }) { // Check for BalancesBetween -> Transactions let dependencies_for_step = dependencies.dependencies_for_step(&step.id()); if dependencies_for_step.len() == 1 && dependencies_for_step[0].dependency.kind == ReportingProductKind::BalancesBetween { return true; } } // Check lookup or builder - with args /*match has_step_or_can_build( &ReportingProductId { name, kind: ReportingProductKind::Transactions, args: args.clone(), }, steps, dependencies, context, ) { HasStepOrCanBuild::HasStep(step) => unreachable!(), HasStepOrCanBuild::CanLookup(_) | HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} }*/ } return false; } fn build( name: &'static str, _kind: ReportingProductKind, args: Box, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _context: &ReportingContext, ) -> Box { Box::new(UpdateBalancesBetween { step_name: name, args: *args.downcast().unwrap(), }) } } impl Display for UpdateBalancesBetween { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_fmt(format_args!("{} {{UpdateBalancesBetween}}", self.id())) } } impl ReportingStep for UpdateBalancesBetween { fn id(&self) -> ReportingStepId { ReportingStepId { name: self.step_name, product_kinds: &[ReportingProductKind::BalancesBetween], args: Box::new(self.args.clone()), } } fn init_graph( &self, steps: &Vec>, dependencies: &mut ReportingGraphDependencies, _context: &ReportingContext, ) { // Add a dependency on the Transactions result // Look up that step, so we can extract the appropriate args let parent_step = steps .iter() .find(|s| { s.id().name == self.step_name && s.id() .product_kinds .contains(&ReportingProductKind::Transactions) }) .unwrap(); // Existence is checked in can_build dependencies.add_dependency( self.id(), ReportingProductId { name: self.step_name, kind: ReportingProductKind::Transactions, args: parent_step.id().args.clone(), }, ); } }