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 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<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
pub async fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> {
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<AccountConfiguration> {
/// Get unreconciled statement lines from the database
pub async fn get_unreconciled_statement_lines(&self) -> Vec<StatementLine> {
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
}
}

View File

@ -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;

View File

@ -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<ReportingProductId> {
// 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<Box<dyn ReportingStep>>,
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<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
dependencies: &ReportingGraphDependencies,
products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> {
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::<Transactions>()
.unwrap()
.transactions;

View File

@ -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<dyn ReportingStepArgs>) -> bool {
args.is::<DateArgs>()
args.is::<VoidArgs>()
}
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
Box::new(PostUnreconciledStatementLines {
args: *args.downcast().unwrap(),
})
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
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<Box<dyn ReportingStep>>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock<ReportingProducts>,
) -> 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(),
};
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),
);

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,
}