diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/CryptoKauf01.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/CryptoKauf01.txt new file mode 100644 index 0000000000..9041e06695 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/CryptoKauf01.txt @@ -0,0 +1,30 @@ +PDFBox Version: 1.8.17 +Portfolio Performance Version: 0.72.2 +----------------------------------------- +PostFinance AG +Sie werden betreut von +KmLaf STJiQ oTqDQh und Team Post CH AG +Telefon +41 20 486 17 12 +www.postfinance.ch P.P. CH-4808 Zofingen A-PRIORITY +yObY +xCqJFfs rrSEgT WlfX BapaDg ICOS Pkoibzr +GweJpN gqJCiOWpctTNeseNL 69 +6979 eQAbpo +Transaktion: Kauf Seite: 1 / 1 +Krypto Datum: 02.12.2024 +Kryptoportfolio 33-705809-6 Auftrag 37463178 +Datum der 01.12.2024 +Auftragserteilung +Ihr Auftrag wurde wie folgt am 01.12.2024 ausgeführt: +Position Anzahl Währung Kurs Betrag +Bitcoin BTC 0.006124 USD 97 376.126639 +Kurswert in Handelswährung USD 596.33 +Handelsgebühr USD 5.67 +Total USD 602.00 +Total in Kontowährung zum Kurs von USD/CHF 0.8949 538.73 +Der Totalbetrag von CHF 538.73 wurde Ihrem Konto bD93 4072 6753 0009 6042 0 mit Valuta 01.12.2024 belastet. +Bitte prüfen Sie dieses Dokument und benachrichtigen Sie uns bei Unstimmigkeiten innert Monatsfrist. +Im Rahmen dieser Transaktion hat PostFinance als Kommissionärin gehandelt. +Freundliche Grüsse +PostFinance AG +0000011111888883333355555 DDDDDEEEEE 000000000000000000000000055555.....0000000000 \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/PostfinancePDFExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/PostfinancePDFExtractorTest.java index e14c1b5288..57f906edec 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/PostfinancePDFExtractorTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/postfinance/PostfinancePDFExtractorTest.java @@ -5,7 +5,10 @@ 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.hasFeed; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasFeedProperty; 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; @@ -44,6 +47,7 @@ import name.abuchen.portfolio.datatransfer.actions.CheckCurrenciesAction; import name.abuchen.portfolio.datatransfer.pdf.PDFInputFile; import name.abuchen.portfolio.datatransfer.pdf.PostfinancePDFExtractor; +import name.abuchen.portfolio.datatransfer.pdf.TestCoinSearchProvider; import name.abuchen.portfolio.model.Account; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.BuySellEntry; @@ -54,10 +58,21 @@ import name.abuchen.portfolio.money.CurrencyUnit; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; +import name.abuchen.portfolio.online.SecuritySearchProvider; +import name.abuchen.portfolio.online.impl.CoinGeckoQuoteFeed; @SuppressWarnings("nls") public class PostfinancePDFExtractorTest { + PostfinancePDFExtractor extractor = new PostfinancePDFExtractor(new Client()) + { + @Override + protected List lookupCryptoProvider() + { + return TestCoinSearchProvider.cryptoProvider(); + } + }; + @Test public void testWertpapierKauf01() { @@ -421,6 +436,38 @@ public void testWertpapierVerkauf01() is(Money.of("CHF", Values.Amount.factorize(2.60)))); } + @Test + public void testCryptoKauf01() + { + List errors = new ArrayList<>(); + + List results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "CryptoKauf01.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(null), hasWkn(null), hasTicker("BTC"), // + hasName("Bitcoin"), // + hasCurrencyCode("USD"), // + hasFeed(CoinGeckoQuoteFeed.ID), // + hasFeedProperty(CoinGeckoQuoteFeed.COINGECKO_COIN_ID, "bitcoin")))); + + // check buy sell transaction + assertThat(results, hasItem(purchase( // + hasDate("2024-12-01T00:00"), hasShares(0.006124), // + hasSource("CryptoKauf01.txt"), // + hasNote("Auftrag 37463178"), // + hasAmount("CHF", 538.73), hasGrossValue("CHF", 533.66), // + hasForexGrossValue("USD", 596.33), // + hasTaxes("CHF", 0.00), hasFees("CHF", 5.07)))); + } + @Test public void testDividende01() { diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/PostfinancePDFExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/PostfinancePDFExtractor.java index ed704e461f..5ddc661bf2 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/PostfinancePDFExtractor.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/PostfinancePDFExtractor.java @@ -42,6 +42,7 @@ public PostfinancePDFExtractor(Client client) addBankIdentifier("PostFinance"); addBuySellTransaction(); + addBuySellCryptoTransaction(); addSettlementTransaction(); addDividendeTransaction(); addPaymentTransaction(); @@ -155,6 +156,87 @@ private void addBuySellTransaction() addFeesSectionsTransaction(pdfTransaction, type); } + private void addBuySellCryptoTransaction() + { + final DocumentType type = new DocumentType("Krypto"); + this.addDocumentTyp(type); + + Transaction pdfTransaction = new Transaction<>(); + + Block firstRelevantLine = new Block("^Krypto .*$"); + type.addBlock(firstRelevantLine); + firstRelevantLine.set(pdfTransaction); + + pdfTransaction // + + .subject(() -> { + BuySellEntry portfolioTransaction = new BuySellEntry(); + portfolioTransaction.setType(PortfolioTransaction.Type.BUY); + return portfolioTransaction; + }) + + // @formatter:off + // Bitcoin BTC 0.006124 USD 97 376.126639 + // @formatter:on + .section("name", "tickerSymbol", "currency") // + .match("^(?.*) (?[A-Z]+) [\\.'\\d]+ (?[\\w]{3}) [\\.'\\d]+ [\\.'\\d]+.*$") // + .assign((t, v) -> t.setSecurity(getOrCreateCryptoCurrency(v))) + + // @formatter:off + // Bitcoin BTC 0.006124 USD 97 376.126639 + // @formatter:on + .section("shares") // + .match("^.* [A-Z]+ (?[\\.'\\d]+) [\\w]{3} [\\.'\\d]+ [\\.'\\d]+.*$") // + .assign((t, v) -> t.setShares(asShares(v.get("shares")))) + + // @formatter:off + // Ihr Auftrag wurde wie folgt am 01.12.2024 ausgeführt: + // @formatter:on + .section("date") // + .match("^Ihr Auftrag wurde wie folgt am[\\s]{1,}(?[\\d]{2}\\.[\\d]{2}\\.[\\d]{4}) ausgef.hrt:.*$") // + .assign((t, v) -> t.setDate(asDate(v.get("date")))) + + // @formatter:off + // Der Totalbetrag von CHF 538.73 wurde Ihrem Konto bD93 4072 6753 0009 6042 0 mit Valuta 01.12.2024 belastet. + // @formatter:on + .section("currency", "amount") // + .match("^Der Totalbetrag von[\\s]{1,}(?[\\w]{3}) (?[\\.'\\d]+) wurde Ihrem Konto .*$") // + .assign((t, v) -> { + t.setCurrencyCode(asCurrencyCode(v.get("currency"))); + t.setAmount(asAmount(v.get("amount"))); + }) + + + // @formatter:off + // Kurswert in Handelswährung USD 596.33 + // Total in Kontowährung zum Kurs von USD/CHF 0.8949 538.73 + // @formatter:on + .section("fxGross", "termCurrency", "baseCurrency", "exchangeRate") // + .match("^Kurswert in Handelsw.hrung [\\w]{3} (?[\\.'\\d]+).*$") // + .match("^Total in Kontow.hrung zum Kurs von (?[\\w]{3})\\/(?[\\w]{3}) (?[\\.'\\d]+) [\\.'\\d]+.*$") // + .assign((t, v) -> { + ExtrExchangeRate rate = asExchangeRate(v); + type.getCurrentContext().putType(rate); + + Money fxGross = Money.of(rate.getBaseCurrency(), asAmount(v.get("fxGross"))); + Money gross = rate.convert(rate.getTermCurrency(), fxGross); + + checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); + }) + + // @formatter:off + // Kryptoportfolio 33-705809-6 Auftrag 37463178 + // @formatter:on + .section("note").optional() // + .match("^Kryptoportfolio [\\d\\-]+ (?Auftrag [\\d]+).*$") // + .assign((t, v) -> t.setNote(trim(v.get("note")))) + + .wrap(BuySellEntryItem::new); + + addTaxesSectionsTransaction(pdfTransaction, type); + addFeesSectionsTransaction(pdfTransaction, type); + } + private void addSettlementTransaction() { DocumentType type = new DocumentType("Transaktionsabrechnung: (Zeichnung|Fondssparplan)"); @@ -1085,6 +1167,13 @@ private > void addFeesSectionsTransaction(T transaction // @formatter:on .section("currency", "fee").optional() // .match("^B.rsengeb.hren und sonstige Spesen (?[\\w]{3}) (?[\\.'\\d\\s]+).*$") // + .assign((t, v) -> processFeeEntries(t, v, type)) + + // @formatter:off + // Handelsgebühr USD 5.67 + // @formatter:on + .section("currency", "fee").optional() // + .match("^Handelsgeb.hr (?[\\w]{3}) (?[\\.'\\d\\s]+).*$") // .assign((t, v) -> processFeeEntries(t, v, type)); } diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Security.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Security.java index 0cdabaed55..488b962072 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Security.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Security.java @@ -939,4 +939,5 @@ private boolean notEmpty(String s) { return s != null && s.length() > 0; } + }