Implement getting transactions from database
This commit is contained in:
parent
00c7833706
commit
4f1db12688
48
src/db.rs
48
src/db.rs
@ -24,6 +24,7 @@ use sqlx::{Connection, Row, SqliteConnection};
|
||||
|
||||
use crate::account_config::AccountConfiguration;
|
||||
use crate::statements::StatementLine;
|
||||
use crate::transaction::{Posting, Transaction, TransactionWithPostings};
|
||||
use crate::{util::format_date, QuantityInt};
|
||||
|
||||
pub struct DbConnection {
|
||||
@ -90,7 +91,7 @@ impl DbConnection {
|
||||
let mut connection = self.connect().await;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"-- Get last transaction for each account
|
||||
"-- Get last transaction for each account
|
||||
WITH max_dt_by_account AS (
|
||||
SELECT account, max(dt) AS max_dt
|
||||
FROM joined_transactions
|
||||
@ -117,13 +118,56 @@ impl DbConnection {
|
||||
balances
|
||||
}
|
||||
|
||||
/// Get transactions from the database
|
||||
pub async fn get_transactions(&self) -> Vec<TransactionWithPostings> {
|
||||
let mut connection = self.connect().await;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT transaction_id, dt, transaction_description, id, description, account, quantity, commodity, quantity_ascost
|
||||
FROM transactions_with_quantity_ascost
|
||||
ORDER BY dt, transaction_id, id"
|
||||
).fetch_all(&mut connection).await.expect("SQL error");
|
||||
|
||||
// Un-flatten transaction list
|
||||
let mut transactions: Vec<TransactionWithPostings> = Vec::new();
|
||||
|
||||
for row in rows {
|
||||
if transactions.is_empty()
|
||||
|| transactions.last().unwrap().transaction.id != row.get("transaction_id")
|
||||
{
|
||||
// New transaction
|
||||
transactions.push(TransactionWithPostings {
|
||||
transaction: Transaction {
|
||||
id: row.get("transaction_id"),
|
||||
dt: NaiveDateTime::parse_from_str(row.get("dt"), "%Y-%m-%d %H:%M:%S.%6f")
|
||||
.expect("Invalid transactions.dt"),
|
||||
description: row.get("transaction_description"),
|
||||
},
|
||||
postings: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
transactions.last_mut().unwrap().postings.push(Posting {
|
||||
id: row.get("id"),
|
||||
transaction_id: row.get("transaction_id"),
|
||||
description: row.get("description"),
|
||||
account: row.get("account"),
|
||||
quantity: row.get("quantity"),
|
||||
commodity: row.get("commodity"),
|
||||
quantity_ascost: row.get("quantity_ascost"),
|
||||
});
|
||||
}
|
||||
|
||||
transactions
|
||||
}
|
||||
|
||||
/// Get unreconciled statement lines from the database
|
||||
pub async fn get_unreconciled_statement_lines(&self) -> Vec<StatementLine> {
|
||||
let mut connection = self.connect().await;
|
||||
|
||||
let rows = sqlx::query(
|
||||
// On testing, JOIN is much faster than WHERE NOT EXISTS
|
||||
"SELECT statement_lines.* FROM statement_lines
|
||||
"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 {
|
||||
|
@ -2,6 +2,7 @@ pub mod account_config;
|
||||
pub mod db;
|
||||
pub mod reporting;
|
||||
pub mod transaction;
|
||||
pub mod serde;
|
||||
pub mod statements;
|
||||
pub mod util;
|
||||
|
||||
|
59
src/main.rs
59
src/main.rs
@ -26,13 +26,13 @@ use libdrcr::reporting::dynamic_report::DynamicReport;
|
||||
use libdrcr::reporting::generate_report;
|
||||
use libdrcr::reporting::steps::register_lookup_fns;
|
||||
use libdrcr::reporting::types::{
|
||||
DateArgs, DateStartDateEndArgs, MultipleDateArgs, ReportingContext, ReportingProductId,
|
||||
ReportingProductKind, VoidArgs,
|
||||
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
|
||||
ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs,
|
||||
};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
const YEAR: i32 = 2023;
|
||||
const YEAR: i32 = 2025;
|
||||
|
||||
// Connect to database
|
||||
let db_connection = DbConnection::new("sqlite:drcr_testing.db").await;
|
||||
@ -56,6 +56,13 @@ async fn main() {
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(VoidArgs {}),
|
||||
},
|
||||
// ReportingProductId {
|
||||
// name: "AllTransactionsExceptEarningsToEquity",
|
||||
// kind: ReportingProductKind::Transactions,
|
||||
// args: Box::new(DateArgs {
|
||||
// date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
|
||||
// }),
|
||||
// },
|
||||
ReportingProductId {
|
||||
name: "BalanceSheet",
|
||||
kind: ReportingProductKind::Generic,
|
||||
@ -65,6 +72,16 @@ async fn main() {
|
||||
}],
|
||||
}),
|
||||
},
|
||||
ReportingProductId {
|
||||
name: "IncomeStatement",
|
||||
kind: ReportingProductKind::Generic,
|
||||
args: Box::new(MultipleDateStartDateEndArgs {
|
||||
dates: vec![DateStartDateEndArgs {
|
||||
date_start: NaiveDate::from_ymd_opt(YEAR - 1, 7, 1).unwrap(),
|
||||
date_end: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
|
||||
}],
|
||||
}),
|
||||
},
|
||||
];
|
||||
let (sorted_steps, dependencies) = steps_for_targets(targets, &context).unwrap();
|
||||
|
||||
@ -181,4 +198,40 @@ async fn main() {
|
||||
"{}",
|
||||
result.downcast_ref::<DynamicReport>().unwrap().to_json()
|
||||
);
|
||||
|
||||
// Get all transactions
|
||||
|
||||
/*let targets = vec![
|
||||
ReportingProductId {
|
||||
name: "CalculateIncomeTax",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(VoidArgs {}),
|
||||
},
|
||||
ReportingProductId {
|
||||
name: "AllTransactionsExceptEarningsToEquity",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
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: "AllTransactionsExceptEarningsToEquity",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(DateArgs {
|
||||
date: NaiveDate::from_ymd_opt(YEAR, 6, 30).unwrap(),
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
println!("All transactions:");
|
||||
println!(
|
||||
"{}",
|
||||
result.downcast_ref::<Transactions>().unwrap().to_json()
|
||||
);*/
|
||||
}
|
||||
|
@ -134,6 +134,31 @@ fn build_step_for_product(
|
||||
}
|
||||
HasStepOrCanBuild::CanLookup(from_args_fn) => {
|
||||
new_step = from_args_fn(product.args.clone());
|
||||
|
||||
// Check new step meets the dependency
|
||||
if new_step.id().name != product.name {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function (expected name {}, got {})",
|
||||
product.name,
|
||||
new_step.id().name
|
||||
);
|
||||
}
|
||||
if new_step.id().args != product.args {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function {} (expected args {:?}, got {:?})",
|
||||
product.name,
|
||||
product.args,
|
||||
new_step.id().args
|
||||
);
|
||||
}
|
||||
if !new_step.id().product_kinds.contains(&product.kind) {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})",
|
||||
product.name,
|
||||
product.kind,
|
||||
new_step.id().product_kinds
|
||||
);
|
||||
}
|
||||
}
|
||||
HasStepOrCanBuild::CanBuild(builder) => {
|
||||
new_step = (builder.build)(
|
||||
@ -144,37 +169,40 @@ fn build_step_for_product(
|
||||
&dependencies,
|
||||
&context,
|
||||
);
|
||||
|
||||
// Check new step meets the dependency
|
||||
if new_step.id().name != product.name {
|
||||
panic!(
|
||||
"Unexpected step returned from builder {} (expected name {}, got {})",
|
||||
builder.name,
|
||||
product.name,
|
||||
new_step.id().name
|
||||
);
|
||||
}
|
||||
if new_step.id().args != product.args {
|
||||
panic!(
|
||||
"Unexpected step returned from builder {} for {} (expected args {:?}, got {:?})",
|
||||
builder.name,
|
||||
product.name,
|
||||
product.args,
|
||||
new_step.id().args
|
||||
);
|
||||
}
|
||||
if !new_step.id().product_kinds.contains(&product.kind) {
|
||||
panic!(
|
||||
"Unexpected step returned from builder {} for {} (expected kind {:?}, got {:?})",
|
||||
builder.name,
|
||||
product.name,
|
||||
product.kind,
|
||||
new_step.id().product_kinds
|
||||
);
|
||||
}
|
||||
}
|
||||
HasStepOrCanBuild::None => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// Check new step meets the dependency
|
||||
if new_step.id().name != product.name {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function (expected name {}, got {})",
|
||||
product.name,
|
||||
new_step.id().name
|
||||
);
|
||||
}
|
||||
if new_step.id().args != product.args {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function {} (expected args {:?}, got {:?})",
|
||||
product.name,
|
||||
product.args,
|
||||
new_step.id().args
|
||||
);
|
||||
}
|
||||
if !new_step.id().product_kinds.contains(&product.kind) {
|
||||
panic!(
|
||||
"Unexpected step returned from lookup function {} (expected kind {:?}, got {:?})",
|
||||
product.name,
|
||||
product.kind,
|
||||
new_step.id().product_kinds
|
||||
);
|
||||
}
|
||||
|
||||
Some(new_step)
|
||||
}
|
||||
|
||||
@ -231,6 +259,10 @@ pub fn steps_for_targets(
|
||||
dependencies.add_dependency(new_step.id(), dependency);
|
||||
}
|
||||
new_step.init_graph(&steps, &mut dependencies, &context);
|
||||
} else {
|
||||
return Err(ReportingCalculationError::NoStepForProduct {
|
||||
message: format!("No step builds target product {}", target),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,30 +48,100 @@ use super::types::{
|
||||
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
|
||||
pub fn register_lookup_fns(context: &mut ReportingContext) {
|
||||
AllTransactionsExceptEarningsToEquity::register_lookup_fn(context);
|
||||
AllTransactionsExceptEarningsToEquityBalances::register_lookup_fn(context);
|
||||
AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context);
|
||||
BalanceSheet::register_lookup_fn(context);
|
||||
CalculateIncomeTax::register_lookup_fn(context);
|
||||
CombineOrdinaryTransactions::register_lookup_fn(context);
|
||||
CombineOrdinaryTransactionsBalances::register_lookup_fn(context);
|
||||
CurrentYearEarningsToEquity::register_lookup_fn(context);
|
||||
DBBalances::register_lookup_fn(context);
|
||||
DBTransactions::register_lookup_fn(context);
|
||||
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
|
||||
/// Target representing all transactions except charging current year and retained earnings to equity (returns transaction list)
|
||||
///
|
||||
/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax].
|
||||
///
|
||||
/// Used as the basis for the income statement.
|
||||
#[derive(Debug)]
|
||||
pub struct AllTransactionsExceptEarningsToEquity {
|
||||
pub args: DateArgs,
|
||||
}
|
||||
|
||||
impl AllTransactionsExceptEarningsToEquity {
|
||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||
context.register_lookup_fn(
|
||||
"AllTransactionsExceptEarningsToEquity",
|
||||
&[ReportingProductKind::Transactions],
|
||||
Self::takes_args,
|
||||
Self::from_args,
|
||||
);
|
||||
}
|
||||
|
||||
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||
args.is::<DateArgs>()
|
||||
}
|
||||
|
||||
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
||||
Box::new(AllTransactionsExceptEarningsToEquity {
|
||||
args: *args.downcast().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AllTransactionsExceptEarningsToEquity {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.id()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReportingStep for AllTransactionsExceptEarningsToEquity {
|
||||
fn id(&self) -> ReportingStepId {
|
||||
ReportingStepId {
|
||||
name: "AllTransactionsExceptEarningsToEquity",
|
||||
product_kinds: &[ReportingProductKind::Transactions],
|
||||
args: Box::new(self.args.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> {
|
||||
// AllTransactionsExceptEarningsToEquity always depends on CombineOrdinaryTransactions at least
|
||||
vec![ReportingProductId {
|
||||
name: "CombineOrdinaryTransactions",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(self.args.clone()),
|
||||
}]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_context: &ReportingContext,
|
||||
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||
dependencies: &ReportingGraphDependencies,
|
||||
products: &RwLock<ReportingProducts>,
|
||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
||||
combine_transactions_of_all_dependencies(self.id(), dependencies, products).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Target representing all transactions except charging current year and retained earnings to equity (returns balances)
|
||||
///
|
||||
/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax].
|
||||
///
|
||||
/// Used as the basis for the income statement.
|
||||
#[derive(Debug)]
|
||||
pub struct AllTransactionsExceptEarningsToEquityBalances {
|
||||
pub product_kinds: &'static [ReportingProductKind; 1], // Must have single member - represented as static array for compatibility with ReportingStepId
|
||||
pub args: Box<dyn ReportingStepArgs>,
|
||||
}
|
||||
|
||||
impl AllTransactionsExceptEarningsToEquity {
|
||||
impl AllTransactionsExceptEarningsToEquityBalances {
|
||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||
context.register_lookup_fn(
|
||||
"AllTransactionsExceptEarningsToEquity",
|
||||
@ -96,21 +166,21 @@ impl AllTransactionsExceptEarningsToEquity {
|
||||
product_kinds: &'static [ReportingProductKind; 1],
|
||||
args: Box<dyn ReportingStepArgs>,
|
||||
) -> Box<dyn ReportingStep> {
|
||||
Box::new(AllTransactionsExceptEarningsToEquity {
|
||||
Box::new(AllTransactionsExceptEarningsToEquityBalances {
|
||||
product_kinds,
|
||||
args,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AllTransactionsExceptEarningsToEquity {
|
||||
impl Display for AllTransactionsExceptEarningsToEquityBalances {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.id()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReportingStep for AllTransactionsExceptEarningsToEquity {
|
||||
impl ReportingStep for AllTransactionsExceptEarningsToEquityBalances {
|
||||
fn id(&self) -> ReportingStepId {
|
||||
ReportingStepId {
|
||||
name: "AllTransactionsExceptEarningsToEquity",
|
||||
@ -141,6 +211,7 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity {
|
||||
let step_dependencies = dependencies.dependencies_for_step(&self.id());
|
||||
|
||||
// Identify the product_kind dependency most recently generated
|
||||
// TODO: Make this deterministic - parallel execution may cause the order to vary
|
||||
let product_kind = self.product_kinds[0];
|
||||
|
||||
for (product_id, product) in products.map().iter().rev() {
|
||||
@ -502,8 +573,8 @@ impl CalculateIncomeTax {
|
||||
);
|
||||
}
|
||||
|
||||
fn takes_args(_args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||
true
|
||||
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||
args.is::<VoidArgs>()
|
||||
}
|
||||
|
||||
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
||||
@ -546,14 +617,20 @@ impl ReportingStep for CalculateIncomeTax {
|
||||
_context: &ReportingContext,
|
||||
) {
|
||||
for other in steps {
|
||||
if let Some(other) = other.downcast_ref::<AllTransactionsExceptEarningsToEquity>() {
|
||||
if let Some(other) =
|
||||
other.downcast_ref::<AllTransactionsExceptEarningsToEquityBalances>()
|
||||
{
|
||||
// AllTransactionsExceptEarningsToEquity depends on CalculateIncomeTax
|
||||
dependencies.add_dependency(
|
||||
other.id(),
|
||||
ReportingProductId {
|
||||
name: self.id().name,
|
||||
kind: other.product_kinds[0],
|
||||
args: other.id().args,
|
||||
args: if other.product_kinds[0] == ReportingProductKind::Transactions {
|
||||
Box::new(VoidArgs {})
|
||||
} else {
|
||||
other.id().args
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -586,9 +663,9 @@ impl ReportingStep for CalculateIncomeTax {
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines all steps producing ordinary transactions
|
||||
/// Combines all steps producing ordinary transactions (returns transaction list)
|
||||
///
|
||||
/// By default, these are [DBBalances] and [PostUnreconciledStatementLines]
|
||||
/// By default, these are [DBTransactions] and [PostUnreconciledStatementLines].
|
||||
#[derive(Debug)]
|
||||
pub struct CombineOrdinaryTransactions {
|
||||
pub args: DateArgs,
|
||||
@ -598,7 +675,7 @@ impl CombineOrdinaryTransactions {
|
||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||
context.register_lookup_fn(
|
||||
"CombineOrdinaryTransactions",
|
||||
&[ReportingProductKind::BalancesAt],
|
||||
&[ReportingProductKind::Transactions],
|
||||
Self::takes_args,
|
||||
Self::from_args,
|
||||
);
|
||||
@ -623,6 +700,79 @@ impl Display for CombineOrdinaryTransactions {
|
||||
|
||||
#[async_trait]
|
||||
impl ReportingStep for CombineOrdinaryTransactions {
|
||||
fn id(&self) -> ReportingStepId {
|
||||
ReportingStepId {
|
||||
name: "CombineOrdinaryTransactions",
|
||||
product_kinds: &[ReportingProductKind::Transactions],
|
||||
args: Box::new(self.args.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn requires(&self, _context: &ReportingContext) -> Vec<ReportingProductId> {
|
||||
vec![
|
||||
// CombineOrdinaryTransactions depends on DBTransactions
|
||||
ReportingProductId {
|
||||
name: "DBTransactions",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(VoidArgs {}),
|
||||
},
|
||||
// CombineOrdinaryTransactions depends on PostUnreconciledStatementLines
|
||||
ReportingProductId {
|
||||
name: "PostUnreconciledStatementLines",
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(VoidArgs {}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
_context: &ReportingContext,
|
||||
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||
dependencies: &ReportingGraphDependencies,
|
||||
products: &RwLock<ReportingProducts>,
|
||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
||||
combine_transactions_of_all_dependencies(self.id(), dependencies, products).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines all steps producing ordinary transactions (returns balances)
|
||||
///
|
||||
/// By default, these are [DBBalances] and [PostUnreconciledStatementLines].
|
||||
#[derive(Debug)]
|
||||
pub struct CombineOrdinaryTransactionsBalances {
|
||||
pub args: DateArgs,
|
||||
}
|
||||
|
||||
impl CombineOrdinaryTransactionsBalances {
|
||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||
context.register_lookup_fn(
|
||||
"CombineOrdinaryTransactions",
|
||||
&[ReportingProductKind::BalancesAt],
|
||||
Self::takes_args,
|
||||
Self::from_args,
|
||||
);
|
||||
}
|
||||
|
||||
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||
args.is::<DateArgs>()
|
||||
}
|
||||
|
||||
fn from_args(args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
||||
Box::new(CombineOrdinaryTransactionsBalances {
|
||||
args: *args.downcast().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CombineOrdinaryTransactionsBalances {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.id()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReportingStep for CombineOrdinaryTransactionsBalances {
|
||||
fn id(&self) -> ReportingStepId {
|
||||
ReportingStepId {
|
||||
name: "CombineOrdinaryTransactions",
|
||||
@ -795,6 +945,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
|
||||
account: account.clone(),
|
||||
quantity: -balance,
|
||||
commodity: context.reporting_commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
Posting {
|
||||
id: None,
|
||||
@ -803,6 +954,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
|
||||
account: "Current Year Earnings".to_string(),
|
||||
quantity: *balance,
|
||||
commodity: context.reporting_commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -893,6 +1045,71 @@ impl ReportingStep for DBBalances {
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up transactions from the database
|
||||
#[derive(Debug)]
|
||||
pub struct DBTransactions {}
|
||||
|
||||
impl DBTransactions {
|
||||
fn register_lookup_fn(context: &mut ReportingContext) {
|
||||
context.register_lookup_fn(
|
||||
"DBTransactions",
|
||||
&[ReportingProductKind::Transactions],
|
||||
Self::takes_args,
|
||||
Self::from_args,
|
||||
);
|
||||
}
|
||||
|
||||
fn takes_args(args: &Box<dyn ReportingStepArgs>) -> bool {
|
||||
args.is::<VoidArgs>()
|
||||
}
|
||||
|
||||
fn from_args(_args: Box<dyn ReportingStepArgs>) -> Box<dyn ReportingStep> {
|
||||
Box::new(DBTransactions {})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for DBTransactions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.id()))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ReportingStep for DBTransactions {
|
||||
fn id(&self) -> ReportingStepId {
|
||||
ReportingStepId {
|
||||
name: "DBTransactions",
|
||||
product_kinds: &[ReportingProductKind::Transactions],
|
||||
args: Box::new(VoidArgs {}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute(
|
||||
&self,
|
||||
context: &ReportingContext,
|
||||
_steps: &Vec<Box<dyn ReportingStep>>,
|
||||
_dependencies: &ReportingGraphDependencies,
|
||||
_products: &RwLock<ReportingProducts>,
|
||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
||||
// Get transactions from DB
|
||||
let transactions = Transactions {
|
||||
transactions: context.db_connection.get_transactions().await,
|
||||
};
|
||||
|
||||
// Store result
|
||||
let mut result = ReportingProducts::new();
|
||||
result.insert(
|
||||
ReportingProductId {
|
||||
name: self.id().name,
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: Box::new(VoidArgs {}),
|
||||
},
|
||||
Box::new(transactions),
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates an income statement [DynamicReport]
|
||||
#[derive(Debug)]
|
||||
pub struct IncomeStatement {
|
||||
@ -1150,6 +1367,7 @@ impl ReportingStep for PostUnreconciledStatementLines {
|
||||
account: line.source_account.clone(),
|
||||
quantity: line.quantity,
|
||||
commodity: line.commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
Posting {
|
||||
id: None,
|
||||
@ -1158,6 +1376,7 @@ impl ReportingStep for PostUnreconciledStatementLines {
|
||||
account: unclassified_account.to_string(),
|
||||
quantity: -line.quantity,
|
||||
commodity: line.commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
@ -1286,6 +1505,7 @@ impl ReportingStep for RetainedEarningsToEquity {
|
||||
account: account.clone(),
|
||||
quantity: -balance,
|
||||
commodity: context.reporting_commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
Posting {
|
||||
id: None,
|
||||
@ -1294,6 +1514,7 @@ impl ReportingStep for RetainedEarningsToEquity {
|
||||
account: "Retained Earnings".to_string(),
|
||||
quantity: *balance,
|
||||
commodity: context.reporting_commodity.clone(),
|
||||
quantity_ascost: None,
|
||||
},
|
||||
],
|
||||
})
|
||||
@ -1464,3 +1685,45 @@ impl ReportingStep for TrialBalance {
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combines the transactions of all dependencies and returns [Transactions] as [ReportingProducts] for the given step
|
||||
///
|
||||
/// Used to implement [CombineOrdinaryTransactions] and [AllTransactionsExceptEarningsToEquity].
|
||||
async fn combine_transactions_of_all_dependencies(
|
||||
step_id: ReportingStepId,
|
||||
dependencies: &ReportingGraphDependencies,
|
||||
products: &RwLock<ReportingProducts>,
|
||||
) -> Result<ReportingProducts, ReportingExecutionError> {
|
||||
let products = products.read().await;
|
||||
|
||||
// Combine transactions of all dependencies
|
||||
|
||||
let mut transactions = Transactions {
|
||||
transactions: Vec::new(),
|
||||
};
|
||||
|
||||
for dependency in dependencies.dependencies_for_step(&step_id) {
|
||||
let dependency_transactions = &products
|
||||
.get_or_err(&dependency.product)?
|
||||
.downcast_ref::<Transactions>()
|
||||
.unwrap()
|
||||
.transactions;
|
||||
|
||||
for transaction in dependency_transactions.iter() {
|
||||
transactions.transactions.push(transaction.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Store result
|
||||
let mut result = ReportingProducts::new();
|
||||
result.insert(
|
||||
ReportingProductId {
|
||||
name: step_id.name,
|
||||
kind: ReportingProductKind::Transactions,
|
||||
args: step_id.args,
|
||||
},
|
||||
Box::new(transactions),
|
||||
);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ use dyn_clone::DynClone;
|
||||
use dyn_eq::DynEq;
|
||||
use dyn_hash::DynHash;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::db::DbConnection;
|
||||
@ -167,11 +168,18 @@ downcast_rs::impl_downcast!(ReportingProduct);
|
||||
dyn_clone::clone_trait_object!(ReportingProduct);
|
||||
|
||||
/// Records a list of transactions generated by a [ReportingStep]
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Transactions {
|
||||
pub transactions: Vec<TransactionWithPostings>,
|
||||
}
|
||||
|
||||
impl Transactions {
|
||||
/// Serialise the product (as JSON) using serde
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string(&self.transactions).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ReportingProduct for Transactions {}
|
||||
|
||||
/// Records cumulative account balances at a particular point in time
|
||||
@ -234,6 +242,18 @@ impl ReportingProducts {
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_owned_or_err(
|
||||
mut self,
|
||||
key: &ReportingProductId,
|
||||
) -> Result<Box<dyn ReportingProduct>, ReportingExecutionError> {
|
||||
match self.map.swap_remove(key) {
|
||||
Some(value) => Ok(value),
|
||||
None => Err(ReportingExecutionError::DependencyNotAvailable {
|
||||
message: format!("Product {} not available when expected", key),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ReportingProducts {
|
||||
|
62
src/serde.rs
Normal file
62
src/serde.rs
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
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/>.
|
||||
*/
|
||||
|
||||
/// Serialises [chrono::NaiveDateTime] in database format
|
||||
///
|
||||
/// Use as `#[serde(with = "crate::serde::naivedatetime_to_js")]`, etc.
|
||||
pub mod naivedatetime_to_js {
|
||||
use std::fmt;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{
|
||||
de::{self, Unexpected, Visitor},
|
||||
Deserializer, Serializer,
|
||||
};
|
||||
|
||||
pub(crate) fn serialize<S: Serializer>(
|
||||
dt: &NaiveDateTime,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&dt.format("%Y-%m-%d %H:%M:%S%.6f").to_string())
|
||||
}
|
||||
|
||||
struct DateVisitor;
|
||||
impl<'de> Visitor<'de> for DateVisitor {
|
||||
type Value = NaiveDateTime;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(formatter, "a date string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.6f") {
|
||||
Ok(dt) => Ok(dt),
|
||||
Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<NaiveDateTime, D::Error> {
|
||||
deserializer.deserialize_str(DateVisitor)
|
||||
}
|
||||
}
|
@ -19,23 +19,26 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::QuantityInt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Transaction {
|
||||
pub id: Option<u64>,
|
||||
#[serde(with = "crate::serde::naivedatetime_to_js")]
|
||||
pub dt: NaiveDateTime,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct TransactionWithPostings {
|
||||
#[serde(flatten)]
|
||||
pub transaction: Transaction,
|
||||
pub postings: Vec<Posting>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Posting {
|
||||
pub id: Option<u64>,
|
||||
pub transaction_id: Option<u64>,
|
||||
@ -43,9 +46,14 @@ pub struct Posting {
|
||||
pub account: String,
|
||||
pub quantity: QuantityInt,
|
||||
pub commodity: String,
|
||||
pub quantity_ascost: Option<QuantityInt>,
|
||||
//pub running_balance: Option<QuantityInt>,
|
||||
}
|
||||
|
||||
pub(crate) fn update_balances_from_transactions<'a, I: Iterator<Item = &'a TransactionWithPostings>>(
|
||||
pub(crate) fn update_balances_from_transactions<
|
||||
'a,
|
||||
I: Iterator<Item = &'a TransactionWithPostings>,
|
||||
>(
|
||||
balances: &mut HashMap<String, QuantityInt>,
|
||||
transactions: I,
|
||||
) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user