Implement PostUnreconciledStatementLines

This commit is contained in:
RunasSudo 2025-05-27 17:54:48 +10:00
parent bb8383b222
commit 2a2fb5764c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 207 additions and 56 deletions

View File

@ -18,11 +18,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use chrono::NaiveDate; use chrono::{NaiveDate, NaiveDateTime};
use sqlx::sqlite::SqliteRow; use sqlx::sqlite::SqliteRow;
use sqlx::{Connection, Row, SqliteConnection}; use sqlx::{Connection, Row, SqliteConnection};
use crate::account_config::AccountConfiguration; use crate::account_config::AccountConfiguration;
use crate::statements::StatementLine;
use crate::{util::format_date, QuantityInt}; use crate::{util::format_date, QuantityInt};
pub struct DbConnection { pub struct DbConnection {
@ -51,6 +52,39 @@ impl DbConnection {
.expect("SQL error") .expect("SQL error")
} }
/// Get account configurations from the database
pub async fn get_account_configurations(&self) -> Vec<AccountConfiguration> {
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 /// Get account balances from the database
pub async fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> { pub async fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> {
let mut connection = self.connect().await; let mut connection = self.connect().await;
@ -83,37 +117,26 @@ impl DbConnection {
balances balances
} }
/// Get account configurations from the database /// Get unreconciled statement lines from the database
pub async fn get_account_configurations(&self) -> Vec<AccountConfiguration> { pub async fn get_unreconciled_statement_lines(&self) -> Vec<StatementLine> {
let mut connection = self.connect().await; let mut connection = self.connect().await;
let mut account_configurations = let rows = sqlx::query(
sqlx::query("SELECT id, account, kind, data FROM account_configurations") // On testing, JOIN is much faster than WHERE NOT EXISTS
.map(|r: SqliteRow| AccountConfiguration { "SELECT statement_lines.* FROM statement_lines
id: r.get("id"), LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id
account: r.get("account"), WHERE statement_line_reconciliations.id IS NULL"
kind: r.get("kind"), ).map(|r: SqliteRow| StatementLine {
data: r.get("data"), id: Some(r.get("id")),
}) source_account: r.get("source_account"),
.fetch_all(&mut connection) dt: NaiveDateTime::parse_from_str(r.get("dt"), "%Y-%m-%d").expect("Invalid statement_lines.dt"),
.await description: r.get("description"),
.expect("SQL error"); quantity: r.get("quantity"),
balance: r.get("balance"),
commodity: r.get("commodity"),
}).fetch_all(&mut connection).await.expect("SQL error");
// System accounts rows
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
} }
} }

View File

@ -2,6 +2,7 @@ pub mod account_config;
pub mod db; pub mod db;
pub mod reporting; pub mod reporting;
pub mod transaction; pub mod transaction;
pub mod statements;
pub mod util; pub mod util;
pub type QuantityInt = i64; pub type QuantityInt = i64;

View File

@ -33,7 +33,7 @@ use super::executor::ReportingExecutionError;
use super::types::{ use super::types::{
BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext, BalancesAt, BalancesBetween, DateArgs, DateStartDateEndArgs, ReportingContext,
ReportingProductId, ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, 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 /// Call [ReportingContext::register_dynamic_builder] for all dynamic builders provided by this module
@ -241,6 +241,7 @@ impl GenerateBalances {
) -> bool { ) -> bool {
// Check for Transactions -> BalancesAt // Check for Transactions -> BalancesAt
if kind == ReportingProductKind::BalancesAt { if kind == ReportingProductKind::BalancesAt {
// Try DateArgs
match has_step_or_can_build( match has_step_or_can_build(
&ReportingProductId { &ReportingProductId {
name, name,
@ -266,6 +267,33 @@ impl GenerateBalances {
} }
HasStepOrCanBuild::CanBuild(_) | HasStepOrCanBuild::None => {} 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; return false;
} }
@ -301,31 +329,66 @@ impl ReportingStep for GenerateBalances {
} }
} }
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> { fn init_graph(
// GenerateBalances depends on Transactions &self,
vec![ReportingProductId { steps: &Vec<Box<dyn ReportingStep>>,
name: self.step_name, dependencies: &mut ReportingGraphDependencies,
kind: ReportingProductKind::Transactions, context: &ReportingContext,
args: Box::new(self.args.clone()), ) {
}] // 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( async fn execute(
&self, &self,
_context: &ReportingContext, _context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, dependencies: &ReportingGraphDependencies,
products: &RwLock<ReportingProducts>, products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await; let products = products.read().await;
// Get the transactions // Get the transactions
let transactions_product = &dependencies.dependencies_for_step(&self.id())[0].product;
let transactions = &products let transactions = &products
.get_or_err(&ReportingProductId { .get_or_err(transactions_product)?
name: self.step_name,
kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()),
})?
.downcast_ref::<Transactions>() .downcast_ref::<Transactions>()
.unwrap() .unwrap()
.transactions; .transactions;

View File

@ -1075,9 +1075,7 @@ impl ReportingStep for IncomeStatement {
/// Generate transactions for unreconciled statement lines /// Generate transactions for unreconciled statement lines
#[derive(Debug)] #[derive(Debug)]
pub struct PostUnreconciledStatementLines { pub struct PostUnreconciledStatementLines {}
pub args: DateArgs,
}
impl PostUnreconciledStatementLines { impl PostUnreconciledStatementLines {
fn register_lookup_fn(context: &mut ReportingContext) { fn register_lookup_fn(context: &mut ReportingContext) {
@ -1090,13 +1088,11 @@ impl PostUnreconciledStatementLines {
} }
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool { fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
args.is::<DateArgs>() args.is::<VoidArgs>()
} }
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> { fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(PostUnreconciledStatementLines { Box::new(PostUnreconciledStatementLines {})
args: *args.downcast().unwrap(),
})
} }
} }
@ -1112,30 +1108,67 @@ impl ReportingStep for PostUnreconciledStatementLines {
ReportingStepId { ReportingStepId {
name: "PostUnreconciledStatementLines", name: "PostUnreconciledStatementLines",
product_kinds: &[ReportingProductKind::Transactions], product_kinds: &[ReportingProductKind::Transactions],
args: Box::new(self.args.clone()), args: Box::new(VoidArgs {}),
} }
} }
async fn execute( async fn execute(
&self, &self,
_context: &ReportingContext, context: &ReportingContext,
_steps: &Vec<Box<dyn ReportingStep>>, _steps: &Vec<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies, _dependencies: &ReportingGraphDependencies,
_products: &RwLock<ReportingProducts>, _products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
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(), 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 // Store result
let mut result = ReportingProducts::new(); let mut result = ReportingProducts::new();
result.insert( result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
args: Box::new(self.args.clone()), args: Box::new(VoidArgs {}),
}, },
Box::new(transactions), Box::new(transactions),
); );

31
src/statements.rs Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
*/
use chrono::NaiveDateTime;
use crate::QuantityInt;
pub struct StatementLine {
pub id: Option<u64>,
pub source_account: String,
pub dt: NaiveDateTime,
pub description: String,
pub quantity: QuantityInt,
pub balance: QuantityInt,
pub commodity: String,
}