diff --git a/src/db.rs b/src/db.rs index 05d1595..684d1f1 100644 --- a/src/db.rs +++ b/src/db.rs @@ -18,11 +18,12 @@ use std::collections::HashMap; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use sqlx::sqlite::SqliteRow; use sqlx::{Connection, Row, SqliteConnection}; use crate::account_config::AccountConfiguration; +use crate::statements::StatementLine; use crate::{util::format_date, QuantityInt}; pub struct DbConnection { @@ -51,6 +52,39 @@ impl DbConnection { .expect("SQL error") } + /// Get account configurations from the database + pub async fn get_account_configurations(&self) -> Vec { + let mut connection = self.connect().await; + + let mut account_configurations = + sqlx::query("SELECT id, account, kind, data FROM account_configurations") + .map(|r: SqliteRow| AccountConfiguration { + id: r.get("id"), + account: r.get("account"), + kind: r.get("kind"), + data: r.get("data"), + }) + .fetch_all(&mut connection) + .await + .expect("SQL error"); + + // System accounts + account_configurations.push(AccountConfiguration { + id: None, + account: "Current Year Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + account_configurations.push(AccountConfiguration { + id: None, + account: "Retained Earnings".to_string(), + kind: "drcr.equity".to_string(), + data: None, + }); + + account_configurations + } + /// Get account balances from the database pub async fn get_balances(&self, date: NaiveDate) -> HashMap { let mut connection = self.connect().await; @@ -83,37 +117,26 @@ impl DbConnection { balances } - /// Get account configurations from the database - pub async fn get_account_configurations(&self) -> Vec { + /// Get unreconciled statement lines from the database + pub async fn get_unreconciled_statement_lines(&self) -> Vec { let mut connection = self.connect().await; - let mut account_configurations = - sqlx::query("SELECT id, account, kind, data FROM account_configurations") - .map(|r: SqliteRow| AccountConfiguration { - id: r.get("id"), - account: r.get("account"), - kind: r.get("kind"), - data: r.get("data"), - }) - .fetch_all(&mut connection) - .await - .expect("SQL error"); + let rows = sqlx::query( + // On testing, JOIN is much faster than WHERE NOT EXISTS + "SELECT statement_lines.* FROM statement_lines + LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id + WHERE statement_line_reconciliations.id IS NULL" + ).map(|r: SqliteRow| StatementLine { + id: Some(r.get("id")), + source_account: r.get("source_account"), + dt: NaiveDateTime::parse_from_str(r.get("dt"), "%Y-%m-%d").expect("Invalid statement_lines.dt"), + description: r.get("description"), + quantity: r.get("quantity"), + balance: r.get("balance"), + commodity: r.get("commodity"), + }).fetch_all(&mut connection).await.expect("SQL error"); - // System accounts - account_configurations.push(AccountConfiguration { - id: None, - account: "Current Year Earnings".to_string(), - kind: "drcr.equity".to_string(), - data: None, - }); - account_configurations.push(AccountConfiguration { - id: None, - account: "Retained Earnings".to_string(), - kind: "drcr.equity".to_string(), - data: None, - }); - - account_configurations + rows } } diff --git a/src/lib.rs b/src/lib.rs index 4400e00..a5404eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod account_config; pub mod db; pub mod reporting; pub mod transaction; +pub mod statements; pub mod util; pub type QuantityInt = i64; diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs index 1fa5012..b5ced36 100644 --- a/src/reporting/builders.rs +++ b/src/reporting/builders.rs @@ -33,7 +33,7 @@ use super::executor::ReportingExecutionError; use super::types::{ BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, - ReportingStepDynamicBuilder, ReportingStepId, Transactions, + ReportingStepDynamicBuilder, ReportingStepId, Transactions, VoidArgs, }; /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module @@ -241,6 +241,7 @@ impl GenerateBalances { ) -> bool { // Check for Transactions -> BalancesAt if kind == ReportingProductKind::BalancesAt { + // Try DateArgs match has_step_or_can_build( &ReportingProductId { name, @@ -266,6 +267,33 @@ impl GenerateBalances { } HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} } + + // Try VoidArgs + match has_step_or_can_build( + &ReportingProductId { + name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + 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; } @@ -301,31 +329,66 @@ impl ReportingStep for GenerateBalances { } } - 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()), - }] + 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 + + // Try DateArgs + match has_step_or_can_build( + &ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + steps, + dependencies, + context, + ) { + HasStepOrCanBuild::HasStep(_) + | HasStepOrCanBuild::CanLookup(_) + | HasStepOrCanBuild::CanBuild(_) => { + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(self.args.clone()), + }, + ); + return; + } + HasStepOrCanBuild::None => (), + } + + // Must be VoidArgs (as checked in can_build) + dependencies.add_dependency( + self.id(), + ReportingProductId { + name: self.step_name, + kind: ReportingProductKind::Transactions, + args: Box::new(VoidArgs {}), + }, + ); } async fn execute( &self, _context: &ReportingContext, _steps: &Vec>, - _dependencies: &ReportingGraphDependencies, + dependencies: &ReportingGraphDependencies, products: &RwLock, ) -> Result { let products = products.read().await; // Get the transactions + let transactions_product = &dependencies.dependencies_for_step(&self.id())[0].product; let transactions = &products - .get_or_err(&ReportingProductId { - name: self.step_name, - kind: ReportingProductKind::Transactions, - args: Box::new(self.args.clone()), - })? + .get_or_err(transactions_product)? .downcast_ref::() .unwrap() .transactions; diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs index 076ae80..2e3a1de 100644 --- a/src/reporting/steps.rs +++ b/src/reporting/steps.rs @@ -1075,9 +1075,7 @@ impl ReportingStep for IncomeStatement { /// Generate transactions for unreconciled statement lines #[derive(Debug)] -pub struct PostUnreconciledStatementLines { - pub args: DateArgs, -} +pub struct PostUnreconciledStatementLines {} impl PostUnreconciledStatementLines { fn register_lookup_fn(context: &mut ReportingContext) { @@ -1090,13 +1088,11 @@ impl PostUnreconciledStatementLines { } fn takes_args(args: &Box) -> bool { - args.is::() + args.is::() } - fn from_args(args: Box) -> Box { - Box::new(PostUnreconciledStatementLines { - args: *args.downcast().unwrap(), - }) + fn from_args(_args: Box) -> Box { + Box::new(PostUnreconciledStatementLines {}) } } @@ -1112,30 +1108,67 @@ impl ReportingStep for PostUnreconciledStatementLines { ReportingStepId { name: "PostUnreconciledStatementLines", product_kinds: &[ReportingProductKind::Transactions], - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), } } async fn execute( &self, - _context: &ReportingContext, + context: &ReportingContext, _steps: &Vec>, _dependencies: &ReportingGraphDependencies, _products: &RwLock, ) -> Result { - eprintln!("Stub: PostUnreconciledStatementLines.execute"); + let unreconciled_statement_lines = context + .db_connection + .get_unreconciled_statement_lines() + .await; - let transactions = Transactions { + // Post unreconciled statement lines + let mut transactions = Transactions { transactions: Vec::new(), }; + for line in unreconciled_statement_lines { + let unclassified_account = if line.quantity >= 0 { + "Unclassified Statement Line Debits" + } else { + "Unclassified Statement Line Credits" + }; + transactions.transactions.push(TransactionWithPostings { + transaction: Transaction { + id: None, + dt: line.dt, + description: line.description.clone(), + }, + postings: vec![ + Posting { + id: None, + transaction_id: None, + description: None, + account: line.source_account.clone(), + quantity: line.quantity, + commodity: line.commodity.clone(), + }, + Posting { + id: None, + transaction_id: None, + description: None, + account: unclassified_account.to_string(), + quantity: -line.quantity, + commodity: line.commodity.clone(), + }, + ], + }); + } + // Store result let mut result = ReportingProducts::new(); result.insert( ReportingProductId { name: self.id().name, kind: ReportingProductKind::Transactions, - args: Box::new(self.args.clone()), + args: Box::new(VoidArgs {}), }, Box::new(transactions), ); diff --git a/src/statements.rs b/src/statements.rs new file mode 100644 index 0000000..87a4123 --- /dev/null +++ b/src/statements.rs @@ -0,0 +1,31 @@ +/* + 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; + +pub struct StatementLine { + pub id: Option, + pub source_account: String, + pub dt: NaiveDateTime, + pub description: String, + pub quantity: QuantityInt, + pub balance: QuantityInt, + pub commodity: String, +}