diff --git a/Cargo.toml b/Cargo.toml index 41951f7..53d2a87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptools" -version = "0.9.3" +version = "0.9.4" authors = ["scoobybejesus "] edition = "2018" description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'." diff --git a/src/crptls_lib/account.rs b/src/crptls_lib/account.rs index f9284d5..ad8678d 100644 --- a/src/crptls_lib/account.rs +++ b/src/crptls_lib/account.rs @@ -7,8 +7,7 @@ use std::fmt; use std::collections::{HashMap}; use std::error::Error; -use chrono::{NaiveDate, NaiveTime, NaiveDateTime, DateTime, Utc, TimeZone}; -use chrono_tz::US::Eastern; +use chrono::{NaiveDate}; use decimal::d128; use serde_derive::{Serialize, Deserialize}; @@ -198,7 +197,15 @@ impl Movement { self.proceeds.get() + self.cost_basis.get() } - pub fn get_term(&self, acct_map: &HashMap, ar_map: &HashMap,) -> Term { + /// This function is only called during export operations. In addition, this will + /// only be called on flow and outgoing exchange `transactions`. Lastly, the only + /// `movement`s subject to this call with have non-margin accounts. + pub fn get_term( + &self, + acct_map: &HashMap, + ar_map: &HashMap, + txns_map: &HashMap + ) -> Term { use time::Duration; let ar = ar_map.get(&self.action_record_key).unwrap(); @@ -207,14 +214,22 @@ impl Movement { match ar.direction() { Polarity::Incoming => { - let today = Utc::now(); - let utc_lot_date = Self::create_date_time_from_atlantic( - lot.date_for_basis_purposes, - NaiveTime::from_hms_milli(12, 34, 56, 789) - ); - // if today.signed_duration_since(self.lot.date_for_basis_purposes) > 365 - if (today - utc_lot_date) > Duration::days(365) { - // TODO: figure out how to instantiate today's date and convert it to compare to NaiveDate + + // For a dual-`action record` `transaction` with a non-margin `account` incoming amount, + // if there was like-kind treatment, the basis date may be before the `transaction` date. + let txn = txns_map.get(&self.transaction_key).unwrap(); + if txn.action_record_idx_vec.len() == 2 { + let lot_date_for_basis_purposes = lot.date_for_basis_purposes; + if self.date.signed_duration_since(lot_date_for_basis_purposes) > Duration::days(365) { + return Term::LT + } + return Term::ST + } + + // For a single-`action record` `transaction`, term is meaningless, but it is being shown + // in the context of the holding period, in the event it were sold "today". + let today: NaiveDate = chrono::Local::now().naive_utc().date(); + if today.signed_duration_since(lot.date_for_basis_purposes) > Duration::days(365) { Term::LT } else { @@ -234,14 +249,6 @@ impl Movement { } } - pub fn create_date_time_from_atlantic(date: NaiveDate, time: NaiveTime) -> DateTime { - - let naive_datetime = NaiveDateTime::new(date, time); - let east_time = Eastern.from_local_datetime(&naive_datetime).unwrap(); - - east_time.with_timezone(&Utc) - } - pub fn get_income( &self, ar_map: &HashMap().expect("Header row account number should parse into u16."); + let account_num = field.trim().parse::().expect(&format!("Header row account number should parse into u16: {}", field)); // For now, their columns aren't remembered. Instead, they must have a particular index. 0th idx is the 1st account, and so on. if account_num != ((idx + 1) as u16) { println!("FATAL: CSV Import: {}", acct_num_warn); diff --git a/src/export_all.rs b/src/export_all.rs index ee5f231..5d7e023 100644 --- a/src/export_all.rs +++ b/src/export_all.rs @@ -66,6 +66,14 @@ pub fn export( &transactions_map )?; + export_csv::_7_gain_loss_8949_to_csv( + &settings, + &raw_acct_map, + &account_map, + &action_records_map, + &transactions_map + )?; + export_txt::_1_account_lot_detail_to_txt( &settings, &raw_acct_map, diff --git a/src/export_csv.rs b/src/export_csv.rs index cb686ea..5081d0a 100644 --- a/src/export_csv.rs +++ b/src/export_csv.rs @@ -7,8 +7,9 @@ use std::path::PathBuf; use std::error::Error; use decimal::d128; +use chrono::NaiveDate; -use crptls::transaction::{Transaction, ActionRecord, Polarity, TxType}; +use crptls::transaction::{ActionRecord, Polarity, Transaction, TxType}; use crptls::account::{Account, RawAccount, Term}; use crptls::core_functions::{ImportProcessParameters}; @@ -314,7 +315,7 @@ pub fn _4_transaction_mvmt_detail_to_csv( let mut amount = d128!(0); amount += mvmt.amount; // To prevent printing -5E+1 instead of 50, for example let ticker = raw_acct.ticker.to_string(); - let term = mvmt.get_term(acct_map, ars).to_string(); + let term = mvmt.get_term(acct_map, ars, txns_map).to_string(); let mut proceeds_lk = mvmt.proceeds_lk.get(); let mut cost_basis_lk = mvmt.cost_basis_lk.get(); let mut gain_loss = mvmt.get_lk_gain_or_loss(); @@ -449,7 +450,7 @@ pub fn _5_transaction_mvmt_summaries_to_csv( }; } - let term = mvmt.get_term(acct_map, ars); + let term = mvmt.get_term(acct_map, ars, txns_map); if term == Term::LT { amount_lt += mvmt.amount; @@ -622,7 +623,7 @@ pub fn _6_transaction_mvmt_detail_to_csv_w_orig( let mut amount = d128!(0); amount += mvmt.amount; // To prevent printing -5E+1 instead of 50, for example let ticker = raw_acct.ticker.to_string(); - let term = mvmt.get_term(acct_map, ars).to_string(); + let term = mvmt.get_term(acct_map, ars, txns_map).to_string(); let mut proceeds_lk = mvmt.proceeds_lk.get(); let mut cost_basis_lk = mvmt.cost_basis_lk.get(); let mut gain_loss = mvmt.get_lk_gain_or_loss(); @@ -679,3 +680,192 @@ pub fn _6_transaction_mvmt_detail_to_csv_w_orig( Ok(()) } + +pub fn _7_gain_loss_8949_to_csv( + settings: &ImportProcessParameters, + raw_acct_map: &HashMap, + acct_map: &HashMap, + ars: &HashMap, + txns_map: &HashMap, +) -> Result<(), Box> { + + let mut rows: Vec> = [].to_vec(); + + let columns = [ + "Term".to_string(), + "Txn#".to_string(), // not in 8949; just useful + "Description".to_string(), // auto_memo + "Amt in term".to_string(), // auto_memo amt split by ST/LT + "Date Acquired".to_string(), // lot basis date + "Date Sold".to_string(), // txn date + "Proceeds".to_string(), // txn proceeds (for LT or ST portion only) + "Cost basis".to_string(), // txn cost basis (for LT or ST portion only) + "Gain/loss".to_string(), + ]; + + let total_columns = columns.len(); + let mut header: Vec = Vec::with_capacity(total_columns); + header.extend_from_slice(&columns); + rows.push(header); + + let length = txns_map.len(); + + for txn_num in 1..=length { + + let txn_num = txn_num as u32; + let txn = txns_map.get(&(txn_num)).unwrap(); + let txn_date_string = txn.date.to_string(); + let tx_num_string = txn.tx_number.to_string(); + let tx_memo_string = txn.get_auto_memo(ars,raw_acct_map,acct_map, &settings.home_currency)?; + + let mut term_st: Option = None; + let mut term_lt: Option = None; + let mut ticker: Option = None; + let mut polarity: Option = None; + + let mut amount_st = d128!(0); + let mut proceeds_st = d128!(0); + let mut cost_basis_st = d128!(0); + + let mut expense_st = d128!(0); + + let mut amount_lt = d128!(0); + let mut proceeds_lt = d128!(0); + let mut cost_basis_lt = d128!(0); + + let mut expense_lt = d128!(0); + + let flow_or_outgoing_exchange_movements = txn.get_outgoing_exchange_and_flow_mvmts( + &settings.home_currency, + ars, + raw_acct_map, + acct_map, + txns_map + )?; + + let mut purchase_date_lt: NaiveDate = NaiveDate::parse_from_str("1-1-1", "%y-%m-%d").unwrap(); + let mut purchase_date_st: NaiveDate = NaiveDate::parse_from_str("1-1-1", "%y-%m-%d").unwrap(); + let mut various_dates_lt: bool = false; + let mut various_dates_st: bool = false; + let mut lt_set = false; + let mut st_set = false; + for mvmt in flow_or_outgoing_exchange_movements.iter() { + let lot = mvmt.get_lot(acct_map, ars); + let acct = acct_map.get(&lot.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + + if ticker.is_none() { ticker = Some(raw_acct.ticker.clone()) }; + + if polarity.is_none() { + polarity = if mvmt.amount > d128!(0) { + Some(Polarity::Incoming) + } else { Some(Polarity::Outgoing) + }; + } + + fn dates_are_different(existing: &NaiveDate, current: &NaiveDate) -> bool { + if existing != current {true} else {false} + } + + let term = mvmt.get_term(acct_map, ars, txns_map); + + if term == Term::LT { + if lt_set {} else { purchase_date_lt = lot.date_for_basis_purposes; lt_set = true } + various_dates_lt = dates_are_different(&purchase_date_lt, &lot.date_for_basis_purposes); + + amount_lt += mvmt.amount; + proceeds_lt += mvmt.proceeds_lk.get(); + cost_basis_lt += mvmt.cost_basis_lk.get(); + + if term_lt.is_none() { term_lt = Some(term) } + + } else { + if st_set {} else { purchase_date_st = lot.date_for_basis_purposes; st_set = true} + various_dates_st = dates_are_different(&purchase_date_st, &lot.date_for_basis_purposes); + + assert_eq!(term, Term::ST); + amount_st += mvmt.amount; + proceeds_st += mvmt.proceeds_lk.get(); + cost_basis_st += mvmt.cost_basis_lk.get(); + + if term_st.is_none() { + term_st = Some(term); + } + } + } + let lt_purchase_date = if various_dates_lt { "Various".to_string() } else { purchase_date_lt.to_string() }; + let st_purchase_date = if various_dates_st { "Various".to_string() } else { purchase_date_st.to_string() }; + + if (txn.transaction_type( + ars, + &raw_acct_map, + &acct_map)? == TxType::Flow + ) & (polarity == Some(Polarity::Incoming)) { + // The only incoming flow transaction to report would be margin profit, which is a dual-`action record` `transaction` + if txn.action_record_idx_vec.len() == 2 { + proceeds_st = -proceeds_st; // Proceeds are negative for incoming txns + cost_basis_st = d128!(0); + proceeds_lt = -proceeds_lt; // Proceeds are negative for incoming txns + cost_basis_lt = d128!(0); + } else { + continue // Plain, old income isn't reported on form 8949 + } + } + + if (txn.transaction_type( + ars, + &raw_acct_map, + &acct_map)? == TxType::Flow + ) & (polarity == Some(Polarity::Outgoing)) { + expense_st -= proceeds_st; + expense_lt -= proceeds_lt; + } + + if let Some(term) = term_st { + + let mut row: Vec = Vec::with_capacity(total_columns); + + row.push(term.abbr_string()); + row.push(tx_num_string.clone()); + row.push(tx_memo_string.clone()); + row.push(amount_st.to_string()); + row.push(st_purchase_date.clone()); + row.push(txn_date_string.clone()); + row.push(proceeds_st.to_string()); + row.push(cost_basis_st.to_string()); + row.push((proceeds_st + cost_basis_st).to_string()); + + rows.push(row); + } + if let Some(term) = term_lt { + + let mut row: Vec = Vec::with_capacity(total_columns); + + row.push(term.abbr_string()); + row.push(tx_num_string); + row.push(tx_memo_string); + row.push(amount_lt.to_string()); + row.push(lt_purchase_date.clone()); + row.push(txn_date_string); + row.push(proceeds_lt.to_string()); + row.push(cost_basis_lt.to_string()); + row.push((proceeds_lt + cost_basis_lt).to_string()); + + rows.push(row); + } + } + + let file_name = PathBuf::from("C7_Form_8949.csv"); + let path = PathBuf::from(&settings.export_path); + + let full_path: PathBuf = [path, file_name].iter().collect(); + let buffer = File::create(full_path).unwrap(); + let mut wtr = csv::Writer::from_writer(buffer); + + for row in rows.iter() { + wtr.write_record(row).expect("Could not write row to CSV file"); + } + wtr.flush().expect("Could not flush Writer, though file should exist and be complete"); + + Ok(()) +} \ No newline at end of file diff --git a/src/export_je.rs b/src/export_je.rs index 0eea778..49b86cd 100644 --- a/src/export_je.rs +++ b/src/export_je.rs @@ -129,7 +129,7 @@ depending on the bookkeeping practices you employ."; }; } - let term = mvmt.get_term(acct_map, ars); + let term = mvmt.get_term(acct_map, ars, txns_map); if term == Term::LT { amount_lt += mvmt.amount; diff --git a/src/export_txt.rs b/src/export_txt.rs index b9d660d..2d48d83 100644 --- a/src/export_txt.rs +++ b/src/export_txt.rs @@ -219,7 +219,7 @@ Enable like-kind treatment: {}", let activity_str = format!("\t Proceeds: {:>10.2}; Cost basis: {:>10.2}; for Gain/loss: {} {:>10.2}; Inc.: {:>10.2}; Exp.: {:>10.2}.", lk_proceeds.to_string().as_str().parse::()?, lk_cost_basis.to_string().as_str().parse::()?, - mvmt.get_term(acct_map, ars), + mvmt.get_term(acct_map, ars, txns_map), gain_loss.to_string().as_str().parse::()?, income.to_string().as_str().parse::()?, expense.to_string().as_str().parse::()?, diff --git a/src/mytui/app.rs b/src/mytui/app.rs index 40632fa..1d1c96d 100644 --- a/src/mytui/app.rs +++ b/src/mytui/app.rs @@ -13,17 +13,18 @@ use crate::export_txt; use crate::export_je; -pub (crate) const REPORTS: [&'static str; 10] = [ +pub (crate) const REPORTS: [&'static str; 11] = [ "1. CSV: Account Sums", "2. CSV: Account Sums (Non-zero only)", "3. CSV: Account Sums (Orig. basis vs like-kind basis)", "4. CSV: Transactions by movement (every movement)", "5. CSV: Transactions by movement (summarized by long-term/short-term)", "6. CSV: Transactions by movement (every movement, w/ orig. and like-kind basis", - "7. TXT: Accounts by lot (every movement)", - "8. TXT: Accounts by lot (every lot balance)", - "9. TXT: Accounts by lot (every non-zero lot balance)", - "10. TXT: Bookkeeping journal entries", + "7. CSV: Transactions summary by LT/ST for Form 8949", + "8. TXT: Accounts by lot (every movement)", + "9. TXT: Accounts by lot (every lot balance)", + "10. TXT: Accounts by lot (every non-zero lot balance)", + "11. TXT: Bookkeeping journal entries", ]; pub struct ListState { @@ -194,6 +195,16 @@ pub fn export( )?; } 7 => { + export_csv::_7_gain_loss_8949_to_csv( + &settings, + &raw_acct_map, + &account_map, + &action_records_map, + &transactions_map + )?; + } + + 8 => { export_txt::_1_account_lot_detail_to_txt( &settings, &raw_acct_map, @@ -202,21 +213,21 @@ pub fn export( &transactions_map, )?; } - 8 => { + 9 => { export_txt::_2_account_lot_summary_to_txt( &settings, &raw_acct_map, &account_map, )?; } - 9 => { + 10 => { export_txt::_3_account_lot_summary_non_zero_to_txt( &settings, &raw_acct_map, &account_map, )?; } - 10 => { + 11 => { if !settings.lk_treatment_enabled { export_je::prepare_non_lk_journal_entries( &settings,