Added Form 8949 report. Updated get_term().
This commit is contained in:
parent
a44e0f145d
commit
d3c7c8c6a3
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cryptools"
|
||||
version = "0.9.3"
|
||||
version = "0.9.4"
|
||||
authors = ["scoobybejesus <scoobybejesus@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
description = "Command-line utility for processing cryptocurrency transactions into 'lots' and 'movements'."
|
||||
|
|
|
@ -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<u16, Account>, ar_map: &HashMap<u32, ActionRecord>,) -> 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<u16, Account>,
|
||||
ar_map: &HashMap<u32, ActionRecord>,
|
||||
txns_map: &HashMap<u32, Transaction>
|
||||
) -> 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<Utc> {
|
||||
|
||||
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<u32,
|
||||
|
|
|
@ -89,7 +89,7 @@ The next column's value should be 2, then 3, etc, until the final account).";
|
|||
for (idx, field) in headerstrings[3..*length].iter().enumerate() {
|
||||
|
||||
// Parse account numbers.
|
||||
let account_num = field.parse::<u16>().expect("Header row account number should parse into u16.");
|
||||
let account_num = field.trim().parse::<u16>().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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<u16, RawAccount>,
|
||||
acct_map: &HashMap<u16, Account>,
|
||||
ars: &HashMap<u32, ActionRecord>,
|
||||
txns_map: &HashMap<u32, Transaction>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let mut rows: Vec<Vec<String>> = [].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<String> = 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<Term> = None;
|
||||
let mut term_lt: Option<Term> = None;
|
||||
let mut ticker: Option<String> = None;
|
||||
let mut polarity: Option<Polarity> = 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<String> = 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<String> = 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(())
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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::<f32>()?,
|
||||
lk_cost_basis.to_string().as_str().parse::<f32>()?,
|
||||
mvmt.get_term(acct_map, ars),
|
||||
mvmt.get_term(acct_map, ars, txns_map),
|
||||
gain_loss.to_string().as_str().parse::<f32>()?,
|
||||
income.to_string().as_str().parse::<f32>()?,
|
||||
expense.to_string().as_str().parse::<f32>()?,
|
||||
|
|
|
@ -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<I> {
|
||||
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue