Make reporting API async

This commit is contained in:
RunasSudo 2025-05-27 14:29:27 +10:00
parent b111e7023c
commit 148390f030
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
10 changed files with 553 additions and 328 deletions

12
Cargo.lock generated
View File

@ -38,6 +38,17 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "async-trait"
version = "0.1.88"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "atoi" name = "atoi"
version = "2.0.0" version = "2.0.0"
@ -676,6 +687,7 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
name = "libdrcr" name = "libdrcr"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait",
"chrono", "chrono",
"downcast-rs", "downcast-rs",
"dyn-clone", "dyn-clone",

View File

@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
async-trait = "0.1.88"
chrono = "0.4.41" chrono = "0.4.41"
downcast-rs = "2.0.1" downcast-rs = "2.0.1"
dyn-clone = "1.0.19" dyn-clone = "1.0.19"

View File

@ -17,39 +17,26 @@
*/ */
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::DerefMut;
use std::{cell::RefCell, future::Future};
use chrono::NaiveDate; use chrono::NaiveDate;
use sqlx::sqlite::SqliteRow; use sqlx::sqlite::SqliteRow;
use sqlx::{Connection, Row, SqliteConnection}; use sqlx::{Connection, Row, SqliteConnection};
use tokio::runtime::Runtime;
use crate::account_config::AccountConfiguration; use crate::account_config::AccountConfiguration;
use crate::{util::format_date, QuantityInt}; use crate::{util::format_date, QuantityInt};
pub struct DbConnection { pub struct DbConnection {
sqlx_connection: RefCell<SqliteConnection>, url: String,
metadata: DbMetadata, metadata: DbMetadata,
} }
fn run_blocking<F: Future>(future: F) -> F::Output {
let rt = Runtime::new().unwrap();
rt.block_on(future)
}
impl DbConnection { impl DbConnection {
/// Connect to the given Sqlite database pub async fn new(url: &str) -> Self {
pub fn connect(url: &str) -> Self {
run_blocking(DbConnection::connect_async(url))
}
async fn connect_async(url: &str) -> Self {
let mut connection = SqliteConnection::connect(url).await.expect("SQL error"); let mut connection = SqliteConnection::connect(url).await.expect("SQL error");
let metadata = DbMetadata::from_database(&mut connection).await; let metadata = DbMetadata::from_database(&mut connection).await;
Self { Self {
sqlx_connection: RefCell::new(connection), url: url.to_string(),
metadata, metadata,
} }
} }
@ -58,13 +45,15 @@ impl DbConnection {
&self.metadata &self.metadata
} }
/// Get account balances from the database pub async fn connect(&self) -> SqliteConnection {
pub fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> { SqliteConnection::connect(&self.url)
run_blocking(self.get_balances_async(date)) .await
.expect("SQL error")
} }
async fn get_balances_async(&self, date: NaiveDate) -> HashMap<String, QuantityInt> { /// Get account balances from the database
let mut connection = self.sqlx_connection.borrow_mut(); pub async fn get_balances(&self, date: NaiveDate) -> HashMap<String, QuantityInt> {
let mut connection = self.connect().await;
let rows = sqlx::query( let rows = sqlx::query(
"-- Get last transaction for each account "-- Get last transaction for each account
@ -84,7 +73,7 @@ impl DbConnection {
SELECT max_tid_by_account.account, running_balance AS quantity SELECT max_tid_by_account.account, running_balance AS quantity
FROM max_tid_by_account FROM max_tid_by_account
JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account" JOIN transactions_with_running_balances ON max_tid = transactions_with_running_balances.transaction_id AND max_tid_by_account.account = transactions_with_running_balances.account"
).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error"); ).bind(format_date(date)).fetch_all(&mut connection).await.expect("SQL error");
let mut balances = HashMap::new(); let mut balances = HashMap::new();
for row in rows { for row in rows {
@ -95,12 +84,8 @@ impl DbConnection {
} }
/// Get account configurations from the database /// Get account configurations from the database
pub fn get_account_configurations(&self) -> Vec<AccountConfiguration> { pub async fn get_account_configurations(&self) -> Vec<AccountConfiguration> {
run_blocking(self.get_account_configurations_async()) let mut connection = self.connect().await;
}
async fn get_account_configurations_async(&self) -> Vec<AccountConfiguration> {
let mut connection = self.sqlx_connection.borrow_mut();
let mut account_configurations = let mut account_configurations =
sqlx::query("SELECT id, account, kind, data FROM account_configurations") sqlx::query("SELECT id, account, kind, data FROM account_configurations")
@ -110,7 +95,7 @@ impl DbConnection {
kind: r.get("kind"), kind: r.get("kind"),
data: r.get("data"), data: r.get("data"),
}) })
.fetch_all(connection.deref_mut()) .fetch_all(&mut connection)
.await .await
.expect("SQL error"); .expect("SQL error");

View File

@ -28,11 +28,12 @@ use libdrcr::reporting::types::{
ReportingProductKind, VoidArgs, ReportingProductKind, VoidArgs,
}; };
fn main() { #[tokio::main]
async fn main() {
const YEAR: i32 = 2023; const YEAR: i32 = 2023;
// Connect to database // Connect to database
let db_connection = DbConnection::connect("sqlite:drcr_testing.db"); let db_connection = DbConnection::new("sqlite:drcr_testing.db").await;
// Initialise ReportingContext // Initialise ReportingContext
let mut context = ReportingContext::new( let mut context = ReportingContext::new(
@ -85,7 +86,7 @@ fn main() {
}, },
]; ];
let products = generate_report(targets, &context).unwrap(); let products = generate_report(targets, &context).await.unwrap();
let result = products let result = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity", name: "AllTransactionsExceptEarningsToEquity",
@ -119,7 +120,7 @@ fn main() {
}, },
]; ];
let products = generate_report(targets, &context).unwrap(); let products = generate_report(targets, &context).await.unwrap();
let result = products let result = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
name: "BalanceSheet", name: "BalanceSheet",

View File

@ -23,6 +23,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use async_trait::async_trait;
use tokio::sync::RwLock;
use crate::transaction::update_balances_from_transactions; use crate::transaction::update_balances_from_transactions;
use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies}; use super::calculator::{has_step_or_can_build, HasStepOrCanBuild, ReportingGraphDependencies};
@ -124,6 +127,7 @@ impl Display for BalancesAtToBalancesBetween {
} }
} }
#[async_trait]
impl ReportingStep for BalancesAtToBalancesBetween { impl ReportingStep for BalancesAtToBalancesBetween {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -153,13 +157,15 @@ impl ReportingStep for BalancesAtToBalancesBetween {
] ]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get balances at dates // Get balances at dates
let balances_start = &products let balances_start = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
@ -196,7 +202,8 @@ impl ReportingStep for BalancesAtToBalancesBetween {
} }
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
@ -204,8 +211,7 @@ impl ReportingStep for BalancesAtToBalancesBetween {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -285,6 +291,7 @@ impl Display for GenerateBalances {
} }
} }
#[async_trait]
impl ReportingStep for GenerateBalances { impl ReportingStep for GenerateBalances {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -303,13 +310,15 @@ impl ReportingStep for GenerateBalances {
}] }]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get the transactions // Get the transactions
let transactions = &products let transactions = &products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
@ -328,7 +337,8 @@ impl ReportingStep for GenerateBalances {
update_balances_from_transactions(&mut balances.balances, transactions.iter()); update_balances_from_transactions(&mut balances.balances, transactions.iter());
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
@ -336,8 +346,7 @@ impl ReportingStep for GenerateBalances {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -441,6 +450,7 @@ impl Display for UpdateBalancesAt {
} }
} }
#[async_trait]
impl ReportingStep for UpdateBalancesAt { impl ReportingStep for UpdateBalancesAt {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -499,13 +509,15 @@ impl ReportingStep for UpdateBalancesAt {
} }
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Look up the parent step, so we can extract the appropriate args // Look up the parent step, so we can extract the appropriate args
let parent_step = steps let parent_step = steps
.iter() .iter()
@ -566,7 +578,8 @@ impl ReportingStep for UpdateBalancesAt {
); );
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
@ -574,8 +587,7 @@ impl ReportingStep for UpdateBalancesAt {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -646,6 +658,7 @@ impl Display for UpdateBalancesBetween {
} }
} }
#[async_trait]
impl ReportingStep for UpdateBalancesBetween { impl ReportingStep for UpdateBalancesBetween {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -683,13 +696,15 @@ impl ReportingStep for UpdateBalancesBetween {
); );
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Look up the parent step, so we can extract the appropriate args // Look up the parent step, so we can extract the appropriate args
let parent_step = steps let parent_step = steps
.iter() .iter()
@ -736,7 +751,8 @@ impl ReportingStep for UpdateBalancesBetween {
); );
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.step_name, name: self.step_name,
kind: ReportingProductKind::BalancesBetween, kind: ReportingProductKind::BalancesBetween,
@ -744,7 +760,6 @@ impl ReportingStep for UpdateBalancesBetween {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }

