The Problem

My grandparents gifted me money for my eighteen birthday, which had some restrictions on how I was allowed to use it, then my parents gave me a different amount, which had to be used for other purposes and I started to invest a bit of my money and this became a bit too much work to comfortably work out how much I am allowed to spend on what every nearly every time I looked at my account, so I thought there has to be a better way to organize it.

Requirements

Budgets Import So I basically need to be able to budget money and regulate its use and track what I have used it. And it should be easy to import real bank data, but also edit the entry for special occasions.

What is beancount

Beancount is basically just a format, on how to save transaction data and this data is formatted in nothing complicated but just plain text, which makes it really easy to write your own importer, in plain python without much magic. It uses the dual entry book keeping system, of what I knew nothing about, but given that I worked in the backend of a bookkeeping software, I used the documentation to learn the basics. The way that I use it, that I have one main.beancount file where I opened all my possible accounts, which again are also my budgets, so for example the money I got from my grandparents, is not in my Bank account, but the Assets:GrandParents:Travel Account and when I use it, I subtract from that, so that I can see the real sum I am allowed to use in my normal Asset:Account. This then can also be used to generate a report, to show, for what exactly I used the money for.

How I use it

Download

First I setup a recurring todo in my todolist, which asks me once a month to download my data from george. I just download it as an csv, as it is the easiest to work with and the data is rather simple. The only weird thing is that they enclose everything in quotes, and use commas, not only as line column separators but also to separate the Euro to cent amounts, so the Amount column looks like this:

…,”15,99”,...

which one has to account for.

Extraction

I then wrote a simple python script that first reads in all the data from the csv into a dedicated struct(class) and then writes all the data to the example.beancount file. Before that every transaction runs through the rules.py file, which I can edit to make custom rules. For example Every transaction, that includes Billa in it’s name, gets added to a subcategory of Supermarket accounts called Billa. I am also able to create more complicated logic, but even for the Supermarkets it’s quite important to have more than exact pattern matching, as Not all Billa markets share the same Name.

Then I edit singular occurrences manually, something that doesn’t even take that long, but is also used as a sanity check, so that my program doesn’t produce any bugs.

This is how the importer looks currently and one would only have to adjust the other files to personalize it:


from __future__ import annotations
from enum import Enum
from decimal import *
import csv
from datetime import datetime
from decimal import Decimal
from typing import List, Dict
from accounts import Account, Tag

BANK_ACCOUNT = "Assets:Bank:Girokonto"
DEFAULT_OTHER_ACCOUNT = "Expenses:Uncategorized"

class Currency(Enum):
    EUR = "EURO"
    USD = "US-Dollar"

    def parse(string: str) -> Currency:
        if(string.strip() == "EUR"):
            return Currency.EUR
        elif(string.strip() == "USD"):
            return Currency.USD

class Entry:

    def __init__(self, date: datetime, payee: str, narration: str, amount: Decimal, currency: Currency):
        self.date = date
        self.payee = payee
        self.narration = narration
        self.amount = amount
        self.currency = currency
        self.intern_account: Account = None
        self.extern_account: Account = None
        self.tags: List[Tag] = []

    def tag_add(self, tag: Tag):
        self.tags.append(tag)

    def render_tags(self) -> str:
        result = ""
        for tag in self.tags:
            result += f"#{tag.value} "
        return result


    def to_string(self) -> str:
        from rules import rule
        entry = rule(self)
        return (f"\n{entry.date} txn {format_description(entry.payee,entry.narration)} {entry.render_tags()}\n" +
            f"\t{entry.intern_account.value if entry.intern_account != None else BANK_ACCOUNT}  {entry.amount} {entry.currency.name}\n" +
            f"\t{entry.extern_account.value if entry.extern_account != None else DEFAULT_OTHER_ACCOUNT}\n")

def parse_euro_amount(s):
    return Decimal(s.replace('.', '').replace(',', '.'))

def format_description(partner, details):
    return f"\"{partner}\" \"{details}\""

def import_csv_to_beancount(input_csv_path, output_beancount_path):
    with open(input_csv_path, newline='', encoding='utf-16') as csvfile:
        reader = csv.DictReader(csvfile)
        entries: List[Entry] = []
        for row in reader:
            date = datetime.strptime(row["Buchungsdatum"], "%d.%m.%Y").date()
            amount = parse_euro_amount(row["Betrag"])
            payee = row["Partnername"]
            narration = row.get("Buchungs-Details", "")

            entries.append(Entry(
                date.isoformat(),
                payee,
                narration,
                Decimal(amount),
                Currency.parse(row["Währung"].strip('"') or "EUR"),
            ))

    with open(output_beancount_path, "a", encoding="utf-8") as f:
        for tx in entries:
            f.write(tx.to_string())

    print(f"Appended {len(entries)} transactions to {output_beancount_path}.")

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Import bank CSV into Beancount v3 ledger.")
    parser.add_argument("input_csv", help="Path to your bank CSV file")
    parser.add_argument("output_beancount", help="Path to the Beancount ledger file to append to")
    args = parser.parse_args()
    import_csv_to_beancount(args.input_csv, args.output_beancount)

Edeting

I am edeting with helix, because it is my goto editor, and there is allready simple download and play lsp support for the beancount lsp.

Interconnection

My main.beancount file then includes all the other files which I produce, so that I have a single point to start all services and checks on.

option "title" "Ben Konto"
option "operating_currency" "EUR"

2023-01-01 open Assets:Bank:Girokonto

...

include "Backlog/Juli_2023_August_2025.beancount"

Conclusion

I have only been using it now for a short amount of time, but it looks like a really powerful, but simple solution, so I how that I can keep using it, and don’t have to switch again, especially as versioning is so simple with git. The Only Problem that I currently have, is that I only update it once a month and so don’t have any realtime information, but I might work on that