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",
]
[[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]]
name = "atoi"
version = "2.0.0"
@ -676,6 +687,7 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
name = "libdrcr"
version = "0.1.0"
dependencies = [
"async-trait",
"chrono",
"downcast-rs",
"dyn-clone",

View File

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

View File

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

View File

@ -28,11 +28,12 @@ use libdrcr::reporting::types::{
ReportingProductKind, VoidArgs,
};
fn main() {
#[tokio::main]
async fn main() {
const YEAR: i32 = 2023;
// Connect to database
let db_connection = DbConnection::connect("sqlite:drcr_testing.db");
let db_connection = DbConnection::new("sqlite:drcr_testing.db").await;
// Initialise ReportingContext
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
.get_or_err(&ReportingProductId {
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
.get_or_err(&ReportingProductId {
name: "BalanceSheet",

View File

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

View File

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

View File

@ -16,23 +16,35 @@
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)]
pub enum ReportingExecutionError {
DependencyNotAvailable { message: String }
DependencyNotAvailable { message: String },
}
pub fn execute_steps(
pub async fn execute_steps(
steps: Vec<Box<dyn ReportingStep>>,
dependencies: ReportingGraphDependencies,
context: &ReportingContext,
) -> Result<ReportingProducts, ReportingExecutionError> {
let mut products = ReportingProducts::new();
let products = RwLock::new(ReportingProducts::new());
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
///
/// Helper function to call [steps_for_targets] followed by [execute_steps].
pub fn generate_report(
pub async fn generate_report(
targets: Vec<ReportingProductId>,
context: &ReportingContext,
) -> Result<ReportingProducts, ReportingError> {
@ -56,7 +56,7 @@ pub fn generate_report(
let (sorted_steps, dependencies) = steps_for_targets(targets, context)?;
// Execute steps
let products = execute_steps(sorted_steps, dependencies, context)?;
let products = execute_steps(sorted_steps, dependencies, context).await?;
Ok(products)
}

View File

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

View File

@ -20,12 +20,14 @@ use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::hash::Hash;
use async_trait::async_trait;
use chrono::NaiveDate;
use downcast_rs::Downcast;
use dyn_clone::DynClone;
use dyn_eq::DynEq;
use dyn_hash::DynHash;
use indexmap::IndexMap;
use tokio::sync::RwLock;
use crate::db::DbConnection;
use crate::transaction::TransactionWithPostings;
@ -159,7 +161,7 @@ pub enum ReportingProductKind {
}
/// 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);
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>> {
&self.map
}
/// Insert a key-value pair in the map
///
/// See [IndexMap::insert].
pub fn insert(&mut self, key: ReportingProductId, value: Box<dyn ReportingProduct>) {
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(
&self,
key: &ReportingProductId,
@ -260,7 +273,8 @@ impl Display for ReportingStepId {
}
/// 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]
fn id(&self) -> ReportingStepId;
@ -293,14 +307,16 @@ pub trait ReportingStep: Debug + Display + Downcast {
}
/// Called to generate the [ReportingProduct] for this [ReportingStep]
///
/// Returns a [ReportingProducts] containing (only) the new [ReportingProduct]s.
#[allow(unused_variables)]
fn execute(
async fn execute(
&self,
context: &ReportingContext,
steps: &Vec<Box<dyn ReportingStep>>,
dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> {
products: &RwLock<ReportingProducts>,
) -> Result<ReportingProducts, ReportingExecutionError> {
todo!("{}", self);
}
}
@ -311,7 +327,10 @@ downcast_rs::impl_downcast!(ReportingStep);
// REPORTING STEP ARGUMENTS
/// 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);
dyn_clone::clone_trait_object!(ReportingStepArgs);