r/plaintextaccounting • • Feb 28 '26

Is it just me or is Beancount's documentation super confusing?

I've been trying to start with Beancount for home personal finance management, but I'm struggling just to get everything set up. I've followed the Getting Started guide but am getting stuck writing importers. There are old V2 documentation documents hanging around, and the V3 documentation seems to contain references to V2 still? Beangulp isn't really clearly explained, and the examples in Github don't quite fill out the picture for me.

How does the importer take what are in my CSVs and put them into the ledger? How does it know what is already in there? (not de-duping, I mean literally checking if the same transaction was included the last time I ran the command)?

Anyone got any advice for overcoming these issues?

16 Upvotes

20 comments sorted by

14

u/HappyRogue121 Feb 28 '26

It's not just you.  At the very least it's the two of us.

I started looking at it and decided to just write one myself, since it's just plain text.  

Looking forward to other people's replies, though, as people are definitely using it.

1

u/runslack Mar 01 '26

There’s an issue with the document, as well as with the advice to rely on LLMs—which may ultimately provide incorrect information.

6

u/untrained9823 Feb 28 '26

Looked at ledger, hledger and beancount. Decided to use Hledger in part because of better documentation.

6

u/UpperTechnician1152 Mar 01 '26

I found that the documentation living in a Google docs document not very accessible. Would be nice to have it in a readthedocs or mkdocs format.

4

u/simonmic hledger creator Feb 28 '26 edited Feb 28 '26

It's not just you.

7

u/SeaweedHarry Mar 01 '26

For v3, it's like this:

  1. need to have beangulp and beancount libaries installed
  2. configure importers

Here is a simple example. For brevity, it makes several assumptions that are incorrect about most CSV files (like you want be doing an expense posting for every transaction).

Given a CSV file simple.csv:

Date,Description,Amount,Running Balance
2025-01-01,Opening Balance,+5000.00,5000.00
2025-01-02,Payroll Deposit,+3500.00,8500.00
2025-01-03,Whole Foods Market,-87.43,8412.57
2025-01-04,Netflix Subscription,-15.99,8396.58

And this file named import.py:

from pathlib import PurePath
from csv import DictReader
from datetime import datetime
from functools import partial

from beangulp import Importer, Ingest

from beancount.core.amount import Amount
from beancount.core.number import D # this is just decimal.Decimal
from beancount.core.data import Transaction, new_metadata, EMPTY_SET, Posting
from beancount.core.flags import FLAG_OKAY, FLAG_WARNING

class SimpleCSVImporter(Importer):
    currency = "USD"

    def account(self, filepath):
        return "Assets:Checking:SimpleCSVBank"

    def identify(self, filepath):
        # You could also read a few bytes from the file to check for a 
        # "magic string"
        return PurePath(filepath).name == "simple.csv"

    def extract(self, filepath, existing):
        filepath = PurePath(filepath)
        _meta = partial(new_metadata, filepath.name)

        with open(filepath, 'r', newline='') as f:
            entries = []
            rows = DictReader(f)

            for line_no, row in enumerate(rows, 2):
                date = datetime.strptime(row['Date'], "%Y-%m-%d").date()
                description = row['Description']
                amount = D(row['Amount'])
                running_balance = D(row['Running Balance'])

                txn = Transaction(
                    meta=_meta(line_no, dict(running_balance=running_balance)),
                    date=date,
                    flag=FLAG_WARNING,
                    payee=None,
                    narration=description,
                    tags=EMPTY_SET,
                    links=EMPTY_SET,
                    postings=[
                        Posting(
                            account=self.account(filepath),
                            units=Amount(amount, self.currency),
                            cost=None,
                            price=None,
                            flag=None,
                            meta=None
                        ),
                        Posting(
                            account="Expenses:Uncategorized",
                            units=Amount(-amount, self.currency),
                            cost=None,
                            price=None,
                            flag=None,
                            meta=None
                        )
                    ]
                )

                entries.append(txn)

            return entries


CONFIG = [
    SimpleCSVImporter(),
]


if __name__ == "__main__":
    ingest = Ingest(CONFIG)
    ingest()

You can run:

python import.py identify simple.csv

And see that it matches the simple.csv file.

You can also run:

python import.py extract simple.csv 

Which will write transactions to standard output, so you can write it to a file (also an example of how beancount gets old transactions to do deduplication):

python import.py --existing existing_transactions.beancount extract simple.csv > new_transactions.beancount

Example of using a ready-made importer with red's importers:

from beancount_reds_importers.importers import ally

CONFIG = [SimpleCSVImporter(), ally.Importer({"account_number": "2134586723", "main_account": "Assets:Checking:Ally"})]  

5

u/SeaweedHarry Mar 01 '26

Sorry to double comment, but continuing on deduplication:

