/* 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 . */ use std::collections::HashSet; use std::sync::Arc; use chrono::NaiveDate; use libdrcr::db::DbConnection; use libdrcr::model::assertions::BalanceAssertion; use libdrcr::reporting::dynamic_report::DynamicReport; use libdrcr::reporting::generate_report; use libdrcr::reporting::types::{ BalancesAt, DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs, ReportingContext, ReportingProduct, ReportingProductId, ReportingProductKind, ReportingStepArgs, Transactions, }; use serde::{Deserialize, Serialize}; use tauri::path::BaseDirectory; use tauri::{AppHandle, Manager, State}; use tokio::sync::Mutex; use crate::AppState; fn prepare_reporting_context(context: &mut ReportingContext) { libdrcr::reporting::steps::register_lookup_fns(context); libdrcr::reporting::builders::register_dynamic_builders(context); libdrcr::plugin::register_lookup_fns(context); } pub(crate) async fn get_report( app: AppHandle, state: State<'_, Mutex>, target: &ReportingProductId, ) -> Box { let state = state.lock().await; let db_filename = state.db_filename.clone().unwrap(); // Connect to database let db_connection = DbConnection::new(format!("sqlite:{}", db_filename.as_str()).as_str()).await; // Initialise ReportingContext let eofy_date = db_connection.metadata().eofy_date; let plugin_names = db_connection.metadata().plugins.clone(); let mut context = ReportingContext::new( db_connection, app.path() .resolve("plugins", BaseDirectory::Resource) .unwrap() .to_str() .unwrap() .to_string(), plugin_names, eofy_date, "$".to_string(), ); prepare_reporting_context(&mut context); // Get dynamic report let mut targets = vec![target.clone()]; // Add plugin targets // FIXME: Detect this robustly if context.plugin_names.contains(&"austax".to_string()) { targets.push(ReportingProductId { name: "CalculateIncomeTax".to_string(), kind: ReportingProductKind::Transactions, args: ReportingStepArgs::VoidArgs, }); } let products = generate_report(targets, Arc::new(context)).await.unwrap(); let result = products.get_owned_or_err(&target).unwrap(); result } #[tauri::command] pub(crate) async fn get_all_transactions_except_earnings_to_equity( app: AppHandle, state: State<'_, Mutex>, ) -> Result { let transactions = get_report( app, state, &ReportingProductId { name: "AllTransactionsExceptEarningsToEquity".to_string(), kind: ReportingProductKind::Transactions, args: ReportingStepArgs::DateArgs(DateArgs { date: NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), }), }, ) .await .downcast::() .unwrap() .transactions; Ok(serde_json::to_string(&transactions).unwrap()) } #[tauri::command] pub(crate) async fn get_all_transactions_except_earnings_to_equity_for_account( app: AppHandle, state: State<'_, Mutex>, account: String, ) -> Result { let transactions = get_report( app, state, &ReportingProductId { name: "AllTransactionsExceptEarningsToEquity".to_string(), kind: ReportingProductKind::Transactions, args: ReportingStepArgs::DateArgs(DateArgs { date: NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(), }), }, ) .await .downcast::() .unwrap() .transactions; // Filter only transactions affecting this account let filtered_transactions = transactions .into_iter() .filter(|t| t.postings.iter().any(|p| p.account == account)) .collect::>(); Ok(serde_json::to_string(&filtered_transactions).unwrap()) } #[tauri::command] pub(crate) async fn get_balance_sheet( app: AppHandle, state: State<'_, Mutex>, dates: Vec, ) -> Result { let mut date_args = Vec::new(); for date in dates.iter() { date_args.push(DateArgs { date: NaiveDate::parse_from_str(date, "%Y-%m-%d").expect("Invalid date"), }) } Ok(get_report( app, state, &ReportingProductId { name: "BalanceSheet".to_string(), kind: ReportingProductKind::DynamicReport, args: ReportingStepArgs::MultipleDateArgs(MultipleDateArgs { dates: date_args.clone(), }), }, ) .await .downcast_ref::() .unwrap() .to_json()) } #[tauri::command] pub(crate) async fn get_income_statement( app: AppHandle, state: State<'_, Mutex>, dates: Vec<(String, String)>, ) -> Result { let mut date_args = Vec::new(); for (date_start, date_end) in dates.iter() { date_args.push(DateStartDateEndArgs { date_start: NaiveDate::parse_from_str(date_start, "%Y-%m-%d").expect("Invalid date"), date_end: NaiveDate::parse_from_str(date_end, "%Y-%m-%d").expect("Invalid date"), }) } Ok(get_report( app, state, &ReportingProductId { name: "IncomeStatement".to_string(), kind: ReportingProductKind::DynamicReport, args: ReportingStepArgs::MultipleDateStartDateEndArgs(MultipleDateStartDateEndArgs { dates: date_args.clone(), }), }, ) .await .downcast_ref::() .unwrap() .to_json()) } #[tauri::command] pub(crate) async fn get_trial_balance( app: AppHandle, state: State<'_, Mutex>, date: String, ) -> Result { let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date"); Ok(get_report( app, state, &ReportingProductId { name: "TrialBalance".to_string(), kind: ReportingProductKind::DynamicReport, args: ReportingStepArgs::DateArgs(DateArgs { date }), }, ) .await .downcast_ref::() .unwrap() .to_json()) } #[derive(Deserialize, Serialize)] struct ValidatedBalanceAssertion { #[serde(flatten)] assertion: BalanceAssertion, is_valid: bool, } #[tauri::command] pub(crate) async fn get_validated_balance_assertions( app: AppHandle, state: State<'_, Mutex>, ) -> Result { let state = state.lock().await; let db_filename = state.db_filename.clone().unwrap(); // Connect to database let db_connection = DbConnection::new(format!("sqlite:{}", db_filename.as_str()).as_str()).await; let reporting_commodity = db_connection.metadata().reporting_commodity.clone(); // Needed later // First get balance assertions from database let balance_assertions = db_connection.get_balance_assertions().await; // Get dates of balance assertions let dates = balance_assertions .iter() .map(|b| b.dt) .collect::>(); // Initialise ReportingContext let eofy_date = db_connection.metadata().eofy_date; let plugin_names = db_connection.metadata().plugins.clone(); let mut context = ReportingContext::new( db_connection, app.path() .resolve("plugins", BaseDirectory::Resource) .unwrap() .to_str() .unwrap() .to_string(), plugin_names, eofy_date, "$".to_string(), ); prepare_reporting_context(&mut context); // Get report targets let mut targets = Vec::new(); for dt in dates { // Request ordinary transaction balances at each balance assertion date targets.push(ReportingProductId { name: "CombineOrdinaryTransactions".to_string(), kind: ReportingProductKind::BalancesAt, args: ReportingStepArgs::DateArgs(DateArgs { date: dt.date() }), }); } // Add plugin targets // FIXME: Detect this robustly if context.plugin_names.contains(&"austax".to_string()) { targets.push(ReportingProductId { name: "CalculateIncomeTax".to_string(), kind: ReportingProductKind::Transactions, args: ReportingStepArgs::VoidArgs, }); } // Run report let products = generate_report(targets, Arc::new(context)).await.unwrap(); // Validate each balance assertion let mut validated_assertions = Vec::new(); for balance_assertion in balance_assertions { let balances_at_date = products .get_or_err(&ReportingProductId { name: "CombineOrdinaryTransactions".to_string(), kind: ReportingProductKind::BalancesAt, args: ReportingStepArgs::DateArgs(DateArgs { date: balance_assertion.dt.date(), }), }) .unwrap() .downcast_ref::() .unwrap(); let account_balance = *balances_at_date .balances .get(&balance_assertion.account) .unwrap_or(&0); let is_valid = balance_assertion.quantity == account_balance && balance_assertion.commodity == reporting_commodity; validated_assertions.push(ValidatedBalanceAssertion { assertion: balance_assertion, is_valid, }); } Ok(serde_json::to_string(&validated_assertions).unwrap()) }