View File

@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// FIXME: Tidy up this file
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::HashMap; use std::collections::HashMap;
@ -25,17 +27,21 @@ use crate::QuantityInt;
use super::types::{GenericReportingProduct, ReportingProduct}; use super::types::{GenericReportingProduct, ReportingProduct};
/// Represents a dynamically generated report composed of [DynamicReportEntry] /// Represents a dynamically generated report composed of [CalculatableDynamicReportEntry]
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug)]
pub struct DynamicReport { pub struct CalculatableDynamicReport {
pub title: String, pub title: String,
pub columns: Vec<String>, pub columns: Vec<String>,
// This must use RefCell as, during calculation, we iterate while mutating the report // This must use RefCell as, during calculation, we iterate while mutating the report
pub entries: Vec<RefCell<DynamicReportEntry>>, pub entries: Vec<RefCell<CalculatableDynamicReportEntry>>,
} }
impl DynamicReport { impl CalculatableDynamicReport {
pub fn new(title: String, columns: Vec<String>, entries: Vec<DynamicReportEntry>) -> Self { pub fn new(
title: String,
columns: Vec<String>,
entries: Vec<CalculatableDynamicReportEntry>,
) -> Self {
Self { Self {
title, title,
columns, columns,
@ -43,67 +49,60 @@ impl DynamicReport {
} }
} }
/// Remove all entries from the report where auto_hide is enabled and quantity is zero
pub fn auto_hide(&mut self) {
self.entries.retain(|e| match &mut *e.borrow_mut() {
DynamicReportEntry::Section(section) => {
section.auto_hide_children();
if section.can_auto_hide_self() {
false
} else {
true
}
}
DynamicReportEntry::LiteralRow(row) => {
if row.can_auto_hide() {
false
} else {
true
}
}
DynamicReportEntry::CalculatedRow(_) => true,
DynamicReportEntry::Spacer => true,
});
}
/// Recursively calculate all [CalculatedRow] entries /// Recursively calculate all [CalculatedRow] entries
pub fn calculate(&mut self) { pub fn calculate(self) -> DynamicReport {
let mut calculated_entries = Vec::new();
for (entry_idx, entry) in self.entries.iter().enumerate() { for (entry_idx, entry) in self.entries.iter().enumerate() {
let entry_ref = entry.borrow(); let entry_ref = entry.borrow();
match &*entry_ref { match &*entry_ref {
DynamicReportEntry::Section(section) => { CalculatableDynamicReportEntry::CalculatableSection(section) => {
// Clone first, in case calculation needs to take reference to the section // Clone first, in case calculation needs to take reference to the section
let mut updated_section = section.clone(); let updated_section = section.clone().calculate(&self);
updated_section.calculate(&self);
drop(entry_ref); // Drop entry_ref so we can borrow mutably drop(entry_ref); // Drop entry_ref so we can borrow mutably
let mut entry_mut = self.entries[entry_idx].borrow_mut(); let mut entry_mut = self.entries[entry_idx].borrow_mut();
*entry_mut = DynamicReportEntry::Section(updated_section); *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone());
calculated_entries.push(DynamicReportEntry::Section(updated_section));
} }
DynamicReportEntry::LiteralRow(_) => (), CalculatableDynamicReportEntry::Section(section) => {
DynamicReportEntry::CalculatedRow(row) => { calculated_entries.push(DynamicReportEntry::Section(section.clone()));
}
CalculatableDynamicReportEntry::LiteralRow(row) => {
calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone()));
}
CalculatableDynamicReportEntry::CalculatedRow(row) => {
let updated_row = row.calculate(&self); let updated_row = row.calculate(&self);
drop(entry_ref); // Drop entry_ref so we can borrow mutably drop(entry_ref); // Drop entry_ref so we can borrow mutably
let mut entry_mut = self.entries[entry_idx].borrow_mut(); let mut entry_mut = self.entries[entry_idx].borrow_mut();
*entry_mut = DynamicReportEntry::LiteralRow(updated_row); *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone());
calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row));
} }
DynamicReportEntry::Spacer => (), CalculatableDynamicReportEntry::Spacer => (),
} }
} }
DynamicReport {
title: self.title,
columns: self.columns,
entries: calculated_entries,
}
} }
/// Look up [DynamicReportEntry] by id /// Look up [CalculatableDynamicReportEntry] by id
/// ///
/// Returns a cloned copy of the [DynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow. /// Returns a cloned copy of the [CalculatableDynamicReportEntry]. This is necessary because the entry may be within a [Section], and [RefCell] semantics cannot express this type of nested borrow.
pub fn by_id(&self, id: &str) -> Option<DynamicReportEntry> { pub fn by_id(&self, id: &str) -> Option<CalculatableDynamicReportEntry> {
// Manually iterate over self.entries rather than self.entries() // Manually iterate over self.entries rather than self.entries()
// To catch the situation where entry is already mutably borrowed // To catch the situation where entry is already mutably borrowed
for entry in self.entries.iter() { for entry in self.entries.iter() {
match entry.try_borrow() { match entry.try_borrow() {
Ok(entry) => match &*entry { Ok(entry) => match &*entry {
DynamicReportEntry::Section(section) => { CalculatableDynamicReportEntry::CalculatableSection(section) => {
if let Some(i) = &section.id { if let Some(i) = &section.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry.clone());
@ -113,15 +112,35 @@ impl DynamicReport {
return Some(e); return Some(e);
} }
} }
DynamicReportEntry::LiteralRow(row) => { CalculatableDynamicReportEntry::Section(section) => {
if let Some(i) = &section.id {
if i == id {
return Some(entry.clone());
}
}
if let Some(e) = section.by_id(id) {
return Some(match e {
DynamicReportEntry::Section(section) => {
CalculatableDynamicReportEntry::Section(section.clone())
}
DynamicReportEntry::LiteralRow(row) => {
CalculatableDynamicReportEntry::LiteralRow(row.clone())
}
DynamicReportEntry::Spacer => {
CalculatableDynamicReportEntry::Spacer
}
});
}
}
CalculatableDynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id { if let Some(i) = &row.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry.clone());
} }
} }
} }
DynamicReportEntry::CalculatedRow(_) => (), CalculatableDynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (), CalculatableDynamicReportEntry::Spacer => (),
}, },
Err(err) => panic!( Err(err) => panic!(
"Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}",
@ -136,7 +155,7 @@ impl DynamicReport {
/// Calculate the subtotals for the [Section] with the given id /// Calculate the subtotals for the [Section] with the given id
pub fn subtotal_for_id(&self, id: &str) -> Vec<QuantityInt> { pub fn subtotal_for_id(&self, id: &str) -> Vec<QuantityInt> {
let entry = self.by_id(id).expect("Invalid id"); let entry = self.by_id(id).expect("Invalid id");
if let DynamicReportEntry::Section(section) = entry { if let CalculatableDynamicReportEntry::CalculatableSection(section) = entry {
section.subtotal(&self) section.subtotal(&self)
} else { } else {
panic!("Called subtotal_for_id on non-Section"); panic!("Called subtotal_for_id on non-Section");
@ -146,59 +165,26 @@ impl DynamicReport {
// Return the quantities for the [LiteralRow] with the given id // Return the quantities for the [LiteralRow] with the given id
pub fn quantity_for_id(&self, id: &str) -> Vec<QuantityInt> { pub fn quantity_for_id(&self, id: &str) -> Vec<QuantityInt> {
let entry = self.by_id(id).expect("Invalid id"); let entry = self.by_id(id).expect("Invalid id");
if let DynamicReportEntry::LiteralRow(row) = entry { if let CalculatableDynamicReportEntry::LiteralRow(row) = entry {
row.quantity row.quantity
} else { } else {
panic!("Called quantity_for_id on non-LiteralRow"); panic!("Called quantity_for_id on non-LiteralRow");
} }
} }
/// Serialise the report (as JSON) using serde
pub fn to_json(&self) -> String {
serde_json::to_string(self).unwrap()
}
} }
impl GenericReportingProduct for DynamicReport {} /// Represents a dynamically generated report composed of [DynamicReportEntry], with no [CalculatedRow]s
impl ReportingProduct for DynamicReport {}
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub enum DynamicReportEntry { pub struct DynamicReport {
Section(Section), pub title: String,
LiteralRow(LiteralRow), pub columns: Vec<String>,
#[serde(skip)] pub entries: Vec<DynamicReportEntry>,
CalculatedRow(CalculatedRow),
Spacer,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] impl DynamicReport {
pub struct Section { /// Remove all entries from the report where auto_hide is enabled and quantity is zero
pub text: String, pub fn auto_hide(&mut self) {
pub id: Option<String>, self.entries.retain_mut(|e| match e {
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<RefCell<DynamicReportEntry>>,
}
impl Section {
pub fn new(
text: String,
id: Option<String>,
visible: bool,
auto_hide: bool,
entries: Vec<DynamicReportEntry>,
) -> Self {
Self {
text,
id,
visible,
auto_hide,
entries: entries.into_iter().map(|e| RefCell::new(e)).collect(),
}
}
fn auto_hide_children(&mut self) {
self.entries.retain(|e| match &mut *e.borrow_mut() {
DynamicReportEntry::Section(section) => { DynamicReportEntry::Section(section) => {
section.auto_hide_children(); section.auto_hide_children();
if section.can_auto_hide_self() { if section.can_auto_hide_self() {
@ -214,59 +200,116 @@ impl Section {
true true
} }
} }
DynamicReportEntry::CalculatedRow(_) => true,
DynamicReportEntry::Spacer => true, DynamicReportEntry::Spacer => true,
}); });
} }
fn can_auto_hide_self(&self) -> bool { /// Serialise the report (as JSON) using serde
self.auto_hide pub fn to_json(&self) -> String {
&& self.entries.iter().all(|e| match &*e.borrow() { serde_json::to_string(self).unwrap()
DynamicReportEntry::Section(section) => section.can_auto_hide_self(), }
DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(), }
DynamicReportEntry::CalculatedRow(_) => false,
DynamicReportEntry::Spacer => true, impl GenericReportingProduct for DynamicReport {}
}) impl ReportingProduct for DynamicReport {}
#[derive(Clone, Debug)]
pub enum CalculatableDynamicReportEntry {
CalculatableSection(CalculatableSection),
Section(Section),
LiteralRow(LiteralRow),
CalculatedRow(CalculatedRow),
Spacer,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum DynamicReportEntry {
Section(Section),
LiteralRow(LiteralRow),
Spacer,
}
#[derive(Clone, Debug)]
pub struct CalculatableSection {
pub text: String,
pub id: Option<String>,
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<RefCell<CalculatableDynamicReportEntry>>,
}
impl CalculatableSection {
pub fn new(
text: String,
id: Option<String>,
visible: bool,
auto_hide: bool,
entries: Vec<CalculatableDynamicReportEntry>,
) -> Self {
Self {
text,
id,
visible,
auto_hide,
entries: entries.into_iter().map(|e| RefCell::new(e)).collect(),
}
} }
/// Recursively calculate all [CalculatedRow] entries /// Recursively calculate all [CalculatedRow] entries
pub fn calculate(&mut self, report: &DynamicReport) { pub fn calculate(&mut self, report: &CalculatableDynamicReport) -> Section {
let mut calculated_entries = Vec::new();
for (entry_idx, entry) in self.entries.iter().enumerate() { for (entry_idx, entry) in self.entries.iter().enumerate() {
let entry_ref = entry.borrow(); let entry_ref = entry.borrow();
match &*entry_ref { match &*entry_ref {
DynamicReportEntry::Section(section) => { CalculatableDynamicReportEntry::CalculatableSection(section) => {
// Clone first, in case calculation needs to take reference to the section let updated_section = section.clone().calculate(&report);
let mut updated_section = section.clone();
updated_section.calculate(&report);
drop(entry_ref); // Drop entry_ref so we can borrow mutably drop(entry_ref); // Drop entry_ref so we can borrow mutably
let mut entry_mut = self.entries[entry_idx].borrow_mut(); let mut entry_mut = self.entries[entry_idx].borrow_mut();
*entry_mut = DynamicReportEntry::Section(updated_section); *entry_mut = CalculatableDynamicReportEntry::Section(updated_section.clone());
calculated_entries.push(DynamicReportEntry::Section(updated_section));
} }
DynamicReportEntry::LiteralRow(_) => (), CalculatableDynamicReportEntry::Section(section) => {
DynamicReportEntry::CalculatedRow(row) => { calculated_entries.push(DynamicReportEntry::Section(section.clone()));
}
CalculatableDynamicReportEntry::LiteralRow(row) => {
calculated_entries.push(DynamicReportEntry::LiteralRow(row.clone()));
}
CalculatableDynamicReportEntry::CalculatedRow(row) => {
let updated_row = row.calculate(&report); let updated_row = row.calculate(&report);
drop(entry_ref); // Drop entry_ref so we can borrow mutably drop(entry_ref); // Drop entry_ref so we can borrow mutably
let mut entry_mut = self.entries[entry_idx].borrow_mut(); let mut entry_mut = self.entries[entry_idx].borrow_mut();
*entry_mut = DynamicReportEntry::LiteralRow(updated_row); *entry_mut = CalculatableDynamicReportEntry::LiteralRow(updated_row.clone());
calculated_entries.push(DynamicReportEntry::LiteralRow(updated_row));
} }
DynamicReportEntry::Spacer => (), CalculatableDynamicReportEntry::Spacer => (),
} }
} }
Section {
text: self.text.clone(),
id: self.id.clone(),
visible: self.visible,
auto_hide: self.auto_hide,
entries: calculated_entries,
}
} }
/// Look up [DynamicReportEntry] by id /// Look up [CalculatableDynamicReportEntry] by id
/// ///
/// Returns a cloned copy of the [DynamicReportEntry]. /// Returns a cloned copy of the [CalculatableDynamicReportEntry].
pub fn by_id(&self, id: &str) -> Option<DynamicReportEntry> { pub fn by_id(&self, id: &str) -> Option<CalculatableDynamicReportEntry> {
// Manually iterate over self.entries rather than self.entries() // Manually iterate over self.entries rather than self.entries()
// To catch the situation where entry is already mutably borrowed // To catch the situation where entry is already mutably borrowed
for entry in self.entries.iter() { for entry in self.entries.iter() {
match entry.try_borrow() { match entry.try_borrow() {
Ok(entry) => match &*entry { Ok(entry) => match &*entry {
DynamicReportEntry::Section(section) => { CalculatableDynamicReportEntry::CalculatableSection(section) => {
if let Some(i) = &section.id { if let Some(i) = &section.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry.clone());
@ -276,15 +319,16 @@ impl Section {
return Some(e); return Some(e);
} }
} }
DynamicReportEntry::LiteralRow(row) => { CalculatableDynamicReportEntry::Section(_) => todo!(),
CalculatableDynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id { if let Some(i) = &row.id {
if i == id { if i == id {
return Some(entry.clone()); return Some(entry.clone());
} }
} }
} }
DynamicReportEntry::CalculatedRow(_) => (), CalculatableDynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (), CalculatableDynamicReportEntry::Spacer => (),
}, },
Err(err) => panic!( Err(err) => panic!(
"Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}", "Attempt to call by_id on DynamicReportEntry which is mutably borrowed: {}",
@ -296,11 +340,111 @@ impl Section {
None None
} }
/// Calculate the subtotals for this [Section] /// Calculate the subtotals for this [CalculatableSection]
pub fn subtotal(&self, report: &DynamicReport) -> Vec<QuantityInt> { pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec<QuantityInt> {
let mut subtotals = vec![0; report.columns.len()]; let mut subtotals = vec![0; report.columns.len()];
for entry in self.entries.iter() { for entry in self.entries.iter() {
match &*entry.borrow() { match &*entry.borrow() {
CalculatableDynamicReportEntry::CalculatableSection(section) => {
for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() {
subtotals[col_idx] += subtotal;
}
}
CalculatableDynamicReportEntry::Section(section) => {
for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() {
subtotals[col_idx] += subtotal;
}
}
CalculatableDynamicReportEntry::LiteralRow(row) => {
for (col_idx, subtotal) in row.quantity.iter().enumerate() {
subtotals[col_idx] += subtotal;
}
}
CalculatableDynamicReportEntry::CalculatedRow(_) => (),
CalculatableDynamicReportEntry::Spacer => (),
}
}
subtotals
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Section {
pub text: String,
pub id: Option<String>,
pub visible: bool,
pub auto_hide: bool,
pub entries: Vec<DynamicReportEntry>,
}
impl Section {
fn auto_hide_children(&mut self) {
self.entries.retain_mut(|e| match e {
DynamicReportEntry::Section(section) => {
section.auto_hide_children();
if section.can_auto_hide_self() {
false
} else {
true
}
}
DynamicReportEntry::LiteralRow(row) => {
if row.can_auto_hide() {
false
} else {
true
}
}
DynamicReportEntry::Spacer => true,
});
}
fn can_auto_hide_self(&self) -> bool {
self.auto_hide
&& self.entries.iter().all(|e| match e {
DynamicReportEntry::Section(section) => section.can_auto_hide_self(),
DynamicReportEntry::LiteralRow(row) => row.can_auto_hide(),
DynamicReportEntry::Spacer => true,
})
}
/// Look up [DynamicReportEntry] by id
///
/// Returns a cloned copy of the [DynamicReportEntry].
pub fn by_id(&self, id: &str) -> Option<DynamicReportEntry> {
// Manually iterate over self.entries rather than self.entries()
// To catch the situation where entry is already mutably borrowed
for entry in self.entries.iter() {
match entry {
DynamicReportEntry::Section(section) => {
if let Some(i) = &section.id {
if i == id {
return Some(entry.clone());
}
}
if let Some(e) = section.by_id(id) {
return Some(e);
}
}
DynamicReportEntry::LiteralRow(row) => {
if let Some(i) = &row.id {
if i == id {
return Some(entry.clone());
}
}
}
DynamicReportEntry::Spacer => (),
}
}
None
}
/// Calculate the subtotals for this [Section]
pub fn subtotal(&self, report: &CalculatableDynamicReport) -> Vec<QuantityInt> {
let mut subtotals = vec![0; report.columns.len()];
for entry in self.entries.iter() {
match entry {
DynamicReportEntry::Section(section) => { DynamicReportEntry::Section(section) => {
for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() { for (col_idx, subtotal) in section.subtotal(report).into_iter().enumerate() {
subtotals[col_idx] += subtotal; subtotals[col_idx] += subtotal;
@ -311,7 +455,6 @@ impl Section {
subtotals[col_idx] += subtotal; subtotals[col_idx] += subtotal;
} }
} }
DynamicReportEntry::CalculatedRow(_) => (),
DynamicReportEntry::Spacer => (), DynamicReportEntry::Spacer => (),
} }
} }
@ -341,7 +484,7 @@ impl LiteralRow {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CalculatedRow { pub struct CalculatedRow {
//pub text: String, //pub text: String,
pub calculate_fn: fn(report: &DynamicReport) -> LiteralRow, pub calculate_fn: fn(report: &CalculatableDynamicReport) -> LiteralRow,
//pub id: Option<String>, //pub id: Option<String>,
//pub visible: bool, //pub visible: bool,
//pub auto_hide: bool, //pub auto_hide: bool,
@ -351,7 +494,7 @@ pub struct CalculatedRow {
} }
impl CalculatedRow { impl CalculatedRow {
fn calculate(&self, report: &DynamicReport) -> LiteralRow { fn calculate(&self, report: &CalculatableDynamicReport) -> LiteralRow {
(self.calculate_fn)(report) (self.calculate_fn)(report)
} }
} }
@ -361,7 +504,7 @@ pub fn entries_for_kind(
invert: bool, invert: bool,
balances: &Vec<&HashMap<String, QuantityInt>>, balances: &Vec<&HashMap<String, QuantityInt>>,
kinds_for_account: &HashMap<String, Vec<String>>, kinds_for_account: &HashMap<String, Vec<String>>,
) -> Vec<DynamicReportEntry> { ) -> Vec<CalculatableDynamicReportEntry> {
// Get accounts of specified kind // Get accounts of specified kind
let mut accounts = kinds_for_account let mut accounts = kinds_for_account
.iter() .iter()
@ -393,7 +536,7 @@ pub fn entries_for_kind(
heading: false, heading: false,
bordered: false, bordered: false,
}; };
entries.push(DynamicReportEntry::LiteralRow(entry)); entries.push(CalculatableDynamicReportEntry::LiteralRow(entry));
} }
entries entries

View File

@ -16,23 +16,35 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use super::{calculator::ReportingGraphDependencies, types::{ReportingContext, ReportingProducts, ReportingStep}}; use tokio::sync::RwLock;
use super::{
calculator::ReportingGraphDependencies,
types::{ReportingContext, ReportingProducts, ReportingStep},
};
#[derive(Debug)] #[derive(Debug)]
pub enum ReportingExecutionError { pub enum ReportingExecutionError {
DependencyNotAvailable { message: String } DependencyNotAvailable { message: String },
} }
pub fn execute_steps( pub async fn execute_steps(
steps: Vec<Box<dyn ReportingStep>>, steps: Vec<Box<dyn ReportingStep>>,
dependencies: ReportingGraphDependencies, dependencies: ReportingGraphDependencies,
context: &ReportingContext, context: &ReportingContext,
) -> Result<ReportingProducts, ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let mut products = ReportingProducts::new(); let products = RwLock::new(ReportingProducts::new());
for step in steps.iter() { for step in steps.iter() {
step.execute(context, &steps, &dependencies, &mut products)?; // Execute the step
// TODO: Do this in parallel
let mut new_products = step
.execute(context, &steps, &dependencies, &products)
.await?;
// Insert the new products
products.write().await.append(&mut new_products);
} }
Ok(products) Ok(products.into_inner())
} }

View File

@ -48,7 +48,7 @@ impl From<ReportingExecutionError> for ReportingError {
/// Calculate the steps required to generate the requested [ReportingProductId]s and then execute them /// Calculate the steps required to generate the requested [ReportingProductId]s and then execute them
/// ///
/// Helper function to call [steps_for_targets] followed by [execute_steps]. /// Helper function to call [steps_for_targets] followed by [execute_steps].
pub fn generate_report( pub async fn generate_report(
targets: Vec<ReportingProductId>, targets: Vec<ReportingProductId>,
context: &ReportingContext, context: &ReportingContext,
) -> Result<ReportingProducts, ReportingError> { ) -> Result<ReportingProducts, ReportingError> {
@ -56,7 +56,7 @@ pub fn generate_report(
let (sorted_steps, dependencies) = steps_for_targets(targets, context)?; let (sorted_steps, dependencies) = steps_for_targets(targets, context)?;
// Execute steps // Execute steps
let products = execute_steps(sorted_steps, dependencies, context)?; let products = execute_steps(sorted_steps, dependencies, context).await?;
Ok(products) Ok(products)
} }

View File

@ -21,7 +21,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use async_trait::async_trait;
use chrono::Datelike; use chrono::Datelike;
use tokio::sync::RwLock;
use crate::account_config::kinds_for_account; use crate::account_config::kinds_for_account;
use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions}; use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions};
@ -33,7 +35,8 @@ use crate::QuantityInt;
use super::calculator::ReportingGraphDependencies; use super::calculator::ReportingGraphDependencies;
use super::dynamic_report::{ use super::dynamic_report::{
entries_for_kind, CalculatedRow, DynamicReport, DynamicReportEntry, LiteralRow, Section, entries_for_kind, CalculatableDynamicReport, CalculatableDynamicReportEntry,
CalculatableSection, CalculatedRow, LiteralRow,
}; };
use super::executor::ReportingExecutionError; use super::executor::ReportingExecutionError;
use super::types::{ use super::types::{
@ -105,6 +108,7 @@ impl Display for AllTransactionsExceptEarningsToEquity {
} }
} }
#[async_trait]
impl ReportingStep for AllTransactionsExceptEarningsToEquity { impl ReportingStep for AllTransactionsExceptEarningsToEquity {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -123,13 +127,15 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity {
}] }]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get all dependencies // Get all dependencies
let step_dependencies = dependencies.dependencies_for_step(&self.id()); let step_dependencies = dependencies.dependencies_for_step(&self.id());
@ -139,7 +145,8 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity {
for (product_id, product) in products.map().iter().rev() { for (product_id, product) in products.map().iter().rev() {
if step_dependencies.iter().any(|d| d.product == *product_id) { if step_dependencies.iter().any(|d| d.product == *product_id) {
// Store the result // Store the result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: product_kind, kind: product_kind,
@ -147,8 +154,7 @@ impl ReportingStep for AllTransactionsExceptEarningsToEquity {
}, },
product.clone(), product.clone(),
); );
return Ok(result);
return Ok(());
} }
} }
@ -197,6 +203,7 @@ impl Display for AllTransactionsIncludingEarningsToEquity {
} }
} }
#[async_trait]
impl ReportingStep for AllTransactionsIncludingEarningsToEquity { impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -229,13 +236,15 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
] ]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get opening balances from AllTransactionsExceptEarningsToEquity // Get opening balances from AllTransactionsExceptEarningsToEquity
let opening_balances = products let opening_balances = products
.get_or_err(&ReportingProductId { .get_or_err(&ReportingProductId {
@ -280,7 +289,8 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
); );
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
@ -288,8 +298,7 @@ impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -326,6 +335,7 @@ impl Display for BalanceSheet {
} }
} }
#[async_trait]
impl ReportingStep for BalanceSheet { impl ReportingStep for BalanceSheet {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -350,13 +360,15 @@ impl ReportingStep for BalanceSheet {
result result
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get balances for each period // Get balances for each period
let mut balances: Vec<&HashMap<String, QuantityInt>> = Vec::new(); let mut balances: Vec<&HashMap<String, QuantityInt>> = Vec::new();
for date_args in self.args.dates.iter() { for date_args in self.args.dates.iter() {
@ -371,14 +383,14 @@ impl ReportingStep for BalanceSheet {
// Get names of all balance sheet accounts // Get names of all balance sheet accounts
let kinds_for_account = let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations()); kinds_for_account(context.db_connection.get_account_configurations().await);
// Init report // Init report
let mut report = DynamicReport::new( let report = CalculatableDynamicReport::new(
"Balance sheet".to_string(), "Balance sheet".to_string(),
self.args.dates.iter().map(|d| d.date.to_string()).collect(), self.args.dates.iter().map(|d| d.date.to_string()).collect(),
vec![ vec![
DynamicReportEntry::Section(Section::new( CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Assets".to_string(), "Assets".to_string(),
Some("assets".to_string()), Some("assets".to_string()),
true, true,
@ -386,23 +398,25 @@ impl ReportingStep for BalanceSheet {
{ {
let mut entries = let mut entries =
entries_for_kind("drcr.asset", false, &balances, &kinds_for_account); entries_for_kind("drcr.asset", false, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { entries.push(CalculatableDynamicReportEntry::CalculatedRow(
calculate_fn: |report| LiteralRow { CalculatedRow {
text: "Total assets".to_string(), calculate_fn: |report| LiteralRow {
quantity: report.subtotal_for_id("assets"), text: "Total assets".to_string(),
id: Some("total_assets".to_string()), quantity: report.subtotal_for_id("assets"),
visible: true, id: Some("total_assets".to_string()),
auto_hide: false, visible: true,
link: None, auto_hide: false,
heading: true, link: None,
bordered: true, heading: true,
bordered: true,
},
}, },
})); ));
entries entries
}, },
)), )),
DynamicReportEntry::Spacer, CalculatableDynamicReportEntry::Spacer,
DynamicReportEntry::Section(Section::new( CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Liabilities".to_string(), "Liabilities".to_string(),
Some("liabilities".to_string()), Some("liabilities".to_string()),
true, true,
@ -410,23 +424,25 @@ impl ReportingStep for BalanceSheet {
{ {
let mut entries = let mut entries =
entries_for_kind("drcr.liability", true, &balances, &kinds_for_account); entries_for_kind("drcr.liability", true, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { entries.push(CalculatableDynamicReportEntry::CalculatedRow(
calculate_fn: |report| LiteralRow { CalculatedRow {
text: "Total liabilities".to_string(), calculate_fn: |report| LiteralRow {
quantity: report.subtotal_for_id("liabilities"), text: "Total liabilities".to_string(),
id: Some("total_liabilities".to_string()), quantity: report.subtotal_for_id("liabilities"),
visible: true, id: Some("total_liabilities".to_string()),
auto_hide: false, visible: true,
link: None, auto_hide: false,
heading: true, link: None,
bordered: true, heading: true,
bordered: true,
},
}, },
})); ));
entries entries
}, },
)), )),
DynamicReportEntry::Spacer, CalculatableDynamicReportEntry::Spacer,
DynamicReportEntry::Section(Section::new( CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Equity".to_string(), "Equity".to_string(),
Some("equity".to_string()), Some("equity".to_string()),
true, true,
@ -434,29 +450,32 @@ impl ReportingStep for BalanceSheet {
{ {
let mut entries = let mut entries =
entries_for_kind("drcr.equity", true, &balances, &kinds_for_account); entries_for_kind("drcr.equity", true, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { entries.push(CalculatableDynamicReportEntry::CalculatedRow(
calculate_fn: |report| LiteralRow { CalculatedRow {
text: "Total equity".to_string(), calculate_fn: |report| LiteralRow {
quantity: report.subtotal_for_id("equity"), text: "Total equity".to_string(),
id: Some("total_equity".to_string()), quantity: report.subtotal_for_id("equity"),
visible: true, id: Some("total_equity".to_string()),
auto_hide: false, visible: true,
link: None, auto_hide: false,
heading: true, link: None,
bordered: true, heading: true,
bordered: true,
},
}, },
})); ));
entries entries
}, },
)), )),
], ],
); );
report.calculate(); let mut report = report.calculate();
report.auto_hide(); report.auto_hide();
// Store the result // Store the result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: "BalanceSheet", name: "BalanceSheet",
kind: ReportingProductKind::Generic, kind: ReportingProductKind::Generic,
@ -464,8 +483,7 @@ impl ReportingStep for BalanceSheet {
}, },
Box::new(report), Box::new(report),
); );
Ok(result)
Ok(())
} }
} }
@ -498,6 +516,7 @@ impl Display for CalculateIncomeTax {
} }
} }
#[async_trait]
impl ReportingStep for CalculateIncomeTax { impl ReportingStep for CalculateIncomeTax {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -540,20 +559,21 @@ impl ReportingStep for CalculateIncomeTax {
} }
} }
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: &mut ReportingProducts, _products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
eprintln!("Stub: CalculateIncomeTax.execute"); eprintln!("Stub: CalculateIncomeTax.execute");
let transactions = Transactions { let transactions = Transactions {
transactions: Vec::new(), transactions: Vec::new(),
}; };
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
@ -561,8 +581,7 @@ impl ReportingStep for CalculateIncomeTax {
}, },
Box::new(transactions), Box::new(transactions),
); );
Ok(result)
Ok(())
} }
} }
@ -601,6 +620,7 @@ impl Display for CombineOrdinaryTransactions {
} }
} }
#[async_trait]
impl ReportingStep for CombineOrdinaryTransactions { impl ReportingStep for CombineOrdinaryTransactions {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -627,13 +647,15 @@ impl ReportingStep for CombineOrdinaryTransactions {
] ]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Sum balances of all dependencies // Sum balances of all dependencies
let mut balances = BalancesAt { let mut balances = BalancesAt {
@ -653,7 +675,8 @@ impl ReportingStep for CombineOrdinaryTransactions {
} }
// Store result // Store result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
@ -661,8 +684,7 @@ impl ReportingStep for CombineOrdinaryTransactions {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -699,6 +721,7 @@ impl Display for CurrentYearEarningsToEquity {
} }
} }
#[async_trait]
impl ReportingStep for CurrentYearEarningsToEquity { impl ReportingStep for CurrentYearEarningsToEquity {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -722,13 +745,14 @@ impl ReportingStep for CurrentYearEarningsToEquity {
}] }]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
let eofy_date = get_eofy(&self.args.date, &context.eofy_date); let eofy_date = get_eofy(&self.args.date, &context.eofy_date);
// Get balances for this financial year // Get balances for this financial year
@ -746,7 +770,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
// Get income and expense accounts // Get income and expense accounts
let kinds_for_account = let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations()); kinds_for_account(context.db_connection.get_account_configurations().await);
// Transfer income and expense balances to current year earnings // Transfer income and expense balances to current year earnings
let mut transactions = Transactions { let mut transactions = Transactions {
@ -789,7 +813,8 @@ impl ReportingStep for CurrentYearEarningsToEquity {
} }
// Store product // Store product
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
@ -797,8 +822,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
}, },
Box::new(transactions), Box::new(transactions),
); );
Ok(result)
Ok(())
} }
} }
@ -835,6 +859,7 @@ impl Display for DBBalances {
} }
} }
#[async_trait]
impl ReportingStep for DBBalances { impl ReportingStep for DBBalances {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -844,19 +869,21 @@ impl ReportingStep for DBBalances {
} }
} }
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: &mut ReportingProducts, _products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
// Get balances from DB // Get balances from DB
let balances = BalancesAt { let balances = BalancesAt {
balances: context.db_connection.get_balances(self.args.date), balances: context.db_connection.get_balances(self.args.date).await,
}; };
products.insert( // Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::BalancesAt, kind: ReportingProductKind::BalancesAt,
@ -864,8 +891,7 @@ impl ReportingStep for DBBalances {
}, },
Box::new(balances), Box::new(balances),
); );
Ok(result)
Ok(())
} }
} }
@ -902,6 +928,7 @@ impl Display for IncomeStatement {
} }
} }
#[async_trait]
impl ReportingStep for IncomeStatement { impl ReportingStep for IncomeStatement {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -926,13 +953,15 @@ impl ReportingStep for IncomeStatement {
result result
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
// Get balances for each period // Get balances for each period
let mut balances: Vec<&HashMap<String, QuantityInt>> = Vec::new(); let mut balances: Vec<&HashMap<String, QuantityInt>> = Vec::new();
for date_args in self.args.dates.iter() { for date_args in self.args.dates.iter() {
@ -947,10 +976,10 @@ impl ReportingStep for IncomeStatement {
// Get names of all income statement accounts // Get names of all income statement accounts
let kinds_for_account = let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations()); kinds_for_account(context.db_connection.get_account_configurations().await);
// Init report // Init report
let mut report = DynamicReport::new( let report = CalculatableDynamicReport::new(
"Income statement".to_string(), "Income statement".to_string(),
self.args self.args
.dates .dates
@ -958,7 +987,7 @@ impl ReportingStep for IncomeStatement {
.map(|d| d.date_end.to_string()) .map(|d| d.date_end.to_string())
.collect(), .collect(),
vec![ vec![
DynamicReportEntry::Section(Section::new( CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Income".to_string(), "Income".to_string(),
Some("income".to_string()), Some("income".to_string()),
true, true,
@ -966,23 +995,25 @@ impl ReportingStep for IncomeStatement {
{ {
let mut entries = let mut entries =
entries_for_kind("drcr.income", true, &balances, &kinds_for_account); entries_for_kind("drcr.income", true, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { entries.push(CalculatableDynamicReportEntry::CalculatedRow(
calculate_fn: |report| LiteralRow { CalculatedRow {
text: "Total income".to_string(), calculate_fn: |report| LiteralRow {
quantity: report.subtotal_for_id("income"), text: "Total income".to_string(),
id: Some("total_income".to_string()), quantity: report.subtotal_for_id("income"),
visible: true, id: Some("total_income".to_string()),
auto_hide: false, visible: true,
link: None, auto_hide: false,
heading: true, link: None,
bordered: true, heading: true,
bordered: true,
},
}, },
})); ));
entries entries
}, },
)), )),
DynamicReportEntry::Spacer, CalculatableDynamicReportEntry::Spacer,
DynamicReportEntry::Section(Section::new( CalculatableDynamicReportEntry::CalculatableSection(CalculatableSection::new(
"Expenses".to_string(), "Expenses".to_string(),
Some("expenses".to_string()), Some("expenses".to_string()),
true, true,
@ -990,23 +1021,25 @@ impl ReportingStep for IncomeStatement {
{ {
let mut entries = let mut entries =
entries_for_kind("drcr.expense", false, &balances, &kinds_for_account); entries_for_kind("drcr.expense", false, &balances, &kinds_for_account);
entries.push(DynamicReportEntry::CalculatedRow(CalculatedRow { entries.push(CalculatableDynamicReportEntry::CalculatedRow(
calculate_fn: |report| LiteralRow { CalculatedRow {
text: "Total expenses".to_string(), calculate_fn: |report| LiteralRow {
quantity: report.subtotal_for_id("expenses"), text: "Total expenses".to_string(),
id: Some("total_expenses".to_string()), quantity: report.subtotal_for_id("expenses"),
visible: true, id: Some("total_expenses".to_string()),
auto_hide: false, visible: true,
link: None, auto_hide: false,
heading: true, link: None,
bordered: true, heading: true,
bordered: true,
},
}, },
})); ));
entries entries
}, },
)), )),
DynamicReportEntry::Spacer, CalculatableDynamicReportEntry::Spacer,
DynamicReportEntry::CalculatedRow(CalculatedRow { CalculatableDynamicReportEntry::CalculatedRow(CalculatedRow {
calculate_fn: |report| LiteralRow { calculate_fn: |report| LiteralRow {
text: "Net surplus (deficit)".to_string(), text: "Net surplus (deficit)".to_string(),
quantity: report quantity: report
@ -1026,11 +1059,12 @@ impl ReportingStep for IncomeStatement {
], ],
); );
report.calculate(); let mut report = report.calculate();
report.auto_hide(); report.auto_hide();
// Store the result // Store the result
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: "IncomeStatement", name: "IncomeStatement",
kind: ReportingProductKind::Generic, kind: ReportingProductKind::Generic,
@ -1038,8 +1072,7 @@ impl ReportingStep for IncomeStatement {
}, },
Box::new(report), Box::new(report),
); );
Ok(result)
Ok(())
} }
} }
@ -1076,6 +1109,7 @@ impl Display for PostUnreconciledStatementLines {
} }
} }
#[async_trait]
impl ReportingStep for PostUnreconciledStatementLines { impl ReportingStep for PostUnreconciledStatementLines {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -1085,20 +1119,22 @@ impl ReportingStep for PostUnreconciledStatementLines {
} }
} }
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: &mut ReportingProducts, _products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
eprintln!("Stub: PostUnreconciledStatementLines.execute"); eprintln!("Stub: PostUnreconciledStatementLines.execute");
let transactions = Transactions { let transactions = Transactions {
transactions: Vec::new(), transactions: Vec::new(),
}; };
products.insert( // Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
@ -1106,8 +1142,7 @@ impl ReportingStep for PostUnreconciledStatementLines {
}, },
Box::new(transactions), Box::new(transactions),
); );
Ok(result)
Ok(())
} }
} }
@ -1144,6 +1179,7 @@ impl Display for RetainedEarningsToEquity {
} }
} }
#[async_trait]
impl ReportingStep for RetainedEarningsToEquity { impl ReportingStep for RetainedEarningsToEquity {
fn id(&self) -> ReportingStepId { fn id(&self) -> ReportingStepId {
ReportingStepId { ReportingStepId {
@ -1167,13 +1203,14 @@ impl ReportingStep for RetainedEarningsToEquity {
}] }]
} }
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
let products = products.read().await;
let eofy_date = get_eofy(&self.args.date, &context.eofy_date); let eofy_date = get_eofy(&self.args.date, &context.eofy_date);
let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap(); let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap();
@ -1191,7 +1228,7 @@ impl ReportingStep for RetainedEarningsToEquity {
// Get income and expense accounts // Get income and expense accounts
let kinds_for_account = let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations()); kinds_for_account(context.db_connection.get_account_configurations().await);
// Transfer income and expense balances to retained earnings // Transfer income and expense balances to retained earnings
let mut transactions = Transactions { let mut transactions = Transactions {
@ -1234,7 +1271,8 @@ impl ReportingStep for RetainedEarningsToEquity {
} }
// Store product // Store product
products.insert( let mut result = ReportingProducts::new();
result.insert(
ReportingProductId { ReportingProductId {
name: self.id().name, name: self.id().name,
kind: ReportingProductKind::Transactions, kind: ReportingProductKind::Transactions,
@ -1242,7 +1280,6 @@ impl ReportingStep for RetainedEarningsToEquity {
}, },
Box::new(transactions), Box::new(transactions),
); );
Ok(result)
Ok(())
} }
} }

