From 3add701d3cf9169786a9e31fcad027bc4922b5bc Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 23 May 2025 23:27:00 +1000 Subject: [PATCH] Implement RetainedEarningsToEquity --- src/account_config.rs | 43 +++++++++++++++++++++++++++ src/db.rs | 27 ++++++++++++++++- src/lib.rs | 1 + src/main.rs | 10 +++++-- src/reporting/steps.rs | 66 +++++++++++++++++++++++++++++++++++++++--- src/reporting/types.rs | 12 ++++++-- src/transaction.rs | 2 +- 7 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 src/account_config.rs 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,