Skip to main content

guides/livebook/reporting.livemd

# Reporting with beancount_ex and Explorer

```elixir
Mix.install([
  {:beancount_ex, "~> 0.6"},
  {:explorer, "~> 0.11"},
  {:kino, "~> 0.13"}
])

Application.put_env(:beancount_ex, :engine, Beancount.Engine.Elixir)
```

## A sample ledger

```elixir
ledger = [
  Beancount.open(~D[2026-01-01], "Assets:Bank", ["USD"]),
  Beancount.open(~D[2026-01-01], "Income:Salary", ["USD"]),
  Beancount.open(~D[2026-01-01], "Expenses:Rent", ["USD"]),
  Beancount.open(~D[2026-01-01], "Equity:Opening", ["USD"]),
  Beancount.transaction(~D[2026-01-31], "*", "Employer", "Salary", [
    Beancount.posting("Assets:Bank", Decimal.new("5000"), "USD"),
    Beancount.posting("Income:Salary", Decimal.new("-5000"), "USD")
  ]),
  Beancount.transaction(~D[2026-02-01], "*", "Landlord", "February rent", [
    Beancount.posting("Expenses:Rent", Decimal.new("1500"), "USD"),
    Beancount.posting("Assets:Bank", Decimal.new("-1500"), "USD")
  ])
]
```

## Balances as a DataFrame

`Beancount.balances/1` returns a `Beancount.Query.Result`. Explorer converts it
to a `DataFrame`, which Livebook renders as an interactive table.

```elixir
{:ok, result} = Beancount.balances(ledger)
Beancount.Explorer.to_dataframe(result)
```

## Balance sheet and income statement

```elixir
{:ok, result} = Beancount.balance_sheet(ledger)
Beancount.Explorer.to_dataframe(result)
```

```elixir
{:ok, result} = Beancount.income_statement(ledger)
Beancount.Explorer.to_dataframe(result)
```

## Holdings (investments)

```elixir
investment_ledger = [
  Beancount.open(~D[2026-01-01], "Assets:Stocks", ["AAPL"], booking: "FIFO"),
  Beancount.open(~D[2026-01-01], "Assets:Cash", ["USD"]),
  Beancount.open(~D[2026-01-01], "Equity:Opening", ["USD"]),
  Beancount.transaction(~D[2026-01-02], "*", "Broker", "Buy", [
    Beancount.posting("Assets:Stocks", Decimal.new("10"), "AAPL",
      cost: %{amount: Decimal.new("150"), currency: "USD"}
    ),
    Beancount.posting("Assets:Cash", Decimal.new("-1500"), "USD")
  ])
]

{:ok, result} = Beancount.holdings(investment_ledger)
Beancount.Explorer.to_dataframe(result)
```

## Journal for one account

```elixir
{:ok, result} = Beancount.journal(ledger, "Assets:Bank")
Beancount.Explorer.to_dataframe(result)
```

## Custom BQL query

The native engine supports only the canned report queries (balances,
balance_sheet, income_statement, holdings, journal). For arbitrary BQL, either
switch to `Engine.CLI` or use one of the canned shapes:

```elixir
# Works with Engine.Elixir (canned balances query)
{:ok, result} =
  Beancount.query(
    ledger,
    "SELECT account, sum(position) AS balance GROUP BY account ORDER BY account"
  )

Beancount.Explorer.to_dataframe(result)
```

## Kino DataTable (no Explorer)

```elixir
{:ok, result} = Beancount.balances(ledger)

result
|> Beancount.Query.Result.to_maps()
|> Kino.DataTable.new()
```