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