dinero/
models.rs

1use num::rational::BigRational;
2use std::{
3    collections::{HashMap, HashSet},
4    convert::TryFrom,
5    path::PathBuf,
6};
7
8pub use account::Account;
9pub use balance::Balance;
10pub use comment::Comment;
11pub use currency::{Currency, CurrencyDisplayFormat, DigitGrouping};
12pub use money::Money;
13pub use payee::Payee;
14pub use price::conversion;
15pub use price::{Price, PriceType};
16pub use transaction::{
17    Cleared, Posting, PostingOrigin, PostingType, Transaction, TransactionStatus, TransactionType,
18};
19
20use crate::parser::value_expr::build_root_node_from_expression;
21use crate::parser::{tokenizers, value_expr};
22use crate::List;
23use crate::{error::EmptyLedgerFileError, parser::ParsedLedger};
24use crate::{filter::filter_expression, CommonOpts};
25use crate::{models::transaction::Cost, parser::Tokenizer};
26use num::BigInt;
27use std::cell::RefCell;
28use std::rc::Rc;
29
30mod account;
31mod balance;
32mod comment;
33mod currency;
34mod money;
35mod payee;
36mod price;
37mod transaction;
38
39#[derive(Debug, Clone)]
40pub struct Ledger {
41    pub accounts: List<Account>,
42    pub(crate) commodities: List<Currency>,
43    pub(crate) transactions: Vec<Transaction<Posting>>,
44    pub(crate) prices: Vec<Price>,
45    pub(crate) payees: List<Payee>,
46    pub(crate) files: Vec<PathBuf>,
47}
48
49impl TryFrom<&CommonOpts> for Ledger {
50    type Error = Box<dyn std::error::Error>;
51    fn try_from(options: &CommonOpts) -> Result<Self, Self::Error> {
52        // Get the options
53        let path: PathBuf = options.input_file.clone();
54        let mut tokenizer: Tokenizer = Tokenizer::try_from(&path)?;
55        let items = tokenizer.tokenize(options);
56        if items.is_empty() {
57            Err(Box::new(EmptyLedgerFileError))
58        } else {
59            let ledger = items.to_ledger(options)?;
60            Ok(ledger)
61        }
62    }
63}
64
65impl Ledger {
66    pub fn get_commodities(&self) -> &List<Currency> {
67        &self.commodities
68    }
69    pub fn get_prices(&self) -> &Vec<Price> {
70        &self.prices
71    }
72}
73
74impl ParsedLedger {
75    /// Creates a proper ledger from a parsed ledger
76    ///
77    /// 1. Create the lists of accounts, commodities and payees
78    /// 2. Load the commodity prices
79    /// 3. Balance the transactions by filling in missing amounts (this previously sorts the transactions by date)
80    /// 4. Create automated transactions
81    /// 5. Checks whether transactions are balanced again
82    ///
83    /// There may be room for optimization here
84    pub fn to_ledger(mut self, options: &CommonOpts) -> Result<Ledger, Box<dyn std::error::Error>> {
85        let mut commodity_strs = HashSet::<String>::new();
86        let mut account_strs = HashSet::<String>::new();
87        let mut payee_strs = HashSet::<String>::new();
88
89        // 1. Populate the directive lists
90        for transaction in self.transactions.iter() {
91            for p in transaction.postings.borrow().iter() {
92                account_strs.insert(p.account.clone());
93                if let Some(payee) = p.payee.clone() {
94                    payee_strs.insert(payee);
95                }
96            }
97        }
98        for price in self.prices.iter() {
99            commodity_strs.insert(price.clone().commodity);
100            commodity_strs.insert(price.clone().other_commodity);
101        }
102
103        //
104        // 2. Append to the parsed ledger commodities and accounts
105        //
106        // Commodities
107        for alias in commodity_strs {
108            match self.commodities.get(&alias) {
109                Ok(_) => {} // do nothing
110                Err(_) => {
111                    if options.pedantic {
112                        panic!("Error: commodity {} not declared.", &alias);
113                    }
114                    if options.strict {
115                        eprintln!("Warning: commodity {} not declared.", &alias);
116                    }
117                    self.commodities.insert(Currency::from(alias.as_str()));
118                }
119            }
120        }
121
122        // Accounts
123        for alias in account_strs {
124            match self.accounts.get(&alias) {
125                Ok(_) => {} // do nothing
126                Err(_) => {
127                    if options.pedantic {
128                        panic!("Error: account {} not declared.", &alias);
129                    }
130                    if options.strict {
131                        eprintln!("Warning: account {} not declared.", &alias);
132                    }
133                    self.accounts.insert(Account::from(alias.as_str()))
134                }
135            }
136        }
137
138        // Payees
139        let payees_copy = self.payees.clone();
140        for alias in payee_strs {
141            match self.payees.get(&alias) {
142                Ok(_) => {} // do nothing
143                Err(_) => {
144                    // Payees are actually matched by regex
145                    let mut matched = false;
146                    let mut alias_to_add = "".to_string();
147                    let mut payee_to_add = None;
148                    'outer: for (_, p) in payees_copy.iter() {
149                        for p_alias in p.get_aliases().iter() {
150                            if p_alias.is_match(alias.as_str()) {
151                                payee_to_add = Some(p);
152                                alias_to_add = alias.to_string();
153                                matched = true;
154                                break 'outer;
155                            }
156                        }
157                    }
158                    if !matched {
159                        self.payees.insert(Payee::from(alias.as_str()))
160                    } else {
161                        self.payees.add_alias(alias_to_add, payee_to_add.unwrap());
162                    }
163                }
164            }
165        }
166
167        // 3. Prices from price statements
168        let mut prices: Vec<Price> = Vec::new();
169        for price in self.prices.iter() {
170            prices.push(Price::new(
171                price.date,
172                self.commodities
173                    .get(price.commodity.as_str())
174                    .unwrap()
175                    .clone(),
176                Money::Money {
177                    amount: price.other_quantity.clone(),
178                    currency: self
179                        .commodities
180                        .get(price.other_commodity.as_str())
181                        .unwrap()
182                        .clone(),
183                },
184            ));
185        }
186
187        //
188        // 4. Get the right postings
189        //
190        let mut transactions = Vec::new();
191        let mut automated_transactions = Vec::new();
192
193        for parsed in self.transactions.iter() {
194            let mut transformer = self._transaction_to_ledger(parsed)?;
195            // (mut t, mut auto, mut new_prices)
196            transactions.append(&mut transformer.ledger_transactions);
197            automated_transactions.append(&mut transformer.raw_transactions);
198            prices.append(&mut transformer.prices);
199        }
200
201        // Now sort the transactions vector by date
202        transactions.sort_by(|a, b| a.date.unwrap().cmp(&b.date.unwrap()));
203
204        // Populate balances
205        let mut balances: HashMap<Rc<Account>, Balance> = HashMap::new();
206        for account in self.accounts.values() {
207            balances.insert(account.clone(), Balance::new());
208        }
209
210        // Balance the transactions
211        for t in transactions.iter_mut() {
212            let date = t.date.unwrap();
213            // output_balances(&balances);
214            let balance = t.balance(&mut balances, options.no_balance_check)?;
215            if balance.len() == 2 {
216                let vec = balance.iter().map(|(_, x)| x.abs()).collect::<Vec<Money>>();
217
218                let commodity = vec[0].get_commodity().unwrap().clone();
219                let price = Money::Money {
220                    amount: vec[1].get_amount() / vec[0].get_amount(),
221                    currency: vec[1].get_commodity().unwrap().clone(),
222                };
223
224                prices.push(Price::new(date, commodity, price));
225            }
226        }
227
228        // 5. Go over the transactions again and see if there is something we need to do with them
229        if !automated_transactions.is_empty() {
230            // Build a cache of abstract value trees, it takes time to parse expressions, so better do it only once
231            let mut root_nodes = HashMap::new();
232            let mut regexes = HashMap::new();
233            for automated in automated_transactions.iter_mut() {
234                let query = automated.get_filter_query();
235                let node = build_root_node_from_expression(query.as_str(), &mut regexes);
236                root_nodes.insert(query, node);
237            }
238
239            for t in transactions.iter_mut() {
240                for automated in automated_transactions.iter_mut() {
241                    let mut extra_postings = vec![];
242
243                    for p in t.postings.borrow().iter() {
244                        if p.origin != PostingOrigin::FromTransaction {
245                            continue;
246                        }
247                        let node = root_nodes.get(automated.get_filter_query().as_str());
248                        if filter_expression(
249                            node.unwrap(), // automated.get_filter_query().as_str(),
250                            p,
251                            t,
252                            &self.commodities,
253                            &mut regexes,
254                        )? {
255                            for comment in automated.comments.iter() {
256                                p.tags.borrow_mut().append(&mut comment.get_tags());
257                            }
258
259                            for auto_posting in automated.postings.borrow().iter() {
260                                let account_alias = auto_posting.account.clone();
261                                match self.accounts.get(&account_alias) {
262                                    Ok(_) => {} // do nothing
263                                    Err(_) => {
264                                        self.accounts.insert(Account::from(account_alias.as_str()))
265                                    }
266                                }
267                                let payee = if let Some(payee_alias) = &auto_posting.payee {
268                                    match self.payees.get(payee_alias) {
269                                        Ok(_) => {} // do nothing
270                                        Err(_) => {
271                                            self.payees.insert(Payee::from(payee_alias.as_str()))
272                                        }
273                                    }
274                                    Some(self.payees.get(payee_alias).unwrap().clone())
275                                } else {
276                                    p.payee.clone()
277                                };
278                                let account = self.accounts.get(&account_alias).unwrap();
279                                let money = match &auto_posting.money_currency {
280                                    None => Some(value_expr::eval_value_expression(
281                                        auto_posting.amount_expr.clone().unwrap().as_str(),
282                                        p,
283                                        t,
284                                        &mut self.commodities,
285                                        &mut regexes,
286                                    )),
287                                    Some(alias) => {
288                                        if alias.is_empty() {
289                                            Some(Money::from((
290                                                p.amount.clone().unwrap().get_commodity().unwrap(),
291                                                p.amount.clone().unwrap().get_amount()
292                                                    * auto_posting.money_amount.clone().unwrap(),
293                                            )))
294                                        } else {
295                                            match self.commodities.get(alias) {
296                                                Ok(_) => {} // do nothing
297                                                Err(_) => self
298                                                    .commodities
299                                                    .insert(Currency::from(alias.as_str())),
300                                            }
301                                            Some(Money::from((
302                                                self.commodities.get(alias).unwrap().clone(),
303                                                auto_posting.money_amount.clone().unwrap(),
304                                            )))
305                                        }
306                                    }
307                                };
308
309                                let posting = Posting {
310                                    account: account.clone(),
311                                    date: p.date,
312                                    amount: money,
313                                    balance: None,
314                                    cost: None,
315                                    kind: auto_posting.kind,
316                                    comments: vec![],
317                                    tags: RefCell::new(vec![]),
318                                    payee,
319                                    transaction: RefCell::new(Rc::downgrade(&Rc::new(t.clone()))),
320                                    origin: PostingOrigin::Automated,
321                                };
322
323                                extra_postings.push(posting);
324                            }
325                        }
326                    }
327                    t.postings.borrow_mut().append(&mut extra_postings);
328                    // if matched {
329                    //     break;
330                    // }
331                }
332            }
333            // Populate balances
334            let mut balances: HashMap<Rc<Account>, Balance> = HashMap::new();
335            for account in self.accounts.values() {
336                balances.insert(account.clone(), Balance::new());
337            }
338
339            // Balance the transactions
340            for t in transactions.iter_mut() {
341                let date = t.date.unwrap();
342                // output_balances(&balances);
343                let balance = match t.balance(&mut balances, options.no_balance_check) {
344                    Ok(balance) => balance,
345                    Err(e) => {
346                        eprintln!("{}", t);
347                        return Err(e);
348                    }
349                };
350                if balance.len() == 2 {
351                    let vec = balance.iter().map(|(_, x)| x.abs()).collect::<Vec<Money>>();
352
353                    let commodity = vec[0].get_commodity().unwrap().clone();
354                    let price = Money::Money {
355                        amount: vec[1].get_amount() / vec[0].get_amount(),
356                        currency: vec[1].get_commodity().unwrap().clone(),
357                    };
358
359                    prices.push(Price::new(date, commodity, price));
360                }
361            }
362        }
363        Ok(Ledger {
364            accounts: self.accounts,
365            commodities: self.commodities,
366            transactions,
367            prices,
368            payees: self.payees,
369            files: self.files,
370        })
371    }
372
373    fn _transaction_to_ledger(
374        &self,
375        parsed: &Transaction<tokenizers::transaction::RawPosting>,
376    ) -> Result<TransactionTransformer, Box<dyn std::error::Error>> {
377        let mut automated_transactions = vec![];
378        let mut prices = vec![];
379        let mut transactions = vec![];
380        match parsed.transaction_type {
381            TransactionType::Real => {
382                let mut transaction = Transaction::<Posting>::new(TransactionType::Real);
383                transaction.description = parsed.description.clone();
384                transaction.code = parsed.code.clone();
385                transaction.comments = parsed.comments.clone();
386                transaction.date = parsed.date;
387                transaction.effective_date = parsed.effective_date;
388                transaction.payee = parsed.payee.clone();
389                transaction.cleared = parsed.cleared;
390
391                for comment in parsed.comments.iter() {
392                    transaction.tags.append(&mut comment.get_tags());
393                }
394
395                // Go posting by posting
396                for p in parsed.postings.borrow().iter() {
397                    let payee = match &p.payee {
398                        None => transaction.get_payee(&self.payees).unwrap(),
399                        Some(x) => self.payees.get(x).unwrap().clone(),
400                    };
401                    let account = if p.account.to_lowercase().ends_with("unknown") {
402                        let mut account = None;
403                        for (_, acc) in self.accounts.iter() {
404                            for alias in acc.payees().iter() {
405                                if alias.is_match(payee.get_name()) {
406                                    account = Some(acc.clone());
407                                    break;
408                                }
409                            }
410                        }
411                        match account {
412                            Some(x) => x,
413                            None => self.accounts.get(&p.account)?.clone(),
414                        }
415                    } else {
416                        self.accounts.get(&p.account)?.clone()
417                    };
418                    let mut posting: Posting = Posting::new(
419                        &account,
420                        p.kind,
421                        &payee,
422                        PostingOrigin::FromTransaction,
423                        p.date.unwrap(),
424                    );
425                    posting.tags = RefCell::new(transaction.tags.clone());
426                    for comment in p.comments.iter() {
427                        posting.tags.borrow_mut().append(&mut comment.get_tags());
428                    }
429
430                    // Modify posting with amounts
431                    if let Some(c) = &p.money_currency {
432                        posting.amount = Some(Money::from((
433                            self.commodities.get(c.as_str()).unwrap().clone(),
434                            p.money_amount.clone().unwrap(),
435                        )));
436                    }
437                    if let Some(c) = &p.cost_currency {
438                        let posting_currency = self
439                            .commodities
440                            .get(p.money_currency.as_ref().unwrap().as_str())
441                            .unwrap();
442                        let amount = Money::from((
443                            self.commodities.get(c.as_str()).unwrap().clone(),
444                            p.cost_amount.clone().unwrap(),
445                        ));
446                        posting.cost = match p.cost_type.as_ref().unwrap() {
447                            PriceType::Total => Some(Cost::Total {
448                                amount: amount.clone(),
449                            }),
450                            PriceType::PerUnit => Some(Cost::PerUnit {
451                                amount: amount.clone(),
452                            }),
453                        };
454                        prices.push(Price::new(
455                            transaction.date.unwrap(),
456                            posting_currency.clone(),
457                            Money::Money {
458                                amount: p.cost_amount.clone().unwrap()
459                                    / match p.cost_type.as_ref().unwrap() {
460                                        PriceType::Total => {
461                                            posting.amount.as_ref().unwrap().get_amount()
462                                        }
463                                        PriceType::PerUnit => BigRational::from(BigInt::from(1)),
464                                    },
465                                currency: amount.get_commodity().unwrap().clone(),
466                            },
467                        ))
468                    }
469                    if let Some(c) = &p.balance_currency {
470                        posting.balance = Some(Money::from((
471                            self.commodities.get(c.as_str()).unwrap().clone(),
472                            p.balance_amount.clone().unwrap(),
473                        )));
474                    }
475                    transaction.postings.borrow_mut().push(posting.to_owned());
476                }
477                if transaction.is_balanced() {
478                    transaction.status = TransactionStatus::InternallyBalanced;
479                }
480                transactions.push(transaction);
481            }
482            TransactionType::Automated => {
483                // Add transaction to the automated transactions queue, we'll process them
484                // later.
485                automated_transactions.push(parsed.clone());
486            }
487            TransactionType::Periodic => {
488                eprintln!("Found periodic transaction. Skipping.");
489            }
490        }
491        Ok(TransactionTransformer {
492            ledger_transactions: transactions,
493            raw_transactions: automated_transactions,
494            prices,
495        })
496    }
497}
498
499#[derive(Copy, Clone, Debug)]
500pub enum Origin {
501    FromDirective,
502    FromTransaction,
503    Other,
504}
505
506pub trait HasName {
507    fn get_name(&self) -> &str;
508}
509
510pub trait HasAliases {
511    fn get_aliases(&self) -> &HashSet<String>;
512}
513
514pub trait FromDirective {
515    fn is_from_directive(&self) -> bool;
516}
517
518#[cfg(test)]
519mod tests {
520    use structopt::StructOpt;
521
522    use crate::{parser::Tokenizer, CommonOpts};
523
524    #[test]
525    fn payee_with_pipe_issue_121() {
526        let mut tokenizer = Tokenizer::from(
527            "2022-05-13 ! (8760) Intereses | EstateGuru
528            EstateGuru               1.06 EUR
529            Ingresos:Rendimientos
530            "
531            .to_string(),
532        );
533        let options = CommonOpts::from_iter(["", "-f", ""].iter());
534
535        let items = tokenizer.tokenize(&options);
536        let ledger = items.to_ledger(&options).unwrap();
537        let t = &ledger.transactions[0];
538        let payee = t.get_payee(&ledger.payees);
539        assert!(&ledger.payees.get("EstateGuru").is_ok());
540        assert!(payee.is_some());
541    }
542}
543
544use chrono::NaiveDate;
545
546#[derive(Debug, Clone)]
547pub struct ParsedPrice {
548    pub(crate) date: NaiveDate,
549    pub(crate) commodity: String,
550    pub(crate) other_commodity: String,
551    pub(crate) other_quantity: BigRational,
552}
553
554#[derive(Debug, Clone, Eq, PartialEq)]
555pub struct Tag {
556    pub name: String,
557    pub check: Vec<String>,
558    pub assert: Vec<String>,
559    pub value: Option<String>,
560}
561
562impl HasName for Tag {
563    fn get_name(&self) -> &str {
564        self.name.as_str()
565    }
566}
567
568struct TransactionTransformer {
569    ledger_transactions: Vec<Transaction<Posting>>,
570    raw_transactions: Vec<Transaction<tokenizers::transaction::RawPosting>>,
571    prices: Vec<Price>,
572}