Implement getting transactions from database

This commit is contained in:
RunasSudo 2025-05-27 23:48:40 +10:00
parent 00c7833706
commit 4f1db12688
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 530 additions and 47 deletions

View File

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

View File

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

View File

@ -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()
);*/
}

View File

@ -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),
});
}
}
}

View File

@ -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)
}

View File

@ -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
View 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)
}
}

View File

@ -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,
) {