From 4d5affed7ee3ae661792faa8c6a0be334d192589 Mon Sep 17 00:00:00 2001 From: Jacek Czaja Date: Wed, 28 Feb 2024 12:12:15 +0100 Subject: [PATCH] Added PDF parser to new morgan&stanley account statement documents (#96) * - implemented recognition of PDF document types * - excluded borkerage statement to seprate function * - Writing parser for account statement (WIP) * - Added sequnce parsing for div and tax of account statement * - Added parsing (val) format of f32 * - Fixed taxation transaction * - Fix to tax * - quantity of stock is f32 now * - Fixes to detection documents * - Finished adding parser for new type of PDF (POC) * - Added ignoring "$" * - Added UT * - Added tracing of legacy pdf documents parser * - Partially done date parsing from new type of PDF * - Implemented but does not work yet * - All unit tests works e.g. implemented getting a fiscal date * - Documents info updated * - Added picture * - removed deprecated TODOs * - Added unit test (ignored e.g. depending on the locally stored data) --- Cargo.lock | 2 +- Cargo.toml | 2 +- Pictures/GUI.png | Bin 0 -> 62762 bytes README.md | 12 +- src/lib.rs | 10 +- src/main.rs | 41 ++- src/pdfparser.rs | 617 ++++++++++++++++++++++++++++++++++++++++---- src/transactions.rs | 26 +- 8 files changed, 642 insertions(+), 68 deletions(-) create mode 100644 Pictures/GUI.png diff --git a/Cargo.lock b/Cargo.lock index 9326487..98c4132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,7 +571,7 @@ checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" [[package]] name = "etradeTaxReturnHelper" -version = "0.4.0" +version = "0.4.1" dependencies = [ "calamine", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 299cffc..85ffaf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "etradeTaxReturnHelper" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "Parses etrade financial documents for transaction details (income, tax paid, cost basis) and compute total income and total tax paid according to chosen tax residency (currency)" license = "BSD-3-Clause" diff --git a/Pictures/GUI.png b/Pictures/GUI.png new file mode 100644 index 0000000000000000000000000000000000000000..1549a05ea41186345f8dfd8de209ae39a65f51cd GIT binary patch literal 62762 zcmd43bzD{J_Ak5?MHCT15EKvr0}<&)Kxw2)8brFgK~zE-=`Ja0=}rj==>`F5>F&5= z?!C`>&$;*g{O-T^v(Lt}!L`<$&m7Mf-+J(tk`ThWcJ~?zg~AdM=9fXCE}5ZFXm}Xt z@STe|*IZDj3+@Jdd{QEOeD}>Q&2$Znbx1vLWvFi0QudPZ*%aKT zn*J(J$?GBYv}oy#$xH-AH}quMp^|k*HpNxq;R=QZLdY@ajZEMjvOn z-hyZ1m(zGPn!BKf!Rs!z#6Klb5|0{05$u{|;*1+O9?&*|X6kzhgT zSJx>!dVf_DDna;p`}Mv@6ZhjMim*D58#P}C%THTP(}&OqpZu_Cnqln}h`1bohlTNv z!!%vK;>w#;H>K8ouXHc1%rx1W&%26qRZU7+{ff)+Cmu;vH3c0u(b5m{y&ekROiJ`* zp!$ZBF8{cRH+t`u+R67^4e|)yTn0k++fp9rBfDmSIvc$tWYQ#3gq7l_NhF!@XagMBH{v97B3QCBYYUl|FRCg zb;nvj(VEZ9#6-u`8pUU+qiL<9ec#T&TK~R~h`5x(hZ_Va)P0l)|4TXh(Uo6zig-gu zS2osZs##y&es*2>aVm~M=6;hGgN&arjw~1TzD>gt^hDtcLY`OC+; zKQ{)3G?Ik^@6jV;MW^87TiafV zC0^mNCVWOrf;0T|MRx^@>BMyt6B550q!N!W!*y)kPM$^Xehb~u)GTnz#}ZYyy--d` zMdKM~BD=V}jE177?l`C4WG~jg5QJgBM@#e*KgUOTw`63PGqR@pAWQgJuzq}; zSgEVPVsY5Q{nGAbesW@VTjFvD2Rc^98MVLPS(rjzNy(eXbo_F8Mm zrQVSKU5#d$eG8ow&$viBpT*t3?Y+6NsYT4A=2xGD*6+w8x|J-1LNPKjDk>_z5EUiL z$dJ_3G`$h?Xb^dbtDJU;IXPH?=MV36#s;?ZrAun+7+k$|OY6C~Xx{$8!Su%No}i;6 zy6MEptsFH~^TQ=(IR%B}Pa1ZMON&2pXnA-@1J5G-@(T;+{PR<_FlXoHC~6Z?CSye( zf|5Qcx1`&azk0~!jB)GMEyc(f3W}$g-mYg?jg}wjuT9-AJUR4@NIb38zJroGSvS(P zc{Vdr?H;ecaACa$6ZJJSw_)b7>u&+gq{J4Z)JAd~-lNOkn@6LM zjb}GB$7?Y$eQ%aJvBsyR)_>2zRIj3JL{`99+x^=a*-hW3vV>4~%m|txBWDhdQL35l zp`lNur39TV8g;*+o)Z!RZ>)|sOms@3JYJz1S-!oRV%S4GdkUR*cRU;9}@>iwR{ zg!FM7t4|v~%G$<;-n#pRzIW5X_MVBVq%gnkAY=KgxTB+^TmJ<#G_>nCNRWbpQk>c; zw<>cw|L;4WFDfcZOx-;EQ*LdQ)gmtW*Vg{DUpXQm zAYfeJpm2qFyxJ9eguP4p_3NYB-5STO@Mz5x~sP{Fw>TZqcR5wueElrmL z?{V`g3!-<3iBJ9cO;#H4y0i1m8~cMrdNFaasre=4bsfqg)ej2Z*g5B6G5*C^?-ev~ zvcyO7#0QG~ii?ZNW(FJb*-=wXz66FjvLiJ}``~9xO-*HU-XYx5sG2ej$E&jc@VzB~ z;sF~5I(lXTD($a^)r;|Gx%O2!lb!kd(unj9rdt%r9685LzP!ZTw%5~|n-_b25!}|* z9WJpVU{xd6%tOD*F^YAURD)Q?^IX)N?!|8f59g}t#Psw}!NIuNTl(f9k`sQZS1(d;`Kf

U_X#u!}K+unNN5%v@kd~aO{X{ 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 8731da2..b069c50 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ # etradeTaxReturnHelper -Project that parse e-trade PDF brokerage statements and Gain and Losses documents and compute total gross gain and tax paid in US that are needed for tax return forms out of US. +Project that parse e-trade PDF account statements and Gain and Losses documents and compute total gross gain and tax paid in US that are needed for tax return forms out of US. ### Data for Tax form from capital gains (PIT-38 in Poland) 1. Install this program: `cargo install etradeTaxReturnHelper` -2. Download PDF documents from a year you are filling your tax return form for example: `Brokerage Statement .pdf`: +2. Download PDF documents from a year you are filling your tax return form for example: `Brokerage Statement .pdf` and `MS_ClientStatements_.pdf`: 1. Login to e-trade, navigate to [Documents/Brokerage Statements](https://edoc.etrade.com/e/t/onlinedocs/docsearch?doc_type=stmt) 2. Select date period 3. Download all `ACCOUNT STATEMENT` documents -3. Run: `etradeTaxReturnHelper ` +3. Run: + 1. `etradeTaxReturnHelper ` + 2. Alternatively you can run `etradeTaxReturnHelper` to have program running with GUI (graphical user interface): + ![gui](/Pictures/GUI.png) ### FAQ 1. How to install this project? @@ -17,6 +20,9 @@ Project that parse e-trade PDF brokerage statements and Gain and Losses document `cargo install etradeTaxReturnHelper` 3. For Linux where there is no X server or no priviligies to install system dependencies then you could try to install non-GUI version: `cargo install etradeTaxReturnHelper --no-default-features` +2. Does it work for other financial institutions apart from etrade ? + There is support for saving accounts statements of Revolut bank (CSV files) , as Revolut does not pay tax on customer behalf and tax from capital gain of saving account should be paid by customer. + 2. How does it work? Here is a [demo(PL)](https://www.youtube.com/watch?v=Juw3KJ1JdcA) diff --git a/src/lib.rs b/src/lib.rs index 8d2d674..0b993a0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -265,7 +265,7 @@ pub fn run_taxation( String, > { let mut parsed_div_transactions: Vec<(String, f32, f32)> = vec![]; - let mut parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![]; + let mut parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let mut parsed_gain_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; let mut parsed_revolut_transactions: Vec<(String, Currency)> = vec![]; @@ -274,15 +274,17 @@ pub fn run_taxation( // If name contains .pdf then parse as pdf // if name contains .xlsx then parse as spreadsheet if x.contains(".pdf") { - let (mut div_t, mut sold_t, _) = pdfparser::parse_brokerage_statement(x)?; + let (mut div_t, mut sold_t, _) = pdfparser::parse_statement(x)?; parsed_div_transactions.append(&mut div_t); parsed_sold_transactions.append(&mut sold_t); } else if x.contains(".xlsx") { parsed_gain_and_losses.append(&mut xlsxparser::parse_gains_and_losses(x)?); - } else { + } else if x.contains(".csv") { parsed_revolut_transactions.append(&mut csvparser::parse_revolut_transactions(x)?); + } else { + return Err(format!("Error: Unable to open a file: {x}")); } - Ok::<(), &str>(()) + Ok::<(), String>(()) })?; // 2. Verify Transactions verify_dividends_transactions(&parsed_div_transactions)?; diff --git a/src/main.rs b/src/main.rs index ff75f13..440ec44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,8 +11,9 @@ mod gui; use etradeTaxReturnHelper::run_taxation; use logging::ResultExt; -// TODO: Finish parse_revolut_transactions -// TODO: Add UT for parsing investment document +// TODO: Make a parsing of incomplete date +// TODO: Dividends of revolut should combined with dividends not sold +// TODO: When I sold on Dec there was EST cost (0.04). Make sure it is included in your results // TODO: async to get currency // TODO: parse_gain_and_losses expect -> ? // TODO: GUI : choosing residency @@ -327,6 +328,42 @@ mod tests { } } + #[test] + #[ignore] + fn test_sold_dividends_taxation_2023() -> Result<(), clap::Error> { + // Get all brokerage with dividends only + let myapp = App::new("E-trade tax helper").setting(AppSettings::ArgRequiredElseHelp); + let rd: Box = Box::new(pl::PL {}); + + let matches = create_cmd_line_pattern(myapp).get_matches_from_safe(vec![ + "mytest", + "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202302.pdf", + "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202303.pdf", + "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202306.pdf", + "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202308.pdf", + "etrade_data_2023/Brokerage Statement - XXXXX6557 - 202309.pdf", + "etrade_data_2023/MS_ClientStatements_6557_202309.pdf", + "etrade_data_2023/MS_ClientStatements_6557_202311.pdf", + "etrade_data_2023/MS_ClientStatements_6557_202312.pdf", + "etrade_data_2023/G&L_Collapsed-2023.xlsx", + ])?; + let pdfnames = matches + .values_of("financial documents") + .expect_and_log("error getting brokarage statements pdfs names"); + let pdfnames: Vec = pdfnames.map(|x| x.to_string()).collect(); + + match etradeTaxReturnHelper::run_taxation(&rd, pdfnames) { + Ok((gross_div, tax_div, gross_sold, cost_sold, _, _, _)) => { + assert_eq!( + (gross_div, tax_div, gross_sold, cost_sold), + (8369.726, 1253.2899, 14983.293, 7701.9253) + ); + Ok(()) + } + Err(x) => panic!("Error in taxation process"), + } + } + #[test] #[ignore] fn test_sold_dividends_only_taxation() -> Result<(), clap::Error> { diff --git a/src/pdfparser.rs b/src/pdfparser.rs index acab0d3..c3cca29 100644 --- a/src/pdfparser.rs +++ b/src/pdfparser.rs @@ -1,15 +1,25 @@ use pdf::file::File; +use pdf::object::PageRc; use pdf::primitive::Primitive; pub use crate::logging::ResultExt; +enum StatementType { + BrokerageStatement, + AccountStatement, +} + +#[derive(Clone, Debug, PartialEq)] enum TransactionType { Dividends, Sold, + Tax, Trade, } +#[derive(Debug, PartialEq)] enum ParserState { + SearchingCashFlowBlock, SearchingTransactionEntry, ProcessingTransaction(TransactionType), } @@ -49,8 +59,12 @@ impl Entry for F32Entry { self.val = mystr .trim() .replace(",", "") + .replace("(", "") + .replace(")", "") + .replace("$", "") .parse::() .expect(&format!("Error parsing : {} to f32", mystr)); + log::info!("Parsed f32 value: {}", self.val); } fn getf32(&self) -> Option { Some(self.val) @@ -70,6 +84,7 @@ impl Entry for I32Entry { self.val = mystr .parse::() .expect(&format!("Error parsing : {} to f32", mystr)); + log::info!("Parsed i32 value: {}", self.val); } fn geti32(&self) -> Option { Some(self.val) @@ -89,6 +104,7 @@ impl Entry for DateEntry { if chrono::NaiveDate::parse_from_str(&mystr, "%m/%d/%y").is_ok() { self.val = mystr; + log::info!("Parsed date value: {}", self.val); } } fn getdate(&self) -> Option { @@ -107,6 +123,7 @@ impl Entry for StringEntry { .clone() .into_string() .expect(&format!("Error parsing : {:#?} to f32", pstr)); + log::info!("Parsed String value: {}", self.val); } fn getstring(&self) -> Option { Some(self.val.clone()) @@ -125,8 +142,58 @@ fn create_dividend_parsing_sequence(sequence: &mut std::collections::VecDeque>) { + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTEL CORP".to_owned()], + })); + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Tax Entry +} + +fn create_dividend_fund_parsing_sequence( + sequence: &mut std::collections::VecDeque>, +) { + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["TREASURY LIQUIDITY FUND".to_owned()], + })); + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["DIV PAYMENT".to_owned()], + })); + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Income Entry +} + +fn create_qualified_dividend_parsing_sequence( + sequence: &mut std::collections::VecDeque>, +) { + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTEL CORP".to_owned()], + })); + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Income Entry +} + fn create_sold_parsing_sequence(sequence: &mut std::collections::VecDeque>) { - sequence.push_back(Box::new(I32Entry { val: 0 })); // Quantity + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Quantity + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Price + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Amount Sold +} + +fn create_sold_2_parsing_sequence(sequence: &mut std::collections::VecDeque>) { + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["INTEL CORP".to_owned()], + })); + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["ACTED AS AGENT".to_owned()], + })); + sequence.push_back(Box::new(StringEntry { + val: String::new(), + patterns: vec!["UNSOLICITED TRADE".to_owned()], + })); + sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Quantity sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Price sequence.push_back(Box::new(F32Entry { val: 0.0 })); // Amount Sold } @@ -211,11 +278,11 @@ fn create_trade_parsing_sequence(sequence: &mut std::collections::VecDeque>, transaction_dates: &mut Vec, -) -> Option<(String, String, i32, f32, f32)> { +) -> Option<(String, String, f32, f32, f32)> { let quantity = transaction .next() .unwrap() - .geti32() + .getf32() .expect_and_log("Processing of Sold transaction went wrong"); let price = transaction .next() @@ -226,70 +293,203 @@ fn yield_sold_transaction( .next() .unwrap() .getf32() - .expect_and_log("Prasing of Sold transaction went wrong"); + .expect_and_log("Parsing of Sold transaction went wrong"); // Last transaction date is settlement date // next to last is trade date let (trade_date, settlement_date) = match transaction_dates.len() { - 2 => { - let settlement_date = transaction_dates - .pop() - .expect("Error: missing trade date when parsing"); - let trade_date = transaction_dates - .pop() - .expect("Error: missing settlement_date when parsing"); - (trade_date, settlement_date) - } 1 => { log::info!("Detected unsettled sold transaction. Skipping"); return None; } - _ => { + 0 => { log::error!( "Error parsing transaction & settlement dates. Number of parsed dates: {}", transaction_dates.len() ); panic!("Error processing sold transaction. Exitting!") } + _ => { + let settlement_date = transaction_dates + .pop() + .expect("Error: missing trade date when parsing"); + let trade_date = transaction_dates + .pop() + .expect("Error: missing settlement_date when parsing"); + (trade_date, settlement_date) + } }; Some((trade_date, settlement_date, quantity, price, amount_sold)) } -/// This function parses given PDF document -/// and returns result of parsing which is a tuple of -/// found Dividends paid transactions (div_transactions), -/// Sold stock transactions (sold_transactions) -/// information on transactions in case of parsing trade document (trades) -/// Dividends paid transaction is: -/// transaction date, gross_us, tax_us, -/// Sold stock transaction is : -/// (trade_date, settlement_date, quantity, price, amount_sold) -pub fn parse_brokerage_statement( - pdftoparse: &str, +/// Recognize whether PDF document is of Brokerage Statement type (old e-trade type of PDF +/// document) or maybe Single account statment (newer e-trade/morgan stanley type of document) +fn recognize_statement(page: PageRc) -> Result { + log::info!("Starting to recognize PDF document type"); + let contents = page + .contents + .as_ref() + .ok_or("Unable to get content of first PDF page")?; + + let mut statement_type = StatementType::BrokerageStatement; + contents.operations.iter().try_for_each(|op| { + match op.operator.as_ref() { + "Tj" => { + // Text show + if op.operands.len() > 0 { + //transaction_date = op.operands[0]; + let a = &op.operands[0]; + log::info!("Detected PDF object: {a}"); + match a { + Primitive::String(actual_string) => { + let raw_string = actual_string.clone().into_string(); + let rust_string = if let Ok(r) = raw_string { + r.trim().to_uppercase() + } else { + "".to_owned() + }; + + if rust_string == "CLIENT STATEMENT" { + statement_type = StatementType::AccountStatement; + log::info!("PDF parser recognized Account Statement document by finding: \"{rust_string}\""); + return Ok(()); + } + }, + + _ => (), + } + } + } + _ => {} + } + Ok::<(),String>(()) + })?; + + Ok(statement_type) +} + +fn process_transaction( + div_transactions: &mut Vec<(String, f32, f32)>, + sold_transactions: &mut Vec<(String, String, f32, f32, f32)>, + actual_string: &pdf::primitive::PdfString, + transaction_dates: &mut Vec, + processed_sequence: &mut Vec>, + sequence: &mut std::collections::VecDeque>, + transaction_type: TransactionType, +) -> Result { + let mut state = ParserState::ProcessingTransaction(transaction_type.clone()); + let possible_obj = sequence.pop_front(); + match possible_obj { + // Move executed parser objects into Vector + // attach only i32 and f32 elements to + // processed queue + Some(mut obj) => { + obj.parse(actual_string); + // attach to sequence the same string parser if pattern is not met + match obj.getstring() { + Some(token) => { + if obj.is_pattern() == false && token != "$" { + sequence.push_front(obj); + } + } + + None => processed_sequence.push(obj), + } + + // If sequence of expected entries is + // empty then extract data from + // processeed elements + if sequence.is_empty() { + state = ParserState::SearchingTransactionEntry; + let mut transaction = processed_sequence.iter(); + match transaction_type { + TransactionType::Tax => { + // Ok we assume here that taxation of transaction appears later in document + // than actual transaction that is a subject to taxation + let tax_us = transaction + .next() + .unwrap() + .getf32() + .ok_or("Processing of Tax transaction went wrong")?; + + // Here we just go through registered transactions and pick the one where + // income is higher than tax and apply tax value + let subject_to_tax = div_transactions + .iter_mut() + .find(|x| x.1 > tax_us) + .ok_or("Error: Unable to find transaction that was taxed")?; + log::info!("Tax: {tax_us} was applied to {subject_to_tax:?}"); + subject_to_tax.2 = tax_us; + log::info!("Completed parsing Tax transaction"); + } + TransactionType::Dividends => { + let gross_us = transaction + .next() + .unwrap() + .getf32() + .ok_or("Processing of Dividend transaction went wrong")?; + + div_transactions.push(( + transaction_dates + .pop() + .ok_or("Error: missing transaction dates when parsing")?, + gross_us, + 0.0, // No tax info yet. It will be added later in Tax section + )); + log::info!("Completed parsing Dividend transaction"); + } + TransactionType::Sold => { + if let Some(trans_details) = + yield_sold_transaction(&mut transaction, transaction_dates) + { + sold_transactions.push(trans_details); + } + log::info!("Completed parsing Sold transaction"); + } + TransactionType::Trade => { + return Err("TransactionType::Trade should not appear during account statement processing!".to_string()); + } + } + processed_sequence.clear(); + } else { + state = ParserState::ProcessingTransaction(transaction_type); + } + } + + // In nothing more to be done then just extract + // parsed data from paser objects + None => { + state = ParserState::ProcessingTransaction(transaction_type); + } + } + Ok(state) +} + +/// Parse borkerage statement document type +fn parse_brokerage_statement<'a, I>( + pages_iter: I, ) -> Result< ( Vec<(String, f32, f32)>, - Vec<(String, String, i32, f32, f32)>, + Vec<(String, String, f32, f32, f32)>, Vec<(String, String, i32, f32, f32, f32, f32, f32)>, ), - &str, -> { - //2. parsing each pdf - let mypdffile = File::>::open(pdftoparse) - .expect_and_log(&format!("Error opening and parsing file: {}", pdftoparse)); - + String, +> +where + I: Iterator>, +{ + let mut div_transactions: Vec<(String, f32, f32)> = vec![]; + let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; + let mut trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; let mut state = ParserState::SearchingTransactionEntry; let mut sequence: std::collections::VecDeque> = std::collections::VecDeque::new(); let mut processed_sequence: Vec> = vec![]; // Queue for transaction dates. Pop last one or last two as trade and settlement dates let mut transaction_dates: Vec = vec![]; - let mut div_transactions: Vec<(String, f32, f32)> = vec![]; - let mut sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![]; - let mut trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; - log::info!("Parsing: {} of {} pages", pdftoparse, mypdffile.num_pages()); - for page in mypdffile.pages() { + for page in pages_iter { let page = page.unwrap(); let contents = page.contents.as_ref().unwrap(); for op in contents.operations.iter() { @@ -299,11 +499,15 @@ pub fn parse_brokerage_statement( if op.operands.len() > 0 { //transaction_date = op.operands[0]; let a = &op.operands[0]; + log::trace!("Detected PDF object: {a}"); match a { Primitive::Array(c) => { for e in c { if let Primitive::String(actual_string) = e { match state { + ParserState::SearchingCashFlowBlock => { + log::error!("Brokerage documents do not have cashflow block!") + } ParserState::SearchingTransactionEntry => { let rust_string = actual_string.clone().into_string().unwrap(); @@ -364,6 +568,9 @@ pub fn parse_brokerage_statement( let mut transaction = processed_sequence.iter(); match transaction_type { + TransactionType::Tax => { + return Err("TransactionType::Tax should not appear during brokerage statement processing!".to_string()); + } TransactionType::Dividends => { let tax_us = transaction.next().unwrap().getf32().expect_and_log("Processing of Dividend transaction went wrong"); let gross_us = transaction.next().unwrap().getf32().expect_and_log("Processing of Dividend transaction went wrong"); @@ -440,6 +647,208 @@ pub fn parse_brokerage_statement( Ok((div_transactions, sold_transactions, trades)) } +fn check_if_transaction( + candidate_string: &str, + dates: &mut Vec, + sequence: &mut std::collections::VecDeque>, + year: Option, +) -> Result { + let mut state = ParserState::SearchingTransactionEntry; + + log::info!("Searching for transaction through: \"{candidate_string}\""); + + let actual_year = + year.ok_or("Missing year that should be parsed before transactions".to_owned())?; + + if candidate_string == "DIVIDEND" { + create_dividend_fund_parsing_sequence(sequence); + state = ParserState::ProcessingTransaction(TransactionType::Dividends); + log::info!("Starting to parse Dividend Fund transaction"); + } else if candidate_string == "QUALIFIED DIVIDEND" { + create_qualified_dividend_parsing_sequence(sequence); + state = ParserState::ProcessingTransaction(TransactionType::Dividends); + log::info!("Starting to parse Qualified Dividend transaction"); + } else if candidate_string == "SOLD" { + create_sold_2_parsing_sequence(sequence); + state = ParserState::ProcessingTransaction(TransactionType::Sold); + log::info!("Starting to parse Sold transaction"); + } else if candidate_string == "TAX WITHHOLDING" { + create_tax_parsing_sequence(sequence); + state = ParserState::ProcessingTransaction(TransactionType::Tax); + log::info!("Starting to parse Tax transaction"); + } else if candidate_string == "NET CREDITS/(DEBITS)" { + // "NET CREDITS/(DEBITS)" is marking the end of CASH FLOW ACTIVITIES block + state = ParserState::SearchingCashFlowBlock; + log::info!("Finished parsing transactions"); + } else { + let datemonth_pattern = + regex::Regex::new(r"^(0?[1-9]|1[012])/(0?[1-9]|[12][0-9]|3[01])$").unwrap(); + if datemonth_pattern.is_match(candidate_string) { + dates.push(candidate_string.to_owned() + "/" + actual_year.as_str()); + } + } + Ok(state) +} + +/// Get las two digits of year from pattern like: "(AS OF 12/31/23)" +fn yield_year(rust_string: &str) -> Option { + let period_pattern = regex::Regex::new(r"\d{2}\)").unwrap(); + match period_pattern.find(rust_string) { + Some(x) => { + let year_str = x.as_str(); + let last_two_digits = &year_str[..year_str.len() - 1]; + Some(last_two_digits.to_string()) + } + None => None, + } +} + +/// Parse borkerage statement document type +fn parse_account_statement<'a, I>( + pages_iter: I, +) -> Result< + ( + Vec<(String, f32, f32)>, + Vec<(String, String, f32, f32, f32)>, + Vec<(String, String, i32, f32, f32, f32, f32, f32)>, + ), + String, +> +where + I: Iterator>, +{ + let mut div_transactions: Vec<(String, f32, f32)> = vec![]; + let mut sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; + let trades: Vec<(String, String, i32, f32, f32, f32, f32, f32)> = vec![]; + let mut state = ParserState::SearchingCashFlowBlock; + let mut sequence: std::collections::VecDeque> = + std::collections::VecDeque::new(); + let mut processed_sequence: Vec> = vec![]; + // Queue for transaction dates. Pop last one or last two as trade and settlement dates + let mut transaction_dates: Vec = vec![]; + let mut year: Option = None; + + for page in pages_iter { + let page = page.unwrap(); + let contents = page.contents.as_ref().unwrap(); + for op in contents.operations.iter() { + match op.operator.as_ref() { + "Tj" => { + // Text show + if op.operands.len() > 0 { + //transaction_date = op.operands[0]; + let a = &op.operands[0]; + log::trace!("Parsing account statement: Detected PDF object: {a}"); + match a { + Primitive::String(actual_string) => { + let raw_string = actual_string.clone().into_string(); + let rust_string = if let Ok(r) = raw_string { + r.trim().to_uppercase().replace("$", "") + } else { + "".to_owned() + }; + // Ignore empty tokens + if rust_string != "" { + match state { + ParserState::SearchingCashFlowBlock => { + // Pattern to match "(AS OF )" + let date_pattern = regex::Regex::new(r"\(AS OF (\d{1,2}\/\d{1,2}\/\d{2})\)").map_err(|_| "Unable to create regular expression to capture fiscal year")?; + + // When we find "CASH FLOW ACTIVITY BY DATE" then + // it is a starting point of transactions we are + // interested in + if rust_string == "CASH FLOW ACTIVITY BY DATE" { + state = ParserState::SearchingTransactionEntry; + log::info!("Parsing account statement: \"CASH FLOW ACTIVITY BY DATE\" detected. Start to parse transactions"); + } else if date_pattern.is_match(rust_string.as_str()) + && year.is_none() + { + // If we find (AS OF )) + // get year (last two digits out of it) + year = yield_year(&rust_string); + } + } + ParserState::SearchingTransactionEntry => { + state = check_if_transaction( + &rust_string, + &mut transaction_dates, + &mut sequence, + year.clone(), + )?; + } + ParserState::ProcessingTransaction(transaction_type) => { + state = process_transaction( + &mut div_transactions, + &mut sold_transactions, + &actual_string, + &mut transaction_dates, + &mut processed_sequence, + &mut sequence, + transaction_type, + )? + } + } + } + } + _ => (), + } + } + } + _ => {} + } + } + } + + Ok((div_transactions, sold_transactions, trades)) +} +/// This function parses given PDF document +/// and returns result of parsing which is a tuple of +/// found Dividends paid transactions (div_transactions), +/// Sold stock transactions (sold_transactions) +/// information on transactions in case of parsing trade document (trades) +/// Dividends paid transaction is: +/// transaction date, gross_us, tax_us, +/// Sold stock transaction is : +/// (trade_date, settlement_date, quantity, price, amount_sold) +pub fn parse_statement( + pdftoparse: &str, +) -> Result< + ( + Vec<(String, f32, f32)>, + Vec<(String, String, f32, f32, f32)>, + Vec<(String, String, i32, f32, f32, f32, f32, f32)>, + ), + String, +> { + //2. parsing each pdf + let mypdffile = File::>::open(pdftoparse) + .map_err(|_| format!("Error opening and parsing file: {}", pdftoparse))?; + + log::info!("Parsing: {} of {} pages", pdftoparse, mypdffile.num_pages()); + + let mut pdffile_iter = mypdffile.pages(); + + let first_page = pdffile_iter + .next() + .unwrap() + .map_err(|_| "Unable to get first page of PDF file".to_string())?; + + let document_type = recognize_statement(first_page)?; + + let (div_transactions, sold_transactions, trades) = match document_type { + StatementType::BrokerageStatement => { + log::info!("Processing brokerage statement PDF"); + parse_brokerage_statement(pdffile_iter)? + } + StatementType::AccountStatement => { + log::info!("Processing Account statement PDF"); + parse_account_statement(pdffile_iter)? + } + }; + + Ok((div_transactions, sold_transactions, trades)) +} + #[cfg(test)] mod tests { use super::*; @@ -468,6 +877,25 @@ mod tests { f.parse(&pdf::primitive::PdfString::new(data)); assert_eq!(f.getf32(), Some(4877.36)); + let data: Vec = vec![ + '(' as u8, '5' as u8, '7' as u8, '.' as u8, '9' as u8, '8' as u8, ')' as u8, + ]; + let mut f = F32Entry { val: 0.0 }; + f.parse(&pdf::primitive::PdfString::new(data)); + assert_eq!(f.getf32(), Some(57.98)); + + let data: Vec = vec!['$' as u8, '1' as u8, '.' as u8, '2' as u8, '2' as u8]; + let mut f = F32Entry { val: 0.0 }; + f.parse(&pdf::primitive::PdfString::new(data)); + assert_eq!(f.getf32(), Some(1.22)); + + let data: Vec = vec![ + '8' as u8, '2' as u8, '.' as u8, '0' as u8, '0' as u8, '0' as u8, + ]; + let mut f = F32Entry { val: 0.0 }; + f.parse(&pdf::primitive::PdfString::new(data)); + assert_eq!(f.getf32(), Some(82.00)); + // company code let data: Vec = vec!['D' as u8, 'L' as u8, 'B' as u8]; let mut s = StringEntry { @@ -486,7 +914,27 @@ mod tests { std::collections::VecDeque::new(); create_sold_parsing_sequence(&mut sequence); let mut processed_sequence: Vec> = vec![]; - processed_sequence.push(Box::new(I32Entry { val: 42 })); //quantity + processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity + processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price + processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold + + yield_sold_transaction(&mut processed_sequence.iter(), &mut transaction_dates) + .ok_or("Parsing error".to_string())?; + Ok(()) + } + + #[test] + fn test_transaction_validation_more_dates() -> Result<(), String> { + let mut transaction_dates: Vec = vec![ + "11/28/22".to_string(), + "11/29/22".to_string(), + "12/01/22".to_string(), + ]; + let mut sequence: std::collections::VecDeque> = + std::collections::VecDeque::new(); + create_sold_parsing_sequence(&mut sequence); + let mut processed_sequence: Vec> = vec![]; + processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold @@ -502,7 +950,7 @@ mod tests { std::collections::VecDeque::new(); create_sold_parsing_sequence(&mut sequence); let mut processed_sequence: Vec> = vec![]; - processed_sequence.push(Box::new(I32Entry { val: 42 })); //quantity + processed_sequence.push(Box::new(F32Entry { val: 42.0 })); //quantity processed_sequence.push(Box::new(F32Entry { val: 28.8400 })); // Price processed_sequence.push(Box::new(F32Entry { val: 1210.83 })); // Amount Sold @@ -513,11 +961,92 @@ mod tests { Ok(()) } + #[test] + fn test_check_if_transaction() -> Result<(), String> { + let rust_string = "DIVIDEND"; + let mut transaction_dates = vec![]; + let mut sequence = std::collections::VecDeque::new(); + + assert_eq!( + check_if_transaction( + &rust_string, + &mut transaction_dates, + &mut sequence, + Some("23".to_owned()) + ), + Ok(ParserState::ProcessingTransaction( + TransactionType::Dividends + )) + ); + + let rust_string = "QUALIFIED DIVIDEND"; + assert_eq!( + check_if_transaction( + &rust_string, + &mut transaction_dates, + &mut sequence, + Some("23".to_owned()) + ), + Ok(ParserState::ProcessingTransaction( + TransactionType::Dividends + )) + ); + + let rust_string = "QUALIFIED DIVIDEND"; + assert_eq!( + check_if_transaction(&rust_string, &mut transaction_dates, &mut sequence, None), + Err("Missing year that should be parsed before transactions".to_owned()) + ); + + let rust_string = "CASH"; + assert_eq!( + check_if_transaction( + &rust_string, + &mut transaction_dates, + &mut sequence, + Some("23".to_owned()) + ), + Ok(ParserState::SearchingTransactionEntry) + ); + + Ok(()) + } + + #[test] + fn test_yield_year() -> Result<(), String> { + let rust_string = "(AS OF 12/31/23)"; + assert_eq!(yield_year(&rust_string), Some("23".to_owned())); + Ok(()) + } + + #[test] + #[ignore] + fn test_account_statement() -> Result<(), String> { + assert_eq!( + parse_statement("data/MS_ClientStatements_6557_202312.pdf"), + (Ok(( + vec![ + ("12/1/23".to_owned(), 1.22, 0.00), + ("12/1/23".to_owned(), 386.50, 57.98), + ], + vec![( + "12/21/23".to_owned(), + "12/26/23".to_owned(), + 82.0, + 46.45, + 3808.86 + )], + vec![] + ))) + ); + Ok(()) + } + #[test] #[ignore] fn test_parse_brokerage_statement() -> Result<(), String> { assert_eq!( - parse_brokerage_statement("data/example-divs.pdf"), + parse_statement("data/example-divs.pdf"), (Ok(( vec![("03/01/22".to_owned(), 698.25, 104.74)], vec![], @@ -525,13 +1054,13 @@ mod tests { ))) ); assert_eq!( - parse_brokerage_statement("data/example-sold-wire.pdf"), + parse_statement("data/example-sold-wire.pdf"), Ok(( vec![], vec![( "05/02/22".to_owned(), "05/04/22".to_owned(), - -1, + -1.0, 43.69, 43.67 )], @@ -541,7 +1070,7 @@ mod tests { //TODO(jczaja): Renable reinvest dividends case as soon as you get some PDFs //assert_eq!( - // parse_brokerage_statement("data/example3.pdf"), + // parse_statement("data/example3.pdf"), // ( // vec![ // ("06/01/21".to_owned(), 0.17, 0.03), @@ -553,7 +1082,7 @@ mod tests { //); //assert_eq!( - // parse_brokerage_statement("data/example5.pdf"), + // parse_statement("data/example5.pdf"), // ( // vec![], // vec![], diff --git a/src/transactions.rs b/src/transactions.rs index 72aa882..5a5d031 100644 --- a/src/transactions.rs +++ b/src/transactions.rs @@ -38,7 +38,7 @@ pub fn verify_dividends_transactions( /// we ignore those and use net income rather than principal /// Actual Tax is to be paid from settlement_date pub fn reconstruct_sold_transactions( - sold_transactions: &Vec<(String, String, i32, f32, f32)>, + sold_transactions: &Vec<(String, String, f32, f32, f32)>, gains_and_losses: &Vec<(String, String, f32, f32, f32)>, ) -> Result, String> { // Ok What do I need. @@ -442,7 +442,7 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_dividiends_only() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![]; + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![]; let parsed_gains_and_losses: Vec<(String, String, f32, f32, f32)> = vec![]; @@ -459,18 +459,18 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_ok() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), - 1, + 1.0, 25.0, 24.8, ), ( "03/01/21".to_string(), "03/03/21".to_string(), - 2, + 2.0, 10.0, 19.8, ), @@ -526,10 +526,10 @@ mod tests { #[test] #[should_panic] fn test_sold_transaction_reconstruction_second_fail() { - let parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![( + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![( "11/07/22".to_string(), // trade date "11/09/22".to_string(), // settlement date - 173, // quantity + 173.0, // quantity 28.2035, // price 4877.36, // amount sold )]; @@ -563,18 +563,18 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_multistock() -> Result<(), String> { - let parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ ( "12/21/22".to_string(), "12/23/22".to_string(), - 163, + 163.0, 26.5900, 4332.44, ), ( "12/19/22".to_string(), "12/21/22".to_string(), - 252, + 252.0, 26.5900, 6698.00, ), @@ -652,18 +652,18 @@ mod tests { #[test] fn test_sold_transaction_reconstruction_no_gains_fail() { - let parsed_sold_transactions: Vec<(String, String, i32, f32, f32)> = vec![ + let parsed_sold_transactions: Vec<(String, String, f32, f32, f32)> = vec![ ( "06/01/21".to_string(), "06/03/21".to_string(), - 1, + 1.0, 25.0, 24.8, ), ( "03/01/21".to_string(), "03/03/21".to_string(), - 2, + 2.0, 10.0, 19.8, ),