use crate::kernel::report_item_selector::BalanceSelector;
use crate::kernel::Settings;
use crate::model::balance_tree_node::ord_by_btn;
use crate::model::{BalanceTreeNode, Commodity, TxnAccount, TxnRefs, TxnSet};
use itertools::Itertools;
use rust_decimal::Decimal;
use std::collections::{HashMap, HashSet};
use std::error::Error;
pub type Deltas = HashMap<Option<Commodity>, Decimal>;
pub type BTNs = Vec<BalanceTreeNode>;
#[derive(Debug)]
pub struct Balance {
pub(crate) title: String,
pub(crate) bal: BTNs,
pub(crate) deltas: Deltas,
}
impl Balance {
pub(crate) fn is_empty(&self) -> bool {
self.bal.is_empty()
}
}
impl Balance {
fn get_balance_tree_nodes(
me: &(TxnAccount, Decimal),
acc_sums: &[(TxnAccount, Decimal)],
) -> Vec<BalanceTreeNode> {
let (my_acctn, my_sum) = me;
let childs = acc_sums
.iter()
.filter(|(atn, _)| my_acctn.is_parent_of(atn));
let childs_balance_trees = childs
.flat_map(|c| Balance::get_balance_tree_nodes(c, acc_sums))
.collect::<Vec<BalanceTreeNode>>();
let my_childs_sum = childs_balance_trees
.iter()
.filter(|btn| btn.acctn.atn.parent == my_acctn.atn.account)
.map(|btn| btn.sub_acc_tree_sum)
.sum::<Decimal>();
let my_btn = BalanceTreeNode {
acctn: my_acctn.clone(),
sub_acc_tree_sum: my_childs_sum + my_sum,
account_sum: *my_sum,
};
let mut x = vec![my_btn];
x.extend(childs_balance_trees);
x
}
fn bubble_up_acctn(
my_acctn_sum: &(TxnAccount, Decimal),
acc_sums: &[(TxnAccount, Decimal)],
settings: &Settings,
) -> Result<Vec<(TxnAccount, Decimal)>, Box<dyn Error>> {
let my_acctn = &my_acctn_sum.0;
if my_acctn.is_root() {
Ok(vec![my_acctn_sum.clone()])
} else {
let parent = acc_sums
.iter()
.filter(|(atn, _)| atn.is_parent_of(my_acctn))
.collect::<Vec<&(TxnAccount, Decimal)>>();
assert!(parent.is_empty() || parent.len() == 1);
if parent.is_empty() {
if my_acctn.my_parent_is_root() {
let new_parent_atn = settings
.get_txn_account(my_acctn.atn.parent.as_str(), my_acctn.comm.clone())?;
Ok(vec![(new_parent_atn, Decimal::ZERO), my_acctn_sum.clone()])
} else {
let new_parent_atn = settings
.get_txn_account(my_acctn.atn.parent.as_str(), my_acctn.comm.clone())?;
let mut sub_tree = vec![my_acctn_sum.clone()];
let mut x = Balance::bubble_up_acctn(
&(new_parent_atn, Decimal::ZERO),
acc_sums,
settings,
)?;
x.append(&mut sub_tree);
Ok(x)
}
} else {
let mut sub_tree = vec![my_acctn_sum.clone()];
let mut x = Balance::bubble_up_acctn(parent[0], acc_sums, settings)?;
x.append(&mut sub_tree);
Ok(x)
}
}
}
fn balance(
txns: &TxnRefs,
settings: &Settings,
) -> Result<Vec<BalanceTreeNode>, Box<dyn Error>> {
let account_sums: Vec<_> = txns
.iter()
.flat_map(|txn| &txn.posts)
.sorted_by_key(|p| &p.acctn)
.chunk_by(|p| &p.acctn)
.into_iter()
.map(|(_, postings)| {
let mut ps = postings.peekable();
let acctn = ps.peek().unwrap().acctn.clone();
let acc_sum = ps.map(|p| p.amount).sum::<Decimal>();
(acctn, acc_sum)
})
.collect();
let complete_coa_sum_tree: &Vec<(TxnAccount, Decimal)> = &account_sums
.iter()
.try_fold(
Vec::new(),
|mut trees: Vec<Vec<(TxnAccount, Decimal)>>, acc| {
let bua = Balance::bubble_up_acctn(acc, &account_sums, settings)?;
trees.push(bua);
Ok::<Vec<Vec<(TxnAccount, Decimal)>>, Box<dyn Error>>(trees)
},
)?
.iter()
.flatten()
.cloned()
.collect::<HashSet<_>>() .into_iter()
.collect::<Vec<(TxnAccount, Decimal)>>();
let roots = complete_coa_sum_tree
.iter()
.filter(|(acctn, _)| acctn.atn.depth == 1);
let mut bal = roots
.flat_map(|root_acc_sum| {
Balance::get_balance_tree_nodes(root_acc_sum, complete_coa_sum_tree)
})
.collect::<Vec<BalanceTreeNode>>();
bal.sort_by(ord_by_btn);
Ok(bal)
}
pub fn from<T>(
title: &str,
txn_set: &TxnSet,
accounts: &T,
settings: &Settings,
) -> Result<Balance, Box<dyn Error>>
where
T: BalanceSelector + ?Sized,
{
let bal = Balance::balance(&txn_set.txns, settings)?;
let filt_bal: Vec<_> = bal.iter().filter(|b| accounts.eval(b)).cloned().collect();
if filt_bal.is_empty() {
Ok(Balance {
title: title.to_string(),
bal: Default::default(),
deltas: Default::default(),
})
} else {
let deltas = filt_bal
.iter()
.chunk_by(|btn| &btn.acctn.comm.name)
.into_iter()
.map(|(c, bs)| {
let dsum = bs.map(|b| b.account_sum).sum();
if c.is_empty() {
(None, dsum)
} else {
(Some(Commodity { name: c.clone() }), dsum)
}
})
.collect();
Ok(Balance {
title: title.to_string(),
bal: filt_bal,
deltas,
})
}
}
}