package name.abuchen.portfolio.ui.dialogs.transactions;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
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.Client;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Transaction;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.CurrencyConverterImpl;
import name.abuchen.portfolio.money.ExchangeRate;
import name.abuchen.portfolio.money.ExchangeRateTimeSeries;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.snapshot.SecurityPosition;
import name.abuchen.portfolio.ui.Messages;
public class AccountTransactionModel extends AbstractModel
{
public enum Properties
{
security, account, date, shares, fxGrossAmount, dividendAmount, exchangeRate, inverseExchangeRate, grossAmount, // NOSONAR
fxTaxes, taxes, total, note, exchangeRateCurrencies, inverseExchangeRateCurrencies, // NOSONAR
accountCurrencyCode, securityCurrencyCode, fxCurrencyCode, calculationStatus; // NOSONAR
}
public static final Security EMPTY_SECURITY = new Security("", ""); //$NON-NLS-1$ //$NON-NLS-2$
private final Client client;
private AccountTransaction.Type type;
private Account sourceAccount;
private AccountTransaction sourceTransaction;
private Security security;
private Account account;
private LocalDate date = LocalDate.now();
private long shares;
private long fxGrossAmount;
private BigDecimal dividendAmount = BigDecimal.ZERO;
private BigDecimal exchangeRate = BigDecimal.ONE;
private long grossAmount;
private long fxTaxes;
private long taxes;
private long total;
private String note;
private IStatus calculationStatus = ValidationStatus.ok();
public AccountTransactionModel(Client client, AccountTransaction.Type type)
{
this.client = client;
this.type = type;
checkType();
}
@Override
public String getHeading()
{
return type.toString();
}
private void checkType()
{
switch (type)
{
case DEPOSIT:
case REMOVAL:
case FEES:
case FEES_REFUND:
case TAXES:
case TAX_REFUND:
case INTEREST:
case INTEREST_CHARGE:
case DIVIDENDS:
return;
case BUY:
case SELL:
case TRANSFER_IN:
case TRANSFER_OUT:
default:
throw new UnsupportedOperationException();
}
}
@Override
public void applyChanges()
{
if (security == null && supportsSecurity() && !supportsOptionalSecurity())
throw new UnsupportedOperationException(Messages.MsgMissingSecurity);
if (account == null)
throw new UnsupportedOperationException(Messages.MsgMissingAccount);
AccountTransaction t;
if (sourceTransaction != null && sourceAccount.equals(account))
{
// transactions stays in same account
t = sourceTransaction;
}
else
{
if (sourceTransaction != null)
{
sourceAccount.deleteTransaction(sourceTransaction, client);
sourceTransaction = null;
sourceAccount = null;
}
t = new AccountTransaction();
account.addTransaction(t);
}
t.setDate(date);
t.setSecurity(!EMPTY_SECURITY.equals(security) ? security : null);
t.setShares(supportsShares() ? shares : 0);
t.setAmount(total);
t.setType(type);
t.setNote(note);
t.setCurrencyCode(getAccountCurrencyCode());
t.clearUnits();
if (taxes != 0)
t.addUnit(new Transaction.Unit(Transaction.Unit.Type.TAX, Money.of(getAccountCurrencyCode(), taxes)));
String fxCurrencyCode = getFxCurrencyCode();
if (!fxCurrencyCode.equals(account.getCurrencyCode()))
{
Transaction.Unit forex = new Transaction.Unit(Transaction.Unit.Type.GROSS_VALUE, //
Money.of(getAccountCurrencyCode(), grossAmount), //
Money.of(getSecurityCurrencyCode(), fxGrossAmount), //
getExchangeRate());
t.addUnit(forex);
if (fxTaxes != 0)
t.addUnit(new Transaction.Unit(Transaction.Unit.Type.TAX, //
Money.of(getAccountCurrencyCode(), Math.round(fxTaxes * exchangeRate.doubleValue())), //
Money.of(getSecurityCurrencyCode(), fxTaxes), //
exchangeRate));
}
}
@Override
public void resetToNewTransaction()
{
this.sourceAccount = null;
this.sourceTransaction = null;
setShares(0);
setFxGrossAmount(0);
setDividendAmount(BigDecimal.ZERO);
setGrossAmount(0);
setTaxes(0);
setFxTaxes(0);
setNote(null);
}
public boolean supportsShares()
{
return type == AccountTransaction.Type.DIVIDENDS;
}
public boolean supportsSecurity()
{
return type == AccountTransaction.Type.DIVIDENDS || type == AccountTransaction.Type.TAX_REFUND;
}
public boolean supportsOptionalSecurity()
{
return type == AccountTransaction.Type.TAX_REFUND;
}
public boolean supportsTaxUnits()
{
return type == AccountTransaction.Type.DIVIDENDS;
}
public void setSource(Account account, AccountTransaction transaction)
{
this.sourceAccount = account;
this.sourceTransaction = transaction;
this.security = transaction.getSecurity();
if (this.security == null && supportsOptionalSecurity())
this.security = EMPTY_SECURITY;
this.account = account;
this.date = transaction.getDate();
this.shares = transaction.getShares();
this.total = transaction.getAmount();
// both will be overwritten if forex data exists
this.exchangeRate = BigDecimal.ONE;
this.taxes = 0;
this.fxTaxes = 0;
transaction.getUnits().forEach(unit -> {
switch (unit.getType())
{
case GROSS_VALUE:
this.exchangeRate = unit.getExchangeRate();
this.grossAmount = unit.getAmount().getAmount();
this.fxGrossAmount = unit.getForex().getAmount();
break;
case TAX:
if (unit.getForex() != null)
this.fxTaxes += unit.getForex().getAmount();
else
this.taxes += unit.getAmount().getAmount();
break;
default:
throw new UnsupportedOperationException();
}
});
this.grossAmount = calculateGrossAmount4Total();
// in case units have to forex gross value
if (exchangeRate.equals(BigDecimal.ONE))
this.fxGrossAmount = grossAmount;
this.dividendAmount = calculateDividendAmount();
this.note = transaction.getNote();
}
@Override
public IStatus getCalculationStatus()
{
return calculationStatus;
}
/**
* Due to the limited precision of the exchange rate (4 digits), the amount
* is checked against a range.
*/
private IStatus calculateStatus()
{
// check whether converted amount is in range
long upper = Math.round(fxGrossAmount * exchangeRate.add(BigDecimal.valueOf(0.0001)).doubleValue());
long lower = Math.round(fxGrossAmount * exchangeRate.add(BigDecimal.valueOf(-0.0001)).doubleValue());
if (grossAmount < lower || grossAmount > upper)
return ValidationStatus.error(Messages.MsgErrorConvertedAmount);
if (grossAmount == 0L)
return ValidationStatus.error(MessageFormat.format(Messages.MsgDialogInputRequired, Messages.ColumnTotal));
return ValidationStatus.ok();
}
public Account getAccount()
{
return account;
}
public void setAccount(Account account)
{
String oldCurrencyCode = getAccountCurrencyCode();
String oldFxCurrencyCode = getFxCurrencyCode();
String oldExchangeRateCurrencies = getExchangeRateCurrencies();
String oldInverseExchangeRateCurrencies = getInverseExchangeRateCurrencies();
firePropertyChange(Properties.account.name(), this.account, this.account = account);
firePropertyChange(Properties.accountCurrencyCode.name(), oldCurrencyCode, getAccountCurrencyCode());
firePropertyChange(Properties.fxCurrencyCode.name(), oldFxCurrencyCode, getFxCurrencyCode());
firePropertyChange(Properties.exchangeRateCurrencies.name(), oldExchangeRateCurrencies,
getExchangeRateCurrencies());
firePropertyChange(Properties.inverseExchangeRateCurrencies.name(), oldInverseExchangeRateCurrencies,
getInverseExchangeRateCurrencies());
updateExchangeRate();
}
public Security getSecurity()
{
return security;
}
public void setSecurity(Security security)
{
if (!supportsSecurity())
return;
String oldCurrencyCode = getSecurityCurrencyCode();
String oldFxCurrencyCode = getFxCurrencyCode();
String oldExchangeRateCurrencies = getExchangeRateCurrencies();
String oldInverseExchangeRateCurrencies = getInverseExchangeRateCurrencies();
firePropertyChange(Properties.security.name(), this.security, this.security = security);
firePropertyChange(Properties.securityCurrencyCode.name(), oldCurrencyCode, getSecurityCurrencyCode());
firePropertyChange(Properties.fxCurrencyCode.name(), oldFxCurrencyCode, getFxCurrencyCode());
firePropertyChange(Properties.exchangeRateCurrencies.name(), oldExchangeRateCurrencies,
getExchangeRateCurrencies());
firePropertyChange(Properties.inverseExchangeRateCurrencies.name(), oldInverseExchangeRateCurrencies,
getInverseExchangeRateCurrencies());
updateExchangeRate();
updateShares();
}
private void updateExchangeRate()
{
if (getAccountCurrencyCode().equals(getSecurityCurrencyCode()))
{
setExchangeRate(BigDecimal.ONE);
}
else if (!getSecurityCurrencyCode().isEmpty())
{
ExchangeRateTimeSeries series = getExchangeRateProviderFactory() //
.getTimeSeries(getSecurityCurrencyCode(), getAccountCurrencyCode());
if (series != null)
setExchangeRate(series.lookupRate(date).orElse(new ExchangeRate(date, BigDecimal.ONE)).getValue());
else
setExchangeRate(BigDecimal.ONE);
}
}
private void updateShares()
{
if (!supportsShares() || security == null)
return;
CurrencyConverter converter = new CurrencyConverterImpl(getExchangeRateProviderFactory(),
client.getBaseCurrency());
ClientSnapshot snapshot = ClientSnapshot.create(client, converter, date);
SecurityPosition p = snapshot.getJointPortfolio().getPositionsBySecurity().get(security);
setShares(p != null ? p.getShares() : 0);
}
public LocalDate getDate()
{
return date;
}
public void setDate(LocalDate date)
{
firePropertyChange(Properties.date.name(), this.date, this.date = date);
updateShares();
updateExchangeRate();
}
public long getShares()
{
return shares;
}
public void setShares(long shares)
{
firePropertyChange(Properties.shares.name(), this.shares, this.shares = shares);
firePropertyChange(Properties.dividendAmount.name(), this.dividendAmount,
this.dividendAmount = calculateDividendAmount());
}
public long getFxGrossAmount()
{
return fxGrossAmount;
}
public void setFxGrossAmount(long foreignCurrencyAmount)
{
firePropertyChange(Properties.fxGrossAmount.name(), this.fxGrossAmount,
this.fxGrossAmount = foreignCurrencyAmount);
triggerGrossAmount(Math.round(exchangeRate.doubleValue() * foreignCurrencyAmount));
firePropertyChange(Properties.dividendAmount.name(), this.dividendAmount,
this.dividendAmount = calculateDividendAmount());
firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus,
this.calculationStatus = calculateStatus());
}
public BigDecimal getDividendAmount()
{
return dividendAmount;
}
public void setDividendAmount(BigDecimal amount)
{
triggerDividendAmount(amount);
long myGrossAmount = calculateGrossAmount4Dividend();
setFxGrossAmount(myGrossAmount);
}
public void triggerDividendAmount(BigDecimal amount)
{
firePropertyChange(Properties.dividendAmount.name(), this.dividendAmount, this.dividendAmount = amount);
}
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());
triggerGrossAmount(Math.round(newRate.doubleValue() * fxGrossAmount));
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 getGrossAmount()
{
return grossAmount;
}
public void setGrossAmount(long amount)
{
triggerGrossAmount(amount);
if (fxGrossAmount != 0)
{
BigDecimal newExchangeRate = BigDecimal.valueOf(amount).divide(BigDecimal.valueOf(fxGrossAmount), 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 triggerGrossAmount(long amount)
{
firePropertyChange(Properties.grossAmount.name(), this.grossAmount, this.grossAmount = amount);
triggerTotal(calculateTotal());
}
public long getFxTaxes()
{
return fxTaxes;
}
public void setFxTaxes(long fxTaxes)
{
firePropertyChange(Properties.fxTaxes.name(), this.fxTaxes, this.fxTaxes = fxTaxes);
triggerTotal(calculateTotal());
firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus,
this.calculationStatus = calculateStatus());
}
public long getTaxes()
{
return taxes;
}
public void setTaxes(long taxes)
{
firePropertyChange(Properties.taxes.name(), this.taxes, this.taxes = taxes);
triggerTotal(calculateTotal());
firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus,
this.calculationStatus = calculateStatus());
}
public long getTotal()
{
return total;
}
public void setTotal(long total)
{
triggerTotal(total);
firePropertyChange(Properties.grossAmount.name(), this.grossAmount,
this.grossAmount = calculateGrossAmount4Total());
firePropertyChange(Properties.fxGrossAmount.name(), this.fxGrossAmount,
this.fxGrossAmount = Math.round(grossAmount / exchangeRate.doubleValue()));
firePropertyChange(Properties.dividendAmount.name(), this.dividendAmount,
this.dividendAmount = calculateDividendAmount());
firePropertyChange(Properties.calculationStatus.name(), this.calculationStatus,
this.calculationStatus = calculateStatus());
}
public void triggerTotal(long total)
{
firePropertyChange(Properties.total.name(), this.total, this.total = total);
}
protected BigDecimal calculateDividendAmount()
{
if (shares > 0)
return BigDecimal.valueOf(
(fxGrossAmount * Values.Share.factor()) / (double) shares / Values.Amount.divider());
else
return BigDecimal.ZERO;
}
protected long calculateGrossAmount4Total()
{
long totalTaxes = taxes + Math.round(exchangeRate.doubleValue() * fxTaxes);
return total + totalTaxes;
}
protected long calculateGrossAmount4Dividend()
{
return Math.round((shares * dividendAmount.doubleValue() * Values.Amount.factor())
/ (double) Values.Share.factor());
}
private long calculateTotal()
{
long totalTaxes = taxes + Math.round(exchangeRate.doubleValue() * fxTaxes);
return Math.max(0, grossAmount - totalTaxes);
}
public String getNote()
{
return note;
}
public void setNote(String note)
{
firePropertyChange(Properties.note.name(), this.note, this.note = note);
}
public String getAccountCurrencyCode()
{
return account != null ? account.getCurrencyCode() : ""; //$NON-NLS-1$
}
public String getSecurityCurrencyCode()
{
return security != null ? security.getCurrencyCode() : ""; //$NON-NLS-1$
}
public String getFxCurrencyCode()
{
return security != null && !security.getCurrencyCode().isEmpty() ? security.getCurrencyCode()
: getAccountCurrencyCode();
}
/**
* Returns exchange rate label in direct (price) notation.
*/
public String getExchangeRateCurrencies()
{
return String.format("%s/%s", getSecurityCurrencyCode(), getAccountCurrencyCode()); //$NON-NLS-1$
}
/**
* Returns exchange rate label in indirect (quantity) notation.
*/
public String getInverseExchangeRateCurrencies()
{
return String.format("%s/%s", getAccountCurrencyCode(), getSecurityCurrencyCode()); //$NON-NLS-1$
}
public AccountTransaction.Type getType()
{
return type;
}
}