package name.abuchen.portfolio.ui.dialogs.transactions; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; import java.util.Optional; import org.eclipse.core.databinding.validation.ValidationStatus; import org.eclipse.core.runtime.IStatus; import com.ibm.icu.text.MessageFormat; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.AccountTransferEntry; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.Transaction; import name.abuchen.portfolio.model.TransactionOwner; import name.abuchen.portfolio.money.ExchangeRate; import name.abuchen.portfolio.money.ExchangeRateTimeSeries; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.ui.Messages; public class AccountTransferModel extends AbstractModel { public enum Properties { sourceAccount, targetAccount, date, fxAmount, exchangeRate, inverseExchangeRate, amount, // note, sourceAccountCurrency, targetAccountCurrency, exchangeRateCurrencies, // inverseExchangeRateCurrencies, calculationStatus; } private final Client client; private AccountTransferEntry source; private Account sourceAccount; private Account targetAccount; private LocalDate date = LocalDate.now(); private long fxAmount; private BigDecimal exchangeRate = BigDecimal.ONE; private long amount; private String note; private IStatus calculationStatus = ValidationStatus.ok(); public AccountTransferModel(Client client) { this.client = client; } @Override public String getHeading() { return Messages.LabelTransfer; } @Override public void applyChanges() { if (sourceAccount == null) throw new UnsupportedOperationException(Messages.MsgAccountFromMissing); if (targetAccount == null) throw new UnsupportedOperationException(Messages.MsgAccountToMissing); AccountTransferEntry t; if (source != null && sourceAccount.equals(source.getOwner(source.getSourceTransaction())) && targetAccount.equals(source.getOwner(source.getTargetTransaction()))) { // transaction stays in same accounts t = source; } else { if (source != null) { @SuppressWarnings("unchecked") TransactionOwner<Transaction> owner = (TransactionOwner<Transaction>) source .getOwner(source.getSourceTransaction()); owner.deleteTransaction(source.getSourceTransaction(), client); source = null; } t = new AccountTransferEntry(sourceAccount, targetAccount); t.insert(); } t.setDate(date); t.setNote(note); // if source and target account have the same currencies, no forex data // needs to be stored AccountTransaction sourceTransaction = t.getSourceTransaction(); sourceTransaction.setCurrencyCode(sourceAccount.getCurrencyCode()); t.getTargetTransaction().setCurrencyCode(targetAccount.getCurrencyCode()); sourceTransaction.clearUnits(); if (sourceAccount.getCurrencyCode().equals(targetAccount.getCurrencyCode())) { sourceTransaction.setAmount(amount); t.getTargetTransaction().setAmount(amount); } else { // TODO improve naming of fields: the source amount is called // 'fxAmount' while the target amount is just called 'amount' but // then the source account holds the 'forex' which is switched sourceTransaction.setAmount(fxAmount); t.getTargetTransaction().setAmount(amount); Transaction.Unit forex = new Transaction.Unit(Transaction.Unit.Type.GROSS_VALUE, // Money.of(sourceAccount.getCurrencyCode(), fxAmount), // Money.of(targetAccount.getCurrencyCode(), amount), // getInverseExchangeRate()); sourceTransaction.addUnit(forex); } } @Override public void resetToNewTransaction() { this.source = null; setFxAmount(0); setAmount(0); setNote(null); } public void setSource(AccountTransferEntry entry) { this.source = entry; this.sourceAccount = (Account) entry.getOwner(entry.getSourceTransaction()); this.targetAccount = (Account) entry.getOwner(entry.getTargetTransaction()); this.date = entry.getSourceTransaction().getDate(); this.note = entry.getSourceTransaction().getNote(); this.fxAmount = entry.getSourceTransaction().getAmount(); this.amount = entry.getTargetTransaction().getAmount(); Optional<Transaction.Unit> forex = entry.getSourceTransaction().getUnit(Transaction.Unit.Type.GROSS_VALUE); if (forex.isPresent() && forex.get().getAmount().getCurrencyCode().equals(sourceAccount.getCurrencyCode()) && forex.get().getForex().getCurrencyCode().equals(targetAccount.getCurrencyCode())) { this.exchangeRate = ExchangeRate.inverse(forex.get().getExchangeRate()); } else { this.exchangeRate = BigDecimal.ONE; } } @Override public IStatus getCalculationStatus() { return calculationStatus; } /** * Check whether calculation works out. */ private IStatus calculateStatus() { // check whether converted amount is in range long upper = Math.round(fxAmount * exchangeRate.add(BigDecimal.valueOf(0.0001)).doubleValue()); long lower = Math.round(fxAmount * exchangeRate.add(BigDecimal.valueOf(-0.0001)).doubleValue()); if (amount < lower || amount > upper) return ValidationStatus.error(Messages.MsgErrorConvertedAmount); if (amount == 0L || fxAmount == 0L) return ValidationStatus.error(MessageFormat.format(Messages.MsgDialogInputRequired, Messages.ColumnTotal)); return ValidationStatus.ok(); } public Account getSourceAccount() { return sourceAccount; } public void setSourceAccount(Account account) { String oldCurrencyCode = getSourceAccountCurrency(); String oldExchangeRateCurrencies = getExchangeRateCurrencies(); String oldInverseExchangeRateCurrencies = getInverseExchangeRateCurrencies(); firePropertyChange(Properties.sourceAccount.name(), this.sourceAccount, this.sourceAccount = account); firePropertyChange(Properties.sourceAccountCurrency.name(), oldCurrencyCode, getSourceAccountCurrency()); firePropertyChange(Properties.exchangeRateCurrencies.name(), oldExchangeRateCurrencies, getExchangeRateCurrencies()); firePropertyChange(Properties.inverseExchangeRateCurrencies.name(), oldInverseExchangeRateCurrencies, getInverseExchangeRateCurrencies()); updateExchangeRate(); } public Account getTargetAccount() { return targetAccount; } public void setTargetAccount(Account account) { String oldCurrencyCode = getTargetAccountCurrency(); String oldExchangeRateCurrencies = getExchangeRateCurrencies(); String oldInverseExchangeRateCurrencies = getInverseExchangeRateCurrencies(); firePropertyChange(Properties.targetAccount.name(), this.targetAccount, this.targetAccount = account); firePropertyChange(Properties.targetAccountCurrency.name(), oldCurrencyCode, getTargetAccountCurrency()); firePropertyChange(Properties.exchangeRateCurrencies.name(), oldExchangeRateCurrencies, getExchangeRateCurrencies()); firePropertyChange(Properties.inverseExchangeRateCurrencies.name(), oldInverseExchangeRateCurrencies, getInverseExchangeRateCurrencies()); updateExchangeRate(); } private void updateExchangeRate() { if (getSourceAccountCurrency().equals(getTargetAccountCurrency())) { setExchangeRate(BigDecimal.ONE); } else { ExchangeRateTimeSeries series = getExchangeRateProviderFactory() // .getTimeSeries(getSourceAccountCurrency(), getTargetAccountCurrency()); if (series != null) setExchangeRate(series.lookupRate(date).orElse(new ExchangeRate(date, BigDecimal.ONE)).getValue()); else setExchangeRate(BigDecimal.ONE); } } public LocalDate getDate() { return date; } public void setDate(LocalDate date) { firePropertyChange(Properties.date.name(), this.date, this.date = date); updateExchangeRate(); } public long getFxAmount() { return fxAmount; } public void setFxAmount(long foreignCurrencyAmount) { firePropertyChange(Properties.fxAmount.name(), this.fxAmount, this.fxAmount = foreignCurrencyAmount); triggerAmount(Math.round(exchangeRate.doubleValue() * foreignCurrencyAmount)); firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus, this.calculationStatus = calculateStatus()); } public BigDecimal getExchangeRate() { return exchangeRate; } public void setExchangeRate(BigDecimal exchangeRate) { BigDecimal newRate = exchangeRate == null ? BigDecimal.ZERO : exchangeRate; BigDecimal oldInverseRate = getInverseExchangeRate(); firePropertyChange(Properties.exchangeRate.name(), this.exchangeRate, this.exchangeRate = newRate); firePropertyChange(Properties.inverseExchangeRate.name(), oldInverseRate, getInverseExchangeRate()); triggerAmount(Math.round(newRate.doubleValue() * fxAmount)); firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus, this.calculationStatus = calculateStatus()); } public BigDecimal getInverseExchangeRate() { return BigDecimal.ONE.divide(exchangeRate, 10, BigDecimal.ROUND_HALF_DOWN); } public void setInverseExchangeRate(BigDecimal rate) { setExchangeRate(BigDecimal.ONE.divide(rate, 10, BigDecimal.ROUND_HALF_DOWN)); } public long getAmount() { return amount; } public void setAmount(long amount) { triggerAmount(amount); if (fxAmount != 0) { BigDecimal newExchangeRate = BigDecimal.valueOf(amount).divide(BigDecimal.valueOf(fxAmount), 10, RoundingMode.HALF_UP); BigDecimal oldInverseRate = getInverseExchangeRate(); firePropertyChange(Properties.exchangeRate.name(), this.exchangeRate, this.exchangeRate = newExchangeRate); firePropertyChange(Properties.inverseExchangeRate.name(), oldInverseRate, getInverseExchangeRate()); } firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus, this.calculationStatus = calculateStatus()); } public void triggerAmount(long amount) { firePropertyChange(Properties.amount.name(), this.amount, this.amount = amount); } public String getNote() { return note; } public void setNote(String note) { firePropertyChange(Properties.note.name(), this.note, this.note = note); } public String getSourceAccountCurrency() { return sourceAccount != null ? sourceAccount.getCurrencyCode() : ""; //$NON-NLS-1$ } public String getTargetAccountCurrency() { return targetAccount != null ? targetAccount.getCurrencyCode() : ""; //$NON-NLS-1$ } /** * Returns exchange rate label in direct (price) notation. */ public String getExchangeRateCurrencies() { return String.format("%s/%s", getSourceAccountCurrency(), getTargetAccountCurrency()); //$NON-NLS-1$ } /** * Returns exchange rate label in indirect (quantity) notation. */ public String getInverseExchangeRateCurrencies() { return String.format("%s/%s", getTargetAccountCurrency(), getSourceAccountCurrency()); //$NON-NLS-1$ } }