Skip to main content

tackler_core/export/
equity_exporter.rs

1/*
2 * Tackler-NG 2024-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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            // todo: log warning that equity transaction is empty and ask to check account selector
83            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                /*
144                 * equity transaction per commodity
145                 */
146                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}