From 148390f030d802d7e5b854e3f463d2f37dc41e1b Mon Sep 17 00:00:00 2001
From: RunasSudo <runassudo@yingtongli.me>
Date: Tue, 27 May 2025 14:29:27 +1000
Subject: [PATCH] Make reporting API async

---
 Cargo.lock                      |  12 +
 Cargo.toml                      |   1 +
 src/db.rs                       |  43 ++--
 src/main.rs                     |   9 +-
 src/reporting/builders.rs       |  63 ++++--
 src/reporting/dynamic_report.rs | 387 ++++++++++++++++++++++----------
 src/reporting/executor.rs       |  24 +-
 src/reporting/mod.rs            |   4 +-
 src/reporting/steps.rs          | 307 ++++++++++++++-----------
 src/reporting/types.rs          |  31 ++-
 10 files changed, 553 insertions(+), 328 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index a9e4885..f81a1ac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index d5bbea0..1546094 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"
diff --git a/src/db.rs b/src/db.rs
index c977132..05d1595 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -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");
 
diff --git a/src/main.rs b/src/main.rs
index 27da957..2037235 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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",
diff --git a/src/reporting/builders.rs b/src/reporting/builders.rs
index d6a5e8b..1f1e860 100644
--- a/src/reporting/builders.rs
+++ b/src/reporting/builders.rs
@@ -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)
 	}
 }
diff --git a/src/reporting/dynamic_report.rs b/src/reporting/dynamic_report.rs
index d1b25a6..82ef0ca 100644
--- a/src/reporting/dynamic_report.rs
+++ b/src/reporting/dynamic_report.rs
@@ -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
diff --git a/src/reporting/executor.rs b/src/reporting/executor.rs
index 5c9dd4d..decc709 100644
--- a/src/reporting/executor.rs
+++ b/src/reporting/executor.rs
@@ -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())
 }
diff --git a/src/reporting/mod.rs b/src/reporting/mod.rs
index 1ea2e13..9214bd3 100644
--- a/src/reporting/mod.rs
+++ b/src/reporting/mod.rs
@@ -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)
 }
diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs
index 7febc34..d836b9c 100644
--- a/src/reporting/steps.rs
+++ b/src/reporting/steps.rs
@@ -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)
 	}
 }
diff --git a/src/reporting/types.rs b/src/reporting/types.rs
index 1cbfb95..d1e6428 100644
--- a/src/reporting/types.rs
+++ b/src/reporting/types.rs
@@ -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);