From e0439325e28656f54c13821684985b395f84a62d Mon Sep 17 00:00:00 2001 From: scoobybejesus Date: Sun, 24 Nov 2019 01:32:08 -0500 Subject: [PATCH] Created accounting journal entry report. --- Cargo.toml | 2 +- src/crptls_lib/account.rs | 10 +- src/crptls_lib/core_functions.rs | 1 + src/crptls_lib/transaction.rs | 25 ++- src/export_all.rs | 11 ++ src/export_csv.rs | 1 - src/export_je.rs | 292 +++++++++++++++++++++++++++++++ src/export_txt.rs | 4 +- src/main.rs | 23 +++ src/setup.rs | 1 + 10 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 src/export_je.rs diff --git a/Cargo.toml b/Cargo.toml index fd2c73c..0e31ed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptools" -version = "0.8.1" +version = "0.8.2" 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 576d403..2901c65 100644 --- a/src/crptls_lib/account.rs +++ b/src/crptls_lib/account.rs @@ -25,7 +25,15 @@ pub struct RawAccount { impl RawAccount { pub fn is_home_currency(&self, compare: &String) -> bool { &self.ticker == compare - } + } + + pub fn margin_string(&self) -> String { + if self.is_margin { + "Margin".to_string() + } else { + "Non-margin".to_string() + } + } } #[derive(Clone, Debug)] diff --git a/src/crptls_lib/core_functions.rs b/src/crptls_lib/core_functions.rs index 1a67f87..a2c4111 100644 --- a/src/crptls_lib/core_functions.rs +++ b/src/crptls_lib/core_functions.rs @@ -32,6 +32,7 @@ pub struct ImportProcessParameters { pub input_file_has_iso_date_style: bool, pub should_export: bool, pub print_menu: bool, + pub journal_entry_export: bool, } pub fn import_and_process_final( diff --git a/src/crptls_lib/transaction.rs b/src/crptls_lib/transaction.rs index b5f12b1..7f08a67 100644 --- a/src/crptls_lib/transaction.rs +++ b/src/crptls_lib/transaction.rs @@ -223,6 +223,8 @@ impl Transaction { let auto_memo = if self.action_record_idx_vec.len() == 2 { + let tx_type = self.transaction_type(ars, raw_accts, acct_map)?; + let marginness = self.marginness(ars, raw_accts, acct_map); if (marginness == TxHasMargin::NoARs) | (marginness == TxHasMargin::TwoARs) { @@ -239,9 +241,13 @@ impl Transaction { let ic_raw_acct = raw_accts.get(&ic_acct.raw_key).unwrap(); let ic_ticker = &ic_raw_acct.ticker; - format!("Paid {} {} for {} {}, valued at {} {}.", - og_amt, og_ticker, ic_amt, ic_ticker, self.proceeds, home_currency) - + if tx_type == TxType::Exchange { + format!("Paid {} {} for {} {}, valued at {} {}.", + og_amt, og_ticker, ic_amt, ic_ticker, self.proceeds, home_currency) + } else { + format!("Transferred {} {} to another account. Received {} {}, likely after a transaction fee.", + og_amt, og_ticker, ic_amt, ic_ticker) + } } else { format!("Margin profit or loss valued at {} {}.", self.proceeds, home_currency) @@ -284,7 +290,18 @@ impl ActionRecord { pub fn direction(&self) -> Polarity { if self.amount < d128!(0.0) { Polarity::Outgoing} else { Polarity::Incoming } - } + } + + pub fn cost_basis_in_ar(&self) -> d128 { + + let mut cb = d128!(0); + + for mvmt in self.movements.borrow().iter() { + cb += mvmt.cost_basis.get() + } + + cb.abs() + } // pub fn is_quote_acct_for_margin_exch( // &self, diff --git a/src/export_all.rs b/src/export_all.rs index 6ce33a4..449190d 100644 --- a/src/export_all.rs +++ b/src/export_all.rs @@ -9,6 +9,7 @@ use crptls::account::{Account, RawAccount}; use crptls::core_functions::{ImportProcessParameters}; use crate::export_csv; use crate::export_txt; +use crate::export_je; pub fn export( @@ -85,5 +86,15 @@ pub fn export( &account_map, )?; + if !settings.lk_treatment_enabled { + export_je::prepare_non_lk_journal_entries( + &settings, + &action_records_map, + &raw_acct_map, + &account_map, + &transactions_map, + )?; + } + Ok(()) } \ No newline at end of file diff --git a/src/export_csv.rs b/src/export_csv.rs index a3c3b09..6845c56 100644 --- a/src/export_csv.rs +++ b/src/export_csv.rs @@ -622,7 +622,6 @@ pub fn _6_transaction_mvmt_detail_to_csv_w_orig( let mut orig_proc = mvmt.proceeds.get(); let mut orig_cost = mvmt.cost_basis.get(); let mut orig_gain_loss = mvmt.get_orig_gain_or_loss(); - println!("{:?}", expense); if tx_type == TxType::Flow && amount > d128!(0) { proceeds_lk = d128!(0); diff --git a/src/export_je.rs b/src/export_je.rs new file mode 100644 index 0000000..52e8a7e --- /dev/null +++ b/src/export_je.rs @@ -0,0 +1,292 @@ +// Copyright (c) 2017-2019, scoobybejesus +// Redistributions must include the license: https://github.com/scoobybejesus/cryptools/blob/master/LEGAL.txt + +use std::fs::{OpenOptions}; +use std::collections::{HashMap}; +use std::path::PathBuf; +use std::error::Error; +use std::io::prelude::Write; + +use decimal::d128; + +use crptls::transaction::{Transaction, ActionRecord, Polarity, TxType}; +use crptls::account::{Account, RawAccount, Term}; +use crptls::core_functions::{ImportProcessParameters}; + + +pub fn prepare_non_lk_journal_entries( + settings: &ImportProcessParameters, + ars: &HashMap, + raw_acct_map: &HashMap, + acct_map: &HashMap, + txns_map: &HashMap, +) -> Result<(), Box> { + + let file_name = PathBuf::from("J1_Journal_Entries.txt"); + let path = PathBuf::from(&settings.export_path.clone()); + let full_path: PathBuf = [path, file_name].iter().collect(); + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(full_path)?; + + writeln!(file, "Journal Entries +\nCosting method used: {}. +Home currency: {} +Enable like-kind treatment: {}", + settings.costing_method, + settings.home_currency, + settings.lk_treatment_enabled + )?; + + if settings.lk_treatment_enabled { + writeln!(file, "Like-kind cut-off date: {}.", + settings.lk_cutoff_date + )?; + } + + 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 date = txn.date; + let user_memo = txn.user_memo.to_string(); + let auto_memo = txn.get_auto_memo(ars, raw_acct_map,acct_map, &settings.home_currency)?; + let tx_type = txn.transaction_type(&ars, &raw_acct_map, &acct_map)?; + + writeln!(file, "\n=====================================\n")?; + writeln!(file, "Txn {} on {}. {}. {}", + txn_num, + date, + user_memo, + auto_memo, + )?; + + let mut cost_basis_ic: Option = None; + let mut cost_basis_og: Option = None; + + let mut acct_string_ic = "".to_string(); + let mut acct_string_og = "".to_string(); + + for ar_num in txn.action_record_idx_vec.iter() { + + let ar = ars.get(ar_num).unwrap(); + let acct = acct_map.get(&ar.account_key).unwrap(); + let raw_acct = raw_acct_map.get(&acct.raw_key).unwrap(); + + if ar.direction() == Polarity::Incoming { + cost_basis_ic = Some(ar.cost_basis_in_ar()); + acct_string_ic = format!("{} - {} ({}) (#{})", + raw_acct.name, + raw_acct.ticker, + raw_acct.margin_string(), + raw_acct.account_num, + ); + } else { + cost_basis_og = Some(ar.cost_basis_in_ar()); + acct_string_og = format!("{} - {} ({}) (#{})", + raw_acct.name, + raw_acct.ticker, + raw_acct.margin_string(), + raw_acct.account_num, + ); + } + } + + let mut term_st: Option = None; + let mut term_lt: 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 amount_lt = d128!(0); + let mut proceeds_lt = d128!(0); + let mut cost_basis_lt = d128!(0); + + let mut income = d128!(0); + let mut expense = 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 + )?; + + for mvmt in flow_or_outgoing_exchange_movements.iter() { + + if let None = polarity { + polarity = if mvmt.amount > d128!(0) { + Some(Polarity::Incoming) + } else { Some(Polarity::Outgoing) + }; + } + + let term = mvmt.get_term(acct_map, ars); + + if term == Term::LT { + amount_lt += mvmt.amount; + proceeds_lt += mvmt.proceeds_lk.get(); + cost_basis_lt += mvmt.cost_basis_lk.get(); + match term_lt { + None => { term_lt = Some(term)} + _ => {} + } + } else { + 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 == None { + term_st = Some(term); + } + } + income += mvmt.get_income(ars, &raw_acct_map, &acct_map, &txns_map)?; + expense += mvmt.get_expense(ars, &raw_acct_map, &acct_map, &txns_map)?; + } + + if (txn.transaction_type( + ars, + &raw_acct_map, + &acct_map)? == TxType::Flow + ) & (polarity == Some(Polarity::Incoming)) { + + proceeds_st = d128!(0); + cost_basis_st = d128!(0); + + proceeds_lt = d128!(0); + cost_basis_lt = d128!(0); + } + + let lt_gain_loss = proceeds_lt + cost_basis_lt; + let st_gain_loss = proceeds_st + cost_basis_st; + + let mut debits = d128!(0); + let mut credits = d128!(0); + + if let Some(cb) = cost_basis_ic { + debits += cb; + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + acct_string_ic, + "", + cb.to_string(), + "", + "", + )?; + } + + if let Some(cb) = cost_basis_og { + credits += cb; + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + acct_string_og, + "", + "", + "", + cb.to_string(), + )?; + } + + if lt_gain_loss != d128!(0) { + + if lt_gain_loss > d128!(0) { + credits += lt_gain_loss.abs(); + let ltg_string = format!("Long-term gain disposing {}", amount_lt.abs()); + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + ltg_string, + "", + "", + "", + lt_gain_loss.to_string(), + )?; + } else { + debits += lt_gain_loss.abs(); + let ltl_string = format!("Long-term loss disposing {}", amount_lt.abs()); + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + ltl_string, + "", + lt_gain_loss.abs().to_string(), + "", + "", + )?; + } + } + + if st_gain_loss != d128!(0) { + + if st_gain_loss > d128!(0) { + credits += st_gain_loss.abs(); + let stg_string = format!("Short-term gain disposing {}", amount_st.abs()); + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + stg_string, + "", + "", + "", + st_gain_loss.to_string(), + )?; + } else { + debits += st_gain_loss.abs(); + let stl_string = format!("Short-term loss disposing {}", amount_st.abs()); + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + stl_string, + "", + st_gain_loss.abs().to_string(), + "", + "", + )?; + } + } + + if income != d128!(0) { + credits += income; + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + "Income", + "", + "", + "", + income.to_string(), + )?; + } + + if expense != d128!(0) { + debits += expense.abs(); + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + "Expense", + "", + expense.abs().to_string(), + "", + "", + )?; + } + + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + "", + "", + "--------------------", + "", + "--------------------", + )?; + + writeln!(file, "{:50}{:5}{:>20}{:5}{:>20}", + " Totals", + "", + debits, + "", + credits, + )?; + + // if (debits - credits) != d128!(0) { + // println!("Rounding issue on transaction #{}", txn_num); + // } + + } + + Ok(()) +} \ No newline at end of file diff --git a/src/export_txt.rs b/src/export_txt.rs index 9978cf9..9db26a0 100644 --- a/src/export_txt.rs +++ b/src/export_txt.rs @@ -378,4 +378,6 @@ Enable like-kind treatment: {}", } Ok(()) -} \ No newline at end of file +} + + diff --git a/src/main.rs b/src/main.rs index 846eb49..e4391c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,6 +21,7 @@ mod skip_wizard; mod mytui; mod export_csv; mod export_txt; +mod export_je; mod export_all; mod tests; @@ -48,6 +49,13 @@ pub struct Flags { #[structopt(name = "accept args", short = "a", long = "accept")] accept_args: bool, + /// This flag will suppress the printing of "all" reports, except that it will trigger the + /// production of a txt file containing an accounting journal entry for every transaction. + /// Individual account and transaction reports may still be printed via the print_menu + /// with the -p flag. The journal entry report is only suitable for non-like-kind activity. + #[structopt(name = "journal entries", short, long = "journal-entries")] + journal_entries_only: bool, + /// This will cause the program to expect the txDate field in the file_to_import to use the format /// YYYY-MM-dd or YY-MM-dd (or YYYY/MM/dd or YY/MM/dd, depending on the date-separator option) /// instead of the default US-style MM-dd-YYYY or MM-dd-YY (or MM/dd/YYYY or MM/dd/YY, depending on the @@ -126,8 +134,10 @@ fn main() -> Result<(), Box> { let mut should_export_all = settings.should_export; let present_print_menu_tui = settings.print_menu; + let print_journal_entries_only = settings.journal_entry_export; if present_print_menu_tui { should_export_all = false } + if print_journal_entries_only { should_export_all = false } if should_export_all { @@ -140,6 +150,19 @@ fn main() -> Result<(), Box> { )?; } + if print_journal_entries_only { + + if !settings.lk_treatment_enabled { + export_je::prepare_non_lk_journal_entries( + &settings, + &action_records_map, + &raw_acct_map, + &account_map, + &transactions_map, + )?; + } + } + if present_print_menu_tui { mytui::print_menu_tui::print_menu_tui( diff --git a/src/setup.rs b/src/setup.rs index 778a9d0..bd80bae 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -73,6 +73,7 @@ pub (crate) fn run_setup(args: super::Cli) -> Result<(PathBuf, ImportProcessPara should_export: should_export, export_path: output_dir_path, print_menu: args.flags.print_menu, + journal_entry_export: args.flags.journal_entries_only, }; Ok((input_file_path, settings))