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::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(|s| s.as_str()).collect();
47 let ras = BalanceByAccountSelector::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 pub(crate) fn txt_report<W: io::Write + ?Sized>(
59 writer: &mut W,
60 bal_report: &Balance,
61 bal_settings: &BalanceSettings,
62 ) -> Result<(), tackler::Error> {
63 let get_max_sum_len = |bal: &BTNs, f: fn(&BalanceTreeNode) -> Decimal| -> usize {
64 bal.iter()
65 .map(|btn| {
66 let d = f(btn);
67 format!("{:+.prec$}", d, prec = bal_settings.scale.get_precision(&d))
69 .chars()
70 .count()
71 })
72 .fold(0, max)
73 };
74 fn get_max_delta_len(deltas: &Deltas) -> usize {
75 deltas
76 .iter()
77 .map(|(_, d)| format!("{}", d).chars().count())
78 .fold(0, max)
79 }
80 fn get_max_commodity_len(deltas: &Deltas) -> usize {
83 deltas
84 .iter()
85 .map(|(opt_comm, _)| {
86 opt_comm
87 .as_ref()
88 .map_or(0, |comm| comm.name.chars().count())
89 })
90 .fold(0, max)
91 }
92
93 let delta_max_len = get_max_delta_len(&bal_report.deltas);
94 let comm_max_len = get_max_commodity_len(&bal_report.deltas);
95
96 let left_sum_len = max(
98 12,
99 max(
100 get_max_sum_len(&bal_report.bal, |btn| btn.account_sum),
101 delta_max_len,
102 ),
103 );
104
105 let sub_acc_tree_sum_len = get_max_sum_len(&bal_report.bal, |btn| btn.sub_acc_tree_sum);
106
107 let filler_field = if comm_max_len.is_zero() {
111 " ".repeat(3)
112 } else {
113 " ".repeat(4 + comm_max_len)
114 };
115
116 fn make_commodity_field(
117 comm_max_len: usize,
118 btn: &BalanceTreeNode,
119 bal_settings: &BalanceSettings,
120 ) -> String {
121 if comm_max_len.is_zero() {
122 match bal_settings.bal_type {
124 BalanceType::Tree => {
125 " ".repeat(2)
127 }
128 BalanceType::Flat => {
129 String::default()
131 }
132 }
133 } else {
134 let comm = &btn.acctn.comm;
135 match &comm.is_any() {
136 true => {
137 match bal_settings.bal_type {
138 BalanceType::Tree => {
139 format!(" {: <cl$} ", comm.name, cl = comm_max_len)
140 }
141 BalanceType::Flat => {
142 format!("{: <cl$} ", comm.name, cl = comm_max_len)
144 }
145 }
146 }
147 false => {
148 match bal_settings.bal_type {
149 BalanceType::Tree => {
150 format!(" {} ", " ".repeat(comm_max_len))
151 }
152 BalanceType::Flat => {
153 format!("{} ", " ".repeat(comm_max_len))
155 }
156 }
157 }
158 }
159 }
160 }
161
162 let left_ruler = " ".repeat(9);
163
164 writeln!(writer, "{}", bal_report.title)?;
165 writeln!(writer, "{}", "-".repeat(bal_report.title.chars().count()))?;
166
167 if !bal_report.is_empty() {
168 for btn in &bal_report.bal {
169 let acc_sum =
170 format_with_scale(left_sum_len, &btn.account_sum, &bal_settings.scale);
171 let comm = make_commodity_field(comm_max_len, btn, bal_settings);
172 let atn = &btn.acctn.atn;
173
174 match bal_settings.bal_type {
175 BalanceType::Tree => {
176 writeln!(
177 writer,
178 "{left_ruler}{acc_sum}{filler_field}{acc_tree_sum}{comm}{atn}",
179 acc_tree_sum = format_with_scale(
180 sub_acc_tree_sum_len,
181 &btn.sub_acc_tree_sum,
182 &bal_settings.scale
183 )
184 )?;
185 }
186 BalanceType::Flat => {
187 writeln!(writer, "{left_ruler}{acc_sum}{filler_field}{comm}{atn}")?;
188 }
189 }
190 }
191
192 writeln!(
193 writer,
194 "{}",
195 "=".repeat(
196 left_ruler.chars().count()
197 + left_sum_len
198 + (if comm_max_len.is_zero() {
199 0
200 } else {
201 comm_max_len + 1
202 })
203 )
204 )?;
205
206 for delta in &bal_report.deltas {
207 writeln!(
208 writer,
209 "{left_ruler}{}{}",
210 format_with_scale(left_sum_len, delta.1, &bal_settings.scale),
211 delta
212 .0
213 .as_ref()
214 .map_or(String::default(), |c| format!(" {}", &c.name)),
215 )?;
216 }
217 }
218 Ok(())
219 }
220
221 fn btn_to_api(btn: &BalanceTreeNode, report_settings: &BalanceSettings) -> BalanceItem {
222 let acc_sum = match report_settings.bal_type {
223 BalanceType::Tree => Some(format_with_scale(
224 0,
225 &btn.sub_acc_tree_sum,
226 &report_settings.scale,
227 )),
228 BalanceType::Flat => None,
229 };
230 BalanceItem {
231 account_sum: format_with_scale(0, &btn.account_sum, &report_settings.scale),
232 account_tree_sum: acc_sum,
233 account: btn.acctn.atn.account.clone(),
234 commodity: if btn.acctn.comm.is_any() {
235 Some(btn.acctn.comm.name.clone())
236 } else {
237 None
238 },
239 }
240 }
241
242 pub fn balance_to_api(
243 metadata: Option<&Metadata>,
244 bal: &Balance,
245 report_settings: &BalanceSettings,
246 ) -> BalanceReport {
247 let balances = bal
248 .bal
249 .iter()
250 .map(|btn| Self::btn_to_api(btn, report_settings))
251 .collect::<Vec<BalanceItem>>();
252
253 let deltas = bal
254 .deltas
255 .iter()
256 .map(|(c, v)| Delta {
257 commodity: c.as_ref().map(|c| c.name.clone()),
258 delta: format_with_scale(0, v, &report_settings.scale),
259 })
260 .collect::<Vec<Delta>>();
261
262 BalanceReport {
263 metadata: metadata.cloned(),
264 title: bal.title.clone(),
265 balances,
266 deltas,
267 }
268 }
269}
270
271impl Report for BalanceReporter {
272 fn write_reports<W: Write + ?Sized>(
273 &self,
274 cfg: &Settings,
275 writers: &mut Vec<FormatWriter<'_>>,
276 metadata: Option<&Metadata>,
277 txn_data: &TxnSet<'_>,
278 ) -> Result<(), Error> {
279 let acc_sel = self.get_acc_selector()?;
280
281 let price_lookup_ctx = self.report_settings.price_lookup.make_ctx(
282 &txn_data.txns,
283 self.report_settings.report_commodity.clone(),
284 &cfg.price.price_db,
285 );
286 let bal_report = Balance::from(
287 &self.report_settings.title,
288 txn_data,
289 &price_lookup_ctx,
290 acc_sel.as_ref(),
291 cfg,
292 )?;
293
294 let mut metadata = match metadata {
295 Some(md) => md.clone(),
296 None => Metadata::default(),
297 };
298
299 if let Some(hash) = cfg.get_hash() {
300 let asc = acc_sel.account_selector_metadata(hash)?;
301 metadata.push(asc);
302 }
303
304 if !price_lookup_ctx.is_empty() {
305 let rtz = MetadataItem::TimeZoneInfo(report_timezone(cfg)?);
306 metadata.push(rtz);
307
308 let pr = MetadataItem::PriceRecords(price_lookup_ctx.metadata());
309 metadata.push(pr);
310 }
311
312 for w in writers {
313 match w {
314 FormatWriter::TxtFormat(writer) => {
315 if !metadata.is_empty() {
316 writeln!(writer, "{}\n", metadata.text(cfg.report.report_tz.clone()))?;
317 }
318
319 BalanceReporter::txt_report(writer, &bal_report, &self.report_settings)?
320 }
321 FormatWriter::JsonFormat(writer) => {
322 let md = if metadata.is_empty() {
323 None
324 } else {
325 Some(&metadata)
326 };
327 serde_json::to_writer_pretty(
328 &mut *writer,
329 &Self::balance_to_api(md, &bal_report, &self.report_settings),
330 )?;
331 writeln!(writer)?
332 }
333 }
334 }
335 Ok(())
336 }
337}