From 00c7833706c2fe1eb4ca25093405dc78431eca48 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Tue, 27 May 2025 22:19:36 +1000 Subject: [PATCH] Implement trial balance report --- src/main.rs | 36 ++++++++ src/reporting/dynamic_report.rs | 11 ++- src/reporting/steps.rs | 153 +++++++++++++++++++++++++++++++- src/reporting/types.rs | 3 - 4 files changed, 197 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7ab29a9..0dfcdc4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,4 +145,40 @@ async fn main() { "{}", result.downcast_ref::().unwrap().to_json() ); + + // Get trial balance + + let targets = vec![ + ReportingProductId { + name: "CalculateIncomeTax", + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }, + ]; + + let products = generate_report(targets, Arc::clone(&context)) + .await + .unwrap(); + let result = products + .get_or_err(&ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(DateArgs { + date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(), + }), + }) + .unwrap(); + + println!("Trial balance:"); + println!( + "{}", + result.downcast_ref::().unwrap().to_json() + ); } diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs index aae7fa1..131cb07 100644 --- a/src/reporting/dynamic_report.rs +++ b/src/reporting/dynamic_report.rs @@ -25,7 +25,7 @@ use serde::{Deserialize, Serialize}; use crate::QuantityInt; -use super::types::{GenericReportingProduct, ReportingProduct}; +use super::types::ReportingProduct; /// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry] #[derive(Clone, Debug)] @@ -184,6 +184,14 @@ pub struct DynamicReport { } impl DynamicReport { + pub fn new(title: String, columns: Vec, entries: Vec) -> Self { + Self { + title, + columns, + entries, + } + } + /// Remove all entries from the report where auto_hide is enabled and quantity is zero pub fn auto_hide(&mut self) { self.entries.retain_mut(|e| match e { @@ -212,7 +220,6 @@ impl DynamicReport { } } -impl GenericReportingProduct for DynamicReport {} impl ReportingProduct for DynamicReport {} #[derive(Clone, Debug)] diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 2e3a1de..893026a 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -36,7 +36,7 @@ use crate::QuantityInt; use super::calculator::ReportingGraphDependencies; use super::dynamic_report::{ entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry, - CalculatableSection, CalculatedRow, LiteralRow, + CalculatableSection, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, }; use super::executor::ReportingExecutionError; use super::types::{ @@ -57,6 +57,7 @@ pub fn register_lookup_fns(context: &mut ReportingContext) { IncomeStatement::register_lookup_fn(context); PostUnreconciledStatementLines::register_lookup_fn(context); RetainedEarningsToEquity::register_lookup_fn(context); + TrialBalance::register_lookup_fn(context); } /// Target representing all transactions except charging current year and retained earnings to equity @@ -1313,3 +1314,153 @@ impl ReportingStep for RetainedEarningsToEquity { Ok(result) } } + +/// Generates a trial balance [DynamicReport] +#[derive(Debug)] +pub struct TrialBalance { + pub args: DateArgs, +} + +impl TrialBalance { + fn register_lookup_fn(context: &mut ReportingContext) { + context.register_lookup_fn( + "TrialBalance", + &[ReportingProductKind::Generic], + Self::takes_args, + Self::from_args, + ); + } + + fn takes_args(args: &Box) -> bool { + args.is::() + } + + fn from_args(args: Box) -> Box { + Box::new(TrialBalance { + args: *args.downcast().unwrap(), + }) + } +} + +impl Display for TrialBalance { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}", self.id())) + } +} + +#[async_trait] +impl ReportingStep for TrialBalance { + fn id(&self) -> ReportingStepId { + ReportingStepId { + name: "TrialBalance", + product_kinds: &[ReportingProductKind::Generic], + args: Box::new(self.args.clone()), + } + } + + fn requires(&self, _context: &ReportingContext) -> Vec { + let mut result = Vec::new(); + + // TrialBalance depends on AllTransactionsExceptEarningsToEquity at the requested date + result.push(ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + }); + + result + } + + async fn execute( + &self, + _context: &ReportingContext, + _steps: &Vec>, + _dependencies: &ReportingGraphDependencies, + products: &RwLock, + ) -> Result { + let products = products.read().await; + + // Get balances for each period + let balances = &products + .get_or_err(&ReportingProductId { + name: "AllTransactionsExceptEarningsToEquity", + kind: ReportingProductKind::BalancesAt, + args: Box::new(self.args.clone()), + })? + .downcast_ref::() + .unwrap() + .balances; + + // Get sorted list of accounts + let mut accounts = balances.keys().collect::>(); + accounts.sort(); + + // Get total debits and credits + let total_dr = balances.values().filter(|b| **b >= 0).sum::(); + let total_cr = -balances.values().filter(|b| **b < 0).sum::(); + + // Init report + let mut report = DynamicReport::new( + "Trial balance".to_string(), + vec!["Dr".to_string(), "Cr".to_string()], + { + let mut entries = Vec::new(); + + // Entry for each account + for account in accounts { + entries.push(DynamicReportEntry::LiteralRow(LiteralRow { + text: account.clone(), + quantity: vec![ + // Dr cell + if balances[account] >= 0 { + balances[account] + } else { + 0 + }, + // Cr cell + if balances[account] < 0 { + -balances[account] + } else { + 0 + }, + ], + id: None, + visible: true, + auto_hide: true, + link: None, + heading: false, + bordered: false, + })); + } + + // Total row + entries.push(DynamicReportEntry::LiteralRow(LiteralRow { + text: "Totals".to_string(), + quantity: vec![total_dr, total_cr], + id: Some("totals".to_string()), + visible: true, + auto_hide: false, + link: None, + heading: true, + bordered: true, + })); + + entries + }, + ); + + report.auto_hide(); + + // Store result + let mut result = ReportingProducts::new(); + result.insert( + ReportingProductId { + name: "TrialBalance", + kind: ReportingProductKind::Generic, + args: Box::new(self.args.clone()), + }, + Box::new(report), + ); + Ok(result) + } +} diff --git a/src/reporting/types.rs b/src/reporting/types.rs index d1e6428..253f25b 100644 --- a/src/reporting/types.rs +++ b/src/reporting/types.rs @@ -190,9 +190,6 @@ pub struct BalancesBetween { impl ReportingProduct for BalancesBetween {} -/// Represents a custom [ReportingProduct] generated by a [ReportingStep] -pub trait GenericReportingProduct: Debug + ReportingProduct {} - /// Map from [ReportingProductId] to [ReportingProduct] #[derive(Clone, Debug)] pub struct ReportingProducts {