CLI tool to ingest bank statements from various sources into plain text ledger format

README.md

Hefter #

Hefter imports bank statements into ledger-cli plain text account format.

hleder already has built-in support for csv import, but I want more features:

  • Interaction: I want to review and change a transaction before commiting it to the journal.
  • Learn from data: I don't want to write rules to rewrite subject and account.
  • Import from PDF: Some unnamed credit card providers still cannot export CSV files.
  • I want to type this and be done:

hefter <file.csv> <file-2.csv> <file.pdf>

How it works #

It reads your $LEDGER_JOURNAL; it follow include statements.

A rules file ($HEFTER_RULES) defines what file patterns should be processed by what source (CSV, PDF) and what account is linked to these statements:

rule "my-bank" {
  pattern "export_DE23*_*.*.*.csv"  // use globs
  account "Assets:My Bank"
  source type="csv" {
    skip 1  // skip the header row
    columns date=4 aux_date=5 subject=6 note=10 amount=11  // map transaction fields to columns
    currency "EUR"  // default currency for amounts

    // check that the header row contains the expected column names
    assertions {
      date row=0 value="Buchungstag"
      aux_date row=0 value="Valutadatum"
      subject row=0 value="Name Zahlungsbeteiligter"
      note row=0 value="Verwendungszweck"
      amount row=0 value="Betrag"

      // you can check any row in any named column
    }
    // deal with locales
    date_format "%d.%m.%Y"  // strptime() format codes, see here: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior
    delimiter ";"
    decimal_separator ","
  }
}

rule "my-credit-card" {
  pattern "report-202*-*-*.pdf"
  account "Liabilities:My Credit Card"
  source type="pdf" {
    // finds the largest table on each page, expects a header row
    columns date=1 aux_date=0 note=2 amount=4  // subject and note are optional, but at least one must be set
    currency "EUR"

    // assertions always refer to the header row
    assertions {
      date value="Transaktions-datum"
      aux_date value="Buchungsdatum"
    }
    date_format "%d.%m.%Y"
    decimal_separator ","

    // strip newlines from columns, e.g., `Transaktions-\ndatum` becomes `Transaktions-datum`
    strip_newlines

    // remove duplicate whitespace from subject and note
    strip_duplicate_whitespace

    // replace / delete with regular expressions
    replace "Kartenumsatz" with="" in="note"
    // also works as block
    replace {
        "Verwendungszweck: (.+)" with="\\1" in="note"
        "Bäcker (.+)$" with="Bäcker" in="subject"
    }

    // ... or ignore whole rows based on content
    ignore "^Gutschrift" in="note"
    // (also works as block)
  }
}

Sources yield bank statements (date, auxiliary date, subject, note and amount). Bank statements that have a matching transaction in the ledger are ignored. The rest are shown to the user to change subject, target account, and dates. Users can accept or discard an item. Accepetd bank statements are turned into proper transactions with postings and all that and then saved to $LEDGER_FILE.