1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225
/*!
* Transaction module
*
* Transaction, or Xact abbreviated, is the main element of the Journal.
* It contains contains Postings.
*/
use chrono::NaiveDate;
use crate::{
balance::Balance,
journal::{Journal, PostIndex, XactIndex},
parser,
post::Post,
};
pub struct Xact {
pub date: Option<NaiveDate>,
pub aux_date: Option<NaiveDate>,
pub payee: String,
pub posts: Vec<PostIndex>,
pub note: Option<String>,
// pub balance: Amount,
}
impl Xact {
pub fn new(date: Option<NaiveDate>, payee: &str, note: Option<String>) -> Self {
// code: Option<String>
Self {
payee: payee.to_owned(),
note,
posts: vec![],
date,
aux_date: None,
// balance: Amount::null(),
}
}
/// Creates a new Transaction from the scanned tokens.
pub fn create(date: &str, aux_date: &str, payee: &str, note: &str) -> Self {
let _date = if date.is_empty() {
None
} else {
Some(parser::parse_date(date))
};
let _aux_date = if aux_date.is_empty() {
None
} else {
Some(parser::parse_date(aux_date))
};
let _payee = if payee.is_empty() {
"Unknown Payee".to_string()
} else {
payee.to_string()
};
let _note = if note.is_empty() {
None
} else {
Some(note.to_string())
};
Self {
date: _date,
payee: _payee,
posts: vec![],
note: _note,
aux_date: _aux_date,
}
}
}
/// Finalize transaction.
/// Adds the Xact and the Posts to the Journal.
///
/// `bool xact_base_t::finalize()`
///
pub fn finalize(xact_index: XactIndex, journal: &mut Journal) {
// let mut balance: Option<Amount> = None;
let mut balance = Balance::new();
// The pointer to the post that has no amount.
let mut null_post: Option<PostIndex> = None;
let xact = journal.xacts.get(xact_index).expect("xact");
// Balance
for post_index in &xact.posts {
// must balance?
let post = journal.posts.get(*post_index).expect("post");
// amount = post.cost ? post.amount
// for now, just use the amount
if post.amount.is_some() {
// Add to balance.
let Some(amt) = &post.amount
else {panic!("should not happen")};
balance.add(amt);
} else if null_post.is_some() {
todo!()
} else {
null_post = Some(*post_index);
}
}
// If there is only one post, balance against the default account if one has
// been set.
if xact.posts.len() == 1 {
todo!("handle")
}
if null_post.is_none() && balance.amounts.len() == 2 {
// When an xact involves two different commodities (regardless of how
// many posts there are) determine the conversion ratio by dividing the
// total value of one commodity by the total value of the other. This
// establishes the per-unit cost for this post for both commodities.
let mut top_post: Option<&Post> = None;
for i in &xact.posts {
let post = journal.get_post(*i);
if post.amount.is_some() && top_post.is_none() {
top_post = Some(post);
}
}
// if !saw_cost && top_post
if top_post.is_some() {
// log::debug("there were no costs, and a valid top_post")
let mut x = balance.amounts.iter().nth(0).unwrap();
let mut y = balance.amounts.iter().nth(1).unwrap();
// if x && y
if !x.is_zero() && !y.is_zero() {
if x.commodity_index != top_post.unwrap().amount.unwrap().commodity_index {
(x, y) = (y, x);
}
let comm = x.commodity_index;
let per_unit_cost = (*y / *x).abs();
for i in &xact.posts {
let post = journal.posts.get_mut(*i).unwrap();
let amt = post.amount.unwrap();
if amt.commodity_index == comm {
balance -= amt;
post.cost = Some(per_unit_cost * amt);
balance += post.cost.unwrap();
}
}
}
}
}
// if (has_date())
{
for post_index in &xact.posts {
let p = journal.posts.get_mut(*post_index).unwrap();
if p.cost.is_none() {
continue;
}
let Some(amt) = &p.amount else {panic!("No amount found on the posting")};
let Some(cost) = &p.cost else {panic!("No cost found on the posting")};
if amt.commodity_index == cost.commodity_index {
panic!("A posting's cost must be of a different commodity than its amount");
}
{
// Cost breakdown
// TODO: virtual cost does not create a price
// let today = NaiveDateTime::new(Local::now().date_naive(), NaiveTime::MIN);
let moment = xact.date.unwrap().and_hms_opt(0, 0, 0).unwrap();
let breakdown = journal
.commodity_pool
.exchange(amt, cost, false, true, moment);
// add price
if amt.commodity_index != cost.commodity_index {
journal
.commodity_pool
.add_price(amt.commodity_index.unwrap(), moment, *cost);
}
p.amount = Some(breakdown.amount);
}
}
}
// Handle null-amount post.
if null_post.is_some() {
// If one post has no value at all, its value will become the inverse of
// the rest. If multiple commodities are involved, multiple posts are
// generated to balance them all.
log::debug!("There was a null posting");
let Some(null_post_index) = null_post
else {panic!("should not happen")};
let Some(post) = journal.posts.get_mut(null_post_index)
else {panic!("should not happen")};
// use inverse amount
let amt = if balance.amounts.len() == 1 {
// only one commodity
let amt_bal = balance.amounts.iter().nth(0).unwrap();
amt_bal.inverse()
} else {
// TODO: handle option when there are multiple currencies and only one blank posting.
todo!("check this option")
};
post.amount = Some(amt);
null_post = None;
}
// TODO: Process Commodities?
// TODO: Process Account records from Posts.
}