View File

@ -20,12 +20,14 @@ use std::collections::HashMap;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use std::hash::Hash; use std::hash::Hash;
use async_trait::async_trait;
use chrono::NaiveDate; use chrono::NaiveDate;
use downcast_rs::Downcast; use downcast_rs::Downcast;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use dyn_eq::DynEq; use dyn_eq::DynEq;
use dyn_hash::DynHash; use dyn_hash::DynHash;
use indexmap::IndexMap; use indexmap::IndexMap;
use tokio::sync::RwLock;
use crate::db::DbConnection; use crate::db::DbConnection;
use crate::transaction::TransactionWithPostings; use crate::transaction::TransactionWithPostings;
@ -159,7 +161,7 @@ pub enum ReportingProductKind {
} }
/// Represents the result of a [ReportingStep] /// Represents the result of a [ReportingStep]
pub trait ReportingProduct: Debug + Downcast + DynClone {} pub trait ReportingProduct: Debug + Downcast + DynClone + Send + Sync {}
downcast_rs::impl_downcast!(ReportingProduct); downcast_rs::impl_downcast!(ReportingProduct);
dyn_clone::clone_trait_object!(ReportingProduct); dyn_clone::clone_trait_object!(ReportingProduct);
@ -205,14 +207,25 @@ impl ReportingProducts {
} }
} }
/// Returns a reference to the underlying [IndexMap]
pub fn map(&self) -> &IndexMap<ReportingProductId, Box<dyn ReportingProduct>> { pub fn map(&self) -> &IndexMap<ReportingProductId, Box<dyn ReportingProduct>> {
&self.map &self.map
} }
/// Insert a key-value pair in the map
///
/// See [IndexMap::insert].
pub fn insert(&mut self, key: ReportingProductId, value: Box<dyn ReportingProduct>) { pub fn insert(&mut self, key: ReportingProductId, value: Box<dyn ReportingProduct>) {
self.map.insert(key, value); self.map.insert(key, value);
} }
/// Moves all key-value pairs from `other` into `self`, leaving `other` empty
///
/// See [IndexMap::append].
pub fn append(&mut self, other: &mut ReportingProducts) {
self.map.append(&mut other.map);
}
pub fn get_or_err( pub fn get_or_err(
&self, &self,
key: &ReportingProductId, key: &ReportingProductId,
@ -260,7 +273,8 @@ impl Display for ReportingStepId {
} }
/// Represents a step in a reporting job /// Represents a step in a reporting job
pub trait ReportingStep: Debug + Display + Downcast { #[async_trait]
pub trait ReportingStep: Debug + Display + Downcast + Send + Sync {
/// Get the [ReportingStepId] for this [ReportingStep] /// Get the [ReportingStepId] for this [ReportingStep]
fn id(&self) -> ReportingStepId; fn id(&self) -> ReportingStepId;
@ -293,14 +307,16 @@ pub trait ReportingStep: Debug + Display + Downcast {
} }
/// Called to generate the [ReportingProduct] for this [ReportingStep] /// Called to generate the [ReportingProduct] for this [ReportingStep]
///
/// Returns a [ReportingProducts] containing (only) the new [ReportingProduct]s.
#[allow(unused_variables)] #[allow(unused_variables)]
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: &mut ReportingProducts, products: &RwLock<ReportingProducts>,
) -> Result<(), ReportingExecutionError> { ) -> Result<ReportingProducts, ReportingExecutionError> {
todo!("{}", self); todo!("{}", self);
} }
} }
@ -311,7 +327,10 @@ downcast_rs::impl_downcast!(ReportingStep);
// REPORTING STEP ARGUMENTS // REPORTING STEP ARGUMENTS
/// Represents arguments to a [ReportingStep] /// Represents arguments to a [ReportingStep]
pub trait ReportingStepArgs: Debug + Display + Downcast + DynClone + DynEq + DynHash {} pub trait ReportingStepArgs:
Debug + Display + Downcast + DynClone + DynEq + DynHash + Send + Sync
{
}
downcast_rs::impl_downcast!(ReportingStepArgs); downcast_rs::impl_downcast!(ReportingStepArgs);
dyn_clone::clone_trait_object!(ReportingStepArgs); dyn_clone::clone_trait_object!(ReportingStepArgs);