tackler_core/report/
register_reporter.rs1use crate::kernel::Settings;
7use crate::kernel::accumulator;
8use crate::kernel::report_item_selector::{
9 RegisterAllSelector, RegisterByAccountSelector, RegisterSelector,
10};
11use crate::kernel::report_settings::RegisterSettings;
12use crate::math::format::format_with_scale;
13use crate::model::{RegisterEntry, TxnSet};
14use crate::report::{FormatWriter, Report, report_timezone};
15use crate::tackler;
16use crate::tackler::Error;
17use jiff::Zoned;
18use jiff::tz::TimeZone;
19use rust_decimal::Decimal;
20use std::io;
21use std::io::Write;
22use tackler_api::metadata::Metadata;
23use tackler_api::metadata::items::{CreditAccountReport, MetadataItem};
24use tackler_api::reports::register_report::{RegisterPosting, RegisterReport, RegisterTxn};
25use tackler_api::txn_ts;
26use tackler_api::txn_ts::TimestampStyle;
27
28#[derive(Debug, Clone)]
29pub struct RegisterReporter {
30 pub report_settings: RegisterSettings,
31}
32
33impl RegisterReporter {
34 fn get_acc_selector(&self) -> Result<Box<dyn RegisterSelector<'_>>, tackler::Error> {
35 let ras = &self.report_settings.ras;
36 if ras.is_empty() {
37 Ok(Box::<RegisterAllSelector>::default())
38 } else {
39 let s: Vec<_> = ras.iter().map(String::as_str).collect();
40 let ras = RegisterByAccountSelector::try_from(&s)?;
41
42 Ok(Box::new(ras))
43 }
44 }
45}
46
47fn reg_entry_txt_writer<W: io::Write + ?Sized>(
48 f: &mut W,
49 re: &RegisterEntry<'_>,
50 register_settings: &RegisterSettings,
51) -> Result<(), tackler::Error> {
52 let ts_style = register_settings.timestamp_style;
53 let report_tz = register_settings.report_tz.clone();
54
55 let fmt: fn(&Zoned, TimeZone) -> String = match ts_style {
56 TimestampStyle::Date => txn_ts::as_tz_date,
57 TimestampStyle::Secodns => txn_ts::as_tz_seconds,
58 TimestampStyle::Full => txn_ts::as_tz_full,
59 };
60
61 if !re.posts.is_empty() {
62 write!(f, "{}", re.fmt_with_cfg(fmt, report_tz, register_settings))?;
63 }
64 Ok(())
65}
66
67fn register_entry_to_api(
68 re: &RegisterEntry<'_>,
69 register_settings: &RegisterSettings,
70) -> Option<RegisterTxn> {
71 if re.posts.is_empty() {
72 return None;
73 }
74
75 let inverter = Decimal::from(-1);
76
77 let scale = ®ister_settings.scale;
78 let ts_style = register_settings.timestamp_style;
79 let report_tz = register_settings.report_tz.clone();
80
81 let fmt: fn(&Zoned, TimeZone) -> String = match ts_style {
82 TimestampStyle::Date => txn_ts::as_tz_date,
83 TimestampStyle::Secodns => txn_ts::as_tz_seconds,
84 TimestampStyle::Full => txn_ts::as_tz_full,
85 };
86
87 let txn = re.txn;
88
89 let r = re
90 .posts
91 .iter()
92 .map(|p| {
93 let commodity = if p.target_commodity.is_any() {
94 Some(p.target_commodity.name.clone())
95 } else {
96 None
97 };
98 let base_commodity = if p.is_commodity_conv() {
99 Some(p.post.acctn.comm.name.clone())
100 } else {
101 None
102 };
103
104 let (a, rt) = if register_settings.inverted {
105 (&(p.post.amount * inverter), &(p.amount * inverter))
106 } else {
107 (&p.post.amount, &p.amount)
108 };
109
110 RegisterPosting {
111 account: p.post.acctn.atn.account.clone(),
112 amount: format_with_scale(0, a, scale),
113 running_total: format_with_scale(0, rt, scale),
114 comment: p.post.comment.clone(),
115 commodity,
116 rate: p.rate.map(|r| format_with_scale(0, &r, scale)),
117 base_commodity,
118 }
119 })
120 .collect();
121
122 Some(RegisterTxn {
123 display_time: fmt(&txn.header.timestamp, report_tz),
124 txn: txn.header.clone(),
125 postings: r,
126 })
127}
128
129impl Report for RegisterReporter {
130 fn write_reports<W: Write + ?Sized>(
131 &self,
132 cfg: &Settings,
133 writers: &mut Vec<FormatWriter<'_>>,
134 metadata: Option<&Metadata>,
135 txn_data: &TxnSet<'_>,
136 ) -> Result<(), Error> {
137 assert_eq!(self.report_settings.inverted, cfg.inverted);
138
139 let acc_sel = self.get_acc_selector()?;
140
141 let report_commodity = self.report_settings.report_commodity.clone();
142 let price_lookup_ctx = self.report_settings.price_lookup.make_ctx(
143 &txn_data.txns,
144 report_commodity,
145 &cfg.price.price_db,
146 );
147
148 let mut metadata = match metadata {
149 Some(md) => md.clone(),
150 None => Metadata::default(),
151 };
152
153 if let Some(hash) = cfg.get_hash() {
154 let asc = acc_sel.account_selector_metadata(hash);
155 metadata.push(asc);
156 }
157
158 let rtz = MetadataItem::TimeZoneInfo(report_timezone(cfg)?);
159 metadata.push(rtz);
160
161 if !price_lookup_ctx.is_empty() {
162 let pr = MetadataItem::PriceRecords(price_lookup_ctx.metadata());
163 metadata.push(pr);
164 }
165
166 if self.report_settings.inverted {
167 let credit = MetadataItem::CreditAccountReport(CreditAccountReport {});
168 metadata.push(credit);
169 }
170
171 let ras = self.get_acc_selector()?;
172
173 let register =
174 accumulator::register_engine(&txn_data.txns, &price_lookup_ctx, ras.as_ref());
175
176 for w in writers {
177 match w {
178 FormatWriter::TxtFormat(writer) => {
179 writeln!(writer, "{}\n", metadata.text(cfg.report.tz.clone()))?;
181
182 let title = &self.report_settings.title;
183 writeln!(writer, "{title}")?;
184 writeln!(writer, "{}", "-".repeat(title.chars().count()))?;
185
186 for re in ®ister {
187 reg_entry_txt_writer(writer, re, &self.report_settings)?;
188 }
189 }
190 FormatWriter::JsonFormat(writer) => {
191 let transactions = register
192 .iter()
193 .filter_map(|re| register_entry_to_api(re, &self.report_settings))
194 .collect();
195
196 let rr = RegisterReport {
197 metadata: Some(metadata.clone()), title: self.report_settings.title.clone(),
199 transactions,
200 };
201 serde_json::to_writer_pretty(&mut *writer, &rr)?;
202 writeln!(writer)?;
203 }
204 }
205 }
206 Ok(())
207 }
208}