diff --git a/name.abuchen.portfolio.bootstrap/Application.e4xmi b/name.abuchen.portfolio.bootstrap/Application.e4xmi
index 778cc498ec..a31b71a07a 100644
--- a/name.abuchen.portfolio.bootstrap/Application.e4xmi
+++ b/name.abuchen.portfolio.bootstrap/Application.e4xmi
@@ -51,6 +51,11 @@
+
+
+
+
+
diff --git a/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle.properties b/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle.properties
index af82c4ac74..9ea4cbc9c0 100644
--- a/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle.properties
+++ b/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle.properties
@@ -19,6 +19,8 @@ command.import.pdf.dab = DAB Bank
command.import.pdf.db = Deutsche Bank
command.import.pdf.flatex = Flatex
command.import.pdf.import-pdf = PDF Documents (experimental)
+command.import.xml.ib = Interactive Brokers Activity Statement
+command.import.xml.import-xml = XML Documents (experimental)
command.newFile.mnemonic = N
command.newFile.name = New...
command.openFile.mnemonic = O
diff --git a/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle_de.properties b/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle_de.properties
index 2b726cdbfc..73d0d4dd80 100644
--- a/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle_de.properties
+++ b/name.abuchen.portfolio.bootstrap/OSGI-INF/l10n/bundle_de.properties
@@ -19,6 +19,8 @@ command.import.pdf.dab = DAB Bank
command.import.pdf.db = Deutsche Bank
command.import.pdf.flatex = Flatex
command.import.pdf.import-pdf = PDF Dokumente (experimentell)
+command.import.xml.ib = Interactive Brokers Activity Statement
+command.import.xml.import-xml = XML Dokumente (experimentell)
command.newFile.mnemonic = N
command.newFile.name = Neu...
command.openFile.mnemonic = O
diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBActivityStatement.xml b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBActivityStatement.xml
new file mode 100644
index 0000000000..6748cd0b7b
--- /dev/null
+++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBActivityStatement.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractorTest.java
new file mode 100644
index 0000000000..c3e0e53a9f
--- /dev/null
+++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractorTest.java
@@ -0,0 +1,131 @@
+package name.abuchen.portfolio.datatransfer;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import name.abuchen.portfolio.datatransfer.Extractor.BuySellEntryItem;
+import name.abuchen.portfolio.datatransfer.Extractor.Item;
+import name.abuchen.portfolio.datatransfer.Extractor.SecurityItem;
+import name.abuchen.portfolio.model.AccountTransaction;
+import name.abuchen.portfolio.model.BuySellEntry;
+import name.abuchen.portfolio.model.Client;
+import name.abuchen.portfolio.model.PortfolioTransaction;
+import name.abuchen.portfolio.model.Security;
+import name.abuchen.portfolio.util.Dates;
+
+import org.junit.Test;
+
+@SuppressWarnings("nls")
+public class IBFlexStatementExtractorTest
+{
+
+ private InputStream activityStatement;
+ private InputStream otherFile;
+
+ public IBFlexStatementExtractorTest()
+ {
+ activityStatement = getClass().getResourceAsStream("IBActivityStatement.xml");
+ otherFile = getClass().getResourceAsStream("Gutschrift.txt");
+ }
+
+ @Test
+ public void testIBAcitvityStatement() throws IOException
+ {
+
+ Client client = new Client();
+ IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client);
+ List errors = new ArrayList();
+ extractor.importActivityStatement(activityStatement, errors);
+ List- results = extractor.getResults();
+
+ // 1 Error Messages for negative interest which is not yet supported
+ assertThat(errors.size(), is(1));
+ assertThat(results.size(), is(25));
+
+ assertFirstSecurity(results.stream().filter(i -> i instanceof SecurityItem).findFirst());
+ assertFirstTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).findFirst());
+
+ assertSecondSecurity(results.stream().filter(i -> i instanceof SecurityItem)
+ .reduce((previous, current) -> current).get());
+ assertFourthTransaction(results.stream().filter(i -> i instanceof BuySellEntryItem).skip(3).findFirst());
+
+ // TODO Check CorporateActions
+ }
+
+ private void assertFirstSecurity(Optional
- item)
+ {
+ assertThat(item.isPresent(), is(true));
+ Security security = ((SecurityItem) item.get()).getSecurity();
+ assertThat(security.getIsin(), is("CA38501D2041"));
+ assertThat(security.getWkn(), is("80845553"));
+ assertThat(security.getName(), is("GRAN COLOMBIA GOLD CORP"));
+ assertThat(security.getTickerSymbol(), is("GCM.TO"));
+ }
+
+ private void assertSecondSecurity(Item item)
+ {
+ // Why is the second Security the GCM after Split ??? expected to be UUU
+ Security security = ((SecurityItem) item).getSecurity();
+ assertThat(security.getIsin(), is("CA38501D5010"));
+ assertThat(security.getWkn(), is("129258970"));
+ assertThat(security.getName(),
+ is("GCM(CA38501D2041) SPLIT 1 FOR 25 (GCM, GRAN COLOMBIA GOLD CORP, CA38501D5010)"));
+
+ // setting GCM.TO as ticker symbol
+ // currently fails because the exchange is empty in corporate actions.
+ }
+
+ private void assertFirstTransaction(Optional
- item)
+ {
+ assertThat(item.isPresent(), is(true));
+ assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class));
+ BuySellEntry entry = (BuySellEntry) item.get().getSubject();
+
+ assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY));
+ assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY));
+
+ assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("GRAN COLOMBIA GOLD CORP"));
+ assertThat(entry.getPortfolioTransaction().getAmount(), is(1356_75L));
+ assertThat(entry.getPortfolioTransaction().getDate(), is(Dates.date("2013-04-01")));
+ assertThat(entry.getPortfolioTransaction().getShares(), is(5000_000000L));
+ assertThat(entry.getPortfolioTransaction().getFees(), is(6_75L));
+ assertThat(entry.getPortfolioTransaction().getActualPurchasePrice(), is(27L));
+
+ }
+
+ private void assertFourthTransaction(Optional
- item)
+ {
+ assertThat(item.isPresent(), is(true));
+ assertThat(item.get().getSubject(), instanceOf(BuySellEntry.class));
+ BuySellEntry entry = (BuySellEntry) item.get().getSubject();
+
+ assertThat(entry.getPortfolioTransaction().getType(), is(PortfolioTransaction.Type.BUY));
+ assertThat(entry.getAccountTransaction().getType(), is(AccountTransaction.Type.BUY));
+
+ assertThat(entry.getPortfolioTransaction().getSecurity().getName(), is("URANIUM ONE INC."));
+ assertThat(entry.getPortfolioTransaction().getAmount(), is(232_00L));
+ assertThat(entry.getPortfolioTransaction().getDate(), is(Dates.date("2013-01-02")));
+ assertThat(entry.getPortfolioTransaction().getShares(), is(100_000000L));
+ assertThat(entry.getPortfolioTransaction().getFees(), is(1_00L));
+ }
+
+ @Test
+ public void testThatExceptionIsAddedForNonFlexStatementDocuments() throws IOException
+ {
+ Client client = new Client();
+ IBFlexStatementExtractor extractor = new IBFlexStatementExtractor(client);
+ List errors = new ArrayList();
+ extractor.importActivityStatement(otherFile, errors);
+ List
- results = extractor.getResults();
+
+ assertThat(results.isEmpty(), is(true));
+ assertThat(errors.size(), is(1));
+ }
+}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/ImportPDFHandler.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/ImportPDFHandler.java
index 38e0a23043..99352cdd35 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/ImportPDFHandler.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/handlers/ImportPDFHandler.java
@@ -14,6 +14,7 @@
import name.abuchen.portfolio.datatransfer.DeutscheBankPDFExctractor;
import name.abuchen.portfolio.datatransfer.Extractor;
import name.abuchen.portfolio.datatransfer.FlatexPDFExctractor;
+import name.abuchen.portfolio.datatransfer.IBFlexStatementExtractor;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.ui.wizards.datatransfer.ImportExtractedItemsWizard;
@@ -64,6 +65,9 @@ public void execute(@Named(IServiceConstants.ACTIVE_PART) MPart part,
case "flatex": //$NON-NLS-1$
extractor = new FlatexPDFExctractor(client);
break;
+ case "ib": //$NON-NLS-1$
+ extractor = new IBFlexStatementExtractor(client);
+ break;
default:
throw new UnsupportedOperationException("Unknown pdf type: " + type); //$NON-NLS-1$
}
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
index 39bf531b0f..3d143b9346 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties
@@ -1,5 +1,5 @@
-AboutText = Portfolio Performance\n\nVersion {0}\n\n(c) Copyright Andreas Buchen 2012 - 2015. All rights reserved.\n\nWith contributions by simpsus, jahzoo, gynngr, nistude, mwhesse, derari and necoro.\n\nThis product includes software developed by the\n Eclipse Foundation http://eclipse.org/\n Apache Software Foundation http://apache.org/\n SWT Chart Project http://www.swtchart.org/\n Tree Map Library http://code.google.com/p/treemaplib/\n jsoup Java HTML Parser http://jsoup.org\n JSON.simple https://code.google.com/p/json-simple/\n D3.js http://d3js.org\n\nSome icons are based on\n FatCow Hosting Icons http://www.fatcow.com/free-icons\n (Creative Commons Attribution 3.0 United States)\n iconmonstr http://iconmonstr.com\n\nThis product is published under the Eclipse Public License\nhttp://www.eclipse.org/legal/epl-v10.html
+AboutText = Portfolio Performance\n\nVersion {0}\n\n(c) Copyright Andreas Buchen 2012 - 2015. All rights reserved.\n\nWith contributions by simpsus, jahzoo, gynngr, nistude, mwhesse,\n derari, necoro, alainschaefer and sebasbaumh.\n\nThis product includes software developed by the\n Eclipse Foundation http://eclipse.org/\n Apache Software Foundation http://apache.org/\n SWT Chart Project http://www.swtchart.org/\n Tree Map Library http://code.google.com/p/treemaplib/\n jsoup Java HTML Parser http://jsoup.org\n JSON.simple https://code.google.com/p/json-simple/\n D3.js http://d3js.org\n\nSome icons are based on\n FatCow Hosting Icons http://www.fatcow.com/free-icons\n (Creative Commons Attribution 3.0 United States)\n iconmonstr http://iconmonstr.com\n\nThis product is published under the Eclipse Public License\nhttp://www.eclipse.org/legal/epl-v10.html
AccountFilterRetiredAccounts = Hide inactive accounts
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties
index 7d3441b80c..c07abaa0a0 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties
@@ -1,5 +1,5 @@
-AboutText = Portfolio Performance\n\nVersion {0}\n\n(c) Copyright Andreas Buchen 2012 - 2015. All rights reserved.\n\nWith contributions by simpsus, jahzoo, gynngr, nistude, mwhesse, derari and necoro.\n\nThis product includes software developed by the\n Eclipse Foundation http://eclipse.org/\n Apache Software Foundation http://apache.org/\n SWT Chart Project http://www.swtchart.org/\n Tree Map Library http://code.google.com/p/treemaplib/\n jsoup Java HTML Parser http://jsoup.org\n JSON.simple https://code.google.com/p/json-simple/\n D3.js http://d3js.org\n\nSome icons are based on\n FatCow Hosting Icons http://www.fatcow.com/free-icons\n (Creative Commons Attribution 3.0 United States)\n iconmonstr http://iconmonstr.com\n\nThis product is published under the Eclipse Public License\nhttp://www.eclipse.org/legal/epl-v10.html
+AboutText = Portfolio Performance\n\nVersion {0}\n\n(c) Copyright Andreas Buchen 2012 - 2015. All rights reserved.\n\nWith contributions by simpsus, jahzoo, gynngr, nistude, mwhesse,\n derari, necoro, alainschaefer and sebasbaumh.\n\nThis product includes software developed by the\n Eclipse Foundation http://eclipse.org/\n Apache Software Foundation http://apache.org/\n SWT Chart Project http://www.swtchart.org/\n Tree Map Library http://code.google.com/p/treemaplib/\n jsoup Java HTML Parser http://jsoup.org\n JSON.simple https://code.google.com/p/json-simple/\n D3.js http://d3js.org\n\nSome icons are based on\n FatCow Hosting Icons http://www.fatcow.com/free-icons\n (Creative Commons Attribution 3.0 United States)\n iconmonstr http://iconmonstr.com\n\nThis product is published under the Eclipse Public License\nhttp://www.eclipse.org/legal/epl-v10.html
AccountFilterRetiredAccounts = Inaktive Konten verbergen
@@ -187,7 +187,7 @@ ColumnExchangeRate = Wechselkurs
ColumnFees = Geb\u00FChren
-ColumnFees_Description = Angefallen Geb\u00FChren f\u00FCr K\u00E4ufe, Verk\u00E4ufe, Einlieferungen und Auslieferungen.
+ColumnFees_Description = Angefallene Geb\u00FChren f\u00FCr K\u00E4ufe, Verk\u00E4ufe, Einlieferungen und Auslieferungen.
ColumnFix = Fix
@@ -219,7 +219,7 @@ ColumnLatest = Letzter
ColumnLatestDate = Letzter (Datum)
-ColumnLatestHistoricalDate = Letzer historischer (Datum)
+ColumnLatestHistoricalDate = Letzter historischer (Datum)
ColumnLatestPrice = Letzter Kurs
@@ -807,7 +807,7 @@ MsgNoFileOpenText = \u00D6ffnen oder erstellen Sie zun\u00E4chst eine Portfolio
MsgNoIssuesFound = Keine Probleme gefunden.
-MsgNoProfileFound = Kein Installationsprofile gefunden. L\u00E4uft die Anwendung in der IDE?
+MsgNoProfileFound = Kein Installationsprofil gefunden. L\u00E4uft die Anwendung in der IDE?
MsgNoUpdatesAvailable = Keine Updates vorhanden.
@@ -967,7 +967,7 @@ SplitWizardDefinitionDescription = F\u00FCr welches Papier, zu welchem Datum (Ex
SplitWizardDefinitionTitle = Aktiensplit
-SplitWizardErrorNewAndOldMustNotBeEqual = Das Splitverh\u00E4ltnis (neu f\u00FCr alt) ist identisch sein.
+SplitWizardErrorNewAndOldMustNotBeEqual = Das Splitverh\u00E4ltnis (neu f\u00FCr alt) ist identisch.
SplitWizardLabelNewForOld = f\u00FCr
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/preferences/LanguagePreferencePage.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/preferences/LanguagePreferencePage.java
index 8c17d6a503..da831901fe 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/preferences/LanguagePreferencePage.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/preferences/LanguagePreferencePage.java
@@ -105,6 +105,10 @@ protected Control createContents(Composite parent)
@Override
public boolean performOk()
{
+ // check if viewer is initialized at all
+ if (viewer == null)
+ return true;
+
Language language = (Language) ((IStructuredSelection) viewer.getSelection()).getFirstElement();
switch (language)
diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/update/UpdateHelper.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/update/UpdateHelper.java
index 0360d7c50e..226fb4e945 100644
--- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/update/UpdateHelper.java
+++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/update/UpdateHelper.java
@@ -15,6 +15,7 @@
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.e4.ui.workbench.IWorkbench;
@@ -52,12 +53,12 @@ private class NewVersion
private String version;
private String description;
private String minimumJavaVersionRequired;
+ private String updateNotSupportedOSList;
+ private String updateNotSupportedOSMessage;
- public NewVersion(String version, String description, String minimumJavaVersionRequired)
+ public NewVersion(String version)
{
this.version = version;
- this.description = description;
- this.minimumJavaVersionRequired = minimumJavaVersionRequired;
}
public String getVersion()
@@ -70,6 +71,31 @@ public String getDescription()
return description;
}
+ public void setDescription(String description)
+ {
+ this.description = description;
+ }
+
+ public void setMinimumJavaVersionRequired(String minimumJavaVersionRequired)
+ {
+ this.minimumJavaVersionRequired = minimumJavaVersionRequired;
+ }
+
+ public void setUpdateNotSupportedOSList(String updateNotSupportedOSList)
+ {
+ this.updateNotSupportedOSList = updateNotSupportedOSList;
+ }
+
+ public void setUpdateNotSupportedOSMessage(String updateNotSupportedOSMessage)
+ {
+ this.updateNotSupportedOSMessage = updateNotSupportedOSMessage;
+ }
+
+ public String getUpdateNotSupportedOSMessage()
+ {
+ return updateNotSupportedOSMessage;
+ }
+
public boolean requiresNewJavaVersion()
{
if (minimumJavaVersionRequired == null)
@@ -93,6 +119,20 @@ private double parseJavaVersion(String version)
return Double.parseDouble(version.substring(0, pos));
}
+
+ public boolean isUpdateOnOSSupported()
+ {
+ if (updateNotSupportedOSList == null)
+ return true;
+
+ String[] list = updateNotSupportedOSList.split(","); //$NON-NLS-1$
+ String currentOS = Platform.getOS();
+ for (String os : list)
+ if (currentOS.equals(os))
+ return false;
+
+ return true;
+ }
}
private final IWorkbench workbench;
@@ -249,13 +289,18 @@ private NewVersion checkForUpdates(IProgressMonitor monitor) throws OperationCan
if (update == null)
{
- return new NewVersion(Messages.LabelUnknownVersion, null, null);
+ return new NewVersion(Messages.LabelUnknownVersion);
}
else
{
- return new NewVersion(update.replacement.getVersion().toString(), //
- update.replacement.getProperty("latest.changes.description", null), //$NON-NLS-1$
- update.replacement.getProperty("latest.changes.minimumJavaVersionRequired", null)); //$NON-NLS-1$
+ NewVersion v = new NewVersion(update.replacement.getVersion().toString());
+ v.setDescription(update.replacement.getProperty("latest.changes.description", null)); //$NON-NLS-1$
+ v.setMinimumJavaVersionRequired(update.replacement.getProperty(
+ "latest.changes.minimumJavaVersionRequired", null)); //$NON-NLS-1$
+ v.setUpdateNotSupportedOSList(update.replacement.getProperty("latest.changes.notSupportedOSList", null)); //$NON-NLS-1$
+ v.setUpdateNotSupportedOSMessage(update.replacement.getProperty("latest.changes.notSupportedOSMessage", //$NON-NLS-1$
+ null));
+ return v;
}
}
@@ -328,6 +373,7 @@ public ExtendedMessageDialog(Shell parentShell, String title, String message, Ne
protected Control createCustomArea(Composite parent)
{
Composite container = new Composite(parent, SWT.NONE);
+ GridDataFactory.fillDefaults().grab(true, false).applyTo(container);
GridLayoutFactory.fillDefaults().numColumns(1).applyTo(container);
createText(container);
@@ -350,6 +396,21 @@ public void widgetSelected(SelectionEvent e)
return container;
}
+ @Override
+ protected Control createButtonBar(Composite parent)
+ {
+ Control control = super.createButtonBar(parent);
+
+ if (!newVersion.isUpdateOnOSSupported())
+ {
+ Button okButton = getButton(IDialogConstants.OK_ID);
+ if (okButton != null)
+ okButton.setEnabled(false);
+ }
+
+ return control;
+ }
+
private void createText(Composite container)
{
StyledText text = new StyledText(container, SWT.MULTI | SWT.WRAP | SWT.READ_ONLY | SWT.BORDER);
@@ -357,6 +418,20 @@ private void createText(Composite container)
List ranges = new ArrayList();
StringBuilder buffer = new StringBuilder();
+ if (!newVersion.isUpdateOnOSSupported())
+ {
+ String message = newVersion.getUpdateNotSupportedOSMessage();
+ StyleRange style = new StyleRange();
+ style.start = buffer.length();
+ style.length = message.length();
+ style.foreground = Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED);
+ style.fontStyle = SWT.BOLD;
+ ranges.add(style);
+
+ buffer.append(message);
+ buffer.append("\n\n"); //$NON-NLS-1$
+ }
+
if (newVersion.requiresNewJavaVersion())
{
StyleRange style = new StyleRange();
@@ -376,4 +451,4 @@ private void createText(Composite container)
GridDataFactory.fillDefaults().grab(true, true).applyTo(text);
}
}
-}
\ No newline at end of file
+}
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java
index edef4df777..a31682ecd2 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/Messages.java
@@ -23,6 +23,8 @@ public class Messages extends NLS
public static String ColumnPerformance;
public static String ColumnPerformanceIZF;
public static String ColumnTransfers;
+ public static String CSVColumn_BaseCurrencyAmount;
+ public static String CSVColumn_BaseCurrencyCode;
public static String CSVColumn_Date;
public static String CSVColumn_Description;
public static String CSVColumn_Fees;
@@ -35,7 +37,10 @@ public class Messages extends NLS
public static String CSVColumn_Value;
public static String CSVColumn_WKN;
public static String CSVColumn_CumulatedPerformanceInPercent;
+ public static String CSVColumn_CurrencyCode;
public static String CSVColumn_DeltaInPercent;
+ public static String CSVColumn_ExchangeRate;
+ public static String CSVColumn_Note;
public static String CSVColumn_Transferals;
public static String CSVDefAccountTransactions;
public static String CSVDefHistoricalQuotes;
@@ -71,6 +76,7 @@ public class Messages extends NLS
public static String FixDeleteTransaction;
public static String FixDeleteTransactionDone;
public static String FixReferenceAccountNameProposal;
+ public static String IBXML_Label;
public static String IssueBuySellWithoutSecurity;
public static String IssueDividendWithoutSecurity;
public static String IssueInconsistentSharesHeld;
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/CrossEntryCheck.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/CrossEntryCheck.java
index 2e909940ff..f095bb45f5 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/CrossEntryCheck.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/CrossEntryCheck.java
@@ -179,6 +179,8 @@ private void matchBuySell()
entry.setFees(match.transaction.getFees());
entry.setTaxes(match.transaction.getTaxes());
entry.setAmount(match.transaction.getAmount());
+ entry.setCurrencyCode(match.transaction.getCurrencyCode());
+ entry.setForex(match.transaction.getForex());
entry.insert();
match.portfolio.getTransactions().remove(match.transaction);
@@ -260,6 +262,7 @@ else if (suspect.transaction.getType() == AccountTransaction.Type.TRANSFER_OUT)
crossentry.setDate(match.transaction.getDate());
crossentry.setAmount(match.transaction.getAmount());
+ crossentry.setCurrencyCode(match.transaction.getCurrencyCode());
crossentry.insert();
suspect.account.getTransactions().remove(suspect.transaction);
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellAccountIssue.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellAccountIssue.java
index 577a62472d..b582822034 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellAccountIssue.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellAccountIssue.java
@@ -52,6 +52,8 @@ public void execute()
t.setFees(transaction.getFees());
t.setTaxes(transaction.getTaxes());
t.setAmount(transaction.getAmount());
+ t.setCurrencyCode(transaction.getCurrencyCode());
+ t.setForex(transaction.getForex());
portfolio.addTransaction(t);
portfolio.getTransactions().remove(transaction);
@@ -90,6 +92,8 @@ public void execute()
entry.setFees(transaction.getFees());
entry.setTaxes(transaction.getTaxes());
entry.setAmount(transaction.getAmount());
+ entry.setCurrencyCode(transaction.getCurrencyCode());
+ entry.setForex(transaction.getForex());
entry.insert();
portfolio.getTransactions().remove(transaction);
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellPortfolioIssue.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellPortfolioIssue.java
index 7c7b3887a8..6b06acbdb1 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellPortfolioIssue.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/checks/impl/MissingBuySellPortfolioIssue.java
@@ -47,6 +47,8 @@ public void execute()
entry.setFees(0);
entry.setTaxes(0);
entry.setAmount(transaction.getAmount());
+ entry.setCurrencyCode(transaction.getCurrencyCode());
+ entry.setForex(transaction.getForex());
entry.insert();
account.getTransactions().remove(transaction);
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVExporter.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVExporter.java
index 5b8b2e41dc..a604f9fa31 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVExporter.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVExporter.java
@@ -48,17 +48,39 @@ public void exportAccountTransactions(File file, Account account) throws IOExcep
printer.println(new String[] { Messages.CSVColumn_Date, //
Messages.CSVColumn_Type, //
+ Messages.CSVColumn_BaseCurrencyAmount, //
+ Messages.CSVColumn_BaseCurrencyCode, //
+ Messages.CSVColumn_ExchangeRate, //
Messages.CSVColumn_Value, //
+ Messages.CSVColumn_CurrencyCode, //
+ Messages.CSVColumn_Note, //
Messages.CSVColumn_ISIN, //
Messages.CSVColumn_WKN, //
Messages.CSVColumn_TickerSymbol, //
Messages.CSVColumn_Description });
+ DecimalFormat df = new DecimalFormat();
+ df.setMaximumFractionDigits(4);
+
for (AccountTransaction t : account.getTransactions())
{
printer.print(dateFormat.format(t.getDate()));
printer.print(t.getType().toString());
printer.print(currencyFormat.format(t.getAmount() / Values.Amount.divider()));
+ printer.print(escapeNull(t.getCurrencyCode()));
+ if (t.getForex() != null)
+ {
+ printer.print(df.format(t.getForex().getExchangeRate()));
+ printer.print(currencyFormat.format((t.getForex().getBaseAmount()) / Values.Amount.divider()));
+ printer.print(escapeNull(t.getForex().getBaseCurrency()));
+ }
+ else
+ {
+ printer.print("1"); //$NON-NLS-1$
+ printer.print(currencyFormat.format(t.getAmount() / Values.Amount.divider()));
+ printer.print(escapeNull(t.getCurrencyCode()));
+ }
+ printer.print(escapeNull(t.getNote()));
printSecurityInfo(printer, t);
@@ -82,23 +104,45 @@ public void exportPortfolioTransactions(File file, Portfolio portfolio) throws I
printer.println(new String[] { Messages.CSVColumn_Date, //
Messages.CSVColumn_Type, //
+ Messages.CSVColumn_BaseCurrencyAmount, //
+ Messages.CSVColumn_BaseCurrencyCode, //
+ Messages.CSVColumn_ExchangeRate, //
Messages.CSVColumn_Value, //
+ Messages.CSVColumn_CurrencyCode, //
Messages.CSVColumn_Fees, //
Messages.CSVColumn_Taxes, //
Messages.CSVColumn_Shares, //
+ Messages.CSVColumn_Note, //
Messages.CSVColumn_ISIN, //
Messages.CSVColumn_WKN, //
Messages.CSVColumn_TickerSymbol, //
Messages.CSVColumn_Description });
+ DecimalFormat df = new DecimalFormat();
+ df.setMaximumFractionDigits(5);
+
for (PortfolioTransaction t : portfolio.getTransactions())
{
printer.print(dateFormat.format(t.getDate()));
printer.print(t.getType().toString());
printer.print(currencyFormat.format(t.getAmount() / Values.Amount.divider()));
+ printer.print(escapeNull(t.getCurrencyCode()));
+ if (t.getForex() != null)
+ {
+ printer.print(df.format(t.getForex().getExchangeRate()));
+ printer.print(currencyFormat.format((t.getForex().getBaseAmount()) / Values.Amount.divider()));
+ printer.print(escapeNull(t.getForex().getBaseCurrency()));
+ }
+ else
+ {
+ printer.print("1"); //$NON-NLS-1$
+ printer.print(currencyFormat.format(t.getAmount() / Values.Amount.divider()));
+ printer.print(escapeNull(t.getCurrencyCode()));
+ }
printer.print(currencyFormat.format(t.getFees() / Values.Amount.divider()));
printer.print(currencyFormat.format(t.getTaxes() / Values.Amount.divider()));
printer.print(Values.Share.format(t.getShares()));
+ printer.print(escapeNull(t.getNote()));
printSecurityInfo(printer, t);
@@ -143,7 +187,9 @@ public void exportSecurityMasterData(File file, List securities) throw
Messages.CSVColumn_WKN, //
Messages.CSVColumn_TickerSymbol, //
Messages.CSVColumn_Description, //
- Messages.CSVColumn_TickerSymbol });
+ Messages.CSVColumn_TickerSymbol, //
+ Messages.CSVColumn_CurrencyCode, //
+ Messages.CSVColumn_Description }); //$NON-NLS-1$
for (Security s : securities)
{
@@ -152,6 +198,8 @@ public void exportSecurityMasterData(File file, List securities) throw
printer.print(escapeNull(s.getTickerSymbol()));
printer.print(escapeNull(s.getName()));
printer.print(escapeNull(s.getTickerSymbol()));
+ printer.print(escapeNull(s.getCurrencyCode()));
+ printer.print(escapeNull(s.getNote()));
printer.println();
}
}
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImportDefinition.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImportDefinition.java
index f0fe1f88a3..8fd7b0ea42 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImportDefinition.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImportDefinition.java
@@ -1,5 +1,6 @@
package name.abuchen.portfolio.datatransfer;
+import java.math.BigDecimal;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
@@ -18,10 +19,12 @@
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.Client;
+import name.abuchen.portfolio.model.ForexData;
import name.abuchen.portfolio.model.Portfolio;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.SecurityPrice;
+import name.abuchen.portfolio.model.Transaction;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.online.QuoteFeed;
import name.abuchen.portfolio.online.impl.YahooFinanceQuoteFeed;
@@ -113,7 +116,8 @@ protected String getTextValue(String name, String[] rawValues, Map field2column)
+ throws ParseException
+ {
+ String value = getTextValue(Messages.CSVColumn_ExchangeRate, rawValues, field2column);
+ if (value == null)
+ return null;
+
+ Number num = (Number) field2column.get(Messages.CSVColumn_ExchangeRate).getFormat().getFormat()
+ .parseObject(value);
+
+ return new BigDecimal(num.toString());
+ }
+
+ protected String setCurrencyAndExchangeRate(Transaction transaction, String[] rawValues,
+ Map field2column, String termCurrency) throws ParseException
+ {
+ String currencyCode = getTextValue(Messages.CSVColumn_CurrencyCode, rawValues, field2column);
+ BigDecimal exchangeRate = convertExchangeRate(rawValues, field2column);
+
+ transaction.setCurrencyCode(termCurrency);
+
+ if (currencyCode == null)
+ {
+ currencyCode = termCurrency;
+ }
+
+ if (termCurrency.equals(currencyCode))
+ {
+ transaction.setForex(null);
+ }
+ else
+ {
+ ForexData forex = new ForexData();
+ forex.setBaseCurrency(currencyCode);
+ forex.setTermCurrency(termCurrency);
+ forex.setExchangeRate(exchangeRate);
+ forex.setBaseAmount(transaction.getAmount());
+ transaction.setForex(forex);
+ }
+
+ return currencyCode;
+
+ }
+
//
// implementations
//
@@ -155,6 +204,9 @@ else if (wkn != null && wkn.equals(s.getWkn()))
fields.add(new AmountField(Messages.CSVColumn_Value));
fields.add(new EnumField(Messages.CSVColumn_Type, AccountTransaction.Type.class)
.setOptional(true));
+ fields.add(new Field(Messages.CSVColumn_CurrencyCode).setOptional(true));
+ fields.add(new AmountField(Messages.CSVColumn_ExchangeRate).setOptional(true));
+ fields.add(new Field(Messages.CSVColumn_Description).setOptional(true));
}
@Override
@@ -193,9 +245,12 @@ void build(Client client, Object target, String[] rawValues, Map
String tickerSymbol = getTextValue(Messages.CSVColumn_TickerSymbol, rawValues, field2column);
String wkn = getTextValue(Messages.CSVColumn_WKN, rawValues, field2column);
+ String termCurrency = account.getCurrencyCode();
+ String currencyCode = setCurrencyAndExchangeRate(transaction, rawValues, field2column, termCurrency);
+
if (isin != null || tickerSymbol != null || wkn != null)
{
- Security security = lookupSecurity(client, isin, tickerSymbol, wkn, true);
+ Security security = lookupSecurity(client, isin, tickerSymbol, wkn, currencyCode, true);
transaction.setSecurity(security);
}
@@ -206,6 +261,9 @@ else if (transaction.getSecurity() != null)
else
transaction.setType(amount < 0 ? AccountTransaction.Type.REMOVAL : AccountTransaction.Type.DEPOSIT);
+ String description = getTextValue(Messages.CSVColumn_Description, rawValues, field2column);
+ transaction.setNote(description);
+
account.addTransaction(transaction);
}
@@ -228,6 +286,10 @@ else if (transaction.getSecurity() != null)
fields.add(new AmountField(Messages.CSVColumn_Shares));
fields.add(new EnumField(Messages.CSVColumn_Type,
PortfolioTransaction.Type.class).setOptional(true));
+ fields.add(new Field(Messages.CSVColumn_CurrencyCode).setOptional(true));
+ fields.add(new AmountField(Messages.CSVColumn_ExchangeRate).setOptional(true));
+ fields.add(new Field(Messages.CSVColumn_Description).setOptional(true));
+
}
@Override
@@ -283,7 +345,11 @@ void build(Client client, Object target, String[] rawValues, Map
PortfolioTransaction transaction = new PortfolioTransaction();
transaction.setDate(date);
transaction.setAmount(Math.abs(amount));
- transaction.setSecurity(lookupSecurity(client, isin, tickerSymbol, wkn, true));
+
+ String termCurrency = portfolio.getReferenceAccount().getCurrencyCode();
+ String currencyCode = setCurrencyAndExchangeRate(transaction, rawValues, field2column, termCurrency);
+
+ transaction.setSecurity(lookupSecurity(client, isin, tickerSymbol, wkn, currencyCode, true));
transaction.setShares(Math.abs(shares));
transaction.setFees(Math.abs(fees));
transaction.setTaxes(Math.abs(taxes));
@@ -348,6 +414,7 @@ void build(Client client, Object target, String[] rawValues, Map
fields.add(new Field(Messages.CSVColumn_TickerSymbol).setOptional(true));
fields.add(new Field(Messages.CSVColumn_WKN).setOptional(true));
fields.add(new Field(Messages.CSVColumn_Description).setOptional(true));
+ fields.add(new Field(Messages.CSVColumn_CurrencyCode).setOptional(true));
}
@Override
@@ -372,7 +439,7 @@ void build(Client client, Object target, String[] rawValues, Map
Messages.CSVColumn_ISIN + ", " + Messages.CSVColumn_TickerSymbol + ", " //$NON-NLS-1$ //$NON-NLS-2$
+ Messages.CSVColumn_WKN), 0);
- Security security = lookupSecurity(client, isin, tickerSymbol, wkn, false);
+ Security security = lookupSecurity(client, isin, tickerSymbol, wkn, null, false);
if (security != null)
throw new ParseException(MessageFormat.format(Messages.CSVImportSecurityExists, security.getName(),
isin != null ? isin : tickerSymbol != null ? tickerSymbol : wkn), 0);
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImporter.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImporter.java
index 8436143bf0..5d76101e6a 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImporter.java
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/CSVImporter.java
@@ -106,11 +106,13 @@ public Format getFormat()
public static class Field
{
private final String name;
+ private final String normalizedName;
private boolean isOptional = false;
public Field(String name)
{
this.name = name;
+ this.normalizedName = normalizeColumnName(name);
}
public String getName()
@@ -118,6 +120,11 @@ public String getName()
return name;
}
+ public String getNormalizedName()
+ {
+ return normalizedName;
+ }
+
public Field setOptional(boolean isOptional)
{
this.isOptional = isOptional;
@@ -148,6 +155,33 @@ public static class DateField extends CSVImporter.Field
{
super(name);
}
+
+ /**
+ * Guesses the used date format from the given value.
+ *
+ * @param value
+ * value (can be null)
+ * @return date format on success, else first date format
+ */
+ public static FieldFormat guessDateFormat(String value)
+ {
+ if (value != null)
+ {
+ for (FieldFormat f : FORMATS)
+ {
+ try
+ {
+ // try to parse the value and return it on success
+ f.format.parseObject(value);
+ return f;
+ }
+ catch (ParseException e)
+ {}
+ }
+ }
+ // fallback
+ return FORMATS[0];
+ }
}
public static class AmountField extends CSVImporter.Field
@@ -380,20 +414,29 @@ private void mapToImportDefinition()
for (Column column : columns)
{
column.setField(null);
+ String normalizedColumnName = normalizeColumnName(column.getLabel());
Iterator iter = list.iterator();
while (iter.hasNext())
{
Field field = iter.next();
- if (field.getName().equalsIgnoreCase(column.getLabel()))
+ if (field.getNormalizedName().equals(normalizedColumnName))
{
column.setField(field);
if (field instanceof DateField)
- column.setFormat(DateField.FORMATS[0]);
+ {
+ // try to guess date format
+ String value = getFirstNonEmptyValue(column);
+ column.setFormat(DateField.guessDateFormat(value));
+ }
else if (field instanceof AmountField)
+ {
column.setFormat(AmountField.FORMATS[0]);
+ }
else if (field instanceof EnumField>)
+ {
column.setFormat(new FieldFormat(null, ((EnumField>) field).createFormat()));
+ }
iter.remove();
break;
@@ -428,4 +471,63 @@ public void createObjects(List errors)
}
}
}
+
+ /**
+ * Finds the first value that is not empty for the given column.
+ *
+ * @param column
+ * {@link Column}
+ * @return value on success, else null
+ */
+ private String getFirstNonEmptyValue(Column column)
+ {
+ int index = column.getColumnIndex();
+ for (String[] rawValues : values)
+ {
+ String value = rawValues[index];
+ // check if value is set and is not empty (ignore whitespace)
+ if ((value != null) && (!value.trim().isEmpty()))
+ return value;
+ }
+ return null;
+ }
+
+ /**
+ * Normalizes the given column name for better matching to field names.
+ *
+ * @param name
+ * name of the column
+ * @return normalized name (upper case)
+ */
+ private static String normalizeColumnName(String name)
+ {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < name.length(); i++)
+ {
+ // get uppercase character
+ char c = Character.toUpperCase(name.charAt(i));
+ // transform special characters (Ä->AE etc.)
+ switch (c)
+ {
+ case 'Ä':
+ sb.append("AE"); //$NON-NLS-1$
+ break;
+ case 'Ö':
+ sb.append("OE"); //$NON-NLS-1$
+ break;
+ case 'Ü':
+ sb.append("UE"); //$NON-NLS-1$
+ break;
+ case 'ß':
+ sb.append("SS"); //$NON-NLS-1$
+ break;
+ case ' ':
+ // strip whitespace
+ break;
+ default:
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ }
}
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractor.java
new file mode 100644
index 0000000000..35d08bb41c
--- /dev/null
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/IBFlexStatementExtractor.java
@@ -0,0 +1,442 @@
+package name.abuchen.portfolio.datatransfer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import name.abuchen.portfolio.Messages;
+import name.abuchen.portfolio.model.AccountTransaction;
+import name.abuchen.portfolio.model.BuySellEntry;
+import name.abuchen.portfolio.model.Client;
+import name.abuchen.portfolio.model.PortfolioTransaction;
+import name.abuchen.portfolio.model.Security;
+import name.abuchen.portfolio.model.Transaction;
+import name.abuchen.portfolio.money.Values;
+import name.abuchen.portfolio.online.QuoteFeed;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+@SuppressWarnings("nls")
+public class IBFlexStatementExtractor implements Extractor
+{
+ private final Client client;
+ private final List
- results;
+ private List allSecurities;
+
+ private Map exchanges;
+
+ public IBFlexStatementExtractor(Client client)
+ {
+ this.client = client;
+ this.results = new ArrayList
- ();
+ allSecurities = new ArrayList(client.getSecurities());
+
+ // Maps Interactive Broker Exchange to Yahoo Exchanges, to be completed
+ this.exchanges = new HashMap();
+
+ this.exchanges.put("EBS", "SW");
+ this.exchanges.put("LSE", "L");
+ this.exchanges.put("SWX", "SW");
+ this.exchanges.put("TSE", "TO");
+ this.exchanges.put("VENTURE", "V");
+ }
+
+ private Date convertDate(String date) throws ParseException
+ {
+
+ if (date.length() > 8)
+ {
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+ return df.parse(date);
+ }
+ else
+ {
+ DateFormat df = new SimpleDateFormat("yyyyMMdd");
+ return df.parse(date);
+ }
+ }
+
+ /**
+ * Lookup a Security in the Model or create a new one if it does not yet
+ * exist It uses IB ContractID (conID) for the WKN, tries to degrade if
+ * conID or ISIN are not available
+ */
+ private Security getOrCreateSecurity(Client client, Element eElement, boolean doCreate)
+ {
+ // Lookup the Exchange Suffix for Yahoo
+ String tickerSymbol = eElement.getAttribute("symbol");
+ String yahooSymbol = tickerSymbol;
+ String exchange = eElement.getAttribute("exchange");
+ String isin = eElement.getAttribute("isin");
+ String cusip = eElement.getAttribute("cusip");
+ // Store cusip in isin if isin is not available
+ if (isin.length() == 0 && cusip.length() > 0)
+ isin = cusip;
+
+ String conID = eElement.getAttribute("conid");
+ String description = eElement.getAttribute("description");
+
+ if (tickerSymbol != null)
+ {
+ String exch = this.exchanges.get(exchange);
+ if (exch != null && exch.length() > 0)
+ yahooSymbol = tickerSymbol + '.' + exch;
+ }
+
+ for (Security s : allSecurities)
+ {
+ // Find security with same conID or isin or yahooSymbol
+ if (conID != null && conID.length() > 0 && conID.equals(s.getWkn()))
+ return s;
+ if (isin != null && isin.length() > 0 && isin.equals(s.getIsin()))
+ return s;
+ if (yahooSymbol != null && yahooSymbol.length() > 0 && yahooSymbol.equals(s.getTickerSymbol()))
+ return s;
+ }
+
+ if (!doCreate)
+ return null;
+
+ Security security = new Security(description, isin, yahooSymbol, QuoteFeed.MANUAL);
+ // We use the Wkn to store the IB conID as a unique identifier
+ security.setWkn(conID);
+ security.setNote(description);
+
+ // Store
+ allSecurities.add(security);
+ // add to result
+ SecurityItem item = new SecurityItem(security);
+ results.add(item);
+
+ return security;
+ }
+
+ /**
+ * Construct a BuySellEntry based on Trade object defined in eElement
+ */
+ private void buildPortfolioTransaction(Client client, Element eElement) throws ParseException
+ {
+ // Unused Information from Flexstatement Trades, to be used in the
+ // future: currency, tradeTime, transactionID, ibOrderID
+
+ BuySellEntry transaction = new BuySellEntry();
+
+ // Set Transaction Type
+ if (eElement.getAttribute("buySell").equals("BUY"))
+ {
+ transaction.setType(PortfolioTransaction.Type.BUY);
+ }
+ else if (eElement.getAttribute("buySell").equals("SELL"))
+ {
+ transaction.setType(PortfolioTransaction.Type.SELL);
+ }
+ else
+ {
+ throw new IllegalArgumentException();
+ }
+
+ String d = eElement.getAttribute("tradeDate");
+ if (d == null || d.length() == 0)
+ {
+ // use reportDate for CorporateActions
+ d = eElement.getAttribute("reportDate");
+ }
+ transaction.setDate(convertDate(d));
+
+ // Share Quantity
+ Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
+ transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
+
+ Double fees = Math.abs(Double.parseDouble(eElement.getAttribute("ibCommission")));
+ transaction.setFees(Math.round(fees.doubleValue() * Values.Amount.factor()));
+
+ Double taxes = Math.abs(Double.parseDouble(eElement.getAttribute("taxes")));
+ transaction.setTaxes(Math.round(taxes.doubleValue() * Values.Amount.factor()));
+
+ // Set the Amount which is ( tradePrice * qty ) + Fees + Taxes
+ Double amount = Double.parseDouble(eElement.getAttribute("tradePrice")) * qty + fees + taxes;
+ transaction.setAmount(Math.abs(Math.round(amount.doubleValue() * Values.Amount.factor())));
+
+ transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
+
+ transaction.setNote(eElement.getAttribute("description"));
+
+ results.add(new BuySellEntryItem(transaction));
+
+ }
+
+ /**
+ * Constructs a Transaction object for a Corporate Transaction defined in
+ * eElement.
+ */
+ private void buildCorporateTransaction(Client client, Element eElement) throws ParseException
+ {
+ Double amount = Double.parseDouble(eElement.getAttribute("proceeds"));
+ if (amount != 0)
+ {
+ BuySellEntry transaction = new BuySellEntry();
+
+ if (Double.parseDouble(eElement.getAttribute("quantity")) >= 0)
+ {
+ transaction.setType(PortfolioTransaction.Type.BUY);
+ }
+ else
+ {
+ transaction.setType(PortfolioTransaction.Type.SELL);
+ }
+ transaction.setDate(convertDate(eElement.getAttribute("reportDate")));
+ // Share Quantity
+ Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
+ transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
+
+ transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
+ transaction.setNote(eElement.getAttribute("description"));
+
+ transaction.setAmount(Math.abs(Math.round(amount.doubleValue() * Values.Amount.factor())));
+
+ results.add(new BuySellEntryItem(transaction));
+
+ }
+ else
+ {
+ // Set Transaction Type
+ PortfolioTransaction transaction = new PortfolioTransaction();
+ if (Double.parseDouble(eElement.getAttribute("quantity")) >= 0)
+ {
+ transaction.setType(PortfolioTransaction.Type.DELIVERY_INBOUND);
+ }
+ else
+ {
+ transaction.setType(PortfolioTransaction.Type.DELIVERY_OUTBOUND);
+ }
+ transaction.setDate(convertDate(eElement.getAttribute("reportDate")));
+ // Share Quantity
+ Double qty = Math.abs(Double.parseDouble(eElement.getAttribute("quantity")));
+ transaction.setShares(Math.round(qty.doubleValue() * Values.Share.factor()));
+
+ transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
+ transaction.setNote(eElement.getAttribute("description"));
+
+ results.add(new TransactionItem(transaction));
+ }
+
+ }
+
+ /**
+ * Figure out how many shares a dividend payment is related to. Extracts the
+ * information from the description string given by IB
+ */
+ private void calculateShares(Transaction transaction, Element eElement)
+ {
+ // Figure out how many shares were holding related to this Dividend
+ // Payment
+ long numShares = 0;
+ String desc = eElement.getAttribute("description");
+ double amount = Double.parseDouble(eElement.getAttribute("amount"));
+
+ // Regex Pattern matches the Dividend per Share and calculate number of
+ // shares
+ Pattern dividendPattern = Pattern.compile("DIVIDEND ([0-9]*\\.[0-9]*) .*");
+ Matcher tagmatch = dividendPattern.matcher(desc);
+ if (tagmatch.find())
+ {
+ double dividend = Double.parseDouble(tagmatch.group(1));
+ numShares = Math.round(amount / dividend) * Values.Share.factor();
+ }
+
+ transaction.setShares(numShares);
+ }
+
+ private void buildAccountTransaction(Client client, Element eElement) throws ParseException
+ {
+ AccountTransaction transaction = new AccountTransaction();
+
+ transaction.setDate(convertDate(eElement.getAttribute("dateTime")));
+ Double amount = Double.parseDouble(eElement.getAttribute("amount"));
+
+ // Set Transaction Type
+ if (eElement.getAttribute("type").equals("Deposits")
+ || eElement.getAttribute("type").equals("Deposits & Withdrawals"))
+ {
+ if (amount >= 0)
+ {
+ transaction.setType(AccountTransaction.Type.DEPOSIT);
+ }
+ else
+ {
+ transaction.setType(AccountTransaction.Type.REMOVAL);
+ }
+ }
+ else if (eElement.getAttribute("type").equals("Dividends")
+ || eElement.getAttribute("type").equals("Payment In Lieu Of Dividends"))
+ {
+ transaction.setType(AccountTransaction.Type.DIVIDENDS);
+
+ // Set the Symbol
+ if (eElement.getAttribute("symbol").length() > 0)
+ transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
+
+ this.calculateShares(transaction, eElement);
+ }
+ else if (eElement.getAttribute("type").equals("Withholding Tax"))
+ {
+ // Set the Symbol
+ if (eElement.getAttribute("symbol").length() > 0)
+ transaction.setSecurity(this.getOrCreateSecurity(client, eElement, true));
+
+ transaction.setType(AccountTransaction.Type.TAXES);
+
+ // Temporary until the model supports negative interest rates and
+ // dividends see #310
+ throw new ParseException(eElement.getAttribute("dateTime") + " Witholding Tax is not supported", 0);
+ }
+ else if (eElement.getAttribute("type").equals("Broker Interest Received"))
+ {
+ transaction.setType(AccountTransaction.Type.INTEREST);
+ }
+ else if (eElement.getAttribute("type").equals("Broker Interest Paid"))
+ {
+ // Temporary until the model supports negative interest see #310
+ throw new ParseException(eElement.getAttribute("dateTime") + " Broker Interest Paid is not supported", 0);
+ }
+ else if (eElement.getAttribute("type").equals("Other Fees"))
+ {
+ transaction.setType(AccountTransaction.Type.FEES);
+ }
+ else
+ {
+ throw new IllegalArgumentException();
+ }
+
+ amount = Math.abs(amount);
+ transaction.setAmount(Math.round(amount.doubleValue() * Values.Amount.factor()));
+
+ transaction.setNote(eElement.getAttribute("description"));
+
+ results.add(new TransactionItem(transaction));
+ }
+
+ /**
+ * Imports Trades, CorporateActions and CashTransactions from Document
+ */
+ private void importModelObjects(Document doc, String type, List errors)
+ {
+ NodeList nList = doc.getElementsByTagName(type);
+ for (int temp = 0; temp < nList.getLength(); temp++)
+ {
+ Node nNode = nList.item(temp);
+
+ if (nNode.getNodeType() == Node.ELEMENT_NODE)
+ {
+ try
+ {
+ if (type.equals("Trade"))
+ {
+ this.buildPortfolioTransaction(client, (Element) nNode);
+ }
+ else if (type.equals("CorporateAction"))
+ {
+ this.buildCorporateTransaction(client, (Element) nNode);
+ }
+ else if (type.equals("CashTransaction"))
+ {
+ this.buildAccountTransaction(client, (Element) nNode);
+ }
+ }
+ catch (ParseException e)
+ {
+ errors.add(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import an Interactive Broker ActivityStatement from an XML file. It
+ * currently only imports Trades, Corporate Transactions and Cash
+ * Transactions.
+ */
+ /* package */void importActivityStatement(InputStream f, List errors)
+ {
+
+ try
+ {
+ DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
+ Document doc = dBuilder.parse(f);
+
+ doc.getDocumentElement().normalize();
+
+ // Process all Trades
+ importModelObjects(doc, "Trade", errors);
+
+ // Process all CashTransaction
+ importModelObjects(doc, "CashTransaction", errors);
+
+ // Process all CorporateTransactions
+ importModelObjects(doc, "CorporateAction", errors);
+
+ // TODO: Process all FxTransactions and ConversionRates
+ }
+ catch (ParserConfigurationException | SAXException | IOException e)
+ {
+ errors.add(e);
+ }
+ }
+
+ /* package */List
- getResults()
+ {
+ return results;
+ }
+
+ @Override
+ public String getLabel()
+ {
+ return Messages.IBXML_Label;
+ }
+
+ @Override
+ public String getFilterExtension()
+ {
+ return "*.xml"; //$NON-NLS-1$
+ }
+
+ @Override
+ public List
- extract(List files, List errors)
+ {
+ results.clear();
+ for (File f : files)
+ {
+ try
+ {
+ importActivityStatement(new FileInputStream(f), errors);
+ }
+ catch (FileNotFoundException e)
+ {
+ errors.add(e);
+ }
+ }
+ return results;
+ }
+
+}
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties
index 826d3c23e8..c322af84a0 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages.properties
@@ -14,23 +14,33 @@ AttributesVendorColumn = Vendor
AttributesVendorName = Vendor
+CSVColumn_BaseCurrencyAmount= Amount base currency
+
+CSVColumn_BaseCurrencyCode= Base currency
+
CSVColumn_CumulatedPerformanceInPercent = Cumulated Performance in %
+CSVColumn_CurrencyCode= Currency
+
CSVColumn_Date = Date
CSVColumn_DeltaInPercent = Delta in %
CSVColumn_Description = Description
+CSVColumn_ExchangeRate= Exchange Rate
+
CSVColumn_Fees = Fees
CSVColumn_ISIN = ISIN
+CSVColumn_Note=Note
+
CSVColumn_Quote = Quote
CSVColumn_Shares = Shares
-CSVColumn_Taxes=Steuern
+CSVColumn_Taxes = Taxes
CSVColumn_TickerSymbol = Ticker Symbol
@@ -130,6 +140,8 @@ FixDeleteTransactionDone = deleted
FixReferenceAccountNameProposal = Reference account {0}
+IBXML_Label = IB Flexstatment XML-Files
+
IssueBuySellWithoutSecurity = ''{0}'' without security
IssueDividendWithoutSecurity = Dividend payout without an instrument
diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages_de.properties b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages_de.properties
index 20600fbb55..9d1c951b07 100644
--- a/name.abuchen.portfolio/src/name/abuchen/portfolio/messages_de.properties
+++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/messages_de.properties
@@ -1,11 +1,12 @@
-AttributesAcquisitionFeeName = Kaufgeb\u00FChr (prozentual)
-
-AttributesAcquisitionFeeColumn = Kaufgeb\u00FChr
AttributesAUMColumn = Fondsgr\u00F6\u00DFe
AttributesAUMName = Fondsgr\u00F6\u00DFe
+AttributesAcquisitionFeeColumn = Kaufgeb\u00FChr
+
+AttributesAcquisitionFeeName = Kaufgeb\u00FChr (prozentual)
+
AttributesTERColumn = TER
AttributesTERName = Gesamtkostenquote (TER)
@@ -14,14 +15,22 @@ AttributesVendorColumn = Anbieter
AttributesVendorName = Anbieter
+CSVColumn_BaseCurrencyAmount = Wert Hauptw\u00E4hrung
+
+CSVColumn_BaseCurrencyCode = Hauptw\u00E4hrung
+
CSVColumn_CumulatedPerformanceInPercent = Kumulierte Performance in %
+CSVColumn_CurrencyCode = Currency
+
CSVColumn_Date = Datum
CSVColumn_DeltaInPercent = Delta in %
CSVColumn_Description = Bezeichnung
+CSVColumn_ExchangeRate = Wechselkurs
+
CSVColumn_Fees = Geb\u00FChren
CSVColumn_ISIN = ISIN
@@ -130,6 +139,8 @@ FixDeleteTransactionDone = gel\u00F6scht
FixReferenceAccountNameProposal = Verrechnungskonto {0}
+IBXML_Label = IB Flexstatment XML-Dateien
+
IssueBuySellWithoutSecurity = ''{0}'' ohne Wertpapier
IssueDividendWithoutSecurity = Dividende (Aussch\u00FCttung) ohne Wertpapier
@@ -144,7 +155,7 @@ IssueMissingBuySellInAccount = Fehlende Gegenbuchung auf Konto\n{0} von {1} St\u
IssueMissingBuySellInPortfolio = Fehlende Gegenbuchung im Portfolio\n{0}\n{1}
-IssueMissingCurrencyCode = Keine Währung definiert. Wählen Sie eine Währung als Standardwährung.\nUm einem einzelnen Konto oder Wertpapier eine alternative Währung zuzuweisen, verwenden Sie den Editierdialog.
+IssueMissingCurrencyCode = Keine W\u00E4hrung definiert. W\u00E4hlen Sie eine W\u00E4hrung als Standardw\u00E4hrung.\nUm einem einzelnen Konto oder Wertpapier eine alternative W\u00E4hrung zuzuweisen, verwenden Sie den Editierdialog.
IssueMissingPortfolioTransfer = Fehlende Gegenbuchung\n{0} von {1} St\u00FCck zu {2}\n{3}
@@ -226,18 +237,18 @@ MsgUnsupportedVersionClientFiled = Diese Datei wurde mit einer neueren Version v
MsgXMLFormatInvalid = XML kann nicht geparst werden: {0}
+PDFMsgFileNotSupported = Datei ''{0}'' ist kein unterst\u00FCtztes Dokument der {1}
+
PDFcomdirectLabel = comdirect Postbox PDFs
-PDFcomdirectMsgCannotDetermineFileType = Unbekannter oder nicht unterstützter Buchungstyp in Datei ''{0}''
+PDFcomdirectMsgCannotDetermineFileType = Unbekannter oder nicht unterst\u00FCtzter Buchungstyp in Datei ''{0}''
-PDFcomdirectMsgFileNotSupported = Datei ''{0}'' ist kein unterstütztes Dokument der comdirect bank
+PDFcomdirectMsgFileNotSupported = Datei ''{0}'' ist kein unterst\u00FCtztes Dokument der comdirect bank
PDFdbLabel = Deutsche Bank
-PDFdbMsgCannotDetermineFileType = Unbekannter oder nicht unterstützter Buchungstyp in Datei ''{0}''
+PDFdbMsgCannotDetermineFileType = Unbekannter oder nicht unterst\u00FCtzter Buchungstyp in Datei ''{0}''
PDFdbMsgCannotFindSecurity = Kein Wertpapier oder ISIN gefunden in Datei ''{0}''
-PDFMsgFileNotSupported = Datei ''{0}'' ist kein unterstütztes Dokument der {1}
-
QuoteFeedManual = Kein automatischer Download