From dbda9e34d1df3a489a3a733daf59b68cc3313bd4 Mon Sep 17 00:00:00 2001 From: Diego Tipaldi Date: Sun, 20 Sep 2026 18:08:32 +0200 Subject: [PATCH] Add "Dividend Reinvestment" action to Schwab importer When opting-in for dividend reinvestments in the equity awards, Schwab creates four entries: * Dividends: The dividend amount being paid * Tax Withholding: The taxes paid on such amount * Dividend Reinvestment: The net money available to be reinvested * Deposit: The stocks being bought with such amount The importer missed the logic to handle the Dividend Reinvestment action and this PR adds it. The PR also adds VOO (Vanguard S&P 500 ETF) to the known securities. Tested with my 2025 tax declaration. --- data/security_identifiers.csv | 1 + .../importers/schwab/transaction_extractor.py | 23 +++++++++- .../schwab/test_transaction_extractor.py | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/data/security_identifiers.csv b/data/security_identifiers.csv index 05b01446..a53b52fa 100644 --- a/data/security_identifiers.csv +++ b/data/security_identifiers.csv @@ -6,5 +6,6 @@ SCHB,,10712436 SCHE,,10912918 SCHF,US8085248057,20251231 VGT,US92204A7028,1781764 +VOO,US9229083632,22423967 VT,US9220427424,4354003 META,US30303M1027,14917609 diff --git a/src/opensteuerauszug/importers/schwab/transaction_extractor.py b/src/opensteuerauszug/importers/schwab/transaction_extractor.py index 61a05986..a54210d5 100644 --- a/src/opensteuerauszug/importers/schwab/transaction_extractor.py +++ b/src/opensteuerauszug/importers/schwab/transaction_extractor.py @@ -25,6 +25,7 @@ "Cash Merger", "Cash Merger Adj", "Div Adjustment", + "Dividend Reinvestment", "Full Redemption", "Full Redemption Adj", "Foreign Tax Paid", @@ -409,7 +410,7 @@ def create_cash_stock( requires_settlement=requires_settlement, ) - if action == "Buy" or action == "Reinvest Shares": + if action in ("Buy", "Reinvest Shares"): if schwab_qty and schwab_qty > 0 and isinstance(pos_object, SecurityPosition): calculated_cost = None cash_flow = None @@ -438,6 +439,23 @@ def create_cash_stock( requires_settlement=True, ) + elif action == "Dividend Reinvestment": + if ( + schwab_amount is not None + and schwab_amount < 0 + and isinstance(pos_object, SecurityPosition) + ): + cash_stock = create_cash_stock( + schwab_amount, + f"Cash out for {action} {pos_object.symbol}", + requires_settlement=True, + ) + else: + raise ValueError( + "Dividend Reinvestment requires a negative amount and a valid " + f"SecurityPosition. Amount: {schwab_amount}, Position: {pos_object}" + ) + elif action == "Sale" or action == "Sell": if schwab_qty and isinstance(pos_object, SecurityPosition): if schwab_qty < 0: @@ -471,6 +489,9 @@ def create_cash_stock( name=action, ) if cash_flow: + logger.info( + f"Creating cash stock mutation for \"{action}\" transaction for symbol {pos_object.symbol}." + ) cash_stock = create_cash_stock( cash_flow, f"Cash in for {action} {pos_object.symbol}", diff --git a/tests/importers/schwab/test_transaction_extractor.py b/tests/importers/schwab/test_transaction_extractor.py index e0cfd6bd..ff767d46 100644 --- a/tests/importers/schwab/test_transaction_extractor.py +++ b/tests/importers/schwab/test_transaction_extractor.py @@ -483,6 +483,51 @@ def test_action_reinvest_dividend(self): assert cash_stock_entry.mutation is True, "Cash stock entry should be a mutation" assert cash_stock_entry.quantity == Decimal("555.65") + def test_action_dividend_reinvestment_creates_cash_outflow(self): + extractor = create_extractor("EquityAwardsCenter_Transactions_20260916082416.json") + data = { + "FromDate": "01/01/2025", + "ToDate": "12/31/2025", + "Transactions": [ + { + "Date": "12/15/2025", + "Action": "Dividend Reinvestment", + "Symbol": "GOOG", + "Description": "Debit", + "Amount": "-$558.74", + }, + { + "Date": "12/15/2025", + "Action": "Deposit", + "Symbol": "GOOG", + "Quantity": "1.8079", + "Description": "Div Reinv", + "TransactionDetails": [{"Details": {"PurchasePrice": "$309.0497"}}], + }, + ], + } + + result = run_extraction_test(extractor, data, 2) + assert result is not None + + goog_data = find_position(result, SecurityPosition, "GOOG") + cash_data = find_position(result, CashPosition) + assert goog_data is not None + assert cash_data is not None + + _, stocks, payments = goog_data + assert len(stocks) == 1 + assert stocks[0].quantity == Decimal("1.8079") + assert payments is None + + _, cash_stocks, cash_payments = cash_data + assert cash_payments is None + assert len(cash_stocks) == 1 + cash_stock = cash_stocks[0] + assert cash_stock.quantity == Decimal("-558.74") + assert cash_stock.balance == Decimal("-558.74") + assert cash_stock.requires_settlement is True + def test_action_qual_div_reinvest(self): extractor = create_extractor() data = {