diff --git a/src/account_config.rs b/src/account_config.rs
new file mode 100644
index 0000000..c17e5d6
--- /dev/null
+++ b/src/account_config.rs
@@ -0,0 +1,43 @@
+/*
+ DrCr: Web-based double-entry bookkeeping framework
+ Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+use std::collections::HashMap;
+
+pub struct AccountConfiguration {
+ pub id: Option,
+ pub account: String,
+ pub kind: String,
+ pub data: Option,
+}
+
+/// Convert [`Vec`] into a [HashMap] mapping account names to account kinds
+pub fn kinds_for_account(
+ account_configurations: Vec,
+) -> HashMap> {
+ let mut result = HashMap::new();
+
+ for account_configuration in account_configurations {
+ // Record the account kind
+ result
+ .entry(account_configuration.account)
+ .or_insert_with(|| Vec::new())
+ .push(account_configuration.kind);
+ }
+
+ result
+}
diff --git a/src/db.rs b/src/db.rs
index b7f0307..6c39107 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -21,9 +21,11 @@ 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 {
@@ -73,7 +75,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.unwrap();
+ ).bind(format_date(date)).fetch_all(connection.deref_mut()).await.expect("SQL error");
let mut balances = HashMap::new();
for row in rows {
@@ -82,4 +84,27 @@ impl DbConnection {
balances
}
+
+ /// Get account configurations from the database
+ pub fn get_account_configurations(&self) -> Vec {
+ run_blocking(self.get_account_configurations_async())
+ }
+
+ async fn get_account_configurations_async(&self) -> Vec {
+ let mut connection = self.sqlx_connection.borrow_mut();
+
+ let account_configurations =
+ sqlx::query("SELECT id, account, kind, data FROM account_configurations")
+ .map(|r: SqliteRow| AccountConfiguration {
+ id: r.get("id"),
+ account: r.get("account"),
+ kind: r.get("kind"),
+ data: r.get("data"),
+ })
+ .fetch_all(connection.deref_mut())
+ .await
+ .expect("SQL error");
+
+ account_configurations
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 8f1052b..4400e00 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod account_config;
pub mod db;
pub mod reporting;
pub mod transaction;
diff --git a/src/main.rs b/src/main.rs
index 7f81e00..e402984 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -34,8 +34,11 @@ fn main() {
let db_connection = DbConnection::connect("sqlite:drcr_testing.db");
// Initialise ReportingContext
- let mut context =
- ReportingContext::new(db_connection, NaiveDate::from_ymd_opt(2025, 6, 30).unwrap());
+ let mut context = ReportingContext::new(
+ db_connection,
+ NaiveDate::from_ymd_opt(2025, 6, 30).unwrap(),
+ "$".to_string(),
+ );
register_lookup_fns(&mut context);
register_dynamic_builders(&mut context);
@@ -65,6 +68,7 @@ fn main() {
})
.unwrap();
+ println!("Income statement:");
println!("{:?}", result);
// Get balance sheet
@@ -89,6 +93,6 @@ fn main() {
})
.unwrap();
- //println!("{}", products);
+ println!("Balance sheet:");
println!("{:?}", result);
}
diff --git a/src/reporting/steps.rs b/src/reporting/steps.rs
index 39174ce..4d6d8ec 100644
--- a/src/reporting/steps.rs
+++ b/src/reporting/steps.rs
@@ -23,8 +23,11 @@ use std::fmt::Display;
use chrono::Datelike;
+use crate::account_config::kinds_for_account;
use crate::reporting::types::{BalancesAt, DateStartDateEndArgs, ReportingProductId, Transactions};
-use crate::transaction::update_balances_from_transactions;
+use crate::transaction::{
+ update_balances_from_transactions, Posting, Transaction, TransactionWithPostings,
+};
use crate::util::sofy_from_eofy;
use super::calculator::ReportingGraphDependencies;
@@ -659,17 +662,72 @@ impl ReportingStep for RetainedEarningsToEquity {
fn execute(
&self,
- _context: &ReportingContext,
+ context: &ReportingContext,
_steps: &Vec>,
_dependencies: &ReportingGraphDependencies,
products: &mut ReportingProducts,
) -> Result<(), ReportingExecutionError> {
- eprintln!("Stub: RetainedEarningsToEquity.execute");
+ // Get balances at end of last financial year
+ let last_eofy_date = context
+ .eofy_date
+ .with_year(context.eofy_date.year() - 1)
+ .unwrap();
- let transactions = Transactions {
+ let balances_last_eofy = products
+ .get_or_err(&ReportingProductId {
+ name: "CombineOrdinaryTransactions",
+ kind: ReportingProductKind::BalancesAt,
+ args: Box::new(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());
+
+ // 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(),
+ },
+ Posting {
+ id: None,
+ transaction_id: None,
+ description: None,
+ account: "Retained Earnings".to_string(),
+ quantity: *balance,
+ commodity: context.reporting_commodity.clone(),
+ },
+ ],
+ })
+ }
+ }
+ }
+
products.insert(
ReportingProductId {
name: self.id().name,
diff --git a/src/reporting/types.rs b/src/reporting/types.rs
index 3c7cb6a..c78547b 100644
--- a/src/reporting/types.rs
+++ b/src/reporting/types.rs
@@ -41,6 +41,7 @@ pub struct ReportingContext {
// Configuration
pub db_connection: DbConnection,
pub eofy_date: NaiveDate,
+ pub reporting_commodity: String,
// State
pub(crate) step_lookup_fn: HashMap<
@@ -52,10 +53,15 @@ pub struct ReportingContext {
impl ReportingContext {
/// Initialise a new [ReportingContext]
- pub fn new(db_connection: DbConnection, eofy_date: NaiveDate) -> Self {
+ pub fn new(
+ db_connection: DbConnection,
+ eofy_date: NaiveDate,
+ reporting_commodity: String,
+ ) -> Self {
Self {
- db_connection: db_connection,
- eofy_date: eofy_date,
+ db_connection,
+ eofy_date,
+ reporting_commodity,
step_lookup_fn: HashMap::new(),
step_dynamic_builders: Vec::new(),
}
diff --git a/src/transaction.rs b/src/transaction.rs
index 1220c95..f887cbc 100644
--- a/src/transaction.rs
+++ b/src/transaction.rs
@@ -39,7 +39,7 @@ pub struct TransactionWithPostings {
pub struct Posting {
pub id: Option,
pub transaction_id: Option,
- pub description: String,
+ pub description: Option,
pub account: String,
pub quantity: QuantityInt,
pub commodity: String,