From 698d6f36d09b22c7631abfccf0a55a699a1b3a84 Mon Sep 17 00:00:00 2001 From: Nirus2000 Date: Tue, 3 Oct 2023 09:10:45 +0200 Subject: [PATCH] Remove ATTRIBUTE_EXCHANGE_RATE Co-authored-by: buchen --- .../comdirect/ComdirectPDFExtractorTest.java | 60 +++-- .../pdf/comdirect/Dividende27.txt | 64 +++++ .../pdf/ComdirectPDFExtractor.java | 251 ++++++++++-------- 3 files changed, 251 insertions(+), 124 deletions(-) create mode 100644 name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/Dividende27.txt diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/ComdirectPDFExtractorTest.java b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/ComdirectPDFExtractorTest.java index 8435735890..a41631eab9 100644 --- a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/ComdirectPDFExtractorTest.java +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/ComdirectPDFExtractorTest.java @@ -1,13 +1,5 @@ package name.abuchen.portfolio.datatransfer.pdf.comdirect; -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.collection.IsEmptyCollection.empty; - import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.check; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.deposit; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.dividend; @@ -35,6 +27,13 @@ import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.taxRefund; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.taxes; import static name.abuchen.portfolio.datatransfer.ExtractorMatchers.withFailureMessage; +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.collection.IsEmptyCollection.empty; import java.util.ArrayList; import java.util.List; @@ -460,7 +459,6 @@ public void testWertpapierKaufMitSteuerbehandlung05() hasSource("KaufMitSteuerbehandlung05.txt"), // hasNote(null), // hasAmount("EUR", 0.00), hasGrossValue("EUR", 0.00), // - hasForexGrossValue("USD", 0.00), // hasTaxes("EUR", 0.00), hasFees("EUR", 0.00))))); } @@ -594,7 +592,6 @@ public void testWertpapierKaufMitSteuerbehandlung07() hasSource("KaufMitSteuerbehandlung07.txt"), // hasNote(null), // hasAmount("EUR", 0.00), hasGrossValue("EUR", 0.00), // - hasForexGrossValue("USD", 0.00), // hasTaxes("EUR", 0.00), hasFees("EUR", 0.00))))); } @@ -686,7 +683,6 @@ public void testWertpapierKaufMitSteuerbehandlung08() hasSource("KaufMitSteuerbehandlung08.txt"), // hasNote(null), // hasAmount("EUR", 0.00), hasGrossValue("EUR", 0.00), // - hasForexGrossValue("GBP", 0.00), // hasTaxes("EUR", 0.00), hasFees("EUR", 0.00))))); } @@ -1030,7 +1026,6 @@ public void testWertpapierKaufMitSteuerbehandlung15() hasSource("KaufMitSteuerbehandlung15.txt"), // hasNote(null), // hasAmount("EUR", 0.00), hasGrossValue("EUR", 0.00), // - hasForexGrossValue("USD", 0.00), // hasTaxes("EUR", 0.00), hasFees("EUR", 0.00))))); } @@ -4448,6 +4443,39 @@ public void testDividende25() hasTaxes("EUR", 0.00), hasFees("EUR", 0.00))))); } + @Test + public void testDividende27() + { + ComdirectPDFExtractor extractor = new ComdirectPDFExtractor(new Client()); + + List errors = new ArrayList<>(); + + List results = extractor.extract(PDFInputFile.loadTestCase(getClass(), "Dividende27.txt"), errors); + + assertThat(errors, empty()); + assertThat(countSecurities(results), is(1L)); + assertThat(countBuySell(results), is(0L)); + assertThat(countAccountTransactions(results), is(1L)); + assertThat(results.size(), is(2)); + new AssertImportActions().check(results, CurrencyUnit.EUR); + + // check security + assertThat(results, hasItem(security( // + hasIsin("DE0009848002"), hasWkn("984800"), hasTicker(null), // + hasName("D WS In te rn e t - A k t i en T y p O I n ha b er - A nt e il e"), // + hasCurrencyCode("EUR")))); + + // check cancellation transaction + assertThat(results, hasItem(withFailureMessage( // + Messages.MsgErrorTransactionTypeNotSupported, // + dividend( // + hasDate("2006-10-02T00:00"), hasShares(67.000), // + hasSource("Dividende27.txt"), // + hasNote(null), // + hasAmount("EUR", 1.28), hasGrossValue("EUR", 1.34), // + hasTaxes("EUR", 0.06), hasFees("EUR", 0.00))))); + } + @Test public void testVorabpauschaleSteuerbehandlung01() { @@ -5026,14 +5054,14 @@ public void testCheckIfSellWithTwoBuyTaxesTransactionsOnTheSameDate() // "2013-05-15T00":00={ // "Google Inc. Reg. Shares Class A DL -", // 001=[ - // 15.05.2013 Verkauf EUR 1.366, 60 Google Inc. Reg. Shares Class A DL -, 001 Test03.txt, - // 15.05.2013 Steuern EUR 0,00 Google Inc. Reg. Shares Class A DL -, 001 Test03.txt + // 15.05.2013 Verkauf EUR 1.366, 60 Google Inc. Reg. Shares Class A DL -, 001 VerkaufMitSteuerbehandlung13.txt, + // 15.05.2013 Steuern EUR 0,00 Google Inc. Reg. Shares Class A DL -, 001 VerkaufMitSteuerbehandlung13.txt // ] // }, // "2022-10-04T00":00={ // "BASF SE Namens-Aktien o.N.="[ - // 04.10.2022 Steuern EUR 0,00 BASF SE Namens-Aktien o.N. Test01.txt, - // 04.10.2022 Steuern EUR 0,00 BASF SE Namens-Aktien o.N. Test02.txt + // 04.10.2022 Steuern EUR 0,00 BASF SE Namens-Aktien o.N. KaufMitSteuerbehandlung13.txt, + // 04.10.2022 Steuern EUR 0,00 BASF SE Namens-Aktien o.N. KaufMitSteuerbehandlung14.txt // ] // } // @formatter:off diff --git a/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/Dividende27.txt b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/Dividende27.txt new file mode 100644 index 0000000000..8eee2aabfd --- /dev/null +++ b/name.abuchen.portfolio.tests/src/name/abuchen/portfolio/datatransfer/pdf/comdirect/Dividende27.txt @@ -0,0 +1,64 @@ +``` +PDFBox Version: 1.8.17 +Portfolio Performance Version: 0.65.2.qualifier +----------------------------------------- + +25449 Quickborn + + + + + + + + + 73235 024 Depo tnummer: 3865094 00 +Herrn +TsYJVv kZkz + + +Musterstr. 75 + +05764 GnrsCSxZFU +05.10.2006 +G u t s c h ri f t fä ll ig e r W e r t p a p i e r -E r tr ä g e +Ertragsthesaurierung bei Freistellungsauftrag +Depotbestand Wertpapier-Bezeichnung WKN/ISIN + p er 2 9 .0 9 . 2 0 0 6 D WS In te rn e t - A k t i en T y p O 9 8 48 0 0 +S T K 67 , 0 00 I n ha b er - A nt e il e D E0 0 0 98 4 8 00 2 + Emissionsland: DEUTSCHLAND +EUR 0,0166 Thesaurierung pro Stück für Geschäftsjahr 01.10.05 bis 30.09.06 +EUR 0,00 Anteil Körperschaftsteuerminderungsbetrag pro Stück +Zufluß: 30.09.2006 + EUR 0,0607 Zinsabschlagpflichtiger Anteil + Ermittlung der Steuerbasisbeträge +Nettothesaurierung EUR 1,11 +zinsabschlagpflichtiger Anteil / Gesamtbetrag EUR 4,07 +Zwischensumme zur Disposition des Freistellungsbetrags EUR 4,07 +- berücksichtigter Freibetrag EUR 4,07 - +zinsabschlagpflichtig EUR 0,00 + gesamt vergütet bescheinigt +Zinsabschlag 30,000 % EUR 1,22 1,22 0,00 +Solidaritätszuschlag EUR 0,06 0,06 0,00 + K E I N E B A R A U S S C H Ü T T U N G + Abrechnung gem. Freistellung zu vergütender Steuern + +30,000 % Zinsabschlag auf EUR 4,07 EUR 1,22 + 5,500 % Solidaritätszuschl. auf EUR 1,22 EUR 0,06 +Gutschrift auf Konto Valuta Zu Ihren Gunsten +1189949 01 EUR 02.10.2006 EUR 1,28 +anrechenbare Quellensteuer Privatvermögen/Betriebsvermögen EUR 1,67 +Wegen der Besteuerung im Einzelfall weisen wir auf den Rechenschaftsbericht +der Gesellschaft hin. +Aufgrund des Freistellungsauftrages werden Kapitalerträge - bis zur Höhe des +beantragten Freibetrages - vom KEST-Abzug freigestellt; +die KEST wird sofort vergütet. +comdirect bank +Aktiengesellschaft + +*Diese Abrechnung wird von der Bank nicht unterschrieben + Keine Steuerbescheinigung +Kapitalerträge sind einkommensteuerpflichtig +D71111/11/1111 + +``` \ No newline at end of file diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/ComdirectPDFExtractor.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/ComdirectPDFExtractor.java index 4d130d26ad..7ab1673d1d 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/ComdirectPDFExtractor.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/datatransfer/pdf/ComdirectPDFExtractor.java @@ -9,7 +9,9 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -27,7 +29,6 @@ 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.AttributeType; import name.abuchen.portfolio.model.BuySellEntry; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.PortfolioTransaction; @@ -35,6 +36,7 @@ import name.abuchen.portfolio.model.Transaction.Unit; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; +import name.abuchen.portfolio.util.Pair; /** * @formatter:off @@ -46,8 +48,16 @@ * and the taxes treatment includes all taxes (including withholding tax), * but not all fees. * - * Therefore, we use the documents based on their function and merge both documents, - * if possible, as one transaction. + * Therefore, we use the documents based on their function and merge both documents, if possible, as one transaction. + * {@code + * matchSaleAndTaxTransactions(List saleTransactionList,List taxesTransactionList) + * } + * + * The separate taxes statement does only contain taxes in the account currency. + * However, if the security currency differs, we need to provide the currency conversion. + * {@code + * fixMissingCurrencyConversionForTaxesTransactions(Collection) + * } * * Always import the securities transaction and the taxes treatment for a correct transaction. * Due to rounding differences, the correct gross amount is not always shown in the securities transaction. @@ -66,7 +76,10 @@ @SuppressWarnings("nls") public class ComdirectPDFExtractor extends AbstractPDFExtractor { - private static final String ATTRIBUTE_EXCHANGE_RATE = "exchange_rate"; + private static record SaleTaxPair(Item sale, Item tax) + { + } + private static final String ATTRIBUTE_GROSS_TAXES_TREATMENT = "gross_taxes_treatment"; public ComdirectPDFExtractor(Client client) @@ -303,9 +316,6 @@ private void addBuySellTransaction() t.setMonetaryAmount(gross); - t.getPortfolioTransaction().getSecurity().getAttributes() - .put(new AttributeType(ATTRIBUTE_EXCHANGE_RATE), v.get("exchangeRate")); - checkAndSetGrossUnit(t.getPortfolioTransaction().getMonetaryAmount(), fxGross, t, type.getCurrentContext()); }), // @formatter:off @@ -329,9 +339,6 @@ private void addBuySellTransaction() t.setMonetaryAmount(gross); - t.getPortfolioTransaction().getSecurity().getAttributes() - .put(new AttributeType(ATTRIBUTE_EXCHANGE_RATE), v.get("exchangeRate")); - checkAndSetGrossUnit(t.getPortfolioTransaction().getMonetaryAmount(), fxGross, t, type.getCurrentContext()); }), // @formatter:off @@ -371,9 +378,6 @@ private void addBuySellTransaction() Money fxGross = Money.of(rate.getTermCurrency(), asAmount(v.get("fxGross"))); Money gross = rate.convert(rate.getBaseCurrency(), fxGross); - t.getPortfolioTransaction().getSecurity().getAttributes() - .put(new AttributeType(ATTRIBUTE_EXCHANGE_RATE), v.get("exchangeRate")); - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); } }), @@ -394,9 +398,6 @@ private void addBuySellTransaction() Money fxGross = Money.of(rate.getTermCurrency(), asAmount(v.get("fxGross"))); Money gross = rate.convert(rate.getBaseCurrency(), fxGross); - t.getPortfolioTransaction().getSecurity().getAttributes() - .put(new AttributeType(ATTRIBUTE_EXCHANGE_RATE),v.get("exchangeRate")); - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); } }), @@ -417,9 +418,6 @@ private void addBuySellTransaction() Money fxGross = Money.of(rate.getTermCurrency(), asAmount(v.get("fxGross"))); Money gross = rate.convert(rate.getBaseCurrency(), fxGross); - t.getPortfolioTransaction().getSecurity().getAttributes() - .put(new AttributeType(ATTRIBUTE_EXCHANGE_RATE), v.get("exchangeRate")); - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); } })) @@ -521,24 +519,6 @@ private void addSellWithNegativeAmountTransaction() v.put("name", trim(replaceMultipleBlanks(v.get("name")))); v.put("nameContinued", trim(replaceMultipleBlanks(v.get("nameContinued")))); - t.setSecurity(getOrCreateSecurity(v)); - }), - // @formatter:off - // Wertpapier-Bezeichnung WPKNR/ISIN - // LVMH Moët Henn. L. Vuitton SA 853292 - // Actions Port. (C.R.) EO 0,3 FR0000121014 - // St. 100 EUR 86,00 - // @formatter:on - section -> section // - .attributes("name", "wkn", "nameContinued", "isin", "currency") // - .find("Wertpapier\\-Bezeichnung .*") // - .match("^(?.*)[\\s]{2,}(?[A-Z0-9]{6}).*$") // - .match("^(?.*)[\\s]{2,}(?[A-Z]{2}[A-Z0-9]{9}[0-9]).*$") // - .match("^([\\s]+)?St\\.[\\s]{1,}[\\.,\\d]+[\\s]{1,}(?[\\w]{3}).*$") // - .assign((t, v) -> { - v.put("name", trim(replaceMultipleBlanks(v.get("name")))); - v.put("nameContinued", trim(replaceMultipleBlanks(v.get("nameContinued")))); - t.setSecurity(getOrCreateSecurity(v)); })) @@ -1138,22 +1118,6 @@ private void addTaxesTreatmentTransaction() { t.setMonetaryAmount(deductedTaxes); } - - if (!t.getSecurity().getCurrencyCode().equals(t.getMonetaryAmount().getCurrencyCode()) // - && t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)) != null) - { - v.put("exchangeRate", t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)).toString()); - v.put("baseCurrency", t.getMonetaryAmount().getCurrencyCode()); - v.put("termCurrency", t.getSecurity().getCurrencyCode()); - - ExtrExchangeRate rate = asExchangeRate(v); - type.getCurrentContext().putType(rate); - - Money gross = Money.of(rate.getBaseCurrency(), t.getMonetaryAmount().getAmount()); - Money fxGross = rate.convert(rate.getTermCurrency(), gross); - - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); - } }), // @formatter:off // Zu Ih r e n G u n s t e n v o r S te u e r n : E U R 4,6 5 @@ -1188,22 +1152,6 @@ private void addTaxesTreatmentTransaction() { t.setMonetaryAmount(deductedTaxes); } - - if (!t.getSecurity().getCurrencyCode().equals(t.getMonetaryAmount().getCurrencyCode()) // - && t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)) != null) - { - v.put("exchangeRate", t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)).toString()); - v.put("baseCurrency", t.getMonetaryAmount().getCurrencyCode()); - v.put("termCurrency", t.getSecurity().getCurrencyCode()); - - ExtrExchangeRate rate = asExchangeRate(v); - type.getCurrentContext().putType(rate); - - Money gross = Money.of(rate.getBaseCurrency(), t.getMonetaryAmount().getAmount()); - Money fxGross = rate.convert(rate.getTermCurrency(), gross); - - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); - } }), // @formatter:off // Z u Ih r e n G u n s t e n v o r S te u e r n : E U R 1.263,0 5 @@ -1224,22 +1172,6 @@ private void addTaxesTreatmentTransaction() t.setCurrencyCode(asCurrencyCode(stripBlanksAndUnderscores(v.get("currencyRefundedTaxes")))); t.setAmount(asAmount(stripBlanksAndUnderscores(v.get("refundedTaxes")))); - - if (!t.getSecurity().getCurrencyCode().equals(t.getMonetaryAmount().getCurrencyCode()) // - && t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)) != null) - { - v.put("exchangeRate", t.getSecurity().getAttributes().get(new AttributeType(ATTRIBUTE_EXCHANGE_RATE)).toString()); - v.put("baseCurrency", t.getMonetaryAmount().getCurrencyCode()); - v.put("termCurrency", t.getSecurity().getCurrencyCode()); - - ExtrExchangeRate rate = asExchangeRate(v); - type.getCurrentContext().putType(rate); - - Money gross = Money.of(rate.getBaseCurrency(), t.getMonetaryAmount().getAmount()); - Money fxGross = rate.convert(rate.getTermCurrency(), gross); - - checkAndSetGrossUnit(gross, fxGross, t, type.getCurrentContext()); - } })) // @formatter:off @@ -1853,12 +1785,14 @@ public List postProcessing(List items) .filter(TransactionItem.class::isInstance) // .map(TransactionItem.class::cast) // .filter(i -> i.getSubject() instanceof AccountTransaction) // - .filter(i -> AccountTransaction.Type.TAXES // - .equals((((AccountTransaction) i.getSubject()).getType()))) // + .filter(i -> { // + var type = ((AccountTransaction) i.getSubject()).getType(); // + return type == AccountTransaction.Type.TAXES || type == AccountTransaction.Type.TAX_REFUND; // + }) // .collect(Collectors.toList()); // Filter transactions by buySell transactions - List sellTransactionList = items.stream() // + List saleTransactionList = items.stream() // .filter(BuySellEntryItem.class::isInstance) // .map(BuySellEntryItem.class::cast) // .filter(i -> i.getSubject() instanceof BuySellEntry) // @@ -1875,13 +1809,13 @@ public List postProcessing(List items) .equals((((AccountTransaction) i.getSubject()).getType()))) // .collect(Collectors.toList()); - // Group sell and taxes transactions together and group by date and security - Map>> sellTaxesTransactions; - if (!sellTransactionList.isEmpty()) - sellTaxesTransactions = Stream.concat(sellTransactionList.stream(), taxesTransactionList.stream()) + // Group sale and taxes transactions together and group by date and security + Map>> saleTaxesTransactions; + if (!saleTransactionList.isEmpty()) + saleTaxesTransactions = Stream.concat(saleTransactionList.stream(), taxesTransactionList.stream()) .collect(Collectors.groupingBy(item -> item.getDate().toLocalDate(), Collectors.groupingBy(Item::getSecurity))); else - sellTaxesTransactions = Collections.emptyMap(); + saleTaxesTransactions = Collections.emptyMap(); // Group dividend and taxes transactions together and group by date and security Map>> dividendTaxesTransactions; @@ -1891,47 +1825,50 @@ public List postProcessing(List items) else dividendTaxesTransactions = Collections.emptyMap(); - sellTaxesTransactions.forEach((k, v) -> { + var saleTaxPairs = matchSaleAndTaxTransactions(saleTransactionList, taxesTransactionList); + fixMissingCurrencyConversionForTaxesTransactions(saleTaxPairs); + + saleTaxesTransactions.forEach((k, v) -> { v.forEach((security, transactions) -> { if (transactions.size() == 2) { - BuySellEntry sellTransaction = null; + BuySellEntry saleTransaction = null; AccountTransaction taxesTransaction = null; - // Which transaction is the taxes and which the sell? + // Which transaction is the taxes and which the sale? if (transactions.get(0).getSubject() instanceof BuySellEntry && transactions.get(1).getSubject() instanceof AccountTransaction) { - sellTransaction = (BuySellEntry) transactions.get(0).getSubject(); + saleTransaction = (BuySellEntry) transactions.get(0).getSubject(); taxesTransaction = (AccountTransaction) transactions.get(1).getSubject(); } else if (transactions.get(1).getSubject() instanceof BuySellEntry && transactions.get(0).getSubject() instanceof AccountTransaction) { - sellTransaction = (BuySellEntry) transactions.get(1).getSubject(); + saleTransaction = (BuySellEntry) transactions.get(1).getSubject(); taxesTransaction = (AccountTransaction) transactions.get(0).getSubject(); } - // Check if not null and there is a sell transaction and a taxes transaction - if (sellTransaction != null && taxesTransaction != null + // Check if not null and there is a sale transaction and a taxes transaction + if (saleTransaction != null && taxesTransaction != null && AccountTransaction.Type.TAXES.equals(taxesTransaction.getType()) - && PortfolioTransaction.Type.SELL.equals(sellTransaction.getPortfolioTransaction().getType())) + && PortfolioTransaction.Type.SELL.equals(saleTransaction.getPortfolioTransaction().getType())) { // Subtract the tax from the taxes transaction from the total amount - sellTransaction.setMonetaryAmount(sellTransaction.getPortfolioTransaction().getMonetaryAmount() + saleTransaction.setMonetaryAmount(saleTransaction.getPortfolioTransaction().getMonetaryAmount() .subtract(taxesTransaction.getMonetaryAmount())); // Add taxes as tax unit - sellTransaction.getPortfolioTransaction() + saleTransaction.getPortfolioTransaction() .addUnit(new Unit(Unit.Type.TAX, taxesTransaction.getMonetaryAmount())); // Combine at sources file (One or two files?) - if (!sellTransaction.getSource().equals(taxesTransaction.getSource())) - sellTransaction.setSource(sellTransaction.getSource() + "; " + taxesTransaction.getSource()); + if (!saleTransaction.getSource().equals(taxesTransaction.getSource())) + saleTransaction.setSource(saleTransaction.getSource() + "; " + taxesTransaction.getSource()); // Combine at notes - sellTransaction.setNote(concat(sellTransaction.getNote(), taxesTransaction.getNote())); + saleTransaction.setNote(concat(saleTransaction.getNote(), taxesTransaction.getNote())); // Add the taxes treatment to the hash table, which can be deleted TaxesTransactionToBeDelete.add(taxesTransaction); @@ -1953,8 +1890,8 @@ else if (transactions.get(1).getSubject() instanceof BuySellEntry } else if (transactions.size() == 2) { - AccountTransaction dividendTransaction = (AccountTransaction) transactions.get(0).getSubject(); - AccountTransaction taxesTransaction = (AccountTransaction) transactions.get(1).getSubject(); + var dividendTransaction = (AccountTransaction) transactions.get(0).getSubject(); + var taxesTransaction = (AccountTransaction) transactions.get(1).getSubject(); // Memory the Item with taxes properties Item taxesTransactionItem = transactions.get(1); @@ -2023,6 +1960,104 @@ else if (transactions.size() == 2) return items; } + /** + * @formatter:off + * Match sale and taxes transactions, ensuring unique pairs based on date and security. + * + * This method matches sale and taxes transactions by creating a Pair consisting of the transaction's + * date and security. It uses a Set called 'keys' to prevent duplicates based on these Pair keys, + * ensuring that the same combination of date and security is not processed multiple times. + * Duplicate sale transactions for the same security on the same day are avoided. + * + * @param saleTransactionList A list of sale transactions. + * @param taxesTransactionList A list of taxes transactions. + * @return A collection of SaleTaxPair objects representing matched sale and taxes transactions. + * @formatter:on + */ + private Collection matchSaleAndTaxTransactions(List saleTransactionList, + List taxesTransactionList) + { + // Use a Set to prevent duplicates + Set> keys = new HashSet<>(); + Map, SaleTaxPair> pairs = new HashMap<>(); + + // Match identified sale and taxes transactions + saleTransactionList.forEach( // + sale -> { + var key = new Pair<>(sale.getDate().toLocalDate(), sale.getSecurity()); + + // Prevent duplicates + if (keys.add(key)) + pairs.put(key, new SaleTaxPair(sale, null)); + } // + ); + + // Iterate through the list of taxes transactions to match them with sale transactions + taxesTransactionList.forEach( // + tax -> { + // Check if the taxes treatment has a security + if (tax.getSecurity() == null) + return; + + // Create a key based on the taxes transaction's date and security + var key = new Pair<>(tax.getDate().toLocalDate(), tax.getSecurity()); + + // Retrieve the SaleTaxPair associated with this key, if it exists + var pair = pairs.get(key); + + // Skip if no sale transaction is found or if a taxes transaction already exists + if (pair != null && pair.tax() == null) + pairs.put(key, new SaleTaxPair(pair.sale(), tax)); + } // + ); + + return pairs.values().stream().filter(p -> p.tax() != null).toList(); + } + + /** + * @formatter:off + * This method fixes missing currency conversion for taxes transactions. + * + * It iterates through a collection of SaleTaxPair objects and performs the necessary currency conversions + * if required based on the currency codes of the involved transactions. + * + * @param saleTaxPairs A collection of SaleTaxPair objects containing taxes and sale transactions. + * @formatter:on + */ + private void fixMissingCurrencyConversionForTaxesTransactions(Collection saleTaxPairs) + { + saleTaxPairs.forEach( // + pair -> { + // Get the taxes transaction from the SaleTaxPair + var tax = (AccountTransaction) pair.tax.getSubject(); + + // Check if currency conversion is needed + if (!tax.getSecurity().getCurrencyCode().equals(tax.getMonetaryAmount().getCurrencyCode())) + { + // Get the sale transaction from the SaleTaxPair + var sale = (BuySellEntry) pair.sale.getSubject(); + + // Check if we have an exchange rate available from the sale transaction + var grossValue = sale.getPortfolioTransaction().getUnit(Unit.Type.GROSS_VALUE); + + if (grossValue.isPresent() && grossValue.get().getExchangeRate() != null) + { + // Create and set the required grossUnit to the taxes treatment + var rate = new ExtrExchangeRate(grossValue.get().getExchangeRate(), + sale.getPortfolioTransaction().getSecurity().getCurrencyCode(), + tax.getCurrencyCode()); + + String termCurrency = sale.getPortfolioTransaction().getSecurity().getCurrencyCode(); + Money fxGross = rate.convert(termCurrency, tax.getMonetaryAmount()); + + // Add the converted gross value unit to the tax transaction + tax.addUnit(new Unit(Unit.Type.GROSS_VALUE, tax.getMonetaryAmount(), fxGross, rate.getRate())); + } + } + } // + ); + } + private String concat(String first, String second) { if (first == null && second == null)