Lines
89.87 %
Functions
8.89 %
Branches
100 %
use chrono::{DateTime, Utc};
use sqlx::types::Uuid;
use supp_macro::command;
use crate::{config::ConfigError, user::User};
use super::super::{
ActivityData, ActivityGroup, ActivityGroupResult, ActivityPeriod, CmdError, CmdResult,
FilterEntity, PeriodGrouping, ReportFilter, ReportMeta,
};
use super::fetch::{
fetch_accounts, fetch_date_range_splits_filtered_no_conversion,
fetch_date_range_splits_filtered_with_conversion, fetch_target_symbol,
use super::period::{
generate_month_boundaries, generate_quarter_boundaries, generate_year_boundaries,
use super::tree::build_tree;
// TODO(script-classifier): swap this helper for one that evaluates a
// user-supplied nomiscript expression returning a Vec<ActivityGroup>. See
// doc/reporting.org "Future work" for the intent.
fn default_activity_groups() -> Vec<ActivityGroup> {
vec![
ActivityGroup {
label: "Income".to_owned(),
filter: ReportFilter::Tag {
entity: FilterEntity::Account,
name: "type".to_owned(),
value: "income".to_owned(),
},
flip_sign: true,
label: "Expense".to_owned(),
value: "expense".to_owned(),
flip_sign: false,
]
}
fn compose_group_filter(group: &ActivityGroup, user_filter: Option<&ReportFilter>) -> ReportFilter {
match user_filter {
Some(f) => ReportFilter::And(vec![group.filter.clone(), f.clone()]),
None => group.filter.clone(),
fn boundaries_for(
grouping: PeriodGrouping,
from: DateTime<Utc>,
to: DateTime<Utc>,
) -> Vec<(String, DateTime<Utc>, DateTime<Utc>)> {
match grouping {
PeriodGrouping::Month => generate_month_boundaries(from, to),
PeriodGrouping::Quarter => generate_quarter_boundaries(from, to),
PeriodGrouping::Year => generate_year_boundaries(from, to),
async fn fetch_range(
conn: &mut sqlx::PgConnection,
target_commodity_id: Option<Uuid>,
target_symbol: Option<&str>,
filter: &ReportFilter,
) -> Result<super::tree::AccountAmounts, CmdError> {
match (target_commodity_id, target_symbol) {
(Some(tid), Some(sym)) => {
fetch_date_range_splits_filtered_with_conversion(conn, tid, sym, from, to, filter).await
_ => fetch_date_range_splits_filtered_no_conversion(conn, from, to, filter).await,
struct RunCtx<'a> {
accounts: &'a [super::tree::AccountRow],
user_filter: Option<&'a ReportFilter>,
target_symbol: Option<&'a str>,
async fn run_group_for_period(
ctx: &RunCtx<'_>,
group: &ActivityGroup,
) -> Result<ActivityGroupResult, CmdError> {
let filter = compose_group_filter(group, ctx.user_filter);
let amounts = fetch_range(
conn,
ctx.target_commodity_id,
ctx.target_symbol,
from,
to,
&filter,
)
.await?;
let roots = build_tree(ctx.accounts, &amounts);
Ok(ActivityGroupResult {
label: group.label.clone(),
flip_sign: group.flip_sign,
roots,
})
command! {
ActivityReport {
#[required]
user_id: Uuid,
date_from: DateTime<Utc>,
date_to: DateTime<Utc>,
#[optional]
target_commodity_id: Uuid,
period_grouping: PeriodGrouping,
groups: Vec<ActivityGroup>,
report_filter: ReportFilter,
} => {
let user = User { id: user_id };
let mut conn = user.get_connection().await.map_err(|err| {
log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
ConfigError::DB
})?;
let accounts = fetch_accounts(&mut conn).await?;
let groups = groups.unwrap_or_else(default_activity_groups);
let target_symbol = match target_commodity_id {
Some(tid) => Some(fetch_target_symbol(&mut conn, tid).await?),
None => None,
let period_boundaries = period_grouping
.map(|g| boundaries_for(g, date_from, date_to))
.unwrap_or_default();
let ctx = RunCtx {
accounts: &accounts,
user_filter: report_filter.as_ref(),
target_commodity_id,
target_symbol: target_symbol.as_deref(),
let periods = if period_boundaries.is_empty() {
let mut group_results = Vec::with_capacity(groups.len());
for group in &groups {
group_results.push(
run_group_for_period(&mut conn, &ctx, group, date_from, date_to).await?,
);
vec![ActivityPeriod {
label: None,
groups: group_results,
}]
} else {
let mut out = Vec::with_capacity(period_boundaries.len());
for (label, pfrom, pto) in &period_boundaries {
group_results
.push(run_group_for_period(&mut conn, &ctx, group, *pfrom, *pto).await?);
out.push(ActivityPeriod {
label: Some(label.clone()),
});
out
Ok(Some(CmdResult::Activity(ActivityData {
meta: ReportMeta {
date_from: Some(date_from),
date_to: Some(date_to),
periods,
})))