From f59d01cbf1a980527f6e9dec414818dfd13a809a Mon Sep 17 00:00:00 2001 From: Alexander Ott Date: Sat, 11 Oct 2025 08:53:27 +0200 Subject: [PATCH] Modify Bourse Direct PDF-Importer to support new transaction Closes #5074 --- .../BourseDirectPDFExtractorTest.java | 80 ++++++++++- .../{Dividende01.txt => ReleveDeCompte03.txt} | 0 .../pdf/boursedirect/ReleveDeCompte04.txt | 18 +++ .../pdf/boursedirect/ReleveDeCompte05.txt | 31 +++++ .../pdf/BourseDirectPDFExtractor.java | 131 ++++++++++++++++-- 5 files changed, 245 insertions(+), 15 deletions(-) rename name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/{Dividende01.txt => ReleveDeCompte03.txt} (100%) create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte04.txt create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte05.txt diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/BourseDirectPDFExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/BourseDirectPDFExtractorTest.java index d4150ea1d2..c427ebddd5 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/BourseDirectPDFExtractorTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/BourseDirectPDFExtractorTest.java @@ -1,5 +1,6 @@ package name.abuchen.portfolio.datatransfer.pdf.boursedirect; +import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.deposit; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.dividend; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasAmount; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.hasCurrencyCode; @@ -15,8 +16,10 @@ 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.sale; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.security; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countAccountTransactions; +import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countAccountTransfers; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countBuySell; import static name.abuchen.portfolio.datatransfer.ExtractorTestUtilities.countSecurities; import static org.hamcrest.CoreMatchers.hasItem; @@ -50,6 +53,7 @@ public void testReleveDeCompte01() assertThat(countSecurities(results), is(3L)); assertThat(countBuySell(results), is(3L)); assertThat(countAccountTransactions(results), is(0L)); + assertThat(countAccountTransfers(results), is(0L)); assertThat(results.size(), is(6)); new AssertImportActions().check(results, "EUR"); @@ -109,6 +113,7 @@ public void testReleveDeCompte02() assertThat(countSecurities(results), is(5L)); assertThat(countBuySell(results), is(5L)); assertThat(countAccountTransactions(results), is(0L)); + assertThat(countAccountTransfers(results), is(0L)); assertThat(results.size(), is(10)); new AssertImportActions().check(results, "EUR"); @@ -184,18 +189,19 @@ public void testReleveDeCompte02() } @Test - public void testDividende01() + public void testReleveDeCompte03() { var extractor = new BourseDirectPDFExtractor(new Client()); List errors = new ArrayList<>(); - var results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "Dividende01.txt"), errors); + var results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "ReleveDeCompte03.txt"), errors); assertThat(errors, empty()); assertThat(countSecurities(results), is(1L)); assertThat(countBuySell(results), is(0L)); assertThat(countAccountTransactions(results), is(1L)); + assertThat(countAccountTransfers(results), is(0L)); assertThat(results.size(), is(2)); new AssertImportActions().check(results, "EUR"); @@ -208,9 +214,77 @@ public void testDividende01() // check dividends transaction assertThat(results, hasItem(dividend( // hasDate("2025-08-06T00:00"), hasShares(3.00), // - hasSource("Dividende01.txt"), // + hasSource("ReleveDeCompte03.txt"), // hasNote(null), // hasAmount("EUR", 4.08), hasGrossValue("EUR", 4.80), // hasTaxes("EUR", 0.72), hasFees("EUR", 0.00)))); } + + @Test + public void testReleveDeCompte04() + { + var extractor = new BourseDirectPDFExtractor(new Client()); + + List errors = new ArrayList<>(); + + var results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "ReleveDeCompte04.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(0L)); + assertThat(countBuySell(results), is(0L)); + assertThat(countAccountTransactions(results), is(1L)); + assertThat(countAccountTransfers(results), is(0L)); + assertThat(results.size(), is(1)); + new AssertImportActions().check(results, "EUR"); + + // assert transaction + assertThat(results, hasItem(deposit(hasDate("2021-02-24"), hasAmount("EUR", 2400.00), // + hasSource("ReleveDeCompte04.txt"), hasNote(null)))); + } + + @Test + public void testReleveDeCompte05() + { + var extractor = new BourseDirectPDFExtractor(new Client()); + + List errors = new ArrayList<>(); + + var results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "ReleveDeCompte05.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(2L)); + assertThat(countBuySell(results), is(2L)); + assertThat(countAccountTransactions(results), is(0L)); + assertThat(countAccountTransfers(results), is(0L)); + assertThat(results.size(), is(4)); + new AssertImportActions().check(results, "EUR"); + + // check security + assertThat(results, hasItem(security( // + hasIsin("US0995021062"), hasWkn(null), hasTicker(null), // + hasName("BOOZ ALLEN CL.A"), // + hasCurrencyCode("USD")))); + + // check security + assertThat(results, hasItem(security( // + hasIsin("US5024311095"), hasWkn(null), hasTicker(null), // + hasName("L3HARRIS TECHN."), // + hasCurrencyCode("USD")))); + + // check buy sell transaction + assertThat(results, hasItem(sale( // + hasDate("2021-03-16T19:50:11"), hasShares(45.00), // + hasSource("ReleveDeCompte05.txt"), // + hasNote(null), // + hasAmount("EUR", 2977.53), hasGrossValue("EUR", 2986.04), // + hasTaxes("EUR", 0.01), hasFees("EUR", 8.50)))); + + // check buy sell transaction + assertThat(results, hasItem(sale( // + hasDate("2021-03-16T19:45:03"), hasShares(20.00), // + hasSource("ReleveDeCompte05.txt"), // + hasNote(null), // + hasAmount("EUR", 3178.78), hasGrossValue("EUR", 3187.29), // + hasTaxes("EUR", 0.01), hasFees("EUR", 8.50)))); + } } diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/Dividende01.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte03.txt similarity index 100% rename from name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/Dividende01.txt rename to name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte03.txt diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte04.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte04.txt new file mode 100644 index 0000000000..971440421b --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte04.txt @@ -0,0 +1,18 @@ +PDFBox Version: 3.0.5 +Portfolio Performance Version: 0.80.2 +System: win32 | x86_64 | 21.0.5+11-LTS | Azul Systems, Inc. +----------------------------------------- +1/1 +Nous vous prions de trouver ci-dessous votre relevé d'opérations. Sans observation de votre part au +sujet du présent relevé, nous le considérerons comme ayant obtenu votre accord. Veuillez agréer nos +salutations distinguées. +Le 24/02/2021 +Avis d'Opération +COMPTE N° 508TI00085554410EUR Ordinaire MR XKCFj JjkDQO +192 ROUTE DE CAZKIsv +MR ueWCI UVLkVC 60763 gAkPFv +Date Désignation Débit (€) Crédit (€) + 24/02/2021 VIREMENT ESPECES VIRT M. ET/OU MME 2 400,00 +Sous réserve de bonne fin / Ce relevé ne constitue pas une facture +Les montants des colonnes Débit et Crédit sont stipulés TVA Comprise +Bourse Direct , SA au capital de 13.988.845,75 €, R.C.S Paris B 408 790 608, Siège Social : 374 rue Saint-Honoré, 75001 Paris - Groupe VIEL et Cie \ No newline at end of file diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte05.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte05.txt new file mode 100644 index 0000000000..eadca66de3 --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/boursedirect/ReleveDeCompte05.txt @@ -0,0 +1,31 @@ +PDFBox Version: 3.0.5 +Portfolio Performance Version: 0.80.2 +System: win32 | x86_64 | 21.0.5+11-LTS | Azul Systems, Inc. +----------------------------------------- +1/1 +Nous vous prions de trouver ci-dessous votre relevé d'opérations. Sans observation de votre part au +sujet du présent relevé, nous le considérerons comme ayant obtenu votre accord. Veuillez agréer nos +salutations distinguées. +Le 16/03/2021 +Avis d'Opération +COMPTE N° 508TI00085554410EUR Ordinaire MR SLlPD yVlqGo +192 ROUTE DE qRRZPSm +MR lYoBQ xsdjTp 77158 IvQiGu +Date Désignation Débit (€) Crédit (€) + 16/03/2021 VENTE ETRANGER US0995021062 BOOZ ALLEN CL.A 2 977,53 + QUANTITE : -45 + COURS : +66,356440254 BRUT : +2 986,04 + COURTAGE : +8,50 TVA : +0,00 + COURS EN USD : +79,2 TX USD/EUR : +1,193554080 + TAXE ETRANG : +0,01 + Heure Execution: 19:50:11 Lieu: NEW YORK STOCK EXCHANGE, INC. + 16/03/2021 VENTE ETRANGER US5024311095 L3HARRIS TECHN. 3 178,78 + QUANTITE : -20 + COURS : +159,364458794 BRUT : +3 187,29 + COURTAGE : +8,50 TVA : +0,00 + COURS EN USD : +190,2101 TX USD/EUR : +1,193554080 + TAXE ETRANG : +0,01 + Heure Execution: 19:45:03 Lieu: NEW YORK STOCK EXCHANGE, INC. +Sous réserve de bonne fin / Ce relevé ne constitue pas une facture +Les montants des colonnes Débit et Crédit sont stipulés TVA Comprise +Bourse Direct , SA au capital de 13.988.845,75 €, R.C.S Paris B 408 790 608, Siège Social : 374 rue Saint-Honoré, 75001 Paris - Groupe VIEL et Cie \ No newline at end of file diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/BourseDirectPDFExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/BourseDirectPDFExtractor.java index 22abf04bcd..ec82264903 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/BourseDirectPDFExtractor.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/BourseDirectPDFExtractor.java @@ -1,5 +1,6 @@ package name.abuchen.portfolio.datatransfer.pdf; +import static name.abuchen.portfolio.datatransfer.ExtractorUtils.checkAndSetGrossUnit; import static name.abuchen.portfolio.datatransfer.ExtractorUtils.checkAndSetTax; import java.time.LocalDateTime; @@ -31,6 +32,7 @@ public BourseDirectPDFExtractor(Client client) addBuySellTransaction(); addDividendeTransaction(); + addDepositTransaction(); } @Override @@ -83,7 +85,7 @@ private void addBuySellTransaction() var pdfTransaction = new Transaction(); - var firstRelevantLine = new Block("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}ACHAT.*$", "^.*Heure Execution:.*$"); + var firstRelevantLine = new Block("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}(ACHAT|VENTE).*$", "^.*Heure Execution:.*$"); type.addBlock(firstRelevantLine); firstRelevantLine.set(pdfTransaction); @@ -95,37 +97,61 @@ private void addBuySellTransaction() return portfolioTransaction; }) - // @formatter:off - // 10/12/2024 ACHAT COMPTANT FR0011550185 BNPP S&P500EUR ETF 4 978,30 - // 05/08/2025 ACHAT ETRANGER IE00B4BNMY34 ACCENTURE CL.A 216,93 - // @formatter:on - .section("isin", "name") // - .documentContext("currency") // - .match("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}ACHAT.*[\\s]{1,}(?[A-Z]{2}[A-Z0-9]{9}[0-9])[\\s]{1,}(?.*)[\\s]{2,}[\\d\\s]+,[\\d]{2}$") // - .assign((t, v) -> t.setSecurity(getOrCreateSecurity(v))) + // Is type --> "VENTE" change from BUY to SELL + .section("type").optional() // + .match("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}(?(ACHAT|VENTE)).*$") // + .assign((t, v) -> { + if ("VENTE".equals(v.get("type"))) // + t.setType(PortfolioTransaction.Type.SELL); + }) + + .oneOf( // + // @formatter:off + // 16/03/2021 VENTE ETRANGER US0995021062 BOOZ ALLEN CL.A 2 977,53 + // COURS EN USD : +79,2 TX USD/EUR : +1,193554080 + // @formatter:on + section -> section // + .attributes("name", "currency", "isin") // + .match("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}(ACHAT|VENTE).*[\\s]{1,}(?[A-Z]{2}[A-Z0-9]{9}[0-9])[\\s]{1,}(?.*)[\\s]{2,}[\\d\\s]+,[\\d]{2}$") // + .match("^.*COURS EN [A-Z]{3} :[\\s]{1,}\\+[\\.,\\d]+[\\s]{1,}TX (?[A-Z]{3})\\/[A-Z]{3} :[\\s]{1,}\\+[\\.,\\d]+$") // + .assign((t, v) -> t.setSecurity(getOrCreateSecurity(v))), + // @formatter:off + // 10/12/2024 ACHAT COMPTANT FR0011550185 BNPP S&P500EUR ETF 4 978,30 + // 05/08/2025 ACHAT ETRANGER IE00B4BNMY34 ACCENTURE CL.A 216,93 + // @formatter:on + section -> section // + .attributes("isin", "name") // + .documentContext("currency") // + .match("^.*[\\d]{2}\\/[\\d]{2}\\/[\\d]{4}[\\s]{1,}(ACHAT|VENTE).*[\\s]{1,}(?[A-Z]{2}[A-Z0-9]{9}[0-9])[\\s]{1,}(?.*)[\\s]{2,}[\\d\\s]+,[\\d]{2}$") // + .assign((t, v) -> t.setSecurity(getOrCreateSecurity(v)))) // @formatter:off // QUANTITE : +173 + // QUANTITE : -45 // @formatter:on .section("shares") // - .match("^.*QUANTITE :[\\s]{1,}\\+(?[\\d\\s]+(,[\\d]{2})?)$") // + .match("^.*QUANTITE :[\\s]{1,}[\\-|\\+](?[\\d\\s]+(,[\\d]{2})?)$") // .assign((t, v) -> t.setShares(asShares(v.get("shares")))) // @formatter:off // 10/12/2024 ACHAT COMPTANT FR0011550185 BNPP S&P500EUR ETF 4 978,30 // Heure Execution: 09:04:28 Lieu: EURONEXT - EURONEXT PARIS + // + // 16/03/2021 VENTE ETRANGER US0995021062 BOOZ ALLEN CL.A 2 977,53 + // Heure Execution: 19:50:11 Lieu: NEW YORK STOCK EXCHANGE, INC. // @formatter:on .section("date", "time") // - .match("^.*(?[\\d]{2}\\/[\\d]{2}\\/[\\d]{4})[\\s]{1,}ACHAT.*$") // + .match("^.*(?[\\d]{2}\\/[\\d]{2}\\/[\\d]{4})[\\s]{1,}(ACHAT|VENTE).*$") // .match("^.*Heure Execution: (?