From 821e29e5274db520a165833363b0fe1be313574c Mon Sep 17 00:00:00 2001 From: Alexander Ott <45203494+Nirus2000@users.noreply.github.com> Date: Fri, 20 Dec 2024 07:47:13 +0100 Subject: [PATCH] Modify Saxo Bank PDF-Importer to support new transactions (#4402) https://forum.portfolio-performance.info/t/pdf-importer-fur-saxo-bank/30548/12 --- .../pdf/saxobank/CashTransfer01.txt | 25 +++ .../datatransfer/pdf/saxobank/Kauf01.txt | 38 ++++ .../saxobank/SaxoBankPDFExtractorTest.java | 110 +++++++++- .../datatransfer/ExtractorUtils.java | 3 +- .../pdf/SaxoBankPDFExtractor.java | 192 ++++++++++++++++++ 5 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/CashTransfer01.txt create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/Kauf01.txt diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/CashTransfer01.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/CashTransfer01.txt new file mode 100644 index 0000000000..51a986ee6a --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/CashTransfer01.txt @@ -0,0 +1,25 @@ +PDFBox Version: 1.8.17 +Portfolio Performance Version: 0.72.2 +----------------------------------------- +CH-19958475 - 19958475 +Cash Transfer Detail Report +Berichtszeitraum: 28-Nov-2024 - 28-Nov-2024 +Generiert um: 06-Dez-2024 07:24:28 19:24:28 (W. Europe Standard Time) +CH-19958475 Saxo Bank CH +Switzerland The Circle 38 +8058 Zürich-Flughafen +Switzerland +Saxo Bank CH / The Circle 38 / 8058 Zürich-Flughafen / Switzerland CH-19958475 Berichtszeitraum +Währung: CHF 28-Nov-2024 - 28-Nov-2024 +Seite 1 von 2 Konten: ETF Privat 80000/137772 Generiert um: 06-Dez-2024 +Cash Transfer Details, CHF +Berichtszeitraum +28-Nov-2024 bis 28-Nov-2024 +Bargeldtransfer +Amount Gebuchter Betrag +Event Buchungsbetrag-ID Trade-Datum Value Date Umrechnungskurs Umrechnungskosten +CHF CHF +Einlage 39482097030 28-Nov-2024 28-Nov-2024 4.600,00 1,000000 0,00 4.600,00 +Saxo Bank CH / The Circle 38 / 8058 Zürich-Flughafen / Switzerland CH-19958475 Berichtszeitraum +Währung: CHF 28-Nov-2024 - 28-Nov-2024 +Seite 2 von 2 Konten: ETF Privat 80000/137772 Generiert um: 06-Dez-2024 \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/Kauf01.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/Kauf01.txt new file mode 100644 index 0000000000..839c17f993 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/Kauf01.txt @@ -0,0 +1,38 @@ +PDFBox Version: 1.8.17 +Portfolio Performance Version: 0.72.2 +----------------------------------------- +CH-19958475 - 19958475 +Bericht zu Trade-Details +Berichtszeitraum: 05-Dez-2024 - 05-Dez-2024 +Generiert um: 06-Dez-2024 06:52:00 18:52:00 (W. Europe Standard Time) +CH-19958475 Saxo Bank CH +Switzerland The Circle 38 +8058 Zürich-Flughafen +Switzerland +Saxo Bank CH / The Circle 38 / 8058 Zürich-Flughafen / Switzerland CH-19958475 Berichtszeitraum +Währung: CHF 05-Dez-2024 - 05-Dez-2024 +Seite 1 von 2 Konten: Sparkonto 80000/138521 Generiert um: 06-Dez-2024 +Trade details, CHF +Berichtszeitraum +05-Dez-2024 bis 05-Dez-2024 +ETF +Instrument iShares Core MSCI World UCITS ETF Handelszeit 05-Dez-2024 11:21:27 +ISIN IE00B4L5Y983 Valuta 09-Dez-2024 +Symbol SWDA:xswx Order-ID 5236807355 +Handelsplatz Exchange Trade-ID 6093088529 +Ordertyp Marktorder Gehandelter Wert -5.480,43 USD +K/V Kauf Preis 111,8455 USD +Eröffnung/Schluss To-Open Menge 49,00 +Spread-Kosten 0,00 CHF +Gesamte Trading-Kosten -19,41 CHF +Buchungsbetrag- Amount Umrechnungskosten Gebuchter Betrag +Trade-Datum Value Date Umrechnungskurs +ID USD CHF CHF +Schweizerische +Stempelgebühr 39683058642 05-Dez-2024 09-Dez-2024 -8,22 0,887182 -0,02 -7,29 +Ausland +Aktienbetrag 39679982249 05-Dez-2024 09-Dez-2024 -5.480,43 0,887182 -12,12 -4.862,14 +Nettobetrag - - - - - -12,14 -4.869,43 +Saxo Bank CH / The Circle 38 / 8058 Zürich-Flughafen / Switzerland CH-19958475 Berichtszeitraum +Währung: CHF 05-Dez-2024 - 05-Dez-2024 +Seite 2 von 2 Konten: Sparkonto 80000/138521 Generiert um: 06-Dez-2024 \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/SaxoBankPDFExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/SaxoBankPDFExtractorTest.java index 385261a6d7..ac404292a9 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/SaxoBankPDFExtractorTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/saxobank/SaxoBankPDFExtractorTest.java @@ -1,16 +1,29 @@ package name.abuchen.portfolio.datatransfer.pdf.saxobank; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.check; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.deposit; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasAmount; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasCurrencyCode; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasDate; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasFees; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasForexGrossValue; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasGrossValue; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasIsin; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasName; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasNote; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasShares; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasSource; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasTaxes; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasTicker; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasWkn; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.purchase; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.security; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countAccountTransactions; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countBuySell; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countSecurities; +import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.collection.IsEmptyCollection.empty; import java.util.ArrayList; @@ -19,14 +32,109 @@ import org.junit.Test; import name.abuchen.portfolio.datatransfer.Extractor.Item; +import name.abuchen.portfolio.datatransfer.ImportAction.Status; import name.abuchen.portfolio.datatransfer.actions.AssertImportActions; +import name.abuchen.portfolio.datatransfer.actions.CheckCurrenciesAction; import name.abuchen.portfolio.datatransfer.pdf.PDFInputFile; import name.abuchen.portfolio.datatransfer.pdf.SaxoBankPDFExtractor; import name.abuchen.portfolio.model.Client; +import name.abuchen.portfolio.model.Portfolio; +import name.abuchen.portfolio.model.PortfolioTransaction; +import name.abuchen.portfolio.model.Security; @SuppressWarnings("nls") public class SaxoBankPDFExtractorTest { + @Test + public void testWertpapierKauf01() + { + SaxoBankPDFExtractor extractor = new SaxoBankPDFExtractor(new Client()); + + List errors = new ArrayList<>(); + + List results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "Kauf01.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(1L)); + assertThat(countBuySell(results), is(1L)); + assertThat(countAccountTransactions(results), is(0L)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, "CHF"); + + // check security + assertThat(results, hasItem(security( // + hasIsin("IE00B4L5Y983"), hasWkn(null), hasTicker("SWDA"), // + hasName("iShares Core MSCI World UCITS ETF"), // + hasCurrencyCode("USD")))); + + // check buy sell transaction + assertThat(results, hasItem(purchase( // + hasDate("2024-12-05T11:21:27"), hasShares(49.00), // + hasSource("Kauf01.txt"), // + hasNote("Order-ID 5236807355 | Trade-ID 6093088529"), // + hasAmount("CHF", 4869.43), hasGrossValue("CHF", 4842.73), // + hasForexGrossValue("USD", 5458.55), // + hasTaxes("CHF", 7.29), hasFees("CHF", 19.41)))); + } + + @Test + public void testWertpapierKauf01WithSecurityInCHF() + { + Security security = new Security("iShares Core MSCI World UCITS ETF", "CHF"); + security.setIsin("IE00B4L5Y983"); + security.setTickerSymbol("SWDA"); + + Client client = new Client(); + client.addSecurity(security); + + SaxoBankPDFExtractor extractor = new SaxoBankPDFExtractor(client); + + List errors = new ArrayList<>(); + + List results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "Kauf01.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(0L)); + assertThat(countBuySell(results), is(1L)); + assertThat(countAccountTransactions(results), is(0L)); + assertThat(results.size(), is(1)); + new AssertImportActions().check(results, "CHF"); + + // check buy sell transaction + assertThat(results, hasItem(purchase( // + hasDate("2024-12-05T11:21:27"), hasShares(49.00), // + hasSource("Kauf01.txt"), // + hasNote("Order-ID 5236807355 | Trade-ID 6093088529"), // + hasAmount("CHF", 4869.43), hasGrossValue("CHF", 4842.73), // + hasTaxes("CHF", 7.29), hasFees("CHF", 19.41), // + check(tx -> { + CheckCurrenciesAction c = new CheckCurrenciesAction(); + Status s = c.process((PortfolioTransaction) tx, new Portfolio()); + assertThat(s, is(Status.OK_STATUS)); + })))); + } + + @Test + public void testCashTransfer01() + { + SaxoBankPDFExtractor extractor = new SaxoBankPDFExtractor(new Client()); + + List errors = new ArrayList<>(); + + List results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "CashTransfer01.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(0L)); + assertThat(countBuySell(results), is(0L)); + assertThat(countAccountTransactions(results), is(1L)); + assertThat(results.size(), is(1)); + new AssertImportActions().check(results, "CHF"); + + // assert transaction + assertThat(results, hasItem(deposit(hasDate("2024-11-28"), hasAmount("CHF", 4600.00), // + hasSource("CashTransfer01.txt"), hasNote("39482097030")))); + } + @Test public void testKontoauszug01() { diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java index bd989c9d76..4db837ebce 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/ExtractorUtils.java @@ -187,6 +187,8 @@ private static DateTimeFormatter createFormatter(String pattern, Locale locale) createFormatter("dd.MM.yyyy H:mm:ss", Locale.GERMANY), // createFormatter("d LLL yyyy HH:mm:ss", Locale.GERMANY), // createFormatter("dd LLL yyyy HH:mm:ss", Locale.GERMANY), // + createFormatter("d-LLL-yyyy HH:mm:ss", Locale.GERMANY), // + createFormatter("dd-LLL-yyyy HH:mm:ss", Locale.GERMANY), // createFormatter("d MMMM yyyy HH:mm:ss", Locale.GERMANY), // createFormatter("dd MMMM yyyy HH:mm:ss", Locale.GERMANY), // createFormatter("d. MMMM yyyy HH:mm:ss", Locale.GERMANY), // @@ -197,7 +199,6 @@ private static DateTimeFormatter createFormatter(String pattern, Locale locale) createFormatter("dd/MM/yyyy HH:mm:ss", Locale.GERMANY), // createFormatter("LLL d, yyyy hh:mm:ss a", Locale.US), // createFormatter("LLL dd, yyyy hh:mm:ss a", Locale.US), // - createFormatter("yyyy-LL-dd HH:mm:ss", Locale.US), // createFormatter("yyyy-LL-d HH:mm:ss", Locale.US), // createFormatter("yyyy-LL-dd HH:mm:ss", Locale.US), // createFormatter("yyyyLLd HHmmss", Locale.US), // diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/SaxoBankPDFExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/SaxoBankPDFExtractor.java index d73a9f9ba9..9423b3a4f2 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/SaxoBankPDFExtractor.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/SaxoBankPDFExtractor.java @@ -1,10 +1,19 @@ package name.abuchen.portfolio.datatransfer.pdf; +import static name.abuchen.portfolio.datatransfer.ExtractorUtils.checkAndSetGrossUnit; +import static name.abuchen.portfolio.util.TextUtil.concatenate; +import static name.abuchen.portfolio.util.TextUtil.trim; + +import name.abuchen.portfolio.datatransfer.ExtrExchangeRate; +import name.abuchen.portfolio.datatransfer.ExtractorUtils; import name.abuchen.portfolio.datatransfer.pdf.PDFParser.Block; import name.abuchen.portfolio.datatransfer.pdf.PDFParser.DocumentType; import name.abuchen.portfolio.datatransfer.pdf.PDFParser.Transaction; 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.money.Money; @SuppressWarnings("nls") public class SaxoBankPDFExtractor extends AbstractPDFExtractor @@ -15,6 +24,8 @@ public SaxoBankPDFExtractor(Client client) addBankIdentifier("Saxo Bank"); + addBuySellTransaction(); + addDepositTransaction(); addAccountStatementTransaction(); } @@ -24,6 +35,162 @@ public String getLabel() return "Saxo Bank A/S"; } + private void addBuySellTransaction() + { + final DocumentType type = new DocumentType("Bericht zu Trade\\-Details", // + documentContext -> documentContext // + // @formatter:off + // Währung: CHF 05-Dez-2024 - 05-Dez-2024 + // @formatter:on + .section("currency") // + .match("^W.hrung: (?[\\w]{3}).*$") // + .assign((ctx, v) -> ctx.put("currency", asCurrencyCode(v.get("currency"))))); + + this.addDocumentTyp(type); + + Transaction pdfTransaction = new Transaction<>(); + + Block firstRelevantLine = new Block("^Berichtszeitraum:.*$"); + type.addBlock(firstRelevantLine); + firstRelevantLine.set(pdfTransaction); + + pdfTransaction // + + .subject(() -> { + BuySellEntry portfolioTransaction = new BuySellEntry(); + portfolioTransaction.setType(PortfolioTransaction.Type.BUY); + return portfolioTransaction; + }) + + .oneOf( // + // @formatter:off + // Instrument iShares Core MSCI World UCITS ETF Handelszeit 05-Dez-2024 11:21:27 + // ISIN IE00B4L5Y983 Valuta 09-Dez-2024 + // Symbol SWDA:xswx Order-ID 5236807355 + // Handelsplatz Exchange Trade-ID 6093088529 + // Ordertyp Marktorder Gehandelter Wert -5.480,43 USD + // @formatter:on + section -> section // + .attributes("name", "isin", "tickerSymbol", "currency") // + .match("^Instrument (?.*) Handelszeit.*$") // + .match("^ISIN (?[A-Z]{2}[A-Z0-9]{9}[0-9]) Valuta.*$") // + .match("^Symbol (?[\\w]{3,4}):.*$") // + .match("^Ordertyp .* \\-[\\.,\\d]+ (?[\\w]{3})$") // + .assign((t, v) -> t.setSecurity(getOrCreateSecurity(v)))) + + .oneOf( // + // @formatter:off + // Eröffnung/Schluss To-Open Menge 49,00 + // @formatter:on + section -> section // + .attributes("shares") // + .match("^Er.ffnung\\/Schluss To-Open Menge (?[\\.,\\d]+)$") // + .assign((t, v) -> t.setShares(asShares(v.get("shares"))))) + + .oneOf( // + // @formatter:off + // Instrument iShares Core MSCI World UCITS ETF Handelszeit 05-Dez-2024 11:21:27 + // @formatter:on + section -> section // + .attributes("date", "time") // + .match("^.* Handelszeit (?[\\d]{2}\\-[\\w]+\\-[\\d]{4}) (?