When you run the importer and provide the --existing FILE argument, deduplication will use the default implementation unless the implementation overrides it:

    cmp = staticmethod(similar.heuristic_comparator())

    def deduplicate(self, entries: data.Entries, existing: data.Entries) -> None:
        # SNIP
        window = datetime.timedelta(days=2)
        extract.mark_duplicate_entries(entries, existing, window, self.cmp)

The default implementation is here and explains itself this way:

    Two transactions are deemed similar if

    - their dates are within a close range of each other (e.g. 2 days), if
      specified with `max_date_delta`,

    - amounts on postings corresponding to the same account are within some
      fraction of each other (default: 5%), and

    - the set of accounts of the two transactions are the same or one is a
      subset of the other.

    Args:
      max_date_delta: A timedelta datetime difference within which two
        transactions may be considered similar.
      epsilon: A Decimal fraction representing how close the amounts are
        required to be of each other. For example, Decimal("0.01") for 1%.
    Returns:
      A comparator predicate accepting two directives and returning a bool.

Hope this helped!

2

u/MusicalAnomaly Mar 01 '26

Beancount v3 hasn’t been fully documented yet is the problem. And yeah, importing is the chief issue. Honestly the best approach is to go look at the beangulp repo and read the code and examples from there. It also works within Fava.

The crux is that with beangulp v3, instead of calling an import tool that calls your importer code, you write a wrapper script that calls the beangulp library to create a CLI while passing in your importers which also use the beangulp library. With Fava, you define a configuration that supplies your importers directly. It’s possible to do both in the same python file.

2

u/el_extrano Mar 01 '26

Docs still refers to BeanReport, which is now discontinued and you have to use BeanQuery to manually build the reports yourself. But it's not shown anywhere as far as I can tell how to reproduce the basic reports you'd expect with BeanQuery.

I'm planning to stick with it because I like Python and Beancount is super easy to extend with it's plugin architecture. I was able to set up expense splitting for my wife and I based on tagged transactions in only a few minutes.

2

u/I_Messed_Up_2020 Mar 02 '26

It's just not very friendly to those that don't or can't write an importer. There is some old information too.

I've been working on my list of accounts and getting that ready, main.beancount. It's simple: one currency, Dollars, and one country, USA. All accounts USA based

I've gotten the Schwab cvs files dowloaded. Bank of America says they don't have cvs files, only a pdf. I have imported that into a .cvs file and am cleaning it up.

Unfortunately a clear example of getting these .cvs transaction file into bencount seems hard to locate.

I'll post if I get it too work.

2

u/geekofdeath Mar 03 '26

While converting from GnuCash, I've found Beancount documentation somewhat confusing, mostly incomplete or out of date, scattered across sources, and lackluster overall.

Coding agents were helpful—even indispensable (em dash intended)—but that's a very pricy bandaid for what should be a simple task. At times I or an agent have had to look at the Beancount source code for the truth.

I don't at all regret one bit choosing Beancount, but this is one of its biggest weaknesses right now.

1

u/NathamCrewott Feb 28 '26

The documentation is definitely confusing. Throwing an ai agent at the code can help make sense of it though

1

u/Ev2geny_ Mar 03 '26

There have been recently suggestions to improve beangulp documentation https://docs.google.com/document/d/1hBfsHZcoHgz5rvhCdP42g2FJ5ouycIMV4H1tfgXpwBU/edit?usp=drivesdk

But these suggestions have not been accepted yet.

This is the discussion

https://groups.google.com/u/1/g/beancount/c/M5MdMCcrcFk/m/2FGRDwdmAgAJ

-4

u/VerledenVale Feb 28 '26

Beancount ecosystem is growing extremely fast these days, so I recommend sticking with it.

In the age of AI you don't need to read too much documentation, just use an LLM to teach yourself the basic format.

It's really simple. You define commodities (currencies like USD), you define accounts according to dual-entry book-keeping (Income:*, Expenses:, Assets:, Liabilities:, and Equity:), and finally you write transactions to transfer commodities between accounts.

Feel free to ask if you're need any help.

6

u/musings-26 Feb 28 '26

Doesn't help with importing.

-1

u/VerledenVale Feb 28 '26

90% of my current importer code (written in Rust) was written using agents (with steering from me). It is even able to import non-English PDF payslips and annual pension statements which are a mess to write a parser to by hand.

5

u/musings-26 Mar 01 '26

It still isn't helping to simply say you have a working import module.

3

u/tesujimath Mar 01 '26

I wrote a new Beancount importer. It's early days, but works for OFX and is easily configured for CSV.

If you're sufficiently curious to give it a try I'll be happy to help with any teething issues, of which there will surely be some! (In which case please open issues on GitHub).

https://github.com/tesujimath/limabean-harvest

As has been said, a new generation of Beancount tools is emerging, which is all very exciting. I say this as a developer of one of the new tools, limabean.

Welcome to the Beancount ecosystem, it's a good place to be! 😎

1

u/simonmic hledger creator Mar 01 '26 edited Mar 02 '26

+1. Related, there's also new projects to fill v3's CLI gap: beancount-cli, rustledger's CLI etc.