1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::ops::Deref;
4use std::rc::{Rc, Weak};
5
6use chrono::NaiveDate;
7use num::rational::BigRational;
8
9use crate::error::{BalanceError, LedgerError};
10use crate::models::balance::Balance;
11use crate::models::{Account, Comment, HasName, Money, Payee};
12use crate::List;
13use num::BigInt;
14use std::fmt;
15use std::fmt::{Display, Formatter};
16
17use super::Tag;
18use crate::filter::preprocess_query;
19use regex::Regex;
20
21#[derive(Debug, Clone)]
22pub struct Transaction<PostingType> {
23 pub status: TransactionStatus,
24 pub date: Option<NaiveDate>,
25 pub effective_date: Option<NaiveDate>,
26 pub cleared: Cleared,
27 pub code: Option<String>,
28 pub description: String,
29 pub payee: Option<String>,
30 pub postings: RefCell<Vec<PostingType>>,
31 pub comments: Vec<Comment>,
32 pub transaction_type: TransactionType,
33 pub tags: Vec<Tag>,
34 filter_query: Option<String>,
35}
36
37#[derive(Debug, Clone)]
38pub struct Posting {
39 pub(crate) account: Rc<Account>,
40 pub date: NaiveDate,
41 pub amount: Option<Money>,
42 pub balance: Option<Money>,
43 pub cost: Option<Cost>,
44 pub kind: PostingType,
45 pub comments: Vec<Comment>,
46 pub tags: RefCell<Vec<Tag>>,
47 pub payee: Option<Rc<Payee>>,
48 pub transaction: RefCell<Weak<Transaction<Posting>>>,
49 pub origin: PostingOrigin,
50}
51
52#[derive(Debug, Clone, Copy, Eq, PartialEq)]
53pub enum PostingOrigin {
54 FromTransaction,
55 Automated,
56 Periodic,
57}
58
59impl<T> Transaction<T> {
60 pub fn get_filter_query(&mut self) -> String {
61 match self.filter_query.clone() {
62 None => {
63 let mut parts: Vec<String> = vec![];
64 let mut current = String::new();
65 let mut in_regex = false;
66 let mut in_string = false;
67 for c in self.description.chars() {
68 if (c == ' ') & !in_string & !in_regex {
69 parts.push(current.clone());
70 current = String::new();
71 }
72 if c == '"' {
73 in_string = !in_string;
74 } else if c == '/' {
75 in_regex = !in_regex;
76 current.push(c);
77 } else {
78 current.push(c)
79 }
80 }
81 parts.push(current);
82 let res = preprocess_query(&parts, &false);
84 self.filter_query = Some(res.clone());
85 res
86 }
87 Some(x) => x,
88 }
89 }
90 pub fn get_payee(&self, payees: &List<Payee>) -> Option<Rc<Payee>> {
91 match &self.payee {
92 Some(payee) => match payees.get(payee) {
93 Ok(x) => Some(x.clone()),
94 Err(_) => panic!("Couldn't find payee {}", payee),
95 },
96 None => match payees.get(&self.description) {
97 Ok(x) => Some(x.clone()),
98 Err(_) => None,
99 },
100 }
101 }
102}
103
104#[derive(Debug, Copy, Clone, PartialEq, Eq)]
105pub enum TransactionStatus {
106 NotChecked,
107 InternallyBalanced,
108 Correct,
109}
110
111#[derive(Debug, Copy, Clone, PartialEq, Eq)]
112pub enum TransactionType {
113 Real,
114 Automated,
115 Periodic,
116}
117
118#[derive(Debug, Copy, Clone, PartialEq, Eq)]
119pub enum Cleared {
120 Unknown,
121 NotCleared,
122 Cleared,
123}
124
125#[derive(Debug, Clone, Copy, Eq, PartialEq)]
126pub enum PostingType {
127 Real,
128 Virtual,
129 VirtualMustBalance,
130}
131
132impl Posting {
133 pub fn new(
134 account: &Rc<Account>,
135 kind: PostingType,
136 payee: &Payee,
137 origin: PostingOrigin,
138 date: NaiveDate,
139 ) -> Posting {
140 Posting {
141 account: account.clone(),
142 amount: None,
143 date,
144 balance: None,
145 cost: None,
146 kind,
147 comments: vec![],
148 tags: RefCell::new(vec![]),
149 payee: Some(Rc::new(payee.clone())),
150 transaction: RefCell::new(Default::default()),
151 origin,
152 }
153 }
154 pub fn set_amount(&mut self, money: Money) {
155 self.amount = Some(money)
156 }
157 pub fn has_tag(&self, regex: Regex) -> bool {
158 for t in self.tags.borrow().iter() {
159 if regex.is_match(t.get_name()) {
160 return true;
161 }
162 }
163 false
164 }
165 pub fn get_tag(&self, regex: Regex) -> Option<String> {
166 for t in self.tags.borrow().iter() {
167 if regex.is_match(t.get_name()) {
168 return t.value.clone();
169 }
170 }
171 None
172 }
173 pub fn get_exact_tag(&self, regex: String) -> Option<String> {
174 for t in self.tags.borrow().iter() {
175 if regex.as_str() == t.get_name() {
176 return t.value.clone();
177 }
178 }
179 None
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub enum Cost {
185 Total { amount: Money },
186 PerUnit { amount: Money },
187}
188
189impl<PostingType> Transaction<PostingType> {
190 pub fn new(t_type: TransactionType) -> Transaction<PostingType> {
191 Transaction {
192 status: TransactionStatus::NotChecked,
193 date: None,
194 effective_date: None,
195 cleared: Cleared::Unknown,
196 code: None,
197 description: "".to_string(),
198 payee: None,
199 postings: RefCell::new(vec![]),
200 comments: vec![],
201 transaction_type: t_type,
202 tags: vec![],
203 filter_query: None,
204 }
205 }
206}
207
208fn total_balance(postings: &[Posting], kind: PostingType) -> Balance {
209 let bal = Balance::new();
210 postings
211 .iter()
212 .filter(|p| p.amount.is_some() & (p.kind == kind))
213 .map(|p| match &p.cost {
214 None => Balance::from(p.amount.as_ref().unwrap().clone()),
215 Some(cost) => match cost {
216 Cost::Total { amount } => {
217 if p.amount.as_ref().unwrap().clone().is_negative() {
218 Balance::from(-amount.clone())
219 } else {
220 Balance::from(amount.clone())
221 }
222 }
223 Cost::PerUnit { amount } => {
224 let currency = match amount {
225 Money::Zero => panic!("Cost has no currency"),
226 Money::Money { currency, .. } => currency,
227 };
228 let units = match amount {
229 Money::Zero => BigRational::new(BigInt::from(0), BigInt::from(1)),
230 Money::Money { amount, .. } => amount.clone(),
231 } * match p.amount.as_ref().unwrap() {
232 Money::Zero => BigRational::new(BigInt::from(0), BigInt::from(1)),
233 Money::Money { amount, .. } => amount.clone(),
234 };
235 let money = Money::Money {
236 amount: units
237 * (if p.amount.as_ref().unwrap().is_negative() {
238 -BigInt::from(1)
239 } else {
240 BigInt::from(1)
241 }),
242 currency: currency.clone(),
243 };
244 Balance::from(money)
245 }
246 },
247 })
248 .fold(bal, |acc, cur| acc + cur)
249}
250
251impl Transaction<Posting> {
252 pub fn is_balanced(&self) -> bool {
253 total_balance(&*self.postings.borrow(), PostingType::Real).can_be_zero()
254 }
255
256 pub fn num_empty_postings(&self) -> usize {
257 self.postings
258 .borrow()
259 .iter()
260 .filter(|p| p.amount.is_none() & p.balance.is_none())
261 .count()
262 }
263
264 pub fn balance(
275 &mut self,
276 balances: &mut HashMap<Rc<Account>, Balance>,
277 skip_balance_check: bool,
278 ) -> Result<Balance, Box<dyn std::error::Error>> {
279 let mut transaction_balance = Balance::new();
280
281 match total_balance(&*self.postings.borrow(), PostingType::VirtualMustBalance).can_be_zero()
283 {
284 true => {}
285 false => return Err(Box::new(BalanceError::TransactionIsNotBalanced)),
286 }
287
288 let mut fill_account = Rc::new(Account::from("this will never be used"));
290 let mut fill_payee = None;
291 let mut fill_date: NaiveDate = NaiveDate::from_ymd(1900, 1, 1); let mut postings: Vec<Posting> = Vec::new();
293
294 for p in self.postings.get_mut().iter() {
295 if p.kind != PostingType::Real {
296 continue;
297 }
298 if let Some(money) = &p.amount {
300 let expected_balance = balances.get(p.account.deref()).unwrap().clone() + Balance::from(money.clone()); if !skip_balance_check {
303 if let Some(balance) = &p.balance {
304 if Balance::from(balance.clone()) != expected_balance {
305 eprintln!("Found: {}", balance);
306 eprintln!("Expected: {}", expected_balance);
307 eprintln!(
308 "Difference: {}",
309 expected_balance - Balance::from(balance.clone())
310 );
311 return Err(Box::new(BalanceError::TransactionIsNotBalanced));
312 }
313 }
314 }
315
316 balances.insert(p.account.clone(), expected_balance);
318
319 transaction_balance = transaction_balance + match &p.cost {
322 None => Balance::from(money.clone()),
323 Some(cost) => match cost {
325 Cost::Total { amount } => {
326 if p.amount.as_ref().unwrap().is_negative() {
327 Balance::from(-amount.clone())
328 } else {
329 Balance::from(amount.clone())
330 }
331 }
332 Cost::PerUnit { amount } => {
333 let currency = match amount {
334 Money::Zero => panic!("Cost has no currency"),
335 Money::Money { currency, .. } => currency,
336 };
337 let units = match amount {
338 Money::Zero => BigRational::from(BigInt::from(0)),
339 Money::Money { amount, .. } => amount.clone(),
340 } * match p.amount.as_ref().unwrap() {
341 Money::Zero => BigRational::from(BigInt::from(0)),
342 Money::Money { amount, .. } => amount.clone(),
343 };
344 let money = Money::Money {
345 amount: units,
346 currency: currency.clone(),
347 };
348 Balance::from(money)
349 }
350 },
351 };
352
353 postings.push(Posting {
355 account: p.account.clone(),
356 amount: p.amount.clone(),
357 date: p.date,
358 balance: p.balance.clone(),
359 cost: p.cost.clone(),
360 kind: PostingType::Real,
361 comments: p.comments.clone(),
362 tags: p.tags.clone(),
363 payee: p.payee.clone(),
364 transaction: p.transaction.clone(),
365 origin: PostingOrigin::FromTransaction,
366 });
367 } else if p.balance.is_some() & !skip_balance_check {
368 let balance = p.balance.as_ref().unwrap();
370
371 let account_bal = balances.get(p.account.deref()).unwrap().clone();
373 let amount_bal = Balance::from(balance.clone()) - account_bal;
374 let money = amount_bal.to_money()?;
375 transaction_balance = transaction_balance + Balance::from(money.clone());
376 balances.insert(p.account.clone(), Balance::from(balance.clone()));
378 postings.push(Posting {
379 account: p.account.clone(),
380 date: p.date,
381 amount: Some(money),
382 balance: p.balance.clone(),
383 cost: p.cost.clone(),
384 kind: PostingType::Real,
385 comments: p.comments.clone(),
386 tags: p.tags.clone(),
387 payee: p.payee.clone(),
388 transaction: p.transaction.clone(),
389 origin: PostingOrigin::FromTransaction,
390 });
391 } else {
392 fill_account = p.account.clone();
394 fill_payee = p.payee.clone();
395 fill_date = p.date;
396 }
397 }
398
399 let empties = self
400 .postings
401 .borrow()
402 .iter()
403 .filter(|p| p.kind == PostingType::Real)
404 .count()
405 - postings.len();
406 if empties > 1 {
407 Err(Box::new(LedgerError::TooManyEmptyPostings(empties)))
408 } else if empties == 0 {
409 match transaction_balance.can_be_zero() {
410 true => {
411 postings.append(
413 &mut self
414 .postings
415 .borrow_mut()
416 .iter()
417 .filter(|p| p.kind != PostingType::Real)
418 .cloned()
419 .collect(),
420 );
421 self.postings.replace(postings);
422 Ok(transaction_balance)
423 }
424 false => Err(Box::new(BalanceError::TransactionIsNotBalanced)),
425 }
426 } else {
427 for (_, money) in (-transaction_balance).iter() {
430 let expected_balance = balances.get(&fill_account.clone()).unwrap().clone()
431 + Balance::from(money.clone());
432
433 balances.insert(fill_account.clone(), expected_balance);
434
435 postings.push(Posting {
436 account: fill_account.clone(),
437 amount: Some(money.clone()),
438 balance: None,
439 cost: None,
440 kind: PostingType::Real,
441 comments: self.comments.clone(),
442 tags: RefCell::new(self.tags.clone()),
443 payee: fill_payee.clone(),
444 date: fill_date,
445 transaction: self.postings.borrow()[0].transaction.clone(),
446 origin: PostingOrigin::FromTransaction,
447 });
448 }
449 postings.append(
451 &mut self
452 .postings
453 .get_mut()
454 .iter()
455 .filter(|p| p.kind != PostingType::Real)
456 .cloned()
457 .collect(),
458 );
459 self.postings.replace(postings);
460 Ok(Balance::new())
462 }
463 }
464}
465
466impl Display for Transaction<Posting> {
467 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
468 let mut message = String::new();
469 message.push_str(format!("{} {}", self.date.unwrap(), self.description).as_str());
470 for p in self.postings.borrow().iter() {
471 if p.amount.as_ref().is_some() {
472 message.push_str(
473 format!(
474 "\n\t{:50}{}",
475 p.account.get_name(),
476 p.amount.as_ref().unwrap()
477 )
478 .as_str(),
479 );
480 } else {
481 message.push_str(format!("\n\t{:50}", p.account.get_name()).as_str());
482 }
483 }
484 write!(f, "{}", message)
485 }
486}