package name.abuchen.portfolio.ui.dialogs.transactions; import static name.abuchen.portfolio.ui.util.FormDataFactory.startingWith; import static name.abuchen.portfolio.ui.util.SWTHelper.amountWidth; import static name.abuchen.portfolio.ui.util.SWTHelper.currencyWidth; import static name.abuchen.portfolio.ui.util.SWTHelper.widest; import java.text.MessageFormat; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.e4.core.di.extensions.Preference; import org.eclipse.e4.ui.services.IServiceConstants; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.databinding.swt.WidgetProperties; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.CurrencyConverterImpl; import name.abuchen.portfolio.money.ExchangeRateProviderFactory; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.snapshot.ClientSnapshot; import name.abuchen.portfolio.snapshot.PortfolioSnapshot; import name.abuchen.portfolio.snapshot.SecurityPosition; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.UIConstants; import name.abuchen.portfolio.ui.dialogs.transactions.AccountTransactionModel.Properties; import name.abuchen.portfolio.ui.util.DateTimePicker; import name.abuchen.portfolio.ui.util.FormDataFactory; import name.abuchen.portfolio.ui.util.LabelOnly; import name.abuchen.portfolio.ui.util.SimpleDateTimeSelectionProperty; @SuppressWarnings("restriction") public class AccountTransactionDialog extends AbstractTransactionDialog // NOSONAR { @Inject private Client client; @Preference(value = UIConstants.Preferences.USE_INDIRECT_QUOTATION) @Inject private boolean useIndirectQuotation = false; private Menu contextMenu; @Inject public AccountTransactionDialog(@Named(IServiceConstants.ACTIVE_SHELL) Shell parentShell) { super(parentShell); } @PostConstruct private void createModel(ExchangeRateProviderFactory factory, AccountTransaction.Type type) // NOSONAR { AccountTransactionModel m = new AccountTransactionModel(client, type); m.setExchangeRateProviderFactory(factory); setModel(m); // set account only if exactly one exists // (otherwise force user to choose) List<Account> activeAccounts = client.getActiveAccounts(); if (activeAccounts.size() == 1) m.setAccount(activeAccounts.get(0)); } private AccountTransactionModel model() { return (AccountTransactionModel) this.model; } @Override protected void createFormElements(Composite editArea) // NOSONAR { // // input elements // // security ComboInput securities = null; if (model().supportsSecurity()) securities = setupSecurities(editArea); // account ComboInput accounts = new ComboInput(editArea, Messages.ColumnAccount); accounts.value.setInput(including(client.getActiveAccounts(), model().getAccount())); accounts.bindValue(Properties.account.name(), Messages.MsgMissingAccount); accounts.bindCurrency(Properties.accountCurrencyCode.name()); // date Label lblDate = new Label(editArea, SWT.RIGHT); lblDate.setText(Messages.ColumnDate); DateTimePicker valueDate = new DateTimePicker(editArea); context.bindValue(new SimpleDateTimeSelectionProperty().observe(valueDate.getControl()), BeanProperties.value(Properties.date.name()).observe(model)); // shares Input shares = new Input(editArea, Messages.ColumnShares); shares.bindValue(Properties.shares.name(), Messages.ColumnShares, Values.Share, false); shares.setVisible(model().supportsShares()); Button btnShares = new Button(editArea, SWT.ARROW | SWT.DOWN); btnShares.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { showSharesContextMenu(); } }); btnShares.setVisible(model().supportsShares()); editArea.addDisposeListener(e -> AccountTransactionDialog.this.widgetDisposed()); Input dividendAmount = new Input(editArea, Messages.LabelDividendPerShare); dividendAmount.bindBigDecimal(Properties.dividendAmount.name(), "#,##0.0000"); //$NON-NLS-1$ dividendAmount.bindCurrency(Properties.fxCurrencyCode.name()); dividendAmount.setVisible(model().supportsShares()); // other input fields String totalLabel = model().supportsTaxUnits() ? Messages.ColumnGrossValue : getTotalLabel(); Input fxGrossAmount = new Input(editArea, totalLabel); fxGrossAmount.bindValue(Properties.fxGrossAmount.name(), totalLabel, Values.Amount, true); fxGrossAmount.bindCurrency(Properties.fxCurrencyCode.name()); Input exchangeRate = new Input(editArea, useIndirectQuotation ? "/ " : "x "); //$NON-NLS-1$ //$NON-NLS-2$ exchangeRate.bindBigDecimal( useIndirectQuotation ? Properties.inverseExchangeRate.name() : Properties.exchangeRate.name(), Values.ExchangeRate.pattern()); exchangeRate.bindCurrency(useIndirectQuotation ? Properties.inverseExchangeRateCurrencies.name() : Properties.exchangeRateCurrencies.name()); model().addPropertyChangeListener(Properties.exchangeRate.name(), e -> exchangeRate.value.setToolTipText(AbstractModel.createCurrencyToolTip( model().getExchangeRate(), model().getAccountCurrencyCode(), model().getSecurityCurrencyCode()))); Input grossAmount = new Input(editArea, "="); //$NON-NLS-1$ grossAmount.bindValue(Properties.grossAmount.name(), totalLabel, Values.Amount, true); grossAmount.bindCurrency(Properties.accountCurrencyCode.name()); // taxes Label plusForexTaxes = new Label(editArea, SWT.NONE); plusForexTaxes.setText("+"); //$NON-NLS-1$ plusForexTaxes.setVisible(model().supportsTaxUnits()); Input forexTaxes = new Input(editArea, Messages.ColumnTaxes); forexTaxes.bindValue(Properties.fxTaxes.name(), Messages.ColumnTaxes, Values.Amount, false); forexTaxes.bindCurrency(Properties.fxCurrencyCode.name()); forexTaxes.setVisible(model().supportsTaxUnits()); Input taxes = new Input(editArea, Messages.ColumnTaxes); taxes.bindValue(Properties.taxes.name(), Messages.ColumnTaxes, Values.Amount, false); taxes.bindCurrency(Properties.accountCurrencyCode.name()); taxes.setVisible(model().supportsTaxUnits()); taxes.label.setVisible(false); // will only show if no fx available // total Input total = new Input(editArea, getTotalLabel()); total.bindValue(Properties.total.name(), Messages.ColumnTotal, Values.Amount, false); total.bindCurrency(Properties.accountCurrencyCode.name()); total.setVisible(model().supportsTaxUnits()); // note Label lblNote = new Label(editArea, SWT.LEFT); lblNote.setText(Messages.ColumnNote); Text valueNote = new Text(editArea, SWT.BORDER); context.bindValue(WidgetProperties.text(SWT.Modify).observe(valueNote), BeanProperties.value(Properties.note.name()).observe(model)); // // form layout // int widest = widest(securities != null ? securities.label : null, accounts.label, lblDate, shares.label, fxGrossAmount.label, lblNote); FormDataFactory forms; if (securities != null) { forms = startingWith(securities.value.getControl(), securities.label).suffix(securities.currency) .thenBelow(accounts.value.getControl()).label(accounts.label).suffix(accounts.currency); startingWith(securities.label).width(widest); } else { forms = startingWith(accounts.value.getControl(), accounts.label).suffix(accounts.currency); startingWith(accounts.label).width(widest); } int amountWidth = amountWidth(grossAmount.value); int currencyWidth = currencyWidth(fxGrossAmount.currency); // date // shares forms = forms.thenBelow(valueDate.getControl()).label(lblDate) // // shares [- amount per share] .thenBelow(shares.value).width(amountWidth).label(shares.label).suffix(btnShares) // // fxAmount - exchange rate - amount .thenBelow(fxGrossAmount.value).width(amountWidth).label(fxGrossAmount.label) // .thenRight(fxGrossAmount.currency).width(currencyWidth) // .thenRight(exchangeRate.label) // .thenRight(exchangeRate.value).width(amountWidth) // .thenRight(exchangeRate.currency).width(amountWidth) // .thenRight(grossAmount.label) // .thenRight(grossAmount.value).width(amountWidth) // .thenRight(grossAmount.currency).width(currencyWidth); if (model().supportsShares()) { // shares [- amount per share] startingWith(btnShares).thenRight(dividendAmount.label) // .thenRight(dividendAmount.value).width(amountWidth) // .thenRight(dividendAmount.currency).width(currencyWidth); // } // forexTaxes - taxes if (model().supportsTaxUnits()) { startingWith(grossAmount.value) // .thenBelow(taxes.value).width(amountWidth).label(taxes.label).suffix(taxes.currency) // .thenBelow(total.value).width(amountWidth).label(total.label).thenRight(total.currency) .width(currencyWidth); startingWith(taxes.value).thenLeft(plusForexTaxes).thenLeft(forexTaxes.currency).width(currencyWidth) .thenLeft(forexTaxes.value).width(amountWidth).thenLeft(forexTaxes.label); forms = startingWith(total.value); } // note forms.thenBelow(valueNote).left(accounts.value.getControl()).right(grossAmount.value).label(lblNote); // // hide / show exchange rate if necessary // model.addPropertyChangeListener(Properties.exchangeRateCurrencies.name(), event -> { // NOSONAR String securityCurrency = model().getSecurityCurrencyCode(); String accountCurrency = model().getAccountCurrencyCode(); // make exchange rate visible if both are set but different boolean isFxVisible = securityCurrency.length() > 0 && accountCurrency.length() > 0 && !securityCurrency.equals(accountCurrency); exchangeRate.setVisible(isFxVisible); grossAmount.setVisible(isFxVisible); forexTaxes.setVisible(isFxVisible && model().supportsShares()); plusForexTaxes.setVisible(isFxVisible && model().supportsShares()); taxes.label.setVisible(!isFxVisible && model().supportsShares()); // in case fx taxes have been entered if (!isFxVisible) model().setFxTaxes(0); // move input fields to have a nicer layout if (isFxVisible) startingWith(grossAmount.value).thenBelow(taxes.value); else startingWith(fxGrossAmount.value).thenBelow(taxes.value); editArea.layout(); }); WarningMessages warnings = new WarningMessages(this); warnings.add(() -> model().getDate().isAfter(LocalDate.now()) ? Messages.MsgDateIsInTheFuture : null); model.addPropertyChangeListener(Properties.date.name(), e -> warnings.check()); model.firePropertyChange(Properties.exchangeRateCurrencies.name(), "", model().getExchangeRateCurrencies()); //$NON-NLS-1$ } private ComboInput setupSecurities(Composite editArea) { List<Security> activeSecurities = new ArrayList<>(); activeSecurities.addAll(including(client.getActiveSecurities(), model().getSecurity())); // add empty security only if it has not been added previously // --> happens when editing an existing transaction if (model().supportsOptionalSecurity() && !activeSecurities.contains(AccountTransactionModel.EMPTY_SECURITY)) activeSecurities.add(0, AccountTransactionModel.EMPTY_SECURITY); ComboInput securities = new ComboInput(editArea, Messages.ColumnSecurity); securities.value.setInput(activeSecurities); securities.bindValue(Properties.security.name(), Messages.MsgMissingSecurity); securities.bindCurrency(Properties.securityCurrencyCode.name()); return securities; } private void showSharesContextMenu() { if (contextMenu == null) { MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(this::sharesMenuAboutToShow); contextMenu = menuMgr.createContextMenu(getShell()); } contextMenu.setVisible(true); } private void sharesMenuAboutToShow(IMenuManager manager) // NOSONAR { manager.add(new LabelOnly(Messages.DividendsDialogTitleShares)); CurrencyConverter converter = new CurrencyConverterImpl(model.getExchangeRateProviderFactory(), client.getBaseCurrency()); ClientSnapshot snapshot = ClientSnapshot.create(client, converter, model().getDate()); if (snapshot != null && model().getSecurity() != null) { PortfolioSnapshot jointPortfolio = snapshot.getJointPortfolio(); addAction(manager, jointPortfolio, Messages.ColumnSharesOwned); List<PortfolioSnapshot> list = snapshot.getPortfolios(); if (list.size() > 1) { for (PortfolioSnapshot ps : list) addAction(manager, ps, ps.getSource().getName()); } } manager.add(new Action(Messages.DividendsDialogLabelSpecialDistribution) { @Override public void run() { model().setShares(0); } }); } private void addAction(IMenuManager manager, PortfolioSnapshot portfolio, final String label) { final SecurityPosition position = portfolio.getPositionsBySecurity().get(model().getSecurity()); if (position != null) { Action action = new Action(MessageFormat.format(Messages.DividendsDialogLabelPortfolioSharesHeld, Values.Share.format(position.getShares()), label, Values.Date.format(portfolio.getTime()))) { @Override public void run() { model().setShares(position.getShares()); } }; manager.add(action); } } private void widgetDisposed() { if (contextMenu != null && !contextMenu.isDisposed()) contextMenu.dispose(); } private String getTotalLabel() // NOSONAR { switch (model().getType()) { case TAXES: case FEES: case INTEREST_CHARGE: case REMOVAL: return Messages.ColumnDebitNote; case INTEREST: case TAX_REFUND: case DIVIDENDS: case DEPOSIT: case FEES_REFUND: return Messages.ColumnCreditNote; case BUY: case SELL: case TRANSFER_IN: case TRANSFER_OUT: default: throw new UnsupportedOperationException(); } } @Override public void setAccount(Account account) { model().setAccount(account); } @Override public void setSecurity(Security security) { model().setSecurity(security); } public void setTransaction(Account account, AccountTransaction transaction) { model().setSource(account, transaction); } }