334 lines
8.9 KiB
Rust
334 lines
8.9 KiB
Rust
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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<AppState>>,
|
|
target: &ReportingProductId,
|
|
) -> Box<dyn ReportingProduct> {
|
|
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<AppState>>,
|
|
) -> Result<String, ()> {
|
|
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::<Transactions>()
|
|
.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<AppState>>,
|
|
account: String,
|
|
) -> Result<String, ()> {
|
|
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::<Transactions>()
|
|
.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::<Vec<_>>();
|
|
|
|
Ok(serde_json::to_string(&filtered_transactions).unwrap())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn get_balance_sheet(
|
|
app: AppHandle,
|
|
state: State<'_, Mutex<AppState>>,
|
|
dates: Vec<String>,
|
|
) -> Result<String, ()> {
|
|
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::<DynamicReport>()
|
|
.unwrap()
|
|
.to_json())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn get_income_statement(
|
|
app: AppHandle,
|
|
state: State<'_, Mutex<AppState>>,
|
|
dates: Vec<(String, String)>,
|
|
) -> Result<String, ()> {
|
|
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::<DynamicReport>()
|
|
.unwrap()
|
|
.to_json())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub(crate) async fn get_trial_balance(
|
|
app: AppHandle,
|
|
state: State<'_, Mutex<AppState>>,
|
|
date: String,
|
|
) -> Result<String, ()> {
|
|
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::<DynamicReport>()
|
|
.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<AppState>>,
|
|
) -> Result<String, ()> {
|
|
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::<HashSet<_>>();
|
|
|
|
// 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::<BalancesAt>()
|
|
.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())
|
|
}
|