Skip to content

Commit 6bf387a

Browse files
committed
Add opt-in per-transaction rounding (default off per Art. 63 OP)
Per tax advisor consultation with Naczelna Izba Skarbowa and Art. 63 Ordynacja Podatkowa, rounding should only be applied to final tax base and tax amount sums, not to individual transactions. Change default behavior to carry full f32 precision through per-transaction FX conversions. The previous per-transaction rounding to grosz is preserved as an opt-in flag: - CLI: --round-per-transaction - GUI: Options menu toggle (MenuBar) Also add default-run to Cargo.toml to resolve binary ambiguity.
1 parent 4d12f05 commit 6bf387a

4 files changed

Lines changed: 127 additions & 31 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ readme = "README.md"
1111
keywords = ["etrade", "revolut"]
1212
repository = "https://github.com/jczaja/e-trade-tax-return-pl-helper"
1313
homepage = "https://github.com/jczaja/e-trade-tax-return-pl-helper"
14+
default-run = "etradeTaxReturnHelper"
1415
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1516

1617
exclude = [

src/gui.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ use fltk::{
99
browser::MultiBrowser,
1010
button::Button,
1111
dialog,
12-
enums::{Event, Font, FrameType, Key},
12+
enums::{Event, Font, FrameType, Key, Shortcut},
1313
frame::Frame,
1414
group::Pack,
15+
menu::{MenuBar, MenuFlag},
1516
prelude::*,
1617
text::{TextBuffer, TextDisplay},
1718
window,
@@ -77,6 +78,7 @@ fn create_clear_documents(
7778

7879
fn create_execute_documents(
7980
browser: Rc<RefCell<MultiBrowser>>,
81+
menubar: Rc<RefCell<MenuBar>>,
8082
tdisplay: Rc<RefCell<TextDisplay>>,
8183
sdisplay: Rc<RefCell<TextDisplay>>,
8284
ndisplay: Rc<RefCell<TextDisplay>>,
@@ -118,6 +120,12 @@ fn create_execute_documents(
118120
buffer.set_text("");
119121
tbuffer.set_text("");
120122
nbuffer.set_text("Running...");
123+
let round_per_transaction = {
124+
let mb = menubar.borrow();
125+
mb.find_item("Options/Round per transaction")
126+
.map(|item| item.value())
127+
.unwrap_or(false)
128+
};
121129
let rd: Box<dyn etradeTaxReturnHelper::Residency> = Box::new(PL {});
122130
let etradeTaxReturnHelper::TaxCalculationResult {
123131
gross_interests,
@@ -130,7 +138,7 @@ fn create_execute_documents(
130138
revolut_dividends_transactions: revolut_transactions,
131139
sold_transactions,
132140
revolut_sold_transactions,
133-
} = match run_taxation(&rd, file_names,false, false) {
141+
} = match run_taxation(&rd, file_names, false, false, round_per_transaction) {
134142
Ok(res) => {
135143
nbuffer.set_text("Finished.\n\n (Double check if generated tax data (Summary) makes sense and then copy it to your tax form)");
136144
res
@@ -229,7 +237,16 @@ pub fn run_gui() {
229237

230238
wind.make_resizable(true);
231239

232-
let mut uberpack = Pack::new(0, 0, WIND_SIZE_X as i32, WIND_SIZE_Y as i32, "");
240+
let mut menubar = MenuBar::new(0, 0, WIND_SIZE_X, 25, "");
241+
menubar.add(
242+
"Options/Round per transaction",
243+
Shortcut::None,
244+
MenuFlag::Toggle,
245+
|_| {},
246+
);
247+
let menubar = Rc::new(RefCell::new(menubar));
248+
249+
let mut uberpack = Pack::new(0, 25, WIND_SIZE_X as i32, WIND_SIZE_Y as i32 - 25, "");
233250

234251
let mut pack = Pack::new(0, 0, WIND_SIZE_X as i32, WIND_SIZE_Y / 2 as i32, "");
235252
pack.set_type(fltk::group::PackType::Horizontal);
@@ -328,6 +345,7 @@ pub fn run_gui() {
328345
);
329346
create_execute_documents(
330347
browser.clone(),
348+
menubar.clone(),
331349
tdisplay.clone(),
332350
sdisplay.clone(),
333351
ndisplay.clone(),

src/lib.rs

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -298,32 +298,64 @@ fn round_to_grosz(val: f32) -> f32 {
298298
(val * 100.0).round() / 100.0
299299
}
300300

301-
fn compute_div_taxation(transactions: &Vec<Transaction>) -> (f32, f32) {
301+
fn compute_div_taxation(
302+
transactions: &Vec<Transaction>,
303+
round_per_transaction: bool,
304+
) -> (f32, f32) {
302305
// Gross income from dividends in target currency (PLN, EUR etc.)
303-
// Each transaction's FX-converted amount is rounded to 0.01 before summing.
304306
let gross_us_pl: f32 = transactions
305307
.iter()
306-
.map(|x| round_to_grosz(x.exchange_rate * x.gross.value() as f32))
308+
.map(|x| {
309+
let v = x.exchange_rate * x.gross.value() as f32;
310+
if round_per_transaction {
311+
round_to_grosz(v)
312+
} else {
313+
v
314+
}
315+
})
307316
.sum();
308317
// Tax paid in US in PLN
309318
let tax_us_pl: f32 = transactions
310319
.iter()
311-
.map(|x| round_to_grosz(x.exchange_rate * x.tax_paid.value() as f32))
320+
.map(|x| {
321+
let v = x.exchange_rate * x.tax_paid.value() as f32;
322+
if round_per_transaction {
323+
round_to_grosz(v)
324+
} else {
325+
v
326+
}
327+
})
312328
.sum();
313329
(gross_us_pl, tax_us_pl)
314330
}
315331

316-
fn compute_sold_taxation(transactions: &Vec<SoldTransaction>) -> (f32, f32) {
332+
fn compute_sold_taxation(
333+
transactions: &Vec<SoldTransaction>,
334+
round_per_transaction: bool,
335+
) -> (f32, f32) {
317336
// Net income from sold stock in target currency (PLN, EUR etc.)
318-
// Each transaction's FX-converted amount is rounded to 0.01 before summing.
319337
let gross_us_pl: f32 = transactions
320338
.iter()
321-
.map(|x| round_to_grosz(x.exchange_rate_settlement * x.income_us))
339+
.map(|x| {
340+
let v = x.exchange_rate_settlement * x.income_us;
341+
if round_per_transaction {
342+
round_to_grosz(v)
343+
} else {
344+
v
345+
}
346+
})
322347
.sum();
323348
// Cost of income e.g. cost_basis[target currency]
324349
let cost_us_pl: f32 = transactions
325350
.iter()
326-
.map(|x| round_to_grosz(x.exchange_rate_acquisition * x.cost_basis))
351+
.map(|x| {
352+
let v = x.exchange_rate_acquisition * x.cost_basis;
353+
if round_per_transaction {
354+
round_to_grosz(v)
355+
} else {
356+
v
357+
}
358+
})
327359
.sum();
328360
(gross_us_pl, cost_us_pl)
329361
}
@@ -387,6 +419,7 @@ pub fn run_taxation(
387419
names: Vec<String>,
388420
per_company: bool,
389421
multiyear: bool,
422+
round_per_transaction: bool,
390423
) -> Result<TaxCalculationResult, String> {
391424
validate_file_names(&names)?;
392425

@@ -532,9 +565,10 @@ pub fn run_taxation(
532565
println!("{}", per_company_report);
533566
}
534567

535-
let (gross_etrade_interests, _) = compute_div_taxation(&interests);
536-
let (gross_etrade_div, tax_etrade_div) = compute_div_taxation(&transactions);
537-
let (gross_sold, cost_sold) = compute_sold_taxation(&sold_transactions);
568+
let (gross_etrade_interests, _) = compute_div_taxation(&interests, round_per_transaction);
569+
let (gross_etrade_div, tax_etrade_div) =
570+
compute_div_taxation(&transactions, round_per_transaction);
571+
let (gross_sold, cost_sold) = compute_sold_taxation(&sold_transactions, round_per_transaction);
538572

539573
// Split Revolut transactions: savings interests (company=None, art. 30a pkt 1-3)
540574
// vs stock dividends (company=Some, art. 30a pkt 4).
@@ -548,9 +582,12 @@ pub fn run_taxation(
548582
.filter(|x| x.company.is_some())
549583
.cloned()
550584
.collect();
551-
let (gross_revolut_interests, _) = compute_div_taxation(&revolut_interests_txns);
552-
let (gross_revolut_div, tax_revolut) = compute_div_taxation(&revolut_div_txns);
553-
let (gross_revolut_sold, cost_revolut_sold) = compute_sold_taxation(&revolut_sold_transactions);
585+
let (gross_revolut_interests, _) =
586+
compute_div_taxation(&revolut_interests_txns, round_per_transaction);
587+
let (gross_revolut_div, tax_revolut) =
588+
compute_div_taxation(&revolut_div_txns, round_per_transaction);
589+
let (gross_revolut_sold, cost_revolut_sold) =
590+
compute_sold_taxation(&revolut_sold_transactions, round_per_transaction);
554591
Ok(TaxCalculationResult {
555592
gross_interests: gross_etrade_interests + gross_revolut_interests,
556593
gross_div: gross_etrade_div + gross_revolut_div,
@@ -603,7 +640,7 @@ mod tests {
603640
company: None,
604641
},
605642
];
606-
let (gross, _) = compute_div_taxation(&transactions);
643+
let (gross, _) = compute_div_taxation(&transactions, true);
607644
// Per-transaction: round(1.005) + round(1.005) = 1.01 + 1.01 = 2.02
608645
assert_eq!(gross, 2.02);
609646
// Sanity check: rounding the raw sum would give a different answer
@@ -642,7 +679,7 @@ mod tests {
642679
company: Some("TFC".to_owned()),
643680
},
644681
];
645-
let (gross, cost) = compute_sold_taxation(&transactions);
682+
let (gross, cost) = compute_sold_taxation(&transactions, true);
646683
// Per-transaction: round(1.005) + round(1.005) = 1.01 + 1.01 = 2.02
647684
assert_eq!(gross, 2.02);
648685
assert_eq!(cost, 2.02);
@@ -722,7 +759,7 @@ mod tests {
722759
exchange_rate: 4.0,
723760
company: Some("INTEL CORP".to_owned()),
724761
}];
725-
assert_eq!(compute_div_taxation(&transactions), (400.0, 100.0));
762+
assert_eq!(compute_div_taxation(&transactions, false), (400.0, 100.0));
726763
Ok(())
727764
}
728765

@@ -748,7 +785,7 @@ mod tests {
748785
},
749786
];
750787
assert_eq!(
751-
compute_div_taxation(&transactions),
788+
compute_div_taxation(&transactions, false),
752789
(400.0 + 126.0 * 3.5, 100.0 + 10.0 * 3.5)
753790
);
754791
Ok(())
@@ -774,7 +811,7 @@ mod tests {
774811
},
775812
];
776813
assert_eq!(
777-
compute_div_taxation(&transactions),
814+
compute_div_taxation(&transactions, false),
778815
(0.44 * 1.0 + 0.45 * 1.0, 0.0)
779816
);
780817
Ok(())
@@ -801,7 +838,7 @@ mod tests {
801838
},
802839
];
803840
assert_eq!(
804-
compute_div_taxation(&transactions),
841+
compute_div_taxation(&transactions, false),
805842
(0.44 * 2.0 + 0.45 * 3.0, 0.0)
806843
);
807844
Ok(())
@@ -823,7 +860,7 @@ mod tests {
823860
company: Some("TFC".to_owned()),
824861
}];
825862
assert_eq!(
826-
compute_sold_taxation(&transactions),
863+
compute_sold_taxation(&transactions, false),
827864
(100.0 * 5.0, 70.0 * 6.0)
828865
);
829866
Ok(())
@@ -859,7 +896,7 @@ mod tests {
859896
},
860897
];
861898
assert_eq!(
862-
compute_sold_taxation(&transactions),
899+
compute_sold_taxation(&transactions, false),
863900
(100.0 * 5.0 + 10.0 * 2.0, 70.0 * 6.0 + 4.0 * 3.0)
864901
);
865902
Ok(())

src/main.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ fn create_cmd_line_pattern(myapp: Command) -> Command {
5858
.help("Allow processing documents across more than year")
5959
.action(clap::ArgAction::SetTrue)
6060
)
61+
.arg(
62+
Arg::new("round-per-transaction")
63+
.long("round-per-transaction")
64+
.help("Round each FX-converted amount to grosz before summing (off by default)")
65+
.action(clap::ArgAction::SetTrue)
66+
)
6167
}
6268

6369
fn configure_dataframes_format() {
@@ -119,6 +125,7 @@ fn main() {
119125
pdfnames,
120126
matches.get_flag("per-company"),
121127
matches.get_flag("multiyear"),
128+
matches.get_flag("round-per-transaction"),
122129
) {
123130
Ok(res) => res,
124131
Err(msg) => panic!("\nError: Unable to compute taxes. \n\nDetails: {msg}"),
@@ -376,7 +383,7 @@ mod tests {
376383
.expect_and_log("error getting financial documents names");
377384
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
378385

379-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
386+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
380387
Ok(_) => panic!("Expected an error from run_taxation, but got Ok"),
381388
Err(_) => Ok(()), // Expected error, test passes
382389
}
@@ -397,7 +404,7 @@ mod tests {
397404
.expect_and_log("error getting brokarage statements pdfs names");
398405
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
399406

400-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
407+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
401408
Ok(TaxCalculationResult {
402409
gross_interests,
403410
gross_div,
@@ -431,7 +438,40 @@ mod tests {
431438
.expect_and_log("error getting brokarage statements pdfs names");
432439
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
433440

434-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
441+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
442+
Ok(TaxCalculationResult {
443+
gross_interests,
444+
gross_div,
445+
tax: tax_div,
446+
gross_sold,
447+
cost_sold,
448+
..
449+
}) => {
450+
assert_eq!(
451+
(gross_interests, gross_div, tax_div, gross_sold, cost_sold),
452+
(0.0, 9142.319, 1207.08, 22988.617, 20163.5),
453+
);
454+
Ok(())
455+
}
456+
Err(x) => panic!("Error in taxation process: {x}"),
457+
}
458+
}
459+
460+
#[test]
461+
fn test_revolut_sold_and_dividends_round_per_transaction() -> Result<(), clap::Error> {
462+
let myapp = Command::new("etradeTaxHelper").arg_required_else_help(true);
463+
let rd: Box<dyn etradeTaxReturnHelper::Residency> = Box::new(pl::PL {});
464+
465+
let matches = create_cmd_line_pattern(myapp).get_matches_from(vec![
466+
"mytest",
467+
"revolut_data/trading-pnl-statement_2022-11-01_2024-09-01_pl-pl_e989f4.csv",
468+
]);
469+
let pdfnames = matches
470+
.get_many::<String>("financial documents")
471+
.expect_and_log("error getting brokarage statements pdfs names");
472+
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
473+
474+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, true) {
435475
Ok(TaxCalculationResult {
436476
gross_interests,
437477
gross_div,
@@ -465,7 +505,7 @@ mod tests {
465505
.expect_and_log("error getting brokarage statements pdfs names");
466506
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
467507

468-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
508+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
469509
Ok(TaxCalculationResult {
470510
gross_interests,
471511
gross_div,
@@ -500,7 +540,7 @@ mod tests {
500540
.expect_and_log("error getting brokarage statements pdfs names");
501541
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
502542

503-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
543+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
504544
Ok(TaxCalculationResult {
505545
gross_interests,
506546
gross_div,
@@ -532,7 +572,7 @@ mod tests {
532572
.expect_and_log("error getting brokarage statements pdfs names");
533573
let pdfnames: Vec<String> = pdfnames.map(|x| x.to_string()).collect();
534574

535-
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false) {
575+
match etradeTaxReturnHelper::run_taxation(&rd, pdfnames, false, false, false) {
536576
Ok(TaxCalculationResult {
537577
gross_interests,
538578
gross_div,

0 commit comments

Comments
 (0)