From 5a71d8c7dd3c1a945e93e6f3e4d8c2d350723354 Mon Sep 17 00:00:00 2001 From: mierin12 Date: Fri, 26 Sep 2025 00:26:36 +0200 Subject: [PATCH 1/7] Allow information pane for Transaction to be filtered by accounts Issue: #5025 --- .../abuchen/portfolio/ui/views/panes/TransactionsPane.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/panes/TransactionsPane.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/panes/TransactionsPane.java index 077c546f49..9d71694103 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/panes/TransactionsPane.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/panes/TransactionsPane.java @@ -6,6 +6,7 @@ import java.util.List; import jakarta.inject.Inject; +import jakarta.inject.Named; import org.eclipse.jface.action.Separator; import org.eclipse.jface.action.ToolBarManager; @@ -25,6 +26,7 @@ import name.abuchen.portfolio.model.TransactionPair; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; +import name.abuchen.portfolio.ui.UIConstants; import name.abuchen.portfolio.ui.editor.AbstractFinanceView; import name.abuchen.portfolio.ui.util.DropDown; import name.abuchen.portfolio.ui.util.SimpleAction; @@ -37,6 +39,7 @@ public class TransactionsPane implements InformationPanePage { @Inject + @Named(UIConstants.Context.ACTIVE_CLIENT) private Client client; @Inject From e50c73850178bced7e954f4f1e731e217f192642 Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Fri, 10 Oct 2025 09:35:17 +0200 Subject: [PATCH 2/7] Added property originalTransaction as reference to original transaction --- .../actions/DetectDuplicatesActionTest.java | 10 +++++----- .../actions/InsertActionTest.java | 3 ++- .../abuchen/portfolio/model/Transaction.java | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/DetectDuplicatesActionTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/DetectDuplicatesActionTest.java index a535c28aab..8d451a247a 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/DetectDuplicatesActionTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/DetectDuplicatesActionTest.java @@ -35,8 +35,8 @@ public void testDuplicateDetection4AccountTransaction() throws IntrospectionExce { DetectDuplicatesAction action = new DetectDuplicatesAction(new Client()); - new PropertyChecker( - AccountTransaction.class, "note", "source", "forex", "monetaryAmount", "updatedAt") + new PropertyChecker(AccountTransaction.class, "note", "source", "forex", "monetaryAmount", + "updatedAt", "originalTransaction") .before((name, o, c) -> assertThat(name, action.process(o, account(c)).getCode(), is(Code.WARNING))) .after((name, o, c) -> assertThat(name, action.process(o, account(c)).getCode(), @@ -52,7 +52,7 @@ public void testDuplicateDetection4PortfolioTransaction() DetectDuplicatesAction action = new DetectDuplicatesAction(new Client()); new PropertyChecker(PortfolioTransaction.class, "fees", "taxes", "note", "source", - "forex", "monetaryAmount", "updatedAt") // + "forex", "monetaryAmount", "updatedAt", "originalTransaction") // .before((name, o, c) -> assertThat(name, action.process(o, portfolio(c)).getCode(), is(Code.WARNING))) .after((name, o, c) -> assertThat(name, @@ -68,7 +68,7 @@ public void testDuplicateDetectionWithPurchaseAndDeliveryPairs() DetectDuplicatesAction action = new DetectDuplicatesAction(new Client()); new PropertyChecker(PortfolioTransaction.class, "type", "fees", "taxes", "note", "source", - "forex", "monetaryAmount", "updatedAt") // + "forex", "monetaryAmount", "updatedAt", "originalTransaction") // .before((name, o, c) -> { o.setType(PortfolioTransaction.Type.BUY); c.setType(PortfolioTransaction.Type.DELIVERY_INBOUND); @@ -80,7 +80,7 @@ public void testDuplicateDetectionWithPurchaseAndDeliveryPairs() .run(); new PropertyChecker(PortfolioTransaction.class, "type", "fees", "taxes", "note", "source", - "forex", "monetaryAmount", "updatedAt") // + "forex", "monetaryAmount", "updatedAt", "originalTransaction") // .before((name, o, c) -> { o.setType(PortfolioTransaction.Type.SELL); c.setType(PortfolioTransaction.Type.DELIVERY_OUTBOUND); diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/InsertActionTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/InsertActionTest.java index cdcf9aa729..75d4a1cc74 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/InsertActionTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/actions/InsertActionTest.java @@ -127,7 +127,8 @@ public void testPortfolioTransactionAttributes() throws IntrospectionException assertThat(properties, hasItem("note")); assertThat(properties, hasItem("source")); assertThat(properties, hasItem("updatedAt")); + assertThat(properties, hasItem("originalTransaction")); - assertThat(properties.size(), is(10)); + assertThat(properties.size(), is(11)); } } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Transaction.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Transaction.java index a213041258..377527edf7 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Transaction.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Transaction.java @@ -197,6 +197,15 @@ public int compare(Transaction t1, Transaction t2) } } + /** + * Reference to the original transaction if {@link ClientFilter} has to + * manipulate the transaction in order to create the filter. For example, a + * cash transfer might be converted into a withdrawal if the target account + * is not part of the filter. It is marked transient becuase it must not be + * stored in the XML. + */ + private transient Transaction originalTransaction; // NOSONAR + private String uuid; private LocalDateTime date; private String currencyCode; @@ -240,6 +249,16 @@ public Transaction(LocalDateTime date, String currencyCode, long amount, Securit this.note = note; } + public Transaction getOriginalTransaction() + { + return originalTransaction; + } + + public void setOriginalTransaction(Transaction originalTransaction) + { + this.originalTransaction = originalTransaction; + } + public String getUUID() { return uuid; From dbad1b8c5e3c5c4e3594dd5de629fa0386add1f2 Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Fri, 10 Oct 2025 09:53:29 +0200 Subject: [PATCH 3/7] Track original transactions when creating filtered clients --- .../filter/ClientClassificationFilter.java | 165 ++++++++++++------ .../snapshot/filter/ClientFilterHelper.java | 14 +- .../snapshot/filter/ClientSecurityFilter.java | 27 ++- .../filter/PortfolioClientFilter.java | 18 +- .../snapshot/filter/WithoutTaxesFilter.java | 7 + 5 files changed, 159 insertions(+), 72 deletions(-) diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientClassificationFilter.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientClassificationFilter.java index 39742756c9..96c16fe08b 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientClassificationFilter.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientClassificationFilter.java @@ -196,7 +196,8 @@ private void addBuySellT(CalculationState state, Portfolio portfolio, PortfolioT long securityAmount = value(t.getAmount(), securityWeight); securityAmount = t.getType() == PortfolioTransaction.Type.BUY ? securityAmount - taxes : securityAmount + taxes; - Account account = (Account) t.getCrossEntry().getCrossOwner(t); + var entry = (BuySellEntry) t.getCrossEntry(); + Account account = (Account) entry.getCrossOwner(t); BigDecimal accountWeight = state.getWeight(account); long accountAmount = value(t.getAmount(), accountWeight); @@ -209,6 +210,9 @@ private void addBuySellT(CalculationState state, Portfolio portfolio, PortfolioT // assignment and the security assignment BuySellEntry copy = new BuySellEntry(state.asReadOnly(portfolio), state.account2readonly.get(account)); + copy.getPortfolioTransaction().setOriginalTransaction(t); + copy.getAccountTransaction().setOriginalTransaction(entry.getAccountTransaction()); + copy.setDate(t.getDateTime()); copy.setCurrencyCode(t.getCurrencyCode()); copy.setSecurity(t.getSecurity()); @@ -232,6 +236,7 @@ private void addBuySellT(CalculationState state, Portfolio portfolio, PortfolioT accountAmount - commonAmount, null, t.getType() == PortfolioTransaction.Type.BUY ? AccountTransaction.Type.REMOVAL : AccountTransaction.Type.DEPOSIT); + ta.setOriginalTransaction(t); state.asReadOnly(account).internalAddTransaction(ta); } @@ -242,6 +247,7 @@ private void addBuySellT(CalculationState state, Portfolio portfolio, PortfolioT if (securityAmount - commonAmount > 0) { PortfolioTransaction tp = new PortfolioTransaction(); + tp.setOriginalTransaction(t); tp.setDateTime(t.getDateTime()); tp.setCurrencyCode(t.getCurrencyCode()); tp.setSecurity(t.getSecurity()); @@ -262,6 +268,7 @@ private void addDeliveryT(CalculationState state, Portfolio portfolio, Portfolio PortfolioTransaction.Type targetType, BigDecimal weight) { PortfolioTransaction copy = new PortfolioTransaction(); + copy.setOriginalTransaction(t); copy.setDateTime(t.getDateTime()); copy.setCurrencyCode(t.getCurrencyCode()); copy.setSecurity(t.getSecurity()); @@ -290,6 +297,9 @@ private void adaptAccountTransactions(CalculationState state, Account account) { long amount = value(t.getAmount(), accountWeight); + var copy = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount, null, null); + copy.setOriginalTransaction(t); + switch (t.getType()) { case SELL: @@ -298,20 +308,26 @@ private void adaptAccountTransactions(CalculationState state, Account account) // #adaptPortfolioTransactions method), create a deposit or // removal in the account if (!state.isCategorized(t.getSecurity())) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); + { + copy.setType(AccountTransaction.Type.DEPOSIT); + state.asReadOnly(account).internalAddTransaction(copy); + } break; case BUY: if (!state.isCategorized(t.getSecurity())) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.REMOVAL)); + { + copy.setType(AccountTransaction.Type.REMOVAL); + state.asReadOnly(account).internalAddTransaction(copy); + } break; case DIVIDENDS: if (!state.isCategorized(t.getSecurity())) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); + { + copy.setType(AccountTransaction.Type.DEPOSIT); + state.asReadOnly(account).internalAddTransaction(copy); + } else addSecurityRelatedAccountT(state, account, t); break; @@ -320,22 +336,30 @@ private void adaptAccountTransactions(CalculationState state, Account account) if (t.getSecurity() != null && state.isCategorized(t.getSecurity())) addSecurityRelatedAccountT(state, account, t); else if (t.getSecurity() != null) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); + { + copy.setType(AccountTransaction.Type.DEPOSIT); + state.asReadOnly(account).internalAddTransaction(copy); + } else - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, t.getType())); + { + copy.setType(t.getType()); + state.asReadOnly(account).internalAddTransaction(copy); + } break; case FEES: if (t.getSecurity() != null && state.isCategorized(t.getSecurity())) addSecurityRelatedAccountT(state, account, t); else if (t.getSecurity() != null) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.REMOVAL)); + { + copy.setType(AccountTransaction.Type.REMOVAL); + state.asReadOnly(account).internalAddTransaction(copy); + } else - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, t.getType())); + { + copy.setType(t.getType()); + state.asReadOnly(account).internalAddTransaction(copy); + } break; case TRANSFER_IN: @@ -343,8 +367,10 @@ else if (t.getSecurity() != null) // deposit right away. Otherwise a full transfer transaction // is created. if (!state.isCategorized((Account) t.getCrossEntry().getCrossOwner(t))) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); + { + copy.setType(AccountTransaction.Type.DEPOSIT); + state.asReadOnly(account).internalAddTransaction(copy); + } else addTransferT(state, account, t); break; @@ -354,32 +380,42 @@ else if (t.getSecurity() != null) // above); only if the inbound account is not categorized, // then create a removal transaction if (!state.isCategorized((Account) t.getCrossEntry().getCrossOwner(t))) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.REMOVAL)); + { + copy.setType(AccountTransaction.Type.REMOVAL); + state.asReadOnly(account).internalAddTransaction(copy); + } break; case TAX_REFUND: // taxes are never included - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.DEPOSIT)); + copy.setType(AccountTransaction.Type.DEPOSIT); + state.asReadOnly(account).internalAddTransaction(copy); break; case TAXES: // taxes are never included - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount, null, AccountTransaction.Type.REMOVAL)); + copy.setType(AccountTransaction.Type.REMOVAL); + state.asReadOnly(account).internalAddTransaction(copy); break; case DEPOSIT: case REMOVAL: case INTEREST: case INTEREST_CHARGE: - long taxes = value(t.getUnitSum(Unit.Type.TAX).getAmount(), accountWeight); // for INTEREST - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), amount + taxes, null, t.getType())); + // for INTEREST + long taxes = value(t.getUnitSum(Unit.Type.TAX).getAmount(), accountWeight); + + copy.setType(t.getType()); + copy.setAmount(amount + taxes); + state.asReadOnly(account).internalAddTransaction(copy); + if (taxes != 0) - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), taxes, null, AccountTransaction.Type.REMOVAL)); + { + var removal = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), taxes, null, + AccountTransaction.Type.REMOVAL); + removal.setOriginalTransaction(t); + state.asReadOnly(account).internalAddTransaction(removal); + } break; default: @@ -398,6 +434,7 @@ private void addSecurityRelatedAccountT(CalculationState state, Account account, AccountTransaction copy = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount + taxes, t.getSecurity(), t.getType()); + copy.setOriginalTransaction(t); t.getUnits().filter(u -> u.getType() != Unit.Type.TAX).forEach(u -> copy.addUnit(value(u, securityWeight))); state.asReadOnly(account).internalAddTransaction(copy); @@ -409,8 +446,10 @@ private void addSecurityRelatedAccountT(CalculationState state, Account account, { AccountTransaction.Type deltaType = delta > 0 ^ t.getType().isDebit() ? AccountTransaction.Type.DEPOSIT : AccountTransaction.Type.REMOVAL; - state.asReadOnly(account).internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), Math.abs(delta), null, deltaType)); + var deltaTx = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), Math.abs(delta), null, + deltaType); + deltaTx.setOriginalTransaction(t); + state.asReadOnly(account).internalAddTransaction(deltaTx); } } @@ -445,10 +484,11 @@ else if (inboundWeight.compareTo(outboundWeight) < 0) AccountTransaction ot = (AccountTransaction) t.getCrossEntry().getCrossTransaction(t); - state.asReadOnly(outboundAccount) - .internalAddTransaction(new AccountTransaction(ot.getDateTime(), ot.getCurrencyCode(), - value(ot.getAmount(), outboundWeight.subtract(inboundWeight)), null, - AccountTransaction.Type.REMOVAL)); + var removal = new AccountTransaction(ot.getDateTime(), ot.getCurrencyCode(), + value(ot.getAmount(), outboundWeight.subtract(inboundWeight)), null, + AccountTransaction.Type.REMOVAL); + removal.setOriginalTransaction(t); + state.asReadOnly(outboundAccount).internalAddTransaction(removal); } else // inboundWeight > outboundWeight { @@ -458,16 +498,20 @@ else if (inboundWeight.compareTo(outboundWeight) < 0) state.asReadOnly(inboundAccount).internalAddTransaction(entry.getTargetTransaction()); state.asReadOnly(outboundAccount).internalAddTransaction(entry.getSourceTransaction()); - state.asReadOnly(inboundAccount) - .internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - value(t.getAmount(), inboundWeight.subtract(outboundWeight)), null, - AccountTransaction.Type.DEPOSIT)); + var deposit = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), + value(t.getAmount(), inboundWeight.subtract(outboundWeight)), null, + AccountTransaction.Type.DEPOSIT); + deposit.setOriginalTransaction(t); + state.asReadOnly(inboundAccount).internalAddTransaction(deposit); } } private AccountTransferEntry createTransferEntry(AccountTransferEntry entry, BigDecimal weight) { AccountTransferEntry copy = new AccountTransferEntry(); + copy.getSourceTransaction().setOriginalTransaction(entry.getSourceTransaction()); + copy.getTargetTransaction().setOriginalTransaction(entry.getTargetTransaction()); + copy.setDate(entry.getSourceTransaction().getDateTime()); copy.setNote(entry.getSourceTransaction().getNote()); @@ -496,26 +540,39 @@ private void collectSecurityRelatedTx(CalculationState state, Account account) long taxes = value(t.getUnitSum(Unit.Type.TAX).getAmount(), weight); long amount = value(t.getAmount(), weight); - AccountTransaction copy = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - amount + taxes, t.getSecurity(), t.getType()); - - t.getUnits().filter(u -> u.getType() != Unit.Type.TAX).forEach(u -> copy.addUnit(value(u, weight))); - - readOnlyAccount.internalAddTransaction(copy); - readOnlyAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - amount + taxes, null, AccountTransaction.Type.REMOVAL)); + var dividend = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount + taxes, + t.getSecurity(), t.getType()); + dividend.setOriginalTransaction(t); + t.getUnits().filter(u -> u.getType() != Unit.Type.TAX) + .forEach(u -> dividend.addUnit(value(u, weight))); + readOnlyAccount.internalAddTransaction(dividend); + + var reverseDividend = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount + taxes, + null, AccountTransaction.Type.REMOVAL); + reverseDividend.setOriginalTransaction(t); + readOnlyAccount.internalAddTransaction(reverseDividend); break; case FEES: - readOnlyAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - value(t.getAmount(), weight), t.getSecurity(), t.getType())); - readOnlyAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - value(t.getAmount(), weight), null, AccountTransaction.Type.DEPOSIT)); + var fee = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), value(t.getAmount(), weight), + t.getSecurity(), t.getType()); + fee.setOriginalTransaction(t); + readOnlyAccount.internalAddTransaction(fee); + + var reverseFees = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), + value(t.getAmount(), weight), null, AccountTransaction.Type.DEPOSIT); + reverseFees.setOriginalTransaction(t); + readOnlyAccount.internalAddTransaction(reverseFees); break; case FEES_REFUND: - readOnlyAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - value(t.getAmount(), weight), t.getSecurity(), t.getType())); - readOnlyAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - value(t.getAmount(), weight), null, AccountTransaction.Type.REMOVAL)); + var refund = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), + value(t.getAmount(), weight), t.getSecurity(), t.getType()); + refund.setOriginalTransaction(t); + readOnlyAccount.internalAddTransaction(refund); + + var reverseRefund = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), + value(t.getAmount(), weight), null, AccountTransaction.Type.REMOVAL); + reverseRefund.setOriginalTransaction(t); + readOnlyAccount.internalAddTransaction(reverseRefund); break; case TAXES: case TAX_REFUND: diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientFilterHelper.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientFilterHelper.java index b22d984754..da17afc9a4 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientFilterHelper.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientFilterHelper.java @@ -35,8 +35,13 @@ private ClientFilterHelper() copy.setShares(value(t.getShares(), weight)); copy.setAmount(value(t.getAmount(), weight)); - sourcePortfolio.internalAddTransaction(copy.getSourceTransaction()); - targetPortfolio.internalAddTransaction(copy.getTargetTransaction()); + var newSourceTx = copy.getSourceTransaction(); + newSourceTx.setOriginalTransaction(transferEntry.getSourceTransaction()); + sourcePortfolio.internalAddTransaction(newSourceTx); + + var newTargetTx = copy.getTargetTransaction(); + newTargetTx.setOriginalTransaction(transferEntry.getTargetTransaction()); + targetPortfolio.internalAddTransaction(newTargetTx); } /* package */ static void recreateTransfer(AccountTransferEntry transferEntry, ReadOnlyAccount sourceAccount, @@ -49,11 +54,13 @@ private ClientFilterHelper() copy.setDate(t.getDateTime()); copy.setNote(t.getNote()); + copy.getSourceTransaction().setOriginalTransaction(t); copy.getSourceTransaction().setCurrencyCode(t.getCurrencyCode()); copy.getSourceTransaction().setAmount(t.getAmount()); copy.getSourceTransaction().addUnits(t.getUnits()); AccountTransaction tt = transferEntry.getTargetTransaction(); + copy.getTargetTransaction().setOriginalTransaction(tt); copy.getTargetTransaction().setCurrencyCode(tt.getCurrencyCode()); copy.getTargetTransaction().setAmount(tt.getAmount()); @@ -67,8 +74,7 @@ private static long value(long value, BigDecimal weight) return value; else return BigDecimal.valueOf(value) // - .multiply(weight, Values.MC) - .divide(Classification.ONE_HUNDRED_PERCENT_BD, Values.MC) + .multiply(weight, Values.MC).divide(Classification.ONE_HUNDRED_PERCENT_BD, Values.MC) .setScale(0, RoundingMode.HALF_EVEN).longValue(); } } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientSecurityFilter.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientSecurityFilter.java index 05eda83e2e..3edb57e726 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientSecurityFilter.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/ClientSecurityFilter.java @@ -116,25 +116,32 @@ private void addAccountTransaction(Function getAccount AccountTransaction copy = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount + taxes, t.getSecurity(), t.getType()); + copy.setOriginalTransaction(t); t.getUnits().filter(u -> u.getType() != Unit.Type.TAX).forEach(copy::addUnit); getAccount.apply((Account) pair.getOwner()).internalAddTransaction(copy); - getAccount.apply((Account) pair.getOwner()) - .internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - amount + taxes, null, AccountTransaction.Type.REMOVAL)); + + var reverseDividend = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), amount + taxes, null, + AccountTransaction.Type.REMOVAL); + reverseDividend.setOriginalTransaction(t); + getAccount.apply((Account) pair.getOwner()).internalAddTransaction(reverseDividend); break; case FEES: getAccount.apply((Account) pair.getOwner()).internalAddTransaction(t); - getAccount.apply((Account) pair.getOwner()) - .internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - t.getAmount(), null, AccountTransaction.Type.DEPOSIT)); + + var reverseFees = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), t.getAmount(), null, + AccountTransaction.Type.DEPOSIT); + reverseFees.setOriginalTransaction(t); + getAccount.apply((Account) pair.getOwner()).internalAddTransaction(reverseFees); break; case FEES_REFUND: getAccount.apply((Account) pair.getOwner()).internalAddTransaction(t); - getAccount.apply((Account) pair.getOwner()) - .internalAddTransaction(new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), - t.getAmount(), null, AccountTransaction.Type.REMOVAL)); + + var reverseRefund = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), t.getAmount(), null, + AccountTransaction.Type.REMOVAL); + reverseRefund.setOriginalTransaction(t); + getAccount.apply((Account) pair.getOwner()).internalAddTransaction(reverseRefund); break; case TAXES: case TAX_REFUND: @@ -156,6 +163,8 @@ private void addAccountTransaction(Function getAccount private PortfolioTransaction convertToDelivery(PortfolioTransaction t, PortfolioTransaction.Type targetType) { PortfolioTransaction pseudo = new PortfolioTransaction(); + pseudo.setOriginalTransaction(t); + pseudo.setDateTime(t.getDateTime()); pseudo.setCurrencyCode(t.getCurrencyCode()); pseudo.setSecurity(t.getSecurity()); diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/PortfolioClientFilter.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/PortfolioClientFilter.java index 59c87fccaa..8283677b9b 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/PortfolioClientFilter.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/snapshot/filter/PortfolioClientFilter.java @@ -62,7 +62,7 @@ else if (element instanceof Account account) else throw new IllegalArgumentException("element is null or of wrong type: " + element); //$NON-NLS-1$ } - + public boolean hasElement(Object element) { if (element instanceof Portfolio portfolio) @@ -187,6 +187,8 @@ private void recreateBuySell(BuySellEntry buySell, ReadOnlyPortfolio readOnlyPor PortfolioTransaction t = buySell.getPortfolioTransaction(); BuySellEntry copy = new BuySellEntry(readOnlyPortfolio, readOnlyAccount); + copy.getPortfolioTransaction().setOriginalTransaction(buySell.getPortfolioTransaction()); + copy.getAccountTransaction().setOriginalTransaction(buySell.getAccountTransaction()); copy.setDate(t.getDateTime()); copy.setCurrencyCode(t.getCurrencyCode()); @@ -226,8 +228,10 @@ private void collectSecurityRelevantTx(Portfolio portfolio, ReadOnlyAccount pseu if (!processedDividendTx.contains(t)) { pseudoAccount.internalAddTransaction(t); - pseudoAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), t.getAmount(), null, AccountTransaction.Type.REMOVAL)); + var removal = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), t.getAmount(), null, + AccountTransaction.Type.REMOVAL); + removal.setOriginalTransaction(t); + pseudoAccount.internalAddTransaction(removal); processedDividendTx.add(t); } break; @@ -236,8 +240,10 @@ private void collectSecurityRelevantTx(Portfolio portfolio, ReadOnlyAccount pseu if (!processedDividendTx.contains(t)) { pseudoAccount.internalAddTransaction(t); - pseudoAccount.internalAddTransaction(new AccountTransaction(t.getDateTime(), - t.getCurrencyCode(), t.getAmount(), null, AccountTransaction.Type.DEPOSIT)); + var deposit = new AccountTransaction(t.getDateTime(), t.getCurrencyCode(), t.getAmount(), null, + AccountTransaction.Type.DEPOSIT); + deposit.setOriginalTransaction(t); + pseudoAccount.internalAddTransaction(deposit); processedDividendTx.add(t); } break; @@ -318,6 +324,7 @@ private void adaptAccountTransactions(Account account, Map Date: Sun, 12 Oct 2025 16:00:00 +0200 Subject: [PATCH 4/7] Added calculator image to derived transactions --- name.abuchen.portfolio.ui/icons/calculator.png | Bin 0 -> 463 bytes .../icons/calculator@2x.png | Bin 0 -> 774 bytes .../src/name/abuchen/portfolio/ui/Images.java | 2 ++ .../portfolio/ui/views/TransactionsViewer.java | 17 ++++++++++++++--- 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 name.abuchen.portfolio.ui/icons/calculator.png create mode 100644 name.abuchen.portfolio.ui/icons/calculator@2x.png diff --git a/name.abuchen.portfolio.ui/icons/calculator.png b/name.abuchen.portfolio.ui/icons/calculator.png new file mode 100644 index 0000000000000000000000000000000000000000..8e9782cc7bd69c2d46cffd6fbba81a0dec6071b9 GIT binary patch literal 463 zcmV;=0WkiFP)Su2x%k1LKg{{eJR|9;n<5p@?aR=@O|HV^UWLh z#}I(`C*yWm>JhLFw4dMe;ds4? z@L_cJBHRH6!|}SY?j9hj&f2pod+&b?hHlGyKdH|uY#SgBu;<3Q-$8zzXXzR+r%F;2 zjCB)Wp$3Hl2f()=znfu04_F3{rv*OeS$frA+NOwnQq}JOd6r%n>u#Gms0~^I=Bt*! zR&QPGbvv%9olxMlvF=7yyTB_T0ZswqRbLwG2=2bvB=8pa$?P})(rI2Ou$E`(m9cJ_ z$7!qAorpZV>2`WG&{($uT!sRLh$H}U+&Wa%7>HV})-UhN(eC!f!<4a!suq<$V66Md z<0$Vxca(uId6xEKfGCoFS?ZChp7F$Isw$77Nc#Mb_yhlWpc{14THF8t002ovPDHLk FV1jq%)NKF& literal 0 HcmV?d00001 diff --git a/name.abuchen.portfolio.ui/icons/calculator@2x.png b/name.abuchen.portfolio.ui/icons/calculator@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..68d0d75686fe3616ea001a70ea18c1f8c83dda90 GIT binary patch literal 774 zcmV+h1Nr=kP)X%?YVp-YP(r8r3`#i=+*hrD|__+ET@CiFK>*j%NTMs-Zn)Vu_ z%O1HU5?@Hh>sOfhMz&Bb8PzH<_lx8(Q6Z-)ZA|YkOQRJG{qL7pdk>!}+9eC%ai4QRR-~n36GC2A&4T z`qpmpc6-E7hyfU*!@xb4C>C~pI{~lL`O2#c=G9ZPlS^R;()r3ePA%^bBj9$3b@xJy z@i2_*QdkGdz^<=t2jWgd0cqftyG{(4@(0@qD5Yj6%Ml0+r>DvB^#PEYoy;_q%6(whcN+YqvIBiB zWlZNQF(A=_@63JBPJnkz${jLtI3F?KWj79d2aep|%ixI47gaxjo3@>RrHB_%3sSR_ zb=*ItAl!>+%ikC37|KaT4a<^!5%~%rB74EuWd!>jXaGI#8?FU*k3FKQ{qxfkKY`D= zV(p`C+dtaG&QC<7S5-Z(GXnO3yTB;0;h*>{F@6jjd+qt@iGxnD3oz>AYAu0?lmS$A zE?cOMUB=GykS$cl+?R2a3vR%({N{X^_?`HhpJxB@2jA{5LJ{5%dH?_b07*qoM6N<$ Ef-4+hGXMYp literal 0 HcmV?d00001 diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Images.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Images.java index 3159cc27e7..fe378a57b8 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Images.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Images.java @@ -115,6 +115,8 @@ public enum Images ARROW_FORWARD("arrow_forward.png"), //$NON-NLS-1$ ARROW_BACK("arrow_back.png"), //$NON-NLS-1$ + + CALCULATOR("calculator.png"), //$NON-NLS-1$ // 3rd party logos diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java index cd83a0a023..9be10da01e 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java @@ -266,15 +266,26 @@ public Color getBackground(Object element) new DateTimeEditingSupport(Transaction.class, "dateTime").addListener(this).attachTo(column); //$NON-NLS-1$ support.addColumn(column); - column = new Column("1", Messages.ColumnTransactionType, SWT.None, 80); //$NON-NLS-1$ - column.setLabelProvider(new TransactionLabelProvider(t -> { + var typeLabelProvider = new TransactionLabelProvider(t -> { if (t instanceof PortfolioTransaction pt) return pt.getType().toString(); else if (t instanceof AccountTransaction at) return at.getType().toString(); else return null; - })); + }) + { + @Override + public Image getImage(Object element) + { + return element instanceof TransactionPair pair + && pair.getTransaction().getOriginalTransaction() != null ? Images.CALCULATOR.image() + : null; + } + }; + + column = new Column("1", Messages.ColumnTransactionType, SWT.None, 80); //$NON-NLS-1$ + column.setLabelProvider(typeLabelProvider); ColumnViewerSorter.createIgnoreCase(e -> { Transaction t = ((TransactionPair) e).getTransaction(); if (t instanceof PortfolioTransaction pt) From 864add399904268f6c97aa69b347993d78975163 Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Sun, 12 Oct 2025 16:08:08 +0200 Subject: [PATCH 5/7] Prevent editing of derived transaction in context menu --- .../ui/views/TransactionContextMenu.java | 82 +++++++++++++------ .../portfolio/model/TransactionPair.java | 13 +++ 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionContextMenu.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionContextMenu.java index 5d6ce71af1..1c4836403e 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionContextMenu.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionContextMenu.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; +import java.util.List; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; @@ -18,6 +19,7 @@ import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.PortfolioTransferEntry; +import name.abuchen.portfolio.model.Transaction; import name.abuchen.portfolio.model.TransactionPair; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.dialogs.transactions.AccountTransactionDialog; @@ -44,14 +46,27 @@ public TransactionContextMenu(AbstractFinanceView owner) public void menuAboutToShow(IMenuManager manager, boolean fullContextMenu, IStructuredSelection selection) { - if (selection.isEmpty() && fullContextMenu) + if (selection.isEmpty()) { - new SecurityContextMenu(owner).menuAboutToShow(manager, null, null); + if (fullContextMenu) + new SecurityContextMenu(owner).menuAboutToShow(manager, null, null); + + return; } - if (selection.size() == 1) + // convert the selection to a list of unwrapped transaction pairs to + // ensure that any downstream action does not work on the filtered + // client + + // editing, copying, deletion is only possible, if the transaction has + // not been created for calculation purposes + + @SuppressWarnings("unchecked") + var txs = selection.stream().map(o -> ((TransactionPair) o).unwrap()).toList(); + + if (txs.size() == 1) { - TransactionPair tx = (TransactionPair) selection.getFirstElement(); + var tx = txs.getFirst(); tx.withAccountTransaction().ifPresent(t -> fillContextMenuAccountTx(manager, fullContextMenu, t)); tx.withPortfolioTransaction().ifPresent(t -> fillContextMenuPortfolioTx(manager, fullContextMenu, t)); @@ -59,23 +74,23 @@ public void menuAboutToShow(IMenuManager manager, boolean fullContextMenu, IStru manager.add(new Separator()); } - if (!selection.isEmpty()) + if (fullContextMenu) { - if (fullContextMenu) - { - fillContextMenuAccountTxList(manager, selection); - fillContextMenuPortfolioTxList(manager, selection); - } + fillContextMenuAccountTxList(manager, txs); + fillContextMenuPortfolioTxList(manager, txs); + } - manager.add(new Separator()); + manager.add(new Separator()); - manager.add(new SimpleAction(Messages.MenuTransactionDelete, a -> { - for (Object tx : selection.toArray()) - ((TransactionPair) tx).deleteTransaction(owner.getClient()); + var deletableTx = txs.stream().filter(tx -> tx.getTransaction().getOriginalTransaction() == null).toList(); - owner.markDirty(); - })); - } + var deleteAction = new SimpleAction(Messages.MenuTransactionDelete, a -> { + for (var tx : deletableTx) + tx.deleteTransaction(owner.getClient()); + owner.markDirty(); + }); + deleteAction.setEnabled(!deletableTx.isEmpty()); + manager.add(deleteAction); } public void handleEditKey(KeyEvent e, IStructuredSelection selection) @@ -85,7 +100,12 @@ public void handleEditKey(KeyEvent e, IStructuredSelection selection) if (selection.isEmpty()) return; - TransactionPair tx = (TransactionPair) selection.getFirstElement(); + TransactionPair tx = ((TransactionPair) selection.getFirstElement()).unwrap(); + + // do not edit derived transactions + if (tx.getTransaction().getOriginalTransaction() != null) + return; + tx.withAccountTransaction().ifPresent(t -> createEditAccountTransactionAction(t).run()); tx.withPortfolioTransaction().ifPresent(t -> createEditPortfolioTransactionAction(t).run()); } @@ -94,24 +114,29 @@ public void handleEditKey(KeyEvent e, IStructuredSelection selection) if (selection.isEmpty()) return; - TransactionPair tx = (TransactionPair) selection.getFirstElement(); + TransactionPair tx = ((TransactionPair) selection.getFirstElement()).unwrap(); + + // do not edit derived transactions + if (tx.getTransaction().getOriginalTransaction() != null) + return; + tx.withAccountTransaction().ifPresent(t -> createCopyAccountTransactionAction(t).run()); tx.withPortfolioTransaction().ifPresent(t -> createCopyPortfolioTransactionAction(t).run()); } } - private void fillContextMenuAccountTxList(IMenuManager manager, IStructuredSelection selection) + private void fillContextMenuAccountTxList(IMenuManager manager, List> selection) { - @SuppressWarnings("unchecked") var accountTxPairs = selection.stream() // + .filter(p -> ((TransactionPair) p).getTransaction().getOriginalTransaction() == null) .filter(p -> ((TransactionPair) p).isAccountTransaction()) - .map(p -> ((TransactionPair) p)) // + .map(p -> p.withAccountTransaction().get()) // .toList(); if (accountTxPairs.size() != selection.size()) return; - var transfers = accountTxPairs.stream() + var transfers = accountTxPairs.stream().filter(p -> p.getTransaction().getOriginalTransaction() == null) .filter(p -> p.getTransaction().getType() == AccountTransaction.Type.TRANSFER_IN || p.getTransaction().getType() == AccountTransaction.Type.TRANSFER_OUT) .map(p -> p.getTransaction()).toList(); @@ -121,7 +146,7 @@ private void fillContextMenuAccountTxList(IMenuManager manager, IStructuredSelec manager.add(new ConvertTransferToDepositRemovalAction(owner.getClient(), transfers)); } - var dividends = accountTxPairs.stream() + var dividends = accountTxPairs.stream().filter(p -> p.getTransaction().getOriginalTransaction() == null) .filter(p -> p.getTransaction().getType() == AccountTransaction.Type.DIVIDENDS).toList(); if (dividends.size() == accountTxPairs.size()) @@ -130,14 +155,15 @@ private void fillContextMenuAccountTxList(IMenuManager manager, IStructuredSelec } } - private void fillContextMenuPortfolioTxList(IMenuManager manager, IStructuredSelection selection) + private void fillContextMenuPortfolioTxList(IMenuManager manager, List> selection) { Collection> txCollection = new ArrayList<>(selection.size()); Iterator it = selection.iterator(); while (it.hasNext()) { TransactionPair foo = (TransactionPair) it.next(); - foo.withPortfolioTransaction().ifPresent(txCollection::add); + if (foo.getTransaction().getOriginalTransaction() == null) + foo.withPortfolioTransaction().ifPresent(txCollection::add); } if (txCollection.size() != selection.size()) @@ -173,10 +199,12 @@ private void fillContextMenuAccountTx(IMenuManager manager, boolean fullContextM { Action action = createEditAccountTransactionAction(tx); action.setAccelerator(SWT.MOD1 | 'E'); + action.setEnabled(tx.getTransaction().getOriginalTransaction() == null); manager.add(action); Action duplicateAction = createCopyAccountTransactionAction(tx); duplicateAction.setAccelerator(SWT.MOD1 | 'D'); + duplicateAction.setEnabled(tx.getTransaction().getOriginalTransaction() == null); manager.add(duplicateAction); if (fullContextMenu) @@ -194,10 +222,12 @@ private void fillContextMenuPortfolioTx(IMenuManager manager, boolean fullContex Action editAction = createEditPortfolioTransactionAction(tx); editAction.setAccelerator(SWT.MOD1 | 'E'); + editAction.setEnabled(ptx.getOriginalTransaction() == null); manager.add(editAction); Action duplicateAction = createCopyPortfolioTransactionAction(tx); duplicateAction.setAccelerator(SWT.MOD1 | 'D'); + duplicateAction.setEnabled(ptx.getOriginalTransaction() == null); manager.add(duplicateAction); manager.add(new Separator()); diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/TransactionPair.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/TransactionPair.java index 8ec49b3594..7b5ccc4dba 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/TransactionPair.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/TransactionPair.java @@ -6,6 +6,8 @@ import java.util.Optional; import name.abuchen.portfolio.money.Values; +import name.abuchen.portfolio.snapshot.filter.ReadOnlyAccount; +import name.abuchen.portfolio.snapshot.filter.ReadOnlyPortfolio; /** * A pair of transaction owner (account or portfolio) and a transaction. @@ -37,6 +39,17 @@ public TransactionPair(TransactionOwner owner, T transaction) this.transaction = Objects.requireNonNull(transaction); } + @SuppressWarnings("unchecked") + public TransactionPair unwrap() + { + return switch (this.owner) + { + case ReadOnlyAccount a -> new TransactionPair<>((TransactionOwner) a.unwrap(), this.transaction); + case ReadOnlyPortfolio p -> new TransactionPair<>((TransactionOwner) p.unwrap(), this.transaction); + default -> this; + }; + } + public TransactionOwner getOwner() { return owner; From 2fed3f390a864d540dff5ed5826783cd28ac93ee Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Sun, 12 Oct 2025 16:27:24 +0200 Subject: [PATCH 6/7] Prevent editing of derived transaction in table --- .../ui/views/TransactionsViewer.java | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java index 9be10da01e..daffc309b6 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/TransactionsViewer.java @@ -263,7 +263,10 @@ public Color getBackground(Object element) }); ColumnViewerSorter.create(TransactionPair.BY_DATE).attachTo(column, SWT.DOWN); - new DateTimeEditingSupport(Transaction.class, "dateTime").addListener(this).attachTo(column); //$NON-NLS-1$ + new DateTimeEditingSupport(Transaction.class, "dateTime") //$NON-NLS-1$ + .setCanEditCheck( + t -> ((TransactionPair) t).getTransaction().getOriginalTransaction() == null) + .addListener(this).attachTo(column); support.addColumn(column); var typeLabelProvider = new TransactionLabelProvider(t -> { @@ -295,7 +298,16 @@ else if (t instanceof AccountTransaction at) else return null; }).attachTo(column); - new TransactionTypeEditingSupport(owner.getClient()).addListener(this).attachTo(column); + new TransactionTypeEditingSupport(owner.getClient()) + { + @Override + public boolean canEdit(Object element) + { + if (((TransactionPair) element).getTransaction().getOriginalTransaction() != null) + return false; + return super.canEdit(element); + } + }.addListener(this).attachTo(column); support.addColumn(column); column = new Column("2", Messages.ColumnSecurity, SWT.None, 250); //$NON-NLS-1$ @@ -350,12 +362,14 @@ public Color getBackground(Object element) }); ColumnViewerSorter.create(e -> ((TransactionPair) e).getTransaction().getShares()).attachTo(column); new ValueEditingSupport(Transaction.class, "shares", Values.Share) //$NON-NLS-1$ - .setCanEditCheck(e -> ((TransactionPair) e).getTransaction() instanceof PortfolioTransaction - || (((TransactionPair) e).getTransaction() instanceof AccountTransaction - && ((AccountTransaction) ((TransactionPair) e) - .getTransaction()) - .getType() == AccountTransaction.Type.DIVIDENDS)) - .addListener(this).attachTo(column); + .setCanEditCheck(e -> { + var tx = (TransactionPair) e; + if (tx.getTransaction().getOriginalTransaction() != null) + return false; + return tx.isPortfolioTransaction() || tx.withAccountTransaction().map( + ta -> ta.getTransaction().getType() == AccountTransaction.Type.DIVIDENDS) + .orElse(false); + }).addListener(this).attachTo(column); support.addColumn(column); column = new Column("4", Messages.ColumnQuote, SWT.RIGHT, 80); //$NON-NLS-1$ @@ -419,7 +433,15 @@ public String getText(Object element) } }); new TransactionOwnerListEditingSupport(owner.getClient(), TransactionOwnerListEditingSupport.EditMode.OWNER) - .addListener(this).attachTo(column); + { + @Override + public boolean canEdit(Object element) + { + if (((TransactionPair) element).getTransaction().getOriginalTransaction() != null) + return false; + return super.canEdit(element); + } + }.addListener(this).attachTo(column); ColumnViewerSorter.createIgnoreCase(e -> ((TransactionPair) e).getOwner().toString()).attachTo(column); support.addColumn(column); @@ -430,7 +452,16 @@ public String getText(Object element) ? t.getTransaction().getCrossEntry().getCrossOwner(t.getTransaction()) : null)); new TransactionOwnerListEditingSupport(owner.getClient(), - TransactionOwnerListEditingSupport.EditMode.CROSSOWNER).addListener(this).attachTo(column); + TransactionOwnerListEditingSupport.EditMode.CROSSOWNER) + { + @Override + public boolean canEdit(Object element) + { + if (((TransactionPair) element).getTransaction().getOriginalTransaction() != null) + return false; + return super.canEdit(element); + } + }.addListener(this).attachTo(column); ColumnViewerSorter.createIgnoreCase(e -> { Transaction t = ((TransactionPair) e).getTransaction(); return t.getCrossEntry() != null ? t.getCrossEntry().getCrossOwner(t).toString() : null; @@ -467,7 +498,9 @@ public String getToolTipText(Object e) } }); ColumnViewerSorter.createIgnoreCase(e -> ((TransactionPair) e).getTransaction().getNote()).attachTo(column); // $NON-NLS-1$ - new StringEditingSupport(Transaction.class, "note").addListener(this).attachTo(column); //$NON-NLS-1$ + new StringEditingSupport(Transaction.class, "note") //$NON-NLS-1$ + .setCanEditCheck(t -> ((Transaction) t).getOriginalTransaction() == null).addListener(this) + .addListener(this).attachTo(column); support.addColumn(column); column = new Column("source", Messages.ColumnSource, SWT.None, 200); //$NON-NLS-1$ From 674eeed7a71ce67dd44b2ef4689804e9e89179b7 Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Sun, 12 Oct 2025 16:53:31 +0200 Subject: [PATCH 7/7] Remember selection of PaymentsMatrixTab when updating model --- .../ui/views/payments/PaymentsMatrixTab.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/payments/PaymentsMatrixTab.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/payments/PaymentsMatrixTab.java index d71daf58e9..3a338f0501 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/payments/PaymentsMatrixTab.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/payments/PaymentsMatrixTab.java @@ -25,6 +25,7 @@ import org.eclipse.jface.viewers.ColumnPixelData; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.window.ToolTip; @@ -316,6 +317,8 @@ protected void updateColumns(TableViewer records, TableColumnLayout layout) { try { + var selection = (Line) records.getStructuredSelection().getFirstElement(); + // first add, then remove columns // (otherwise rendering of first column is broken) records.getTable().setRedraw(false); @@ -333,6 +336,16 @@ protected void updateColumns(TableViewer records, TableColumnLayout layout) for (TableColumn c : records.getTable().getColumns()) c.pack(); + + if (selection != null) + { + model.getLines().stream().filter(l -> { + var left = l.getVehicle() instanceof ReadOnlyAccount a ? a.unwrap() : l.getVehicle(); + var right = selection.getVehicle() instanceof ReadOnlyAccount a ? a.unwrap() + : selection.getVehicle(); + return left == right; + }).findAny().ifPresent(element -> records.setSelection(new StructuredSelection(element))); + } } finally {