/*
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())
}