Implement PostUnreconciledStatementLines
This commit is contained in:
parent
bb8383b222
commit
2a2fb5764c
81
src/db.rs
81
src/db.rs
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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
31
src/statements.rs
Normal 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,
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user