/*
DrCr: Double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
//! This module contains concrete [ReportingStep] implementations
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::model::transaction::{
update_balances_from_transactions, Posting, Transaction, TransactionWithPostings,
};
use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions};
use crate::util::{get_eofy, sofy_from_eofy};
use crate::{QuantityInt, UNCLASSIFIED_STATEMENT_LINE_CREDITS, UNCLASSIFIED_STATEMENT_LINE_DEBITS};
use super::calculator::ReportingGraphDependencies;
use super::dynamic_report::{entries_for_kind, DynamicReport, DynamicReportEntry, Row, Section};
use super::executor::ReportingExecutionError;
use super::types::{
BalancesBetween, DateArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, ReportingContext,
ReportingProductKind, ReportingProducts, ReportingStep, ReportingStepArgs, ReportingStepId,
};
/// Call [ReportingContext::register_lookup_fn] for all steps provided by this module
pub fn register_lookup_fns(context: &mut ReportingContext) {
AllTransactionsExceptEarningsToEquity::register_lookup_fn(context);
AllTransactionsExceptEarningsToEquityBalances::register_lookup_fn(context);
AllTransactionsIncludingEarningsToEquity::register_lookup_fn(context);
BalanceSheet::register_lookup_fn(context);
CombineOrdinaryTransactions::register_lookup_fn(context);
CombineOrdinaryTransactionsBalances::register_lookup_fn(context);
CurrentYearEarningsToEquity::register_lookup_fn(context);
DBBalances::register_lookup_fn(context);
DBTransactions::register_lookup_fn(context);
IncomeStatement::register_lookup_fn(context);
PostUnreconciledStatementLines::register_lookup_fn(context);
RetainedEarningsToEquity::register_lookup_fn(context);
TrialBalance::register_lookup_fn(context);
}
/// Target representing all transactions except charging current year and retained earnings to equity (returns transaction list)
///
/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax].
///
/// Used as the basis for the income statement.
#[derive(Debug)]
pub struct AllTransactionsExceptEarningsToEquity {
pub args: DateArgs,
}
impl AllTransactionsExceptEarningsToEquity {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"AllTransactionsExceptEarningsToEquity".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(AllTransactionsExceptEarningsToEquity { args: args.into() })
}
}
impl Display for AllTransactionsExceptEarningsToEquity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for AllTransactionsExceptEarningsToEquity {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
// AllTransactionsExceptEarningsToEquity always depends on CombineOrdinaryTransactions at least
vec![ReportingProductId {
name: "CombineOrdinaryTransactions".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
}]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
combine_transactions_of_all_dependencies(self.id(), dependencies, products).await
}
}
/// Target representing all transactions except charging current year and retained earnings to equity (returns balances)
///
/// By default, this is [CombineOrdinaryTransactions] and, if requested, [CalculateIncomeTax].
///
/// Used as the basis for the income statement.
#[derive(Debug)]
pub struct AllTransactionsExceptEarningsToEquityBalances {
pub product_kind: ReportingProductKind,
pub args: ReportingStepArgs,
}
impl AllTransactionsExceptEarningsToEquityBalances {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"AllTransactionsExceptEarningsToEquity".to_string(),
vec![ReportingProductKind::BalancesAt],
Self::takes_args,
|_name, args, _ctx| Self::from_args(ReportingProductKind::BalancesAt, args),
);
context.register_lookup_fn(
"AllTransactionsExceptEarningsToEquity".to_string(),
vec![ReportingProductKind::BalancesBetween],
Self::takes_args,
|_name, args, _ctx| Self::from_args(ReportingProductKind::BalancesBetween, args),
);
}
fn takes_args(_name: &str, _args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
true
}
fn from_args(
product_kind: ReportingProductKind,
args: ReportingStepArgs,
) -> Box {
Box::new(AllTransactionsExceptEarningsToEquityBalances { product_kind, args })
}
}
impl Display for AllTransactionsExceptEarningsToEquityBalances {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for AllTransactionsExceptEarningsToEquityBalances {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
product_kinds: vec![self.product_kind],
args: self.args.clone(),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
// AllTransactionsExceptEarningsToEquity always depends on CombineOrdinaryTransactions at least
vec![ReportingProductId {
name: "CombineOrdinaryTransactions".to_string(),
kind: self.product_kind,
args: self.args.clone(),
}]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get all dependencies
let step_dependencies = dependencies.dependencies_for_step(&self.id());
// Identify the product_kind dependency most recently generated
// TODO: Make this deterministic - parallel execution may cause the order to vary
for (product_id, product) in products.map().iter().rev() {
if step_dependencies.iter().any(|d| d.product == *product_id) {
// Store the result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: self.product_kind,
args: self.args.clone(),
},
product.clone(),
);
return Ok(result);
}
}
// No dependencies?! - this is likely a mistake
panic!(
"Requested {:?} but no available dependencies to provide it",
self.product_kind
);
}
}
/// Target representing all transactions including charging current year and retained earnings to equity
///
/// In other words, this is [AllTransactionsExceptEarningsToEquity], [CurrentYearEarningsToEquity] and [RetainedEarningsToEquity].
///
/// Used as the basis for the balance sheet.
#[derive(Debug)]
pub struct AllTransactionsIncludingEarningsToEquity {
pub args: DateArgs,
}
impl AllTransactionsIncludingEarningsToEquity {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"AllTransactionsIncludingEarningsToEquity".to_string(),
vec![ReportingProductKind::BalancesAt],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(AllTransactionsIncludingEarningsToEquity { args: args.into() })
}
}
impl Display for AllTransactionsIncludingEarningsToEquity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for AllTransactionsIncludingEarningsToEquity {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "AllTransactionsIncludingEarningsToEquity".to_string(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
vec![
// AllTransactionsIncludingEarningsToEquity requires AllTransactionsExceptEarningsToEquity
ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
// AllTransactionsIncludingEarningsToEquity requires CurrentYearEarningsToEquity
ReportingProductId {
name: "CurrentYearEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
// AllTransactionsIncludingEarningsToEquity requires RetainedEarningsToEquity
ReportingProductId {
name: "RetainedEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get opening balances from AllTransactionsExceptEarningsToEquity
let opening_balances = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
})?
.downcast_ref::()
.unwrap();
// Get CurrentYearEarningsToEquity transactions
let transactions_current = products
.get_or_err(&ReportingProductId {
name: "CurrentYearEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
})?
.downcast_ref::()
.unwrap();
// Get RetainedEarningsToEquity transactions
let transactions_retained = products
.get_or_err(&ReportingProductId {
name: "RetainedEarningsToEquity".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
})?
.downcast_ref::()
.unwrap();
// Update balances
let mut balances = BalancesAt {
balances: opening_balances.balances.clone(),
};
update_balances_from_transactions(
&mut balances.balances,
transactions_current.transactions.iter(),
);
update_balances_from_transactions(
&mut balances.balances,
transactions_retained.transactions.iter(),
);
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// Generates a balance sheet [DynamicReport]
#[derive(Debug)]
pub struct BalanceSheet {
pub args: MultipleDateArgs,
}
impl BalanceSheet {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"BalanceSheet".to_string(),
vec![ReportingProductKind::DynamicReport],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::MultipleDateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(BalanceSheet { args: args.into() })
}
}
impl Display for BalanceSheet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for BalanceSheet {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "BalanceSheet".to_string(),
product_kinds: vec![ReportingProductKind::DynamicReport],
args: ReportingStepArgs::MultipleDateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
let mut result = Vec::new();
// BalanceSheet depends on AllTransactionsIncludingEarningsToEquity in each requested period
for date_args in self.args.dates.iter() {
result.push(ReportingProductId {
name: "AllTransactionsIncludingEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(date_args.clone()),
});
}
result
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get balances for each period
let mut balances: Vec<&HashMap> = Vec::new();
for date_args in self.args.dates.iter() {
let product = products.get_or_err(&ReportingProductId {
name: "AllTransactionsIncludingEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(date_args.clone()),
})?;
balances.push(&product.downcast_ref::().unwrap().balances);
}
// Get names of all balance sheet accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await);
// Init report
let mut report = DynamicReport::new(
"Balance sheet".to_string(),
self.args.dates.iter().map(|d| d.date.to_string()).collect(),
Vec::new(),
);
// Add assets section
let mut assets = Section {
text: Some("Assets".to_string()),
id: None,
visible: true,
entries: entries_for_kind("drcr.asset", false, &balances, &kinds_for_account),
};
let total_assets = assets.subtotal(&report);
assets.entries.push(
Row {
text: "Total assets".to_string(),
quantity: total_assets,
id: Some("total_assets".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
report.entries.push(assets.into());
report.entries.push(DynamicReportEntry::Spacer);
// Add liabilities section
let mut liabilities = Section {
text: Some("Liabilities".to_string()),
id: None,
visible: true,
entries: entries_for_kind("drcr.liability", true, &balances, &kinds_for_account),
};
let total_liabilities = liabilities.subtotal(&report);
liabilities.entries.push(
Row {
text: "Total liabilities".to_string(),
quantity: total_liabilities,
id: Some("total_liabilities".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
report.entries.push(liabilities.into());
report.entries.push(DynamicReportEntry::Spacer);
// Add equity section
let mut equity = Section {
text: Some("Equity".to_string()),
id: None,
visible: true,
entries: entries_for_kind("drcr.equity", true, &balances, &kinds_for_account),
};
let total_equity = equity.subtotal(&report);
equity.entries.push(
Row {
text: "Total equity".to_string(),
quantity: total_equity,
id: Some("total_equity".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
report.entries.push(equity.into());
// Store the result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: "BalanceSheet".to_string(),
kind: ReportingProductKind::DynamicReport,
args: ReportingStepArgs::MultipleDateArgs(self.args.clone()),
},
Box::new(report),
);
Ok(result)
}
}
/// Combines all steps producing ordinary transactions (returns transaction list)
///
/// By default, these are [DBTransactions] and [PostUnreconciledStatementLines].
#[derive(Debug)]
pub struct CombineOrdinaryTransactions {
pub args: DateArgs,
}
impl CombineOrdinaryTransactions {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CombineOrdinaryTransactions".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(CombineOrdinaryTransactions { args: args.into() })
}
}
impl Display for CombineOrdinaryTransactions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for CombineOrdinaryTransactions {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CombineOrdinaryTransactions".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
vec![
// CombineOrdinaryTransactions depends on DBTransactions
ReportingProductId {
name: "DBTransactions".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
// CombineOrdinaryTransactions depends on PostUnreconciledStatementLines
ReportingProductId {
name: "PostUnreconciledStatementLines".to_string(),
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
combine_transactions_of_all_dependencies(self.id(), dependencies, products).await
}
}
/// Combines all steps producing ordinary transactions (returns balances)
///
/// By default, these are [DBBalances] and [PostUnreconciledStatementLines].
#[derive(Debug)]
pub struct CombineOrdinaryTransactionsBalances {
pub args: DateArgs,
}
impl CombineOrdinaryTransactionsBalances {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CombineOrdinaryTransactions".to_string(),
vec![ReportingProductKind::BalancesAt],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(CombineOrdinaryTransactionsBalances { args: args.into() })
}
}
impl Display for CombineOrdinaryTransactionsBalances {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for CombineOrdinaryTransactionsBalances {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CombineOrdinaryTransactions".to_string(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
vec![
// CombineOrdinaryTransactions depends on DBBalances
ReportingProductId {
name: "DBBalances".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
// CombineOrdinaryTransactions depends on PostUnreconciledStatementLines
ReportingProductId {
name: "PostUnreconciledStatementLines".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
]
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Sum balances of all dependencies
let mut balances = BalancesAt {
balances: HashMap::new(),
};
for dependency in dependencies.dependencies_for_step(&self.id()) {
let dependency_balances = &products
.get_or_err(&dependency.product)?
.downcast_ref::()
.unwrap()
.balances;
for (account, balance) in dependency_balances.iter() {
let running_balance = balances.balances.get(account).unwrap_or(&0) + balance;
balances.balances.insert(account.clone(), running_balance);
}
}
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// Transfer year-to-date balances in income and expense accounts (as at the requested date) to the current year earnings equity account
#[derive(Debug)]
pub struct CurrentYearEarningsToEquity {
pub args: DateArgs,
}
impl CurrentYearEarningsToEquity {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"CurrentYearEarningsToEquity".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(CurrentYearEarningsToEquity { args: args.into() })
}
}
impl Display for CurrentYearEarningsToEquity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for CurrentYearEarningsToEquity {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "CurrentYearEarningsToEquity".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, context: &ReportingContext) -> Vec {
// CurrentYearEarningsToEquity depends on AllTransactionsExceptEarningsToEquity
vec![ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: sofy_from_eofy(get_eofy(&self.args.date, &context.eofy_date)),
date_end: self.args.date,
}),
}]
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get balances for this financial year
let balances = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(DateStartDateEndArgs {
date_start: sofy_from_eofy(get_eofy(&self.args.date, &context.eofy_date)),
date_end: self.args.date,
}),
})?
.downcast_ref::()
.unwrap();
// Get income and expense accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await);
// Transfer income and expense balances to current year earnings
let mut transactions = Transactions {
transactions: Vec::new(),
};
for (account, balance) in balances.balances.iter() {
if let Some(kinds) = kinds_for_account.get(account) {
if kinds
.iter()
.any(|k| k == "drcr.income" || k == "drcr.expense")
{
transactions.transactions.push(TransactionWithPostings {
transaction: Transaction {
id: None,
dt: self.args.date.and_hms_opt(0, 0, 0).unwrap(),
description: "Current year earnings".to_string(),
},
postings: vec![
Posting {
id: None,
transaction_id: None,
description: None,
account: account.clone(),
quantity: -balance,
commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(-balance),
},
Posting {
id: None,
transaction_id: None,
description: None,
account: crate::CURRENT_YEAR_EARNINGS.to_string(),
quantity: *balance,
commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(*balance),
},
],
})
}
}
}
// Store product
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(transactions),
);
Ok(result)
}
}
/// Look up account balances from the database
#[derive(Debug)]
pub struct DBBalances {
pub args: DateArgs,
}
impl DBBalances {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"DBBalances".to_string(),
vec![ReportingProductKind::BalancesAt],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(DBBalances { args: args.into() })
}
}
impl Display for DBBalances {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for DBBalances {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "DBBalances".to_string(),
product_kinds: vec![ReportingProductKind::BalancesAt],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock,
) -> Result {
// Get balances from DB
let balances = BalancesAt {
balances: context.db_connection.get_balances(self.args.date).await,
};
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(balances),
);
Ok(result)
}
}
/// Look up transactions from the database
#[derive(Debug)]
pub struct DBTransactions {}
impl DBTransactions {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"DBTransactions".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
*args == ReportingStepArgs::VoidArgs
}
fn from_args(
_name: &str,
_args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(DBTransactions {})
}
}
impl Display for DBTransactions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for DBTransactions {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "DBTransactions".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::VoidArgs,
}
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock,
) -> Result {
// Get transactions from DB
let transactions = Transactions {
transactions: context.db_connection.get_transactions().await,
};
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
Box::new(transactions),
);
Ok(result)
}
}
/// Generates an income statement [DynamicReport]
#[derive(Debug)]
pub struct IncomeStatement {
pub args: MultipleDateStartDateEndArgs,
}
impl IncomeStatement {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"IncomeStatement".to_string(),
vec![ReportingProductKind::DynamicReport],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::MultipleDateStartDateEndArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(IncomeStatement { args: args.into() })
}
}
impl Display for IncomeStatement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for IncomeStatement {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "IncomeStatement".to_string(),
product_kinds: vec![ReportingProductKind::DynamicReport],
args: ReportingStepArgs::MultipleDateStartDateEndArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
let mut result = Vec::new();
// IncomeStatement depends on AllTransactionsExceptEarningsToEquity in each requested period
for date_args in self.args.dates.iter() {
result.push(ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(date_args.clone()),
});
}
result
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get balances for each period
let mut balances: Vec<&HashMap> = Vec::new();
for date_args in self.args.dates.iter() {
let product = products.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesBetween,
args: ReportingStepArgs::DateStartDateEndArgs(date_args.clone()),
})?;
balances.push(&product.downcast_ref::().unwrap().balances);
}
// Get names of all income statement accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await);
// Init report
let mut report = DynamicReport::new(
"Income statement".to_string(),
self.args
.dates
.iter()
.map(|d| d.date_end.to_string())
.collect(),
Vec::new(),
);
// Add income section
let mut income = Section {
text: Some("Income".to_string()),
id: None,
visible: true,
entries: entries_for_kind("drcr.income", true, &balances, &kinds_for_account),
};
let total_income = income.subtotal(&report);
income.entries.push(
Row {
text: "Total income".to_string(),
quantity: total_income.clone(),
id: Some("total_income".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
report.entries.push(income.into());
report.entries.push(DynamicReportEntry::Spacer);
// Add expenses section
let mut expenses = Section {
text: Some("Expenses".to_string()),
id: None,
visible: true,
entries: entries_for_kind("drcr.expense", false, &balances, &kinds_for_account),
};
let total_expenses = expenses.subtotal(&report);
expenses.entries.push(
Row {
text: "Total expenses".to_string(),
quantity: total_expenses.clone(),
id: Some("total_expenses".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
report.entries.push(expenses.into());
report.entries.push(DynamicReportEntry::Spacer);
// Add net surplus (deficit) row
let net_surplus = total_income
.into_iter()
.zip(total_expenses.into_iter())
.map(|(i, e)| i - e)
.collect();
report.entries.push(
Row {
text: "Net surplus (deficit)".to_string(),
quantity: net_surplus,
id: Some("net_surplus".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
// Store the result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: "IncomeStatement".to_string(),
kind: ReportingProductKind::DynamicReport,
args: ReportingStepArgs::MultipleDateStartDateEndArgs(self.args.clone()),
},
Box::new(report),
);
Ok(result)
}
}
/// Generate transactions for unreconciled statement lines
#[derive(Debug)]
pub struct PostUnreconciledStatementLines {}
impl PostUnreconciledStatementLines {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"PostUnreconciledStatementLines".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
*args == ReportingStepArgs::VoidArgs
}
fn from_args(
_name: &str,
_args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(PostUnreconciledStatementLines {})
}
}
impl Display for PostUnreconciledStatementLines {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for PostUnreconciledStatementLines {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "PostUnreconciledStatementLines".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::VoidArgs,
}
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
_products: &RwLock,
) -> Result {
let unreconciled_statement_lines = context
.db_connection
.get_unreconciled_statement_lines()
.await;
// Post unreconciled statement lines
let mut transactions = Transactions {
transactions: Vec::new(),
};
for line in unreconciled_statement_lines {
let unclassified_account = if line.quantity >= 0 {
UNCLASSIFIED_STATEMENT_LINE_DEBITS
} else {
UNCLASSIFIED_STATEMENT_LINE_CREDITS
};
transactions.transactions.push(TransactionWithPostings {
transaction: Transaction {
id: None,
dt: line.dt,
description: line.description.clone(),
},
postings: vec![
Posting {
id: None,
transaction_id: None,
description: None,
account: line.source_account.clone(),
quantity: line.quantity,
commodity: line.commodity.clone(),
quantity_ascost: None,
},
Posting {
id: None,
transaction_id: None,
description: None,
account: unclassified_account.to_string(),
quantity: -line.quantity,
commodity: line.commodity.clone(),
quantity_ascost: None,
},
],
});
}
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::VoidArgs,
},
Box::new(transactions),
);
Ok(result)
}
}
/// Transfer historical balances in income and expense accounts to the retained earnings equity account
#[derive(Debug)]
pub struct RetainedEarningsToEquity {
pub args: DateArgs,
}
impl RetainedEarningsToEquity {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"RetainedEarningsToEquity".to_string(),
vec![ReportingProductKind::Transactions],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(RetainedEarningsToEquity { args: args.into() })
}
}
impl Display for RetainedEarningsToEquity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for RetainedEarningsToEquity {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "RetainedEarningsToEquity".to_string(),
product_kinds: vec![ReportingProductKind::Transactions],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, context: &ReportingContext) -> Vec {
let eofy_date = get_eofy(&self.args.date, &context.eofy_date);
let last_eofy_date = eofy_date.with_year(eofy_date.year() - 1).unwrap();
// RetainedEarningsToEquity depends on AllTransactionsExceptEarningsToEquity for last financial year
vec![ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: last_eofy_date,
}),
}]
}
async fn execute(
&self,
context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
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();
// Get balances at end of last financial year
let balances_last_eofy = products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(DateArgs {
date: last_eofy_date.clone(),
}),
})?
.downcast_ref::()
.unwrap();
// Get income and expense accounts
let kinds_for_account =
kinds_for_account(context.db_connection.get_account_configurations().await);
// Transfer income and expense balances to retained earnings
let mut transactions = Transactions {
transactions: Vec::new(),
};
for (account, balance) in balances_last_eofy.balances.iter() {
if let Some(kinds) = kinds_for_account.get(account) {
if kinds
.iter()
.any(|k| k == "drcr.income" || k == "drcr.expense")
{
transactions.transactions.push(TransactionWithPostings {
transaction: Transaction {
id: None,
dt: last_eofy_date.and_hms_opt(0, 0, 0).unwrap(),
description: "Retained earnings".to_string(),
},
postings: vec![
Posting {
id: None,
transaction_id: None,
description: None,
account: account.clone(),
quantity: -balance,
commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(-balance),
},
Posting {
id: None,
transaction_id: None,
description: None,
account: crate::RETAINED_EARNINGS.to_string(),
quantity: *balance,
commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(*balance),
},
],
})
}
}
}
// Store product
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: self.id().name,
kind: ReportingProductKind::Transactions,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(transactions),
);
Ok(result)
}
}
/// Generates a trial balance [DynamicReport]
#[derive(Debug)]
pub struct TrialBalance {
pub args: DateArgs,
}
impl TrialBalance {
fn register_lookup_fn(context: &mut ReportingContext) {
context.register_lookup_fn(
"TrialBalance".to_string(),
vec![ReportingProductKind::DynamicReport],
Self::takes_args,
Self::from_args,
);
}
fn takes_args(_name: &str, args: &ReportingStepArgs, _context: &ReportingContext) -> bool {
matches!(args, ReportingStepArgs::DateArgs(_))
}
fn from_args(
_name: &str,
args: ReportingStepArgs,
_context: &ReportingContext,
) -> Box {
Box::new(TrialBalance { args: args.into() })
}
}
impl Display for TrialBalance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}", self.id()))
}
}
#[async_trait]
impl ReportingStep for TrialBalance {
fn id(&self) -> ReportingStepId {
ReportingStepId {
name: "TrialBalance".to_string(),
product_kinds: vec![ReportingProductKind::DynamicReport],
args: ReportingStepArgs::DateArgs(self.args.clone()),
}
}
fn requires(&self, _context: &ReportingContext) -> Vec {
let mut result = Vec::new();
// TrialBalance depends on AllTransactionsExceptEarningsToEquity at the requested date
result.push(ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
});
result
}
async fn execute(
&self,
_context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Get balances for each period
let balances = &products
.get_or_err(&ReportingProductId {
name: "AllTransactionsExceptEarningsToEquity".to_string(),
kind: ReportingProductKind::BalancesAt,
args: ReportingStepArgs::DateArgs(self.args.clone()),
})?
.downcast_ref::()
.unwrap()
.balances;
// Get sorted list of accounts
let mut accounts = balances.keys().collect::>();
accounts.sort();
// Init report
let mut report = DynamicReport {
title: "Trial balance".to_string(),
columns: vec!["Dr".to_string(), "Cr".to_string()],
entries: Vec::new(),
};
// Add entry for each account
let mut section = Section {
text: None,
id: Some("accounts".to_string()),
visible: true,
entries: Vec::new(),
};
for account in accounts {
section.entries.push(
Row {
text: account.clone(),
quantity: vec![
// Dr cell
if balances[account] >= 0 {
balances[account]
} else {
0
},
// Cr cell
if balances[account] < 0 {
-balances[account]
} else {
0
},
],
id: None,
visible: true,
link: Some(format!("/transactions/{}", account)),
heading: false,
bordered: false,
}
.into(),
);
}
let totals_row = section.subtotal(&report);
report.entries.push(section.into());
// Add total row
report.entries.push(
Row {
text: "Totals".to_string(),
quantity: totals_row,
id: Some("totals".to_string()),
visible: true,
link: None,
heading: true,
bordered: true,
}
.into(),
);
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: "TrialBalance".to_string(),
kind: ReportingProductKind::DynamicReport,
args: ReportingStepArgs::DateArgs(self.args.clone()),
},
Box::new(report),
);
Ok(result)
}
}
/// Combines the transactions of all dependencies and returns [Transactions] as [ReportingProducts] for the given step
///
/// Used to implement [CombineOrdinaryTransactions] and [AllTransactionsExceptEarningsToEquity].
async fn combine_transactions_of_all_dependencies(
step_id: ReportingStepId,
dependencies: &ReportingGraphDependencies,
products: &RwLock,
) -> Result {
let products = products.read().await;
// Combine transactions of all dependencies
let mut transactions = Transactions {
transactions: Vec::new(),
};
for dependency in dependencies.dependencies_for_step(&step_id) {
let dependency_transactions = &products
.get_or_err(&dependency.product)?
.downcast_ref::()
.unwrap()
.transactions;
for transaction in dependency_transactions.iter() {
transactions.transactions.push(transaction.clone());
}
}
// Store result
let mut result = ReportingProducts::new();
result.insert(
ReportingProductId {
name: step_id.name,
kind: ReportingProductKind::Transactions,
args: step_id.args,
},
Box::new(transactions),
);
Ok(result)
}