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 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 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 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 for alias in commodity_strs {
108 match self.commodities.get(&alias) {
109 Ok(_) => {} 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 for alias in account_strs {
124 match self.accounts.get(&alias) {
125 Ok(_) => {} 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 let payees_copy = self.payees.clone();
140 for alias in payee_strs {
141 match self.payees.get(&alias) {
142 Ok(_) => {} Err(_) => {
144 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 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 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 transactions.append(&mut transformer.ledger_transactions);
197 automated_transactions.append(&mut transformer.raw_transactions);
198 prices.append(&mut transformer.prices);
199 }
200
201 transactions.sort_by(|a, b| a.date.unwrap().cmp(&b.date.unwrap()));
203
204 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 for t in transactions.iter_mut() {
212 let date = t.date.unwrap();
213 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 if !automated_transactions.is_empty() {
230 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(), 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(_) => {} 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(_) => {} 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(_) => {} 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 }
332 }
333 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 for t in transactions.iter_mut() {
341 let date = t.date.unwrap();
342 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 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 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 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}