tackler_core/export/
equity_exporter.rs1use crate::export::Export;
7use crate::kernel::Settings;
8use crate::kernel::balance::Balance;
9use crate::kernel::report_item_selector::{
10 BalanceNonZeroByAccountSelector, BalanceNonZeroSelector, BalanceSelector,
11};
12use crate::model::{Transaction, TxnSet};
13use crate::tackler;
14use itertools::Itertools;
15use rust_decimal::Decimal;
16use std::io;
17use tackler_api::metadata::items::{
18 AccountSelectorChecksum, CreditAccountReport, MetadataItem, Text,
19};
20use tackler_api::txn_ts::rfc_3339;
21
22#[derive(Debug, Clone)]
23pub struct EquitySettings {
24 pub eqa: Option<String>,
25 pub ras: Vec<String>,
26}
27
28impl EquitySettings {
29 #[must_use]
30 pub fn from(settings: &Settings) -> EquitySettings {
31 EquitySettings {
32 eqa: Some(settings.export.equity.equity_account.clone()),
33 ras: settings.get_equity_ras(),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct EquityExporter {
40 pub export_settings: EquitySettings,
41}
42
43impl EquityExporter {
44 fn get_acc_selector(&self) -> Result<Box<dyn BalanceSelector>, tackler::Error> {
45 let v = &self.export_settings.ras;
46 if v.is_empty() {
47 Ok(Box::new(BalanceNonZeroSelector {}))
48 } else {
49 let s: Vec<_> = v.iter().map(String::as_str).collect();
50 let ras = BalanceNonZeroByAccountSelector::try_from(&s)?;
51
52 Ok(Box::new(ras))
53 }
54 }
55}
56
57impl Export for EquityExporter {
58 #[allow(clippy::too_many_lines)]
59 fn write_export<W: io::Write + ?Sized>(
60 &self,
61 cfg: &Settings,
62 writer: &mut W,
63 txn_data: &TxnSet<'_>,
64 ) -> Result<(), tackler::Error> {
65 let bal_acc_sel = self.get_acc_selector()?;
66
67 let price_lookup_ctx = cfg.get_price_lookup().make_ctx(
68 &txn_data.txns,
69 cfg.report.commodity.clone(),
70 &cfg.price.price_db,
71 );
72
73 let bal = Balance::from(
74 &String::default(),
75 txn_data,
76 &price_lookup_ctx,
77 bal_acc_sel.as_ref(),
78 cfg,
79 )?;
80
81 if bal.is_empty() {
82 return Ok(());
84 }
85
86 let eq_txn_indent = " ";
87 let equity_account = "Equity:Default·Account".to_string();
88
89 let txn_uuid_str = |last_txn: &&Transaction| -> String {
90 if let Some(uuid) = last_txn.header.uuid {
91 format!("; Last txn (uuid) : {uuid}")
92 } else {
93 String::default()
94 }
95 };
96 let hdr_str = |last_txn: &&Transaction, c: &String| -> String {
97 let comm_str = || -> String {
98 if c.is_empty() {
99 String::default()
100 } else {
101 format!(" for {c}")
102 }
103 };
104 format!(
105 "{} 'Equity txn{}",
106 rfc_3339(&last_txn.header.timestamp),
107 comm_str()
108 )
109 };
110
111 let Some(last_txn) = txn_data.txns.last() else {
112 let msg = "Equity: Internal logic error: last txn";
113 return Err(msg.into());
114 };
115
116 let acc_sel_checksum = cfg.get_hash().map(|hash| AccountSelectorChecksum {
117 hash: bal_acc_sel.checksum(hash),
118 selectors: bal_acc_sel.selectors(),
119 });
120
121 let report_tz_mdi = MetadataItem::TimeZoneInfo(crate::report::report_timezone(cfg)?);
122
123 let equity_txn_str: Vec<String> = bal
124 .bal
125 .iter()
126 .chunk_by(|btn| &btn.acctn.comm.name)
127 .into_iter()
128 .flat_map(|(c, bs)| {
129 let btns: Vec<_> = bs.collect();
130 let dsum: Decimal = btns.clone().into_iter().map(|b| b.account_sum).sum::<Decimal>();
131 let bal_posting = {
132 let value = if c.is_empty() {
133 format!("{}", -dsum)
134 } else {
135 format!("{} {}", -dsum, c)
136 };
137 let ea = match &self.export_settings.eqa {
138 Some(eqa) => eqa,
139 None => &equity_account,
140 };
141 format!("{eq_txn_indent}{ea} {value}")
142 };
143 let eq_postings = btns
147 .into_iter()
148 .map(|b| {
149 let comm = &b.acctn.comm;
150 format!(
151 "{}{} {}{}",
152 eq_txn_indent,
153 b.acctn.atn.account,
154 b.account_sum,
155 if comm.is_any() { format!(" {}", comm.name) } else { String::new() }
156 )
157 })
158 .collect::<Vec<String>>();
159
160 let mut eq_txn = Vec::<String>::new();
161
162 eq_txn.push(hdr_str(last_txn, c));
163 let uuid_str = txn_uuid_str(last_txn);
164 if !uuid_str.is_empty() {
165 eq_txn.push(format!("{eq_txn_indent}{uuid_str}"));
166 eq_txn.push(format!("{eq_txn_indent};"));
167 }
168 if let Some(md) = &txn_data.metadata {
169 for mdi in md.items.clone() {
170 eq_txn.extend(mdi.text(cfg.report.tz.clone()).iter().map(|v| {
171 format!("{eq_txn_indent}; {v}")
172 }).collect::<Vec<_>>());
173 eq_txn.push(format!("{eq_txn_indent}; "));
174 }
175
176 if let Some(asc) = &acc_sel_checksum {
177 for v in asc.text(cfg.report.tz.clone()) {
178 eq_txn.push(format!("{eq_txn_indent}; {v}"));
179 }
180 eq_txn.push(format!("{eq_txn_indent}; "));
181 }
182 }
183
184 if !price_lookup_ctx.is_empty() {
185 for v in report_tz_mdi.text(cfg.report.tz.clone()) {
186 eq_txn.push(format!("{eq_txn_indent}; {v}"));
187 }
188 eq_txn.push(format!("{eq_txn_indent}; "));
189
190 let pr = MetadataItem::PriceRecords(price_lookup_ctx.metadata());
191 for v in pr.text(cfg.report.tz.clone()) {
192 eq_txn.push(format!("{eq_txn_indent}; {v}"));
193 }
194 eq_txn.push(format!("{eq_txn_indent}; "));
195 }
196
197 if cfg.inverted {
198 let credit = MetadataItem::CreditAccountReport(CreditAccountReport { });
199 for v in credit.text(cfg.report.tz.clone()) {
200 eq_txn.push(format!("{eq_txn_indent}; {v}"));
201 }
202 eq_txn.push(format!("{eq_txn_indent}; "));
203 }
204
205 if dsum.is_zero() {
206 eq_txn.push(format!("{eq_txn_indent}; WARNING:"));
207 eq_txn.push(format!("{eq_txn_indent}; WARNING: The sum of equity transaction is zero without equity account."));
208 eq_txn.push(format!("{eq_txn_indent}; WARNING: Therefore there is no equity posting row, and this is probably not right."));
209 eq_txn.push(format!("{eq_txn_indent}; WARNING: Is the account selector correct for this Equity export?"));
210 eq_txn.push(format!("{eq_txn_indent}; WARNING:"));
211 }
212
213 eq_txn.extend(eq_postings);
214 if !dsum.is_zero() {
215 eq_txn.push(bal_posting);
216 }
217 eq_txn.push(String::new());
218 eq_txn
219 })
220 .collect();
221
222 for i in equity_txn_str {
223 writeln!(writer, "{i}")?;
224 }
225
226 Ok(())
227 }
228}