1use crate::config::BalanceType;
7use crate::kernel::balance::{BTNs, Balance, Deltas};
8use crate::kernel::report_item_selector::{
9 BalanceAllSelector, BalanceByAccountSelector, BalanceSelector,
10};
11use crate::kernel::{BalanceSettings, Settings};
12use crate::math::format::format_with_scale;
13use crate::model::{BalanceTreeNode, TxnSet};
14use crate::report::{FormatWriter, Report, report_timezone};
15use crate::tackler;
16use crate::tackler::Error;
17use rust_decimal::Decimal;
18use rust_decimal::prelude::Zero;
19use std::cmp::max;
20use std::io;
21use std::io::Write;
22use tackler_api::metadata::Metadata;
23use tackler_api::metadata::items::{CreditAccountReport, MetadataItem};
24use tackler_api::reports::balance_report::{BalanceItem, BalanceReport, Delta};
25
26#[derive(Debug, Clone)]
27pub struct BalanceReporter {
28 pub report_settings: BalanceSettings,
29}
30
31impl TryFrom<&Settings> for BalanceReporter {
32 type Error = tackler::Error;
33
34 fn try_from(settings: &Settings) -> Result<Self, Self::Error> {
35 Ok(BalanceReporter {
36 report_settings: BalanceSettings::try_from(settings)?,
37 })
38 }
39}
40
41impl BalanceReporter {
42 pub(crate) fn acc_selector(ras: &[String]) -> Result<Box<dyn BalanceSelector>, tackler::Error> {
43 if ras.is_empty() {
44 Ok(Box::<BalanceAllSelector>::default())
45 } else {
46 let s: Vec<_> = ras.iter().map(String::as_str).collect();
47 let ras = BalanceByAccountSelector::try_from(&s)?;
48 Ok(Box::new(ras))
49 }
50 }
51
52 fn get_acc_selector(&self) -> Result<Box<dyn BalanceSelector>, tackler::Error> {
53 BalanceReporter::acc_selector(&self.report_settings.ras)
54 }
55}
56
57impl BalanceReporter {
58 #[allow(clippy::too_many_lines)]
59 pub(crate) fn txt_report<W: io::Write + ?Sized>(
60 writer: &mut W,
61 bal_report: &Balance,
62 bal_settings: &BalanceSettings,
63 ) -> Result<(), tackler::Error> {
64 fn get_max_delta_len(deltas: &Deltas) -> usize {
65 deltas
66 .values()
67 .map(|d| format!("{d}").chars().count())
68 .fold(0, max)
69 }
70 fn get_max_commodity_len(deltas: &Deltas) -> usize {
73 deltas
74 .keys()
75 .map(|opt_comm| {
76 opt_comm
77 .as_ref()
78 .map_or(0, |comm| comm.name.chars().count())
79 })
80 .fold(0, max)
81 }
82
83 fn make_commodity_field(
84 comm_max_len: usize,
85 btn: &BalanceTreeNode,
86 bal_settings: &BalanceSettings,
87 ) -> String {
88 if comm_max_len.is_zero() {
89 match bal_settings.bal_type {
91 BalanceType::Tree => {
92 " ".repeat(2)
94 }
95 BalanceType::Flat => {
96 String::default()
98 }
99 }
100 } else {
101 let comm = &btn.acctn.comm;
102 match &comm.is_any() {
103 true => {
104 match bal_settings.bal_type {
105 BalanceType::Tree => {
106 format!(" {: <cl$} ", comm.name, cl = comm_max_len)
107 }
108 BalanceType::Flat => {
109 format!("{: <cl$} ", comm.name, cl = comm_max_len)
111 }
112 }
113 }
114 false => {
115 match bal_settings.bal_type {
116 BalanceType::Tree => {
117 format!(" {} ", " ".repeat(comm_max_len))
118 }
119 BalanceType::Flat => {
120 format!("{} ", " ".repeat(comm_max_len))
122 }
123 }
124 }
125 }
126 }
127 }
128
129 let get_max_sum_len = |bal: &BTNs, f: fn(&BalanceTreeNode) -> Decimal| -> usize {
130 bal.iter()
131 .map(|btn| {
132 let d = f(btn);
133 format!("{:+.prec$}", d, prec = bal_settings.scale.get_precision(&d))
135 .chars()
136 .count()
137 })
138 .fold(0, max)
139 };
140
141 let delta_max_len = get_max_delta_len(&bal_report.deltas);
142 let comm_max_len = get_max_commodity_len(&bal_report.deltas);
143
144 let left_sum_len = max(
146 12,
147 max(
148 get_max_sum_len(&bal_report.bal, |btn| btn.account_sum),
149 delta_max_len,
150 ),
151 );
152
153 let sub_acc_tree_sum_len = get_max_sum_len(&bal_report.bal, |btn| btn.sub_acc_tree_sum);
154
155 let filler_field = if comm_max_len.is_zero() {
159 " ".repeat(3)
160 } else {
161 " ".repeat(4 + comm_max_len)
162 };
163
164 let left_ruler = " ".repeat(9);
165
166 writeln!(writer, "{}", bal_report.title)?;
167 writeln!(writer, "{}", "-".repeat(bal_report.title.chars().count()))?;
168
169 if !bal_report.is_empty() {
170 for btn in &bal_report.bal {
171 let acc_sum =
172 format_with_scale(left_sum_len, &btn.account_sum, &bal_settings.scale);
173 let comm = make_commodity_field(comm_max_len, btn, bal_settings);
174 let atn = &btn.acctn.atn;
175
176 match bal_settings.bal_type {
177 BalanceType::Tree => {
178 writeln!(
179 writer,
180 "{left_ruler}{acc_sum}{filler_field}{acc_tree_sum}{comm}{atn}",
181 acc_tree_sum = format_with_scale(
182 sub_acc_tree_sum_len,
183 &btn.sub_acc_tree_sum,
184 &bal_settings.scale
185 )
186 )?;
187 }
188 BalanceType::Flat => {
189 writeln!(writer, "{left_ruler}{acc_sum}{filler_field}{comm}{atn}")?;
190 }
191 }
192 }
193
194 writeln!(
195 writer,
196 "{}",
197 "=".repeat(
198 left_ruler.chars().count()
199 + left_sum_len
200 + (if comm_max_len.is_zero() {
201 0
202 } else {
203 comm_max_len + 1
204 })
205 )
206 )?;
207
208 for delta in &bal_report.deltas {
209 writeln!(
210 writer,
211 "{left_ruler}{}{}",
212 format_with_scale(left_sum_len, delta.1, &bal_settings.scale),
213 delta
214 .0
215 .as_ref()
216 .map_or(String::default(), |c| format!(" {}", c.name)),
217 )?;
218 }
219 }
220 Ok(())
221 }
222
223 fn btn_to_api(btn: &BalanceTreeNode, report_settings: &BalanceSettings) -> BalanceItem {
224 let acc_sum = match report_settings.bal_type {
225 BalanceType::Tree => Some(format_with_scale(
226 0,
227 &btn.sub_acc_tree_sum,
228 &report_settings.scale,
229 )),
230 BalanceType::Flat => None,
231 };
232 BalanceItem {
233 account_sum: format_with_scale(0, &btn.account_sum, &report_settings.scale),
234 account_tree_sum: acc_sum,
235 account: btn.acctn.atn.account.clone(),
236 commodity: if btn.acctn.comm.is_any() {
237 Some(btn.acctn.comm.name.clone())
238 } else {
239 None
240 },
241 }
242 }
243
244 #[must_use]
245 pub fn balance_to_api(
246 metadata: Option<&Metadata>,
247 bal: &Balance,
248 report_settings: &BalanceSettings,
249 ) -> BalanceReport {
250 let balances = bal
251 .bal
252 .iter()
253 .map(|btn| Self::btn_to_api(btn, report_settings))
254 .collect::<Vec<BalanceItem>>();
255
256 let deltas = bal
257 .deltas
258 .iter()
259 .map(|(c, v)| Delta {
260 commodity: c.as_ref().map(|c| c.name.clone()),
261 delta: format_with_scale(0, v, &report_settings.scale),
262 })
263 .collect::<Vec<Delta>>();
264
265 BalanceReport {
266 metadata: metadata.cloned(),
267 title: bal.title.clone(),
268 balances,
269 deltas,
270 }
271 }
272}
273
274impl Report for BalanceReporter {
275 fn write_reports<W: Write + ?Sized>(
276 &self,
277 cfg: &Settings,
278 writers: &mut Vec<FormatWriter<'_>>,
279 metadata: Option<&Metadata>,
280 txn_data: &TxnSet<'_>,
281 ) -> Result<(), Error> {
282 assert_eq!(self.report_settings.inverted, cfg.inverted);
283
284 let acc_sel = self.get_acc_selector()?;
285
286 let price_lookup_ctx = self.report_settings.price_lookup.make_ctx(
287 &txn_data.txns,
288 self.report_settings.report_commodity.clone(),
289 &cfg.price.price_db,
290 );
291 let bal_report = Balance::from(
292 &self.report_settings.title,
293 txn_data,
294 &price_lookup_ctx,
295 acc_sel.as_ref(),
296 cfg,
297 )?;
298
299 let mut metadata = match metadata {
300 Some(md) => md.clone(),
301 None => Metadata::default(),
302 };
303
304 if let Some(hash) = cfg.get_hash() {
305 let asc = acc_sel.account_selector_metadata(hash);
306 metadata.push(asc);
307 }
308
309 if !price_lookup_ctx.is_empty() {
310 let rtz = MetadataItem::TimeZoneInfo(report_timezone(cfg)?);
311 metadata.push(rtz);
312
313 let pr = MetadataItem::PriceRecords(price_lookup_ctx.metadata());
314 metadata.push(pr);
315 }
316
317 if self.report_settings.inverted {
318 let credit = MetadataItem::CreditAccountReport(CreditAccountReport {});
319 metadata.push(credit);
320 }
321
322 for w in writers {
323 match w {
324 FormatWriter::TxtFormat(writer) => {
325 if !metadata.is_empty() {
326 writeln!(writer, "{}\n", metadata.text(cfg.report.tz.clone()))?;
327 }
328
329 BalanceReporter::txt_report(writer, &bal_report, &self.report_settings)?;
330 }
331 FormatWriter::JsonFormat(writer) => {
332 let md = if metadata.is_empty() {
333 None
334 } else {
335 Some(&metadata)
336 };
337 serde_json::to_writer_pretty(
338 &mut *writer,
339 &Self::balance_to_api(md, &bal_report, &self.report_settings),
340 )?;
341 writeln!(writer)?;
342 }
343 }
344 }
345 Ok(())
346 }
347}