1use crate::config::BalanceType;
7use crate::kernel::Settings;
8use crate::kernel::price_lookup::PriceLookupCtx;
9use crate::kernel::report_item_selector::BalanceSelector;
10use crate::model::balance_tree_node::ord_by_btn;
11use crate::model::{BalanceTreeNode, Commodity, Transaction, TxnAccount, TxnSet};
12use crate::tackler;
13use itertools::Itertools;
14use rust_decimal::Decimal;
15use std::collections::BTreeMap;
16use std::{collections::HashSet, sync::Arc};
17
18pub type Deltas = BTreeMap<Option<Arc<Commodity>>, Decimal>;
20pub type BTNs = Vec<BalanceTreeNode>;
21#[derive(Debug)]
22pub struct Balance {
23 pub(crate) title: String,
24 pub(crate) bal: BTNs,
25 pub(crate) deltas: Deltas,
26}
27
28impl Balance {
29 pub(crate) fn is_empty(&self) -> bool {
30 self.bal.is_empty()
31 }
32}
33
34impl Balance {
35 fn get_balance_tree_nodes(
45 me: &(TxnAccount, Decimal),
46 acc_sums: &[(TxnAccount, Decimal)],
47 ) -> Vec<BalanceTreeNode> {
48 let (my_acctn, my_sum) = me;
49
50 let childs = acc_sums
52 .iter()
53 .filter(|(atn, _)| my_acctn.is_parent_of(atn));
54
55 let childs_balance_trees = childs
57 .flat_map(|c| Balance::get_balance_tree_nodes(c, acc_sums))
58 .collect::<Vec<BalanceTreeNode>>();
59
60 let my_childs_sum = childs_balance_trees
62 .iter()
63 .filter(|btn| btn.acctn.atn.parent == my_acctn.atn.account)
64 .map(|btn| btn.sub_acc_tree_sum)
65 .sum::<Decimal>();
66
67 let my_btn = BalanceTreeNode {
68 acctn: my_acctn.clone(),
69 sub_acc_tree_sum: my_childs_sum + my_sum,
70 account_sum: *my_sum,
71 };
72
73 let mut x = vec![my_btn];
74 x.extend(childs_balance_trees);
75 x
76 }
77
78 fn bubble_up_acctn(
91 my_acctn_sum: &(TxnAccount, Decimal),
92 acc_sums: &[(TxnAccount, Decimal)],
93 settings: &Settings,
94 ) -> Result<Vec<(TxnAccount, Decimal)>, tackler::Error> {
95 let my_acctn = &my_acctn_sum.0;
96 if my_acctn.is_root() {
97 Ok(vec![my_acctn_sum.clone()])
101 } else {
102 let parent = acc_sums
104 .iter()
105 .filter(|(atn, _)| atn.is_parent_of(my_acctn))
106 .collect::<Vec<&(TxnAccount, Decimal)>>();
107
108 assert!(parent.is_empty() || parent.len() == 1);
109
110 if parent.is_empty() {
111 if my_acctn.my_parent_is_root() {
112 let new_parent_atn =
116 settings.get_txn_account(my_acctn.atn.parent.as_str(), &my_acctn.comm)?;
117 Ok(vec![(new_parent_atn, Decimal::ZERO), my_acctn_sum.clone()])
118 } else {
119 let new_parent_atn =
120 settings.get_txn_account(my_acctn.atn.parent.as_str(), &my_acctn.comm)?;
121 let mut sub_tree = vec![my_acctn_sum.clone()];
122 let mut x = Balance::bubble_up_acctn(
123 &(new_parent_atn, Decimal::ZERO),
124 acc_sums,
125 settings,
126 )?;
127 x.append(&mut sub_tree);
128 Ok(x)
129 }
130 } else {
131 let mut sub_tree = vec![my_acctn_sum.clone()];
133 let mut x = Balance::bubble_up_acctn(parent[0], acc_sums, settings)?;
134 x.append(&mut sub_tree);
135 Ok(x)
136 }
137 }
138 }
139
140 fn calculate_account_sums<'a, I>(
145 txns: I,
146 price_lookup_ctx: &PriceLookupCtx<'_>,
147 inverted: bool,
148 ) -> Vec<(TxnAccount, Decimal)>
149 where
150 I: Iterator<Item = &'a &'a Transaction>,
151 {
152 let inv = Decimal::from(-1);
153
154 txns.flat_map(|txn| price_lookup_ctx.convert_prices(txn))
159 .sorted_by_key(|(acctn, _, _)| acctn.clone())
160 .chunk_by(|(acctn, _, _)| acctn.clone())
161 .into_iter()
162 .map(|(_, postings)| {
163 let mut ps = postings.peekable();
164 let acctn = ps.peek().unwrap().0.clone();
166 let acc_sum = ps.map(|(_, amount, _)| amount).sum::<Decimal>();
167 if inverted {
168 (acctn, acc_sum * inv)
169 } else {
170 (acctn, acc_sum)
171 }
172 })
173 .collect()
174 }
175
176 fn balance_tree<'a, I>(
184 txns: I,
185 price_lookup_ctx: &PriceLookupCtx<'_>,
186 settings: &Settings,
187 ) -> Result<Vec<BalanceTreeNode>, tackler::Error>
188 where
189 I: Iterator<Item = &'a &'a Transaction>,
190 {
191 let account_sums: Vec<(TxnAccount, Decimal)> =
196 Self::calculate_account_sums(txns, price_lookup_ctx, settings.inverted);
197
198 let complete_coa_sum_tree: &Vec<(TxnAccount, Decimal)> = &account_sums
212 .iter()
213 .try_fold(
214 Vec::new(),
215 |mut trees: Vec<Vec<(TxnAccount, Decimal)>>, acc| {
216 let bua = Balance::bubble_up_acctn(acc, &account_sums, settings)?;
217 trees.push(bua);
218 Ok::<Vec<Vec<(TxnAccount, Decimal)>>, tackler::Error>(trees)
219 },
220 )?
221 .into_iter()
222 .flatten()
223 .collect::<HashSet<_>>() .into_iter()
225 .collect::<Vec<(TxnAccount, Decimal)>>();
226
227 let roots = complete_coa_sum_tree
231 .iter()
232 .filter(|(acctn, _)| acctn.atn.depth == 1);
233
234 let mut bal = roots
238 .flat_map(|root_acc_sum| {
239 Balance::get_balance_tree_nodes(root_acc_sum, complete_coa_sum_tree)
240 })
241 .collect::<Vec<BalanceTreeNode>>();
242
243 bal.sort_by(ord_by_btn);
244 Ok(bal)
245 }
246
247 pub fn from<T>(
252 title: &str,
253 txn_set: &TxnSet<'_>,
254 price_lookup_ctx: &PriceLookupCtx<'_>,
255 accounts: &T,
256 settings: &Settings,
257 ) -> Result<Balance, tackler::Error>
258 where
259 T: BalanceSelector + ?Sized,
260 {
261 Self::from_iter(
262 title,
263 &txn_set.txns,
264 price_lookup_ctx,
265 accounts,
266 settings,
267 settings.report.balance.bal_type.clone(),
268 )
269 }
270
271 #[allow(clippy::needless_pass_by_value)]
272 pub(crate) fn from_iter<'a, I, T>(
273 title: &str,
274 txns: I,
275 price_lookup_ctx: &PriceLookupCtx<'_>,
276 accounts: &T,
277 settings: &Settings,
278 bal_type: BalanceType,
279 ) -> Result<Balance, tackler::Error>
280 where
281 T: BalanceSelector + ?Sized,
282 I: IntoIterator<Item = &'a &'a Transaction>,
283 {
284 let bal = match bal_type {
285 BalanceType::Tree => {
286 Balance::balance_tree(txns.into_iter(), price_lookup_ctx, settings)?
287 }
288 BalanceType::Flat => {
289 Balance::balance_flat(txns.into_iter(), price_lookup_ctx, settings)
290 }
291 };
292
293 let filt_bal: Vec<_> = bal.into_iter().filter(|b| accounts.eval(b)).collect();
294
295 if filt_bal.is_empty() {
296 Ok(Balance {
297 title: title.to_string(),
298 bal: Vec::default(),
299 deltas: BTreeMap::default(),
300 })
301 } else {
302 let deltas = filt_bal
303 .iter()
304 .chunk_by(|btn| btn.acctn.comm.clone())
305 .into_iter()
306 .map(|(c, bs)| {
307 let dsum = bs.map(|b| b.account_sum).sum();
308 (c.is_any().then_some(c), dsum)
309 })
310 .collect();
311
312 Ok(Balance {
313 title: title.to_string(),
314 bal: filt_bal,
315 deltas,
316 })
317 }
318 }
319
320 fn balance_flat<'a, I>(
321 txns: I,
322 price_lookup_ctx: &PriceLookupCtx<'_>,
323 settings: &Settings,
324 ) -> Vec<BalanceTreeNode>
325 where
326 I: Iterator<Item = &'a &'a Transaction>,
327 {
328 let account_sums: Vec<(TxnAccount, Decimal)> =
329 Self::calculate_account_sums(txns, price_lookup_ctx, settings.inverted);
330
331 let mut v: Vec<BalanceTreeNode> = account_sums
332 .into_iter()
333 .map(|(acctn, acc_sum)| BalanceTreeNode {
334 acctn: acctn.clone(),
335 sub_acc_tree_sum: Decimal::ZERO,
336 account_sum: acc_sum,
337 })
338 .collect();
339
340 v.sort_by(ord_by_btn);
341 v
342 }
343}