package name.abuchen.portfolio.datatransfer.pdf; import java.io.IOException; import java.math.BigDecimal; import java.text.NumberFormat; import java.text.ParseException; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; 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.model.Transaction.Unit; import name.abuchen.portfolio.money.Money; import name.abuchen.portfolio.money.Values; public class BankSLMPDFExctractor extends AbstractPDFExtractor { private final NumberFormat swissNumberFormat = NumberFormat.getInstance(new Locale("de", "CH")); //$NON-NLS-1$ //$NON-NLS-2$ public BankSLMPDFExctractor(Client client) throws IOException { super(client); addBankIdentifier(getLabel()); addBankIdentifier("Spar + Leihkasse"); //$NON-NLS-1$ addBuyTransaction(); addSellTransaction(); addDividendTransaction(); } @SuppressWarnings("nls") private void addBuyTransaction() { DocumentType type = new DocumentType("abrechnung - Kauf"); this.addDocumentTyp(type); Block block = new Block("B.rsenabrechnung - Kauf"); type.addBlock(block); Transaction<BuySellEntry> extractor = new Transaction<BuySellEntry>() .subject(() -> { BuySellEntry entry = new BuySellEntry(); entry.setType(PortfolioTransaction.Type.BUY); return entry; }) .section("date", "shares", "name", "wkn", "currency") .match("Wir haben f.r Sie am (?<date>\\d+.\\d+.\\d{4}+) gekauft.") // .match("^(?<shares>[\\d.']+) .*$") // .match("^(?<name>.*)$") // .match("^Valor: (?<wkn>[^ ]*)$") // .match("Total Kurswert (?<currency>\\w{3}+) .*") // .assign((t, v) -> { t.setDate(asDate(v.get("date"))); t.setShares(asShares(v.get("shares"))); t.setSecurity(getOrCreateSecurity(v)); }) .section("amount", "currency") // .match("Netto (?<currency>\\w{3}+) -(?<amount>[\\d.']+)") // .assign((t, v) -> { t.setAmount(asAmount(v.get("amount"))); t.setCurrencyCode(v.get("currency")); }) .wrap(t -> new BuySellEntryItem(t)); addForexGrossValue(extractor); addFeesSection(extractor); block.set(extractor); } @SuppressWarnings("nls") private void addSellTransaction() { DocumentType type = new DocumentType("abrechnung - Verkauf"); this.addDocumentTyp(type); Block block = new Block("B.rsenabrechnung - Verkauf"); type.addBlock(block); Transaction<BuySellEntry> extractor = new Transaction<BuySellEntry>() .subject(() -> { BuySellEntry entry = new BuySellEntry(); entry.setType(PortfolioTransaction.Type.SELL); return entry; }) .section("date", "shares", "name", "wkn", "currency") .match("Wir haben f.r Sie am (?<date>\\d+.\\d+.\\d{4}+) verkauft.") // .match("^(?<shares>[\\d.']+) .*$") // .match("^(?<name>.*)$") // .match("^Valor: (?<wkn>[^ ]*)$") // .match("Total Kurswert (?<currency>\\w{3}+) .*") // .assign((t, v) -> { t.setDate(asDate(v.get("date"))); t.setShares(asShares(v.get("shares"))); t.setSecurity(getOrCreateSecurity(v)); }) .section("amount", "currency") // .match("Netto (?<currency>\\w{3}+) (?<amount>[\\d.']+)") // .assign((t, v) -> { t.setAmount(asAmount(v.get("amount"))); t.setCurrencyCode(v.get("currency")); }) .wrap(t -> new BuySellEntryItem(t)); addForexGrossValue(extractor); addFeesSection(extractor); block.set(extractor); } @SuppressWarnings("nls") private void addForexGrossValue(Transaction<BuySellEntry> extractor) { extractor.section("forexSum", "forexCurrency", "grossValue", "currency", "exchangeRate") // .optional() // only present if forex is available .match("Total Kurswert (?<forexCurrency>\\w{3}+) (?<forexSum>[\\d.'-]+)") // .match("Change .../... (?<exchangeRate>[\\d.']+) (?<currency>\\w{3}+) (?<grossValue>[\\d.'-]+)") // .assign((t, v) -> { Money grossValue = Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("grossValue"))); Money forex = Money.of(asCurrencyCode(v.get("forexCurrency")), asAmount(v.get("forexSum"))); BigDecimal exchangeRate = asExchangeRate(v.get("exchangeRate")); Unit unit = new Unit(Unit.Type.GROSS_VALUE, grossValue, forex, exchangeRate); // add gross value unit only if currency code of // security actually matches if (unit.getForex().getCurrencyCode() .equals(t.getPortfolioTransaction().getSecurity().getCurrencyCode())) t.getPortfolioTransaction().addUnit(unit); }); } @SuppressWarnings("nls") private void addFeesSection(Transaction<BuySellEntry> extractor) { extractor.section("fees", "currency") // .match("Eidg. Umsatzabgabe (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.getPortfolioTransaction().addUnit(new Unit(Unit.Type.FEE, // Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("fees", "currency").optional() // .match("B.rsengeb.hr (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.getPortfolioTransaction().addUnit(new Unit(Unit.Type.FEE, // Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("fees", "currency").optional() // .match("B.rsengeb.hr Inland (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.getPortfolioTransaction().addUnit(new Unit(Unit.Type.FEE, // Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("fees", "currency").optional() // .match("Eigene Courtage (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.getPortfolioTransaction().addUnit(new Unit(Unit.Type.FEE, // Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))); } @SuppressWarnings("nls") private void addDividendTransaction() { DocumentType type = new DocumentType("Dividende"); this.addDocumentTyp(type); Block block = new Block("Dividende"); type.addBlock(block); Transaction<AccountTransaction> extractor = new Transaction<AccountTransaction>() .subject(() -> { AccountTransaction transaction = new AccountTransaction(); transaction.setType(AccountTransaction.Type.DIVIDENDS); return transaction; }) .section("date", "shares", "name", "wkn", "currency") .match("Am (?<date>\\d+.\\d+.\\d{4}+) wurde folgende Dividende gutgeschrieben:") // .match("^.*$") // .match("^(?<name>.*)$") // .match("^Valor: (?<wkn>[^ ]*)$") // .match("Brutto \\((?<shares>[\\d.']+) \\* ... ([\\d.']+)\\) (?<currency>\\w{3}+) ([\\d.']+)") // .assign((t, v) -> { t.setDate(asDate(v.get("date"))); t.setShares(asShares(v.get("shares"))); t.setSecurity(getOrCreateSecurity(v)); }) .section("amount", "currency") // .match("Netto (?<currency>\\w{3}+) (?<amount>[\\d.']+)") // .assign((t, v) -> { t.setAmount(asAmount(v.get("amount"))); t.setCurrencyCode(v.get("currency")); }) .section("fees", "currency").optional() // .match(".* Verrechnungssteuer (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.addUnit(new Unit(Unit.Type.TAX, Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("fees", "currency").optional() // .match(".* Quellensteuer (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.addUnit(new Unit(Unit.Type.TAX, Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("fees", "currency").optional() // .match(".* Nicht r.ckforderbare Steuern (?<currency>\\w{3}+) -(?<fees>[\\d.']+)") // .assign((t, v) -> t.addUnit(new Unit(Unit.Type.TAX, Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("fees")))))) .section("grossValue", "forexSum", "forexCurrency", "totalValue", "currency", "exchangeRate") // .optional() // only present if forex is available .match("Brutto \\(([\\d.']+) \\* ... ([\\d.']+)\\) (\\w{3}+) (?<grossValue>[\\d.']+)") // .match("Netto (?<forexCurrency>\\w{3}+) (?<forexSum>[\\d.']+)") // .match("Change ... / ... (?<exchangeRate>[\\d.']+) (?<currency>\\w{3}+) (?<totalValue>[\\d.'-]+)") // .assign((t, v) -> { // NOSONAR // if we end up in the branch, then we have forex // dividends and must convert taxes in local // currency Money totalValue = Money.of(asCurrencyCode(v.get("currency")), asAmount(v.get("totalValue"))); t.setMonetaryAmount(totalValue); // keep tax units in case we need to convert them List<Unit> tax = t.getUnits().collect(Collectors.toList()); t.clearUnits(); Money forexGrossValue = Money.of(asCurrencyCode(v.get("forexCurrency")), asAmount(v.get("grossValue"))); BigDecimal exchangeRate = asExchangeRate(v.get("exchangeRate")); Money grossValue = Money.of(totalValue.getCurrencyCode(), Math.round(exchangeRate.doubleValue() * forexGrossValue.getAmount())); Unit unit = new Unit(Unit.Type.GROSS_VALUE, grossValue, forexGrossValue, exchangeRate); t.addUnit(unit); // convert tax units tax.stream().forEach(u -> { if (u.getAmount().getCurrencyCode().equals(t.getCurrencyCode())) { t.addUnit(u); } else { Money txm = Money.of(t.getCurrencyCode(), Math.round(exchangeRate.doubleValue() * u.getAmount().getAmount())); Unit fu = new Unit(Unit.Type.TAX, txm, u.getAmount(), exchangeRate); t.addUnit(fu); } }); }) .wrap(t -> new TransactionItem(t)); block.set(extractor); } @Override public String getLabel() { return "Bank SLM"; //$NON-NLS-1$ } @Override protected long asAmount(String value) { return asValue(value, Values.Amount); } @Override protected long asShares(String value) { return asValue(value, Values.Share); } protected long asValue(String value, Values<Long> valueType) { try { return Math.abs(Math.round(swissNumberFormat.parse(value).doubleValue() * valueType.factor())); } catch (ParseException e) { throw new IllegalArgumentException(e); } } @Override protected BigDecimal asExchangeRate(String value) { try { return BigDecimal.valueOf(swissNumberFormat.parse(value).doubleValue()); } catch (ParseException e) { throw new IllegalArgumentException(e); } } }