Pete Keen

A Robust Reporting System for Ledger

Note: you can find much more information about ledger on ledger-cli.org, including links to official documentation and other implementations

For the last five years I've kept my personal finances in order using the ledger system, a sophisticated command line program that consumes a lightly formatted text file. It's helped me repay debts and get everything in order, helping me financially absorb an injury last month that would have been extremely detrimental just a few years prior.

The stock ledger program is exclusively command-line oriented. For quick checks and greping over output, this is fine. For some time, though, I've wanted a more graphical, more robust way of looking at my finances. I've also wanted a more familiar query language, since version 2.0's queries were someone limited and version 3.0's query syntax is not very well documented yet. Last year I wrote a simple system that pushed monthly reports out to static HTML files, which got me part of the way there but I really wanted something more flexible. Something where I can just write an arbitrary query and have the output dumped to HTML.

Thus, I present Ledger Web. In Ledger Web, your ledger is kept in a text file, just the same as always, reports are ERB files, and queries are SQL. Ledger Web watches your ledger file and whenever it changes dumps it into a PostgreSQL database table. It's also quite customizable, letting you set hooks both before and after row insertion and before and after ledger file load.

Installation

Ledger Web installation is pretty simple. First make sure you have PostgreSQL version 9.0 or greater installed on your machine. Then, run these commands:

$ gem install ledger_web
$ createdb ledger
$ ledger_web

Then, open your web browser to http://localhost:9090/ where you'll see some simple example reports.

Example Report

Let's walk through a simple pair of reports that shows off most of Ledger Web's features. Yesterday I ran across this blog post which draws a comparison between a typical person's budget and a wooden ship, always springing leaks and at risk of sinking to the bottom. I decided to write a report that shows my expenses both summed by year and broken out into individual lines. First, the Leaky Ship report itself:

<% @query = query({:pivot => "Year"}) do %>
select
    account as "Account",
    xtn_year as "Year",
    coalesce(sum(amount), 0) as "Amount"
from
    accounts_years
    left outer join (
        select
            xtn_year,
            account,
            amount
        from
            ledger
    ) x using (account, xtn_year)
where
    account ~ '(Income|Expenses)'
    and xtn_year <= date_trunc('year', cast(:to as date))
group by
    account,
    xtn_year
order by
    account,
    xtn_year
<% end %>
<div class="page-header">
  <h1>Leaky Ship</h1>
</div>
<%= table(@query, :links => {/\d{4}-\d{2}-\d{2}/ =>
    '/reports/register?account=:0&year=:title'}) %>

It starts off with a database query, defined using a helper named query. It uses a table named ledger, which is where your ledger data will be dumped, as well as a view named accounts_years, which is the cross product of every account by every year. This makes sure that rows show up properly even if there's no data for that particular year. Also, it uses :pivot => "Year", which will pivot the report such that each xtn_year will become it's own column.

The :to param in the where clause is automatically populated with the second date in the range at the top of all reports.

Next, it uses some basic Twitter Bootstrap HTML markup to display a nice title, and then uses the table helper to actually dump the query results to an HTML table. The :links option tells the table helper to link the values in any column who's title matches the regular expression /\d{4}-\d{2}-\d{2}/ to /reports/register?account=:0&year=:title, where :0 will get replaced with the value in column 0 (starting from the left, 0 indexed) and :title will be replaced by the title of the column.

Here's a screenshot of what this report looks like (Note: this uses the Stan example ledger that I generated for my previous reporting system):

The register report that Leaky Ship links to is pretty trivial in comparison. Here's the source:

<% expect ['account', 'year'] %>
<% @query = query do %>
   select
       xtn_date as "Date",
       account as "Account",
       note as "Payee",
       amount as "Amount"
   from
       ledger
   where
       xtn_year = :year
       and account = :account
   order by
       xtn_date
<% end %>
<div class="page-header">
  <h1>Register</h1>
</div>
<%= table @query %>

The only thing new that this does is use the expect helper to ensure that account and year are query params. If they are not, expect throws an exception rather than showing bad data. Here's what this one looks like:

Both of these reports, as well as a few others, can be found in my Ledger Web configuration. My config also shows off some of the more advanced customizations you can do.

The README goes into much more detail on how the helpers work and the various config settings work. Please, install it and let me know what you think!

Share:        Y